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