diff options
Diffstat (limited to 'apps/federation/lib')
-rw-r--r-- | apps/federation/lib/AppInfo/Application.php | 76 | ||||
-rw-r--r-- | apps/federation/lib/BackgroundJob/GetSharedSecret.php | 191 | ||||
-rw-r--r-- | apps/federation/lib/BackgroundJob/RequestSharedSecret.php | 184 | ||||
-rw-r--r-- | apps/federation/lib/Command/SyncFederationAddressBooks.php | 50 | ||||
-rw-r--r-- | apps/federation/lib/Controller/OCSAuthAPIController.php | 187 | ||||
-rw-r--r-- | apps/federation/lib/Controller/SettingsController.php | 175 | ||||
-rw-r--r-- | apps/federation/lib/DAV/FedAuth.php | 34 | ||||
-rw-r--r-- | apps/federation/lib/DbHandler.php | 212 | ||||
-rw-r--r-- | apps/federation/lib/Hooks.php | 51 | ||||
-rw-r--r-- | apps/federation/lib/Listener/SabrePluginAuthInitListener.php | 38 | ||||
-rw-r--r-- | apps/federation/lib/Middleware/AddServerMiddleware.php | 88 | ||||
-rw-r--r-- | apps/federation/lib/Migration/Version1010Date20200630191302.php | 66 | ||||
-rw-r--r-- | apps/federation/lib/Settings/Admin.php | 52 | ||||
-rw-r--r-- | apps/federation/lib/SyncFederationAddressBooks.php | 96 | ||||
-rw-r--r-- | apps/federation/lib/SyncJob.php | 57 | ||||
-rw-r--r-- | apps/federation/lib/TrustedServers.php | 246 |
16 files changed, 645 insertions, 1158 deletions
diff --git a/apps/federation/lib/AppInfo/Application.php b/apps/federation/lib/AppInfo/Application.php index ea8116e4353..358e3f68d50 100644 --- a/apps/federation/lib/AppInfo/Application.php +++ b/apps/federation/lib/AppInfo/Application.php @@ -1,82 +1,32 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Björn Schießle <bjoern@schiessle.org> - * @author Joas Schilling <coding@schilljs.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Federation\AppInfo; -use OCA\Federation\DAV\FedAuth; -use OCA\Federation\Hooks; -use OCA\Federation\Middleware\AddServerMiddleware; +use OCA\DAV\Events\SabrePluginAuthInitEvent; +use OCA\Federation\Listener\SabrePluginAuthInitListener; use OCP\AppFramework\App; -use OCP\SabrePluginEvent; -use OCP\Util; -use Sabre\DAV\Auth\Plugin; -use Sabre\DAV\Server; +use OCP\AppFramework\Bootstrap\IBootContext; +use OCP\AppFramework\Bootstrap\IBootstrap; +use OCP\AppFramework\Bootstrap\IRegistrationContext; -class Application extends App { +class Application extends App implements IBootstrap { /** * @param array $urlParams */ public function __construct($urlParams = []) { parent::__construct('federation', $urlParams); - $this->registerMiddleware(); } - private function registerMiddleware() { - $container = $this->getContainer(); - $container->registerAlias('AddServerMiddleware', AddServerMiddleware::class); - $container->registerMiddleWare('AddServerMiddleware'); + public function register(IRegistrationContext $context): void { + $context->registerEventListener(SabrePluginAuthInitEvent::class, SabrePluginAuthInitListener::class); } - /** - * listen to federated_share_added hooks to auto-add new servers to the - * list of trusted servers. - */ - public function registerHooks() { - - $container = $this->getContainer(); - $hooksManager = $container->query(Hooks::class); - - Util::connectHook( - 'OCP\Share', - 'federated_share_added', - $hooksManager, - 'addServerHook' - ); - - $dispatcher = $container->getServer()->getEventDispatcher(); - $dispatcher->addListener('OCA\DAV\Connector\Sabre::authInit', function($event) use($container) { - if ($event instanceof SabrePluginEvent) { - $server = $event->getServer(); - if ($server instanceof Server) { - $authPlugin = $server->getPlugin('auth'); - if ($authPlugin instanceof Plugin) { - $authPlugin->addBackend($container->query(FedAuth::class)); - } - } - } - }); + public function boot(IBootContext $context): void { } - } diff --git a/apps/federation/lib/BackgroundJob/GetSharedSecret.php b/apps/federation/lib/BackgroundJob/GetSharedSecret.php index 92bb31e369e..dc57db9fd62 100644 --- a/apps/federation/lib/BackgroundJob/GetSharedSecret.php +++ b/apps/federation/lib/BackgroundJob/GetSharedSecret.php @@ -1,139 +1,63 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - - namespace OCA\Federation\BackgroundJob; use GuzzleHttp\Exception\ClientException; -use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\RequestException; -use GuzzleHttp\Ring\Exception\RingException; -use OC\BackgroundJob\JobList; -use OC\BackgroundJob\Job; -use OCA\Federation\DbHandler; use OCA\Federation\TrustedServers; use OCP\AppFramework\Http; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\IJobList; +use OCP\BackgroundJob\Job; use OCP\Http\Client\IClient; use OCP\Http\Client\IClientService; use OCP\Http\Client\IResponse; -use OCP\ILogger; +use OCP\IConfig; use OCP\IURLGenerator; use OCP\OCS\IDiscoveryService; +use Psr\Log\LoggerInterface; /** * Class GetSharedSecret * - * request shared secret from remote Nextcloud + * Request shared secret from remote Nextcloud * * @package OCA\Federation\Backgroundjob */ class GetSharedSecret extends Job { + private IClient $httpClient; + protected bool $retainJob = false; + private string $defaultEndPoint = '/ocs/v2.php/apps/federation/api/v1/shared-secret'; + /** 30 day = 2592000sec */ + private int $maxLifespan = 2592000; - /** @var IClient */ - private $httpClient; - - /** @var IJobList */ - private $jobList; - - /** @var IURLGenerator */ - private $urlGenerator; - - /** @var TrustedServers */ - private $trustedServers; - - /** @var DbHandler */ - private $dbHandler; - - /** @var IDiscoveryService */ - private $ocsDiscoveryService; - - /** @var ILogger */ - private $logger; - - /** @var ITimeFactory */ - private $timeFactory; - - /** @var bool */ - protected $retainJob = false; - - private $format = '?format=json'; - - private $defaultEndPoint = '/ocs/v2.php/apps/federation/api/v1/shared-secret'; - - /** @var int 30 day = 2592000sec */ - private $maxLifespan = 2592000; - - /** - * RequestSharedSecret constructor. - * - * @param IClientService $httpClientService - * @param IURLGenerator $urlGenerator - * @param IJobList $jobList - * @param TrustedServers $trustedServers - * @param ILogger $logger - * @param DbHandler $dbHandler - * @param IDiscoveryService $ocsDiscoveryService - * @param ITimeFactory $timeFactory - */ public function __construct( IClientService $httpClientService, - IURLGenerator $urlGenerator, - IJobList $jobList, - TrustedServers $trustedServers, - ILogger $logger, - DbHandler $dbHandler, - IDiscoveryService $ocsDiscoveryService, - ITimeFactory $timeFactory + private IURLGenerator $urlGenerator, + private IJobList $jobList, + private TrustedServers $trustedServers, + private LoggerInterface $logger, + private IDiscoveryService $ocsDiscoveryService, + ITimeFactory $timeFactory, + private IConfig $config, ) { - $this->logger = $logger; + parent::__construct($timeFactory); $this->httpClient = $httpClientService->newClient(); - $this->jobList = $jobList; - $this->urlGenerator = $urlGenerator; - $this->dbHandler = $dbHandler; - $this->ocsDiscoveryService = $ocsDiscoveryService; - $this->trustedServers = $trustedServers; - $this->timeFactory = $timeFactory; } /** - * run the job, then remove it from the joblist - * - * @param JobList $jobList - * @param ILogger|null $logger + * Run the job, then remove it from the joblist */ - public function execute($jobList, ILogger $logger = null) { + public function start(IJobList $jobList): void { $target = $this->argument['url']; // only execute if target is still in the list of trusted domains if ($this->trustedServers->isTrustedServer($target)) { - $this->parentExecute($jobList, $logger); + $this->parentStart($jobList); } $jobList->remove($this, $this->argument); @@ -143,20 +67,14 @@ class GetSharedSecret extends Job { } } - /** - * call execute() method of parent - * - * @param JobList $jobList - * @param ILogger $logger - */ - protected function parentExecute($jobList, $logger = null) { - parent::execute($jobList, $logger); + protected function parentStart(IJobList $jobList): void { + parent::start($jobList); } protected function run($argument) { $target = $argument['url']; - $created = isset($argument['created']) ? (int)$argument['created'] : $this->timeFactory->getTime(); - $currentTime = $this->timeFactory->getTime(); + $created = isset($argument['created']) ? (int)$argument['created'] : $this->time->getTime(); + $currentTime = $this->time->getTime(); $source = $this->urlGenerator->getAbsoluteURL('/'); $source = rtrim($source, '/'); $token = $argument['token']; @@ -164,50 +82,52 @@ class GetSharedSecret extends Job { // kill job after 30 days of trying $deadline = $currentTime - $this->maxLifespan; if ($created < $deadline) { + $this->logger->warning("The job to get the shared secret job is too old and gets stopped now without retention. Setting server status of '{$target}' to failure."); $this->retainJob = false; - $this->trustedServers->setServerStatus($target,TrustedServers::STATUS_FAILURE); + $this->trustedServers->setServerStatus($target, TrustedServers::STATUS_FAILURE); return; } $endPoints = $this->ocsDiscoveryService->discover($target, 'FEDERATED_SHARING'); - $endPoint = isset($endPoints['shared-secret']) ? $endPoints['shared-secret'] : $this->defaultEndPoint; + $endPoint = $endPoints['shared-secret'] ?? $this->defaultEndPoint; // make sure that we have a well formatted url - $url = rtrim($target, '/') . '/' . trim($endPoint, '/') . $this->format; + $url = rtrim($target, '/') . '/' . trim($endPoint, '/'); $result = null; try { $result = $this->httpClient->get( $url, [ - 'query' => - [ - 'url' => $source, - 'token' => $token - ], + 'query' => [ + 'url' => $source, + 'token' => $token, + 'format' => 'json', + ], 'timeout' => 3, 'connect_timeout' => 3, + 'verify' => !$this->config->getSystemValue('sharing.federation.allowSelfSignedCertificates', false), ] ); $status = $result->getStatusCode(); - } catch (ClientException $e) { $status = $e->getCode(); if ($status === Http::STATUS_FORBIDDEN) { - $this->logger->info($target . ' refused to exchange a shared secret with you.', ['app' => 'federation']); + $this->logger->info($target . ' refused to exchange a shared secret with you.'); } else { - $this->logger->info($target . ' responded with a ' . $status . ' containing: ' . $e->getMessage(), ['app' => 'federation']); + $this->logger->info($target . ' responded with a ' . $status . ' containing: ' . $e->getMessage()); } } catch (RequestException $e) { $status = -1; // There is no status code if we could not connect - $this->logger->info('Could not connect to ' . $target, ['app' => 'federation']); - } catch (RingException $e) { - $status = -1; // There is no status code if we could not connect - $this->logger->info('Could not connect to ' . $target, ['app' => 'federation']); - } catch (\Exception $e) { + $this->logger->info('Could not connect to ' . $target, [ + 'exception' => $e, + ]); + } catch (\Throwable $e) { $status = Http::STATUS_INTERNAL_SERVER_ERROR; - $this->logger->logException($e, ['app' => 'federation']); + $this->logger->error($e->getMessage(), [ + 'exception' => $e, + ]); } // if we received a unexpected response we try again later @@ -216,9 +136,6 @@ class GetSharedSecret extends Job { && $status !== Http::STATUS_FORBIDDEN ) { $this->retainJob = true; - } else { - // reset token if we received a valid response - $this->dbHandler->addToken($target, ''); } if ($status === Http::STATUS_OK && $result instanceof IResponse) { @@ -226,28 +143,26 @@ class GetSharedSecret extends Job { $result = json_decode($body, true); if (isset($result['ocs']['data']['sharedSecret'])) { $this->trustedServers->addSharedSecret( - $target, - $result['ocs']['data']['sharedSecret'] + $target, + $result['ocs']['data']['sharedSecret'] ); } else { $this->logger->error( - 'remote server "' . $target . '"" does not return a valid shared secret', - ['app' => 'federation'] + 'remote server "' . $target . '"" does not return a valid shared secret. Received data: ' . $body ); $this->trustedServers->setServerStatus($target, TrustedServers::STATUS_FAILURE); } } - } /** - * re-add background job + * Re-add background job * * @param array $argument */ - protected function reAddJob(array $argument) { + protected function reAddJob(array $argument): void { $url = $argument['url']; - $created = isset($argument['created']) ? (int)$argument['created'] : $this->timeFactory->getTime(); + $created = $argument['created'] ?? $this->time->getTime(); $token = $argument['token']; $this->jobList->add( GetSharedSecret::class, diff --git a/apps/federation/lib/BackgroundJob/RequestSharedSecret.php b/apps/federation/lib/BackgroundJob/RequestSharedSecret.php index ad7504da7ad..4d57d1f6aef 100644 --- a/apps/federation/lib/BackgroundJob/RequestSharedSecret.php +++ b/apps/federation/lib/BackgroundJob/RequestSharedSecret.php @@ -1,52 +1,27 @@ <?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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/> - * - */ +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ namespace OCA\Federation\BackgroundJob; - use GuzzleHttp\Exception\ClientException; -use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\RequestException; -use GuzzleHttp\Ring\Exception\RingException; -use OC\BackgroundJob\JobList; -use OC\BackgroundJob\Job; -use OCA\Federation\DbHandler; use OCA\Federation\TrustedServers; use OCP\AppFramework\Http; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\IJobList; +use OCP\BackgroundJob\Job; use OCP\Http\Client\IClient; use OCP\Http\Client\IClientService; -use OCP\ILogger; +use OCP\IConfig; use OCP\IURLGenerator; use OCP\OCS\IDiscoveryService; +use Psr\Log\LoggerInterface; /** * Class RequestSharedSecret @@ -56,85 +31,38 @@ use OCP\OCS\IDiscoveryService; * @package OCA\Federation\Backgroundjob */ class RequestSharedSecret extends Job { + private IClient $httpClient; - /** @var IClient */ - private $httpClient; - - /** @var IJobList */ - private $jobList; - - /** @var IURLGenerator */ - private $urlGenerator; - - /** @var DbHandler */ - private $dbHandler; - - /** @var TrustedServers */ - private $trustedServers; - - /** @var IDiscoveryService */ - private $ocsDiscoveryService; - - /** @var ILogger */ - private $logger; + protected bool $retainJob = false; - /** @var ITimeFactory */ - private $timeFactory; + private string $defaultEndPoint = '/ocs/v2.php/apps/federation/api/v1/request-shared-secret'; - /** @var bool */ - protected $retainJob = false; + /** @var int 30 day = 2592000sec */ + private int $maxLifespan = 2592000; - private $format = '?format=json'; - - private $defaultEndPoint = '/ocs/v2.php/apps/federation/api/v1/request-shared-secret'; - - /** @var int 30 day = 2592000sec */ - private $maxLifespan = 2592000; - - /** - * RequestSharedSecret constructor. - * - * @param IClientService $httpClientService - * @param IURLGenerator $urlGenerator - * @param IJobList $jobList - * @param TrustedServers $trustedServers - * @param DbHandler $dbHandler - * @param IDiscoveryService $ocsDiscoveryService - * @param ILogger $logger - * @param ITimeFactory $timeFactory - */ public function __construct( IClientService $httpClientService, - IURLGenerator $urlGenerator, - IJobList $jobList, - TrustedServers $trustedServers, - DbHandler $dbHandler, - IDiscoveryService $ocsDiscoveryService, - ILogger $logger, - ITimeFactory $timeFactory + private IURLGenerator $urlGenerator, + private IJobList $jobList, + private TrustedServers $trustedServers, + private IDiscoveryService $ocsDiscoveryService, + private LoggerInterface $logger, + ITimeFactory $timeFactory, + private IConfig $config, ) { + parent::__construct($timeFactory); $this->httpClient = $httpClientService->newClient(); - $this->jobList = $jobList; - $this->urlGenerator = $urlGenerator; - $this->dbHandler = $dbHandler; - $this->logger = $logger; - $this->ocsDiscoveryService = $ocsDiscoveryService; - $this->trustedServers = $trustedServers; - $this->timeFactory = $timeFactory; } /** * run the job, then remove it from the joblist - * - * @param JobList $jobList - * @param ILogger|null $logger */ - public function execute($jobList, ILogger $logger = null) { + public function start(IJobList $jobList): void { $target = $this->argument['url']; // only execute if target is still in the list of trusted domains if ($this->trustedServers->isTrustedServer($target)) { - $this->parentExecute($jobList, $logger); + $this->parentStart($jobList); } $jobList->remove($this, $this->argument); @@ -145,20 +73,21 @@ class RequestSharedSecret extends Job { } /** - * call execute() method of parent - * - * @param JobList $jobList - * @param ILogger $logger + * Call start() method of parent + * Useful for unit tests */ - protected function parentExecute($jobList, $logger) { - parent::execute($jobList, $logger); + protected function parentStart(IJobList $jobList): void { + parent::start($jobList); } + /** + * @param array $argument + * @return void + */ protected function run($argument) { - $target = $argument['url']; - $created = isset($argument['created']) ? (int)$argument['created'] : $this->timeFactory->getTime(); - $currentTime = $this->timeFactory->getTime(); + $created = isset($argument['created']) ? (int)$argument['created'] : $this->time->getTime(); + $currentTime = $this->time->getTime(); $source = $this->urlGenerator->getAbsoluteURL('/'); $source = rtrim($source, '/'); $token = $argument['token']; @@ -166,16 +95,17 @@ class RequestSharedSecret extends Job { // kill job after 30 days of trying $deadline = $currentTime - $this->maxLifespan; if ($created < $deadline) { + $this->logger->warning("The job to request the shared secret job is too old and gets stopped now without retention. Setting server status of '{$target}' to failure."); $this->retainJob = false; $this->trustedServers->setServerStatus($target, TrustedServers::STATUS_FAILURE); return; } $endPoints = $this->ocsDiscoveryService->discover($target, 'FEDERATED_SHARING'); - $endPoint = isset($endPoints['shared-secret']) ? $endPoints['shared-secret'] : $this->defaultEndPoint; + $endPoint = $endPoints['shared-secret'] ?? $this->defaultEndPoint; - // make sure that we have a well formated url - $url = rtrim($target, '/') . '/' . trim($endPoint, '/') . $this->format; + // make sure that we have a well formatted url + $url = rtrim($target, '/') . '/' . trim($endPoint, '/'); try { $result = $this->httpClient->post( @@ -184,64 +114,60 @@ class RequestSharedSecret extends Job { 'body' => [ 'url' => $source, 'token' => $token, + 'format' => 'json', ], 'timeout' => 3, 'connect_timeout' => 3, + 'verify' => !$this->config->getSystemValue('sharing.federation.allowSelfSignedCertificates', false), ] ); $status = $result->getStatusCode(); - } catch (ClientException $e) { $status = $e->getCode(); if ($status === Http::STATUS_FORBIDDEN) { - $this->logger->info($target . ' refused to ask for a shared secret.', ['app' => 'federation']); + $this->logger->info($target . ' refused to ask for a shared secret.'); } else { - $this->logger->info($target . ' responded with a ' . $status . ' containing: ' . $e->getMessage(), ['app' => 'federation']); + $this->logger->info($target . ' responded with a ' . $status . ' containing: ' . $e->getMessage()); } } catch (RequestException $e) { $status = -1; // There is no status code if we could not connect - $this->logger->info('Could not connect to ' . $target, ['app' => 'federation']); - } catch (RingException $e) { - $status = -1; // There is no status code if we could not connect - $this->logger->info('Could not connect to ' . $target, ['app' => 'federation']); - } catch (\Exception $e) { + $this->logger->info('Could not connect to ' . $target); + } catch (\Throwable $e) { $status = Http::STATUS_INTERNAL_SERVER_ERROR; - $this->logger->logException($e, ['app' => 'federation']); + $this->logger->error($e->getMessage(), ['exception' => $e]); } // if we received a unexpected response we try again later if ( $status !== Http::STATUS_OK - && $status !== Http::STATUS_FORBIDDEN + && ($status !== Http::STATUS_FORBIDDEN || $this->getAttempt($argument) < 5) ) { $this->retainJob = true; } - - if ($status === Http::STATUS_FORBIDDEN) { - // clear token if remote server refuses to ask for shared secret - $this->dbHandler->addToken($target, ''); - } - } /** * re-add background job - * - * @param array $argument */ - protected function reAddJob(array $argument) { + protected function reAddJob(array $argument): void { $url = $argument['url']; - $created = isset($argument['created']) ? (int)$argument['created'] : $this->timeFactory->getTime(); + $created = isset($argument['created']) ? (int)$argument['created'] : $this->time->getTime(); $token = $argument['token']; + $attempt = $this->getAttempt($argument) + 1; $this->jobList->add( RequestSharedSecret::class, [ 'url' => $url, 'token' => $token, - 'created' => $created + 'created' => $created, + 'attempt' => $attempt ] ); } + + protected function getAttempt(array $argument): int { + return $argument['attempt'] ?? 0; + } } diff --git a/apps/federation/lib/Command/SyncFederationAddressBooks.php b/apps/federation/lib/Command/SyncFederationAddressBooks.php index 3b1f95b0502..36cb99473f7 100644 --- a/apps/federation/lib/Command/SyncFederationAddressBooks.php +++ b/apps/federation/lib/Command/SyncFederationAddressBooks.php @@ -1,46 +1,23 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Björn Schießle <bjoern@schiessle.org> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Federation\Command; +use OCA\Federation\SyncFederationAddressBooks as SyncService; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class SyncFederationAddressBooks extends Command { - - /** @var \OCA\Federation\SyncFederationAddressBooks */ - private $syncService; - - /** - * @param \OCA\Federation\SyncFederationAddressBooks $syncService - */ - public function __construct(\OCA\Federation\SyncFederationAddressBooks $syncService) { + public function __construct( + private SyncService $syncService, + ) { parent::__construct(); - - $this->syncService = $syncService; } protected function configure() { @@ -49,19 +26,12 @@ class SyncFederationAddressBooks extends Command { ->setDescription('Synchronizes addressbooks of all federated clouds'); } - /** - * @param InputInterface $input - * @param OutputInterface $output - * @return int - */ - protected function execute(InputInterface $input, OutputInterface $output) { - + protected function execute(InputInterface $input, OutputInterface $output): int { $progress = new ProgressBar($output); $progress->start(); - $this->syncService->syncThemAll(function($url, $ex) use ($progress, $output) { + $this->syncService->syncThemAll(function ($url, $ex) use ($progress, $output): void { if ($ex instanceof \Exception) { $output->writeln("Error while syncing $url : " . $ex->getMessage()); - } else { $progress->advance(); } diff --git a/apps/federation/lib/Controller/OCSAuthAPIController.php b/apps/federation/lib/Controller/OCSAuthAPIController.php index a1284a4e3ad..16b401be251 100644 --- a/apps/federation/lib/Controller/OCSAuthAPIController.php +++ b/apps/federation/lib/Controller/OCSAuthAPIController.php @@ -1,44 +1,28 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - - namespace OCA\Federation\Controller; use OCA\Federation\DbHandler; use OCA\Federation\TrustedServers; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\BruteForceProtection; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSForbiddenException; use OCP\AppFramework\OCSController; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\IJobList; -use OCP\ILogger; use OCP\IRequest; +use OCP\Security\Bruteforce\IThrottler; use OCP\Security\ISecureRandom; +use Psr\Log\LoggerInterface; /** * Class OCSAuthAPI @@ -47,103 +31,74 @@ use OCP\Security\ISecureRandom; * * @package OCA\Federation\Controller */ -class OCSAuthAPIController extends OCSController{ - - /** @var ISecureRandom */ - private $secureRandom; - - /** @var IJobList */ - private $jobList; - - /** @var TrustedServers */ - private $trustedServers; - - /** @var DbHandler */ - private $dbHandler; - - /** @var ILogger */ - private $logger; - - /** @var ITimeFactory */ - private $timeFactory; - - /** - * OCSAuthAPI constructor. - * - * @param string $appName - * @param IRequest $request - * @param ISecureRandom $secureRandom - * @param IJobList $jobList - * @param TrustedServers $trustedServers - * @param DbHandler $dbHandler - * @param ILogger $logger - * @param ITimeFactory $timeFactory - */ +#[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)] +class OCSAuthAPIController extends OCSController { public function __construct( - $appName, + string $appName, IRequest $request, - ISecureRandom $secureRandom, - IJobList $jobList, - TrustedServers $trustedServers, - DbHandler $dbHandler, - ILogger $logger, - ITimeFactory $timeFactory + private ISecureRandom $secureRandom, + private IJobList $jobList, + private TrustedServers $trustedServers, + private DbHandler $dbHandler, + private LoggerInterface $logger, + private ITimeFactory $timeFactory, + private IThrottler $throttler, ) { parent::__construct($appName, $request); - - $this->secureRandom = $secureRandom; - $this->jobList = $jobList; - $this->trustedServers = $trustedServers; - $this->dbHandler = $dbHandler; - $this->logger = $logger; - $this->timeFactory = $timeFactory; } /** - * @NoCSRFRequired - * @PublicPage + * Request received to ask remote server for a shared secret, for legacy end-points * - * request received to ask remote server for a shared secret, for legacy end-points + * @param string $url URL of the server + * @param string $token Token of the server + * @return DataResponse<Http::STATUS_OK, list<empty>, array{}> + * @throws OCSForbiddenException Requesting shared secret is not allowed * - * @param string $url - * @param string $token - * @return Http\DataResponse - * @throws OCSForbiddenException + * 200: Shared secret requested successfully */ - public function requestSharedSecretLegacy($url, $token) { + #[NoCSRFRequired] + #[PublicPage] + #[BruteForceProtection(action: 'federationSharedSecret')] + public function requestSharedSecretLegacy(string $url, string $token): DataResponse { return $this->requestSharedSecret($url, $token); } /** - * @NoCSRFRequired - * @PublicPage + * Create shared secret and return it, for legacy end-points * - * create shared secret and return it, for legacy end-points + * @param string $url URL of the server + * @param string $token Token of the server + * @return DataResponse<Http::STATUS_OK, array{sharedSecret: string}, array{}> + * @throws OCSForbiddenException Getting shared secret is not allowed * - * @param string $url - * @param string $token - * @return Http\DataResponse - * @throws OCSForbiddenException + * 200: Shared secret returned */ - public function getSharedSecretLegacy($url, $token) { + #[NoCSRFRequired] + #[PublicPage] + #[BruteForceProtection(action: 'federationSharedSecret')] + public function getSharedSecretLegacy(string $url, string $token): DataResponse { return $this->getSharedSecret($url, $token); } /** - * @NoCSRFRequired - * @PublicPage + * Request received to ask remote server for a shared secret * - * request received to ask remote server for a shared secret + * @param string $url URL of the server + * @param string $token Token of the server + * @return DataResponse<Http::STATUS_OK, list<empty>, array{}> + * @throws OCSForbiddenException Requesting shared secret is not allowed * - * @param string $url - * @param string $token - * @return Http\DataResponse - * @throws OCSForbiddenException + * 200: Shared secret requested successfully */ - public function requestSharedSecret($url, $token) { + #[NoCSRFRequired] + #[PublicPage] + #[BruteForceProtection(action: 'federationSharedSecret')] + public function requestSharedSecret(string $url, string $token): DataResponse { if ($this->trustedServers->isTrustedServer($url) === false) { - $this->logger->error('remote server not trusted (' . $url . ') while requesting shared secret', ['app' => 'federation']); + $this->throttler->registerAttempt('federationSharedSecret', $this->request->getRemoteAddress()); + $this->logger->error('remote server not trusted (' . $url . ') while requesting shared secret'); throw new OCSForbiddenException(); } @@ -152,8 +107,7 @@ class OCSAuthAPIController extends OCSController{ $localToken = $this->dbHandler->getToken($url); if (strcmp($localToken, $token) > 0) { $this->logger->info( - 'remote server (' . $url . ') presented lower token. We will initiate the exchange of the shared secret.', - ['app' => 'federation'] + 'remote server (' . $url . ') presented lower token. We will initiate the exchange of the shared secret.' ); throw new OCSForbiddenException(); } @@ -167,31 +121,34 @@ class OCSAuthAPIController extends OCSController{ ] ); - return new Http\DataResponse(); + return new DataResponse(); } /** - * @NoCSRFRequired - * @PublicPage + * Create shared secret and return it * - * create shared secret and return it + * @param string $url URL of the server + * @param string $token Token of the server + * @return DataResponse<Http::STATUS_OK, array{sharedSecret: string}, array{}> + * @throws OCSForbiddenException Getting shared secret is not allowed * - * @param string $url - * @param string $token - * @return Http\DataResponse - * @throws OCSForbiddenException + * 200: Shared secret returned */ - public function getSharedSecret($url, $token) { + #[NoCSRFRequired] + #[PublicPage] + #[BruteForceProtection(action: 'federationSharedSecret')] + public function getSharedSecret(string $url, string $token): DataResponse { if ($this->trustedServers->isTrustedServer($url) === false) { - $this->logger->error('remote server not trusted (' . $url . ') while getting shared secret', ['app' => 'federation']); + $this->throttler->registerAttempt('federationSharedSecret', $this->request->getRemoteAddress()); + $this->logger->error('remote server not trusted (' . $url . ') while getting shared secret'); throw new OCSForbiddenException(); } if ($this->isValidToken($url, $token) === false) { + $this->throttler->registerAttempt('federationSharedSecret', $this->request->getRemoteAddress()); $expectedToken = $this->dbHandler->getToken($url); $this->logger->error( - 'remote server (' . $url . ') didn\'t send a valid token (got "' . $token . '" but expected "'. $expectedToken . '") while getting shared secret', - ['app' => 'federation'] + 'remote server (' . $url . ') didn\'t send a valid token (got "' . $token . '" but expected "' . $expectedToken . '") while getting shared secret' ); throw new OCSForbiddenException(); } @@ -199,15 +156,13 @@ class OCSAuthAPIController extends OCSController{ $sharedSecret = $this->secureRandom->generate(32); $this->trustedServers->addSharedSecret($url, $sharedSecret); - // reset token after the exchange of the shared secret was successful - $this->dbHandler->addToken($url, ''); - return new Http\DataResponse([ + return new DataResponse([ 'sharedSecret' => $sharedSecret ]); } - protected function isValidToken($url, $token) { + protected function isValidToken(string $url, string $token): bool { $storedToken = $this->dbHandler->getToken($url); return hash_equals($storedToken, $token); } diff --git a/apps/federation/lib/Controller/SettingsController.php b/apps/federation/lib/Controller/SettingsController.php index 6e64200dc8c..27341eba815 100644 --- a/apps/federation/lib/Controller/SettingsController.php +++ b/apps/federation/lib/Controller/SettingsController.php @@ -1,124 +1,125 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Federation\Controller; -use OC\HintException; +use OCA\Federation\Settings\Admin; use OCA\Federation\TrustedServers; -use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\AppFramework\OCSController; use OCP\IL10N; use OCP\IRequest; - - -class SettingsController extends Controller { - - /** @var IL10N */ - private $l; - - /** @var TrustedServers */ - private $trustedServers; - - /** - * @param string $AppName - * @param IRequest $request - * @param IL10N $l10n - * @param TrustedServers $trustedServers - */ - public function __construct($AppName, - IRequest $request, - IL10N $l10n, - TrustedServers $trustedServers +use Psr\Log\LoggerInterface; + +class SettingsController extends OCSController { + public function __construct( + string $AppName, + IRequest $request, + private IL10N $l, + private TrustedServers $trustedServers, + private LoggerInterface $logger, ) { parent::__construct($AppName, $request); - $this->l = $l10n; - $this->trustedServers = $trustedServers; } /** - * add server to the list of trusted Nextclouds + * Add server to the list of trusted Nextcloud servers + * + * @param string $url The URL of the server to add + * @return DataResponse<Http::STATUS_OK, array{id: int, message: string, url: string}, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_CONFLICT, array{message: string}, array{}> * - * @param string $url - * @return DataResponse - * @throws HintException + * 200: Server added successfully + * 404: Server not found at the given URL + * 409: Server is already in the list of trusted servers */ - public function addServer($url) { - $this->checkServer($url); - $id = $this->trustedServers->addServer($url); - - return new DataResponse( - [ - 'url' => $url, - 'id' => $id, - 'message' => (string) $this->l->t('Added to the list of trusted servers') - ] - ); + #[AuthorizedAdminSetting(settings: Admin::class)] + #[ApiRoute(verb: 'POST', url: '/trusted-servers')] + public function addServer(string $url): DataResponse { + $this->checkServer(trim($url)); + + // Add the server to the list of trusted servers, all is well + $id = $this->trustedServers->addServer(trim($url)); + return new DataResponse([ + 'url' => $url, + 'id' => $id, + 'message' => $this->l->t('Added to the list of trusted servers') + ]); } /** - * add server to the list of trusted Nextclouds + * Add server to the list of trusted Nextcloud servers + * + * @param int $id The ID of the trusted server to remove + * @return DataResponse<Http::STATUS_OK, array{id: int}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{message: string}, array{}> * - * @param int $id - * @return DataResponse + * 200: Server removed successfully + * 404: Server not found at the given ID */ - public function removeServer($id) { - $this->trustedServers->removeServer($id); - return new DataResponse(); + #[AuthorizedAdminSetting(settings: Admin::class)] + #[ApiRoute(verb: 'DELETE', url: '/trusted-servers/{id}', requirements: ['id' => '\d+'])] + public function removeServer(int $id): DataResponse { + try { + $this->trustedServers->getServer($id); + } catch (\Exception $e) { + throw new OCSNotFoundException($this->l->t('No server found with ID: %s', [$id])); + } + + try { + $this->trustedServers->removeServer($id); + return new DataResponse(['id' => $id]); + } catch (\Exception $e) { + $this->logger->error($e->getMessage(), ['e' => $e]); + throw new OCSException($this->l->t('Could not remove server'), Http::STATUS_INTERNAL_SERVER_ERROR); + } } /** - * enable/disable to automatically add servers to the list of trusted servers - * once a federated share was created and accepted successfully + * List all trusted servers * - * @param bool $autoAddServers + * @return DataResponse<Http::STATUS_OK, list<array{id: int, status: int, url: string}>, array{}> + * + * 200: List of trusted servers */ - public function autoAddServers($autoAddServers) { - $this->trustedServers->setAutoAddServers($autoAddServers); + #[AuthorizedAdminSetting(settings: Admin::class)] + #[ApiRoute(verb: 'GET', url: '/trusted-servers')] + public function getServers(): DataResponse { + $servers = $this->trustedServers->getServers(); + + // obfuscate the shared secret + $servers = array_map(function ($server) { + return [ + 'url' => $server['url'], + 'id' => $server['id'], + 'status' => $server['status'], + ]; + }, $servers); + + // return the list of trusted servers + return new DataResponse($servers); } + /** - * check if the server should be added to the list of trusted servers or not - * - * @param string $url - * @return bool - * @throws HintException + * Check if the server should be added to the list of trusted servers or not. */ - protected function checkServer($url) { + #[AuthorizedAdminSetting(settings: Admin::class)] + protected function checkServer(string $url): void { if ($this->trustedServers->isTrustedServer($url) === true) { - $message = 'Server is already in the list of trusted servers.'; - $hint = $this->l->t('Server is already in the list of trusted servers.'); - throw new HintException($message, $hint); + throw new OCSException($this->l->t('Server is already in the list of trusted servers.'), Http::STATUS_CONFLICT); } - if ($this->trustedServers->isOwnCloudServer($url) === false) { - $message = 'No server to federate with found'; - $hint = $this->l->t('No server to federate with found'); - throw new HintException($message, $hint); + if ($this->trustedServers->isNextcloudServer($url) === false) { + throw new OCSNotFoundException($this->l->t('No server to federate with found')); } - - return true; } - } diff --git a/apps/federation/lib/DAV/FedAuth.php b/apps/federation/lib/DAV/FedAuth.php index 511888f7683..45bf422c104 100644 --- a/apps/federation/lib/DAV/FedAuth.php +++ b/apps/federation/lib/DAV/FedAuth.php @@ -1,48 +1,32 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Federation\DAV; use OCA\Federation\DbHandler; +use OCP\Defaults; use Sabre\DAV\Auth\Backend\AbstractBasic; use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; class FedAuth extends AbstractBasic { - /** @var DbHandler */ - private $db; - /** * FedAuth constructor. * * @param DbHandler $db */ - public function __construct(DbHandler $db) { - $this->db = $db; + public function __construct( + private DbHandler $db, + ) { $this->principalPrefix = 'principals/system/'; // setup realm - $defaults = new \OCP\Defaults(); + $defaults = new Defaults(); $this->realm = $defaults->getName(); } diff --git a/apps/federation/lib/DbHandler.php b/apps/federation/lib/DbHandler.php index abfb4c2f1b9..877663b058a 100644 --- a/apps/federation/lib/DbHandler.php +++ b/apps/federation/lib/DbHandler.php @@ -1,128 +1,93 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Björn Schießle <bjoern@schiessle.org> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors* + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - - namespace OCA\Federation; - use OC\Files\Filesystem; -use OC\HintException; +use OCP\DB\Exception as DBException; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\HintException; use OCP\IDBConnection; use OCP\IL10N; /** * Class DbHandler * - * handles all database calls for the federation app + * Handles all database calls for the federation app + * + * @todo Port to QBMapper * * @group DB * @package OCA\Federation */ class DbHandler { + private string $dbTable = 'trusted_servers'; - /** @var IDBConnection */ - private $connection; - - /** @var IL10N */ - private $IL10N; - - /** @var string */ - private $dbTable = 'trusted_servers'; - - /** - * @param IDBConnection $connection - * @param IL10N $il10n - */ public function __construct( - IDBConnection $connection, - IL10N $il10n + private IDBConnection $connection, + private IL10N $IL10N, ) { - $this->connection = $connection; - $this->IL10N = $il10n; } /** - * add server to the list of trusted servers + * Add server to the list of trusted servers * - * @param string $url - * @return int * @throws HintException */ - public function addServer($url) { + public function addServer(string $url): int { $hash = $this->hash($url); $url = rtrim($url, '/'); $query = $this->connection->getQueryBuilder(); $query->insert($this->dbTable) - ->values( - [ - 'url' => $query->createParameter('url'), - 'url_hash' => $query->createParameter('url_hash'), - ] - ) + ->values([ + 'url' => $query->createParameter('url'), + 'url_hash' => $query->createParameter('url_hash'), + ]) ->setParameter('url', $url) ->setParameter('url_hash', $hash); - $result = $query->execute(); + $result = $query->executeStatement(); if ($result) { - return (int)$this->connection->lastInsertId('*PREFIX*'.$this->dbTable); + return $query->getLastInsertId(); } $message = 'Internal failure, Could not add trusted server: ' . $url; $message_t = $this->IL10N->t('Could not add server'); throw new HintException($message, $message_t); + return -1; } /** - * remove server from the list of trusted servers - * - * @param int $id + * Remove server from the list of trusted servers */ - public function removeServer($id) { + public function removeServer(int $id): void { $query = $this->connection->getQueryBuilder(); $query->delete($this->dbTable) ->where($query->expr()->eq('id', $query->createParameter('id'))) ->setParameter('id', $id); - $query->execute(); + $query->executeStatement(); } /** - * get trusted server with given ID + * Get trusted server with given ID * - * @param int $id - * @return array + * @return array{id: int, url: string, url_hash: string, token: ?string, shared_secret: ?string, status: int, sync_token: ?string} * @throws \Exception */ - public function getServerById($id) { + public function getServerById(int $id): array { $query = $this->connection->getQueryBuilder(); $query->select('*')->from($this->dbTable) ->where($query->expr()->eq('id', $query->createParameter('id'))) - ->setParameter('id', $id); - $query->execute(); - $result = $query->execute()->fetchAll(); + ->setParameter('id', $id, IQueryBuilder::PARAM_INT); + + $qResult = $query->executeQuery(); + $result = $qResult->fetchAll(); + $qResult->closeCursor(); if (empty($result)) { throw new \Exception('No Server found with ID: ' . $id); @@ -132,34 +97,32 @@ class DbHandler { } /** - * get all trusted servers + * Get all trusted servers * - * @return array + * @return list<array{id: int, url: string, url_hash: string, shared_secret: ?string, status: int, sync_token: ?string}> + * @throws DBException */ - public function getAllServer() { + public function getAllServer(): array { $query = $this->connection->getQueryBuilder(); $query->select(['url', 'url_hash', 'id', 'status', 'shared_secret', 'sync_token']) ->from($this->dbTable); - $statement = $query->execute(); + $statement = $query->executeQuery(); $result = $statement->fetchAll(); $statement->closeCursor(); return $result; } /** - * check if server already exists in the database table - * - * @param string $url - * @return bool + * Check if server already exists in the database table */ - public function serverExists($url) { + public function serverExists(string $url): bool { $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', $hash); - $statement = $query->execute(); + $statement = $query->executeQuery(); $result = $statement->fetchAll(); $statement->closeCursor(); @@ -167,12 +130,9 @@ class DbHandler { } /** - * write token to database. Token is used to exchange the secret - * - * @param string $url - * @param string $token + * Write token to database. Token is used to exchange the secret */ - public function addToken($url, $token) { + public function addToken(string $url, string $token): void { $hash = $this->hash($url); $query = $this->connection->getQueryBuilder(); $query->update($this->dbTable) @@ -180,24 +140,21 @@ class DbHandler { ->where($query->expr()->eq('url_hash', $query->createParameter('url_hash'))) ->setParameter('url_hash', $hash) ->setParameter('token', $token); - $query->execute(); + $query->executeStatement(); } /** - * get token stored in database - * - * @param string $url - * @return string + * Get token stored in database * @throws \Exception */ - public function getToken($url) { + public function getToken(string $url): string { $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); - $statement = $query->execute(); + $statement = $query->executeQuery(); $result = $statement->fetch(); $statement->closeCursor(); @@ -209,12 +166,9 @@ class DbHandler { } /** - * add shared Secret to database - * - * @param string $url - * @param string $sharedSecret + * Add shared Secret to database */ - public function addSharedSecret($url, $sharedSecret) { + public function addSharedSecret(string $url, string $sharedSecret): void { $hash = $this->hash($url); $query = $this->connection->getQueryBuilder(); $query->update($this->dbTable) @@ -222,89 +176,73 @@ class DbHandler { ->where($query->expr()->eq('url_hash', $query->createParameter('url_hash'))) ->setParameter('url_hash', $hash) ->setParameter('sharedSecret', $sharedSecret); - $query->execute(); + $query->executeStatement(); } /** - * get shared secret from database - * - * @param string $url - * @return string + * Get shared secret from database */ - public function getSharedSecret($url) { + public function getSharedSecret(string $url): string { $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); - $statement = $query->execute(); + $statement = $query->executeQuery(); $result = $statement->fetch(); $statement->closeCursor(); - return $result['shared_secret']; + return (string)$result['shared_secret']; } /** - * set server status - * - * @param string $url - * @param int $status - * @param string|null $token + * Set server status */ - public function setServerStatus($url, $status, $token = null) { + public function setServerStatus(string $url, int $status, ?string $token = null): void { $hash = $this->hash($url); $query = $this->connection->getQueryBuilder(); $query->update($this->dbTable) - ->set('status', $query->createNamedParameter($status)) - ->where($query->expr()->eq('url_hash', $query->createNamedParameter($hash))); + ->set('status', $query->createNamedParameter($status)) + ->where($query->expr()->eq('url_hash', $query->createNamedParameter($hash))); if (!is_null($token)) { $query->set('sync_token', $query->createNamedParameter($token)); } - $query->execute(); + $query->executeStatement(); } /** - * get server status - * - * @param string $url - * @return int + * Get server status */ - public function getServerStatus($url) { + public function getServerStatus(string $url): int { $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); + ->where($query->expr()->eq('url_hash', $query->createParameter('url_hash'))) + ->setParameter('url_hash', $hash); - $statement = $query->execute(); + $statement = $query->executeQuery(); $result = $statement->fetch(); $statement->closeCursor(); return (int)$result['status']; } /** - * create hash from URL - * - * @param string $url - * @return string + * Create hash from URL */ - protected function hash($url) { + protected function hash(string $url): string { $normalized = $this->normalizeUrl($url); return sha1($normalized); } /** - * normalize URL, used to create the sha1 hash - * - * @param string $url - * @return string + * Normalize URL, used to create the sha1 hash */ - protected function normalizeUrl($url) { + protected function normalizeUrl(string $url): string { $normalized = $url; if (strpos($url, 'https://') === 0) { $normalized = substr($url, strlen('https://')); - } else if (strpos($url, 'http://') === 0) { + } elseif (strpos($url, 'http://') === 0) { $normalized = substr($url, strlen('http://')); } @@ -314,23 +252,17 @@ class DbHandler { return $normalized; } - /** - * @param $username - * @param $password - * @return bool - */ - public function auth($username, $password) { + public function auth(string $username, string $password): bool { if ($username !== 'system') { return false; } $query = $this->connection->getQueryBuilder(); $query->select('url')->from($this->dbTable) - ->where($query->expr()->eq('shared_secret', $query->createNamedParameter($password))); + ->where($query->expr()->eq('shared_secret', $query->createNamedParameter($password))); - $statement = $query->execute(); + $statement = $query->executeQuery(); $result = $statement->fetch(); $statement->closeCursor(); return !empty($result); } - } diff --git a/apps/federation/lib/Hooks.php b/apps/federation/lib/Hooks.php deleted file mode 100644 index 6601b9c0eb7..00000000000 --- a/apps/federation/lib/Hooks.php +++ /dev/null @@ -1,51 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Björn Schießle <bjoern@schiessle.org> - * - * @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; - - - -class Hooks { - - /** @var TrustedServers */ - private $trustedServers; - - public function __construct(TrustedServers $trustedServers) { - $this->trustedServers = $trustedServers; - } - - /** - * add servers to the list of trusted servers once a federated share was established - * - * @param array $params - */ - public function addServerHook($params) { - if ( - $this->trustedServers->getAutoAddServers() === true && - $this->trustedServers->isTrustedServer($params['server']) === false - ) { - $this->trustedServers->addServer($params['server']); - } - } - -} diff --git a/apps/federation/lib/Listener/SabrePluginAuthInitListener.php b/apps/federation/lib/Listener/SabrePluginAuthInitListener.php new file mode 100644 index 00000000000..514a893fb39 --- /dev/null +++ b/apps/federation/lib/Listener/SabrePluginAuthInitListener.php @@ -0,0 +1,38 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Federation\Listener; + +use OCA\DAV\Events\SabrePluginAuthInitEvent; +use OCA\Federation\DAV\FedAuth; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use Sabre\DAV\Auth\Plugin; + +/** + * @since 20.0.0 + * @template-implements IEventListener<SabrePluginAuthInitEvent> + */ +class SabrePluginAuthInitListener implements IEventListener { + public function __construct( + private FedAuth $fedAuth, + ) { + } + + public function handle(Event $event): void { + if (!($event instanceof SabrePluginAuthInitEvent)) { + return; + } + + $server = $event->getServer(); + $authPlugin = $server->getPlugin('auth'); + if ($authPlugin instanceof Plugin) { + $authPlugin->addBackend($this->fedAuth); + } + } +} diff --git a/apps/federation/lib/Middleware/AddServerMiddleware.php b/apps/federation/lib/Middleware/AddServerMiddleware.php deleted file mode 100644 index 247cc958833..00000000000 --- a/apps/federation/lib/Middleware/AddServerMiddleware.php +++ /dev/null @@ -1,88 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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\Middleware; - -use OC\HintException; -use OCA\Federation\Controller\SettingsController; -use OCP\AppFramework\Controller; -use OCP\AppFramework\Http; -use OCP\AppFramework\Http\JSONResponse; -use OCP\AppFramework\Middleware; -use OCP\IL10N; -use OCP\ILogger; - -class AddServerMiddleware extends Middleware { - - /** @var string */ - protected $appName; - - /** @var IL10N */ - protected $l; - - /** @var ILogger */ - protected $logger; - - /** - * @param string $appName - * @param IL10N $l - * @param ILogger $logger - */ - public function __construct($appName, IL10N $l, ILogger $logger) { - $this->appName = $appName; - $this->l = $l; - $this->logger = $logger; - } - - /** - * Log error message and return a response which can be displayed to the user - * - * @param Controller $controller - * @param string $methodName - * @param \Exception $exception - * @return JSONResponse - * @throws \Exception - */ - public function afterException($controller, $methodName, \Exception $exception) { - if (($controller instanceof SettingsController) === false) { - throw $exception; - } - $this->logger->error($exception->getMessage(), ['app' => $this->appName]); - if ($exception instanceof HintException) { - $message = $exception->getHint(); - } else { - $message = $exception->getMessage(); - } - - return new JSONResponse( - ['message' => $message], - Http::STATUS_BAD_REQUEST - ); - - } - -} diff --git a/apps/federation/lib/Migration/Version1010Date20200630191302.php b/apps/federation/lib/Migration/Version1010Date20200630191302.php new file mode 100644 index 00000000000..c1a7c38cfc7 --- /dev/null +++ b/apps/federation/lib/Migration/Version1010Date20200630191302.php @@ -0,0 +1,66 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Federation\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version1010Date20200630191302 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + if (!$schema->hasTable('trusted_servers')) { + $table = $schema->createTable('trusted_servers'); + $table->addColumn('id', Types::INTEGER, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 4, + ]); + $table->addColumn('url', Types::STRING, [ + 'notnull' => true, + 'length' => 512, + ]); + $table->addColumn('url_hash', Types::STRING, [ + 'notnull' => true, + 'default' => '', + ]); + $table->addColumn('token', Types::STRING, [ + 'notnull' => false, + 'length' => 128, + ]); + $table->addColumn('shared_secret', Types::STRING, [ + 'notnull' => false, + 'length' => 256, + ]); + $table->addColumn('status', Types::INTEGER, [ + 'notnull' => true, + 'length' => 4, + 'default' => 2, + ]); + $table->addColumn('sync_token', Types::STRING, [ + 'notnull' => false, + 'length' => 512, + ]); + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['url_hash'], 'url_hash'); + } + return $schema; + } +} diff --git a/apps/federation/lib/Settings/Admin.php b/apps/federation/lib/Settings/Admin.php index eccb3237f7d..5cf5346bb85 100644 --- a/apps/federation/lib/Settings/Admin.php +++ b/apps/federation/lib/Settings/Admin.php @@ -1,39 +1,21 @@ <?php + /** - * @copyright Copyright (c) 2016 Arthur Schiwon <blizzz@arthur-schiwon.de> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ - namespace OCA\Federation\Settings; use OCA\Federation\TrustedServers; use OCP\AppFramework\Http\TemplateResponse; -use OCP\Settings\ISettings; - -class Admin implements ISettings { - - /** @var TrustedServers */ - private $trustedServers; - - public function __construct(TrustedServers $trustedServers) { - $this->trustedServers = $trustedServers; +use OCP\IL10N; +use OCP\Settings\IDelegatedSettings; + +class Admin implements IDelegatedSettings { + public function __construct( + private TrustedServers $trustedServers, + private IL10N $l, + ) { } /** @@ -42,7 +24,6 @@ class Admin implements ISettings { public function getForm() { $parameters = [ 'trustedServers' => $this->trustedServers->getServers(), - 'autoAddServers' => $this->trustedServers->getAutoAddServers(), ]; return new TemplateResponse('federation', 'settings-admin', $parameters, ''); @@ -57,8 +38,8 @@ class Admin implements ISettings { /** * @return int whether the form should be rather on the top or bottom of - * the admin section. The forms are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The forms are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * * E.g.: 70 */ @@ -66,4 +47,11 @@ class Admin implements ISettings { return 30; } + public function getName(): ?string { + return $this->l->t('Trusted servers'); + } + + public function getAuthorizedAppConfig(): array { + return []; // Handled by custom controller + } } diff --git a/apps/federation/lib/SyncFederationAddressBooks.php b/apps/federation/lib/SyncFederationAddressBooks.php index b5cd9a574c4..d11f92b76ef 100644 --- a/apps/federation/lib/SyncFederationAddressBooks.php +++ b/apps/federation/lib/SyncFederationAddressBooks.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Federation; @@ -28,29 +11,17 @@ use OC\OCS\DiscoveryService; use OCA\DAV\CardDAV\SyncService; use OCP\AppFramework\Http; use OCP\OCS\IDiscoveryService; +use Psr\Log\LoggerInterface; class SyncFederationAddressBooks { + private DiscoveryService $ocsDiscoveryService; - /** @var DbHandler */ - protected $dbHandler; - - /** @var SyncService */ - private $syncService; - - /** @var DiscoveryService */ - private $ocsDiscoveryService; - - /** - * @param DbHandler $dbHandler - * @param SyncService $syncService - * @param IDiscoveryService $ocsDiscoveryService - */ - public function __construct(DbHandler $dbHandler, - SyncService $syncService, - IDiscoveryService $ocsDiscoveryService + public function __construct( + protected DbHandler $dbHandler, + private SyncService $syncService, + IDiscoveryService $ocsDiscoveryService, + private LoggerInterface $logger, ) { - $this->syncService = $syncService; - $this->dbHandler = $dbHandler; $this->ocsDiscoveryService = $ocsDiscoveryService; } @@ -58,34 +29,63 @@ class SyncFederationAddressBooks { * @param \Closure $callback */ public function syncThemAll(\Closure $callback) { - $trustedServers = $this->dbHandler->getAllServer(); foreach ($trustedServers as $trustedServer) { $url = $trustedServer['url']; $callback($url, null); $sharedSecret = $trustedServer['shared_secret']; - $syncToken = $trustedServer['sync_token']; + $oldSyncToken = $trustedServer['sync_token']; $endPoints = $this->ocsDiscoveryService->discover($url, 'FEDERATED_SHARING'); - $cardDavUser = isset($endPoints['carddav-user']) ? $endPoints['carddav-user'] : 'system'; + $cardDavUser = $endPoints['carddav-user'] ?? 'system'; $addressBookUrl = isset($endPoints['system-address-book']) ? trim($endPoints['system-address-book'], '/') : 'remote.php/dav/addressbooks/system/system/system'; if (is_null($sharedSecret)) { + $this->logger->debug("Shared secret for $url is null"); continue; } $targetBookId = $trustedServer['url_hash']; - $targetPrincipal = "principals/system/system"; + $targetPrincipal = 'principals/system/system'; $targetBookProperties = [ - '{DAV:}displayname' => $url + '{DAV:}displayname' => $url ]; + try { - $newToken = $this->syncService->syncRemoteAddressBook($url, $cardDavUser, $addressBookUrl, $sharedSecret, $syncToken, $targetBookId, $targetPrincipal, $targetBookProperties); - if ($newToken !== $syncToken) { - $this->dbHandler->setServerStatus($url, TrustedServers::STATUS_OK, $newToken); + $syncToken = $oldSyncToken; + + do { + [$syncToken, $truncated] = $this->syncService->syncRemoteAddressBook( + $url, + $cardDavUser, + $addressBookUrl, + $sharedSecret, + $syncToken, + $targetBookId, + $targetPrincipal, + $targetBookProperties + ); + } while ($truncated); + + if ($syncToken !== $oldSyncToken) { + $this->dbHandler->setServerStatus($url, TrustedServers::STATUS_OK, $syncToken); + } else { + $this->logger->debug("Sync Token for $url unchanged from previous sync"); + // The server status might have been changed to a failure status in previous runs. + if ($this->dbHandler->getServerStatus($url) !== TrustedServers::STATUS_OK) { + $this->dbHandler->setServerStatus($url, TrustedServers::STATUS_OK); + } } } catch (\Exception $ex) { if ($ex->getCode() === Http::STATUS_UNAUTHORIZED) { $this->dbHandler->setServerStatus($url, TrustedServers::STATUS_ACCESS_REVOKED); + $this->logger->error("Server sync for $url failed because of revoked access.", [ + 'exception' => $ex, + ]); + } else { + $this->dbHandler->setServerStatus($url, TrustedServers::STATUS_FAILURE); + $this->logger->error("Server sync for $url failed.", [ + 'exception' => $ex, + ]); } $callback($url, $ex); } diff --git a/apps/federation/lib/SyncJob.php b/apps/federation/lib/SyncJob.php index 0aa1d61affb..b802dfa9308 100644 --- a/apps/federation/lib/SyncJob.php +++ b/apps/federation/lib/SyncJob.php @@ -1,55 +1,34 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Federation; -use OC\BackgroundJob\TimedJob; -use OCA\Federation\AppInfo\Application; -use OCP\ILogger; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use Psr\Log\LoggerInterface; class SyncJob extends TimedJob { - - /** @var SyncFederationAddressBooks */ - protected $syncService; - - /** @var ILogger */ - protected $logger; - - /** - * @param SyncFederationAddressBooks $syncService - * @param ILogger $logger - */ - public function __construct(SyncFederationAddressBooks $syncService, ILogger $logger) { + public function __construct( + protected SyncFederationAddressBooks $syncService, + protected LoggerInterface $logger, + ITimeFactory $timeFactory, + ) { + parent::__construct($timeFactory); // Run once a day $this->setInterval(24 * 60 * 60); - $this->syncService = $syncService; - $this->logger = $logger; + $this->setTimeSensitivity(self::TIME_INSENSITIVE); } protected function run($argument) { - $this->syncService->syncThemAll(function($url, $ex) { + $this->syncService->syncThemAll(function ($url, $ex): void { if ($ex instanceof \Exception) { - $this->logger->error("Error while syncing $url : " . $ex->getMessage(), ['app' => 'fed-sync']); + $this->logger->error("Error while syncing $url.", [ + 'exception' => $ex, + ]); } }); } diff --git a/apps/federation/lib/TrustedServers.php b/apps/federation/lib/TrustedServers.php index 79cf86ab67b..3d15cfac448 100644 --- a/apps/federation/lib/TrustedServers.php +++ b/apps/federation/lib/TrustedServers.php @@ -1,122 +1,62 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - - namespace OCA\Federation; -use OC\HintException; +use OCA\Federation\BackgroundJob\RequestSharedSecret; use OCP\AppFramework\Http; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\IJobList; +use OCP\DB\Exception; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Federation\Events\TrustedServerRemovedEvent; +use OCP\HintException; use OCP\Http\Client\IClientService; use OCP\IConfig; -use OCP\ILogger; use OCP\Security\ISecureRandom; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\GenericEvent; +use Psr\Log\LoggerInterface; class TrustedServers { /** after a user list was exchanged at least once successfully */ - const STATUS_OK = 1; + public const STATUS_OK = 1; /** waiting for shared secret or initial user list exchange */ - const STATUS_PENDING = 2; + public const STATUS_PENDING = 2; /** something went wrong, misconfigured server, software bug,... user interaction needed */ - const STATUS_FAILURE = 3; + public const STATUS_FAILURE = 3; /** remote server revoked access */ - const STATUS_ACCESS_REVOKED = 4; - - /** @var dbHandler */ - private $dbHandler; - - /** @var IClientService */ - private $httpClientService; - - /** @var ILogger */ - private $logger; + public const STATUS_ACCESS_REVOKED = 4; - /** @var IJobList */ - private $jobList; + /** @var list<array{id: int, url: string, url_hash: string, shared_secret: ?string, status: int, sync_token: ?string}>|null */ + private ?array $trustedServersCache = null; - /** @var ISecureRandom */ - private $secureRandom; - - /** @var IConfig */ - private $config; - - /** @var EventDispatcherInterface */ - private $dispatcher; - - /** @var ITimeFactory */ - private $timeFactory; - - /** - * @param DbHandler $dbHandler - * @param IClientService $httpClientService - * @param ILogger $logger - * @param IJobList $jobList - * @param ISecureRandom $secureRandom - * @param IConfig $config - * @param EventDispatcherInterface $dispatcher - * @param ITimeFactory $timeFactory - */ public function __construct( - DbHandler $dbHandler, - IClientService $httpClientService, - ILogger $logger, - IJobList $jobList, - ISecureRandom $secureRandom, - IConfig $config, - EventDispatcherInterface $dispatcher, - ITimeFactory $timeFactory + private DbHandler $dbHandler, + private IClientService $httpClientService, + private LoggerInterface $logger, + private IJobList $jobList, + private ISecureRandom $secureRandom, + private IConfig $config, + private IEventDispatcher $dispatcher, + private ITimeFactory $timeFactory, ) { - $this->dbHandler = $dbHandler; - $this->httpClientService = $httpClientService; - $this->logger = $logger; - $this->jobList = $jobList; - $this->secureRandom = $secureRandom; - $this->config = $config; - $this->dispatcher = $dispatcher; - $this->timeFactory = $timeFactory; } /** - * add server to the list of trusted servers - * - * @param $url - * @return int server id + * Add server to the list of trusted servers */ - public function addServer($url) { + public function addServer(string $url): int { $url = $this->updateProtocol($url); $result = $this->dbHandler->addServer($url); if ($result) { $token = $this->secureRandom->generate(16); $this->dbHandler->addToken($url, $token); $this->jobList->add( - 'OCA\Federation\BackgroundJob\RequestSharedSecret', + RequestSharedSecret::class, [ 'url' => $url, 'token' => $token, @@ -129,104 +69,88 @@ class TrustedServers { } /** - * enable/disable to automatically add servers to the list of trusted servers - * once a federated share was created and accepted successfully - * - * @param bool $status + * Get shared secret for the given server */ - public function setAutoAddServers($status) { - $value = $status ? '1' : '0'; - $this->config->setAppValue('federation', 'autoAddServers', $value); + public function getSharedSecret(string $url): string { + return $this->dbHandler->getSharedSecret($url); } /** - * return if we automatically add servers to the list of trusted servers - * once a federated share was created and accepted successfully - * - * @return bool + * Add shared secret for the given server */ - public function getAutoAddServers() { - $value = $this->config->getAppValue('federation', 'autoAddServers', '0'); - return $value === '1'; + public function addSharedSecret(string $url, string $sharedSecret): void { + $this->dbHandler->addSharedSecret($url, $sharedSecret); } /** - * get shared secret for the given server - * - * @param string $url - * @return string + * Remove server from the list of trusted servers */ - public function getSharedSecret($url) { - return $this->dbHandler->getSharedSecret($url); - } + public function removeServer(int $id): void { + $server = $this->dbHandler->getServerById($id); + $this->dbHandler->removeServer($id); + $this->dispatcher->dispatchTyped(new TrustedServerRemovedEvent($server['url_hash'])); - /** - * add shared secret for the given server - * - * @param string $url - * @param $sharedSecret - */ - public function addSharedSecret($url, $sharedSecret) { - $this->dbHandler->addSharedSecret($url, $sharedSecret); } /** - * remove server from the list of trusted servers + * Get all trusted servers * - * @param int $id + * @return list<array{id: int, url: string, url_hash: string, shared_secret: ?string, status: int, sync_token: ?string}> + * @throws \Exception */ - public function removeServer($id) { - $server = $this->dbHandler->getServerById($id); - $this->dbHandler->removeServer($id); - $event = new GenericEvent($server['url_hash']); - $this->dispatcher->dispatch('OCP\Federation\TrustedServerEvent::remove', $event); + public function getServers(): ?array { + if ($this->trustedServersCache === null) { + $this->trustedServersCache = $this->dbHandler->getAllServer(); + } + return $this->trustedServersCache; } /** - * get all trusted servers + * Get a trusted server * - * @return array + * @return array{id: int, url: string, url_hash: string, shared_secret: ?string, status: int, sync_token: ?string} + * @throws Exception */ - public function getServers() { - return $this->dbHandler->getAllServer(); + public function getServer(int $id): ?array { + if ($this->trustedServersCache === null) { + $this->trustedServersCache = $this->dbHandler->getAllServer(); + } + + foreach ($this->trustedServersCache as $server) { + if ($server['id'] === $id) { + return $server; + } + } + + throw new \Exception('No server found with ID: ' . $id); } /** - * check if given server is a trusted Nextcloud server - * - * @param string $url - * @return bool + * Check if given server is a trusted Nextcloud server */ - public function isTrustedServer($url) { + public function isTrustedServer(string $url): bool { return $this->dbHandler->serverExists($url); } /** - * set server status - * - * @param string $url - * @param int $status + * Set server status */ - public function setServerStatus($url, $status) { + public function setServerStatus(string $url, int $status): void { $this->dbHandler->setServerStatus($url, $status); } /** - * @param string $url - * @return int + * Get server status */ - public function getServerStatus($url) { + public function getServerStatus(string $url): int { return $this->dbHandler->getServerStatus($url); } /** - * check if URL point to a ownCloud/Nextcloud server - * - * @param string $url - * @return bool + * Check if URL point to a ownCloud/Nextcloud server */ - public function isOwnCloudServer($url) { - $isValidOwnCloud = false; + public function isNextcloudServer(string $url): bool { + $isValidNextcloud = false; $client = $this->httpClientService->newClient(); try { $result = $client->get( @@ -234,28 +158,31 @@ class TrustedServers { [ 'timeout' => 3, 'connect_timeout' => 3, + 'verify' => !$this->config->getSystemValue('sharing.federation.allowSelfSignedCertificates', false), ] ); if ($result->getStatusCode() === Http::STATUS_OK) { - $isValidOwnCloud = $this->checkOwnCloudVersion($result->getBody()); - + $body = $result->getBody(); + if (is_resource($body)) { + $body = stream_get_contents($body) ?: ''; + } + $isValidNextcloud = $this->checkNextcloudVersion($body); } } catch (\Exception $e) { - $this->logger->debug('No Nextcloud server: ' . $e->getMessage()); + $this->logger->error('No Nextcloud server.', [ + 'exception' => $e, + ]); return false; } - return $isValidOwnCloud; + return $isValidNextcloud; } /** - * check if ownCloud version is >= 9.0 - * - * @param $status - * @return bool + * Check if ownCloud/Nextcloud version is >= 9.0 * @throws HintException */ - protected function checkOwnCloudVersion($status) { + protected function checkNextcloudVersion(string $status): bool { $decoded = json_decode($status, true); if (!empty($decoded) && isset($decoded['version'])) { if (!version_compare($decoded['version'], '9.0.0', '>=')) { @@ -267,19 +194,14 @@ class TrustedServers { } /** - * check if the URL contain a protocol, if not add https - * - * @param string $url - * @return string + * Check if the URL contain a protocol, if not add https */ - protected function updateProtocol($url) { + protected function updateProtocol(string $url): string { if ( strpos($url, 'https://') === 0 || strpos($url, 'http://') === 0 ) { - return $url; - } return 'https://' . $url; |