diff options
author | Thomas Müller <thomas.mueller@tmit.eu> | 2016-03-01 11:27:28 +0100 |
---|---|---|
committer | Thomas Müller <thomas.mueller@tmit.eu> | 2016-03-01 11:27:28 +0100 |
commit | 5fe5233f419624dc3eac8ee4bf95a38b001ea6fd (patch) | |
tree | 4c0b5ea576ec67cc2d5f57ccb186e65276d1cc3c /apps/federatedfilesharing | |
parent | 73e145cf63e94f68dc1f129da14e470695d46abd (diff) | |
parent | 88fc5149eddcbeaf41358c0eb56be45ad2c94a59 (diff) | |
download | nextcloud-server-5fe5233f419624dc3eac8ee4bf95a38b001ea6fd.tar.gz nextcloud-server-5fe5233f419624dc3eac8ee4bf95a38b001ea6fd.zip |
Merge pull request #22681 from owncloud/add-autodiscovery-for-ocs
Add autodiscovery support to server-to-server sharing implementation
Diffstat (limited to 'apps/federatedfilesharing')
-rw-r--r-- | apps/federatedfilesharing/lib/discoverymanager.php | 136 | ||||
-rw-r--r-- | apps/federatedfilesharing/lib/notifications.php | 36 | ||||
-rw-r--r-- | apps/federatedfilesharing/tests/DiscoveryManagerTest.php | 194 |
3 files changed, 349 insertions, 17 deletions
diff --git a/apps/federatedfilesharing/lib/discoverymanager.php b/apps/federatedfilesharing/lib/discoverymanager.php new file mode 100644 index 00000000000..51ea71195fa --- /dev/null +++ b/apps/federatedfilesharing/lib/discoverymanager.php @@ -0,0 +1,136 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * + * @copyright Copyright (c) 2016, 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\FederatedFileSharing; + +use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Exception\ConnectException; +use OCP\Http\Client\IClient; +use OCP\Http\Client\IClientService; +use OCP\ICache; +use OCP\ICacheFactory; + +/** + * Class DiscoveryManager handles the discovery of endpoints used by Federated + * Cloud Sharing. + * + * @package OCA\FederatedFileSharing + */ +class DiscoveryManager { + /** @var ICache */ + private $cache; + /** @var IClient */ + private $client; + + /** + * @param ICacheFactory $cacheFactory + * @param IClientService $clientService + */ + public function __construct(ICacheFactory $cacheFactory, + IClientService $clientService) { + $this->cache = $cacheFactory->create('ocs-discovery'); + $this->client = $clientService->newClient(); + } + + /** + * Returns whether the specified URL includes only safe characters, if not + * returns false + * + * @param string $url + * @return bool + */ + private function isSafeUrl($url) { + return (bool)preg_match('/^[\/\.A-Za-z0-9]+$/', $url); + } + + /** + * Discover the actual data and do some naive caching to ensure that the data + * is not requested multiple times. + * + * If no valid discovery data is found the ownCloud defaults are returned. + * + * @param string $remote + * @return array + */ + private function discover($remote) { + // Check if something is in the cache + if($cacheData = $this->cache->get($remote)) { + return json_decode($cacheData, true); + } + + // Default response body + $discoveredServices = [ + 'webdav' => '/public.php/webdav', + 'share' => '/ocs/v1.php/cloud/shares', + ]; + + // Read the data from the response body + try { + $response = $this->client->get($remote . '/ocs-provider/'); + if($response->getStatusCode() === 200) { + $decodedService = json_decode($response->getBody(), true); + if(is_array($decodedService)) { + $endpoints = [ + 'webdav', + 'share', + ]; + + foreach($endpoints as $endpoint) { + if(isset($decodedService['services']['FEDERATED_SHARING']['endpoints'][$endpoint])) { + $endpointUrl = (string)$decodedService['services']['FEDERATED_SHARING']['endpoints'][$endpoint]; + if($this->isSafeUrl($endpointUrl)) { + $discoveredServices[$endpoint] = $endpointUrl; + } + } + } + } + } + } catch (ClientException $e) { + // Don't throw any exception since exceptions are handled before + } catch (ConnectException $e) { + // Don't throw any exception since exceptions are handled before + } + + // Write into cache + $this->cache->set($remote, json_encode($discoveredServices)); + return $discoveredServices; + } + + /** + * Return the public WebDAV endpoint used by the specified remote + * + * @param string $host + * @return string + */ + public function getWebDavEndpoint($host) { + return $this->discover($host)['webdav']; + } + + /** + * Return the sharing endpoint used by the specified remote + * + * @param string $host + * @return string + */ + public function getShareEndpoint($host) { + return $this->discover($host)['share']; + } +} diff --git a/apps/federatedfilesharing/lib/notifications.php b/apps/federatedfilesharing/lib/notifications.php index d778ac87828..4ec21e81cc7 100644 --- a/apps/federatedfilesharing/lib/notifications.php +++ b/apps/federatedfilesharing/lib/notifications.php @@ -1,6 +1,7 @@ <?php /** * @author Björn Schießle <schiessle@owncloud.com> + * @author Lukas Reschke <lukas@owncloud.com> * * @copyright Copyright (c) 2016, ownCloud, Inc. * @license AGPL-3.0 @@ -22,32 +23,31 @@ namespace OCA\FederatedFileSharing; - use OCP\Http\Client\IClientService; class Notifications { - - const BASE_PATH_TO_SHARE_API = '/ocs/v1.php/cloud/shares'; const RESPONSE_FORMAT = 'json'; // default response format for ocs calls /** @var AddressHandler */ private $addressHandler; - /** @var IClientService */ private $httpClientService; + /** @var DiscoveryManager */ + private $discoveryManager; /** - * Notifications constructor. - * * @param AddressHandler $addressHandler * @param IClientService $httpClientService + * @param DiscoveryManager $discoveryManager */ public function __construct( AddressHandler $addressHandler, - IClientService $httpClientService + IClientService $httpClientService, + DiscoveryManager $discoveryManager ) { $this->addressHandler = $addressHandler; $this->httpClientService = $httpClientService; + $this->discoveryManager = $discoveryManager; } /** @@ -65,7 +65,7 @@ class Notifications { list($user, $remote) = $this->addressHandler->splitUserRemote($shareWith); if ($user && $remote) { - $url = $remote . self::BASE_PATH_TO_SHARE_API . '?format=' . self::RESPONSE_FORMAT; + $url = $remote; $local = $this->addressHandler->generateRemoteURL(); $fields = array( @@ -78,10 +78,10 @@ class Notifications { ); $url = $this->addressHandler->removeProtocolFromUrl($url); - $result = $this->tryHttpPost($url, $fields); + $result = $this->tryHttpPostToShareEndpoint($url, '', $fields); $status = json_decode($result['result'], true); - if ($result['success'] && $status['ocs']['meta']['statuscode'] === 100) { + if ($result['success'] && ($status['ocs']['meta']['statuscode'] === 100 || $status['ocs']['meta']['statuscode'] === 200)) { \OC_Hook::emit('OCP\Share', 'federated_share_added', ['server' => $remote]); return true; } @@ -100,23 +100,24 @@ class Notifications { * @return bool */ public function sendRemoteUnShare($remote, $id, $token) { - $url = rtrim($remote, '/') . self::BASE_PATH_TO_SHARE_API . '/' . $id . '/unshare?format=' . self::RESPONSE_FORMAT; + $url = rtrim($remote, '/'); $fields = array('token' => $token, 'format' => 'json'); $url = $this->addressHandler->removeProtocolFromUrl($url); - $result = $this->tryHttpPost($url, $fields); + $result = $this->tryHttpPostToShareEndpoint($url, '/'.$id.'/unshare', $fields); $status = json_decode($result['result'], true); - return ($result['success'] && $status['ocs']['meta']['statuscode'] === 100); + return ($result['success'] && ($status['ocs']['meta']['statuscode'] === 100 || $status['ocs']['meta']['statuscode'] === 200)); } /** * try http post first with https and then with http as a fallback * - * @param string $url + * @param string $remoteDomain + * @param string $urlSuffix * @param array $fields post parameters * @return array */ - private function tryHttpPost($url, array $fields) { + private function tryHttpPostToShareEndpoint($remoteDomain, $urlSuffix, array $fields) { $client = $this->httpClientService->newClient(); $protocol = 'https://'; $result = [ @@ -124,9 +125,11 @@ class Notifications { 'result' => '', ]; $try = 0; + while ($result['success'] === false && $try < 2) { + $endpoint = $this->discoveryManager->getShareEndpoint($protocol . $remoteDomain); try { - $response = $client->post($protocol . $url, [ + $response = $client->post($protocol . $remoteDomain . $endpoint . $urlSuffix . '?format=' . self::RESPONSE_FORMAT, [ 'body' => $fields ]); $result['result'] = $response->getBody(); @@ -140,5 +143,4 @@ class Notifications { return $result; } - } diff --git a/apps/federatedfilesharing/tests/DiscoveryManagerTest.php b/apps/federatedfilesharing/tests/DiscoveryManagerTest.php new file mode 100644 index 00000000000..9ae62b1ae4d --- /dev/null +++ b/apps/federatedfilesharing/tests/DiscoveryManagerTest.php @@ -0,0 +1,194 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * + * @copyright Copyright (c) 2016, 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\FederatedFileSharing\Tests; + +use OCA\FederatedFileSharing\DiscoveryManager; +use OCP\Http\Client\IClient; +use OCP\Http\Client\IClientService; +use OCP\ICache; +use OCP\ICacheFactory; +use Test\TestCase; + +class DiscoveryManagerTest extends TestCase { + /** @var ICache */ + private $cache; + /** @var IClient */ + private $client; + /** @var DiscoveryManager */ + private $discoveryManager; + + public function setUp() { + parent::setUp(); + $this->cache = $this->getMock('\OCP\ICache'); + /** @var ICacheFactory $cacheFactory */ + $cacheFactory = $this->getMockBuilder('\OCP\ICacheFactory') + ->disableOriginalConstructor()->getMock(); + $cacheFactory + ->expects($this->once()) + ->method('create') + ->with('ocs-discovery') + ->willReturn($this->cache); + + $this->client = $this->getMockBuilder('\OCP\Http\Client\IClient') + ->disableOriginalConstructor()->getMock(); + /** @var IClientService $clientService */ + $clientService = $this->getMockBuilder('\OCP\Http\Client\IClientService') + ->disableOriginalConstructor()->getMock(); + $clientService + ->expects($this->once()) + ->method('newClient') + ->willReturn($this->client); + + $this->discoveryManager = new DiscoveryManager( + $cacheFactory, + $clientService + ); + } + + public function testWithMalformedFormattedEndpointCached() { + $response = $this->getMock('\OCP\Http\Client\IResponse'); + $response + ->expects($this->once()) + ->method('getStatusCode') + ->willReturn(200); + $response + ->expects($this->once()) + ->method('getBody') + ->willReturn('CertainlyNotJson'); + $this->client + ->expects($this->once()) + ->method('get') + ->with('https://myhost.com/ocs-provider/', []) + ->willReturn($response); + $this->cache + ->expects($this->at(0)) + ->method('get') + ->with('https://myhost.com') + ->willReturn(null); + $this->cache + ->expects($this->at(1)) + ->method('set') + ->with('https://myhost.com', '{"webdav":"\/public.php\/webdav","share":"\/ocs\/v1.php\/cloud\/shares"}'); + $this->cache + ->expects($this->at(2)) + ->method('get') + ->with('https://myhost.com') + ->willReturn('{"webdav":"\/public.php\/webdav","share":"\/ocs\/v1.php\/cloud\/shares"}'); + + $this->assertSame('/public.php/webdav', $this->discoveryManager->getWebDavEndpoint('https://myhost.com')); + $this->assertSame('/ocs/v1.php/cloud/shares', $this->discoveryManager->getShareEndpoint('https://myhost.com')); + } + + public function testGetWebDavEndpointWithValidFormattedEndpointAndNotCached() { + $response = $this->getMock('\OCP\Http\Client\IResponse'); + $response + ->expects($this->once()) + ->method('getStatusCode') + ->willReturn(200); + $response + ->expects($this->once()) + ->method('getBody') + ->willReturn('{"version":2,"services":{"PRIVATE_DATA":{"version":1,"endpoints":{"store":"\/ocs\/v2.php\/privatedata\/setattribute","read":"\/ocs\/v2.php\/privatedata\/getattribute","delete":"\/ocs\/v2.php\/privatedata\/deleteattribute"}},"SHARING":{"version":1,"endpoints":{"share":"\/ocs\/v2.php\/apps\/files_sharing\/api\/v1\/shares"}},"FEDERATED_SHARING":{"version":1,"endpoints":{"share":"\/ocs\/v2.php\/cloud\/shares","webdav":"\/public.php\/MyCustomEndpoint\/"}},"ACTIVITY":{"version":1,"endpoints":{"list":"\/ocs\/v2.php\/cloud\/activity"}},"PROVISIONING":{"version":1,"endpoints":{"user":"\/ocs\/v2.php\/cloud\/users","groups":"\/ocs\/v2.php\/cloud\/groups","apps":"\/ocs\/v2.php\/cloud\/apps"}}}}'); + $this->client + ->expects($this->once()) + ->method('get') + ->with('https://myhost.com/ocs-provider/', []) + ->willReturn($response); + + $expectedResult = '/public.php/MyCustomEndpoint/'; + $this->assertSame($expectedResult, $this->discoveryManager->getWebDavEndpoint('https://myhost.com')); + } + + public function testGetWebDavEndpointWithValidFormattedEndpointWithoutDataAndNotCached() { + $response = $this->getMock('\OCP\Http\Client\IResponse'); + $response + ->expects($this->once()) + ->method('getStatusCode') + ->willReturn(200); + $response + ->expects($this->once()) + ->method('getBody') + ->willReturn('{"version":2,"PRIVATE_DATA":{"version":1,"endpoints":{"store":"\/ocs\/v2.php\/privatedata\/setattribute","read":"\/ocs\/v2.php\/privatedata\/getattribute","delete":"\/ocs\/v2.php\/privatedata\/deleteattribute"}},"SHARING":{"version":1,"endpoints":{"share":"\/ocs\/v2.php\/apps\/files_sharing\/api\/v1\/shares"}},"FEDERATED_SHARING":{"version":1,"endpoints":{"share":"\/ocs\/v2.php\/cloud\/shares","webdav":"\/public.php\/MyCustomEndpoint\/"}},"ACTIVITY":{"version":1,"endpoints":{"list":"\/ocs\/v2.php\/cloud\/activity"}},"PROVISIONING":{"version":1,"endpoints":{"user":"\/ocs\/v2.php\/cloud\/users","groups":"\/ocs\/v2.php\/cloud\/groups","apps":"\/ocs\/v2.php\/cloud\/apps"}}}'); + $this->client + ->expects($this->once()) + ->method('get') + ->with('https://myhost.com/ocs-provider/', []) + ->willReturn($response); + + $expectedResult = '/public.php/webdav'; + $this->assertSame($expectedResult, $this->discoveryManager->getWebDavEndpoint('https://myhost.com')); + } + + public function testGetShareEndpointWithValidFormattedEndpointAndNotCached() { + $response = $this->getMock('\OCP\Http\Client\IResponse'); + $response + ->expects($this->once()) + ->method('getStatusCode') + ->willReturn(200); + $response + ->expects($this->once()) + ->method('getBody') + ->willReturn('{"version":2,"services":{"PRIVATE_DATA":{"version":1,"endpoints":{"store":"\/ocs\/v2.php\/privatedata\/setattribute","read":"\/ocs\/v2.php\/privatedata\/getattribute","delete":"\/ocs\/v2.php\/privatedata\/deleteattribute"}},"SHARING":{"version":1,"endpoints":{"share":"\/ocs\/v2.php\/apps\/files_sharing\/api\/v1\/shares"}},"FEDERATED_SHARING":{"version":1,"endpoints":{"share":"\/ocs\/v2.php\/cloud\/MyCustomShareEndpoint","webdav":"\/public.php\/MyCustomEndpoint\/"}},"ACTIVITY":{"version":1,"endpoints":{"list":"\/ocs\/v2.php\/cloud\/activity"}},"PROVISIONING":{"version":1,"endpoints":{"user":"\/ocs\/v2.php\/cloud\/users","groups":"\/ocs\/v2.php\/cloud\/groups","apps":"\/ocs\/v2.php\/cloud\/apps"}}}}'); + $this->client + ->expects($this->once()) + ->method('get') + ->with('https://myhost.com/ocs-provider/', []) + ->willReturn($response); + + $expectedResult = '/ocs/v2.php/cloud/MyCustomShareEndpoint'; + $this->assertSame($expectedResult, $this->discoveryManager->getShareEndpoint('https://myhost.com')); + } + + public function testWithMaliciousEndpointCached() { + $response = $this->getMock('\OCP\Http\Client\IResponse'); + $response + ->expects($this->once()) + ->method('getStatusCode') + ->willReturn(200); + $response + ->expects($this->once()) + ->method('getBody') + ->willReturn('{"version":2,"services":{"PRIVATE_DATA":{"version":1,"endpoints":{"store":"\/ocs\/v2.php\/privatedata\/setattribute","read":"\/ocs\/v2.php\/privatedata\/getattribute","delete":"\/ocs\/v2.php\/privatedata\/deleteattribute"}},"SHARING":{"version":1,"endpoints":{"share":"\/ocs\/v2.php\/apps\/files_sharing\/api\/v1\/shares"}},"FEDERATED_SHARING":{"version":1,"endpoints":{"share":"\/ocs\/v2.php\/cl@oud\/MyCustomShareEndpoint","webdav":"\/public.php\/MyC:ustomEndpoint\/"}},"ACTIVITY":{"version":1,"endpoints":{"list":"\/ocs\/v2.php\/cloud\/activity"}},"PROVISIONING":{"version":1,"endpoints":{"user":"\/ocs\/v2.php\/cloud\/users","groups":"\/ocs\/v2.php\/cloud\/groups","apps":"\/ocs\/v2.php\/cloud\/apps"}}}}'); + $this->client + ->expects($this->once()) + ->method('get') + ->with('https://myhost.com/ocs-provider/', []) + ->willReturn($response); + $this->cache + ->expects($this->at(0)) + ->method('get') + ->with('https://myhost.com') + ->willReturn(null); + $this->cache + ->expects($this->at(1)) + ->method('set') + ->with('https://myhost.com', '{"webdav":"\/public.php\/webdav","share":"\/ocs\/v1.php\/cloud\/shares"}'); + $this->cache + ->expects($this->at(2)) + ->method('get') + ->with('https://myhost.com') + ->willReturn('{"webdav":"\/public.php\/webdav","share":"\/ocs\/v1.php\/cloud\/shares"}'); + + $this->assertSame('/public.php/webdav', $this->discoveryManager->getWebDavEndpoint('https://myhost.com')); + $this->assertSame('/ocs/v1.php/cloud/shares', $this->discoveryManager->getShareEndpoint('https://myhost.com')); + } +} |