parent
a05471eb43
commit
a1ae5275f9
@ -0,0 +1,134 @@ |
||||
<?php |
||||
/** |
||||
* @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> |
||||
* |
||||
* @license GNU AGPL version 3 or any later version |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU Affero General Public License as |
||||
* published by the Free Software Foundation, either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
* |
||||
* 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 |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
* |
||||
*/ |
||||
|
||||
namespace OC\AppFramework\Middleware\Security; |
||||
|
||||
use OC\AppFramework\Utility\ControllerMethodReflector; |
||||
use OC\Security\RateLimiting\Exception\RateLimitExceededException; |
||||
use OC\Security\RateLimiting\Limiter; |
||||
use OCP\AppFramework\Http\JSONResponse; |
||||
use OCP\AppFramework\Http\Response; |
||||
use OCP\AppFramework\Http\TemplateResponse; |
||||
use OCP\AppFramework\Middleware; |
||||
use OCP\IRequest; |
||||
use OCP\IUserSession; |
||||
|
||||
/** |
||||
* Class RateLimitingMiddleware is the middleware responsible for implementing the |
||||
* ratelimiting in Nextcloud. |
||||
* |
||||
* It parses annotations such as: |
||||
* |
||||
* @UserRateThrottle(limit=5, period=100) |
||||
* @AnonRateThrottle(limit=1, period=100) |
||||
* |
||||
* Those annotations above would mean that logged-in users can access the page 5 |
||||
* times within 100 seconds, and anonymous users 1 time within 100 seconds. If |
||||
* only an AnonRateThrottle is specified that one will also be applied to logged-in |
||||
* users. |
||||
* |
||||
* @package OC\AppFramework\Middleware\Security |
||||
*/ |
||||
class RateLimitingMiddleware extends Middleware { |
||||
/** @var IRequest $request */ |
||||
private $request; |
||||
/** @var IUserSession */ |
||||
private $userSession; |
||||
/** @var ControllerMethodReflector */ |
||||
private $reflector; |
||||
/** @var Limiter */ |
||||
private $limiter; |
||||
|
||||
/** |
||||
* @param IRequest $request |
||||
* @param IUserSession $userSession |
||||
* @param ControllerMethodReflector $reflector |
||||
* @param Limiter $limiter |
||||
*/ |
||||
public function __construct(IRequest $request, |
||||
IUserSession $userSession, |
||||
ControllerMethodReflector $reflector, |
||||
Limiter $limiter) { |
||||
$this->request = $request; |
||||
$this->userSession = $userSession; |
||||
$this->reflector = $reflector; |
||||
$this->limiter = $limiter; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritDoc} |
||||
* @throws RateLimitExceededException |
||||
*/ |
||||
public function beforeController($controller, $methodName) { |
||||
parent::beforeController($controller, $methodName); |
||||
|
||||
$anonLimit = $this->reflector->getAnnotationParameter('AnonRateThrottle', 'limit'); |
||||
$anonPeriod = $this->reflector->getAnnotationParameter('AnonRateThrottle', 'period'); |
||||
$userLimit = $this->reflector->getAnnotationParameter('UserRateThrottle', 'limit'); |
||||
$userPeriod = $this->reflector->getAnnotationParameter('UserRateThrottle', 'period'); |
||||
$rateLimitIdentifier = get_class($controller) . '::' . $methodName; |
||||
if($userLimit !== '' && $userPeriod !== '' && $this->userSession->isLoggedIn()) { |
||||
$this->limiter->registerUserRequest( |
||||
$rateLimitIdentifier, |
||||
$userLimit, |
||||
$userPeriod, |
||||
$this->userSession->getUser() |
||||
); |
||||
} elseif ($anonLimit !== '' && $anonPeriod !== '') { |
||||
$this->limiter->registerAnonRequest( |
||||
$rateLimitIdentifier, |
||||
$anonLimit, |
||||
$anonPeriod, |
||||
$this->request->getRemoteAddress() |
||||
); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* {@inheritDoc} |
||||
*/ |
||||
public function afterException($controller, $methodName, \Exception $exception) { |
||||
if($exception instanceof RateLimitExceededException) { |
||||
if (stripos($this->request->getHeader('Accept'),'html') === false) { |
||||
$response = new JSONResponse( |
||||
[ |
||||
'message' => $exception->getMessage(), |
||||
], |
||||
$exception->getCode() |
||||
); |
||||
} else { |
||||
$response = new TemplateResponse( |
||||
'core', |
||||
'403', |
||||
[ |
||||
'file' => $exception->getMessage() |
||||
], |
||||
'guest' |
||||
); |
||||
$response->setStatus($exception->getCode()); |
||||
} |
||||
|
||||
return $response; |
||||
} |
||||
|
||||
throw $exception; |
||||
} |
||||
} |
@ -0,0 +1,283 @@ |
||||
<?php |
||||
/** |
||||
* @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> |
||||
* |
||||
* @license GNU AGPL version 3 or any later version |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU Affero General Public License as |
||||
* published by the Free Software Foundation, either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
* |
||||
* 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 |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
* |
||||
*/ |
||||
|
||||
namespace Test\AppFramework\Middleware\Security; |
||||
|
||||
use OC\AppFramework\Middleware\Security\RateLimitingMiddleware; |
||||
use OC\AppFramework\Utility\ControllerMethodReflector; |
||||
use OC\Security\RateLimiting\Exception\RateLimitExceededException; |
||||
use OC\Security\RateLimiting\Limiter; |
||||
use OCP\AppFramework\Controller; |
||||
use OCP\AppFramework\Http\JSONResponse; |
||||
use OCP\AppFramework\Http\TemplateResponse; |
||||
use OCP\IRequest; |
||||
use OCP\IUser; |
||||
use OCP\IUserSession; |
||||
use Test\TestCase; |
||||
|
||||
class RateLimitingMiddlewareTest extends TestCase { |
||||
/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */ |
||||
private $request; |
||||
/** @var IUserSession|\PHPUnit_Framework_MockObject_MockObject */ |
||||
private $userSession; |
||||
/** @var ControllerMethodReflector|\PHPUnit_Framework_MockObject_MockObject */ |
||||
private $reflector; |
||||
/** @var Limiter|\PHPUnit_Framework_MockObject_MockObject */ |
||||
private $limiter; |
||||
/** @var RateLimitingMiddleware */ |
||||
private $rateLimitingMiddleware; |
||||
|
||||
public function setUp() { |
||||
parent::setUp(); |
||||
|
||||
$this->request = $this->createMock(IRequest::class); |
||||
$this->userSession = $this->createMock(IUserSession::class); |
||||
$this->reflector = $this->createMock(ControllerMethodReflector::class); |
||||
$this->limiter = $this->createMock(Limiter::class); |
||||
|
||||
$this->rateLimitingMiddleware = new RateLimitingMiddleware( |
||||
$this->request, |
||||
$this->userSession, |
||||
$this->reflector, |
||||
$this->limiter |
||||
); |
||||
} |
||||
|
||||
public function testBeforeControllerWithoutAnnotation() { |
||||
$this->reflector |
||||
->expects($this->at(0)) |
||||
->method('getAnnotationParameter') |
||||
->with('AnonRateThrottle', 'limit') |
||||
->willReturn(''); |
||||
$this->reflector |
||||
->expects($this->at(1)) |
||||
->method('getAnnotationParameter') |
||||
->with('AnonRateThrottle', 'period') |
||||
->willReturn(''); |
||||
$this->reflector |
||||
->expects($this->at(2)) |
||||
->method('getAnnotationParameter') |
||||
->with('UserRateThrottle', 'limit') |
||||
->willReturn(''); |
||||
$this->reflector |
||||
->expects($this->at(3)) |
||||
->method('getAnnotationParameter') |
||||
->with('UserRateThrottle', 'period') |
||||
->willReturn(''); |
||||
|
||||
$this->limiter |
||||
->expects($this->never()) |
||||
->method('registerUserRequest'); |
||||
$this->limiter |
||||
->expects($this->never()) |
||||
->method('registerAnonRequest'); |
||||
|
||||
/** @var Controller|\PHPUnit_Framework_MockObject_MockObject $controller */ |
||||
$controller = $this->createMock(Controller::class); |
||||
$this->rateLimitingMiddleware->beforeController($controller, 'testMethod'); |
||||
} |
||||
|
||||
public function testBeforeControllerForAnon() { |
||||
/** @var Controller|\PHPUnit_Framework_MockObject_MockObject $controller */ |
||||
$controller = $this->createMock(Controller::class); |
||||
$this->request |
||||
->expects($this->once()) |
||||
->method('getRemoteAddress') |
||||
->willReturn('127.0.0.1'); |
||||
|
||||
$this->reflector |
||||
->expects($this->at(0)) |
||||
->method('getAnnotationParameter') |
||||
->with('AnonRateThrottle', 'limit') |
||||
->willReturn('100'); |
||||
$this->reflector |
||||
->expects($this->at(1)) |
||||
->method('getAnnotationParameter') |
||||
->with('AnonRateThrottle', 'period') |
||||
->willReturn('10'); |
||||
$this->reflector |
||||
->expects($this->at(2)) |
||||
->method('getAnnotationParameter') |
||||
->with('UserRateThrottle', 'limit') |
||||
->willReturn(''); |
||||
$this->reflector |
||||
->expects($this->at(3)) |
||||
->method('getAnnotationParameter') |
||||
->with('UserRateThrottle', 'period') |
||||
->willReturn(''); |
||||
|
||||
$this->limiter |
||||
->expects($this->never()) |
||||
->method('registerUserRequest'); |
||||
$this->limiter |
||||
->expects($this->once()) |
||||
->method('registerAnonRequest') |
||||
->with(get_class($controller) . '::testMethod', '100', '10', '127.0.0.1'); |
||||
|
||||
|
||||
$this->rateLimitingMiddleware->beforeController($controller, 'testMethod'); |
||||
} |
||||
|
||||
public function testBeforeControllerForLoggedIn() { |
||||
/** @var Controller|\PHPUnit_Framework_MockObject_MockObject $controller */ |
||||
$controller = $this->createMock(Controller::class); |
||||
/** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ |
||||
$user = $this->createMock(IUser::class); |
||||
|
||||
$this->userSession |
||||
->expects($this->once()) |
||||
->method('isLoggedIn') |
||||
->willReturn(true); |
||||
$this->userSession |
||||
->expects($this->once()) |
||||
->method('getUser') |
||||
->willReturn($user); |
||||
|
||||
$this->reflector |
||||
->expects($this->at(0)) |
||||
->method('getAnnotationParameter') |
||||
->with('AnonRateThrottle', 'limit') |
||||
->willReturn(''); |
||||
$this->reflector |
||||
->expects($this->at(1)) |
||||
->method('getAnnotationParameter') |
||||
->with('AnonRateThrottle', 'period') |
||||
->willReturn(''); |
||||
$this->reflector |
||||
->expects($this->at(2)) |
||||
->method('getAnnotationParameter') |
||||
->with('UserRateThrottle', 'limit') |
||||
->willReturn('100'); |
||||
$this->reflector |
||||
->expects($this->at(3)) |
||||
->method('getAnnotationParameter') |
||||
->with('UserRateThrottle', 'period') |
||||
->willReturn('10'); |
||||
|
||||
$this->limiter |
||||
->expects($this->never()) |
||||
->method('registerAnonRequest'); |
||||
$this->limiter |
||||
->expects($this->once()) |
||||
->method('registerUserRequest') |
||||
->with(get_class($controller) . '::testMethod', '100', '10', $user); |
||||
|
||||
|
||||
$this->rateLimitingMiddleware->beforeController($controller, 'testMethod'); |
||||
} |
||||
|
||||
public function testBeforeControllerAnonWithFallback() { |
||||
/** @var Controller|\PHPUnit_Framework_MockObject_MockObject $controller */ |
||||
$controller = $this->createMock(Controller::class); |
||||
$this->request |
||||
->expects($this->once()) |
||||
->method('getRemoteAddress') |
||||
->willReturn('127.0.0.1'); |
||||
|
||||
$this->userSession |
||||
->expects($this->once()) |
||||
->method('isLoggedIn') |
||||
->willReturn(false); |
||||
|
||||
$this->reflector |
||||
->expects($this->at(0)) |
||||
->method('getAnnotationParameter') |
||||
->with('AnonRateThrottle', 'limit') |
||||
->willReturn('200'); |
||||
$this->reflector |
||||
->expects($this->at(1)) |
||||
->method('getAnnotationParameter') |
||||
->with('AnonRateThrottle', 'period') |
||||
->willReturn('20'); |
||||
$this->reflector |
||||
->expects($this->at(2)) |
||||
->method('getAnnotationParameter') |
||||
->with('UserRateThrottle', 'limit') |
||||
->willReturn('100'); |
||||
$this->reflector |
||||
->expects($this->at(3)) |
||||
->method('getAnnotationParameter') |
||||
->with('UserRateThrottle', 'period') |
||||
->willReturn('10'); |
||||
|
||||
$this->limiter |
||||
->expects($this->never()) |
||||
->method('registerUserRequest'); |
||||
$this->limiter |
||||
->expects($this->once()) |
||||
->method('registerAnonRequest') |
||||
->with(get_class($controller) . '::testMethod', '200', '20', '127.0.0.1'); |
||||
|
||||
$this->rateLimitingMiddleware->beforeController($controller, 'testMethod'); |
||||
} |
||||
|
||||
/** |
||||
* @expectedException \Exception |
||||
* @expectedExceptionMessage My test exception |
||||
*/ |
||||
public function testAfterExceptionWithOtherException() { |
||||
/** @var Controller|\PHPUnit_Framework_MockObject_MockObject $controller */ |
||||
$controller = $this->createMock(Controller::class); |
||||
|
||||
$this->rateLimitingMiddleware->afterException($controller, 'testMethod', new \Exception('My test exception')); |
||||
} |
||||
|
||||
public function testAfterExceptionWithJsonBody() { |
||||
/** @var Controller|\PHPUnit_Framework_MockObject_MockObject $controller */ |
||||
$controller = $this->createMock(Controller::class); |
||||
$this->request |
||||
->expects($this->once()) |
||||
->method('getHeader') |
||||
->with('Accept') |
||||
->willReturn('JSON'); |
||||
|
||||
$result = $this->rateLimitingMiddleware->afterException($controller, 'testMethod', new RateLimitExceededException()); |
||||
$expected = new JSONResponse( |
||||
[ |
||||
'message' => 'Rate limit exceeded', |
||||
], |
||||
429 |
||||
); |
||||
$this->assertEquals($expected, $result); |
||||
} |
||||
|
||||
public function testAfterExceptionWithHtmlBody() { |
||||
/** @var Controller|\PHPUnit_Framework_MockObject_MockObject $controller */ |
||||
$controller = $this->createMock(Controller::class); |
||||
$this->request |
||||
->expects($this->once()) |
||||
->method('getHeader') |
||||
->with('Accept') |
||||
->willReturn('html'); |
||||
|
||||
$result = $this->rateLimitingMiddleware->afterException($controller, 'testMethod', new RateLimitExceededException()); |
||||
$expected = new TemplateResponse( |
||||
'core', |
||||
'403', |
||||
[ |
||||
'file' => 'Rate limit exceeded', |
||||
], |
||||
'guest' |
||||
); |
||||
$expected->setStatus(429); |
||||
$this->assertEquals($expected, $result); |
||||
} |
||||
} |
Loading…
Reference in new issue