mirror of
https://github.com/nextcloud/server.git
synced 2026-06-11 01:30:50 -04:00
Enabled CORS on webdav and ocs
* Exclude DAV CORS handling when no Origin specified
This will exclude non-browser clients from CORS handling.
Fixes some clients like davfs which break when CORS is enabled.
* fix: CORS on WebDAV is not working
WebDAV is not working at all when used by on browser Javascript because the CORS headers
are only present in the OPTION request, but not in the subsequent WebDAV methods.
* This behavior is caused by a erroneous json_decode call while retriving the user's domains whitelist.
It return an object, so the is_array always fails and no header are sent.
* Add Access-Control-Expose-Headers - to allow clients to access certain headers
* Adding many headers as allowed headers + add capability to read additional allowed headers from config.php
This commit is contained in:
parent
4ddbc55d5d
commit
c1fa6401a3
11 changed files with 1079 additions and 52 deletions
189
apps/dav/lib/Connector/Sabre/CorsPlugin.php
Normal file
189
apps/dav/lib/Connector/Sabre/CorsPlugin.php
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
<?php
|
||||
/**
|
||||
* @author Noveen Sachdeva <noveen.sachdeva@research.iiit.ac.in>
|
||||
*
|
||||
* @copyright Copyright (c) 2018, ownCloud GmbH
|
||||
* @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\IUserSession;
|
||||
use OCP\Util;
|
||||
use Sabre\DAV\ServerPlugin;
|
||||
use Sabre\HTTP\RequestInterface;
|
||||
use Sabre\HTTP\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Class CorsPlugin is a plugin which adds CORS headers to the responses
|
||||
*/
|
||||
class CorsPlugin extends ServerPlugin {
|
||||
/**
|
||||
* Reference to main server object
|
||||
*
|
||||
* @var \Sabre\DAV\Server
|
||||
*/
|
||||
private $server;
|
||||
|
||||
/**
|
||||
* Reference to logged in user's session
|
||||
*
|
||||
* @var IUserSession
|
||||
*/
|
||||
private $userSession;
|
||||
|
||||
/** @var array */
|
||||
private $extraHeaders;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $alreadyExecuted = false;
|
||||
|
||||
/**
|
||||
* @param IUserSession $userSession
|
||||
*/
|
||||
public function __construct(IUserSession $userSession) {
|
||||
$this->userSession = $userSession;
|
||||
}
|
||||
|
||||
private function getExtraHeaders(RequestInterface $request) {
|
||||
if ($this->extraHeaders === null) {
|
||||
if ($this->userSession->getUser() === null) {
|
||||
$this->extraHeaders['Access-Control-Allow-Methods'] = [
|
||||
'OPTIONS',
|
||||
'GET',
|
||||
'HEAD',
|
||||
'DELETE',
|
||||
'PROPFIND',
|
||||
'PUT',
|
||||
'PROPPATCH',
|
||||
'COPY',
|
||||
'MOVE',
|
||||
'REPORT',
|
||||
'SEARCH',
|
||||
];
|
||||
} else {
|
||||
$this->extraHeaders['Access-Control-Allow-Methods'] = $this->server->getAllowedMethods($request->getPath());
|
||||
}
|
||||
}
|
||||
return $this->extraHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* This initializes the plugin.
|
||||
*
|
||||
* This function is called by \Sabre\DAV\Server, after
|
||||
* addPlugin is called.
|
||||
*
|
||||
* This method should set up the required event subscriptions.
|
||||
*
|
||||
* @param \Sabre\DAV\Server $server
|
||||
* @return void
|
||||
*/
|
||||
public function initialize(\Sabre\DAV\Server $server) {
|
||||
$this->server = $server;
|
||||
|
||||
$request = $this->server->httpRequest;
|
||||
if (!$request->hasHeader('Origin')) {
|
||||
return;
|
||||
}
|
||||
$originHeader = $request->getHeader('Origin');
|
||||
if ($this->ignoreOriginHeader($originHeader)) {
|
||||
return;
|
||||
}
|
||||
if (Util::isSameDomain($originHeader, $request->getAbsoluteUrl())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->server->on('beforeMethod:*', [$this, 'setCorsHeaders']);
|
||||
$this->server->on('exception', [$this, 'onException']);
|
||||
$this->server->on('beforeMethod:OPTIONS', [$this, 'setOptionsRequestHeaders'], 5);
|
||||
}
|
||||
|
||||
public function onException(\Throwable $ex) {
|
||||
$this->setCorsHeaders($this->server->httpRequest, $this->server->httpResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method sets the cors headers for all requests
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @return void
|
||||
*/
|
||||
public function setCorsHeaders(RequestInterface $request, ResponseInterface $response) {
|
||||
if ($request->getHeader('origin') === null) {
|
||||
return;
|
||||
}
|
||||
if ($this->alreadyExecuted) {
|
||||
return;
|
||||
}
|
||||
$this->alreadyExecuted = true;
|
||||
$requesterDomain = $request->getHeader('origin');
|
||||
// unauthenticated request shall add cors headers as well
|
||||
$userId = null;
|
||||
if ($this->userSession->getUser() !== null) {
|
||||
$userId = $this->userSession->getUser()->getUID();
|
||||
}
|
||||
|
||||
$headers = \OC_Response::setCorsHeaders($userId, $requesterDomain, null, $this->getExtraHeaders($request));
|
||||
foreach ($headers as $key => $value) {
|
||||
$response->addHeader($key, \implode(',', $value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the OPTIONS request
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
*
|
||||
* @return false|void
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function setOptionsRequestHeaders(RequestInterface $request, ResponseInterface $response) {
|
||||
$authorization = $request->getHeader('Authorization');
|
||||
if ($authorization === null || $authorization === '') {
|
||||
// Set the proper response
|
||||
$response->setStatus(200);
|
||||
$response = \OC_Response::setOptionsRequestHeaders($response, $this->getExtraHeaders($request));
|
||||
|
||||
// Since All OPTIONS requests are unauthorized, we will have to return false from here
|
||||
// If we don't return false, due to no authorization, a 401-Unauthorized will be thrown
|
||||
// Which we don't want here
|
||||
// Hence this sendResponse
|
||||
$this->server->sapi->sendResponse($response);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* in addition to schemas used by extensions we ignore empty origin header
|
||||
* values as well as 'null' which is not valid by the specification but used
|
||||
* by some clients.
|
||||
* @link https://github.com/owncloud/core/pull/32120#issuecomment-407008243
|
||||
*
|
||||
* @param string $originHeader
|
||||
* @return bool
|
||||
*/
|
||||
public function ignoreOriginHeader($originHeader) {
|
||||
if (\in_array($originHeader, ['', null, 'null'], true)) {
|
||||
return true;
|
||||
}
|
||||
$schema = \parse_url($originHeader, PHP_URL_SCHEME);
|
||||
return \in_array(\strtolower($schema), ['moz-extension', 'chrome-extension']);
|
||||
}
|
||||
}
|
||||
|
|
@ -100,6 +100,7 @@ class ServerFactory {
|
|||
|
||||
// Load plugins
|
||||
$server->addPlugin(new \OCA\DAV\Connector\Sabre\MaintenancePlugin($this->config, $this->l10n));
|
||||
$server->addPlugin(new \OCA\DAV\Connector\Sabre\CorsPlugin($this->userSession));
|
||||
$server->addPlugin(new \OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin($this->config));
|
||||
$server->addPlugin(new \OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin());
|
||||
$server->addPlugin($authPlugin);
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ use OCA\DAV\Connector\Sabre\CachingTree;
|
|||
use OCA\DAV\Connector\Sabre\ChecksumUpdatePlugin;
|
||||
use OCA\DAV\Connector\Sabre\CommentPropertiesPlugin;
|
||||
use OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin;
|
||||
use OCA\DAV\Connector\Sabre\CorsPlugin;
|
||||
use OCA\DAV\Connector\Sabre\DavAclPlugin;
|
||||
use OCA\DAV\Connector\Sabre\DummyGetResponsePlugin;
|
||||
use OCA\DAV\Connector\Sabre\FakeLockerPlugin;
|
||||
|
|
@ -130,6 +131,8 @@ class Server {
|
|||
$this->server->addPlugin(new ProfilerPlugin($this->request));
|
||||
$this->server->addPlugin(new BlockLegacyClientPlugin(\OC::$server->getConfig()));
|
||||
$this->server->addPlugin(new AnonymousOptionsPlugin());
|
||||
$this->server->addPlugin(new CorsPlugin(\OC::$server->getUserSession()));
|
||||
|
||||
$authPlugin = new Plugin();
|
||||
$authPlugin->addBackend(new PublicAuth());
|
||||
$this->server->addPlugin($authPlugin);
|
||||
|
|
|
|||
375
apps/dav/tests/unit/Connector/Sabre/CorsPluginTest.php
Normal file
375
apps/dav/tests/unit/Connector/Sabre/CorsPluginTest.php
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
<?php
|
||||
/**
|
||||
* @author Vincent Petry <pvince81@owncloud.com>
|
||||
*
|
||||
* @copyright Copyright (c) 2018, ownCloud GmbH
|
||||
* @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\Tests\unit\Connector\Sabre;
|
||||
|
||||
use OCA\DAV\Connector\Sabre\CorsPlugin;
|
||||
use OCP\IUserSession;
|
||||
use OCP\IUser;
|
||||
use OCP\IConfig;
|
||||
use Sabre\DAV\Server;
|
||||
use Sabre\DAV\ServerPlugin;
|
||||
use Test\TestCase;
|
||||
|
||||
class CorsPluginTest extends TestCase {
|
||||
/**
|
||||
* @var Server
|
||||
*/
|
||||
private $server;
|
||||
|
||||
/**
|
||||
* @var CorsPlugin
|
||||
*/
|
||||
private $plugin;
|
||||
|
||||
/**
|
||||
* @var IUserSession | \PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private $userSession;
|
||||
|
||||
/**
|
||||
* @var IConfig | \PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private $config;
|
||||
|
||||
public function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->server = new Server();
|
||||
|
||||
$this->server->sapi = $this->getMockBuilder(\stdClass::class)
|
||||
->setMethods(['sendResponse'])
|
||||
->getMock();
|
||||
|
||||
$this->server->httpRequest->setMethod('OPTIONS');
|
||||
|
||||
$this->userSession = $this->createMock(IUserSession::class);
|
||||
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->overwriteService('AllConfig', $this->config);
|
||||
|
||||
$this->plugin = new CorsPlugin($this->userSession);
|
||||
|
||||
/** @var ServerPlugin | \PHPUnit\Framework\MockObject\MockObject $extraMethodPlugin */
|
||||
$extraMethodPlugin = $this->createMock(ServerPlugin::class);
|
||||
$extraMethodPlugin->method('getHTTPMethods')
|
||||
->with('owncloud/remote.php/dav/files/user1/target/path')
|
||||
->willReturn(['EXTRA']);
|
||||
$extraMethodPlugin->method('getFeatures')->willReturn([]);
|
||||
|
||||
$this->server->addPlugin($extraMethodPlugin);
|
||||
}
|
||||
|
||||
public function tearDown(): void {
|
||||
$this->restoreService('AllConfig');
|
||||
}
|
||||
|
||||
public function optionsCases() {
|
||||
$allowedDomains = '["https://requesterdomain.tld", "http://anotherdomain.tld"]';
|
||||
|
||||
$allowedHeaders = [
|
||||
'OC-Checksum', 'OC-Total-Length', 'OCS-APIREQUEST', 'X-OC-Mtime',
|
||||
'OC-RequestAppPassword', 'Accept',
|
||||
'Authorization', 'Brief', 'Content-Length', 'Content-Range',
|
||||
'Content-Type', 'Date', 'Depth', 'Destination', 'Host', 'If', 'If-Match',
|
||||
'If-Modified-Since', 'If-None-Match', 'If-Range', 'If-Unmodified-Since',
|
||||
'Location', 'Lock-Token', 'Overwrite', 'Prefer', 'Range', 'Schedule-Reply',
|
||||
'Timeout', 'User-Agent', 'X-Expected-Entity-Length', 'Accept-Language',
|
||||
'Access-Control-Request-Method', 'Access-Control-Allow-Origin', 'Cache-Control', 'ETag',
|
||||
'OC-Autorename', 'OC-CalDav-Import', 'OC-Chunked', 'OC-Etag', 'OC-FileId',
|
||||
'OC-LazyOps', 'OC-Total-File-Length', 'Origin', 'X-Request-ID', 'X-Requested-With'
|
||||
];
|
||||
$allowedMethods = [
|
||||
'GET',
|
||||
'OPTIONS',
|
||||
'POST',
|
||||
'PUT',
|
||||
'DELETE',
|
||||
'MKCOL',
|
||||
'PROPFIND',
|
||||
'PATCH',
|
||||
'PROPPATCH',
|
||||
'REPORT',
|
||||
'HEAD',
|
||||
'COPY',
|
||||
'MOVE',
|
||||
'EXTRA',
|
||||
];
|
||||
$allowedMethodsUnAuthenticated = [
|
||||
'GET',
|
||||
'OPTIONS',
|
||||
'POST',
|
||||
'PUT',
|
||||
'DELETE',
|
||||
'MKCOL',
|
||||
'PROPFIND',
|
||||
'PATCH',
|
||||
'PROPPATCH',
|
||||
'REPORT',
|
||||
'HEAD',
|
||||
'COPY',
|
||||
'MOVE',
|
||||
];
|
||||
|
||||
return [
|
||||
'OPTIONS headers' =>
|
||||
[
|
||||
$allowedDomains,
|
||||
false,
|
||||
[
|
||||
'Origin' => 'https://requesterdomain.tld',
|
||||
],
|
||||
200,
|
||||
[
|
||||
'Access-Control-Allow-Headers' => \implode(',', $allowedHeaders),
|
||||
'Access-Control-Allow-Origin' => '*',
|
||||
'Access-Control-Allow-Methods' => \implode(',', $allowedMethodsUnAuthenticated),
|
||||
],
|
||||
false
|
||||
],
|
||||
'OPTIONS headers with user' =>
|
||||
[
|
||||
$allowedDomains,
|
||||
true,
|
||||
[
|
||||
'Origin' => 'https://requesterdomain.tld',
|
||||
'Authorization' => 'abc',
|
||||
],
|
||||
200,
|
||||
[
|
||||
'Access-Control-Allow-Headers' => \implode(',', $allowedHeaders),
|
||||
'Access-Control-Allow-Origin' => 'https://requesterdomain.tld',
|
||||
'Access-Control-Allow-Methods' => \implode(',', $allowedMethods),
|
||||
],
|
||||
true
|
||||
],
|
||||
'OPTIONS headers no user' =>
|
||||
[
|
||||
$allowedDomains,
|
||||
false,
|
||||
[
|
||||
'Origin' => 'https://requesterdomain.tld',
|
||||
'Authorization' => 'abc',
|
||||
],
|
||||
200,
|
||||
[
|
||||
'Access-Control-Allow-Headers' => null,
|
||||
'Access-Control-Allow-Origin' => null,
|
||||
'Access-Control-Allow-Methods' => null,
|
||||
],
|
||||
true
|
||||
],
|
||||
'OPTIONS headers domain not allowed' =>
|
||||
[
|
||||
'[]',
|
||||
true,
|
||||
[
|
||||
'Origin' => 'https://requesterdomain.tld',
|
||||
'Authorization' => 'abc',
|
||||
],
|
||||
200,
|
||||
[
|
||||
'Access-Control-Allow-Headers' => null,
|
||||
'Access-Control-Allow-Origin' => null,
|
||||
'Access-Control-Allow-Methods' => null,
|
||||
],
|
||||
true
|
||||
],
|
||||
'OPTIONS headers not allowed but no cross-domain' =>
|
||||
[
|
||||
'[]',
|
||||
true,
|
||||
[
|
||||
'Origin' => 'https://requesterdomain.tld',
|
||||
'Authorization' => 'abc',
|
||||
],
|
||||
200,
|
||||
[
|
||||
'Access-Control-Allow-Headers' => null,
|
||||
'Access-Control-Allow-Origin' => null,
|
||||
'Access-Control-Allow-Methods' => null,
|
||||
],
|
||||
true
|
||||
],
|
||||
'OPTIONS headers allowed but no cross-domain' =>
|
||||
[
|
||||
'["currentdomain.tld:8080"]',
|
||||
true,
|
||||
[
|
||||
'Origin' => 'https://currentdomain.tld:8080',
|
||||
'Authorization' => 'abc',
|
||||
],
|
||||
200,
|
||||
[
|
||||
'Access-Control-Allow-Headers' => null,
|
||||
'Access-Control-Allow-Origin' => null,
|
||||
'Access-Control-Allow-Methods' => null,
|
||||
],
|
||||
true
|
||||
],
|
||||
'OPTIONS headers allowed, cross-domain through different port' =>
|
||||
[
|
||||
'["https://currentdomain.tld:8443"]',
|
||||
true,
|
||||
[
|
||||
'Origin' => 'https://currentdomain.tld:8443',
|
||||
'Authorization' => 'abc',
|
||||
],
|
||||
200,
|
||||
[
|
||||
'Access-Control-Allow-Headers' => \implode(',', $allowedHeaders),
|
||||
'Access-Control-Allow-Origin' => 'https://currentdomain.tld:8443',
|
||||
'Access-Control-Allow-Methods' => \implode(',', $allowedMethods),
|
||||
],
|
||||
true
|
||||
],
|
||||
'no Origin header' =>
|
||||
[
|
||||
$allowedDomains,
|
||||
true,
|
||||
[
|
||||
],
|
||||
200,
|
||||
[
|
||||
'Access-Control-Allow-Headers' => null,
|
||||
'Access-Control-Allow-Origin' => null,
|
||||
'Access-Control-Allow-Methods' => null,
|
||||
],
|
||||
true
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider optionsCases
|
||||
* @param $allowedDomains
|
||||
* @param $hasUser
|
||||
* @param $requestHeaders
|
||||
* @param $expectedStatus
|
||||
* @param array $expectedHeaders
|
||||
* @param bool $expectDavHeaders
|
||||
*/
|
||||
public function testOptionsHeaders($allowedDomains, $hasUser, $requestHeaders, $expectedStatus, array $expectedHeaders, $expectDavHeaders = false) {
|
||||
$this->server->sapi->expects($this->once())->method('sendResponse')->with($this->server->httpResponse);
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->method('getUID')->willReturn('someuser');
|
||||
|
||||
if ($hasUser) {
|
||||
$this->userSession->method('getUser')->willReturn($user);
|
||||
} else {
|
||||
$this->userSession->method('getUser')->willReturn(null);
|
||||
}
|
||||
|
||||
$this->config->method('getSystemValue')->willReturn([]);
|
||||
$this->config->method('getUserValue')
|
||||
->with('someuser', 'core', 'domains')
|
||||
->willReturn($allowedDomains);
|
||||
|
||||
$this->server->httpRequest->setHeaders($requestHeaders);
|
||||
$this->server->httpRequest->setAbsoluteUrl('https://currentdomain.tld:8080/owncloud/remote.php/dav/files/user1/target/path');
|
||||
$this->server->httpRequest->setUrl('/owncloud/remote.php/dav/files/user1/target/path');
|
||||
|
||||
$this->server->addPlugin($this->plugin);
|
||||
$this->server->start();
|
||||
|
||||
$this->assertEquals($expectedStatus, $this->server->httpResponse->getStatus());
|
||||
|
||||
foreach ($expectedHeaders as $headerKey => $headerValue) {
|
||||
if ($headerValue !== null) {
|
||||
$this->assertTrue($this->server->httpResponse->hasHeader($headerKey), "Response header \"$headerKey\" exists");
|
||||
} else {
|
||||
$this->assertFalse($this->server->httpResponse->hasHeader($headerKey), "Response header \"$headerKey\" does not exist");
|
||||
}
|
||||
$this->assertEquals($headerValue, $this->server->httpResponse->getHeader($headerKey));
|
||||
}
|
||||
|
||||
// if it has DAV headers, it means we did not bypass further processing
|
||||
$this->assertEquals($expectDavHeaders, $this->server->httpResponse->hasHeader('DAV'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider providesOriginUrls
|
||||
* @param $expectedValue
|
||||
* @param $url
|
||||
*/
|
||||
public function testExtensionRequests($expectedValue, $url) {
|
||||
$plugin = new CorsPlugin($this->createMock(IUserSession::class));
|
||||
self::assertEquals($expectedValue, $plugin->ignoreOriginHeader($url));
|
||||
}
|
||||
|
||||
public function providesOriginUrls() {
|
||||
return [
|
||||
'Firefox extension' => [true, 'moz-extension://mgmnhfbjphngabcpbpmapnnaabhnchmi/'],
|
||||
'Chrome extension' => [true, 'chrome-extension://mgmnhfbjphngabcpbpmapnnaabhnchmi/'],
|
||||
'Empty Origin' => [true, ''],
|
||||
'Null string Origin' => [true, 'null'],
|
||||
'Null Origin' => [true, null],
|
||||
'plain http' => [false, 'http://example.net/'],
|
||||
];
|
||||
}
|
||||
|
||||
public function testAuthenticatedAdditionalAllowedHeaders() {
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->method('getUID')->willReturn('someuser');
|
||||
|
||||
$this->userSession->method('getUser')->willReturn($user);
|
||||
$this->server->httpRequest->setHeader('Origin', 'https://requesterdomain.tld');
|
||||
$this->server->httpRequest->setUrl('/owncloud/remote.php/dav/files/user1/target/path');
|
||||
|
||||
$this->config->method('getSystemValue')->withConsecutive(
|
||||
['cors.allowed-domains', []],
|
||||
['cors.allowed-headers', []]
|
||||
)
|
||||
->willReturnMap([
|
||||
['cors.allowed-domains', [], []],
|
||||
['cors.allowed-headers', [], ['X-Additional-Configured-Header', 'authorization']]
|
||||
]);
|
||||
$this->config->method('getUserValue')->willReturn('["https://requesterdomain.tld"]');
|
||||
|
||||
$this->server->addPlugin($this->plugin);
|
||||
|
||||
$this->plugin->setCorsHeaders($this->server->httpRequest, $this->server->httpResponse);
|
||||
self::assertEquals(
|
||||
'X-Additional-Configured-Header,authorization,OC-Checksum,OC-Total-Length,OCS-APIREQUEST,X-OC-Mtime,OC-RequestAppPassword,Accept,Authorization,Brief,Content-Length,Content-Range,Content-Type,Date,Depth,Destination,Host,If,If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,Location,Lock-Token,Overwrite,Prefer,Range,Schedule-Reply,Timeout,User-Agent,X-Expected-Entity-Length,Accept-Language,Access-Control-Request-Method,Access-Control-Allow-Origin,Cache-Control,ETag,OC-Autorename,OC-CalDav-Import,OC-Chunked,OC-Etag,OC-FileId,OC-LazyOps,OC-Total-File-Length,Origin,X-Request-ID,X-Requested-With',
|
||||
$this->server->httpResponse->getHeader('Access-Control-Allow-Headers')
|
||||
);
|
||||
}
|
||||
|
||||
public function testUnauthenticatedAdditionalAllowedHeaders() {
|
||||
$this->userSession->method('getUser')->willReturn(null);
|
||||
$this->server->httpRequest->setHeader('Origin', 'https://requesterdomain.tld');
|
||||
|
||||
$this->config->method('getSystemValue')->withConsecutive(
|
||||
['cors.allowed-domains', []],
|
||||
['cors.allowed-headers', []]
|
||||
)
|
||||
->willReturnMap([
|
||||
['cors.allowed-domains', [], ['https://requesterdomain.tld']],
|
||||
['cors.allowed-headers', [], ['X-Additional-Configured-Header', 'authorization']]
|
||||
]);
|
||||
|
||||
$this->server->addPlugin($this->plugin);
|
||||
|
||||
$this->plugin->setCorsHeaders($this->server->httpRequest, $this->server->httpResponse);
|
||||
self::assertEquals(
|
||||
'X-Additional-Configured-Header,authorization,OC-Checksum,OC-Total-Length,OCS-APIREQUEST,X-OC-Mtime,OC-RequestAppPassword,Accept,Authorization,Brief,Content-Length,Content-Range,Content-Type,Date,Depth,Destination,Host,If,If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,Location,Lock-Token,Overwrite,Prefer,Range,Schedule-Reply,Timeout,User-Agent,X-Expected-Entity-Length,Accept-Language,Access-Control-Request-Method,Access-Control-Allow-Origin,Cache-Control,ETag,OC-Autorename,OC-CalDav-Import,OC-Chunked,OC-Etag,OC-FileId,OC-LazyOps,OC-Total-File-Length,Origin,X-Request-ID,X-Requested-With',
|
||||
$this->server->httpResponse->getHeader('Access-Control-Allow-Headers')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -234,7 +234,8 @@ class DIContainer extends SimpleContainer implements IAppContainer {
|
|||
$c->get(IRequest::class),
|
||||
$c->get(IControllerMethodReflector::class),
|
||||
$c->get(IUserSession::class),
|
||||
$c->get(IThrottler::class)
|
||||
$c->get(IThrottler::class),
|
||||
$c->get(IConfig::class),
|
||||
)
|
||||
);
|
||||
$dispatcher->registerMiddleware(
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ namespace OC\AppFramework\Middleware\Security;
|
|||
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
|
||||
use OC\AppFramework\Utility\ControllerMethodReflector;
|
||||
use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
|
||||
use OC\User\Session;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\Attribute\CORS;
|
||||
|
|
@ -37,7 +36,9 @@ use OCP\AppFramework\Http\Attribute\PublicPage;
|
|||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\AppFramework\Http\Response;
|
||||
use OCP\AppFramework\Middleware;
|
||||
use OCP\IConfig;
|
||||
use OCP\IRequest;
|
||||
use OCP\IUserSession;
|
||||
use OCP\Security\Bruteforce\IThrottler;
|
||||
use ReflectionMethod;
|
||||
|
||||
|
|
@ -48,23 +49,12 @@ use ReflectionMethod;
|
|||
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
|
||||
*/
|
||||
class CORSMiddleware extends Middleware {
|
||||
/** @var IRequest */
|
||||
private $request;
|
||||
/** @var ControllerMethodReflector */
|
||||
private $reflector;
|
||||
/** @var Session */
|
||||
private $session;
|
||||
/** @var IThrottler */
|
||||
private $throttler;
|
||||
|
||||
public function __construct(IRequest $request,
|
||||
ControllerMethodReflector $reflector,
|
||||
Session $session,
|
||||
IThrottler $throttler) {
|
||||
$this->request = $request;
|
||||
$this->reflector = $reflector;
|
||||
$this->session = $session;
|
||||
$this->throttler = $throttler;
|
||||
public function __construct(private IRequest $request,
|
||||
private ControllerMethodReflector $reflector,
|
||||
private IUserSession $session,
|
||||
private IThrottler $throttler,
|
||||
private IConfig $config,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -135,24 +125,25 @@ class CORSMiddleware extends Middleware {
|
|||
* @throws SecurityException
|
||||
*/
|
||||
public function afterController($controller, $methodName, Response $response) {
|
||||
$userId = !is_null($this->session->getUser()) ? $this->session->getUser()->getUID() : null;
|
||||
|
||||
// only react if it's a CORS request and if the request sends origin and
|
||||
$reflectionMethod = new ReflectionMethod($controller, $methodName);
|
||||
if ($this->request->getHeader("Origin") !== null
|
||||
&& $this->hasAnnotationOrAttribute($reflectionMethod, 'CORS', CORS::class)
|
||||
&& !is_null($userId)) {
|
||||
$requesterDomain = $this->request->getHeader("Origin");
|
||||
\OC_Response::setCorsHeaders($userId, $requesterDomain, $this->config);
|
||||
|
||||
if (isset($this->request->server['HTTP_ORIGIN'])) {
|
||||
$reflectionMethod = new ReflectionMethod($controller, $methodName);
|
||||
if ($this->hasAnnotationOrAttribute($reflectionMethod, 'CORS', CORS::class)) {
|
||||
// allow credentials headers must not be true or CSRF is possible
|
||||
// otherwise
|
||||
foreach ($response->getHeaders() as $header => $value) {
|
||||
if (strtolower($header) === 'access-control-allow-credentials' &&
|
||||
strtolower(trim($value)) === 'true') {
|
||||
$msg = 'Access-Control-Allow-Credentials must not be '.
|
||||
'set to true in order to prevent CSRF';
|
||||
throw new SecurityException($msg);
|
||||
}
|
||||
// allow credentials headers must not be true or CSRF is possible
|
||||
// otherwise
|
||||
foreach ($response->getHeaders() as $header => $value) {
|
||||
if (strtolower($header) === 'access-control-allow-credentials' &&
|
||||
strtolower(trim($value)) === 'true') {
|
||||
$msg = 'Access-Control-Allow-Credentials must not be '.
|
||||
'set to true in order to prevent CSRF';
|
||||
throw new SecurityException($msg);
|
||||
}
|
||||
|
||||
$origin = $this->request->server['HTTP_ORIGIN'];
|
||||
$response->addHeader('Access-Control-Allow-Origin', $origin);
|
||||
}
|
||||
}
|
||||
return $response;
|
||||
|
|
|
|||
|
|
@ -274,6 +274,41 @@ class Router implements IRouter {
|
|||
}
|
||||
|
||||
$matcher = new UrlMatcher($this->root, $this->context);
|
||||
|
||||
if (\OC::$server->getRequest()->getMethod() === "OPTIONS") {
|
||||
try {
|
||||
// Checking whether the actual request (one which OPTIONS is pre-flight for)
|
||||
// Is actually valid
|
||||
$requestingMethod = \OC::$server->getRequest()->getHeader('Access-Control-Request-Method');
|
||||
$tempContext = $this->context;
|
||||
$tempContext->setMethod($requestingMethod);
|
||||
$tempMatcher = new UrlMatcher($this->root, $tempContext);
|
||||
$parameters = $tempMatcher->match($url);
|
||||
|
||||
// Reach here if it's valid
|
||||
$response = new \OC\OCS\Result(null, 100, 'OPTIONS request successful');
|
||||
$response = \OC_Response::setOptionsRequestHeaders($response);
|
||||
\OC_API::respond($response, \OC_API::requestedFormat());
|
||||
|
||||
// Return since no more processing for an OPTIONS request is required
|
||||
return;
|
||||
} catch (ResourceNotFoundException $e) {
|
||||
if (substr($url, -1) !== '/') {
|
||||
// We allow links to apps/files? for backwards compatibility reasons
|
||||
// However, since Symfony does not allow empty route names, the route
|
||||
// we need to match is '/', so we need to append the '/' here.
|
||||
try {
|
||||
$parameters = $matcher->match($url . '/');
|
||||
} catch (ResourceNotFoundException $newException) {
|
||||
// If we still didn't match a route, we throw the original exception
|
||||
throw $e;
|
||||
}
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$parameters = $matcher->match($url);
|
||||
} catch (ResourceNotFoundException $e) {
|
||||
|
|
|
|||
|
|
@ -103,4 +103,200 @@ class OC_Response {
|
|||
header('X-XSS-Protection: 1; mode=block'); // Enforce browser based XSS filters
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function adds the CORS headers if the requester domain is white-listed
|
||||
*
|
||||
* @param string $userId
|
||||
* @param string $domain
|
||||
* @param \OCP\IConfig $config
|
||||
* @param array $headers
|
||||
*
|
||||
* Format of $headers:
|
||||
* Array [
|
||||
* "Access-Control-Allow-Headers": ["a", "b", "c"],
|
||||
* "Access-Control-Allow-Origin": ["a", "b", "c"],
|
||||
* "Access-Control-Allow-Methods": ["a", "b", "c"]
|
||||
* ]
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function setCorsHeaders($userId, $domain, \OCP\IConfig $config = null, array $headers = []) {
|
||||
if ($config === null) {
|
||||
$config = \OC::$server->getConfig();
|
||||
}
|
||||
// first check if any of the global CORS domains matches
|
||||
$globalAllowedDomains = $config->getSystemValue('cors.allowed-domains', []);
|
||||
$isCorsRequest = (\is_array($globalAllowedDomains) && \in_array($domain, $globalAllowedDomains, true));
|
||||
if (!$isCorsRequest && $userId !== null) {
|
||||
// check if any of the user specific CORS domains matches
|
||||
$allowedDomains = \json_decode($config->getUserValue($userId, 'core', 'domains'), true);
|
||||
$isCorsRequest = (\is_array($allowedDomains) && \in_array($domain, $allowedDomains, true));
|
||||
}
|
||||
if ($isCorsRequest) {
|
||||
// TODO: infer allowed verbs from existing known routes
|
||||
$allHeaders['Access-Control-Allow-Headers'] = self::getAllowedCorsHeaders($config);
|
||||
$allHeaders['Access-Control-Expose-Headers'] = self::getExposeCorsHeaders();
|
||||
$allHeaders['Access-Control-Allow-Origin'] = [$domain];
|
||||
$allHeaders['Access-Control-Allow-Methods'] = ['GET', 'OPTIONS', 'POST', 'PUT', 'DELETE', 'MKCOL', 'PROPFIND', 'PATCH', 'PROPPATCH', 'REPORT'];
|
||||
|
||||
foreach ($headers as $key => $value) {
|
||||
if (\array_key_exists($key, $allHeaders)) {
|
||||
$allHeaders[$key] = \array_unique(\array_merge($allHeaders[$key], $value));
|
||||
}
|
||||
}
|
||||
|
||||
return $allHeaders;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* This function adds the CORS headers for all domains
|
||||
*
|
||||
* @param Sabre\HTTP\ResponseInterface $response
|
||||
* @param array $headers
|
||||
*
|
||||
* Format of $headers:
|
||||
* Array [
|
||||
* "Access-Control-Allow-Headers": ["a", "b", "c"],
|
||||
* "Access-Control-Allow-Origin": ["a", "b", "c"],
|
||||
* "Access-Control-Allow-Methods": ["a", "b", "c"]
|
||||
* ]
|
||||
*
|
||||
* @param \OCP\IConfig|null $config
|
||||
* @return Sabre\HTTP\ResponseInterface $response
|
||||
*/
|
||||
public static function setOptionsRequestHeaders($response, $headers = [], \OCP\IConfig $config = null) {
|
||||
// TODO: infer allowed verbs from existing known routes
|
||||
$allHeaders['Access-Control-Allow-Headers'] = self::getAllowedCorsHeaders($config);
|
||||
$allHeaders['Access-Control-Allow-Origin'] = ['*'];
|
||||
$allHeaders['Access-Control-Allow-Methods'] = self::getAllowedCorsMethods();
|
||||
|
||||
foreach ($headers as $key => $value) {
|
||||
if (\array_key_exists($key, $allHeaders)) {
|
||||
$allHeaders[$key] = \array_unique(\array_merge($allHeaders[$key], $value));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($allHeaders as $key => $value) {
|
||||
$response->addHeader($key, \implode(',', $value));
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* This are the allowed methods a browser can use from javascript code.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private static function getAllowedCorsMethods() {
|
||||
return [
|
||||
'GET',
|
||||
'OPTIONS',
|
||||
'POST',
|
||||
'PUT',
|
||||
'DELETE',
|
||||
'MKCOL',
|
||||
'PROPFIND',
|
||||
'PATCH',
|
||||
'PROPPATCH',
|
||||
'REPORT',
|
||||
'SEARCH'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* These are the header which a browser can access from javascript code.
|
||||
* Simple headers are always accessible.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function getExposeCorsHeaders() {
|
||||
return [
|
||||
'Content-Location',
|
||||
'DAV',
|
||||
'ETag',
|
||||
'Link',
|
||||
'Lock-Token',
|
||||
'OC-ETag',
|
||||
'OC-Checksum',
|
||||
'OC-FileId',
|
||||
'OC-JobStatus-Location',
|
||||
'OC-RequestAppPassword',
|
||||
'Vary',
|
||||
'Webdav-Location',
|
||||
'X-Sabre-Status',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* These are the headers the browser is allowed to ask for in a CORS request.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
|
||||
*
|
||||
* @param \OCP\IConfig $config
|
||||
* @return array|mixed
|
||||
*/
|
||||
private static function getAllowedCorsHeaders(\OCP\IConfig $config = null) {
|
||||
if ($config === null) {
|
||||
$config = \OC::$server->getConfig();
|
||||
}
|
||||
$allowedDefaultHeaders = [
|
||||
// own headers
|
||||
'OC-Checksum',
|
||||
'OC-Total-Length',
|
||||
'OCS-APIREQUEST',
|
||||
'X-OC-Mtime',
|
||||
'OC-RequestAppPassword',
|
||||
// as used in sabre
|
||||
'Accept',
|
||||
'Authorization',
|
||||
'Brief',
|
||||
'Content-Length',
|
||||
'Content-Range',
|
||||
'Content-Type',
|
||||
'Date',
|
||||
'Depth',
|
||||
'Destination',
|
||||
'Host',
|
||||
'If',
|
||||
'If-Match',
|
||||
'If-Modified-Since',
|
||||
'If-None-Match',
|
||||
'If-Range',
|
||||
'If-Unmodified-Since',
|
||||
'Location',
|
||||
'Lock-Token',
|
||||
'Overwrite',
|
||||
'Prefer',
|
||||
'Range',
|
||||
'Schedule-Reply',
|
||||
'Timeout',
|
||||
'User-Agent',
|
||||
'X-Expected-Entity-Length',
|
||||
// generally used headers in core
|
||||
'Accept-Language',
|
||||
'Access-Control-Request-Method',
|
||||
'Access-Control-Allow-Origin',
|
||||
'Cache-Control',
|
||||
'ETag',
|
||||
'OC-Autorename',
|
||||
'OC-CalDav-Import',
|
||||
'OC-Chunked',
|
||||
'OC-Etag',
|
||||
'OC-FileId',
|
||||
'OC-LazyOps',
|
||||
'OC-Total-File-Length',
|
||||
'OC-Total-Length',
|
||||
'Origin',
|
||||
'X-Request-ID',
|
||||
'X-Requested-With'
|
||||
];
|
||||
$corsAllowedHeaders = $config->getSystemValue('cors.allowed-headers', []);
|
||||
$corsAllowedHeaders = \array_merge($corsAllowedHeaders, $allowedDefaultHeaders);
|
||||
$corsAllowedHeaders = \array_unique(\array_values($corsAllowedHeaders));
|
||||
return $corsAllowedHeaders;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -624,4 +624,59 @@ class Util {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the protocol, domain and port as string in a normalized
|
||||
* format for easier comparison.
|
||||
* Example: "HTTPS://HOST.tld:8080" is returned as "https://host.tld:8080"
|
||||
*
|
||||
* If no port was specified, it will add the default port
|
||||
* of the specified protocol (80 for http or 443 for https)
|
||||
*
|
||||
* @param string $url full url
|
||||
* @return string protocol, domain and port as string
|
||||
* @since 28.0.0
|
||||
*/
|
||||
public static function getFullDomain(string $url): string {
|
||||
$parts = \parse_url($url);
|
||||
if ($parts === false) {
|
||||
throw new \InvalidArgumentException('Invalid url "' . $url . '"');
|
||||
}
|
||||
if (!isset($parts['scheme']) || !isset($parts['host'])) {
|
||||
throw new \InvalidArgumentException('Invalid url "' . $url . '"');
|
||||
}
|
||||
$protocol = \strtolower($parts['scheme']);
|
||||
$host = \strtolower($parts['host']);
|
||||
$port = null;
|
||||
if ($protocol === 'http') {
|
||||
$port = 80;
|
||||
} elseif ($protocol === 'https') {
|
||||
$port = 443;
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Only http based URLs supported');
|
||||
}
|
||||
|
||||
if (isset($parts['port']) && $port !== '') {
|
||||
$port = $parts['port'];
|
||||
}
|
||||
|
||||
return $protocol . '://' . \strtolower($host) . ':' . $port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given URLs have the same protocol, domain and port.
|
||||
* This is useful to check a browser's cross-domain situation.
|
||||
* If this method returns false, a browser would consider both URLs to be
|
||||
* a cross-domain situation and would require a CORS setup.
|
||||
*
|
||||
* @param string $url1
|
||||
* @param string $url2
|
||||
*
|
||||
* @return bool true if both URLs have the same protocol, domain and port
|
||||
*
|
||||
* @since 28.0.0
|
||||
*/
|
||||
public static function isSameDomain(string $url1, string $url2): bool {
|
||||
return self::getFullDomain($url1) === self::getFullDomain($url2);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
156
settings/Controller/CorsController.php
Normal file
156
settings/Controller/CorsController.php
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
<?php
|
||||
/**
|
||||
* @author Noveen Sachdeva "noveen.sachdeva@research.iiit.ac.in"
|
||||
* @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 OC\Settings\Controller;
|
||||
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\RedirectResponse;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\ILogger;
|
||||
use OCP\IRequest;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IConfig;
|
||||
use OCP\IUserSession;
|
||||
|
||||
/**
|
||||
* This controller is responsible for managing white-listed domains for CORS
|
||||
*
|
||||
* @package OC\Settings\Controller
|
||||
*/
|
||||
class CorsController extends Controller {
|
||||
/** @var ILogger */
|
||||
private $logger;
|
||||
|
||||
/** @var IURLGenerator */
|
||||
private $urlGenerator;
|
||||
|
||||
/** @var string */
|
||||
private $userId;
|
||||
|
||||
/** @var IConfig */
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* CorsController constructor.
|
||||
*
|
||||
* @param string $AppName The app's name.
|
||||
* @param IRequest $request The request.
|
||||
* @param IUserSession $userSession Logged in user's session
|
||||
* @param ILogger $logger The logger.
|
||||
* @param IURLGenerator $urlGenerator Use for url generation
|
||||
* @param IConfig $config
|
||||
*/
|
||||
public function __construct($AppName, IRequest $request,
|
||||
IUserSession $userSession,
|
||||
ILogger $logger,
|
||||
IURLGenerator $urlGenerator,
|
||||
IConfig $config) {
|
||||
parent::__construct($AppName, $request);
|
||||
|
||||
$this->AppName = $AppName;
|
||||
$this->config = $config;
|
||||
$this->userId = $userSession->getUser()->getUID();
|
||||
$this->logger = $logger;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a redirect response
|
||||
* @return RedirectResponse
|
||||
*/
|
||||
private function getRedirectResponse() {
|
||||
return new RedirectResponse(
|
||||
$this->urlGenerator->linkToRouteAbsolute(
|
||||
'settings.SettingsPage.getPersonal',
|
||||
['sectionid' => 'security']
|
||||
) . '#cors'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all White-listed domains
|
||||
*
|
||||
* @return JSONResponse All the White-listed domains
|
||||
*/
|
||||
public function getDomains() {
|
||||
$userId = $this->userId;
|
||||
|
||||
if (empty($this->config->getUserValue($userId, 'core', 'domains'))) {
|
||||
$domains = [];
|
||||
} else {
|
||||
$domains = json_decode($this->config->getUserValue($userId, 'core', 'domains'));
|
||||
}
|
||||
|
||||
return new JSONResponse($domains);
|
||||
}
|
||||
|
||||
/**
|
||||
* WhiteLists a domain for CORS
|
||||
*
|
||||
* @param string $domain The domain to whitelist
|
||||
* @return RedirectResponse Redirection to the settings page.
|
||||
*/
|
||||
public function addDomain($domain) {
|
||||
if (!isset($domain) || !self::isValidUrl($domain)) {
|
||||
return $this->getRedirectResponse();
|
||||
}
|
||||
|
||||
$userId = $this->userId;
|
||||
$domains = json_decode($this->config->getUserValue($userId, 'core', 'domains'));
|
||||
$domains = array_filter($domains);
|
||||
array_push($domains, $domain);
|
||||
|
||||
// In case same domain is added
|
||||
$domains = array_unique($domains);
|
||||
|
||||
// Store as comma seperated string
|
||||
$domainsString = json_encode($domains);
|
||||
|
||||
$this->config->setUserValue($userId, 'core', 'domains', $domainsString);
|
||||
$this->logger->debug("The domain {$domain} has been white-listed.", ['app' => $this->appName]);
|
||||
|
||||
return $this->getRedirectResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a WhiteListed Domain
|
||||
*
|
||||
* @param string $domain Domain to remove
|
||||
* @return RedirectResponse Redirection to the settings page.
|
||||
*/
|
||||
public function removeDomain($id) {
|
||||
$userId = $this->userId;
|
||||
$domains = json_decode($this->config->getUserValue($userId, 'core', 'domains'));
|
||||
|
||||
if ($id >= 0 && $id < count($domains)) {
|
||||
unset($domains[$id]);
|
||||
$this->config->setUserValue($userId, 'core', 'domains', json_encode($domains));
|
||||
}
|
||||
|
||||
return $this->getRedirectResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a URL is valid
|
||||
* @param string $url URL to check
|
||||
* @return boolean whether URL is valid
|
||||
*/
|
||||
private static function isValidUrl($url) {
|
||||
return (filter_var($url, FILTER_VALIDATE_URL) !== false);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,12 +15,12 @@ use OC\AppFramework\Http\Request;
|
|||
use OC\AppFramework\Middleware\Security\CORSMiddleware;
|
||||
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
|
||||
use OC\AppFramework\Utility\ControllerMethodReflector;
|
||||
use OC\User\Session;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\AppFramework\Http\Response;
|
||||
use OCP\IConfig;
|
||||
use OCP\IRequest;
|
||||
use OCP\IRequestId;
|
||||
use OCP\IUserSession;
|
||||
use OCP\Security\Bruteforce\IThrottler;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\AppFramework\Middleware\Security\Mock\CORSMiddlewareController;
|
||||
|
|
@ -28,17 +28,25 @@ use Test\AppFramework\Middleware\Security\Mock\CORSMiddlewareController;
|
|||
class CORSMiddlewareTest extends \Test\TestCase {
|
||||
/** @var ControllerMethodReflector */
|
||||
private $reflector;
|
||||
/** @var Session|MockObject */
|
||||
/** @var IUserSession|MockObject */
|
||||
private $session;
|
||||
/** @var IThrottler|MockObject */
|
||||
private $throttler;
|
||||
/** @var IConfig|MockObject */
|
||||
private $config;
|
||||
/** @var CORSMiddlewareController */
|
||||
private $controller;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
/** @var MockObject */
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->config->method('getUserValue')->willReturn('["http:\/\/www.test.com"]');
|
||||
$this->config->method('setUserValue')->willReturn(true);
|
||||
|
||||
$this->reflector = new ControllerMethodReflector();
|
||||
$this->session = $this->createMock(Session::class);
|
||||
$this->session = $this->createMock(IUserSession::class);
|
||||
$this->throttler = $this->createMock(IThrottler::class);
|
||||
$this->controller = new CORSMiddlewareController(
|
||||
'test',
|
||||
|
|
@ -51,6 +59,17 @@ class CORSMiddlewareTest extends \Test\TestCase {
|
|||
['testSetCORSAPIHeader'],
|
||||
['testSetCORSAPIHeaderAttribute'],
|
||||
];
|
||||
|
||||
$this->session = $this->getMockBuilder('\OC\User\Session')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->method('getUID')->willReturn('user');
|
||||
$userSession = $this->createMock(IUserSession::class);
|
||||
$userSession->method('getUser')->willReturn($user);
|
||||
|
||||
$this->session = $userSession;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -60,18 +79,24 @@ class CORSMiddlewareTest extends \Test\TestCase {
|
|||
$request = new Request(
|
||||
[
|
||||
'server' => [
|
||||
'HTTP_ORIGIN' => 'test'
|
||||
'HTTP_ORIGIN' => 'http://www.test.com'
|
||||
]
|
||||
],
|
||||
$this->createMock(IRequestId::class),
|
||||
$this->createMock(IConfig::class)
|
||||
);
|
||||
$this->reflector->reflect($this->controller, $method);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
|
||||
$middleware = new CORSMiddleware(
|
||||
$request,
|
||||
$this->reflector,
|
||||
$this->session,
|
||||
$this->throttler,
|
||||
$this->config
|
||||
);
|
||||
|
||||
$response = $middleware->afterController($this->controller, $method, new Response());
|
||||
$headers = $response->getHeaders();
|
||||
$this->assertEquals('test', $headers['Access-Control-Allow-Origin']);
|
||||
$this->assertEquals('http://www.test.com', $headers['Access-Control-Allow-Origin']);
|
||||
}
|
||||
|
||||
public function testNoAnnotationNoCORSHEADER(): void {
|
||||
|
|
@ -84,7 +109,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
|
|||
$this->createMock(IRequestId::class),
|
||||
$this->createMock(IConfig::class)
|
||||
);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->config);
|
||||
|
||||
$response = $middleware->afterController($this->controller, __FUNCTION__, new Response());
|
||||
$headers = $response->getHeaders();
|
||||
|
|
@ -108,7 +133,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
|
|||
$this->createMock(IConfig::class)
|
||||
);
|
||||
$this->reflector->reflect($this->controller, $method);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->config);
|
||||
|
||||
$response = $middleware->afterController($this->controller, $method, new Response());
|
||||
$headers = $response->getHeaders();
|
||||
|
|
@ -131,14 +156,14 @@ class CORSMiddlewareTest extends \Test\TestCase {
|
|||
$request = new Request(
|
||||
[
|
||||
'server' => [
|
||||
'HTTP_ORIGIN' => 'test'
|
||||
'HTTP_ORIGIN' => 'http://www.test.com',
|
||||
]
|
||||
],
|
||||
$this->createMock(IRequestId::class),
|
||||
$this->createMock(IConfig::class)
|
||||
);
|
||||
$this->reflector->reflect($this->controller, $method);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->config);
|
||||
|
||||
$response = new Response();
|
||||
$response->addHeader('AcCess-control-Allow-Credentials ', 'TRUE');
|
||||
|
|
@ -164,7 +189,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
|
|||
$this->createMock(IConfig::class)
|
||||
);
|
||||
$this->reflector->reflect($this->controller, $method);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->config);
|
||||
$this->session->expects($this->once())
|
||||
->method('isLoggedIn')
|
||||
->willReturn(false);
|
||||
|
|
@ -198,7 +223,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
|
|||
$this->createMock(IConfig::class)
|
||||
);
|
||||
$this->reflector->reflect($this->controller, $method);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->config);
|
||||
$this->session->expects($this->once())
|
||||
->method('isLoggedIn')
|
||||
->willReturn(true);
|
||||
|
|
@ -239,7 +264,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
|
|||
->with($this->equalTo('user'), $this->equalTo('pass'))
|
||||
->willReturn(true);
|
||||
$this->reflector->reflect($this->controller, $method);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->config);
|
||||
|
||||
$middleware->beforeController($this->controller, $method);
|
||||
}
|
||||
|
|
@ -272,7 +297,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
|
|||
->with($this->equalTo('user'), $this->equalTo('pass'))
|
||||
->will($this->throwException(new \OC\Authentication\Exceptions\PasswordLoginForbiddenException));
|
||||
$this->reflector->reflect($this->controller, $method);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->config);
|
||||
|
||||
$middleware->beforeController($this->controller, $method);
|
||||
}
|
||||
|
|
@ -305,7 +330,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
|
|||
->with($this->equalTo('user'), $this->equalTo('pass'))
|
||||
->willReturn(false);
|
||||
$this->reflector->reflect($this->controller, $method);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->config);
|
||||
|
||||
$middleware->beforeController($this->controller, $method);
|
||||
}
|
||||
|
|
@ -319,7 +344,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
|
|||
$this->createMock(IRequestId::class),
|
||||
$this->createMock(IConfig::class)
|
||||
);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->config);
|
||||
$response = $middleware->afterException($this->controller, __FUNCTION__, new SecurityException('A security exception'));
|
||||
|
||||
$expected = new JSONResponse(['message' => 'A security exception'], 500);
|
||||
|
|
@ -335,7 +360,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
|
|||
$this->createMock(IRequestId::class),
|
||||
$this->createMock(IConfig::class)
|
||||
);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->config);
|
||||
$response = $middleware->afterException($this->controller, __FUNCTION__, new SecurityException('A security exception', 501));
|
||||
|
||||
$expected = new JSONResponse(['message' => 'A security exception'], 501);
|
||||
|
|
@ -354,7 +379,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
|
|||
$this->createMock(IRequestId::class),
|
||||
$this->createMock(IConfig::class)
|
||||
);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
|
||||
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->config);
|
||||
$middleware->afterException($this->controller, __FUNCTION__, new \Exception('A regular exception'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue