]> git.sur5r.net Git - bacula/bacula/blob - gui/baculum/protected/Web/Class/BaculumAPIClient.php
6e7ce35bb3473879c906a4473e0745375af8808d
[bacula/bacula] / gui / baculum / protected / Web / Class / BaculumAPIClient.php
1 <?php
2 /*
3  * Bacula(R) - The Network Backup Solution
4  * Baculum   - Bacula web interface
5  *
6  * Copyright (C) 2013-2016 Kern Sibbald
7  *
8  * The main author of Baculum is Marcin Haba.
9  * The original author of Bacula is Kern Sibbald, with contributions
10  * from many others, a complete list can be found in the file AUTHORS.
11  *
12  * You may use this file and others of this release according to the
13  * license defined in the LICENSE file, which includes the Affero General
14  * Public License, v3.0 ("AGPLv3") and some additional permissions and
15  * terms pursuant to its AGPLv3 Section 7.
16  *
17  * This notice must be preserved when any source code is
18  * conveyed and/or propagated.
19  *
20  * Bacula(R) is a registered trademark of Kern Sibbald.
21  */
22
23 Prado::using('Application.Common.Class.Errors');
24 Prado::using('Application.Common.Class.Miscellaneous');
25 Prado::using('Application.Web.Class.WebModule');
26 Prado::using('Application.Web.Class.HostConfig');
27 Prado::using('Application.Web.Class.OAuth2Record');
28 Prado::using('Application.Web.Class.HostRecord');
29
30 /**
31  * Internal API client module.
32  *
33  * @author Marcin Haba
34  */
35 class BaculumAPIClient extends WebModule {
36
37         /**
38          * API version (used in HTTP header)
39          */
40         const API_VERSION = '0.2';
41
42         /**
43          * OAuth2 authorization endpoints
44          */
45         const OAUTH2_AUTH_URL = 'api/auth/';
46         const OAUTH2_TOKEN_URL = 'api/token/';
47
48         /**
49          * Session params to put in URLs.
50          *
51          * @access private
52          */
53         private $session_params = array('director');
54
55         /**
56          * Host params to do API requests.
57          *
58          * @access private
59          */
60         private $host_params = array();
61
62         /**
63          * Params used to authentication.
64          */
65         private $auth_params = array();
66
67         /**
68          * Get connection request handler.
69          * For data requests is used cURL interface.
70          *
71          * @access public
72          * @param array $host_cfg host config parameters
73          * @return resource connection handler on success, false on errors
74          */
75         public function getConnection(array $host_cfg) {
76                 $ch = curl_init();
77                 if (count($host_cfg) > 0 && $host_cfg['auth_type'] === 'basic') {
78                         $userpwd = sprintf('%s:%s', $host_cfg['login'], $host_cfg['password']);
79                         curl_setopt($ch, CURLOPT_USERPWD, $userpwd);
80                 }
81                 curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
82                 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
83                 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
84                 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
85                 curl_setopt($ch, CURLOPT_COOKIE, 'PHPSESSID=' . md5(session_id()));
86                 return $ch;
87         }
88
89         /**
90          * Get API specific headers used in HTTP requests.
91          *
92          * @access private
93          * @return API specific headers
94          */
95         private function getAPIHeaders($host = null, $host_cfg = array()) {
96                 $headers = array(
97                         'X-Baculum-API: ' . self::API_VERSION,
98                         'Accept: application/json'
99                 );
100                 if (count($host_cfg) > 0 && !is_null($host) && $host_cfg['auth_type'] === 'oauth2') {
101                         $now = time();
102                         $auth = OAuth2Record::findByPk($host);
103                         if (is_null($auth) || (is_array($auth) && $now >= $auth['refresh_time'])) {
104                                 if (is_array($auth)) {
105                                         OAuth2Record::deleteByPk($host);
106                                 }
107                                 $this->authToHost($host, $host_cfg);
108                                 $auth = OAuth2Record::findByPk($host);
109                         }
110                         if (is_array($auth) && array_key_exists('tokens', $auth)) {
111                                 $headers[] = "Authorization: {$auth['tokens']['token_type']} {$auth['tokens']['access_token']}";
112                         }
113                 }
114                 return $headers;
115         }
116
117         /**
118          * Initializes API module (framework module constructor)
119          *
120          * @access public
121          * @param TXmlElement $config API module configuration
122          */
123         public function init($config) {
124                 $this->initSessionCache();
125         }
126
127         /**
128          * Get URI to use by internal API client's request.
129          *
130          * @access private
131          * @param string $host host name
132          * @param array $params GET params to send in request
133          * @return string URI to internal API server
134          */
135         private function getURIResource($host, array $params) {
136                 $uri = null;
137                 $host_cfg = $this->getHostParams($host);
138                 if (count($host_cfg) > 0) {
139                         $uri = $this->getBaseURI($host_cfg);
140
141                         // API URLs start with /api/
142                         array_unshift($params, 'api');
143
144                         // Add GET params to URI
145                         $uri .= $this->prepareUrlParams($params);
146
147                         // Add special params to URI
148                         $this->addSpecialParams($uri);
149
150                         $this->Application->getModule('logging')->log(
151                                 __FUNCTION__,
152                                 PHP_EOL . PHP_EOL . 'EXECUTE URI ==> ' . $uri . ' <==' . PHP_EOL . PHP_EOL,
153                                 Logging::CATEGORY_APPLICATION,
154                                 __FILE__,
155                                 __LINE__
156                         );
157                 }
158                 return $uri;
159         }
160
161         private function getURIAuth($host, array $params, $endpoint) {
162                 $uri = null;
163                 $host_cfg = $this->getHostParams($host);
164                 if (count($host_cfg) > 0) {
165                         $uri = $this->getBaseURI($host_cfg);
166                         // add auth endpoint
167                         $uri .= $endpoint;
168                         foreach ($params as $key => $value) {
169                                 // add params separator
170                                 $uri .= (preg_match('/\?/', $uri) === 1 ? '&' : '?');
171                                 $params = array($key, $value);
172                                 // add auth param
173                                 $uri .= $this->prepareUrlParams($params, '=');
174                         }
175                 }
176                 return $uri;
177         }
178
179         private function getBaseURI($host_cfg) {
180                 $uri = sprintf('%s://%s:%d%s/',
181                         $host_cfg['protocol'],
182                         $host_cfg['address'],
183                         $host_cfg['port'],
184                         $host_cfg['url_prefix']
185                 );
186                 return $uri;
187         }
188
189         /**
190          * Get host specific params to put in URI.
191          *
192          * @access protected
193          * @param string $host host name
194          * @return array host parameters
195          */
196         protected function getHostParams($host) {
197                 $host_params = HostRecord::findByPk($host);
198                 if (is_null($host_params)) {
199                         $host_params = $this->getModule('host_config')->getHostConfig($host);
200                 }
201                 return $host_params;
202         }
203
204         /**
205          * Set/prepare host specific params.
206          *
207          * @access public
208          * @param string $host host name
209          * @param array $params host parameters
210          * @return none
211          */
212         public function setHostParams($host, $params) {
213                 new HostRecord($host, $params);
214         }
215
216         /**
217          * Prepare GET params to put in URI as string.
218          *
219          * @param array $params GET parameters
220          * @param string $separator char used as glue to join params
221          * @return string parameters ready to put in URI
222          */
223         private function prepareUrlParams(array $params, $separator = '/') {
224                 $params_encoded = array_map('rawurlencode', $params);
225                 $params_url = implode($separator, $params_encoded);
226                 return $params_url;
227         }
228
229         /**
230          * Add special params to URI.
231          *
232          * @access private
233          * @param string &$uri reference to URI string variable
234          */
235         private function addSpecialParams(&$uri) {
236                 // add special session params
237                 for ($i = 0; $i < count($this->session_params); $i++) {
238                         if (array_key_exists($this->session_params[$i], $_SESSION)) {
239                                 // add params separator
240                                 $uri .= (preg_match('/\?/', $uri) === 1 ? '&' : '?');
241                                 $params = array($this->session_params[$i], $_SESSION[$this->session_params[$i]]);
242                                 // add session param
243                                 $uri .= $this->prepareUrlParams($params, '=');
244                         }
245                 }
246         }
247
248         /**
249          * Internal API GET request.
250          *
251          * @access public
252          * @param array $params GET params to send in request
253          * @param string $host host name to send request
254          * @param bool $show_error if true then it shows error as HTML error page
255          * @param bool $use_cache if true then try to use session cache, if false then always use fresh data
256          * @return object stdClass with request result as two properties: 'output' and 'error'
257          */
258         public function get(array $params, $host = null, $show_error = true, $use_cache = false) {
259                 $cached = null;
260                 $ret = null;
261                 if (is_null($host)) {
262                         if (isset($_SESSION['api_host'])) {
263                                 $host = $_SESSION['api_host'];
264                         } else {
265                                 $host = HostConfig::MAIN_CATALOG_HOST;
266                         }
267                 }
268                 if ($use_cache === true) {
269                         $cached = $this->getSessionCache($host, $params);
270                 }
271                 if (!is_null($cached)) {
272                         $ret = $cached;
273                 } else {
274                         $host_cfg = $this->getHostParams($host);
275                         $uri = $this->getURIResource($host, $params);
276                         $ch = $this->getConnection($host_cfg);
277                         curl_setopt($ch, CURLOPT_URL, $uri);
278                         curl_setopt($ch, CURLOPT_HTTPHEADER, $this->getAPIHeaders($host, $host_cfg));
279                         $result = curl_exec($ch);
280                         $error = curl_error($ch);
281                         $errno = curl_errno($ch);
282                         curl_close($ch);
283                         $ret = $this->preParseOutput($result, $error, $errno, $show_error);
284                         if ($use_cache === true && $ret->error === 0) {
285                                 $this->setSessionCache($host, $params, $ret);
286                         }
287                 }
288                 return $ret;
289         }
290
291         /**
292          * Internal API SET request.
293          *
294          * @access public
295          * @param array $params GET params to send in request
296          * @param array $options POST params to send in request
297          * @param string $host host name to send request
298          * @param bool $show_error if true then it shows error as HTML error page
299          * @return object stdClass with request result as two properties: 'output' and 'error'
300          */
301         public function set(array $params, array $options, $host = null, $show_error = true) {
302                 if (is_null($host)) {
303                         if (isset($_SESSION['api_host'])) {
304                                 $host = $_SESSION['api_host'];
305                         } else {
306                                 $host = HostConfig::MAIN_CATALOG_HOST;
307                         }
308                 }
309                 $host_cfg = $this->getHostParams($host);
310                 $uri = $this->getURIResource($host, $params);
311                 $ch = $this->getConnection($host_cfg);
312                 $data = http_build_query(array('update' => $options));
313                 curl_setopt($ch, CURLOPT_URL, $uri);
314                 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
315                 curl_setopt($ch, CURLOPT_HTTPHEADER, array_merge(
316                         $this->getAPIHeaders($host, $host_cfg),
317                         array('X-HTTP-Method-Override: PUT', 'Content-Length: ' . strlen($data), 'Expect:')
318                 ));
319                 curl_setopt($ch, CURLOPT_POST, true);
320                 curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
321                 $result = curl_exec($ch);
322                 $error = curl_error($ch);
323                 $errno = curl_errno($ch);
324                 curl_close($ch);
325                 return $this->preParseOutput($result, $error, $errno, $show_error);
326         }
327
328         /**
329          * Internal API CREATE request.
330          *
331          * @access public
332          * @param array $params GET params to send in request
333          * @param array $options POST params to send in request
334          * @param string $host host name to send request
335          * @param bool $show_error if true then it shows error as HTML error page
336          * @return object stdClass with request result as two properties: 'output' and 'error'
337          */
338         public function create(array $params, array $options, $host = null, $show_error = true) {
339                 if (is_null($host)) {
340                         if (isset($_SESSION['api_host'])) {
341                                 $host = $_SESSION['api_host'];
342                         } else {
343                                 $host = HostConfig::MAIN_CATALOG_HOST;
344                         }
345                 }
346                 $host_cfg = $this->getHostParams($host);
347                 $uri = $this->getURIResource($host, $params);
348                 $ch = $this->getConnection($host_cfg);
349                 $data = http_build_query(array('create' => $options));
350                 curl_setopt($ch, CURLOPT_URL, $uri);
351                 curl_setopt($ch, CURLOPT_HTTPHEADER, array_merge($this->getAPIHeaders($host, $host_cfg), array('Expect:')));
352                 curl_setopt($ch, CURLOPT_POST, true);
353                 curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
354                 $result = curl_exec($ch);
355                 $error = curl_error($ch);
356                 $errno = curl_errno($ch);
357                 curl_close($ch);
358                 return $this->preParseOutput($result, $error, $errno, $show_error);
359         }
360
361         /**
362          * Internal API REMOVE request.
363          *
364          * @access public
365          * @param array $params GET params to send in request
366          * @param string $host host name to send request
367          * @param bool $show_error if true then it shows error as HTML error page
368          * @return object stdClass with request result as two properties: 'output' and 'error'
369          */
370         public function remove(array $params, $host = null, $show_error = true) {
371                 if (is_null($host)) {
372                         if (isset($_SESSION['api_host'])) {
373                                 $host = $_SESSION['api_host'];
374                         } else {
375                                 $host = HostConfig::MAIN_CATALOG_HOST;
376                         }
377                 }
378                 $host_cfg = $this->getHostParams($host);
379                 $uri = $this->getURIResource($host, $params);
380                 $ch = $this->getConnection($host_cfg);
381                 curl_setopt($ch, CURLOPT_URL, $uri);
382                 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
383                 curl_setopt($ch, CURLOPT_HTTPHEADER, array_merge($this->getAPIHeaders($host, $host_cfg), array('X-HTTP-Method-Override: DELETE')));
384                 $result = curl_exec($ch);
385                 $error = curl_error($ch);
386                 $errno = curl_errno($ch);
387                 curl_close($ch);
388                 return $this->preParseOutput($result, $error, $errno, $show_error);
389         }
390
391         /**
392          * Initially parse and prepare every Internal API response.
393          * If a error occurs then redirect to appropriate error page.
394          *
395          * @access private
396          * @param string $result response output as JSON string (not object yet)
397          * @param string $error error message from remote host
398          * @param integer $errno error number from remote host
399          * @param bool $show_error if true then it shows error as HTML error page
400          * @return object stdClass parsed response with two top level properties 'output' and 'error'
401          */
402         private function preParseOutput($result, $error, $errno, $show_error = true) {
403                 // first write log with that what comes
404                 $this->Application->getModule('logging')->log(
405                         __FUNCTION__,
406                         $result,
407                         Logging::CATEGORY_APPLICATION,
408                         __FILE__,
409                         __LINE__
410                 );
411
412                 // decode JSON to object
413                 $resource = json_decode($result);
414                 if (is_null($resource)) {
415                         $resource = (object) array(
416                                 'error' => ConnectionError::ERROR_CONNECTION_TO_HOST_PROBLEM,
417                                 'output' => ConnectionError::MSG_ERROR_CONNECTION_TO_HOST_PROBLEM . " cURL error $errno: $error"
418                         );
419                 }
420
421                 if ($show_error === true && $resource->error != 0) {
422                         $url = $this->Service->constructUrl('BaculumError', (array)$resource, false);
423                         $this->getResponse()->redirect($url);
424                 }
425
426                 $this->Application->getModule('logging')->log(
427                         __FUNCTION__,
428                         $resource,
429                         Logging::CATEGORY_APPLICATION,
430                         __FILE__,
431                         __LINE__
432                 );
433
434                 return $resource;
435         }
436
437         /**
438          * Initialize session cache.
439          *
440          * @access public
441          * @param bool $force if true then cache is force initialized
442          * @return none
443          */
444         public function initSessionCache($force = false) {
445                 if (!isset($_SESSION) || !array_key_exists('cache', $_SESSION) || !is_array($_SESSION['cache']) || $force === true) {
446                         $_SESSION['cache'] = array();
447                 }
448         }
449
450         /**
451          * Get session cache value by params.
452          *
453          * @access private
454          * @param string $host host name
455          * @param array $params command parameters as numeric array
456          * @return mixed if cache exists then returned is cached data, otherwise null
457          */
458         private function getSessionCache($host, array $params) {
459                 $cached = null;
460                 $key = $this->getSessionKey($host, $params);
461                 if ($this->isSessionValue($key)) {
462                         $cached = $_SESSION['cache'][$key];
463                 }
464                 return $cached;
465         }
466
467         /**
468          * Save data to session cache.
469          *
470          * @access private
471          * @param string $host host name
472          * @param array $params command parameters as numeric array
473          * @param mixed $value value to save in cache
474          * @return none
475          */
476         private function setSessionCache($host, array $params, $value) {
477                 $key = $this->getSessionKey($host, $params);
478                 $_SESSION['cache'][$key] = $value;
479         }
480
481         /**
482          * Get session key by command parameters.
483          *
484          * @access private
485          * @param string $host host name
486          * @param array $params command parameters as numeric array
487          * @return string session key for given command
488          */
489         private function getSessionKey($host, array $params) {
490                 array_unshift($params, $host);
491                 $key = implode(';', $params);
492                 $key = base64_encode($key);
493                 return $key;
494         }
495
496         /**
497          * Check if session key exists in session cache.
498          *
499          * @access private
500          * @param string $key session key
501          * @return bool true if session key exists, otherwise false
502          */
503         private function isSessionValue($key) {
504                 $is_value = array_key_exists($key, $_SESSION['cache']);
505                 return $is_value;
506         }
507
508         private function authToHost($host, $host_cfg) {
509                 if (count($host_cfg) > 0 && $host_cfg['auth_type'] === 'oauth2') {
510                         $state = $this->getModule('misc')->getRandomString(16);
511                         $params = array(
512                                 'response_type' => 'code',
513                                 'client_id' => $host_cfg['client_id'],
514                                 'redirect_uri' => $host_cfg['redirect_uri'],
515                                 'scope' => $host_cfg['scope'],
516                                 'state' => $state
517                         );
518                         $auth = new OAuth2Record();
519                         $auth->host = $host;
520                         $auth->state = $state;
521                         $auth->save();
522                         $uri = $this->getURIAuth($host, $params, self::OAUTH2_AUTH_URL);
523                         $ch = $this->getConnection($host_cfg);
524                         curl_setopt($ch, CURLOPT_URL, $uri);
525                         curl_setopt($ch, CURLOPT_HTTPHEADER, $this->getAPIHeaders());
526                         curl_setopt($ch, CURLINFO_HEADER_OUT, true);
527                         curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
528                         $result = curl_exec($ch);
529                         curl_close($ch);
530                         OAuth2Record::forceRefresh();
531                 }
532         }
533
534         public function getTokens($auth_id, $state) {
535                 $st = OAuth2Record::findBy('state', $state);
536                 if (is_array($st)) {
537                         $host_cfg = $this->getHostParams($st['host']);
538                         $uri = $this->getURIAuth($st['host'], array(), self::OAUTH2_TOKEN_URL);
539                         $ch = $this->getConnection($host_cfg);
540                         $data = array(
541                                 'grant_type' => 'authorization_code',
542                                 'code' => $auth_id,
543                                 'redirect_uri' => $host_cfg['redirect_uri'],
544                                 'client_id' => $host_cfg['client_id'],
545                                 'client_secret' => $host_cfg['client_secret']
546                         );
547                         curl_setopt($ch, CURLOPT_URL, $uri);
548                         curl_setopt($ch, CURLOPT_HTTPHEADER, array_merge($this->getAPIHeaders(), array('Expect:')));
549                         curl_setopt($ch, CURLOPT_POST, true);
550                         curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
551                         $result = curl_exec($ch);
552                         curl_close($ch);
553                         $tokens = json_decode($result);
554                         if (is_object($tokens) && isset($tokens->access_token) && isset($tokens->refresh_token)) {
555                                 $auth = new OAuth2Record();
556                                 $auth->host = $st['host'];
557                                 $auth->tokens = (array)$tokens;
558                                 // refresh token 5 seconds before average expires time
559                                 $auth->refresh_time = time() + $tokens->expires_in - 5;
560                                 $auth->save();
561                         }
562                         // Host config in session is no longer needed, so remove it
563                         HostRecord::deleteByPk($st['host']);
564                 }
565         }
566 }
567 ?>