diff options
author | Björn Schießle <bjoern@schiessle.org> | 2015-11-10 10:50:59 +0100 |
---|---|---|
committer | Björn Schießle <bjoern@schiessle.org> | 2015-11-19 18:06:51 +0100 |
commit | 698100d279fdd8c8bc086e729833705d1a31c018 (patch) | |
tree | 3e38110373da82a5a89dbbe1a84708b6208b7d51 /apps/federation | |
parent | ed039ee5ebdba6778b245f249fe206d2423a6a36 (diff) | |
download | nextcloud-server-698100d279fdd8c8bc086e729833705d1a31c018.tar.gz nextcloud-server-698100d279fdd8c8bc086e729833705d1a31c018.zip |
exchange shared secret
Diffstat (limited to 'apps/federation')
-rw-r--r-- | apps/federation/api/ocsauthapi.php | 136 | ||||
-rw-r--r-- | apps/federation/appinfo/application.php | 56 | ||||
-rw-r--r-- | apps/federation/appinfo/database.xml | 22 | ||||
-rw-r--r-- | apps/federation/appinfo/routes.php | 35 | ||||
-rw-r--r-- | apps/federation/backgroundjob/getsharedsecret.php | 155 | ||||
-rw-r--r-- | apps/federation/backgroundjob/requestsharedsecret.php | 134 | ||||
-rw-r--r-- | apps/federation/lib/dbhandler.php | 182 | ||||
-rw-r--r-- | apps/federation/lib/trustedservers.php | 105 | ||||
-rw-r--r-- | apps/federation/settings/settings-admin.php | 4 | ||||
-rw-r--r-- | apps/federation/tests/api/ocsauthapitest.php | 184 | ||||
-rw-r--r-- | apps/federation/tests/lib/dbhandlertest.php | 45 | ||||
-rw-r--r-- | apps/federation/tests/lib/trustedserverstest.php | 113 |
12 files changed, 1052 insertions, 119 deletions
diff --git a/apps/federation/api/ocsauthapi.php b/apps/federation/api/ocsauthapi.php new file mode 100644 index 00000000000..7965e6f2140 --- /dev/null +++ b/apps/federation/api/ocsauthapi.php @@ -0,0 +1,136 @@ +<?php +/** + * @author Björn Schießle <schiessle@owncloud.com> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + + +namespace OCA\Federation\API; + +use OC\BackgroundJob\JobList; +use OCA\Federation\DbHandler; +use OCA\Federation\TrustedServers; +use OCP\AppFramework\Http; +use OCP\IRequest; +use OCP\Security\ISecureRandom; +use OCP\Security\StringUtils; + +/** + * Class OCSAuthAPI + * + * OCS API end-points to exchange shared secret between two connected ownClouds + * + * @package OCA\Federation\API + */ +class OCSAuthAPI { + + /** @var IRequest */ + private $request; + + /** @var ISecureRandom */ + private $secureRandom; + + /** @var JobList */ + private $jobList; + + /** @var TrustedServers */ + private $trustedServers; + + /** @var DbHandler */ + private $dbHandler; + + /** + * AuthController constructor. + * + * @param IRequest $request + * @param ISecureRandom $secureRandom + * @param JobList $jobList + * @param TrustedServers $trustedServers + * @param DbHandler $dbHandler + */ + public function __construct( + IRequest $request, + ISecureRandom $secureRandom, + JobList $jobList, + TrustedServers $trustedServers, + DbHandler $dbHandler + ) { + $this->request = $request; + $this->secureRandom = $secureRandom; + $this->jobList = $jobList; + $this->trustedServers = $trustedServers; + $this->dbHandler = $dbHandler; + } + + /** + * request received to ask remote server for a shared secret + * + * @return \OC_OCS_Result + */ + public function requestSharedSecret() { + + $url = $this->request->getParam('url'); + $token = $this->request->getParam('token'); + + if ($this->trustedServers->isTrustedServer($url) === false) { + return new \OC_OCS_Result(null, HTTP::STATUS_FORBIDDEN); + } + + $this->jobList->add( + 'OCA\Federation\BackgroundJob\GetSharedSecret', + [ + 'url' => $url, + 'token' => $token, + ] + ); + + return new \OC_OCS_Result(null, Http::STATUS_OK); + + } + + /** + * create shared secret and return it + * + * @return \OC_OCS_Result + */ + public function getSharedSecret() { + + $url = $this->request->getParam('url'); + $token = $this->request->getParam('token'); + + if ( + $this->trustedServers->isTrustedServer($url) === false + || $this->isValidToken($url, $token) === false + ) { + return new \OC_OCS_Result(null, HTTP::STATUS_FORBIDDEN); + } + + $sharedSecret = $this->secureRandom->getMediumStrengthGenerator()->generate(32); + + $this->trustedServers->addSharedSecret($url, $sharedSecret); + + return new \OC_OCS_Result(['sharedSecret' => $sharedSecret], Http::STATUS_OK); + + } + + protected function isValidToken($url, $token) { + $storedToken = $this->dbHandler->getToken($url); + return StringUtils::equals($storedToken, $token); + } + +} diff --git a/apps/federation/appinfo/application.php b/apps/federation/appinfo/application.php index 46a791c25d0..e91506a30bd 100644 --- a/apps/federation/appinfo/application.php +++ b/apps/federation/appinfo/application.php @@ -21,13 +21,15 @@ namespace OCA\Federation\AppInfo; +use OCA\Federation\API\OCSAuthAPI; +use OCA\Federation\Controller\AuthController; use OCA\Federation\Controller\SettingsController; use OCA\Federation\DbHandler; use OCA\Federation\Middleware\AddServerMiddleware; use OCA\Federation\TrustedServers; +use OCP\API; use OCP\App; use OCP\AppFramework\IAppContainer; -use OCP\IAppConfig; class Application extends \OCP\AppFramework\App { @@ -38,7 +40,6 @@ class Application extends \OCP\AppFramework\App { parent::__construct('federation', $urlParams); $this->registerService(); $this->registerMiddleware(); - } /** @@ -70,7 +71,9 @@ class Application extends \OCP\AppFramework\App { return new TrustedServers( $c->query('DbHandler'), \OC::$server->getHTTPClientService(), - \OC::$server->getLogger() + \OC::$server->getLogger(), + \OC::$server->getJobList(), + \OC::$server->getSecureRandom() ); }); @@ -83,10 +86,57 @@ class Application extends \OCP\AppFramework\App { $c->query('TrustedServers') ); }); + + + $container->registerService('AuthController', function (IAppContainer $c) { + $server = $c->getServer(); + return new AuthController( + $c->getAppName(), + $server->getRequest(), + $server->getSecureRandom(), + $server->getJobList(), + $c->query('TrustedServers'), + $c->query('DbHandler') + ); + }); } private function registerMiddleware() { $container = $this->getContainer(); $container->registerMiddleware('addServerMiddleware'); } + + /** + * register OCS API Calls + */ + public function registerOCSApi() { + + $container = $this->getContainer(); + $server = $container->getServer(); + + $auth = new OCSAuthAPI( + $server->getRequest(), + $server->getSecureRandom(), + $server->getJobList(), + $container->query('TrustedServers'), + $container->query('DbHandler') + + ); + + API::register('get', + '/apps/federation/api/v1/shared-secret', + array($auth, 'getSharedSecret'), + 'federation', + API::GUEST_AUTH + ); + + API::register('post', + '/apps/federation/api/v1/request-shared-secret', + array($auth, 'requestSharedSecret'), + 'federation', + API::GUEST_AUTH + ); + + } + } diff --git a/apps/federation/appinfo/database.xml b/apps/federation/appinfo/database.xml index da16212ca19..e0bb241918e 100644 --- a/apps/federation/appinfo/database.xml +++ b/apps/federation/appinfo/database.xml @@ -28,7 +28,27 @@ <default></default> <notnull>true</notnull> <length>32</length> - <comments>md5 hash of the url</comments> + <comments>md5 hash of the url without the protocol</comments> + </field> + <field> + <name>token</name> + <type>text</type> + <length>128</length> + <comments>toke used to exchange the shared secret</comments> + </field> + <field> + <name>shared_secret</name> + <type>text</type> + <length>256</length> + <comments>shared secret used to authenticate</comments> + </field> + <field> + <name>status</name> + <type>integer</type> + <length>4</length> + <notnull>true</notnull> + <default>2</default> + <comments>current status of the connection</comments> </field> <index> <name>url_hash</name> diff --git a/apps/federation/appinfo/routes.php b/apps/federation/appinfo/routes.php index 43ccc4ed504..f45db43e4e7 100644 --- a/apps/federation/appinfo/routes.php +++ b/apps/federation/appinfo/routes.php @@ -19,17 +19,24 @@ * */ -return [ - 'routes' => [ - [ - 'name' => 'Settings#addServer', - 'url' => '/trusted-servers', - 'verb' => 'POST' - ], - [ - 'name' => 'Settings#removeServer', - 'url' => '/trusted-servers/{id}', - 'verb' => 'DELETE' - ], - ] -]; +$application = new \OCA\Federation\AppInfo\Application(); + +$application->registerRoutes( + $this, + [ + 'routes' => [ + [ + 'name' => 'Settings#addServer', + 'url' => '/trusted-servers', + 'verb' => 'POST' + ], + [ + 'name' => 'Settings#removeServer', + 'url' => '/trusted-servers/{id}', + 'verb' => 'DELETE' + ], + ] + ] +); + +$application->registerOCSApi(); diff --git a/apps/federation/backgroundjob/getsharedsecret.php b/apps/federation/backgroundjob/getsharedsecret.php new file mode 100644 index 00000000000..665c6ec6cce --- /dev/null +++ b/apps/federation/backgroundjob/getsharedsecret.php @@ -0,0 +1,155 @@ +<?php +/** + * @author Björn Schießle <schiessle@owncloud.com> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + + +namespace OCA\Federation\BackgroundJob; + +use OC\BackgroundJob\QueuedJob; +use OCA\Federation\DbHandler; +use OCA\Federation\TrustedServers; +use OCP\AppFramework\Http; +use OCP\BackgroundJob\IJobList; +use OCP\Http\Client\IClient; +use OCP\ILogger; +use OCP\IURLGenerator; + +/** + * Class GetSharedSecret + * + * request shared secret from remote ownCloud + * + * @package OCA\Federation\Backgroundjob + */ +class GetSharedSecret extends QueuedJob{ + + /** @var IClient */ + private $httpClient; + + /** @var IJobList */ + private $jobList; + + /** @var IURLGenerator */ + private $urlGenerator; + + /** @var TrustedServers */ + private $trustedServers; + + /** @var ILogger */ + private $logger; + + private $endPoint = '/ocs/v2.php/apps/federation/api/v1/shared-secret?format=json'; + + /** + * RequestSharedSecret constructor. + * + * @param IClient $httpClient + * @param IURLGenerator $urlGenerator + * @param IJobList $jobList + * @param TrustedServers $trustedServers + * @param ILogger $logger + */ + public function __construct( + IClient $httpClient = null, + IURLGenerator $urlGenerator = null, + IJobList $jobList = null, + TrustedServers $trustedServers = null, + ILogger $logger = null + ) { + $this->logger = $logger ? $logger : \OC::$server->getLogger(); + $this->httpClient = $httpClient ? $httpClient : \OC::$server->getHTTPClientService()->newClient(); + $this->jobList = $jobList ? $jobList : \OC::$server->getJobList(); + $this->urlGenerator = $urlGenerator ? $urlGenerator : \OC::$server->getURLGenerator(); + if ($trustedServers) { + $this->trustedServers = $trustedServers; + } else { + $this->trustedServers = new TrustedServers( + new DbHandler(\OC::$server->getDatabaseConnection(), \OC::$server->getL10N('federation')), + \OC::$server->getHTTPClientService(), + \OC::$server->getLogger(), + $this->jobList, + \OC::$server->getSecureRandom() + ); + } + } + + /** + * run the job, then remove it from the joblist + * + * @param JobList $jobList + * @param ILogger $logger + */ + public function execute($jobList, ILogger $logger = null) { + $jobList->remove($this, $this->argument); + $target = $this->argument['url']; + // only execute if target is still in the list of trusted domains + if ($this->trustedServers->isTrustedServer($target)) { + parent::execute($jobList, $logger); + } + } + + protected function run($argument) { + $target = $argument['url']; + $source = $this->urlGenerator->getAbsoluteURL('/'); + $source = rtrim($source, '/'); + $token = $argument['token']; + + $result = $this->httpClient->get( + $target . $this->endPoint, + [ + 'query' => + [ + 'url' => $source, + 'token' => $token + ], + 'timeout' => 3, + 'connect_timeout' => 3, + ] + ); + + $status = $result->getStatusCode(); + + // if we received a unexpected response we try again later + if ( + $status !== Http::STATUS_OK + && $status !== Http::STATUS_FORBIDDEN + ) { + $this->jobList->add( + 'OCA\Federation\Backgroundjob\RequestSharedSecret', + $argument + ); + } elseif ($status === Http::STATUS_OK) { + $body = $result->getBody(); + $result = json_decode($body, true); + if (isset($result['ocs']['data']['sharedSecret'])) { + $this->trustedServers->addSharedSecret( + $target, + $result['ocs']['data']['sharedSecret'] + ); + } else { + $this->logger->error( + 'remote server "' . $target . '"" does not return a valid shared secret', + ['app' => 'federation'] + ); + $this->trustedServers->setServerStatus($target, TrustedServers::STATUS_FAILURE); + } + } + } +} diff --git a/apps/federation/backgroundjob/requestsharedsecret.php b/apps/federation/backgroundjob/requestsharedsecret.php new file mode 100644 index 00000000000..b61026a4d66 --- /dev/null +++ b/apps/federation/backgroundjob/requestsharedsecret.php @@ -0,0 +1,134 @@ +<?php +/** + * @author Björn Schießle <schiessle@owncloud.com> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + + +namespace OCA\Federation\BackgroundJob; + + +use OC\BackgroundJob\QueuedJob; +use OCA\Federation\DbHandler; +use OCA\Federation\TrustedServers; +use OCP\AppFramework\Http; +use OCP\BackgroundJob\IJobList; +use OCP\Http\Client\IClient; +use OCP\ILogger; +use OCP\IURLGenerator; + +/** + * Class RequestSharedSecret + * + * Ask remote ownCloud to request a sharedSecret from this server + * + * @package OCA\Federation\Backgroundjob + */ +class RequestSharedSecret extends QueuedJob { + + /** @var IClient */ + private $httpClient; + + /** @var IJobList */ + private $jobList; + + /** @var IURLGenerator */ + private $urlGenerator; + + private $endPoint = '/ocs/v2.php/apps/federation/api/v1/request-shared-secret?format=json'; + + /** + * RequestSharedSecret constructor. + * + * @param IClient $httpClient + * @param IURLGenerator $urlGenerator + * @param IJobList $jobList + * @param TrustedServers $trustedServers + */ + public function __construct( + IClient $httpClient = null, + IURLGenerator $urlGenerator = null, + IJobList $jobList = null, + TrustedServers $trustedServers = null + ) { + $this->httpClient = $httpClient ? $httpClient : \OC::$server->getHTTPClientService()->newClient(); + $this->jobList = $jobList ? $jobList : \OC::$server->getJobList(); + $this->urlGenerator = $urlGenerator ? $urlGenerator : \OC::$server->getURLGenerator(); + if ($trustedServers) { + $this->trustedServers = $trustedServers; + } else { + $this->trustedServers = new TrustedServers( + new DbHandler(\OC::$server->getDatabaseConnection(), \OC::$server->getL10N('federation')), + \OC::$server->getHTTPClientService(), + \OC::$server->getLogger(), + $this->jobList, + \OC::$server->getSecureRandom() + ); + } + } + + + /** + * run the job, then remove it from the joblist + * + * @param JobList $jobList + * @param ILogger $logger + */ + public function execute($jobList, ILogger $logger = null) { + $jobList->remove($this, $this->argument); + $target = $this->argument['url']; + // only execute if target is still in the list of trusted domains + if ($this->trustedServers->isTrustedServer($target)) { + parent::execute($jobList, $logger); + } + } + + protected function run($argument) { + + $target = $argument['url']; + $source = $this->urlGenerator->getAbsoluteURL('/'); + $source = rtrim($source, '/'); + $token = $argument['token']; + + $result = $this->httpClient->post( + $target . $this->endPoint, + [ + 'body' => [ + 'url' => $source, + 'token' => $token, + ], + 'timeout' => 3, + 'connect_timeout' => 3, + ] + ); + + $status = $result->getStatusCode(); + + // if we received a unexpected response we try again later + if ( + $status !== Http::STATUS_OK + && $status !== Http::STATUS_FORBIDDEN + ) { + $this->jobList->add( + 'OCA\Federation\BackgroundJob\RequestSharedSecret', + $argument + ); + } + + } +} diff --git a/apps/federation/lib/dbhandler.php b/apps/federation/lib/dbhandler.php index 1100875cc23..58cf0f7f3b9 100644 --- a/apps/federation/lib/dbhandler.php +++ b/apps/federation/lib/dbhandler.php @@ -23,10 +23,19 @@ namespace OCA\Federation; +use OC\Files\Filesystem; use OC\HintException; use OCP\IDBConnection; use OCP\IL10N; +/** + * Class DbHandler + * + * handles all database calls for the federation app + * + * @group DB + * @package OCA\Federation + */ class DbHandler { /** @var IDBConnection */ @@ -53,12 +62,12 @@ class DbHandler { /** * add server to the list of trusted ownCloud servers * - * @param $url + * @param string $url * @return int * @throws HintException */ - public function add($url) { - $hash = md5($url); + public function addServer($url) { + $hash = $this->hash($url); $query = $this->connection->getQueryBuilder(); $query->insert($this->dbTable) ->values( @@ -73,14 +82,7 @@ class DbHandler { $result = $query->execute(); if ($result) { - $id = $this->connection->lastInsertId(); - // Fallback, if lastInterId() doesn't work we need to perform a select - // to get the ID (seems to happen sometimes on Oracle) - if (!$id) { - $server = $this->get($url); - $id = $server['id']; - } - return $id; + return $this->connection->lastInsertId($this->dbTable); } else { $message = 'Internal failure, Could not add ownCloud as trusted server: ' . $url; $message_t = $this->l->t('Could not add server'); @@ -93,7 +95,7 @@ class DbHandler { * * @param int $id */ - public function remove($id) { + public function removeServer($id) { $query = $this->connection->getQueryBuilder(); $query->delete($this->dbTable) ->where($query->expr()->eq('id', $query->createParameter('id'))) @@ -102,26 +104,11 @@ class DbHandler { } /** - * get trusted server from database - * - * @param $url - * @return mixed - */ - public function get($url) { - $query = $this->connection->getQueryBuilder(); - $query->select('url', 'id')->from($this->dbTable) - ->where($query->expr()->eq('url_hash', $query->createParameter('url_hash'))) - ->setParameter('url_hash', md5($url)); - - return $query->execute()->fetch(); - } - - /** * get all trusted servers * * @return array */ - public function getAll() { + public function getAllServer() { $query = $this->connection->getQueryBuilder(); $query->select('url', 'id')->from($this->dbTable); $result = $query->execute()->fetchAll(); @@ -134,14 +121,149 @@ class DbHandler { * @param string $url * @return bool */ - public function exists($url) { + public function serverExists($url) { + $hash = $this->hash($url); $query = $this->connection->getQueryBuilder(); $query->select('url')->from($this->dbTable) ->where($query->expr()->eq('url_hash', $query->createParameter('url_hash'))) - ->setParameter('url_hash', md5($url)); + ->setParameter('url_hash', $hash); $result = $query->execute()->fetchAll(); return !empty($result); } + /** + * write token to database. Token is used to exchange the secret + * + * @param string $url + * @param string $token + */ + public function addToken($url, $token) { + $hash = $this->hash($url); + $query = $this->connection->getQueryBuilder(); + $query->update($this->dbTable) + ->set('token', $query->createParameter('token')) + ->where($query->expr()->eq('url_hash', $query->createParameter('url_hash'))) + ->setParameter('url_hash', $hash) + ->setParameter('token', $token); + $query->execute(); + } + + /** + * get token stored in database + * + * @param string $url + * @return string + */ + public function getToken($url) { + $hash = $this->hash($url); + $query = $this->connection->getQueryBuilder(); + $query->select('token')->from($this->dbTable) + ->where($query->expr()->eq('url_hash', $query->createParameter('url_hash'))) + ->setParameter('url_hash', $hash); + + $result = $query->execute()->fetch(); + return $result['token']; + } + + /** + * add shared Secret to database + * + * @param string $url + * @param string $sharedSecret + */ + public function addSharedSecret($url, $sharedSecret) { + $hash = $this->hash($url); + $query = $this->connection->getQueryBuilder(); + $query->update($this->dbTable) + ->set('shared_secret', $query->createParameter('sharedSecret')) + ->where($query->expr()->eq('url_hash', $query->createParameter('url_hash'))) + ->setParameter('url_hash', $hash) + ->setParameter('sharedSecret', $sharedSecret); + $query->execute(); + } + + /** + * get shared secret from database + * + * @param string $url + * @return string + */ + public function getSharedSecret($url) { + $hash = $this->hash($url); + $query = $this->connection->getQueryBuilder(); + $query->select('shared_secret')->from($this->dbTable) + ->where($query->expr()->eq('url_hash', $query->createParameter('url_hash'))) + ->setParameter('url_hash', $hash); + + $result = $query->execute()->fetch(); + return $result['shared_secret']; + } + + /** + * set server status + * + * @param string $url + * @param int $status + */ + public function setServerStatus($url, $status) { + $hash = $this->hash($url); + $query = $this->connection->getQueryBuilder(); + $query->update($this->dbTable) + ->set('status', $query->createParameter('status')) + ->where($query->expr()->eq('url_hash', $query->createParameter('url_hash'))) + ->setParameter('url_hash', $hash) + ->setParameter('status', $status); + $query->execute(); + } + + /** + * get server status + * + * @param string $url + * @return int + */ + public function getServerStatus($url) { + $hash = $this->hash($url); + $query = $this->connection->getQueryBuilder(); + $query->select('status')->from($this->dbTable) + ->where($query->expr()->eq('url_hash', $query->createParameter('url_hash'))) + ->setParameter('url_hash', $hash); + + $result = $query->execute()->fetch(); + return $result['status']; + } + + /** + * create hash from URL + * + * @param string $url + * @return string + */ + protected function hash($url) { + $normalized = $this->normalizeUrl($url); + return md5($normalized); + } + + /** + * normalize URL, used to create the md5 hash + * + * @param string $url + * @return string + */ + protected function normalizeUrl($url) { + $normalized = $url; + + if (strpos($url, 'https://') === 0) { + $normalized = substr($url, strlen('https://')); + } else if (strpos($url, 'http://') === 0) { + $normalized = substr($url, strlen('http://')); + } + + $normalized = Filesystem::normalizePath($normalized); + $normalized = trim($normalized, '/'); + + return $normalized; + } + } diff --git a/apps/federation/lib/trustedservers.php b/apps/federation/lib/trustedservers.php index bf31277b2a8..f3ac6e24fc6 100644 --- a/apps/federation/lib/trustedservers.php +++ b/apps/federation/lib/trustedservers.php @@ -22,15 +22,21 @@ namespace OCA\Federation; - -use OC\Files\Filesystem; use OCP\AppFramework\Http; +use OCP\BackgroundJob\IJobList; use OCP\Http\Client\IClientService; -use OCP\IDBConnection; use OCP\ILogger; +use OCP\Security\ISecureRandom; class TrustedServers { + /** after a user list was exchanged at least once successfully */ + const STATUS_OK = 1; + /** waiting for shared secret or initial user list exchange */ + const STATUS_PENDING = 2; + /** something went wrong, misconfigured server, software bug,... user interaction needed */ + const STATUS_FAILURE = 3; + /** @var dbHandler */ private $dbHandler; @@ -40,21 +46,31 @@ class TrustedServers { /** @var ILogger */ private $logger; - private $dbTable = 'trusted_servers'; + /** @var IJobList */ + private $jobList; + + /** @var ISecureRandom */ + private $secureRandom; /** * @param DbHandler $dbHandler * @param IClientService $httpClientService * @param ILogger $logger + * @param IJobList $jobList + * @param ISecureRandom $secureRandom */ public function __construct( DbHandler $dbHandler, IClientService $httpClientService, - ILogger $logger + ILogger $logger, + IJobList $jobList, + ISecureRandom $secureRandom ) { $this->dbHandler = $dbHandler; $this->httpClientService = $httpClientService; $this->logger = $logger; + $this->jobList = $jobList; + $this->secureRandom = $secureRandom; } /** @@ -64,7 +80,41 @@ class TrustedServers { * @return int server id */ public function addServer($url) { - return $this->dbHandler->add($this->normalizeUrl($url)); + $url = $this->updateProtocol($url); + $result = $this->dbHandler->addServer($url); + if ($result) { + $token = $this->secureRandom->getMediumStrengthGenerator()->generate(16); + $this->dbHandler->addToken($url, $token); + $this->jobList->add( + 'OCA\Federation\BackgroundJob\RequestSharedSecret', + [ + 'url' => $url, + 'token' => $token + ] + ); + } + + return $result; + } + + /** + * get shared secret for the given server + * + * @param string $url + * @return string + */ + public function getSharedSecret($url) { + return $this->dbHandler->getSharedSecret($url); + } + + /** + * add shared secret for the given server + * + * @param string $url + * @param $sharedSecret + */ + public function addSharedSecret($url, $sharedSecret) { + $this->dbHandler->addSharedSecret($url, $sharedSecret); } /** @@ -73,7 +123,7 @@ class TrustedServers { * @param int $id */ public function removeServer($id) { - $this->dbHandler->remove($id); + $this->dbHandler->removeServer($id); } /** @@ -82,7 +132,7 @@ class TrustedServers { * @return array */ public function getServers() { - return $this->dbHandler->getAll(); + return $this->dbHandler->getAllServer(); } /** @@ -92,7 +142,25 @@ class TrustedServers { * @return bool */ public function isTrustedServer($url) { - return $this->dbHandler->exists($this->normalizeUrl($url)); + return $this->dbHandler->serverExists($url); + } + + /** + * set server status + * + * @param string $url + * @param int $status + */ + public function setServerStatus($url, $status) { + $this->dbHandler->setServerStatus($url, $status); + } + + /** + * @param string $url + * @return int + */ + public function getServerStatus($url) { + return $this->dbHandler->getServerStatus($url); } /** @@ -137,24 +205,21 @@ class TrustedServers { } /** - * normalize URL + * check if the URL contain a protocol, if not add https * * @param string $url * @return string */ - protected function normalizeUrl($url) { + protected function updateProtocol($url) { + if ( + strpos($url, 'https://') === 0 + || strpos($url, 'http://') === 0 + ) { - $normalized = $url; + return $url; - if (strpos($url, 'https://') === 0) { - $normalized = substr($url, strlen('https://')); - } else if (strpos($url, 'http://') === 0) { - $normalized = substr($url, strlen('http://')); } - $normalized = Filesystem::normalizePath($normalized); - $normalized = trim($normalized, '/'); - - return $normalized; + return 'https://' . $url; } } diff --git a/apps/federation/settings/settings-admin.php b/apps/federation/settings/settings-admin.php index ea71475d619..c160b7ee825 100644 --- a/apps/federation/settings/settings-admin.php +++ b/apps/federation/settings/settings-admin.php @@ -31,7 +31,9 @@ $dbHandler = new \OCA\Federation\DbHandler( $trustedServers = new \OCA\Federation\TrustedServers( $dbHandler, \OC::$server->getHTTPClientService(), - \OC::$server->getLogger() + \OC::$server->getLogger(), + \OC::$server->getJobList(), + \OC::$server->getSecureRandom() ); $template->assign('trustedServers', $trustedServers->getServers()); diff --git a/apps/federation/tests/api/ocsauthapitest.php b/apps/federation/tests/api/ocsauthapitest.php new file mode 100644 index 00000000000..a334686c24e --- /dev/null +++ b/apps/federation/tests/api/ocsauthapitest.php @@ -0,0 +1,184 @@ +<?php +/** + * @author Björn Schießle <schiessle@owncloud.com> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + + +namespace OCA\Federation\Tests\API; + + +use OC\BackgroundJob\JobList; +use OCA\Federation\API\OCSAuthAPI; +use OCA\Federation\DbHandler; +use OCA\Federation\TrustedServers; +use OCP\AppFramework\Http; +use OCP\IRequest; +use OCP\Security\ISecureRandom; +use Test\TestCase; + +class OCSAuthAPITest extends TestCase { + + /** @var \PHPUnit_Framework_MockObject_MockObject | IRequest */ + private $request; + + /** @var \PHPUnit_Framework_MockObject_MockObject | ISecureRandom */ + private $secureRandom; + + /** @var \PHPUnit_Framework_MockObject_MockObject | JobList */ + private $jobList; + + /** @var \PHPUnit_Framework_MockObject_MockObject | TrustedServers */ + private $trustedServers; + + /** @var \PHPUnit_Framework_MockObject_MockObject | DbHandler */ + private $dbHandler; + + /** @var OCSAuthApi */ + private $ocsAuthApi; + + public function setUp() { + parent::setUp(); + + $this->request = $this->getMock('OCP\IRequest'); + $this->secureRandom = $this->getMock('OCP\Security\ISecureRandom'); + $this->trustedServers = $this->getMockBuilder('OCA\Federation\TrustedServers') + ->disableOriginalConstructor()->getMock(); + $this->dbHandler = $this->getMockBuilder('OCA\Federation\DbHandler') + ->disableOriginalConstructor()->getMock(); + $this->jobList = $this->getMockBuilder('OC\BackgroundJob\JobList') + ->disableOriginalConstructor()->getMock(); + + $this->ocsAuthApi = new OCSAuthAPI( + $this->request, + $this->secureRandom, + $this->jobList, + $this->trustedServers, + $this->dbHandler + ); + + } + + /** + * @dataProvider dataTestRequestSharedSecret + * + * @param string $token + * @param string $localToken + * @param bool $isTrustedServer + * @param int $expected + */ + public function testRequestSharedSecret($token, $localToken, $isTrustedServer, $expected) { + + $url = 'url'; + + $this->request->expects($this->at(0))->method('getParam')->with('url')->willReturn($url); + $this->request->expects($this->at(1))->method('getParam')->with('token')->willReturn($token); + $this->trustedServers + ->expects($this->once()) + ->method('isTrustedServer')->with($url)->willReturn($isTrustedServer); + $this->dbHandler->expects($this->any()) + ->method('getToken')->with($url)->willReturn($localToken); + + if ($expected === Http::STATUS_OK) { + $this->jobList->expects($this->once())->method('add') + ->with('OCA\Federation\BackgroundJob\GetSharedSecret', ['url' => $url, 'token' => $token]); + } else { + $this->jobList->expects($this->never())->method('add'); + } + + $result = $this->ocsAuthApi->requestSharedSecret(); + $this->assertSame($expected, $result->getStatusCode()); + } + + public function dataTestRequestSharedSecret() { + return [ + ['token2', 'token1', true, Http::STATUS_OK], + ['token1', 'token2', false, Http::STATUS_FORBIDDEN], + ['token1', 'token2', true, Http::STATUS_FORBIDDEN], + ]; + } + + /** + * @dataProvider dataTestGetSharedSecret + * + * @param bool $isTrustedServer + * @param bool $isValidToken + * @param int $expected + */ + public function testGetSharedSecret($isTrustedServer, $isValidToken, $expected) { + + $url = 'url'; + $token = 'token'; + + $this->request->expects($this->at(0))->method('getParam')->with('url')->willReturn($url); + $this->request->expects($this->at(1))->method('getParam')->with('token')->willReturn($token); + + /** @var OCSAuthAPI | \PHPUnit_Framework_MockObject_MockObject $ocsAuthApi */ + $ocsAuthApi = $this->getMockBuilder('OCA\Federation\API\OCSAuthAPI') + ->setConstructorArgs( + [ + $this->request, + $this->secureRandom, + $this->jobList, + $this->trustedServers, + $this->dbHandler + ] + )->setMethods(['isValidToken'])->getMock(); + + $this->trustedServers + ->expects($this->any()) + ->method('isTrustedServer')->with($url)->willReturn($isTrustedServer); + $ocsAuthApi->expects($this->any()) + ->method('isValidToken')->with($url, $token)->willReturn($isValidToken); + + if($expected === Http::STATUS_OK) { + $this->secureRandom->expects($this->once())->method('getMediumStrengthGenerator') + ->willReturn($this->secureRandom); + $this->secureRandom->expects($this->once())->method('generate')->with(32) + ->willReturn('secret'); + $this->trustedServers->expects($this->once()) + ->method('addSharedSecret')->willReturn($url, 'secret'); + $this->dbHandler->expects($this->once()) + ->method('addToken')->with($url, ''); + } else { + $this->secureRandom->expects($this->never())->method('getMediumStrengthGenerator'); + $this->secureRandom->expects($this->never())->method('generate'); + $this->trustedServers->expects($this->never())->method('addSharedSecret'); + $this->dbHandler->expects($this->never())->method('addToken'); + } + + $result = $ocsAuthApi->getSharedSecret(); + + $this->assertSame($expected, $result->getStatusCode()); + + if ($expected === Http::STATUS_OK) { + $data = $result->getData(); + $this->assertSame('secret', $data['sharedSecret']); + } + } + + public function dataTestGetSharedSecret() { + return [ + [true, true, Http::STATUS_OK], + [false, true, Http::STATUS_FORBIDDEN], + [true, false, Http::STATUS_FORBIDDEN], + [false, false, Http::STATUS_FORBIDDEN], + ]; + } + +} diff --git a/apps/federation/tests/lib/dbhandlertest.php b/apps/federation/tests/lib/dbhandlertest.php index 202199d2b5b..50bb9a73d92 100644 --- a/apps/federation/tests/lib/dbhandlertest.php +++ b/apps/federation/tests/lib/dbhandlertest.php @@ -27,6 +27,9 @@ use OCA\Federation\DbHandler; use OCP\IDBConnection; use Test\TestCase; +/** + * @group DB + */ class DbHandlerTest extends TestCase { /** @var DbHandler */ @@ -63,8 +66,8 @@ class DbHandlerTest extends TestCase { $query->execute(); } - public function testAdd() { - $id = $this->dbHandler->add('server1'); + public function testAddServer() { + $id = $this->dbHandler->addServer('server1'); $query = $this->connection->getQueryBuilder()->select('*')->from($this->dbTable); $result = $query->execute()->fetchAll(); @@ -74,8 +77,8 @@ class DbHandlerTest extends TestCase { } public function testRemove() { - $id1 = $this->dbHandler->add('server1'); - $id2 = $this->dbHandler->add('server2'); + $id1 = $this->dbHandler->addServer('server1'); + $id2 = $this->dbHandler->addServer('server2'); $query = $this->connection->getQueryBuilder()->select('*')->from($this->dbTable); $result = $query->execute()->fetchAll(); @@ -85,7 +88,7 @@ class DbHandlerTest extends TestCase { $this->assertSame($id1, $result[0]['id']); $this->assertSame($id2, $result[1]['id']); - $this->dbHandler->remove($id2); + $this->dbHandler->removeServer($id2); $query = $this->connection->getQueryBuilder()->select('*')->from($this->dbTable); $result = $query->execute()->fetchAll(); $this->assertSame(1, count($result)); @@ -94,10 +97,10 @@ class DbHandlerTest extends TestCase { } public function testGetAll() { - $id1 = $this->dbHandler->add('server1'); - $id2 = $this->dbHandler->add('server2'); + $id1 = $this->dbHandler->addServer('server1'); + $id2 = $this->dbHandler->addServer('server2'); - $result = $this->dbHandler->getAll(); + $result = $this->dbHandler->getAllServer(); $this->assertSame(2, count($result)); $this->assertSame('server1', $result[0]['url']); $this->assertSame('server2', $result[1]['url']); @@ -113,9 +116,9 @@ class DbHandlerTest extends TestCase { * @param bool $expected */ public function testExists($serverInTable, $checkForServer, $expected) { - $this->dbHandler->add($serverInTable); + $this->dbHandler->addServer($serverInTable); $this->assertSame($expected, - $this->dbHandler->exists($checkForServer) + $this->dbHandler->serverExists($checkForServer) ); } @@ -127,4 +130,26 @@ class DbHandlerTest extends TestCase { ]; } + /** + * @dataProvider dataTestNormalizeUrl + * + * @param string $url + * @param string $expected + */ + public function testNormalizeUrl($url, $expected) { + $this->assertSame($expected, + $this->invokePrivate($this->dbHandler, 'normalizeUrl', [$url]) + ); + } + + public function dataTestNormalizeUrl() { + return [ + ['owncloud.org', 'owncloud.org'], + ['http://owncloud.org', 'owncloud.org'], + ['https://owncloud.org', 'owncloud.org'], + ['https://owncloud.org//mycloud', 'owncloud.org/mycloud'], + ['https://owncloud.org/mycloud/', 'owncloud.org/mycloud'], + ]; + } + } diff --git a/apps/federation/tests/lib/trustedserverstest.php b/apps/federation/tests/lib/trustedserverstest.php index 07aa7531274..dabf353ef21 100644 --- a/apps/federation/tests/lib/trustedserverstest.php +++ b/apps/federation/tests/lib/trustedserverstest.php @@ -25,11 +25,13 @@ namespace OCA\Federation\Tests\lib; use OCA\Federation\DbHandler; use OCA\Federation\TrustedServers; +use OCP\BackgroundJob\IJobList; use OCP\Http\Client\IClient; use OCP\Http\Client\IClientService; use OCP\Http\Client\IResponse; -use OCP\IDBConnection; +use OCP\IConfig; use OCP\ILogger; +use OCP\Security\ISecureRandom; use Test\TestCase; class TrustedServersTest extends TestCase { @@ -52,6 +54,15 @@ class TrustedServersTest extends TestCase { /** @var \PHPUnit_Framework_MockObject_MockObject | ILogger */ private $logger; + /** @var \PHPUnit_Framework_MockObject_MockObject | IJobList */ + private $jobList; + + /** @var \PHPUnit_Framework_MockObject_MockObject | ISecureRandom */ + private $secureRandom; + + /** @var \PHPUnit_Framework_MockObject_MockObject | IConfig */ + private $config; + public function setUp() { parent::setUp(); @@ -61,45 +72,79 @@ class TrustedServersTest extends TestCase { $this->httpClient = $this->getMock('OCP\Http\Client\IClient'); $this->response = $this->getMock('OCP\Http\Client\IResponse'); $this->logger = $this->getMock('OCP\ILogger'); + $this->jobList = $this->getMock('OCP\BackgroundJob\IJobList'); + $this->secureRandom = $this->getMock('OCP\Security\ISecureRandom'); + $this->config = $this->getMock('OCP\IConfig'); $this->trustedServers = new TrustedServers( $this->dbHandler, $this->httpClientService, - $this->logger + $this->logger, + $this->jobList, + $this->secureRandom, + $this->config ); } - public function testAddServer() { + /** + * @dataProvider dataTestAddServer + * + * @param bool $success + */ + public function testAddServer($success) { /** @var \PHPUnit_Framework_MockObject_MockObject | TrustedServers $trustedServer */ $trustedServers = $this->getMockBuilder('OCA\Federation\TrustedServers') ->setConstructorArgs( [ $this->dbHandler, $this->httpClientService, - $this->logger + $this->logger, + $this->jobList, + $this->secureRandom, + $this->config ] ) - ->setMethods(['normalizeUrl']) + ->setMethods(['normalizeUrl', 'updateProtocol']) ->getMock(); - $trustedServers->expects($this->once())->method('normalizeUrl') - ->with('url')->willReturn('normalized'); - $this->dbHandler->expects($this->once())->method('add')->with('normalized') - ->willReturn(true); + $trustedServers->expects($this->once())->method('updateProtocol') + ->with('url')->willReturn('https://url'); + $this->dbHandler->expects($this->once())->method('addServer')->with('https://url') + ->willReturn($success); + + if ($success) { + $this->secureRandom->expects($this->once())->method('getMediumStrengthGenerator') + ->willReturn($this->secureRandom); + $this->secureRandom->expects($this->once())->method('generate') + ->willReturn('token'); + $this->dbHandler->expects($this->once())->method('addToken')->with('https://url', 'token'); + $this->jobList->expects($this->once())->method('add') + ->with('OCA\Federation\BackgroundJob\RequestSharedSecret', + ['url' => 'https://url', 'token' => 'token']); + } else { + $this->jobList->expects($this->never())->method('add'); + } - $this->assertTrue( + $this->assertSame($success, $trustedServers->addServer('url') ); } + public function dataTestAddServer() { + return [ + [true], + [false] + ]; + } + public function testRemoveServer() { $id = 42; - $this->dbHandler->expects($this->once())->method('remove')->with($id); + $this->dbHandler->expects($this->once())->method('removeServer')->with($id); $this->trustedServers->removeServer($id); } public function testGetServers() { - $this->dbHandler->expects($this->once())->method('getAll')->willReturn(true); + $this->dbHandler->expects($this->once())->method('getAllServer')->willReturn(true); $this->assertTrue( $this->trustedServers->getServers() @@ -108,24 +153,11 @@ class TrustedServersTest extends TestCase { public function testIsTrustedServer() { - /** @var \PHPUnit_Framework_MockObject_MockObject | TrustedServers $trustedServer */ - $trustedServers = $this->getMockBuilder('OCA\Federation\TrustedServers') - ->setConstructorArgs( - [ - $this->dbHandler, - $this->httpClientService, - $this->logger - ] - ) - ->setMethods(['normalizeUrl']) - ->getMock(); - $trustedServers->expects($this->once())->method('normalizeUrl') - ->with('url')->willReturn('normalized'); - $this->dbHandler->expects($this->once())->method('exists')->with('normalized') - ->willReturn(true); + $this->dbHandler->expects($this->once())->method('serverExists')->with('url') + ->willReturn(true); $this->assertTrue( - $trustedServers->isTrustedServer('url') + $this->trustedServers->isTrustedServer('url') ); } @@ -146,7 +178,10 @@ class TrustedServersTest extends TestCase { [ $this->dbHandler, $this->httpClientService, - $this->logger + $this->logger, + $this->jobList, + $this->secureRandom, + $this->config ] ) ->setMethods(['checkOwnCloudVersion']) @@ -192,7 +227,7 @@ class TrustedServersTest extends TestCase { ->with('simulated exception', ['app' => 'federation']); $this->httpClient->expects($this->once())->method('get')->with($server . '/status.php') - ->willReturnCallback(function() { + ->willReturnCallback(function () { throw new \Exception('simulated exception'); }); @@ -221,24 +256,22 @@ class TrustedServersTest extends TestCase { } /** - * @dataProvider dataTestNormalizeUrl - * + * @dataProvider dataTestUpdateProtocol * @param string $url * @param string $expected */ - public function testNormalizeUrl($url, $expected) { + public function testUpdateProtocol($url, $expected) { $this->assertSame($expected, - $this->invokePrivate($this->trustedServers, 'normalizeUrl', [$url]) + $this->invokePrivate($this->trustedServers, 'updateProtocol', [$url]) ); } - public function dataTestNormalizeUrl() { + public function dataTestUpdateProtocol() { return [ - ['owncloud.org', 'owncloud.org'], - ['http://owncloud.org', 'owncloud.org'], - ['https://owncloud.org', 'owncloud.org'], - ['https://owncloud.org//mycloud', 'owncloud.org/mycloud'], - ['https://owncloud.org/mycloud/', 'owncloud.org/mycloud'], + ['http://owncloud.org', 'http://owncloud.org'], + ['https://owncloud.org', 'https://owncloud.org'], + ['owncloud.org', 'https://owncloud.org'], + ['httpserver', 'https://httpserver'], ]; } } |