3 * Bacula(R) - The Network Backup Solution
4 * Baculum - Bacula web interface
6 * Copyright (C) 2013-2016 Kern Sibbald
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.
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.
17 * This notice must be preserved when any source code is
18 * conveyed and/or propagated.
20 * Bacula(R) is a registered trademark of Kern Sibbald.
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');
31 * Internal API client module.
35 class BaculumAPIClient extends WebModule {
38 * API client version (used in HTTP header)
41 * 0.3 - sending config as json instead of serialized array
43 const API_CLIENT_VERSION = 0.3;
46 * OAuth2 authorization endpoints
48 const OAUTH2_AUTH_URL = 'api/auth/';
49 const OAUTH2_TOKEN_URL = 'api/token/';
52 * API server version for current request.
54 public $api_server_version = 0;
57 * Single request response headers.
59 public $response_headers = array();
62 * Session params to put in URLs.
66 private $session_params = array('director');
69 * Host params to do API requests.
73 private $host_params = array();
76 * Params used to authentication.
78 private $auth_params = array();
81 * Get connection request handler.
82 * For data requests is used cURL interface.
85 * @param array $host_cfg host config parameters
86 * @return resource connection handler on success, false on errors
88 public function getConnection(array $host_cfg) {
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);
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);
104 * Get API specific headers used in HTTP requests.
107 * @return API specific headers
109 private function getAPIHeaders($host = null, $host_cfg = array()) {
111 'X-Baculum-API: ' . strval(self::API_CLIENT_VERSION),
112 'Accept: application/json'
114 if (count($host_cfg) > 0 && !is_null($host) && $host_cfg['auth_type'] === 'oauth2') {
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);
121 $this->authToHost($host, $host_cfg);
122 $auth = OAuth2Record::findByPk($host);
124 if (is_array($auth) && array_key_exists('tokens', $auth)) {
125 $headers[] = "Authorization: {$auth['tokens']['token_type']} {$auth['tokens']['access_token']}";
132 * Initializes API module (framework module constructor)
135 * @param TXmlElement $config API module configuration
137 public function init($config) {
138 $this->initSessionCache();
142 * Get URI to use by internal API client's request.
145 * @param string $host host name
146 * @param array $params GET params to send in request
147 * @return string URI to internal API server
149 private function getURIResource($host, array $params) {
151 $host_cfg = $this->getHostParams($host);
152 if (count($host_cfg) > 0) {
153 $uri = $this->getBaseURI($host_cfg);
155 // API URLs start with /api/
156 array_unshift($params, 'api');
158 // Add GET params to URI
159 $uri .= $this->prepareUrlParams($params);
161 // Add special params to URI
162 $this->addSpecialParams($uri);
164 $this->Application->getModule('logging')->log(
166 PHP_EOL . PHP_EOL . 'EXECUTE URI ==> ' . $uri . ' <==' . PHP_EOL . PHP_EOL,
167 Logging::CATEGORY_APPLICATION,
175 private function getURIAuth($host, array $params, $endpoint) {
177 $host_cfg = $this->getHostParams($host);
178 if (count($host_cfg) > 0) {
179 $uri = $this->getBaseURI($host_cfg);
182 foreach ($params as $key => $value) {
183 // add params separator
184 $uri .= (preg_match('/\?/', $uri) === 1 ? '&' : '?');
185 $params = array($key, $value);
187 $uri .= $this->prepareUrlParams($params, '=');
193 private function getBaseURI($host_cfg) {
194 $uri = sprintf('%s://%s:%d%s/',
195 $host_cfg['protocol'],
196 $host_cfg['address'],
198 $host_cfg['url_prefix']
204 * Get host specific params to put in URI.
207 * @param string $host host name
208 * @return array host parameters
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);
219 * Set/prepare host specific params.
222 * @param string $host host name
223 * @param array $params host parameters
226 public function setHostParams($host, $params) {
227 new HostRecord($host, $params);
231 * Prepare GET params to put in URI as string.
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
237 private function prepareUrlParams(array $params, $separator = '/') {
238 $params_encoded = array_map('rawurlencode', $params);
239 $params_url = implode($separator, $params_encoded);
244 * Add special params to URI.
247 * @param string &$uri reference to URI string variable
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]]);
257 $uri .= $this->prepareUrlParams($params, '=');
263 * Internal API GET request.
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'
272 public function get(array $params, $host = null, $show_error = true, $use_cache = false) {
275 if (is_null($host)) {
276 if (isset($_SESSION['api_host'])) {
277 $host = $_SESSION['api_host'];
279 $host = HostConfig::MAIN_CATALOG_HOST;
282 if ($use_cache === true) {
283 $cached = $this->getSessionCache($host, $params);
285 if (!is_null($cached)) {
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);
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);
310 * Internal API SET request.
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'
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'];
324 $host = HostConfig::MAIN_CATALOG_HOST;
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:')
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);
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);
351 * Internal API CREATE request.
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'
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'];
365 $host = HostConfig::MAIN_CATALOG_HOST;
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);
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);
388 * Internal API REMOVE request.
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'
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'];
401 $host = HostConfig::MAIN_CATALOG_HOST;
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);
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);
422 * Initially parse and prepare every Internal API response.
423 * If a error occurs then redirect to appropriate error page.
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'
432 private function preParseOutput($result, $error, $errno, $show_error = true) {
433 // first write log with that what comes
434 $this->Application->getModule('logging')->log(
437 Logging::CATEGORY_APPLICATION,
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"
451 if ($show_error === true && $resource->error != 0) {
452 $url = $this->Service->constructUrl('BaculumError', (array)$resource, false);
453 $this->getResponse()->redirect($url);
456 $this->Application->getModule('logging')->log(
459 Logging::CATEGORY_APPLICATION,
468 * Parse and set response headers.
469 * Note, header names are lower case.
473 public function parseHeader($header) {
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']);
481 $this->response_headers = $headers;
485 * Initialize session cache.
488 * @param bool $force if true then cache is force initialized
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();
498 * Get session cache value by params.
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
505 private function getSessionCache($host, array $params) {
507 $key = $this->getSessionKey($host, $params);
508 if ($this->isSessionValue($key)) {
509 $cached = $_SESSION['cache'][$key];
515 * Save data to session cache.
518 * @param string $host host name
519 * @param array $params command parameters as numeric array
520 * @param mixed $value value to save in cache
523 private function setSessionCache($host, array $params, $value) {
524 $key = $this->getSessionKey($host, $params);
525 $_SESSION['cache'][$key] = $value;
529 * Get session key by command parameters.
532 * @param string $host host name
533 * @param array $params command parameters as numeric array
534 * @return string session key for given command
536 private function getSessionKey($host, array $params) {
537 array_unshift($params, $host);
538 $key = implode(';', $params);
539 $key = base64_encode($key);
544 * Check if session key exists in session cache.
547 * @param string $key session key
548 * @return bool true if session key exists, otherwise false
550 private function isSessionValue($key) {
551 $is_value = array_key_exists($key, $_SESSION['cache']);
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);
559 'response_type' => 'code',
560 'client_id' => $host_cfg['client_id'],
561 'redirect_uri' => $host_cfg['redirect_uri'],
562 'scope' => $host_cfg['scope'],
565 $auth = new OAuth2Record();
567 $auth->state = $state;
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);
577 OAuth2Record::forceRefresh();
581 public function getTokens($auth_id, $state) {
582 $st = OAuth2Record::findBy('state', $state);
584 $host_cfg = $this->getHostParams($st['host']);
585 $uri = $this->getURIAuth($st['host'], array(), self::OAUTH2_TOKEN_URL);
586 $ch = $this->getConnection($host_cfg);
588 'grant_type' => 'authorization_code',
590 'redirect_uri' => $host_cfg['redirect_uri'],
591 'client_id' => $host_cfg['client_id'],
592 'client_secret' => $host_cfg['client_secret']
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);
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;
609 // Host config in session is no longer needed, so remove it
610 HostRecord::deleteByPk($st['host']);
615 * Get Baculum web server version.
616 * Value available after receiving response.
618 * @return float server version
620 public function getServerVersion() {
622 if (array_key_exists('baculum-api-version', $this->response_headers)) {
623 $version = floatval($this->response_headers['baculum-api-version']);