Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cors middleware #8504

Merged
merged 2 commits into from
May 11, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/private/appframework/dependencyinjection/dicontainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use OC\AppFramework\Core\API;
use OC\AppFramework\Middleware\MiddlewareDispatcher;
use OC\AppFramework\Middleware\Security\SecurityMiddleware;
use OC\AppFramework\Middleware\Security\CORSMiddleware;
use OC\AppFramework\Utility\SimpleContainer;
use OC\AppFramework\Utility\TimeFactory;
use OCP\AppFramework\IApi;
Expand Down Expand Up @@ -92,10 +93,15 @@ public function __construct($appName, $urlParams = array()){
return new SecurityMiddleware($app, $c['Request']);
});

$this['CORSMiddleware'] = $this->share(function($c) {
return new CORSMiddleware($c['Request']);
});

$middleWares = &$this->middleWares;
$this['MiddlewareDispatcher'] = $this->share(function($c) use (&$middleWares) {
$dispatcher = new MiddlewareDispatcher();
$dispatcher->registerMiddleware($c['SecurityMiddleware']);
$dispatcher->registerMiddleware($c['CORSMiddleware']);

foreach($middleWares as $middleWare) {
$dispatcher->registerMiddleware($c[$middleWare]);
Expand Down
72 changes: 72 additions & 0 deletions lib/private/appframework/middleware/security/corsmiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php
/**
* ownCloud - App Framework
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Bernhard Posselt <[email protected]>
* @copyright Bernhard Posselt 2014
*/

namespace OC\AppFramework\Middleware\Security;

use OC\AppFramework\Utility\MethodAnnotationReader;
use OCP\IRequest;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Middleware;

/**
* This middleware sets the correct CORS headers on a response if the
* controller has the @CORS annotation. This is needed for webapps that want
* to access an API and dont run on the same domain, see
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
*/
class CORSMiddleware extends Middleware {

private $request;

/**
* @param IRequest $request
*/
public function __construct(IRequest $request) {
$this->request = $request;
}


/**
* This is being run after a successful controllermethod call and allows
* the manipulation of a Response object. The middleware is run in reverse order
*
* @param Controller $controller the controller that is being called
* @param string $methodName the name of the method that will be called on
* the controller
* @param Response $response the generated response from the controller
* @return Response a Response object
*/
public function afterController($controller, $methodName, Response $response){
// only react if its a CORS request and if the request sends origin and
$reflector = new MethodAnnotationReader($controller, $methodName);

if(isset($this->request->server['HTTP_ORIGIN']) &&
$reflector->hasAnnotation('CORS')) {

// 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;
}


}
93 changes: 93 additions & 0 deletions lib/public/appframework/apicontroller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php
/**
* ownCloud - App Framework
*
* @author Bernhard Posselt
* @copyright 2012 Bernhard Posselt [email protected]
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/

/**
* Public interface of ownCloud for apps to use.
* AppFramework\Controller class
*/

namespace OCP\AppFramework;

use OCP\AppFramework\Http\Response;
use OCP\IRequest;


/**
* Base class to inherit your controllers from that are used for RESTful APIs
*/
abstract class ApiController extends Controller {

private $corsMethods;
private $corsAllowedHeaders;
private $corsMaxAge;

/**
* constructor of the controller
* @param string $appName the name of the app
* @param IRequest $request an instance of the request
* @param string $corsMethods: comma seperated string of HTTP verbs which
* should be allowed for websites or webapps when calling your API, defaults to
* 'PUT, POST, GET, DELETE, PATCH'
* @param string $corsAllowedHeaders: comma seperated string of HTTP headers
* which should be allowed for websites or webapps when calling your API,
* defaults to 'Authorization, Content-Type, Accept'
* @param int $corsMaxAge number in seconds how long a preflighted OPTIONS
* request should be cached, defaults to 1728000 seconds
*/
public function __construct($appName,
IRequest $request,
$corsMethods='PUT, POST, GET, DELETE, PATCH',
$corsAllowedHeaders='Authorization, Content-Type, Accept',
$corsMaxAge=1728000){
parent::__construct($appName, $request);
$this->corsMethods = $corsMethods;
$this->corsAllowedHeaders = $corsAllowedHeaders;
$this->corsMaxAge = $corsMaxAge;
}


/**
* This method implements a preflighted cors response for you that you can
* link to for the options request
*
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
*/
public function preflightedCors() {
if(isset($this->request->server['HTTP_ORIGIN'])) {
$origin = $this->request->server['HTTP_ORIGIN'];
} else {
$origin = '*';
}

$response = new Response();
$response->addHeader('Access-Control-Allow-Origin', $origin);
$response->addHeader('Access-Control-Allow-Methods', $this->corsMethods);
$response->addHeader('Access-Control-Max-Age', $this->corsMaxAge);
$response->addHeader('Access-Control-Allow-Headers', $this->corsAllowedHeaders);
$response->addHeader('Access-Control-Allow-Credentials', 'false');
return $response;
}


}
13 changes: 11 additions & 2 deletions lib/public/appframework/controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
namespace OCP\AppFramework;

use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\IAppContainer;
use OCP\IRequest;


Expand All @@ -49,12 +48,22 @@ abstract class Controller {
*/
protected $request;


/**
* constructor of the controller
* @param string $appName the name of the app
* @param IRequest $request an instance of the request
* @param string $corsMethods: comma seperated string of HTTP verbs which
* should be allowed for websites or webapps when calling your API, defaults to
* 'PUT, POST, GET, DELETE, PATCH'
* @param string $corsAllowedHeaders: comma seperated string of HTTP headers
* which should be allowed for websites or webapps when calling your API,
* defaults to 'Authorization, Content-Type, Accept'
* @param int $corsMaxAge number in seconds how long a preflighted OPTIONS
* request should be cached, defaults to 1728000 seconds
*/
public function __construct($appName, IRequest $request){
public function __construct($appName,
IRequest $request){
$this->appName = $appName;
$this->request = $request;
}
Expand Down
4 changes: 4 additions & 0 deletions lib/public/appframework/http/response.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ public function cacheFor($cacheSeconds) {
* @return Response Reference to this object
*/
public function addHeader($name, $value) {
$name = trim($name); // always remove leading and trailing whitespace
// to be able to reliably check for security
// headers

if(is_null($value)) {
unset($this->headers[$name]);
} else {
Expand Down
55 changes: 55 additions & 0 deletions tests/lib/appframework/controller/ApiControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

/**
* ownCloud - App Framework
*
* @author Bernhard Posselt
* @copyright 2012 Bernhard Posselt [email protected]
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/


namespace OCP\AppFramework;

use OC\AppFramework\Http\Request;
use OCP\AppFramework\Http\TemplateResponse;


class ChildApiController extends ApiController {};


class ApiControllerTest extends \PHPUnit_Framework_TestCase {


public function testCors() {
$request = new Request(
array('server' => array('HTTP_ORIGIN' => 'test'))
);
$this->controller = new ChildApiController('app', $request, 'verbs',
'headers', 100);

$response = $this->controller->preflightedCors();

$headers = $response->getHeaders();

$this->assertEquals('test', $headers['Access-Control-Allow-Origin']);
$this->assertEquals('verbs', $headers['Access-Control-Allow-Methods']);
$this->assertEquals('headers', $headers['Access-Control-Allow-Headers']);
$this->assertEquals('false', $headers['Access-Control-Allow-Credentials']);
$this->assertEquals(100, $headers['Access-Control-Max-Age']);
}

}
4 changes: 2 additions & 2 deletions tests/lib/appframework/controller/ControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@
*/


namespace Test\AppFramework\Controller;
namespace OCP\AppFramework;

use OC\AppFramework\Http\Request;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;


Expand Down Expand Up @@ -129,4 +128,5 @@ public function testGetEnvVariable(){
$this->assertEquals('daheim', $this->controller->env('PATH'));
}


}
2 changes: 1 addition & 1 deletion tests/lib/appframework/http/ResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ protected function setUp(){


public function testAddHeader(){
$this->childResponse->addHeader('hello', 'world');
$this->childResponse->addHeader(' hello ', 'world');
$headers = $this->childResponse->getHeaders();
$this->assertEquals('world', $headers['hello']);
}
Expand Down
77 changes: 77 additions & 0 deletions tests/lib/appframework/middleware/security/CORSMiddlewareTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php
/**
* ownCloud - App Framework
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Bernhard Posselt <[email protected]>
* @copyright Bernhard Posselt 2014
*/


namespace OC\AppFramework\Middleware\Security;

use OC\AppFramework\Http\Request;
use OCP\AppFramework\Http\Response;


class CORSMiddlewareTest extends \PHPUnit_Framework_TestCase {

/**
* @CORS
*/
public function testSetCORSAPIHeader() {
$request = new Request(
array('server' => array('HTTP_ORIGIN' => 'test'))
);

$middleware = new CORSMiddleware($request);
$response = $middleware->afterController($this, __FUNCTION__, new Response());
$headers = $response->getHeaders();

$this->assertEquals('test', $headers['Access-Control-Allow-Origin']);
}


public function testNoAnnotationNoCORSHEADER() {
$request = new Request(
array('server' => array('HTTP_ORIGIN' => 'test'))
);
$middleware = new CORSMiddleware($request);

$response = $middleware->afterController($this, __FUNCTION__, new Response());
$headers = $response->getHeaders();
$this->assertFalse(array_key_exists('Access-Control-Allow-Origin', $headers));
}


/**
* @CORS
*/
public function testNoOriginHeaderNoCORSHEADER() {
$request = new Request();

$middleware = new CORSMiddleware($request);
$response = $middleware->afterController($this, __FUNCTION__, new Response());
$headers = $response->getHeaders();
$this->assertFalse(array_key_exists('Access-Control-Allow-Origin', $headers));
}


/**
* @CORS
* @expectedException \OC\AppFramework\Middleware\Security\SecurityException
*/
public function testCorsIgnoredIfWithCredentialsHeaderPresent() {
$request = new Request(
array('server' => array('HTTP_ORIGIN' => 'test'))
);
$middleware = new CORSMiddleware($request);

$response = new Response();
$response->addHeader('AcCess-control-Allow-Credentials ', 'TRUE');
$response = $middleware->afterController($this, __FUNCTION__, $response);
}

}