From: John Molakvoæ Date: Sun, 15 May 2022 08:38:55 +0000 (+0200) Subject: feat: public dav endpoint v2 X-Git-Tag: v29.0.0beta1~542^2~3 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=7b6a650b6e09b07d4b85a4ae84eb64a6c32b217f;p=nextcloud-server.git feat: public dav endpoint v2 Signed-off-by: John Molakvoæ --- diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index d436596ca5a..02dec49a546 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -86,8 +86,4 @@ OCA\DAV\CardDAV\Activity\Provider\Card - - - appinfo/v1/publicwebdav.php - diff --git a/apps/dav/appinfo/v1/publicwebdav.php b/apps/dav/appinfo/v1/publicwebdav.php index 8ca9c6c577c..b5a88afb16e 100644 --- a/apps/dav/appinfo/v1/publicwebdav.php +++ b/apps/dav/appinfo/v1/publicwebdav.php @@ -43,7 +43,7 @@ OC_Util::obEnd(); \OC::$server->getSession()->close(); // Backends -$authBackend = new OCA\DAV\Connector\PublicAuth( +$authBackend = new OCA\DAV\Connector\LegacyPublicAuth( \OC::$server->getRequest(), \OC::$server->getShareManager(), \OC::$server->getSession(), diff --git a/apps/dav/appinfo/v2/publicremote.php b/apps/dav/appinfo/v2/publicremote.php new file mode 100644 index 00000000000..501f9188c1f --- /dev/null +++ b/apps/dav/appinfo/v2/publicremote.php @@ -0,0 +1,122 @@ + + * @author Björn Schießle + * @author Christoph Wurst + * @author Joas Schilling + * @author Julius Härtl + * @author Lukas Reschke + * @author Morris Jobke + * @author Robin Appelman + * @author Roeland Jago Douma + * @author Thomas Müller + * @author Vincent Petry + * + * @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 + * + */ +// load needed apps +$RUNTIME_APPTYPES = ['filesystem', 'authentication', 'logging']; + +OC_App::loadApps($RUNTIME_APPTYPES); + +OC_Util::obEnd(); +\OC::$server->getSession()->close(); + +// Backends +$authBackend = new OCA\DAV\Connector\Sabre\PublicAuth( + \OC::$server->getRequest(), + \OC::$server->getShareManager(), + \OC::$server->getSession(), + \OC::$server->getBruteForceThrottler() +); +$authPlugin = new \Sabre\DAV\Auth\Plugin($authBackend); + +$serverFactory = new OCA\DAV\Connector\Sabre\ServerFactory( + \OC::$server->getConfig(), + \OC::$server->get(Psr\Log\LoggerInterface::class), + \OC::$server->getDatabaseConnection(), + \OC::$server->getUserSession(), + \OC::$server->getMountManager(), + \OC::$server->getTagManager(), + \OC::$server->getRequest(), + \OC::$server->getPreviewManager(), + \OC::$server->getEventDispatcher(), + \OC::$server->getL10N('dav') +); + +$requestUri = \OC::$server->getRequest()->getRequestUri(); + +$linkCheckPlugin = new \OCA\DAV\Files\Sharing\PublicLinkCheckPlugin(); +$filesDropPlugin = new \OCA\DAV\Files\Sharing\FilesDropPlugin(); + +// Define root url with /public.php/dav/files/TOKEN +preg_match('/(^files\/\w+)/i', substr($requestUri, strlen($baseuri)), $match); +$baseuri = $baseuri . $match[0]; + +$server = $serverFactory->createServer($baseuri, $requestUri, $authPlugin, function (\Sabre\DAV\Server $server) use ($authBackend, $linkCheckPlugin, $filesDropPlugin) { + $isAjax = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest'); + /** @var \OCA\FederatedFileSharing\FederatedShareProvider $shareProvider */ + $federatedShareProvider = \OC::$server->query(\OCA\FederatedFileSharing\FederatedShareProvider::class); + if ($federatedShareProvider->isOutgoingServer2serverShareEnabled() === false && !$isAjax) { + // this is what is thrown when trying to access a non-existing share + throw new \Sabre\DAV\Exception\NotAuthenticated(); + } + + $share = $authBackend->getShare(); + $owner = $share->getShareOwner(); + $isReadable = $share->getPermissions() & \OCP\Constants::PERMISSION_READ; + $fileId = $share->getNodeId(); + + // FIXME: should not add storage wrappers outside of preSetup, need to find a better way + $previousLog = \OC\Files\Filesystem::logWarningWhenAddingStorageWrapper(false); + \OC\Files\Filesystem::addStorageWrapper('sharePermissions', function ($mountPoint, $storage) use ($share) { + return new \OC\Files\Storage\Wrapper\PermissionsMask(['storage' => $storage, 'mask' => $share->getPermissions() | \OCP\Constants::PERMISSION_SHARE]); + }); + \OC\Files\Filesystem::addStorageWrapper('shareOwner', function ($mountPoint, $storage) use ($share) { + return new \OCA\DAV\Storage\PublicOwnerWrapper(['storage' => $storage, 'owner' => $share->getShareOwner()]); + }); + \OC\Files\Filesystem::logWarningWhenAddingStorageWrapper($previousLog); + + OC_Util::tearDownFS(); + OC_Util::setupFS($owner); + $ownerView = new \OC\Files\View('/'. $owner . '/files'); + $path = $ownerView->getPath($fileId); + $fileInfo = $ownerView->getFileInfo($path); + + if ($fileInfo === false) { + throw new \Sabre\DAV\Exception\NotFound(); + } + + $linkCheckPlugin->setFileInfo($fileInfo); + + // If not readble (files_drop) enable the filesdrop plugin + if (!$isReadable) { + $filesDropPlugin->enable(); + } + + $view = new \OC\Files\View($ownerView->getAbsolutePath($path)); + $filesDropPlugin->setView($view); + + return $view; +}); + +$server->addPlugin($linkCheckPlugin); +$server->addPlugin($filesDropPlugin); + +// And off we go! +$server->exec(); diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index 72455e6bf67..d4305195d46 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -149,7 +149,7 @@ return array( 'OCA\\DAV\\Comments\\EntityTypeCollection' => $baseDir . '/../lib/Comments/EntityTypeCollection.php', 'OCA\\DAV\\Comments\\RootCollection' => $baseDir . '/../lib/Comments/RootCollection.php', 'OCA\\DAV\\Connector\\LegacyDAVACL' => $baseDir . '/../lib/Connector/LegacyDAVACL.php', - 'OCA\\DAV\\Connector\\PublicAuth' => $baseDir . '/../lib/Connector/PublicAuth.php', + 'OCA\\DAV\\Connector\\LegacyPublicAuth' => $baseDir . '/../lib/Connector/LegacyPublicAuth.php', 'OCA\\DAV\\Connector\\Sabre\\AnonymousOptionsPlugin' => $baseDir . '/../lib/Connector/Sabre/AnonymousOptionsPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\AppleQuirksPlugin' => $baseDir . '/../lib/Connector/Sabre/AppleQuirksPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\Auth' => $baseDir . '/../lib/Connector/Sabre/Auth.php', @@ -183,6 +183,7 @@ return array( 'OCA\\DAV\\Connector\\Sabre\\ObjectTree' => $baseDir . '/../lib/Connector/Sabre/ObjectTree.php', 'OCA\\DAV\\Connector\\Sabre\\Principal' => $baseDir . '/../lib/Connector/Sabre/Principal.php', 'OCA\\DAV\\Connector\\Sabre\\PropfindCompressionPlugin' => $baseDir . '/../lib/Connector/Sabre/PropfindCompressionPlugin.php', + 'OCA\\DAV\\Connector\\Sabre\\PublicAuth' => $baseDir . '/../lib/Connector/Sabre/PublicAuth.php', 'OCA\\DAV\\Connector\\Sabre\\QuotaPlugin' => $baseDir . '/../lib/Connector/Sabre/QuotaPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\RequestIdHeaderPlugin' => $baseDir . '/../lib/Connector/Sabre/RequestIdHeaderPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\Server' => $baseDir . '/../lib/Connector/Sabre/Server.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 0d715f510f7..9afd73635ff 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -164,7 +164,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Comments\\EntityTypeCollection' => __DIR__ . '/..' . '/../lib/Comments/EntityTypeCollection.php', 'OCA\\DAV\\Comments\\RootCollection' => __DIR__ . '/..' . '/../lib/Comments/RootCollection.php', 'OCA\\DAV\\Connector\\LegacyDAVACL' => __DIR__ . '/..' . '/../lib/Connector/LegacyDAVACL.php', - 'OCA\\DAV\\Connector\\PublicAuth' => __DIR__ . '/..' . '/../lib/Connector/PublicAuth.php', + 'OCA\\DAV\\Connector\\LegacyPublicAuth' => __DIR__ . '/..' . '/../lib/Connector/LegacyPublicAuth.php', 'OCA\\DAV\\Connector\\Sabre\\AnonymousOptionsPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/AnonymousOptionsPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\AppleQuirksPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/AppleQuirksPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\Auth' => __DIR__ . '/..' . '/../lib/Connector/Sabre/Auth.php', @@ -198,6 +198,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Connector\\Sabre\\ObjectTree' => __DIR__ . '/..' . '/../lib/Connector/Sabre/ObjectTree.php', 'OCA\\DAV\\Connector\\Sabre\\Principal' => __DIR__ . '/..' . '/../lib/Connector/Sabre/Principal.php', 'OCA\\DAV\\Connector\\Sabre\\PropfindCompressionPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/PropfindCompressionPlugin.php', + 'OCA\\DAV\\Connector\\Sabre\\PublicAuth' => __DIR__ . '/..' . '/../lib/Connector/Sabre/PublicAuth.php', 'OCA\\DAV\\Connector\\Sabre\\QuotaPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/QuotaPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\RequestIdHeaderPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/RequestIdHeaderPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\Server' => __DIR__ . '/..' . '/../lib/Connector/Sabre/Server.php', diff --git a/apps/dav/lib/Connector/LegacyPublicAuth.php b/apps/dav/lib/Connector/LegacyPublicAuth.php new file mode 100644 index 00000000000..c2cc81103bd --- /dev/null +++ b/apps/dav/lib/Connector/LegacyPublicAuth.php @@ -0,0 +1,130 @@ + + * @author Christoph Wurst + * @author Joas Schilling + * @author Lukas Reschke + * @author Maxence Lange + * @author Robin Appelman + * @author Roeland Jago Douma + * @author Thomas Müller + * @author Vincent Petry + * + * @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\DAV\Connector; + +use OCA\DAV\Connector\Sabre\PublicAuth; +use OCP\IRequest; +use OCP\ISession; +use OCP\Security\Bruteforce\IThrottler; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IManager; +use OCP\Share\IShare; +use Sabre\DAV\Auth\Backend\AbstractBasic; + +/** + * Class PublicAuth + * + * @package OCA\DAV\Connector + */ +class LegacyPublicAuth extends AbstractBasic { + private const BRUTEFORCE_ACTION = 'legacy_public_webdav_auth'; + + private ?IShare $share = null; + private IManager $shareManager; + private ISession $session; + private IRequest $request; + private IThrottler $throttler; + + public function __construct(IRequest $request, + IManager $shareManager, + ISession $session, + IThrottler $throttler) { + $this->request = $request; + $this->shareManager = $shareManager; + $this->session = $session; + $this->throttler = $throttler; + + // setup realm + $defaults = new \OCP\Defaults(); + $this->realm = $defaults->getName(); + } + + /** + * Validates a username and password + * + * This method should return true or false depending on if login + * succeeded. + * + * @param string $username + * @param string $password + * + * @return bool + * @throws \Sabre\DAV\Exception\NotAuthenticated + */ + protected function validateUserPass($username, $password) { + $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION); + + try { + $share = $this->shareManager->getShareByToken($username); + } catch (ShareNotFound $e) { + $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); + return false; + } + + $this->share = $share; + + \OC_User::setIncognitoMode(true); + + // check if the share is password protected + if ($share->getPassword() !== null) { + if ($share->getShareType() === IShare::TYPE_LINK + || $share->getShareType() === IShare::TYPE_EMAIL + || $share->getShareType() === IShare::TYPE_CIRCLE) { + if ($this->shareManager->checkPassword($share, $password)) { + return true; + } elseif ($this->session->exists(PublicAuth::DAV_AUTHENTICATED) + && $this->session->get(PublicAuth::DAV_AUTHENTICATED) === $share->getId()) { + return true; + } else { + if (in_array('XMLHttpRequest', explode(',', $this->request->getHeader('X-Requested-With')))) { + // do not re-authenticate over ajax, use dummy auth name to prevent browser popup + http_response_code(401); + header('WWW-Authenticate: DummyBasic realm="' . $this->realm . '"'); + throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls'); + } + + $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); + return false; + } + } elseif ($share->getShareType() === IShare::TYPE_REMOTE) { + return true; + } else { + $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); + return false; + } + } + return true; + } + + public function getShare(): IShare { + assert($this->share !== null); + return $this->share; + } +} diff --git a/apps/dav/lib/Connector/PublicAuth.php b/apps/dav/lib/Connector/PublicAuth.php deleted file mode 100644 index d613a5a188f..00000000000 --- a/apps/dav/lib/Connector/PublicAuth.php +++ /dev/null @@ -1,127 +0,0 @@ - - * @author Christoph Wurst - * @author Joas Schilling - * @author Lukas Reschke - * @author Maxence Lange - * @author Robin Appelman - * @author Roeland Jago Douma - * @author Thomas Müller - * @author Vincent Petry - * - * @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\DAV\Connector; - -use OCP\IRequest; -use OCP\ISession; -use OCP\Security\Bruteforce\IThrottler; -use OCP\Share\Exceptions\ShareNotFound; -use OCP\Share\IManager; -use OCP\Share\IShare; -use Sabre\DAV\Auth\Backend\AbstractBasic; - -/** - * Class PublicAuth - * - * @package OCA\DAV\Connector - */ -class PublicAuth extends AbstractBasic { - private const BRUTEFORCE_ACTION = 'public_webdav_auth'; - private ?IShare $share = null; - private IManager $shareManager; - private ISession $session; - private IRequest $request; - private IThrottler $throttler; - - public function __construct(IRequest $request, - IManager $shareManager, - ISession $session, - IThrottler $throttler) { - $this->request = $request; - $this->shareManager = $shareManager; - $this->session = $session; - $this->throttler = $throttler; - - // setup realm - $defaults = new \OCP\Defaults(); - $this->realm = $defaults->getName(); - } - - /** - * Validates a username and password - * - * This method should return true or false depending on if login - * succeeded. - * - * @param string $username - * @param string $password - * @return bool - * @throws \Sabre\DAV\Exception\NotAuthenticated - */ - protected function validateUserPass($username, $password) { - $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION); - - try { - $share = $this->shareManager->getShareByToken($username); - } catch (ShareNotFound $e) { - $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); - return false; - } - - $this->share = $share; - - \OC_User::setIncognitoMode(true); - - // check if the share is password protected - if ($share->getPassword() !== null) { - if ($share->getShareType() === IShare::TYPE_LINK - || $share->getShareType() === IShare::TYPE_EMAIL - || $share->getShareType() === IShare::TYPE_CIRCLE) { - if ($this->shareManager->checkPassword($share, $password)) { - return true; - } elseif ($this->session->exists('public_link_authenticated') - && $this->session->get('public_link_authenticated') === (string)$share->getId()) { - return true; - } else { - if (in_array('XMLHttpRequest', explode(',', $this->request->getHeader('X-Requested-With')))) { - // do not re-authenticate over ajax, use dummy auth name to prevent browser popup - http_response_code(401); - header('WWW-Authenticate: DummyBasic realm="' . $this->realm . '"'); - throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls'); - } - - $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); - return false; - } - } elseif ($share->getShareType() === IShare::TYPE_REMOTE) { - return true; - } else { - $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); - return false; - } - } - return true; - } - - public function getShare(): IShare { - assert($this->share !== null); - return $this->share; - } -} diff --git a/apps/dav/lib/Connector/Sabre/PublicAuth.php b/apps/dav/lib/Connector/Sabre/PublicAuth.php new file mode 100644 index 00000000000..5c71d2b47dc --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/PublicAuth.php @@ -0,0 +1,231 @@ + + * @author Christoph Wurst + * @author Joas Schilling + * @author Lukas Reschke + * @author Maxence Lange + * @author Robin Appelman + * @author Roeland Jago Douma + * @author Thomas Müller + * @author Vincent Petry + * + * @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\DAV\Connector\Sabre; + +use OCP\IRequest; +use OCP\ISession; +use OCP\Security\Bruteforce\IThrottler; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IManager; +use OCP\Share\IShare; +use Psr\Log\LoggerInterface; +use Sabre\DAV\Auth\Backend\AbstractBasic; +use Sabre\DAV\Exception\NotAuthenticated; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Exception\ServiceUnavailable; +use Sabre\HTTP; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +/** + * Class PublicAuth + * + * @package OCA\DAV\Connector + */ +class PublicAuth extends AbstractBasic { + private const BRUTEFORCE_ACTION = 'public_dav_auth'; + public const DAV_AUTHENTICATED = 'public_link_authenticated'; + + private IShare $share; + private IManager $shareManager; + private ISession $session; + private IRequest $request; + private IThrottler $throttler; + + + public function __construct(IRequest $request, + IManager $shareManager, + ISession $session, + IThrottler $throttler) { + $this->request = $request; + $this->shareManager = $shareManager; + $this->session = $session; + $this->throttler = $throttler; + + // setup realm + $defaults = new \OCP\Defaults(); + $this->realm = $defaults->getName(); + } + + /** + * @param RequestInterface $request + * @param ResponseInterface $response + * + * @return array + * @throws NotAuthenticated + * @throws ServiceUnavailable + */ + public function check(RequestInterface $request, ResponseInterface $response): array { + try { + $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION); + + $auth = new HTTP\Auth\Basic( + $this->realm, + $request, + $response + ); + + $userpass = $auth->getCredentials(); + // If authentication provided, checking its validity + if ($userpass && !$this->validateUserPass($userpass[0], $userpass[1])) { + return [false, 'Username or password was incorrect']; + } + + return $this->checkToken(); + } catch (NotAuthenticated $e) { + throw $e; + } catch (\Exception $e) { + $class = get_class($e); + $msg = $e->getMessage(); + \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]); + throw new ServiceUnavailable("$class: $msg"); + } + } + + /** + * Extract token from request url + * @return string + * @throws NotFound + */ + private function getToken(): string { + $path = $this->request->getPathInfo(); + // ['', 'dav', 'files', 'token'] + $splittedPath = explode('/', $path); + + if (count($splittedPath) < 4 || $splittedPath[3] === '') { + throw new NotFound(); + } + + return $splittedPath[3]; + } + + /** + * Check token validity + * @return array + * @throws NotFound + * @throws NotAuthenticated + */ + private function checkToken(): array { + $token = $this->getToken(); + + try { + $share = $this->shareManager->getShareByToken($token); + } catch (ShareNotFound $e) { + $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); + throw new NotFound(); + } + + $this->share = $share; + \OC_User::setIncognitoMode(true); + + // If already authenticated + if ($this->session->exists(self::DAV_AUTHENTICATED) + && $this->session->get(self::DAV_AUTHENTICATED) === $share->getId()) { + return [true, $this->principalPrefix . $token]; + } + + // If the share is protected but user is not authenticated + if ($share->getPassword() !== null) { + $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); + throw new NotAuthenticated(); + } + + return [true, $this->principalPrefix . $token]; + } + + /** + * Validates a username and password + * + * This method should return true or false depending on if login + * succeeded. + * + * @param string $username + * @param string $password + * + * @return bool + * @throws NotAuthenticated + */ + protected function validateUserPass($username, $password): bool { + $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION); + + $token = $this->getToken(); + try { + $share = $this->shareManager->getShareByToken($token); + } catch (ShareNotFound $e) { + $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); + return false; + } + + $this->share = $share; + \OC_User::setIncognitoMode(true); + + // check if the share is password protected + if ($share->getPassword() !== null) { + if ($share->getShareType() === IShare::TYPE_LINK + || $share->getShareType() === IShare::TYPE_EMAIL + || $share->getShareType() === IShare::TYPE_CIRCLE) { + if ($this->shareManager->checkPassword($share, $password)) { + // If not set, set authenticated session cookie + if (!$this->session->exists(self::DAV_AUTHENTICATED) + || $this->session->get(self::DAV_AUTHENTICATED) !== $share->getId()) { + $this->session->set(self::DAV_AUTHENTICATED, $share->getId()); + } + return true; + } + + if (in_array('XMLHttpRequest', explode(',', $this->request->getHeader('X-Requested-With')))) { + // do not re-authenticate over ajax, use dummy auth name to prevent browser popup + http_response_code(401); + header('WWW-Authenticate: DummyBasic realm="' . $this->realm . '"'); + throw new NotAuthenticated('Cannot authenticate over ajax calls'); + } + + $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); + return false; + } elseif ($share->getShareType() === IShare::TYPE_REMOTE) { + return true; + } + + $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); + return false; + } else { + return true; + } + } + + public function getShare(): IShare { + assert($this->share !== null); + return $this->share; + } +} diff --git a/apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php b/apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php index f13a77d9368..7e73f89ad41 100644 --- a/apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php +++ b/apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php @@ -31,6 +31,7 @@ */ namespace OCA\FederatedFileSharing\Controller; +use OCA\DAV\Connector\Sabre\PublicAuth; use OCA\FederatedFileSharing\AddressHandler; use OCA\FederatedFileSharing\FederatedShareProvider; use OCP\AppFramework\Controller; @@ -108,7 +109,7 @@ class MountPublicLinkController extends Controller { // make sure that user is authenticated in case of a password protected link $storedPassword = $share->getPassword(); - $authenticated = $this->session->get('public_link_authenticated') === $share->getId() || + $authenticated = $this->session->get(PublicAuth::DAV_AUTHENTICATED) === $share->getId() || $this->shareManager->checkPassword($share, $password); if (!empty($storedPassword) && !$authenticated) { $response = new JSONResponse( diff --git a/apps/files_sharing/lib/Controller/ShareController.php b/apps/files_sharing/lib/Controller/ShareController.php index 835d08c446c..bd9cdffa83d 100644 --- a/apps/files_sharing/lib/Controller/ShareController.php +++ b/apps/files_sharing/lib/Controller/ShareController.php @@ -46,6 +46,7 @@ namespace OCA\Files_Sharing\Controller; use OC\Security\CSP\ContentSecurityPolicy; use OC_Files; use OC_Util; +use OCA\DAV\Connector\Sabre\PublicAuth; use OCA\FederatedFileSharing\FederatedShareProvider; use OCA\Files_Sharing\Activity\Providers\Downloads; use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent; @@ -222,8 +223,12 @@ class ShareController extends AuthPublicShareController { } protected function authSucceeded() { + if ($this->share === null) { + throw new NotFoundException(); + } + // For share this was always set so it is still used in other apps - $this->session->set('public_link_authenticated', (string)$this->share->getId()); + $this->session->set(PublicAuth::DAV_AUTHENTICATED, $this->share->getId()); } protected function authFailed() { diff --git a/lib/public/Share/IShare.php b/lib/public/Share/IShare.php index e5a943b0bac..74d404101cd 100644 --- a/lib/public/Share/IShare.php +++ b/lib/public/Share/IShare.php @@ -474,7 +474,7 @@ interface IShare { * If this share is obtained via a shareprovider the password is * hashed. * - * @return string + * @return string|null * @since 9.0.0 */ public function getPassword(); diff --git a/public.php b/public.php index 2956d7f79dd..5954da7e3a0 100644 --- a/public.php +++ b/public.php @@ -32,33 +32,53 @@ */ require_once __DIR__ . '/lib/versioncheck.php'; +/** + * @param $service + * @return string + */ +function resolveService($service) { + $services = [ + 'webdav' => 'dav/appinfo/v1/publicwebdav.php', + 'dav' => 'dav/appinfo/v2/publicremote.php', + ]; + if (isset($services[$service])) { + return $services[$service]; + } + + return \OC::$server->getConfig()->getAppValue('core', 'remote_' . $service); +} + try { require_once __DIR__ . '/lib/base.php'; + + // All resources served via the DAV endpoint should have the strictest possible + // policy. Exempted from this is the SabreDAV browser plugin which overwrites + // this policy with a softer one if debug mode is enabled. + header("Content-Security-Policy: default-src 'none';"); + if (\OCP\Util::needUpgrade()) { // since the behavior of apps or remotes are unpredictable during // an upgrade, return a 503 directly - OC_Template::printErrorPage('Service unavailable', '', 503); - exit; + throw new RemoteException('Service unavailable', 503); } - OC::checkMaintenanceMode(\OC::$server->get(\OC\SystemConfig::class)); $request = \OC::$server->getRequest(); $pathInfo = $request->getPathInfo(); - - if (!$pathInfo && $request->getParam('service', '') === '') { - http_response_code(404); - exit; - } elseif ($request->getParam('service', '')) { - $service = $request->getParam('service', ''); - } else { - $pathInfo = trim($pathInfo, '/'); - [$service] = explode('/', $pathInfo); + if ($pathInfo === false || $pathInfo === '') { + throw new RemoteException('Path not found', 404); } - $file = \OC::$server->getConfig()->getAppValue('core', 'public_' . strip_tags($service)); - if ($file === '') { - http_response_code(404); - exit; + if (!$pos = strpos($pathInfo, '/', 1)) { + $pos = strlen($pathInfo); } + $service = substr($pathInfo, 1, $pos - 1); + + $file = resolveService($service); + + if (is_null($file)) { + throw new RemoteException('Path not found', 404); + } + + $file = ltrim($file, '/'); $parts = explode('/', $file, 2); $app = $parts[0]; @@ -70,15 +90,13 @@ try { OC_App::loadApps(['filesystem', 'logging']); if (!\OC::$server->getAppManager()->isInstalled($app)) { - http_response_code(404); - exit; + throw new RemoteException('App not installed: ' . $app); } OC_App::loadApp($app); OC_User::setIncognitoMode(true); - $baseuri = OC::$WEBROOT . '/public.php/' . $service . '/'; - - require_once OC_App::getAppPath($app) . '/' . $parts[1]; + $baseuri = OC::$WEBROOT . '/public.php/'.$service.'/'; + require_once $file; } catch (Exception $ex) { $status = 500; if ($ex instanceof \OC\ServiceUnavailableException) {