diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index d436596ca5a9d..02dec49a54668 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 8ca9c6c577c9c..b5a88afb16ea3 100644 --- a/apps/dav/appinfo/v1/publicwebdav.php +++ b/apps/dav/appinfo/v1/publicwebdav.php @@ -43,7 +43,7 @@ \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 0000000000000..0ef9bda8ddabc --- /dev/null +++ b/apps/dav/appinfo/v2/publicremote.php @@ -0,0 +1,155 @@ + + * @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 + * + */ + +use OC\Files\Filesystem; +use OC\Files\Storage\Wrapper\PermissionsMask; +use OC\Files\View; +use OCA\DAV\Storage\PublicOwnerWrapper; +use OCA\FederatedFileSharing\FederatedShareProvider; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Mount\IMountManager; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IPreview; +use OCP\IRequest; +use OCP\ISession; +use OCP\ITagManager; +use OCP\IUserSession; +use OCP\L10N\IFactory; +use OCP\Security\Bruteforce\IThrottler; +use OCP\Share\IManager; +use Psr\Log\LoggerInterface; +use Sabre\DAV\Exception\NotAuthenticated; +use Sabre\DAV\Exception\NotFound; + +// load needed apps +$RUNTIME_APPTYPES = ['filesystem', 'authentication', 'logging']; +OC_App::loadApps($RUNTIME_APPTYPES); +OC_Util::obEnd(); + +$session = \OCP\Server::get(ISession::class); +$request = \OCP\Server::get(IRequest::class); + +$session->close(); +$requestUri = $request->getRequestUri(); + +// Backends +$authBackend = new OCA\DAV\Connector\Sabre\PublicAuth( + $request, + \OCP\Server::get(IManager::class), + $session, + \OCP\Server::get(IThrottler::class), + \OCP\Server::get(LoggerInterface::class) +); +$authPlugin = new \Sabre\DAV\Auth\Plugin($authBackend); + +$l10nFactory = \OCP\Server::get(IFactory::class); +$serverFactory = new OCA\DAV\Connector\Sabre\ServerFactory( + \OCP\Server::get(IConfig::class), + \OCP\Server::get(LoggerInterface::class), + \OCP\Server::get(IDBConnection::class), + \OCP\Server::get(IUserSession::class), + \OCP\Server::get(IMountManager::class), + \OCP\Server::get(ITagManager::class), + $request, + \OCP\Server::get(IPreview::class), + \OCP\Server::get(IEventDispatcher::class), + $l10nFactory->get('dav'), +); + + +$linkCheckPlugin = new \OCA\DAV\Files\Sharing\PublicLinkCheckPlugin(); +$filesDropPlugin = new \OCA\DAV\Files\Sharing\FilesDropPlugin(); + +// Define root url with /public.php/dav/files/TOKEN +/** @var string $baseuri defined in public.php */ +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'); + $federatedShareProvider = \OCP\Server::get(FederatedShareProvider::class); + if ($federatedShareProvider->isOutgoingServer2serverShareEnabled() === false && !$isAjax) { + // this is what is thrown when trying to access a non-existing share + throw new 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 + /** @psalm-suppress InternalMethod */ + $previousLog = Filesystem::logWarningWhenAddingStorageWrapper(false); + + /** @psalm-suppress MissingClosureParamType */ + Filesystem::addStorageWrapper('sharePermissions', function ($mountPoint, $storage) use ($share) { + return new PermissionsMask(['storage' => $storage, 'mask' => $share->getPermissions() | \OCP\Constants::PERMISSION_SHARE]); + }); + + /** @psalm-suppress MissingClosureParamType */ + Filesystem::addStorageWrapper('shareOwner', function ($mountPoint, $storage) use ($share) { + return new PublicOwnerWrapper(['storage' => $storage, 'owner' => $share->getShareOwner()]); + }); + + /** @psalm-suppress InternalMethod */ + Filesystem::logWarningWhenAddingStorageWrapper($previousLog); + + OC_Util::tearDownFS(); + OC_Util::setupFS($owner); + $ownerView = new View('/'. $owner . '/files'); + $path = $ownerView->getPath($fileId); + $fileInfo = $ownerView->getFileInfo($path); + + if ($fileInfo === false) { + throw new NotFound(); + } + + $linkCheckPlugin->setFileInfo($fileInfo); + + // If not readble (files_drop) enable the filesdrop plugin + if (!$isReadable) { + $filesDropPlugin->enable(); + } + + $view = new 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 72455e6bf6779..d4305195d461e 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -149,7 +149,7 @@ '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 @@ '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 0d715f510f778..9afd73635ffd1 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/PublicAuth.php b/apps/dav/lib/Connector/LegacyPublicAuth.php similarity index 92% rename from apps/dav/lib/Connector/PublicAuth.php rename to apps/dav/lib/Connector/LegacyPublicAuth.php index d613a5a188f1b..c2cc81103bd4f 100644 --- a/apps/dav/lib/Connector/PublicAuth.php +++ b/apps/dav/lib/Connector/LegacyPublicAuth.php @@ -29,6 +29,7 @@ */ namespace OCA\DAV\Connector; +use OCA\DAV\Connector\Sabre\PublicAuth; use OCP\IRequest; use OCP\ISession; use OCP\Security\Bruteforce\IThrottler; @@ -42,8 +43,9 @@ * * @package OCA\DAV\Connector */ -class PublicAuth extends AbstractBasic { - private const BRUTEFORCE_ACTION = 'public_webdav_auth'; +class LegacyPublicAuth extends AbstractBasic { + private const BRUTEFORCE_ACTION = 'legacy_public_webdav_auth'; + private ?IShare $share = null; private IManager $shareManager; private ISession $session; @@ -72,6 +74,7 @@ public function __construct(IRequest $request, * * @param string $username * @param string $password + * * @return bool * @throws \Sabre\DAV\Exception\NotAuthenticated */ @@ -96,8 +99,8 @@ protected function validateUserPass($username, $password) { || $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()) { + } 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')))) { diff --git a/apps/dav/lib/Connector/Sabre/PublicAuth.php b/apps/dav/lib/Connector/Sabre/PublicAuth.php new file mode 100644 index 0000000000000..d5b3d41e1ef31 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/PublicAuth.php @@ -0,0 +1,239 @@ + + * @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 = null; + private IManager $shareManager; + private ISession $session; + private IRequest $request; + private IThrottler $throttler; + private LoggerInterface $logger; + + public function __construct(IRequest $request, + IManager $shareManager, + ISession $session, + IThrottler $throttler, + LoggerInterface $logger) { + $this->request = $request; + $this->shareManager = $shareManager; + $this->session = $session; + $this->throttler = $throttler; + $this->logger = $logger; + + // 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(); + $this->logger->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 { + /** @var IShare $share */ + $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) { + $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 ($this->session->exists(PublicAuth::DAV_AUTHENTICATED) + && $this->session->get(PublicAuth::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; + } + + return true; + } + + public function getShare(): IShare { + assert($this->share !== null); + return $this->share; + } +} diff --git a/apps/dav/tests/unit/Connector/PublicAuthTest.php b/apps/dav/tests/unit/Connector/LegacyPublicAuthTest.php similarity index 96% rename from apps/dav/tests/unit/Connector/PublicAuthTest.php rename to apps/dav/tests/unit/Connector/LegacyPublicAuthTest.php index 4a2ebb496b0cb..1c83beb8389bf 100644 --- a/apps/dav/tests/unit/Connector/PublicAuthTest.php +++ b/apps/dav/tests/unit/Connector/LegacyPublicAuthTest.php @@ -34,13 +34,13 @@ use OCP\Share\IShare; /** - * Class PublicAuthTest + * Class LegacyPublicAuthTest * * @group DB * * @package OCA\DAV\Tests\unit\Connector */ -class PublicAuthTest extends \Test\TestCase { +class LegacyPublicAuthTest extends \Test\TestCase { /** @var ISession|\PHPUnit\Framework\MockObject\MockObject */ private $session; @@ -48,7 +48,7 @@ class PublicAuthTest extends \Test\TestCase { private $request; /** @var IManager|\PHPUnit\Framework\MockObject\MockObject */ private $shareManager; - /** @var \OCA\DAV\Connector\PublicAuth */ + /** @var \OCA\DAV\Connector\LegacyPublicAuth */ private $auth; /** @var IThrottler|\PHPUnit\Framework\MockObject\MockObject */ private $throttler; @@ -72,7 +72,7 @@ protected function setUp(): void { ->disableOriginalConstructor() ->getMock(); - $this->auth = new \OCA\DAV\Connector\PublicAuth( + $this->auth = new \OCA\DAV\Connector\LegacyPublicAuth( $this->request, $this->shareManager, $this->session, @@ -195,7 +195,7 @@ public function testSharePasswordMailValidPassword(): void { $this->assertTrue($result); } - public function testSharePasswordLinkValidSession(): void { + public function testInvalidSharePasswordLinkValidSession(): void { $share = $this->getMockBuilder(IShare::class) ->disableOriginalConstructor() ->getMock(); diff --git a/apps/dav/tests/unit/Connector/Sabre/PublicAuthTest.php b/apps/dav/tests/unit/Connector/Sabre/PublicAuthTest.php new file mode 100644 index 0000000000000..ce0ff72940323 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/PublicAuthTest.php @@ -0,0 +1,425 @@ + + * @author Joas Schilling + * @author Lukas Reschke + * @author Morris Jobke + * @author Roeland Jago Douma + * @author Thomas Müller + * + * @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\Tests\unit\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 Psr\Log\LoggerInterface; + +/** + * Class PublicAuthTest + * + * @group DB + * + * @package OCA\DAV\Tests\unit\Connector + */ +class PublicAuthTest extends \Test\TestCase { + + /** @var ISession|MockObject */ + private $session; + /** @var IRequest|MockObject */ + private $request; + /** @var IManager|MockObject */ + private $shareManager; + /** @var PublicAuth */ + private $auth; + /** @var IThrottler|MockObject */ + private $throttler; + /** @var LoggerInterface|MockObject */ + private $logger; + + /** @var string */ + private $oldUser; + + protected function setUp(): void { + parent::setUp(); + + $this->session = $this->createMock(ISession::class); + $this->request = $this->createMock(IRequest::class); + $this->shareManager = $this->createMock(IManager::class); + $this->throttler = $this->createMock(IThrottler::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->auth = new \OCA\DAV\Connector\Sabre\PublicAuth( + $this->request, + $this->shareManager, + $this->session, + $this->throttler, + $this->logger, + ); + + // Store current user + $this->oldUser = \OC_User::getUser(); + } + + protected function tearDown(): void { + \OC_User::setIncognitoMode(false); + + // Set old user + \OC_User::setUserId($this->oldUser); + \OC_Util::setupFS($this->oldUser); + + parent::tearDown(); + } + + public function testGetToken(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $result = $this->invokePrivate($this->auth, 'getToken'); + + $this->assertSame('GX9HSGQrGE', $result); + } + + public function testGetTokenInvalid(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files'); + + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + $this->invokePrivate($this->auth, 'getToken'); + } + + public function testCheckTokenValidShare(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->getMockBuilder(IShare::class) + ->disableOriginalConstructor() + ->getMock(); + $share->method('getPassword')->willReturn(null); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $result = $this->invokePrivate($this->auth, 'checkToken'); + $this->assertSame([true, 'principals/GX9HSGQrGE'], $result); + } + + public function testCheckTokenInvalidShare(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $this->shareManager + ->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->will($this->throwException(new ShareNotFound())); + + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + $this->invokePrivate($this->auth, 'checkToken'); + } + + public function testCheckTokenAlreadyAuthenticated(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->getMockBuilder(IShare::class) + ->disableOriginalConstructor() + ->getMock(); + $share->method('getShareType')->willReturn(42); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $this->session->method('exists')->with('public_link_authenticated')->willReturn(true); + $this->session->method('get')->with('public_link_authenticated')->willReturn('42'); + + $result = $this->invokePrivate($this->auth, 'checkToken'); + $this->assertSame([true, 'principals/GX9HSGQrGE'], $result); + } + + public function testCheckTokenPasswordNotAuthenticated(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->getMockBuilder(IShare::class) + ->disableOriginalConstructor() + ->getMock(); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(42); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $this->session->method('exists')->with('public_link_authenticated')->willReturn(false); + + $this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class); + $this->invokePrivate($this->auth, 'checkToken'); + } + + public function testCheckTokenPasswordAuthenticatedWrongShare(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->getMockBuilder(IShare::class) + ->disableOriginalConstructor() + ->getMock(); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(42); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $this->session->method('exists')->with('public_link_authenticated')->willReturn(false); + $this->session->method('get')->with('public_link_authenticated')->willReturn('43'); + + $this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class); + $this->invokePrivate($this->auth, 'checkToken'); + } + + public function testNoShare(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willThrowException(new ShareNotFound()); + + $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertFalse($result); + } + + public function testShareNoPassword(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->getMockBuilder(IShare::class) + ->disableOriginalConstructor() + ->getMock(); + $share->method('getPassword')->willReturn(null); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertTrue($result); + } + + public function testSharePasswordFancyShareType(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->getMockBuilder(IShare::class) + ->disableOriginalConstructor() + ->getMock(); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(42); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertFalse($result); + } + + + public function testSharePasswordRemote(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->getMockBuilder(IShare::class) + ->disableOriginalConstructor() + ->getMock(); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(IShare::TYPE_REMOTE); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertTrue($result); + } + + public function testSharePasswordLinkValidPassword(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->getMockBuilder(IShare::class) + ->disableOriginalConstructor() + ->getMock(); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(IShare::TYPE_LINK); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $this->shareManager->expects($this->once()) + ->method('checkPassword')->with( + $this->equalTo($share), + $this->equalTo('password') + )->willReturn(true); + + $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertTrue($result); + } + + public function testSharePasswordMailValidPassword(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->getMockBuilder(IShare::class) + ->disableOriginalConstructor() + ->getMock(); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(IShare::TYPE_EMAIL); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $this->shareManager->expects($this->once()) + ->method('checkPassword')->with( + $this->equalTo($share), + $this->equalTo('password') + )->willReturn(true); + + $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertTrue($result); + } + + public function testInvalidSharePasswordLinkValidSession(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->getMockBuilder(IShare::class) + ->disableOriginalConstructor() + ->getMock(); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(IShare::TYPE_LINK); + $share->method('getId')->willReturn('42'); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $this->shareManager->expects($this->once()) + ->method('checkPassword') + ->with( + $this->equalTo($share), + $this->equalTo('password') + )->willReturn(false); + + $this->session->method('exists')->with('public_link_authenticated')->willReturn(true); + $this->session->method('get')->with('public_link_authenticated')->willReturn('42'); + + $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertTrue($result); + } + + public function testSharePasswordLinkInvalidSession(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->getMockBuilder(IShare::class) + ->disableOriginalConstructor() + ->getMock(); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(IShare::TYPE_LINK); + $share->method('getId')->willReturn('42'); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $this->shareManager->expects($this->once()) + ->method('checkPassword') + ->with( + $this->equalTo($share), + $this->equalTo('password') + )->willReturn(false); + + $this->session->method('exists')->with('public_link_authenticated')->willReturn(true); + $this->session->method('get')->with('public_link_authenticated')->willReturn('43'); + + $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertFalse($result); + } + + + public function testSharePasswordMailInvalidSession(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->getMockBuilder(IShare::class) + ->disableOriginalConstructor() + ->getMock(); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(IShare::TYPE_EMAIL); + $share->method('getId')->willReturn('42'); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $this->shareManager->expects($this->once()) + ->method('checkPassword') + ->with( + $this->equalTo($share), + $this->equalTo('password') + )->willReturn(false); + + $this->session->method('exists')->with('public_link_authenticated')->willReturn(true); + $this->session->method('get')->with('public_link_authenticated')->willReturn('43'); + + $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertFalse($result); + } +} diff --git a/apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php b/apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php index f13a77d936858..7e73f89ad4146 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 @@ public function createFederatedShare($shareWith, $token, $password = '') { // 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/js/files_drop.js b/apps/files_sharing/js/files_drop.js index ffbe828443312..5215cfc5b5415 100644 --- a/apps/files_sharing/js/files_drop.js +++ b/apps/files_sharing/js/files_drop.js @@ -24,10 +24,9 @@ var filesClient = new OC.Files.Client({ host: OC.getHost(), port: OC.getPort(), - userName: $('#sharingToken').val(), // note: password not be required, the endpoint // will recognize previous validation from the session - root: OC.getRootPath() + '/public.php/webdav', + root: OC.getRootPath() + '/public.php/dav/files/' + $('#sharingToken').val() + '/', useHTTPS: OC.getProtocol() === 'https' }); @@ -45,7 +44,7 @@ return false; } var base = OC.getProtocol() + '://' + OC.getHost(); - data.url = base + OC.getRootPath() + '/public.php/webdav/' + encodeURI(name); + data.url = base + OC.getRootPath() + '/public.php/dav/files/' + $('#sharingToken').val() + '/' + encodeURI(name); data.multipart = false; @@ -53,14 +52,6 @@ data.headers = {}; } - var userName = filesClient.getUserName(); - var password = filesClient.getPassword(); - if (userName) { - // copy username/password from DAV client - data.headers['Authorization'] = - 'Basic ' + btoa(userName + ':' + (password || '')); - } - $('#drop-upload-done-indicator').addClass('hidden'); $('#drop-upload-progress-indicator').removeClass('hidden'); diff --git a/apps/files_sharing/js/public.js b/apps/files_sharing/js/public.js index 154b970c5be33..d26cecd02ee17 100644 --- a/apps/files_sharing/js/public.js +++ b/apps/files_sharing/js/public.js @@ -69,10 +69,9 @@ OCA.Sharing.PublicApp = { var filesClient = new OC.Files.Client({ host: OC.getHost(), port: OC.getPort(), - userName: token, // note: password not be required, the endpoint // will recognize previous validation from the session - root: OC.getRootPath() + '/public.php/webdav', + root: OC.getRootPath() + '/public.php/dav/files/' + token + '/', useHTTPS: OC.getProtocol() === 'https' }); @@ -167,11 +166,10 @@ OCA.Sharing.PublicApp = { return; } // Undocumented Url to public WebDAV endpoint - var url = parent.location.protocol + '//' + location.host + OC.linkTo('', 'public.php/webdav'); + var url = parent.location.protocol + '//' + location.host + OC.linkTo('', 'public.php/dav/files/'+ token); $.ajax({ url: url, headers: { - Authorization: 'Basic ' + btoa(token + ':'), Range: 'bytes=0-10000' } }).then(function (data) { @@ -247,7 +245,9 @@ OCA.Sharing.PublicApp = { // also add auth in URL due to POST workaround base = OC.getProtocol() + '://' + token + '@' + OC.getHost() + (OC.getPort() ? ':' + OC.getPort() : ''); } - return base + OC.getRootPath() + '/public.php/webdav' + encodedPath; + + // encodedPath starts with a leading slash + return base + OC.getRootPath() + '/public.php/dav/files/' + token + encodedPath; }; this.fileList.getAjaxUrl = function (action, params) { diff --git a/apps/files_sharing/lib/Controller/PublicPreviewController.php b/apps/files_sharing/lib/Controller/PublicPreviewController.php index 54a1a32166561..c59baac73c41d 100644 --- a/apps/files_sharing/lib/Controller/PublicPreviewController.php +++ b/apps/files_sharing/lib/Controller/PublicPreviewController.php @@ -61,7 +61,7 @@ public function __construct(string $appName, $this->previewManager = $previewManager; } - protected function getPasswordHash(): string { + protected function getPasswordHash(): ?string { return $this->share->getPassword(); } diff --git a/apps/files_sharing/lib/Controller/ShareController.php b/apps/files_sharing/lib/Controller/ShareController.php index 835d08c446ca9..eebf964c8f994 100644 --- a/apps/files_sharing/lib/Controller/ShareController.php +++ b/apps/files_sharing/lib/Controller/ShareController.php @@ -46,6 +46,7 @@ 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; @@ -203,7 +204,7 @@ protected function verifyPassword(string $password): bool { return $this->shareManager->checkPassword($this->share, $password); } - protected function getPasswordHash(): string { + protected function getPasswordHash(): ?string { return $this->share->getPassword(); } @@ -222,8 +223,12 @@ protected function isPasswordProtected(): bool { } 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/apps/files_sharing/lib/ResponseDefinitions.php b/apps/files_sharing/lib/ResponseDefinitions.php index aa1ee004971a5..313cf82bd51ca 100644 --- a/apps/files_sharing/lib/ResponseDefinitions.php +++ b/apps/files_sharing/lib/ResponseDefinitions.php @@ -50,18 +50,18 @@ * mimetype: string, * note: string, * parent: null, - * password?: string, + * password?: null|string, * password_expiration_time?: ?string, * path: ?string, * permissions: int, * send_password_by_talk?: bool, * share_type: int, - * share_with?: string, + * share_with?: null|string, * share_with_avatar?: string, * share_with_displayname?: string, * share_with_displayname_unique?: ?string, * share_with_link?: string, - * status?: array{clearAt?: int|null, icon?: ?string, message?: ?string, status?: string}, + * status?: array{clearAt: int|null, icon: ?string, message: ?string, status: string}, * stime: int, * storage: int, * storage_id: string, diff --git a/apps/files_sharing/openapi.json b/apps/files_sharing/openapi.json index bc8128df0d20c..abbc3d250a6ea 100644 --- a/apps/files_sharing/openapi.json +++ b/apps/files_sharing/openapi.json @@ -598,7 +598,8 @@ "nullable": true }, "password": { - "type": "string" + "type": "string", + "nullable": true }, "password_expiration_time": { "type": "string", @@ -620,7 +621,8 @@ "format": "int64" }, "share_with": { - "type": "string" + "type": "string", + "nullable": true }, "share_with_avatar": { "type": "string" @@ -637,6 +639,12 @@ }, "status": { "type": "object", + "required": [ + "clearAt", + "icon", + "message", + "status" + ], "properties": { "clearAt": { "type": "integer", diff --git a/apps/files_sharing/tests/js/fileDropSpec.js b/apps/files_sharing/tests/js/fileDropSpec.js index efe87802d1cc0..18eee661e7fad 100644 --- a/apps/files_sharing/tests/js/fileDropSpec.js +++ b/apps/files_sharing/tests/js/fileDropSpec.js @@ -91,8 +91,7 @@ describe("files Drop tests", function() { OCA.FilesSharingDrop.addFileToUpload('',data); expect(data.submit.calledOnce).toEqual(true); - expect(data.url).toContain("/public.php/webdav/" + encodeURI(testFile.name)); - expect(data.headers['Authorization']).toEqual('Basic ' + btoa(sharingToken+":")); + expect(data.url).toContain("/public.php/dav/files/" + sharingToken + '/' + encodeURI(testFile.name)); }); } }); diff --git a/apps/files_sharing/tests/js/publicAppSpec.js b/apps/files_sharing/tests/js/publicAppSpec.js index 59ac4bd7bbd04..229e57ac4ed1e 100644 --- a/apps/files_sharing/tests/js/publicAppSpec.js +++ b/apps/files_sharing/tests/js/publicAppSpec.js @@ -107,8 +107,7 @@ describe('OCA.Sharing.PublicApp tests', function() { App.initialize($('#preview')); expect(fakeServer.requests.length).toEqual(1); expect(fakeServer.requests[0].method).toEqual('PROPFIND'); - expect(fakeServer.requests[0].url).toEqual('https://example.com:9876/owncloud/public.php/webdav/subdir'); - expect(fakeServer.requests[0].requestHeaders.Authorization).toEqual('Basic c2g0dG9rOm51bGw='); + expect(fakeServer.requests[0].url).toEqual('https://example.com:9876/owncloud/public.php/dav/files/sh4tok/subdir'); uploaderDetectStub.restore(); }); @@ -149,11 +148,11 @@ describe('OCA.Sharing.PublicApp tests', function() { }); it('returns correct upload URL', function() { expect(fileList.getUploadUrl('some file.txt')) - .toEqual('/owncloud/public.php/webdav/subdir/some%20file.txt'); + .toEqual('/owncloud/public.php/dav/files/sh4tok/subdir/some%20file.txt'); }); it('returns correct upload URL with specified dir', function() { expect(fileList.getUploadUrl('some file.txt', 'sub')) - .toEqual('/owncloud/public.php/webdav/sub/some%20file.txt'); + .toEqual('/owncloud/public.php/dav/files/sh4tok/sub/some%20file.txt'); }); }); }); diff --git a/build/integration/features/bootstrap/FilesDropContext.php b/build/integration/features/bootstrap/FilesDropContext.php index a5d4dad14e3df..22eaa63903069 100644 --- a/build/integration/features/bootstrap/FilesDropContext.php +++ b/build/integration/features/bootstrap/FilesDropContext.php @@ -45,9 +45,8 @@ public function droppingFileWith($path, $content) { } $base = substr($this->baseUrl, 0, -4); - $fullUrl = $base . '/public.php/webdav' . $path; + $fullUrl = $base . "/public.php/dav/files/$token/$path"; - $options['auth'] = [$token, '']; $options['headers'] = [ 'X-REQUESTED-WITH' => 'XMLHttpRequest' ]; @@ -73,9 +72,8 @@ public function creatingFolderInDrop($folder) { } $base = substr($this->baseUrl, 0, -4); - $fullUrl = $base . '/public.php/webdav/' . $folder; + $fullUrl = $base . "/public.php/dav/files/$token/$folder"; - $options['auth'] = [$token, '']; $options['headers'] = [ 'X-REQUESTED-WITH' => 'XMLHttpRequest' ]; diff --git a/build/integration/features/bootstrap/Sharing.php b/build/integration/features/bootstrap/Sharing.php index a38ef392e61f0..7993d502707be 100644 --- a/build/integration/features/bootstrap/Sharing.php +++ b/build/integration/features/bootstrap/Sharing.php @@ -187,8 +187,8 @@ public function lastShareWithPasswordCanBeDownloaded($password) { $token = $this->lastShareData->data->token; } - $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/webdav"; - $this->checkDownload($fullUrl, [$token, $password], 'text/plain'); + $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token/"; + $this->checkDownload($fullUrl, ['', $password], 'text/plain'); } private function checkDownload($url, $auth = null, $mimeType = null) { diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php index c239461b78864..b919976e790d3 100644 --- a/build/integration/features/bootstrap/WebDav.php +++ b/build/integration/features/bootstrap/WebDav.php @@ -169,11 +169,10 @@ public function downloadFileWithRange($fileSource, $range) { */ public function downloadPublicFileWithRange($range) { $token = $this->lastShareData->data->token; - $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/webdav"; + $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token"; $client = new GClient(); $options = []; - $options['auth'] = [$token, ""]; $options['headers'] = [ 'Range' => $range ]; @@ -187,7 +186,7 @@ public function downloadPublicFileWithRange($range) { */ public function downloadPublicFileInsideAFolderWithRange($path, $range) { $token = $this->lastShareData->data->token; - $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/webdav" . "$path"; + $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token/$path"; $client = new GClient(); $options = [ @@ -195,7 +194,6 @@ public function downloadPublicFileInsideAFolderWithRange($path, $range) { 'Range' => $range ] ]; - $options['auth'] = [$token, ""]; $this->response = $client->request("GET", $fullUrl, $options); } diff --git a/build/integration/features/maintenance-mode.feature b/build/integration/features/maintenance-mode.feature index 56d3b9c0fb6a7..e6637a5edff1d 100644 --- a/build/integration/features/maintenance-mode.feature +++ b/build/integration/features/maintenance-mode.feature @@ -39,3 +39,9 @@ Feature: maintenance-mode Then the HTTP status code should be "503" Then Maintenance mode is disabled And the command was successful + + Scenario: Accessing /public.php/dav with maintenance mode enabled + When requesting "/public.php/dav" with "GET" + Then the HTTP status code should be "503" + Then Maintenance mode is disabled + And the command was successful diff --git a/lib/public/AppFramework/PublicShareController.php b/lib/public/AppFramework/PublicShareController.php index 7c3725c9290a4..cbcb934319875 100644 --- a/lib/public/AppFramework/PublicShareController.php +++ b/lib/public/AppFramework/PublicShareController.php @@ -86,7 +86,7 @@ final public function getToken(): string { * * @since 14.0.0 */ - abstract protected function getPasswordHash(): string; + abstract protected function getPasswordHash(): ?string; /** * Is the provided token a valid token diff --git a/lib/public/Share/IShare.php b/lib/public/Share/IShare.php index e5a943b0bac89..74d404101cd03 100644 --- a/lib/public/Share/IShare.php +++ b/lib/public/Share/IShare.php @@ -474,7 +474,7 @@ public function setPassword($password); * 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 2956d7f79dd81..9f3ffcec5e919 100644 --- a/public.php +++ b/public.php @@ -32,33 +32,53 @@ */ require_once __DIR__ . '/lib/versioncheck.php'; +/** + * @param $service + * @return string + */ +function resolveService(string $service): string { + $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 (!$file) { + throw new RemoteException('Path not found', 404); + } + + $file = ltrim($file, '/'); $parts = explode('/', $file, 2); $app = $parts[0]; @@ -70,15 +90,13 @@ 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) {