feat: public dav endpoint v2

Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
This commit is contained in:
John Molakvoæ 2022-05-15 10:38:55 +02:00
parent fdc64ea2f5
commit 7b6a650b6e
No known key found for this signature in database
GPG Key ID: 60C25B8C072916CF
11 changed files with 413 additions and 35 deletions

View File

@ -86,8 +86,4 @@
<provider>OCA\DAV\CardDAV\Activity\Provider\Card</provider>
</providers>
</activity>
<public>
<webdav>appinfo/v1/publicwebdav.php</webdav>
</public>
</info>

View File

@ -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(),

View File

@ -0,0 +1,122 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Björn Schießle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
* @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>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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/>
*
*/
// 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();

View File

@ -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',

View File

@ -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',

View File

@ -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 @@ use Sabre\DAV\Auth\Backend\AbstractBasic;
*
* @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 @@ class PublicAuth extends AbstractBasic {
*
* @param string $username
* @param string $password
*
* @return bool
* @throws \Sabre\DAV\Exception\NotAuthenticated
*/
@ -96,8 +99,8 @@ class PublicAuth extends AbstractBasic {
|| $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')))) {

View File

@ -0,0 +1,231 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Björn Schießle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Maxence Lange <maxence@artificial-owl.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\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;
}
}

View File

@ -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(

View File

@ -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() {

View File

@ -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();

View File

@ -32,34 +32,54 @@
*/
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 === false || $pathInfo === '') {
throw new RemoteException('Path not found', 404);
}
if (!$pos = strpos($pathInfo, '/', 1)) {
$pos = strlen($pathInfo);
}
$service = substr($pathInfo, 1, $pos - 1);
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);
}
$file = \OC::$server->getConfig()->getAppValue('core', 'public_' . strip_tags($service));
if ($file === '') {
http_response_code(404);
exit;
$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) {