Browse Source
Add support for ratelimiting via annotations
Add support for ratelimiting via annotations
This allows adding rate limiting via annotations to controllers, as one example: ``` @UserRateThrottle(limit=5, period=100) @AnonRateThrottle(limit=1, period=100) ``` 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. Signed-off-by: Lukas Reschke <lukas@statuscode.ch>pull/4336/head
No known key found for this signature in database
GPG Key ID: B9F6980CF6E759B1
21 changed files with 1026 additions and 160 deletions
-
12.drone.yml
-
2apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php
-
2apps/files_sharing/lib/Controller/ShareController.php
-
20apps/testing/appinfo/routes.php
-
52apps/testing/lib/Controller/RateLimitTestController.php
-
58build/integration/features/ratelimiting.feature
-
2core/Controller/LostController.php
-
24lib/private/AppFramework/DependencyInjection/DIContainer.php
-
47lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php
-
55lib/private/AppFramework/Utility/ControllerMethodReflector.php
-
72lib/private/Security/Bruteforce/Throttler.php
-
106lib/private/Security/Normalizer/IpAddress.php
-
50lib/private/Security/RateLimiting/Backend/IBackend.php
-
100lib/private/Security/RateLimiting/Backend/MemoryCache.php
-
31lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php
-
106lib/private/Security/RateLimiting/Limiter.php
-
44tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php
-
45tests/lib/Security/Bruteforce/ThrottlerTest.php
-
59tests/lib/Security/Normalizer/IpAddressTest.php
-
138tests/lib/Security/RateLimiting/Backend/MemoryCacheTest.php
-
161tests/lib/Security/RateLimiting/LimiterTest.php
@ -0,0 +1,52 @@ |
|||
<?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 OCA\Testing\Controller; |
|||
|
|||
use OCP\AppFramework\Controller; |
|||
use OCP\AppFramework\Http\JSONResponse; |
|||
|
|||
class RateLimitTestController extends Controller { |
|||
/** |
|||
* @PublicPage |
|||
* @NoCSRFRequired |
|||
* |
|||
* @UserRateThrottle(limit=5, period=100) |
|||
* @AnonRateThrottle(limit=1, period=100) |
|||
* |
|||
* @return JSONResponse |
|||
*/ |
|||
public function userAndAnonProtected() { |
|||
return new JSONResponse(); |
|||
} |
|||
|
|||
/** |
|||
* @PublicPage |
|||
* @NoCSRFRequired |
|||
* |
|||
* @AnonRateThrottle(limit=1, period=10) |
|||
* |
|||
* @return JSONResponse |
|||
*/ |
|||
public function onlyAnonProtected() { |
|||
return new JSONResponse(); |
|||
} |
|||
} |
@ -0,0 +1,58 @@ |
|||
Feature: ratelimiting |
|||
|
|||
Background: |
|||
Given user "user0" exists |
|||
Given As an "admin" |
|||
Given app "testing" is enabled |
|||
|
|||
Scenario: Accessing a page with only an AnonRateThrottle as user |
|||
Given user "user0" exists |
|||
# First request should work |
|||
When requesting "/index.php/apps/testing/anonProtected" with "GET" using basic auth |
|||
Then the HTTP status code should be "200" |
|||
# Second one should fail |
|||
When requesting "/index.php/apps/testing/anonProtected" with "GET" using basic auth |
|||
Then the HTTP status code should be "429" |
|||
# After 11 seconds the next request should work |
|||
And Sleep for "11" seconds |
|||
When requesting "/index.php/apps/testing/anonProtected" with "GET" using basic auth |
|||
Then the HTTP status code should be "200" |
|||
|
|||
Scenario: Accessing a page with only an AnonRateThrottle as guest |
|||
Given Sleep for "11" seconds |
|||
# First request should work |
|||
When requesting "/index.php/apps/testing/anonProtected" with "GET" |
|||
Then the HTTP status code should be "200" |
|||
# Second one should fail |
|||
When requesting "/index.php/apps/testing/anonProtected" with "GET" using basic auth |
|||
Then the HTTP status code should be "429" |
|||
# After 11 seconds the next request should work |
|||
And Sleep for "11" seconds |
|||
When requesting "/index.php/apps/testing/anonProtected" with "GET" using basic auth |
|||
Then the HTTP status code should be "200" |
|||
|
|||
Scenario: Accessing a page with UserRateThrottle and AnonRateThrottle |
|||
# First request should work as guest |
|||
When requesting "/index.php/apps/testing/userAndAnonProtected" with "GET" |
|||
Then the HTTP status code should be "200" |
|||
# Second request should fail as guest |
|||
When requesting "/index.php/apps/testing/userAndAnonProtected" with "GET" |
|||
Then the HTTP status code should be "429" |
|||
# First request should work as user |
|||
When requesting "/index.php/apps/testing/userAndAnonProtected" with "GET" using basic auth |
|||
Then the HTTP status code should be "200" |
|||
# Second request should work as user |
|||
When requesting "/index.php/apps/testing/userAndAnonProtected" with "GET" using basic auth |
|||
Then the HTTP status code should be "200" |
|||
# Third request should work as user |
|||
When requesting "/index.php/apps/testing/userAndAnonProtected" with "GET" using basic auth |
|||
Then the HTTP status code should be "200" |
|||
# Fourth request should work as user |
|||
When requesting "/index.php/apps/testing/userAndAnonProtected" with "GET" using basic auth |
|||
Then the HTTP status code should be "200" |
|||
# Fifth request should work as user |
|||
When requesting "/index.php/apps/testing/userAndAnonProtected" with "GET" using basic auth |
|||
Then the HTTP status code should be "200" |
|||
# Sixth request should fail as user |
|||
When requesting "/index.php/apps/testing/userAndAnonProtected" with "GET" |
|||
Then the HTTP status code should be "429" |
@ -0,0 +1,106 @@ |
|||
<?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\Security\Normalizer; |
|||
|
|||
/** |
|||
* Class IpAddress is used for normalizing IPv4 and IPv6 addresses in security |
|||
* relevant contexts in Nextcloud. |
|||
* |
|||
* @package OC\Security\Normalizer |
|||
*/ |
|||
class IpAddress { |
|||
/** @var string */ |
|||
private $ip; |
|||
|
|||
/** |
|||
* @param string $ip IP to normalized |
|||
*/ |
|||
public function __construct($ip) { |
|||
$this->ip = $ip; |
|||
} |
|||
|
|||
/** |
|||
* Return the given subnet for an IPv4 address and mask bits |
|||
* |
|||
* @param string $ip |
|||
* @param int $maskBits |
|||
* @return string |
|||
*/ |
|||
private function getIPv4Subnet($ip, |
|||
$maskBits = 32) { |
|||
$binary = \inet_pton($ip); |
|||
for ($i = 32; $i > $maskBits; $i -= 8) { |
|||
$j = \intdiv($i, 8) - 1; |
|||
$k = (int) \min(8, $i - $maskBits); |
|||
$mask = (0xff - ((pow(2, $k)) - 1)); |
|||
$int = \unpack('C', $binary[$j]); |
|||
$binary[$j] = \pack('C', $int[1] & $mask); |
|||
} |
|||
return \inet_ntop($binary).'/'.$maskBits; |
|||
} |
|||
|
|||
/** |
|||
* Return the given subnet for an IPv6 address and mask bits |
|||
* |
|||
* @param string $ip |
|||
* @param int $maskBits |
|||
* @return string |
|||
*/ |
|||
private function getIPv6Subnet($ip, $maskBits = 48) { |
|||
$binary = \inet_pton($ip); |
|||
for ($i = 128; $i > $maskBits; $i -= 8) { |
|||
$j = \intdiv($i, 8) - 1; |
|||
$k = (int) \min(8, $i - $maskBits); |
|||
$mask = (0xff - ((pow(2, $k)) - 1)); |
|||
$int = \unpack('C', $binary[$j]); |
|||
$binary[$j] = \pack('C', $int[1] & $mask); |
|||
} |
|||
return \inet_ntop($binary).'/'.$maskBits; |
|||
} |
|||
|
|||
/** |
|||
* Gets either the /32 (IPv4) or the /128 (IPv6) subnet of an IP address |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getSubnet() { |
|||
if (\preg_match('/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', $this->ip)) { |
|||
return $this->getIPv4Subnet( |
|||
$this->ip, |
|||
32 |
|||
); |
|||
} |
|||
return $this->getIPv6Subnet( |
|||
$this->ip, |
|||
128 |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Returns the specified IP address |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function __toString() { |
|||
return $this->ip; |
|||
} |
|||
} |
@ -0,0 +1,50 @@ |
|||
<?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\Security\RateLimiting\Backend; |
|||
|
|||
/** |
|||
* Interface IBackend defines a storage backend for the rate limiting data. It |
|||
* should be noted that writing and reading rate limiting data is an expensive |
|||
* operation and one should thus make sure to only use sufficient fast backends. |
|||
* |
|||
* @package OC\Security\RateLimiting\Backend |
|||
*/ |
|||
interface IBackend { |
|||
/** |
|||
* Gets the amount of attempts within the last specified seconds |
|||
* |
|||
* @param string $methodIdentifier |
|||
* @param string $userIdentifier |
|||
* @param int $seconds |
|||
* @return int |
|||
*/ |
|||
public function getAttempts($methodIdentifier, $userIdentifier, $seconds); |
|||
|
|||
/** |
|||
* Registers an attempt |
|||
* |
|||
* @param string $methodIdentifier |
|||
* @param string $userIdentifier |
|||
* @param int $timestamp |
|||
*/ |
|||
public function registerAttempt($methodIdentifier, $userIdentifier, $timestamp); |
|||
} |
@ -0,0 +1,100 @@ |
|||
<?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\Security\RateLimiting\Backend; |
|||
|
|||
use OCP\AppFramework\Utility\ITimeFactory; |
|||
use OCP\ICache; |
|||
use OCP\ICacheFactory; |
|||
|
|||
/** |
|||
* Class MemoryCache uses the configured distributed memory cache for storing |
|||
* rate limiting data. |
|||
* |
|||
* @package OC\Security\RateLimiting\Backend |
|||
*/ |
|||
class MemoryCache implements IBackend { |
|||
/** @var ICache */ |
|||
private $cache; |
|||
/** @var ITimeFactory */ |
|||
private $timeFactory; |
|||
|
|||
/** |
|||
* @param ICacheFactory $cacheFactory |
|||
* @param ITimeFactory $timeFactory |
|||
*/ |
|||
public function __construct(ICacheFactory $cacheFactory, |
|||
ITimeFactory $timeFactory) { |
|||
$this->cache = $cacheFactory->create(__CLASS__); |
|||
$this->timeFactory = $timeFactory; |
|||
} |
|||
|
|||
/** |
|||
* @param string $methodIdentifier |
|||
* @param string $userIdentifier |
|||
* @return string |
|||
*/ |
|||
private function hash($methodIdentifier, $userIdentifier) { |
|||
return hash('sha512', $methodIdentifier . $userIdentifier); |
|||
} |
|||
|
|||
/** |
|||
* @param string $identifier |
|||
* @return array |
|||
*/ |
|||
private function getExistingAttempts($identifier) { |
|||
$cachedAttempts = json_decode($this->cache->get($identifier), true); |
|||
if(is_array($cachedAttempts)) { |
|||
return $cachedAttempts; |
|||
} |
|||
|
|||
return []; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
public function getAttempts($methodIdentifier, $userIdentifier, $seconds) { |
|||
$identifier = $this->hash($methodIdentifier, $userIdentifier); |
|||
$existingAttempts = $this->getExistingAttempts($identifier); |
|||
|
|||
$count = 0; |
|||
$currentTime = $this->timeFactory->getTime(); |
|||
/** @var array $existingAttempts */ |
|||
foreach ($existingAttempts as $attempt) { |
|||
if(($attempt + $seconds) > $currentTime) { |
|||
$count++; |
|||
} |
|||
} |
|||
|
|||
return $count; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
public function registerAttempt($methodIdentifier, $userIdentifier, $timestamp) { |
|||
$identifier = $this->hash($methodIdentifier, $userIdentifier); |
|||
$existingAttempts = $this->getExistingAttempts($identifier); |
|||
$existingAttempts[] = (string)$timestamp; |
|||
$this->cache->set($identifier, json_encode($existingAttempts)); |
|||
} |
|||
} |
@ -0,0 +1,31 @@ |
|||
<?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\Security\RateLimiting\Exception; |
|||
|
|||
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException; |
|||
use OCP\AppFramework\Http; |
|||
|
|||
class RateLimitExceededException extends SecurityException { |
|||
public function __construct() { |
|||
parent::__construct('Rate limit exceeded', Http::STATUS_TOO_MANY_REQUESTS); |
|||
} |
|||
} |
@ -0,0 +1,106 @@ |
|||
<?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\Security\RateLimiting; |
|||
|
|||
use OC\Security\Normalizer\IpAddress; |
|||
use OC\Security\RateLimiting\Backend\IBackend; |
|||
use OC\Security\RateLimiting\Exception\RateLimitExceededException; |
|||
use OCP\AppFramework\Utility\ITimeFactory; |
|||
use OCP\IRequest; |
|||
use OCP\IUser; |
|||
use OCP\IUserSession; |
|||
|
|||
class Limiter { |
|||
/** @var IBackend */ |
|||
private $backend; |
|||
/** @var ITimeFactory */ |
|||
private $timeFactory; |
|||
|
|||
/** |
|||
* @param IUserSession $userSession |
|||
* @param IRequest $request |
|||
* @param ITimeFactory $timeFactory |
|||
* @param IBackend $backend |
|||
*/ |
|||
public function __construct(IUserSession $userSession, |
|||
IRequest $request, |
|||
ITimeFactory $timeFactory, |
|||
IBackend $backend) { |
|||
$this->backend = $backend; |
|||
$this->timeFactory = $timeFactory; |
|||
} |
|||
|
|||
/** |
|||
* @param string $methodIdentifier |
|||
* @param string $userIdentifier |
|||
* @param int $period |
|||
* @param int $limit |
|||
* @throws RateLimitExceededException |
|||
*/ |
|||
private function register($methodIdentifier, |
|||
$userIdentifier, |
|||
$period, |
|||
$limit) { |
|||
$existingAttempts = $this->backend->getAttempts($methodIdentifier, $userIdentifier, (int)$period); |
|||
if ($existingAttempts >= (int)$limit) { |
|||
throw new RateLimitExceededException(); |
|||
} |
|||
|
|||
$this->backend->registerAttempt($methodIdentifier, $userIdentifier, $this->timeFactory->getTime()); |
|||
} |
|||
|
|||
/** |
|||
* Registers attempt for an anonymous request |
|||
* |
|||
* @param string $identifier |
|||
* @param int $anonLimit |
|||
* @param int $anonPeriod |
|||
* @param string $ip |
|||
* @throws RateLimitExceededException |
|||
*/ |
|||
public function registerAnonRequest($identifier, |
|||
$anonLimit, |
|||
$anonPeriod, |
|||
$ip) { |
|||
$ipSubnet = (new IpAddress($ip))->getSubnet(); |
|||
|
|||
$anonHashIdentifier = hash('sha512', 'anon::' . $identifier . $ipSubnet); |
|||
$this->register($identifier, $anonHashIdentifier, $anonPeriod, $anonLimit); |
|||
} |
|||
|
|||
/** |
|||
* Registers attempt for an authenticated request |
|||
* |
|||
* @param string $identifier |
|||
* @param int $userLimit |
|||
* @param int $userPeriod |
|||
* @param IUser $user |
|||
* @throws RateLimitExceededException |
|||
*/ |
|||
public function registerUserRequest($identifier, |
|||
$userLimit, |
|||
$userPeriod, |
|||
IUser $user) { |
|||
$userHashIdentifier = hash('sha512', 'user::' . $identifier . $user->getUID()); |
|||
$this->register($identifier, $userHashIdentifier, $userPeriod, $userLimit); |
|||
} |
|||
} |
@ -0,0 +1,59 @@ |
|||
<?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\Security\Normalizer; |
|||
|
|||
use OC\Security\Normalizer\IpAddress; |
|||
use Test\TestCase; |
|||
|
|||
class IpAddressTest extends TestCase { |
|||
|
|||
public function subnetDataProvider() { |
|||
return [ |
|||
[ |
|||
'64.233.191.254', |
|||
'64.233.191.254/32', |
|||
], |
|||
[ |
|||
'192.168.0.123', |
|||
'192.168.0.123/32', |
|||
], |
|||
[ |
|||
'2001:0db8:85a3:0000:0000:8a2e:0370:7334', |
|||
'2001:db8:85a3::8a2e:370:7334/128', |
|||
], |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider subnetDataProvider |
|||
* |
|||
* @param string $input |
|||
* @param string $expected |
|||
*/ |
|||
public function testGetSubnet($input, $expected) { |
|||
$this->assertSame($expected, (new IpAddress($input))->getSubnet()); |
|||
} |
|||
|
|||
public function testToString() { |
|||
$this->assertSame('127.0.0.1', (string)(new IpAddress('127.0.0.1'))); |
|||
} |
|||
} |
@ -0,0 +1,138 @@ |
|||
<?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\Security\RateLimiting\Backend; |
|||
|
|||
use OC\Security\RateLimiting\Backend\MemoryCache; |
|||
use OCP\AppFramework\Utility\ITimeFactory; |
|||
use OCP\ICache; |
|||
use OCP\ICacheFactory; |
|||
use Test\TestCase; |
|||
|
|||
class MemoryCacheTest extends TestCase { |
|||
/** @var ICacheFactory|\PHPUnit_Framework_MockObject_MockObject */ |
|||
private $cacheFactory; |
|||
/** @var ITimeFactory|\PHPUnit_Framework_MockObject_MockObject */ |
|||
private $timeFactory; |
|||
/** @var ICache|\PHPUnit_Framework_MockObject_MockObject */ |
|||
private $cache; |
|||
/** @var MemoryCache */ |
|||
private $memoryCache; |
|||
|
|||
public function setUp() { |
|||
parent::setUp(); |
|||
|
|||
$this->cacheFactory = $this->createMock(ICacheFactory::class); |
|||
$this->timeFactory = $this->createMock(ITimeFactory::class); |
|||
$this->cache = $this->createMock(ICache::class); |
|||
|
|||
$this->cacheFactory |
|||
->expects($this->once()) |
|||
->method('create') |
|||
->with('OC\Security\RateLimiting\Backend\MemoryCache') |
|||
->willReturn($this->cache); |
|||
|
|||
$this->memoryCache = new MemoryCache( |
|||
$this->cacheFactory, |
|||
$this->timeFactory |
|||
); |
|||
} |
|||
|
|||
public function testGetAttemptsWithNoAttemptsBefore() { |
|||
$this->cache |
|||
->expects($this->once()) |
|||
->method('get') |
|||
->with('eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b') |
|||
->willReturn(false); |
|||
|
|||
$this->assertSame(0, $this->memoryCache->getAttempts('Method', 'User', 123)); |
|||
} |
|||
|
|||
public function testGetAttempts() { |
|||
$this->timeFactory |
|||
->expects($this->once()) |
|||
->method('getTime') |
|||
->willReturn(210); |
|||
$this->cache |
|||
->expects($this->once()) |
|||
->method('get') |
|||
->with('eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b') |
|||
->willReturn(json_encode([ |
|||
'1', |
|||
'2', |
|||
'87', |
|||
'123', |
|||
'123', |
|||
'124', |
|||
])); |
|||
|
|||
$this->assertSame(3, $this->memoryCache->getAttempts('Method', 'User', 123)); |
|||
} |
|||
|
|||
public function testRegisterAttemptWithNoAttemptsBefore() { |
|||
$this->cache |
|||
->expects($this->once()) |
|||
->method('get') |
|||
->with('eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b') |
|||
->willReturn(false); |
|||
$this->cache |
|||
->expects($this->once()) |
|||
->method('set') |
|||
->with( |
|||
'eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b', |
|||
json_encode(['123']) |
|||
); |
|||
|
|||
$this->memoryCache->registerAttempt('Method', 'User', 123); |
|||
} |
|||
|
|||
public function testRegisterAttempts() { |
|||
$this->cache |
|||
->expects($this->once()) |
|||
->method('get') |
|||
->with('eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b') |
|||
->willReturn(json_encode([ |
|||
'1', |
|||
'2', |
|||
'87', |
|||
'123', |
|||
'123', |
|||
'124', |
|||
])); |
|||
$this->cache |
|||
->expects($this->once()) |
|||
->method('set') |
|||
->with( |
|||
'eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b', |
|||
json_encode([ |
|||
'1', |
|||
'2', |
|||
'87', |
|||
'123', |
|||
'123', |
|||
'124', |
|||
'129', |
|||
]) |
|||
); |
|||
|
|||
$this->memoryCache->registerAttempt('Method', 'User', 129); |
|||
} |
|||
} |
@ -0,0 +1,161 @@ |
|||
<?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\Security\RateLimiting; |
|||
|
|||
use OC\Security\RateLimiting\Backend\IBackend; |
|||
use OC\Security\RateLimiting\Limiter; |
|||
use OCP\AppFramework\Utility\ITimeFactory; |
|||
use OCP\ICacheFactory; |
|||
use OCP\IRequest; |
|||
use OCP\IUser; |
|||
use OCP\IUserSession; |
|||
use Test\TestCase; |
|||
|
|||
class LimiterTest extends TestCase { |
|||
/** @var IUserSession|\PHPUnit_Framework_MockObject_MockObject */ |
|||
private $userSession; |
|||
/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */ |
|||
private $request; |
|||
/** @var ITimeFactory|\PHPUnit_Framework_MockObject_MockObject */ |
|||
private $timeFactory; |
|||
/** @var IBackend|\PHPUnit_Framework_MockObject_MockObject */ |
|||
private $backend; |
|||
/** @var Limiter */ |
|||
private $limiter; |
|||
|
|||
public function setUp() { |
|||
parent::setUp(); |
|||
|
|||
$this->userSession = $this->createMock(IUserSession::class); |
|||
$this->request = $this->createMock(IRequest::class); |
|||
$this->timeFactory = $this->createMock(ITimeFactory::class); |
|||
$this->backend = $this->createMock(IBackend::class); |
|||
|
|||
$this->limiter = new Limiter( |
|||
$this->userSession, |
|||
$this->request, |
|||
$this->timeFactory, |
|||
$this->backend |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* @expectedException \OC\Security\RateLimiting\Exception\RateLimitExceededException |
|||
* @expectedExceptionMessage Rate limit exceeded |
|||
*/ |
|||
public function testRegisterAnonRequestExceeded() { |
|||
$this->backend |
|||
->expects($this->once()) |
|||
->method('getAttempts') |
|||
->with( |
|||
'MyIdentifier', |
|||
'4664f0d9c88dcb7552be47b37bb52ce35977b2e60e1ac13757cf625f31f87050a41f3da064887fa87d49fd042e4c8eb20de8f10464877d3959677ab011b73a47', |
|||
100 |
|||
) |
|||
->willReturn(101); |
|||
|
|||
$this->limiter->registerAnonRequest('MyIdentifier', 100, 100, '127.0.0.1'); |
|||
} |
|||
|
|||
public function testRegisterAnonRequestSuccess() { |
|||
$this->timeFactory |
|||
->expects($this->once()) |
|||
->method('getTime') |
|||
->willReturn(2000); |
|||
$this->backend |
|||
->expects($this->once()) |
|||
->method('getAttempts') |
|||
->with( |
|||
'MyIdentifier', |
|||
'4664f0d9c88dcb7552be47b37bb52ce35977b2e60e1ac13757cf625f31f87050a41f3da064887fa87d49fd042e4c8eb20de8f10464877d3959677ab011b73a47', |
|||
100 |
|||
) |
|||
->willReturn(99); |
|||
$this->backend |
|||
->expects($this->once()) |
|||
->method('registerAttempt') |
|||
->with( |
|||
'MyIdentifier', |
|||
'4664f0d9c88dcb7552be47b37bb52ce35977b2e60e1ac13757cf625f31f87050a41f3da064887fa87d49fd042e4c8eb20de8f10464877d3959677ab011b73a47', |
|||
2000 |
|||
); |
|||
|
|||
$this->limiter->registerAnonRequest('MyIdentifier', 100, 100, '127.0.0.1'); |
|||
} |
|||
|
|||
/** |
|||
* @expectedException \OC\Security\RateLimiting\Exception\RateLimitExceededException |
|||
* @expectedExceptionMessage Rate limit exceeded |
|||
*/ |
|||
public function testRegisterUserRequestExceeded() { |
|||
/** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ |
|||
$user = $this->createMock(IUser::class); |
|||
$user |
|||
->expects($this->once()) |
|||
->method('getUID') |
|||
->willReturn('MyUid'); |
|||
$this->backend |
|||
->expects($this->once()) |
|||
->method('getAttempts') |
|||
->with( |
|||
'MyIdentifier', |
|||
'ddb2ec50fa973fd49ecf3d816f677c8095143e944ad10485f30fb3dac85c13a346dace4dae2d0a15af91867320957bfd38a43d9eefbb74fe6919e15119b6d805', |
|||
100 |
|||
) |
|||
->willReturn(101); |
|||
|
|||
$this->limiter->registerUserRequest('MyIdentifier', 100, 100, $user); |
|||
} |
|||
|
|||
public function testRegisterUserRequestSuccess() { |
|||
/** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ |
|||
$user = $this->createMock(IUser::class); |
|||
$user |
|||
->expects($this->once()) |
|||
->method('getUID') |
|||
->willReturn('MyUid'); |
|||
|
|||
$this->timeFactory |
|||
->expects($this->once()) |
|||
->method('getTime') |
|||
->willReturn(2000); |
|||
$this->backend |
|||
->expects($this->once()) |
|||
->method('getAttempts') |
|||
->with( |
|||
'MyIdentifier', |
|||
'ddb2ec50fa973fd49ecf3d816f677c8095143e944ad10485f30fb3dac85c13a346dace4dae2d0a15af91867320957bfd38a43d9eefbb74fe6919e15119b6d805', |
|||
100 |
|||
) |
|||
->willReturn(99); |
|||
$this->backend |
|||
->expects($this->once()) |
|||
->method('registerAttempt') |
|||
->with( |
|||
'MyIdentifier', |
|||
'ddb2ec50fa973fd49ecf3d816f677c8095143e944ad10485f30fb3dac85c13a346dace4dae2d0a15af91867320957bfd38a43d9eefbb74fe6919e15119b6d805', |
|||
2000 |
|||
); |
|||
|
|||
$this->limiter->registerUserRequest('MyIdentifier', 100, 100, $user); |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue