From 4d9522a6548419b399090249e121c7f8b9fe4fa9 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Mon, 28 Jul 2014 12:19:02 +0200 Subject: [PATCH] Implement a JSON-RPC based client to the Bonnie service API (#3093) --- plugins/libkolab/config.inc.php.dist | 9 +- plugins/libkolab/lib/kolab_bonnie_api.php | 82 ++++++ .../libkolab/lib/kolab_bonnie_api_client.php | 239 ++++++++++++++++++ 3 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 plugins/libkolab/lib/kolab_bonnie_api.php create mode 100644 plugins/libkolab/lib/kolab_bonnie_api_client.php diff --git a/plugins/libkolab/config.inc.php.dist b/plugins/libkolab/config.inc.php.dist index fd8ac843..b043bb72 100644 --- a/plugins/libkolab/config.inc.php.dist +++ b/plugins/libkolab/config.inc.php.dist @@ -51,4 +51,11 @@ $rcmail_config['kolab_users_id_attrib'] = null; // Use these attributes when searching users in LDAP $rcmail_config['kolab_users_search_attrib'] = array('cn','mail','alias'); - +// JSON-RPC endpoint configuration of the Bonnie web service providing historic data for groupware objects +$rcmail_config['kolab_bonnie_api'] = array( + 'uri' => 'https://:8080/api/rpc', + 'user' => 'webclient', + 'pass' => 'Welcome2KolabSystems', + 'secret' => '8431f191707fffffff00000000cccc', + 'debug' => true, // logs requests/responses to /bonnie +); diff --git a/plugins/libkolab/lib/kolab_bonnie_api.php b/plugins/libkolab/lib/kolab_bonnie_api.php new file mode 100644 index 00000000..732d29b5 --- /dev/null +++ b/plugins/libkolab/lib/kolab_bonnie_api.php @@ -0,0 +1,82 @@ + + * + * Copyright (C) 2014, Kolab Systems AG + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +class kolab_bonnie_api +{ + public $ready = false; + + private $config = array(); + private $client = null; + + + /** + * Default constructor + */ + public function __construct($config) + { + $this->config = $confg; + + $this->client = new kolab_bonnie_api_client($config['uri'], $config['timeout'] ?: 5, (bool)$config['debug']); + + $this->client->set_secret($config['secret']); + $this->client->set_authentication($config['user'], $config['pass']); + $this->client->set_request_user(rcube::get_instance()->get_user_name()); + + $this->ready = !empty($config['secret']) && !empty($config['user']) && !empty($config['pass']); + } + + /** + * Wrapper function for .changelog() API call + */ + public function changelog($type, $uid, $folder=null) + { + return $this->client->execute($type.'.changelog', array('uid' => $uid, 'folder' => $folder)); + } + + /** + * Wrapper function for .diff() API call + */ + public function diff($type, $uid, $rev, $folder=null) + { + return $this->client->execute($type.'.diff', array('uid' => $uid, 'rev' => $rev, 'folder' => $folder)); + } + + /** + * Wrapper function for .get() API call + */ + public function get($type, $uid, $rev, $folder=null) + { + return $this->client->execute($type.'.get', array('uid' => $uid, 'rev' => intval($rev), 'folder' => $folder)); + } + + /** + * Generic wrapper for direct API calls + */ + public function _execute($method, $params = array()) + { + return $this->client->execute($method, $params); + } + +} \ No newline at end of file diff --git a/plugins/libkolab/lib/kolab_bonnie_api_client.php b/plugins/libkolab/lib/kolab_bonnie_api_client.php new file mode 100644 index 00000000..bc209f41 --- /dev/null +++ b/plugins/libkolab/lib/kolab_bonnie_api_client.php @@ -0,0 +1,239 @@ + + * + * Copyright (C) 2014, Kolab Systems AG + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +class kolab_bonnie_api_client +{ + /** + * URL of the RPC endpoint + * @var string + */ + protected $url; + + /** + * HTTP client timeout in seconds + * @var integer + */ + protected $timeout; + + /** + * Debug flag + * @var bool + */ + protected $debug; + + /** + * Username for authentication + * @var string + */ + protected $username; + + /** + * Password for authentication + * @var string + */ + protected $password; + + /** + * Secret key for request signing + * @var string + */ + protected $secret; + + /** + * Default HTTP headers to send to the server + * @var array + */ + protected $headers = array( + 'Connection' => 'close', + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ); + + /** + * Constructor + * + * @param string $url Server URL + * @param integer $timeout Request timeout + * @param bool $debug Enabled debug logging + * @param array $headers Custom HTTP headers + */ + public function __construct($url, $timeout = 5, $debug = false, $headers = array()) + { + $this->url = $url; + $this->timeout = $timeout; + $this->debug = $debug; + $this->headers = array_merge($this->headers, $headers); + } + + /** + * Setter for secret key for request signing + */ + public function set_secret($secret) + { + $this->secret = $secret; + } + + /** + * Setter for the X-Request-User header + */ + public function set_request_user($username) + { + $this->headers['X-Request-User'] = $username; + } + + /** + * Set authentication parameters + * + * @param string $username Username + * @param string $password Password + */ + public function set_authentication($username, $password) + { + $this->username = $username; + $this->password = $password; + } + + /** + * Automatic mapping of procedures + * + * @param string $method Procedure name + * @param array $params Procedure arguments + * @return mixed + */ + public function __call($method, $params) + { + return $this->execute($method, $params); + } + + /** + * Execute an RPC command + * + * @param string $method Procedure name + * @param array $params Procedure arguments + * @return mixed + */ + public function execute($method, array $params = array()) + { + $id = mt_rand(); + + $payload = array( + 'jsonrpc' => '2.0', + 'method' => $method, + 'id' => $id, + ); + + if (!empty($params)) { + $payload['params'] = $params; + } + + $result = $this->send_request($payload, $method != 'system.keygen'); + + if (isset($result['id']) && $result['id'] == $id && array_key_exists('result', $result)) { + return $result['result']; + } + else if (isset($result['error'])) { + $this->_debug('ERROR', $result); + } + + return null; + } + + /** + * Do the HTTP request + * + * @param string $payload Data to send + */ + protected function send_request($payload, $sign = true) + { + try { + $payload_ = json_encode($payload); + + // add request signature + if ($sign && !empty($this->secret)) { + $this->headers['X-Request-Sign'] = $this->request_signature($payload_); + } + else if ($this->headers['X-Request-Sign']) { + unset($this->headers['X-Request-Sign']); + } + + $this->_debug('REQUEST', $payload, $this->headers); + $request = libkolab::http_request($this->url, 'POST', array('timeout' => $this->timeout)); + $request->setHeader($this->headers); + $request->setAuth($this->username, $this->password); + $request->setBody($payload_); + + $response = $request->send(); + + if ($response->getStatus() == 200) { + $result = json_decode($response->getBody(), true); + $this->_debug('RESPONSE', $result); + } + else { + throw new Exception(sprintf("HTTP %d %s", $response->getStatus(), $response->getReasonPhrase())); + } + } + catch (Exception $e) { + rcube::raise_error(array( + 'code' => 500, + 'type' => 'php', + 'message' => "Bonnie API request failed: " . $e->getMessage(), + ), true); + + return array('id' => $payload['id'], 'error' => $e->getMessage(), 'code' => -32000); + } + + return is_array($result) ? $result : array(); + } + + /** + * Compute the hmac signature for the current event payload using + * the secret key configured for this API client + * + * @param string $data The request payload data + * @return string The request signature + */ + protected function request_signature($data) + { + // TODO: get the session key with a system.keygen call + return hash_hmac('sha256', $this->headers['X-Request-User'] . ':' . $data, $this->secret); + } + + /** + * Write debug log + */ + protected function _debug(/* $message, $data1, data2, ...*/) + { + if (!$this->debug) + return; + + $args = func_get_args(); + + $msg = array(); + foreach ($args as $arg) { + $msg[] = !is_string($arg) ? var_export($arg, true) : $arg; + } + + rcube::write_log('bonnie', join(";\n", $msg)); + } + +} \ No newline at end of file