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 version (used in HTTP header)
40 const API_VERSION = '0.2';
43 * OAuth2 authorization endpoints
45 const OAUTH2_AUTH_URL = 'api/auth/';
46 const OAUTH2_TOKEN_URL = 'api/token/';
49 * Session params to put in URLs.
53 private $session_params = array('director');
56 * Host params to do API requests.
60 private $host_params = array();
63 * Params used to authentication.
65 private $auth_params = array();
68 * Get connection request handler.
69 * For data requests is used cURL interface.
72 * @param array $host_cfg host config parameters
73 * @return resource connection handler on success, false on errors
75 public function getConnection(array $host_cfg) {
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);
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()));
90 * Get API specific headers used in HTTP requests.
93 * @return API specific headers
95 private function getAPIHeaders($host = null, $host_cfg = array()) {
97 'X-Baculum-API: ' . self::API_VERSION,
98 'Accept: application/json'
100 if (count($host_cfg) > 0 && !is_null($host) && $host_cfg['auth_type'] === 'oauth2') {
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);
107 $this->authToHost($host, $host_cfg);
108 $auth = OAuth2Record::findByPk($host);
110 if (is_array($auth) && array_key_exists('tokens', $auth)) {
111 $headers[] = "Authorization: {$auth['tokens']['token_type']} {$auth['tokens']['access_token']}";
118 * Initializes API module (framework module constructor)
121 * @param TXmlElement $config API module configuration
123 public function init($config) {
124 $this->initSessionCache();
128 * Get URI to use by internal API client's request.
131 * @param string $host host name
132 * @param array $params GET params to send in request
133 * @return string URI to internal API server
135 private function getURIResource($host, array $params) {
137 $host_cfg = $this->getHostParams($host);
138 if (count($host_cfg) > 0) {
139 $uri = $this->getBaseURI($host_cfg);
141 // API URLs start with /api/
142 array_unshift($params, 'api');
144 // Add GET params to URI
145 $uri .= $this->prepareUrlParams($params);
147 // Add special params to URI
148 $this->addSpecialParams($uri);
150 $this->Application->getModule('logging')->log(
152 PHP_EOL . PHP_EOL . 'EXECUTE URI ==> ' . $uri . ' <==' . PHP_EOL . PHP_EOL,
153 Logging::CATEGORY_APPLICATION,
161 private function getURIAuth($host, array $params, $endpoint) {
163 $host_cfg = $this->getHostParams($host);
164 if (count($host_cfg) > 0) {
165 $uri = $this->getBaseURI($host_cfg);
168 foreach ($params as $key => $value) {
169 // add params separator
170 $uri .= (preg_match('/\?/', $uri) === 1 ? '&' : '?');
171 $params = array($key, $value);
173 $uri .= $this->prepareUrlParams($params, '=');
179 private function getBaseURI($host_cfg) {
180 $uri = sprintf('%s://%s:%d%s/',
181 $host_cfg['protocol'],
182 $host_cfg['address'],
184 $host_cfg['url_prefix']
190 * Get host specific params to put in URI.
193 * @param string $host host name
194 * @return array host parameters
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);
205 * Set/prepare host specific params.
208 * @param string $host host name
209 * @param array $params host parameters
212 public function setHostParams($host, $params) {
213 new HostRecord($host, $params);
217 * Prepare GET params to put in URI as string.
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
223 private function prepareUrlParams(array $params, $separator = '/') {
224 $params_encoded = array_map('rawurlencode', $params);
225 $params_url = implode($separator, $params_encoded);
230 * Add special params to URI.
233 * @param string &$uri reference to URI string variable
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]]);
243 $uri .= $this->prepareUrlParams($params, '=');
249 * Internal API GET request.
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'
258 public function get(array $params, $host = null, $show_error = true, $use_cache = false) {
261 if (is_null($host)) {
262 if (isset($_SESSION['api_host'])) {
263 $host = $_SESSION['api_host'];
265 $host = HostConfig::MAIN_CATALOG_HOST;
268 if ($use_cache === true) {
269 $cached = $this->getSessionCache($host, $params);
271 if (!is_null($cached)) {
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);
283 $ret = $this->preParseOutput($result, $error, $errno, $show_error);
284 if ($use_cache === true && $ret->error === 0) {
285 $this->setSessionCache($host, $params, $ret);
292 * Internal API SET request.
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'
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'];
306 $host = HostConfig::MAIN_CATALOG_HOST;
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:')
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);
325 return $this->preParseOutput($result, $error, $errno, $show_error);
329 * Internal API CREATE request.
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'
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'];
343 $host = HostConfig::MAIN_CATALOG_HOST;
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);
358 return $this->preParseOutput($result, $error, $errno, $show_error);
362 * Internal API REMOVE request.
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'
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'];
375 $host = HostConfig::MAIN_CATALOG_HOST;
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);
388 return $this->preParseOutput($result, $error, $errno, $show_error);
392 * Initially parse and prepare every Internal API response.
393 * If a error occurs then redirect to appropriate error page.
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'
402 private function preParseOutput($result, $error, $errno, $show_error = true) {
403 // first write log with that what comes
404 $this->Application->getModule('logging')->log(
407 Logging::CATEGORY_APPLICATION,
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"
421 if ($show_error === true && $resource->error != 0) {
422 $url = $this->Service->constructUrl('BaculumError', (array)$resource, false);
423 $this->getResponse()->redirect($url);
426 $this->Application->getModule('logging')->log(
429 Logging::CATEGORY_APPLICATION,
438 * Initialize session cache.
441 * @param bool $force if true then cache is force initialized
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();
451 * Get session cache value by params.
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
458 private function getSessionCache($host, array $params) {
460 $key = $this->getSessionKey($host, $params);
461 if ($this->isSessionValue($key)) {
462 $cached = $_SESSION['cache'][$key];
468 * Save data to session cache.
471 * @param string $host host name
472 * @param array $params command parameters as numeric array
473 * @param mixed $value value to save in cache
476 private function setSessionCache($host, array $params, $value) {
477 $key = $this->getSessionKey($host, $params);
478 $_SESSION['cache'][$key] = $value;
482 * Get session key by command parameters.
485 * @param string $host host name
486 * @param array $params command parameters as numeric array
487 * @return string session key for given command
489 private function getSessionKey($host, array $params) {
490 array_unshift($params, $host);
491 $key = implode(';', $params);
492 $key = base64_encode($key);
497 * Check if session key exists in session cache.
500 * @param string $key session key
501 * @return bool true if session key exists, otherwise false
503 private function isSessionValue($key) {
504 $is_value = array_key_exists($key, $_SESSION['cache']);
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);
512 'response_type' => 'code',
513 'client_id' => $host_cfg['client_id'],
514 'redirect_uri' => $host_cfg['redirect_uri'],
515 'scope' => $host_cfg['scope'],
518 $auth = new OAuth2Record();
520 $auth->state = $state;
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);
530 OAuth2Record::forceRefresh();
534 public function getTokens($auth_id, $state) {
535 $st = OAuth2Record::findBy('state', $state);
537 $host_cfg = $this->getHostParams($st['host']);
538 $uri = $this->getURIAuth($st['host'], array(), self::OAUTH2_TOKEN_URL);
539 $ch = $this->getConnection($host_cfg);
541 'grant_type' => 'authorization_code',
543 'redirect_uri' => $host_cfg['redirect_uri'],
544 'client_id' => $host_cfg['client_id'],
545 'client_secret' => $host_cfg['client_secret']
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);
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;
562 // Host config in session is no longer needed, so remove it
563 HostRecord::deleteByPk($st['host']);