diff --git a/.gitignore b/.gitignore index 2e42105ad8..d8c57c2518 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ !/apps/dav !/apps/files !/apps/federation +!/apps/federatedfilesharing !/apps/encryption !/apps/files_external !/apps/files_sharing diff --git a/apps/federatedfilesharing/appinfo/app.php b/apps/federatedfilesharing/appinfo/app.php new file mode 100644 index 0000000000..804ab69759 --- /dev/null +++ b/apps/federatedfilesharing/appinfo/app.php @@ -0,0 +1,27 @@ + + * + * @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 + * + */ + +namespace OCA\FederatedFileSharing\AppInfo; + +use OCP\AppFramework\App; + +new App('federatedfilesharing'); + diff --git a/apps/federatedfilesharing/appinfo/info.xml b/apps/federatedfilesharing/appinfo/info.xml new file mode 100644 index 0000000000..d88ea2640e --- /dev/null +++ b/apps/federatedfilesharing/appinfo/info.xml @@ -0,0 +1,14 @@ + + + federatedfilesharing + Federated File Sharing + Provide federated file sharing across ownCloud servers + AGPL + Bjoern Schiessle, Roeland Jago Douma + 0.1.0 + FederatedFileSharing + other + + + + diff --git a/apps/federatedfilesharing/lib/addresshandler.php b/apps/federatedfilesharing/lib/addresshandler.php new file mode 100644 index 0000000000..92768f11b9 --- /dev/null +++ b/apps/federatedfilesharing/lib/addresshandler.php @@ -0,0 +1,184 @@ + + * + * @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 + * + */ + +namespace OCA\FederatedFileSharing; +use OC\HintException; +use OCP\IL10N; +use OCP\IURLGenerator; + +/** + * Class AddressHandler - parse, modify and construct federated sharing addresses + * + * @package OCA\FederatedFileSharing + */ +class AddressHandler { + + /** @var IL10N */ + private $l; + + /** @var IURLGenerator */ + private $urlGenerator; + + /** + * AddressHandler constructor. + * + * @param IURLGenerator $urlGenerator + * @param IL10N $il10n + */ + public function __construct( + IURLGenerator $urlGenerator, + IL10N $il10n + ) { + $this->l = $il10n; + $this->urlGenerator = $urlGenerator; + } + + /** + * split user and remote from federated cloud id + * + * @param string $address federated share address + * @return array [user, remoteURL] + * @throws HintException + */ + public function splitUserRemote($address) { + if (strpos($address, '@') === false) { + $hint = $this->l->t('Invalid Federated Cloud ID'); + throw new HintException('Invalid Federated Cloud ID', $hint); + } + + // Find the first character that is not allowed in user names + $id = str_replace('\\', '/', $address); + $posSlash = strpos($id, '/'); + $posColon = strpos($id, ':'); + + if ($posSlash === false && $posColon === false) { + $invalidPos = strlen($id); + } else if ($posSlash === false) { + $invalidPos = $posColon; + } else if ($posColon === false) { + $invalidPos = $posSlash; + } else { + $invalidPos = min($posSlash, $posColon); + } + + // Find the last @ before $invalidPos + $pos = $lastAtPos = 0; + while ($lastAtPos !== false && $lastAtPos <= $invalidPos) { + $pos = $lastAtPos; + $lastAtPos = strpos($id, '@', $pos + 1); + } + + if ($pos !== false) { + $user = substr($id, 0, $pos); + $remote = substr($id, $pos + 1); + $remote = $this->fixRemoteURL($remote); + if (!empty($user) && !empty($remote)) { + return array($user, $remote); + } + } + + $hint = $this->l->t('Invalid Federated Cloud ID'); + throw new HintException('Invalid Federated Cloud ID', $hint); + } + + /** + * generate remote URL part of federated ID + * + * @return string url of the current server + */ + public function generateRemoteURL() { + $url = $this->urlGenerator->getAbsoluteURL('/'); + return $url; + } + + /** + * check if two federated cloud IDs refer to the same user + * + * @param string $user1 + * @param string $server1 + * @param string $user2 + * @param string $server2 + * @return bool true if both users and servers are the same + */ + public function compareAddresses($user1, $server1, $user2, $server2) { + $normalizedServer1 = strtolower($this->removeProtocolFromUrl($server1)); + $normalizedServer2 = strtolower($this->removeProtocolFromUrl($server2)); + + if (rtrim($normalizedServer1, '/') === rtrim($normalizedServer2, '/')) { + // FIXME this should be a method in the user management instead + \OCP\Util::emitHook( + '\OCA\Files_Sharing\API\Server2Server', + 'preLoginNameUsedAsUserName', + array('uid' => &$user1) + ); + \OCP\Util::emitHook( + '\OCA\Files_Sharing\API\Server2Server', + 'preLoginNameUsedAsUserName', + array('uid' => &$user2) + ); + + if ($user1 === $user2) { + return true; + } + } + + return false; + } + + /** + * remove protocol from URL + * + * @param string $url + * @return string + */ + public function removeProtocolFromUrl($url) { + if (strpos($url, 'https://') === 0) { + return substr($url, strlen('https://')); + } else if (strpos($url, 'http://') === 0) { + return substr($url, strlen('http://')); + } + + return $url; + } + + /** + * Strips away a potential file names and trailing slashes: + * - http://localhost + * - http://localhost/ + * - http://localhost/index.php + * - http://localhost/index.php/s/{shareToken} + * + * all return: http://localhost + * + * @param string $remote + * @return string + */ + protected function fixRemoteURL($remote) { + $remote = str_replace('\\', '/', $remote); + if ($fileNamePosition = strpos($remote, '/index.php')) { + $remote = substr($remote, 0, $fileNamePosition); + } + $remote = rtrim($remote, '/'); + + return $remote; + } + +} diff --git a/apps/federatedfilesharing/lib/federatedshareprovider.php b/apps/federatedfilesharing/lib/federatedshareprovider.php new file mode 100644 index 0000000000..05a9432a32 --- /dev/null +++ b/apps/federatedfilesharing/lib/federatedshareprovider.php @@ -0,0 +1,556 @@ + + * + * @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 + * + */ + +namespace OCA\FederatedFileSharing; + +use OC\Share20\Share; +use OCP\Files\IRootFolder; +use OCP\IL10N; +use OCP\ILogger; +use OCP\Share\IShare; +use OCP\Share\IShareProvider; +use OC\Share20\Exception\InvalidShare; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Files\NotFoundException; +use OCP\IDBConnection; +use OCP\Files\Node; + +/** + * Class FederatedShareProvider + * + * @package OCA\FederatedFileSharing + */ +class FederatedShareProvider implements IShareProvider { + + const SHARE_TYPE_REMOTE = 6; + + /** @var IDBConnection */ + private $dbConnection; + + /** @var AddressHandler */ + private $addressHandler; + + /** @var Notifications */ + private $notifications; + + /** @var TokenHandler */ + private $tokenHandler; + + /** @var IL10N */ + private $l; + + /** @var ILogger */ + private $logger; + + /** @var IRootFolder */ + private $rootFolder; + + /** + * DefaultShareProvider constructor. + * + * @param IDBConnection $connection + * @param AddressHandler $addressHandler + * @param Notifications $notifications + * @param TokenHandler $tokenHandler + * @param IL10N $l10n + * @param ILogger $logger + * @param IRootFolder $rootFolder + */ + public function __construct( + IDBConnection $connection, + AddressHandler $addressHandler, + Notifications $notifications, + TokenHandler $tokenHandler, + IL10N $l10n, + ILogger $logger, + IRootFolder $rootFolder + ) { + $this->dbConnection = $connection; + $this->addressHandler = $addressHandler; + $this->notifications = $notifications; + $this->tokenHandler = $tokenHandler; + $this->l = $l10n; + $this->logger = $logger; + $this->rootFolder = $rootFolder; + } + + /** + * Return the identifier of this provider. + * + * @return string Containing only [a-zA-Z0-9] + */ + public function identifier() { + return 'ocFederatedSharing'; + } + + /** + * Share a path + * + * @param IShare $share + * @return IShare The share object + * @throws ShareNotFound + * @throws \Exception + */ + public function create(IShare $share) { + + $shareWith = $share->getSharedWith(); + $itemSource = $share->getNodeId(); + $itemType = $share->getNodeType(); + $uidOwner = $share->getShareOwner(); + $permissions = $share->getPermissions(); + $sharedBy = $share->getSharedBy(); + + /* + * Check if file is not already shared with the remote user + */ + $alreadyShared = $this->getSharedWith($shareWith, self::SHARE_TYPE_REMOTE, $share->getNode(), 1, 0); + if (!empty($alreadyShared)) { + $message = 'Sharing %s failed, because this item is already shared with %s'; + $message_t = $this->l->t('Sharing %s failed, because this item is already shared with %s', array($share->getNode()->getName(), $shareWith)); + $this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']); + throw new \Exception($message_t); + } + + + // don't allow federated shares if source and target server are the same + list($user, $remote) = $this->addressHandler->splitUserRemote($shareWith); + $currentServer = $this->addressHandler->generateRemoteURL(); + $currentUser = $sharedBy; + if ($this->addressHandler->compareAddresses($user, $remote, $currentUser, $currentServer)) { + $message = 'Not allowed to create a federated share with the same user.'; + $message_t = $this->l->t('Not allowed to create a federated share with the same user'); + $this->logger->debug($message, ['app' => 'Federated File Sharing']); + throw new \Exception($message_t); + } + + $token = $this->tokenHandler->generateToken(); + + $shareWith = $user . '@' . $remote; + + $shareId = $this->addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token); + + $send = $this->notifications->sendRemoteShare( + $token, + $shareWith, + $share->getNode()->getName(), + $shareId, + $share->getSharedBy() + ); + + $data = $this->getRawShare($shareId); + $share = $this->createShare($data); + + if ($send === false) { + $this->delete($share); + $message_t = $this->l->t('Sharing %s failed, could not find %s, maybe the server is currently unreachable.', + [$share->getNode()->getName(), $shareWith]); + throw new \Exception($message_t); + } + + return $share; + } + + /** + * add share to the database and return the ID + * + * @param int $itemSource + * @param string $itemType + * @param string $shareWith + * @param string $sharedBy + * @param string $uidOwner + * @param int $permissions + * @param string $token + * @return int + */ + private function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->insert('share') + ->setValue('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)) + ->setValue('item_type', $qb->createNamedParameter($itemType)) + ->setValue('item_source', $qb->createNamedParameter($itemSource)) + ->setValue('file_source', $qb->createNamedParameter($itemSource)) + ->setValue('share_with', $qb->createNamedParameter($shareWith)) + ->setValue('uid_owner', $qb->createNamedParameter($uidOwner)) + ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy)) + ->setValue('permissions', $qb->createNamedParameter($permissions)) + ->setValue('token', $qb->createNamedParameter($token)) + ->setValue('stime', $qb->createNamedParameter(time())); + + $qb->execute(); + $id = $qb->getLastInsertId(); + + return (int)$id; + } + + /** + * Update a share + * + * @param IShare $share + * @return IShare The share object + */ + public function update(IShare $share) { + /* + * We allow updating the permissions of federated shares + */ + $qb = $this->dbConnection->getQueryBuilder(); + $qb->update('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) + ->set('permissions', $qb->createNamedParameter($share->getPermissions())) + ->execute(); + + return $share; + } + + /** + * @inheritdoc + */ + public function move(IShare $share, $recipient) { + /* + * This function does nothing yet as it is just for outgoing + * federated shares. + */ + return $share; + } + + /** + * Get all children of this share + * + * @param IShare $parent + * @return IShare[] + */ + public function getChildren(IShare $parent) { + $children = []; + + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId()))) + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))) + ->orderBy('id'); + + $cursor = $qb->execute(); + while($data = $cursor->fetch()) { + $children[] = $this->createShare($data); + } + $cursor->closeCursor(); + + return $children; + } + + /** + * Delete a share + * + * @param IShare $share + */ + public function delete(IShare $share) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->delete('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))); + $qb->execute(); + + list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedWith()); + $this->notifications->sendRemoteUnShare($remote, $share->getId(), $share->getToken()); + } + + /** + * @inheritdoc + */ + public function deleteFromSelf(IShare $share, $recipient) { + // nothing to do here. Technically deleteFromSelf in the context of federated + // shares is a umount of a external storage. This is handled here + // apps/files_sharing/lib/external/manager.php + // TODO move this code over to this app + return; + } + + /** + * @inheritdoc + */ + public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share'); + + $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))); + + /** + * Reshares for this user are shares where they are the owner. + */ + if ($reshares === false) { + //Special case for old shares created via the web UI + $or1 = $qb->expr()->andX( + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), + $qb->expr()->isNull('uid_initiator') + ); + + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)), + $or1 + ) + ); + } else { + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) + ) + ); + } + + if ($node !== null) { + $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); + } + + if ($limit !== -1) { + $qb->setMaxResults($limit); + } + + $qb->setFirstResult($offset); + $qb->orderBy('id'); + + $cursor = $qb->execute(); + $shares = []; + while($data = $cursor->fetch()) { + $shares[] = $this->createShare($data); + } + $cursor->closeCursor(); + + return $shares; + } + + /** + * @inheritdoc + */ + public function getShareById($id, $recipientId = null) { + $qb = $this->dbConnection->getQueryBuilder(); + + $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))); + + $cursor = $qb->execute(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + if ($data === false) { + throw new ShareNotFound(); + } + + try { + $share = $this->createShare($data); + } catch (InvalidShare $e) { + throw new ShareNotFound(); + } + + return $share; + } + + /** + * Get shares for a given path + * + * @param \OCP\Files\Node $path + * @return IShare[] + */ + public function getSharesByPath(Node $path) { + $qb = $this->dbConnection->getQueryBuilder(); + + $cursor = $qb->select('*') + ->from('share') + ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId()))) + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))) + ->execute(); + + $shares = []; + while($data = $cursor->fetch()) { + $shares[] = $this->createShare($data); + } + $cursor->closeCursor(); + + return $shares; + } + + /** + * @inheritdoc + */ + public function getSharedWith($userId, $shareType, $node, $limit, $offset) { + /** @var IShare[] $shares */ + $shares = []; + + //Get shares directly with this user + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share'); + + // Order by id + $qb->orderBy('id'); + + // Set limit and offset + if ($limit !== -1) { + $qb->setMaxResults($limit); + } + $qb->setFirstResult($offset); + + $qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))); + $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId))); + + // Filter by node if provided + if ($node !== null) { + $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); + } + + $cursor = $qb->execute(); + + while($data = $cursor->fetch()) { + $shares[] = $this->createShare($data); + } + $cursor->closeCursor(); + + + return $shares; + } + + /** + * Get a share by token + * + * @param string $token + * @return IShare + * @throws ShareNotFound + */ + public function getShareByToken($token) { + $qb = $this->dbConnection->getQueryBuilder(); + + $cursor = $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))) + ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token))) + ->execute(); + + $data = $cursor->fetch(); + + if ($data === false) { + throw new ShareNotFound(); + } + + try { + $share = $this->createShare($data); + } catch (InvalidShare $e) { + throw new ShareNotFound(); + } + + return $share; + } + + /** + * get database row of a give share + * + * @param $id + * @return array + * @throws ShareNotFound + */ + private function getRawShare($id) { + + // Now fetch the inserted share and create a complete share object + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); + + $cursor = $qb->execute(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + if ($data === false) { + throw new ShareNotFound; + } + + return $data; + } + + /** + * Create a share object from an database row + * + * @param array $data + * @return IShare + * @throws InvalidShare + * @throws ShareNotFound + */ + private function createShare($data) { + + $share = new Share($this->rootFolder); + $share->setId((int)$data['id']) + ->setShareType((int)$data['share_type']) + ->setPermissions((int)$data['permissions']) + ->setTarget($data['file_target']) + ->setMailSend((bool)$data['mail_send']) + ->setToken($data['token']); + + $shareTime = new \DateTime(); + $shareTime->setTimestamp((int)$data['stime']); + $share->setShareTime($shareTime); + $share->setSharedWith($data['share_with']); + + if ($data['uid_initiator'] !== null) { + $share->setShareOwner($data['uid_owner']); + $share->setSharedBy($data['uid_initiator']); + } else { + //OLD SHARE + $share->setSharedBy($data['uid_owner']); + $path = $this->getNode($share->getSharedBy(), (int)$data['file_source']); + + $owner = $path->getOwner(); + $share->setShareOwner($owner->getUID()); + } + + $share->setNodeId((int)$data['file_source']); + $share->setNodeType($data['item_type']); + + $share->setProviderId($this->identifier()); + + return $share; + } + + /** + * Get the node with file $id for $user + * + * @param string $userId + * @param int $id + * @return \OCP\Files\File|\OCP\Files\Folder + * @throws InvalidShare + */ + private function getNode($userId, $id) { + try { + $userFolder = $this->rootFolder->getUserFolder($userId); + } catch (NotFoundException $e) { + throw new InvalidShare(); + } + + $nodes = $userFolder->getById($id); + + if (empty($nodes)) { + throw new InvalidShare(); + } + + return $nodes[0]; + } + +} diff --git a/apps/federatedfilesharing/lib/notifications.php b/apps/federatedfilesharing/lib/notifications.php new file mode 100644 index 0000000000..d778ac8782 --- /dev/null +++ b/apps/federatedfilesharing/lib/notifications.php @@ -0,0 +1,144 @@ + + * + * @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 + * + */ + + +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; + + /** + * Notifications constructor. + * + * @param AddressHandler $addressHandler + * @param IClientService $httpClientService + */ + public function __construct( + AddressHandler $addressHandler, + IClientService $httpClientService + ) { + $this->addressHandler = $addressHandler; + $this->httpClientService = $httpClientService; + } + + /** + * send server-to-server share to remote server + * + * @param string $token + * @param string $shareWith + * @param string $name + * @param int $remote_id + * @param string $owner + * @return bool + */ + public function sendRemoteShare($token, $shareWith, $name, $remote_id, $owner) { + + list($user, $remote) = $this->addressHandler->splitUserRemote($shareWith); + + if ($user && $remote) { + $url = $remote . self::BASE_PATH_TO_SHARE_API . '?format=' . self::RESPONSE_FORMAT; + $local = $this->addressHandler->generateRemoteURL(); + + $fields = array( + 'shareWith' => $user, + 'token' => $token, + 'name' => $name, + 'remoteId' => $remote_id, + 'owner' => $owner, + 'remote' => $local, + ); + + $url = $this->addressHandler->removeProtocolFromUrl($url); + $result = $this->tryHttpPost($url, $fields); + $status = json_decode($result['result'], true); + + if ($result['success'] && $status['ocs']['meta']['statuscode'] === 100) { + \OC_Hook::emit('OCP\Share', 'federated_share_added', ['server' => $remote]); + return true; + } + + } + + return false; + } + + /** + * send server-to-server unshare to remote server + * + * @param string $remote url + * @param int $id share id + * @param string $token + * @return bool + */ + public function sendRemoteUnShare($remote, $id, $token) { + $url = rtrim($remote, '/') . self::BASE_PATH_TO_SHARE_API . '/' . $id . '/unshare?format=' . self::RESPONSE_FORMAT; + $fields = array('token' => $token, 'format' => 'json'); + $url = $this->addressHandler->removeProtocolFromUrl($url); + $result = $this->tryHttpPost($url, $fields); + $status = json_decode($result['result'], true); + + return ($result['success'] && $status['ocs']['meta']['statuscode'] === 100); + } + + /** + * try http post first with https and then with http as a fallback + * + * @param string $url + * @param array $fields post parameters + * @return array + */ + private function tryHttpPost($url, array $fields) { + $client = $this->httpClientService->newClient(); + $protocol = 'https://'; + $result = [ + 'success' => false, + 'result' => '', + ]; + $try = 0; + while ($result['success'] === false && $try < 2) { + try { + $response = $client->post($protocol . $url, [ + 'body' => $fields + ]); + $result['result'] = $response->getBody(); + $result['success'] = true; + break; + } catch (\Exception $e) { + $try++; + $protocol = 'http://'; + } + } + + return $result; + } + +} diff --git a/apps/federatedfilesharing/lib/tokenhandler.php b/apps/federatedfilesharing/lib/tokenhandler.php new file mode 100644 index 0000000000..ec5f73127d --- /dev/null +++ b/apps/federatedfilesharing/lib/tokenhandler.php @@ -0,0 +1,61 @@ + + * + * @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 + * + */ + + +namespace OCA\FederatedFileSharing; + + +use OCP\Security\ISecureRandom; + +/** + * Class TokenHandler + * + * @package OCA\FederatedFileSharing + */ +class TokenHandler { + + const TOKEN_LENGTH = 15; + + /** @var ISecureRandom */ + private $secureRandom; + + /** + * TokenHandler constructor. + * + * @param ISecureRandom $secureRandom + */ + public function __construct(ISecureRandom $secureRandom) { + $this->secureRandom = $secureRandom; + } + + /** + * generate to token used to authenticate federated shares + * + * @return string + */ + public function generateToken() { + $token = $this->secureRandom->generate( + self::TOKEN_LENGTH, + ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS); + return $token; + } + +} diff --git a/apps/federatedfilesharing/tests/lib/addressHandlerTest.php b/apps/federatedfilesharing/tests/lib/addressHandlerTest.php new file mode 100644 index 0000000000..9c99f70abf --- /dev/null +++ b/apps/federatedfilesharing/tests/lib/addressHandlerTest.php @@ -0,0 +1,198 @@ + + * + * @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 + * + */ + + +namespace OCA\FederatedFileSharing\Tests\lib; + + +use OCA\FederatedFileSharing\AddressHandler; +use OCP\IL10N; +use OCP\IURLGenerator; +use Test\TestCase; + +class AddressHandlerTest extends TestCase { + + /** @var AddressHandler */ + private $addressHandler; + + /** @var IURLGenerator | \PHPUnit_Framework_MockObject_MockObject */ + private $urlGenerator; + + /** @var IL10N | \PHPUnit_Framework_MockObject_MockObject */ + private $il10n; + + public function setUp() { + parent::setUp(); + + $this->urlGenerator = $this->getMock('OCP\IURLGenerator'); + $this->il10n = $this->getMock('OCP\IL10N'); + + $this->addressHandler = new AddressHandler($this->urlGenerator, $this->il10n); + } + + public function dataTestSplitUserRemote() { + $userPrefix = ['user@name', 'username']; + $protocols = ['', 'http://', 'https://']; + $remotes = [ + 'localhost', + 'local.host', + 'dev.local.host', + 'dev.local.host/path', + 'dev.local.host/at@inpath', + '127.0.0.1', + '::1', + '::192.0.2.128', + '::192.0.2.128/at@inpath', + ]; + + $testCases = []; + foreach ($userPrefix as $user) { + foreach ($remotes as $remote) { + foreach ($protocols as $protocol) { + $baseUrl = $user . '@' . $protocol . $remote; + + $testCases[] = [$baseUrl, $user, $protocol . $remote]; + $testCases[] = [$baseUrl . '/', $user, $protocol . $remote]; + $testCases[] = [$baseUrl . '/index.php', $user, $protocol . $remote]; + $testCases[] = [$baseUrl . '/index.php/s/token', $user, $protocol . $remote]; + } + } + } + return $testCases; + } + + /** + * @dataProvider dataTestSplitUserRemote + * + * @param string $remote + * @param string $expectedUser + * @param string $expectedUrl + */ + public function testSplitUserRemote($remote, $expectedUser, $expectedUrl) { + list($remoteUser, $remoteUrl) = $this->addressHandler->splitUserRemote($remote); + $this->assertSame($expectedUser, $remoteUser); + $this->assertSame($expectedUrl, $remoteUrl); + } + + public function dataTestSplitUserRemoteError() { + return array( + // Invalid path + array('user@'), + + // Invalid user + array('@server'), + array('us/er@server'), + array('us:er@server'), + + // Invalid splitting + array('user'), + array(''), + array('us/erserver'), + array('us:erserver'), + ); + } + + /** + * @dataProvider dataTestSplitUserRemoteError + * + * @param string $id + * @expectedException \OC\HintException + */ + public function testSplitUserRemoteError($id) { + $this->addressHandler->splitUserRemote($id); + } + + /** + * @dataProvider dataTestCompareAddresses + * + * @param string $user1 + * @param string $server1 + * @param string $user2 + * @param string $server2 + * @param bool $expected + */ + public function testCompareAddresses($user1, $server1, $user2, $server2, $expected) { + $this->assertSame($expected, + $this->addressHandler->compareAddresses($user1, $server1, $user2, $server2) + ); + } + + public function dataTestCompareAddresses() { + return [ + ['user1', 'http://server1', 'user1', 'http://server1', true], + ['user1', 'https://server1', 'user1', 'http://server1', true], + ['user1', 'http://serVer1', 'user1', 'http://server1', true], + ['user1', 'http://server1/', 'user1', 'http://server1', true], + ['user1', 'server1', 'user1', 'http://server1', true], + ['user1', 'http://server1', 'user1', 'http://server2', false], + ['user1', 'https://server1', 'user1', 'http://server2', false], + ['user1', 'http://serVer1', 'user1', 'http://serer2', false], + ['user1', 'http://server1/', 'user1', 'http://server2', false], + ['user1', 'server1', 'user1', 'http://server2', false], + ['user1', 'http://server1', 'user2', 'http://server1', false], + ['user1', 'https://server1', 'user2', 'http://server1', false], + ['user1', 'http://serVer1', 'user2', 'http://server1', false], + ['user1', 'http://server1/', 'user2', 'http://server1', false], + ['user1', 'server1', 'user2', 'http://server1', false], + ]; + } + + /** + * @dataProvider dataTestRemoveProtocolFromUrl + * + * @param string $url + * @param string $expectedResult + */ + public function testRemoveProtocolFromUrl($url, $expectedResult) { + $result = $this->addressHandler->removeProtocolFromUrl($url); + $this->assertSame($expectedResult, $result); + } + + public function dataTestRemoveProtocolFromUrl() { + return [ + ['http://owncloud.org', 'owncloud.org'], + ['https://owncloud.org', 'owncloud.org'], + ['owncloud.org', 'owncloud.org'], + ]; + } + + /** + * @dataProvider dataTestFixRemoteUrl + * + * @param string $url + * @param string $expected + */ + public function testFixRemoteUrl($url, $expected) { + $this->assertSame($expected, + $this->invokePrivate($this->addressHandler, 'fixRemoteURL', [$url]) + ); + } + + public function dataTestFixRemoteUrl() { + return [ + ['http://localhost', 'http://localhost'], + ['http://localhost/', 'http://localhost'], + ['http://localhost/index.php', 'http://localhost'], + ['http://localhost/index.php/s/AShareToken', 'http://localhost'], + ]; + } + +} diff --git a/apps/federatedfilesharing/tests/lib/tokenHandlerTest.php b/apps/federatedfilesharing/tests/lib/tokenHandlerTest.php new file mode 100644 index 0000000000..c5cb49daee --- /dev/null +++ b/apps/federatedfilesharing/tests/lib/tokenHandlerTest.php @@ -0,0 +1,62 @@ + + * + * @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 + * + */ + + +namespace OCA\FederatedFileSharing\Tests\lib; + + +use OCA\FederatedFileSharing\TokenHandler; +use OCP\Security\ISecureRandom; +use Test\TestCase; + +class TokenHandlerTest extends TestCase { + + /** @var TokenHandler */ + private $tokenHandler; + + /** @var ISecureRandom | \PHPUnit_Framework_MockObject_MockObject */ + private $secureRandom; + + /** @var int */ + private $expectedTokenLength = 15; + + public function setUp() { + parent::setUp(); + + $this->secureRandom = $this->getMock('OCP\Security\ISecureRandom'); + + $this->tokenHandler = new TokenHandler($this->secureRandom); + } + + public function testGenerateToken() { + + $this->secureRandom->expects($this->once())->method('generate') + ->with( + $this->expectedTokenLength, + ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS + ) + ->willReturn(true); + + $this->assertTrue($this->tokenHandler->generateToken()); + + } + +} diff --git a/core/shipped.json b/core/shipped.json index 069bb210ba..236dc625c2 100644 --- a/core/shipped.json +++ b/core/shipped.json @@ -35,7 +35,8 @@ "user_ldap", "user_shibboleth", "windows_network_drive", - "workflow" + "workflow", + "federatedfilesharing" ], "alwaysEnabled": [ "files", diff --git a/tests/enable_all.php b/tests/enable_all.php index 6f2d1fa871..afea5c8966 100644 --- a/tests/enable_all.php +++ b/tests/enable_all.php @@ -23,3 +23,4 @@ enableApp('user_ldap'); enableApp('files_versions'); enableApp('provisioning_api'); enableApp('federation'); +enableApp('federatedfilesharing');