feat: public dav endpoint v2
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>pull/32400/head
parent
fdc64ea2f5
commit
7b6a650b6e
@ -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();
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue