diff options
34 files changed, 1595 insertions, 334 deletions
diff --git a/.drone.yml b/.drone.yml index 24e06dd1b25..2d4134957ad 100644 --- a/.drone.yml +++ b/.drone.yml @@ -277,6 +277,18 @@ pipeline: when: matrix: TESTS: integration-maintenance-mode + integration-ratelimiting: + image: nextcloudci/integration-php7.0:integration-php7.0-3 + commands: + - ./occ maintenance:install --admin-pass=admin + - ./occ config:system:set --type string --value "\\OC\\Memcache\\Redis" memcache.local + - ./occ config:system:set --type string --value "\\OC\\Memcache\\Redis" memcache.distributed + - ./occ app:enable testing + - cd build/integration + - ./run.sh features/ratelimiting.feature + when: + matrix: + TESTS: integration-ratelimiting integration-carddav: image: nextcloudci/integration-php7.0:integration-php7.0-3 commands: @@ -516,6 +528,7 @@ matrix: - TESTS: integration-capabilities_features - TESTS: integration-federation_features - TESTS: integration-maintenance-mode + - TESTS: integration-ratelimiting - TESTS: integration-auth - TESTS: integration-carddav - TESTS: integration-dav-v2 diff --git a/apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php b/apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php index d7e466d1a64..9f848fbbb78 100644 --- a/apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php +++ b/apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php @@ -120,7 +120,7 @@ class MountPublicLinkController extends Controller { * * @NoCSRFRequired * @PublicPage - * @BruteForceProtection publicLink2FederatedShare + * @BruteForceProtection(action=publicLink2FederatedShare) * * @param string $shareWith * @param string $token diff --git a/apps/files_sharing/appinfo/routes.php b/apps/files_sharing/appinfo/routes.php index 439d46253d4..56eece341a9 100644 --- a/apps/files_sharing/appinfo/routes.php +++ b/apps/files_sharing/appinfo/routes.php @@ -125,8 +125,6 @@ $application->registerRoutes($this, [ ]); /** @var $this \OCP\Route\IRouter */ -$this->create('files_sharing_ajax_list', 'ajax/list.php') - ->actionInclude('files_sharing/ajax/list.php'); $this->create('sharing_external_shareinfo', '/shareinfo') ->actionInclude('files_sharing/ajax/shareinfo.php'); diff --git a/apps/files_sharing/lib/Controller/ShareController.php b/apps/files_sharing/lib/Controller/ShareController.php index 2c6e953a0f5..732a1d32ee7 100644 --- a/apps/files_sharing/lib/Controller/ShareController.php +++ b/apps/files_sharing/lib/Controller/ShareController.php @@ -160,7 +160,7 @@ class ShareController extends Controller { /** * @PublicPage * @UseSession - * @BruteForceProtection publicLinkAuth + * @BruteForceProtection(action=publicLinkAuth) * * Authenticates against password-protected shares * @param string $token diff --git a/apps/testing/appinfo/routes.php b/apps/testing/appinfo/routes.php index 13caa2289df..d45cfe00eca 100644 --- a/apps/testing/appinfo/routes.php +++ b/apps/testing/appinfo/routes.php @@ -25,12 +25,32 @@ namespace OCA\Testing\AppInfo; use OCA\Testing\Config; use OCA\Testing\Locking\Provisioning; use OCP\API; +use OCP\AppFramework\App; $config = new Config( \OC::$server->getConfig(), \OC::$server->getRequest() ); +$app = new App('testing'); +$app->registerRoutes( + $this, + [ + 'routes' => [ + [ + 'name' => 'RateLimitTest#userAndAnonProtected', + 'url' => '/userAndAnonProtected', + 'verb' => 'GET', + ], + [ + 'name' => 'RateLimitTest#onlyAnonProtected', + 'url' => '/anonProtected', + 'verb' => 'GET', + ], + ] + ] +); + API::register( 'post', '/apps/testing/api/v1/app/{appid}/{configkey}', diff --git a/apps/testing/lib/Controller/RateLimitTestController.php b/apps/testing/lib/Controller/RateLimitTestController.php new file mode 100644 index 00000000000..c43d33e5335 --- /dev/null +++ b/apps/testing/lib/Controller/RateLimitTestController.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(); + } +} diff --git a/apps/user_ldap/lib/Access.php b/apps/user_ldap/lib/Access.php index dbc4f5b0448..959a8dd2b8e 100644 --- a/apps/user_ldap/lib/Access.php +++ b/apps/user_ldap/lib/Access.php @@ -595,8 +595,8 @@ class Access extends LDAPUtility implements IUserTools { * * gives back the user names as they are used ownClod internally */ - public function ownCloudUserNames($ldapUsers) { - return $this->ldap2ownCloudNames($ldapUsers, true); + public function nextcloudUserNames($ldapUsers) { + return $this->ldap2NextcloudNames($ldapUsers, true); } /** @@ -606,8 +606,8 @@ class Access extends LDAPUtility implements IUserTools { * * gives back the group names as they are used ownClod internally */ - public function ownCloudGroupNames($ldapGroups) { - return $this->ldap2ownCloudNames($ldapGroups, false); + public function nextcloudGroupNames($ldapGroups) { + return $this->ldap2NextcloudNames($ldapGroups, false); } /** @@ -615,14 +615,14 @@ class Access extends LDAPUtility implements IUserTools { * @param bool $isUsers * @return array */ - private function ldap2ownCloudNames($ldapObjects, $isUsers) { + private function ldap2NextcloudNames($ldapObjects, $isUsers) { if($isUsers) { $nameAttribute = $this->connection->ldapUserDisplayName; $sndAttribute = $this->connection->ldapUserDisplayName2; } else { $nameAttribute = $this->connection->ldapGroupDisplayName; } - $ownCloudNames = array(); + $nextcloudNames = array(); foreach($ldapObjects as $ldapObject) { $nameByLDAP = null; @@ -634,9 +634,9 @@ class Access extends LDAPUtility implements IUserTools { $nameByLDAP = $ldapObject[$nameAttribute][0]; } - $ocName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers); - if($ocName) { - $ownCloudNames[] = $ocName; + $ncName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers); + if($ncName) { + $nextcloudNames[] = $ncName; if($isUsers) { //cache the user names so it does not need to be retrieved //again later (e.g. sharing dialogue). @@ -645,11 +645,11 @@ class Access extends LDAPUtility implements IUserTools { } $sndName = isset($ldapObject[$sndAttribute][0]) ? $ldapObject[$sndAttribute][0] : ''; - $this->cacheUserDisplayName($ocName, $nameByLDAP, $sndName); + $this->cacheUserDisplayName($ncName, $nameByLDAP, $sndName); } } } - return $NextcloudNames; + return $nextcloudNames; } /** diff --git a/apps/user_ldap/lib/Group_LDAP.php b/apps/user_ldap/lib/Group_LDAP.php index f1ea831e485..b6013e77766 100644 --- a/apps/user_ldap/lib/Group_LDAP.php +++ b/apps/user_ldap/lib/Group_LDAP.php @@ -388,7 +388,7 @@ class Group_LDAP extends BackendUtility implements \OCP\GroupInterface { $limit, $offset ); - return $this->access->ownCloudUserNames($users); + return $this->access->nextcloudUserNames($users); } catch (\Exception $e) { return array(); } @@ -541,7 +541,7 @@ class Group_LDAP extends BackendUtility implements \OCP\GroupInterface { $groups = array_merge($groups, $this->cachedGroupsByMember[$uid]); } else { $groupsByMember = array_values($this->getGroupsByMember($uid)); - $groupsByMember = $this->access->ownCloudGroupNames($groupsByMember); + $groupsByMember = $this->access->nextcloudGroupNames($groupsByMember); $this->cachedGroupsByMember[$uid] = $groupsByMember; $groups = array_merge($groups, $groupsByMember); } @@ -804,7 +804,7 @@ class Group_LDAP extends BackendUtility implements \OCP\GroupInterface { array($this->access->connection->ldapGroupDisplayName, 'dn'), $limit, $offset); - $ldap_groups = $this->access->ownCloudGroupNames($ldap_groups); + $ldap_groups = $this->access->nextcloudGroupNames($ldap_groups); $this->access->connection->writeToCache($cacheKey, $ldap_groups); return $ldap_groups; diff --git a/apps/user_ldap/lib/User_LDAP.php b/apps/user_ldap/lib/User_LDAP.php index 44de3f5da40..fa959fd9a81 100644 --- a/apps/user_ldap/lib/User_LDAP.php +++ b/apps/user_ldap/lib/User_LDAP.php @@ -234,7 +234,7 @@ class User_LDAP extends BackendUtility implements \OCP\IUserBackend, \OCP\UserIn $filter, $this->access->userManager->getAttributes(true), $limit, $offset); - $ldap_users = $this->access->ownCloudUserNames($ldap_users); + $ldap_users = $this->access->nextcloudUserNames($ldap_users); Util::writeLog('user_ldap', 'getUsers: '.count($ldap_users). ' Users found', Util::DEBUG); $this->access->connection->writeToCache($cachekey, $ldap_users); diff --git a/apps/user_ldap/tests/Group_LDAPTest.php b/apps/user_ldap/tests/Group_LDAPTest.php index 906db6bb17b..621a427eaac 100644 --- a/apps/user_ldap/tests/Group_LDAPTest.php +++ b/apps/user_ldap/tests/Group_LDAPTest.php @@ -315,7 +315,7 @@ class Group_LDAPTest extends \Test\TestCase { $this->enableGroups($access); $access->expects($this->once()) - ->method('ownCloudGroupNames') + ->method('nextcloudGroupNames') ->will($this->returnValue(array('group1', 'group2'))); $groupBackend = new GroupLDAP($access); @@ -350,7 +350,7 @@ class Group_LDAPTest extends \Test\TestCase { ->will($this->returnValue('cn=foobar,dc=foo,dc=bar')); $access->expects($this->once()) - ->method('ownCloudUserNames') + ->method('nextcloudUserNames') ->will($this->returnValue(array('lisa', 'bart', 'kira', 'brad'))); $groupBackend = new GroupLDAP($access); @@ -451,7 +451,7 @@ class Group_LDAPTest extends \Test\TestCase { ->with($dn, 'memberOf'); $access->expects($this->once()) - ->method('ownCloudGroupNames') + ->method('nextcloudGroupNames') ->will($this->returnValue([])); $groupBackend = new GroupLDAP($access); @@ -496,7 +496,7 @@ class Group_LDAPTest extends \Test\TestCase { ]; $access->expects($this->once()) - ->method('ownCloudGroupNames') + ->method('nextcloudGroupNames') ->with([$group1, $group2]) ->will($this->returnValue(['group1', 'group2'])); diff --git a/apps/user_ldap/tests/User_LDAPTest.php b/apps/user_ldap/tests/User_LDAPTest.php index f1a23f9a6c8..1b1f9fdec78 100644 --- a/apps/user_ldap/tests/User_LDAPTest.php +++ b/apps/user_ldap/tests/User_LDAPTest.php @@ -349,7 +349,7 @@ class User_LDAPTest extends TestCase { })); $access->expects($this->any()) - ->method('ownCloudUserNames') + ->method('nextcloudUserNames') ->will($this->returnArgument(0)); } diff --git a/build/integration/features/ratelimiting.feature b/build/integration/features/ratelimiting.feature new file mode 100644 index 00000000000..bd8b2e30a73 --- /dev/null +++ b/build/integration/features/ratelimiting.feature @@ -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" diff --git a/core/Controller/LostController.php b/core/Controller/LostController.php index 639dd9da574..a4768cbeafc 100644 --- a/core/Controller/LostController.php +++ b/core/Controller/LostController.php @@ -204,7 +204,7 @@ class LostController extends Controller { /** * @PublicPage - * @BruteForceProtection passwordResetEmail + * @BruteForceProtection(action=passwordResetEmail) * * @param string $user * @return array diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index ab6a3781147..51f822a0e71 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -298,6 +298,7 @@ return array( 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\NotLoggedInException' => $baseDir . '/lib/private/AppFramework/Middleware/Security/Exceptions/NotLoggedInException.php', 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\SecurityException' => $baseDir . '/lib/private/AppFramework/Middleware/Security/Exceptions/SecurityException.php', 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\StrictCookieMissingException' => $baseDir . '/lib/private/AppFramework/Middleware/Security/Exceptions/StrictCookieMissingException.php', + 'OC\\AppFramework\\Middleware\\Security\\RateLimitingMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\SecurityMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php', 'OC\\AppFramework\\Middleware\\SessionMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/SessionMiddleware.php', 'OC\\AppFramework\\OCS\\BaseResponse' => $baseDir . '/lib/private/AppFramework/OCS/BaseResponse.php', @@ -742,6 +743,11 @@ return array( 'OC\\Security\\IdentityProof\\Key' => $baseDir . '/lib/private/Security/IdentityProof/Key.php', 'OC\\Security\\IdentityProof\\Manager' => $baseDir . '/lib/private/Security/IdentityProof/Manager.php', 'OC\\Security\\IdentityProof\\Signer' => $baseDir . '/lib/private/Security/IdentityProof/Signer.php', + 'OC\\Security\\Normalizer\\IpAddress' => $baseDir . '/lib/private/Security/Normalizer/IpAddress.php', + 'OC\\Security\\RateLimiting\\Backend\\IBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/IBackend.php', + 'OC\\Security\\RateLimiting\\Backend\\MemoryCache' => $baseDir . '/lib/private/Security/RateLimiting/Backend/MemoryCache.php', + 'OC\\Security\\RateLimiting\\Exception\\RateLimitExceededException' => $baseDir . '/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php', + 'OC\\Security\\RateLimiting\\Limiter' => $baseDir . '/lib/private/Security/RateLimiting/Limiter.php', 'OC\\Security\\SecureRandom' => $baseDir . '/lib/private/Security/SecureRandom.php', 'OC\\Security\\TrustedDomainHelper' => $baseDir . '/lib/private/Security/TrustedDomainHelper.php', 'OC\\Server' => $baseDir . '/lib/private/Server.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 1b2c9f84df8..29c90798485 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -328,6 +328,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\NotLoggedInException' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/Exceptions/NotLoggedInException.php', 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\SecurityException' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/Exceptions/SecurityException.php', 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\StrictCookieMissingException' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/Exceptions/StrictCookieMissingException.php', + 'OC\\AppFramework\\Middleware\\Security\\RateLimitingMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\SecurityMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php', 'OC\\AppFramework\\Middleware\\SessionMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/SessionMiddleware.php', 'OC\\AppFramework\\OCS\\BaseResponse' => __DIR__ . '/../../..' . '/lib/private/AppFramework/OCS/BaseResponse.php', @@ -772,6 +773,11 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Security\\IdentityProof\\Key' => __DIR__ . '/../../..' . '/lib/private/Security/IdentityProof/Key.php', 'OC\\Security\\IdentityProof\\Manager' => __DIR__ . '/../../..' . '/lib/private/Security/IdentityProof/Manager.php', 'OC\\Security\\IdentityProof\\Signer' => __DIR__ . '/../../..' . '/lib/private/Security/IdentityProof/Signer.php', + 'OC\\Security\\Normalizer\\IpAddress' => __DIR__ . '/../../..' . '/lib/private/Security/Normalizer/IpAddress.php', + 'OC\\Security\\RateLimiting\\Backend\\IBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/IBackend.php', + 'OC\\Security\\RateLimiting\\Backend\\MemoryCache' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/MemoryCache.php', + 'OC\\Security\\RateLimiting\\Exception\\RateLimitExceededException' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php', + 'OC\\Security\\RateLimiting\\Limiter' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Limiter.php', 'OC\\Security\\SecureRandom' => __DIR__ . '/../../..' . '/lib/private/Security/SecureRandom.php', 'OC\\Security\\TrustedDomainHelper' => __DIR__ . '/../../..' . '/lib/private/Security/TrustedDomainHelper.php', 'OC\\Server' => __DIR__ . '/../../..' . '/lib/private/Server.php', diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index 4fb13b09ae0..d279140afbb 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -40,6 +40,7 @@ use OC\AppFramework\Http\Output; use OC\AppFramework\Middleware\MiddlewareDispatcher; use OC\AppFramework\Middleware\Security\CORSMiddleware; use OC\AppFramework\Middleware\OCSMiddleware; +use OC\AppFramework\Middleware\Security\RateLimitingMiddleware; use OC\AppFramework\Middleware\Security\SecurityMiddleware; use OC\AppFramework\Middleware\SessionMiddleware; use OC\AppFramework\Utility\SimpleContainer; @@ -169,7 +170,6 @@ class DIContainer extends SimpleContainer implements IAppContainer { ); }); - /** * App Framework APIs */ @@ -230,6 +230,18 @@ class DIContainer extends SimpleContainer implements IAppContainer { }); + $this->registerService('RateLimitingMiddleware', function($c) use ($app) { + /** @var \OC\Server $server */ + $server = $app->getServer(); + + return new RateLimitingMiddleware( + $server->getRequest(), + $server->getUserSession(), + $c['ControllerMethodReflector'], + $c->query(OC\Security\RateLimiting\Limiter::class) + ); + }); + $this->registerService('CORSMiddleware', function($c) { return new CORSMiddleware( $c['Request'], @@ -270,6 +282,7 @@ class DIContainer extends SimpleContainer implements IAppContainer { $dispatcher->registerMiddleware($c['OCSMiddleware']); $dispatcher->registerMiddleware($c['SecurityMiddleware']); $dispatcher->registerMiddleWare($c['TwoFactorMiddleware']); + $dispatcher->registerMiddleware($c['RateLimitingMiddleware']); foreach($middleWares as $middleWare) { $dispatcher->registerMiddleware($c[$middleWare]); diff --git a/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php b/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php new file mode 100644 index 00000000000..e2ad7955dd0 --- /dev/null +++ b/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php @@ -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; + } +} diff --git a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php index edba6a3e759..d4d0598c94f 100644 --- a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php @@ -192,7 +192,7 @@ class SecurityMiddleware extends Middleware { } if($this->reflector->hasAnnotation('BruteForceProtection')) { - $action = $this->reflector->getAnnotationParameter('BruteForceProtection'); + $action = $this->reflector->getAnnotationParameter('BruteForceProtection', 'action'); $this->throttler->sleepDelay($this->request->getRemoteAddress(), $action); $this->throttler->registerAttempt($action, $this->request->getRemoteAddress()); } diff --git a/lib/private/AppFramework/Utility/ControllerMethodReflector.php b/lib/private/AppFramework/Utility/ControllerMethodReflector.php index 034fc3a1759..d6a9b596127 100644 --- a/lib/private/AppFramework/Utility/ControllerMethodReflector.php +++ b/lib/private/AppFramework/Utility/ControllerMethodReflector.php @@ -24,27 +24,17 @@ * */ - namespace OC\AppFramework\Utility; use \OCP\AppFramework\Utility\IControllerMethodReflector; - /** * Reads and parses annotations from doc comments */ -class ControllerMethodReflector implements IControllerMethodReflector{ - - private $annotations; - private $types; - private $parameters; - - public function __construct() { - $this->types = array(); - $this->parameters = array(); - $this->annotations = array(); - } - +class ControllerMethodReflector implements IControllerMethodReflector { + public $annotations = []; + private $types = []; + private $parameters = []; /** * @param object $object an object or classname @@ -55,9 +45,21 @@ class ControllerMethodReflector implements IControllerMethodReflector{ $docs = $reflection->getDocComment(); // extract everything prefixed by @ and first letter uppercase - preg_match_all('/^\h+\*\h+@(?P<annotation>[A-Z]\w+)(\h+(?P<parameter>\w+))?$/m', $docs, $matches); + preg_match_all('/^\h+\*\h+@(?P<annotation>[A-Z]\w+)((?P<parameter>.*))?$/m', $docs, $matches); foreach($matches['annotation'] as $key => $annontation) { - $this->annotations[$annontation] = $matches['parameter'][$key]; + $annotationValue = $matches['parameter'][$key]; + if(isset($annotationValue[0]) && $annotationValue[0] === '(' && $annotationValue[strlen($annotationValue) - 1] === ')') { + $cutString = substr($annotationValue, 1, -1); + $cutString = str_replace(' ', '', $cutString); + $splittedArray = explode(',', $cutString); + foreach($splittedArray as $annotationValues) { + list($key, $value) = explode('=', $annotationValues); + $this->annotations[$annontation][$key] = $value; + } + continue; + } + + $this->annotations[$annontation] = [$annotationValue]; } // extract type parameter information @@ -83,7 +85,6 @@ class ControllerMethodReflector implements IControllerMethodReflector{ } } - /** * Inspects the PHPDoc parameters for types * @param string $parameter the parameter whose type comments should be @@ -99,7 +100,6 @@ class ControllerMethodReflector implements IControllerMethodReflector{ } } - /** * @return array the arguments of the method with key => default value */ @@ -107,30 +107,27 @@ class ControllerMethodReflector implements IControllerMethodReflector{ return $this->parameters; } - /** * Check if a method contains an annotation * @param string $name the name of the annotation * @return bool true if the annotation is found */ - public function hasAnnotation($name){ + public function hasAnnotation($name) { return array_key_exists($name, $this->annotations); } - /** - * Get optional annotation parameter + * Get optional annotation parameter by key + * * @param string $name the name of the annotation + * @param string $key the string of the annotation * @return string */ - public function getAnnotationParameter($name){ - $parameter = ''; - if($this->hasAnnotation($name)) { - $parameter = $this->annotations[$name]; + public function getAnnotationParameter($name, $key) { + if(isset($this->annotations[$name][$key])) { + return $this->annotations[$name][$key]; } - return $parameter; + return ''; } - - } diff --git a/lib/private/Security/Bruteforce/Throttler.php b/lib/private/Security/Bruteforce/Throttler.php index 73a27b677b0..b2524b63c63 100644 --- a/lib/private/Security/Bruteforce/Throttler.php +++ b/lib/private/Security/Bruteforce/Throttler.php @@ -23,6 +23,7 @@ namespace OC\Security\Bruteforce; +use OC\Security\Normalizer\IpAddress; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IConfig; use OCP\IDBConnection; @@ -83,67 +84,6 @@ class Throttler { } /** - * 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; - } - - /** - * Return the given subnet for an IP and the configured mask bits - * - * Determine if the IP is an IPv4 or IPv6 address, then pass to the correct - * method for handling that specific type. - * - * @param string $ip - * @return string - */ - private function getSubnet($ip) { - if (\preg_match('/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', $ip)) { - return $this->getIPv4Subnet( - $ip, - 32 - ); - } - return $this->getIPv6Subnet( - $ip, - 128 - ); - } - - /** * Register a failed attempt to bruteforce a security control * * @param string $action @@ -158,11 +98,12 @@ class Throttler { return; } + $ipAddress = new IpAddress($ip); $values = [ 'action' => $action, 'occurred' => $this->timeFactory->getTime(), - 'ip' => $ip, - 'subnet' => $this->getSubnet($ip), + 'ip' => (string)$ipAddress, + 'subnet' => $ipAddress->getSubnet(), 'metadata' => json_encode($metadata), ]; @@ -254,7 +195,8 @@ class Throttler { * @return int */ public function getDelay($ip, $action = '') { - if ($this->isIPWhitelisted($ip)) { + $ipAddress = new IpAddress($ip); + if ($this->isIPWhitelisted((string)$ipAddress)) { return 0; } @@ -266,7 +208,7 @@ class Throttler { $qb->select('*') ->from('bruteforce_attempts') ->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime))) - ->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($this->getSubnet($ip)))); + ->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($ipAddress->getSubnet()))); if ($action !== '') { $qb->andWhere($qb->expr()->eq('action', $qb->createNamedParameter($action))); diff --git a/lib/private/Security/Normalizer/IpAddress.php b/lib/private/Security/Normalizer/IpAddress.php new file mode 100644 index 00000000000..c44a5556678 --- /dev/null +++ b/lib/private/Security/Normalizer/IpAddress.php @@ -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; + } +} diff --git a/lib/private/Security/RateLimiting/Backend/IBackend.php b/lib/private/Security/RateLimiting/Backend/IBackend.php new file mode 100644 index 00000000000..9753eb4997c --- /dev/null +++ b/lib/private/Security/RateLimiting/Backend/IBackend.php @@ -0,0 +1,54 @@ +<?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 Identifier for the method + * @param string $userIdentifier Identifier for the user + * @param int $seconds Seconds to look back at + * @return int + */ + public function getAttempts($methodIdentifier, + $userIdentifier, + $seconds); + + /** + * Registers an attempt + * + * @param string $methodIdentifier Identifier for the method + * @param string $userIdentifier Identifier for the user + * @param int $period Period in seconds how long this attempt should be stored + */ + public function registerAttempt($methodIdentifier, + $userIdentifier, + $period); +} diff --git a/lib/private/Security/RateLimiting/Backend/MemoryCache.php b/lib/private/Security/RateLimiting/Backend/MemoryCache.php new file mode 100644 index 00000000000..25595cda4a5 --- /dev/null +++ b/lib/private/Security/RateLimiting/Backend/MemoryCache.php @@ -0,0 +1,116 @@ +<?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, + $period) { + $identifier = $this->hash($methodIdentifier, $userIdentifier); + $existingAttempts = $this->getExistingAttempts($identifier); + $currentTime = $this->timeFactory->getTime(); + + // Unset all attempts older than $period + foreach ($existingAttempts as $key => $attempt) { + if(($attempt + $period) < $currentTime) { + unset($existingAttempts[$key]); + } + } + $existingAttempts = array_values($existingAttempts); + + // Store the new attempt + $existingAttempts[] = (string)$currentTime; + $this->cache->set($identifier, json_encode($existingAttempts)); + } +} diff --git a/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php b/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php new file mode 100644 index 00000000000..34cbec31c73 --- /dev/null +++ b/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php @@ -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); + } +} diff --git a/lib/private/Security/RateLimiting/Limiter.php b/lib/private/Security/RateLimiting/Limiter.php new file mode 100644 index 00000000000..5c084eb934b --- /dev/null +++ b/lib/private/Security/RateLimiting/Limiter.php @@ -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); + } +} diff --git a/lib/private/Server.php b/lib/private/Server.php index 00698a04f89..2aa7c15af00 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -516,6 +516,21 @@ class Server extends ServerContainer implements IServerContainer { }); $this->registerAlias('Search', \OCP\ISearch::class); + $this->registerService(\OC\Security\RateLimiting\Limiter::class, function($c) { + return new \OC\Security\RateLimiting\Limiter( + $this->getUserSession(), + $this->getRequest(), + new \OC\AppFramework\Utility\TimeFactory(), + $c->query(\OC\Security\RateLimiting\Backend\IBackend::class) + ); + }); + $this->registerService(\OC\Security\RateLimiting\Backend\IBackend::class, function($c) { + return new \OC\Security\RateLimiting\Backend\MemoryCache( + $this->getMemCacheFactory(), + new \OC\AppFramework\Utility\TimeFactory() + ); + }); + $this->registerService(\OCP\Security\ISecureRandom::class, function ($c) { return new SecureRandom(); }); diff --git a/resources/config/ca-bundle.crt b/resources/config/ca-bundle.crt index cce0e5835ee..8849e024f39 100644 --- a/resources/config/ca-bundle.crt +++ b/resources/config/ca-bundle.crt @@ -1,7 +1,7 @@ ## ## Bundle of CA Root Certificates ## -## Certificate data from Mozilla as of: Wed Nov 2 04:12:05 2016 GMT +## Certificate data from Mozilla as of: Wed Jan 18 04:12:05 2017 GMT ## ## This is a bundle of X.509 certificates of public Certificate Authorities ## (CA). These were automatically extracted from Mozilla's root certificates @@ -14,7 +14,7 @@ ## Just configure this file as the SSLCACertificateFile. ## ## Conversion done with mk-ca-bundle.pl version 1.27. -## SHA256: 17e2a90c8a5cfd6a675b3475d3d467e1ab1fe0d5397e907b08206182389caa08 +## SHA256: dffa79e6aa993f558e82884abf7bb54bf440ab66ee91d82a27a627f6f2a4ace4 ## @@ -252,27 +252,6 @@ W3iDVuycNsMm4hH2Z0kdkquM++v/eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0 tHuu2guQOHXvgR1m0vdXcDazv/wor3ElhVsT/h5/WrQ8 -----END CERTIFICATE----- -RSA Security 2048 v3 -==================== ------BEGIN CERTIFICATE----- -MIIDYTCCAkmgAwIBAgIQCgEBAQAAAnwAAAAKAAAAAjANBgkqhkiG9w0BAQUFADA6MRkwFwYDVQQK -ExBSU0EgU2VjdXJpdHkgSW5jMR0wGwYDVQQLExRSU0EgU2VjdXJpdHkgMjA0OCBWMzAeFw0wMTAy -MjIyMDM5MjNaFw0yNjAyMjIyMDM5MjNaMDoxGTAXBgNVBAoTEFJTQSBTZWN1cml0eSBJbmMxHTAb -BgNVBAsTFFJTQSBTZWN1cml0eSAyMDQ4IFYzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEAt49VcdKA3XtpeafwGFAyPGJn9gqVB93mG/Oe2dJBVGutn3y+Gc37RqtBaB4Y6lXIL5F4iSj7 -Jylg/9+PjDvJSZu1pJTOAeo+tWN7fyb9Gd3AIb2E0S1PRsNO3Ng3OTsor8udGuorryGlwSMiuLgb -WhOHV4PR8CDn6E8jQrAApX2J6elhc5SYcSa8LWrg903w8bYqODGBDSnhAMFRD0xS+ARaqn1y07iH -KrtjEAMqs6FPDVpeRrc9DvV07Jmf+T0kgYim3WBU6JU2PcYJk5qjEoAAVZkZR73QpXzDuvsf9/UP -+Ky5tfQ3mBMY3oVbtwyCO4dvlTlYMNpuAWgXIszACwIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/ -MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQHw1EwpKrpRa41JPr/JCwz0LGdjDAdBgNVHQ4E -FgQUB8NRMKSq6UWuNST6/yQsM9CxnYwwDQYJKoZIhvcNAQEFBQADggEBAF8+hnZuuDU8TjYcHnmY -v/3VEhF5Ug7uMYm83X/50cYVIeiKAVQNOvtUudZj1LGqlk2iQk3UUx+LEN5/Zb5gEydxiKRz44Rj -0aRV4VCT5hsOedBnvEbIvz8XDZXmxpBp3ue0L96VfdASPz0+f00/FGj1EVDVwfSQpQgdMWD/YIwj -VAqv/qFuxdF6Kmh4zx6CCiC0H63lhbJqaHVOrSU3lIW+vaHU6rcMSzyd6BIA8F+sDeGscGNz9395 -nzIlQnQFgCi/vcEkllgVsRch6YlL2weIZ/QVrXA+L02FO8K32/6YaCOJ4XQP3vTFhGMpG8zLB8kA -pKnXwiJPZ9d37CAFYd4= ------END CERTIFICATE----- - GeoTrust Global CA ================== -----BEGIN CERTIFICATE----- @@ -1285,30 +1264,6 @@ FAkK+qDmfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdvGDeA U/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= -----END CERTIFICATE----- -IGC/A -===== ------BEGIN CERTIFICATE----- -MIIEAjCCAuqgAwIBAgIFORFFEJQwDQYJKoZIhvcNAQEFBQAwgYUxCzAJBgNVBAYTAkZSMQ8wDQYD -VQQIEwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVE -Q1NTSTEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZy -MB4XDTAyMTIxMzE0MjkyM1oXDTIwMTAxNzE0MjkyMlowgYUxCzAJBgNVBAYTAkZSMQ8wDQYDVQQI -EwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVEQ1NT -STEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZyMIIB -IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsh/R0GLFMzvABIaIs9z4iPf930Pfeo2aSVz2 -TqrMHLmh6yeJ8kbpO0px1R2OLc/mratjUMdUC24SyZA2xtgv2pGqaMVy/hcKshd+ebUyiHDKcMCW -So7kVc0dJ5S/znIq7Fz5cyD+vfcuiWe4u0dzEvfRNWk68gq5rv9GQkaiv6GFGvm/5P9JhfejcIYy -HF2fYPepraX/z9E0+X1bF8bc1g4oa8Ld8fUzaJ1O/Id8NhLWo4DoQw1VYZTqZDdH6nfK0LJYBcNd -frGoRpAxVs5wKpayMLh35nnAvSk7/ZR3TL0gzUEl4C7HG7vupARB0l2tEmqKm0f7yd1GQOGdPDPQ -tQIDAQABo3cwdTAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBRjAVBgNVHSAEDjAMMAoGCCqB -egF5AQEBMB0GA1UdDgQWBBSjBS8YYFDCiQrdKyFP/45OqDAxNjAfBgNVHSMEGDAWgBSjBS8YYFDC -iQrdKyFP/45OqDAxNjANBgkqhkiG9w0BAQUFAAOCAQEABdwm2Pp3FURo/C9mOnTgXeQp/wYHE4RK -q89toB9RlPhJy3Q2FLwV3duJL92PoF189RLrn544pEfMs5bZvpwlqwN+Mw+VgQ39FuCIvjfwbF3Q -MZsyK10XZZOYYLxuj7GoPB7ZHPOpJkL5ZB3C55L29B5aqhlSXa/oovdgoPaN8In1buAKBQGVyYsg -Crpa/JosPL3Dt8ldeCUFP1YUmwza+zpI/pdpXsoQhvdOlgQITeywvl3cO45Pwf2aNjSaTFR+FwNI -lQgRHAdvhQh+XU3Endv7rs6y0bO4g2wdsrN58dhwmX7wEwLOXt1R0982gaEbeC9xs/FZTEYYKKuF -0mBWWg== ------END CERTIFICATE----- - Security Communication EV RootCA1 ================================= -----BEGIN CERTIFICATE----- @@ -1518,58 +1473,6 @@ LBOhgLJeDEoTniDYYkCrkOpkSi+sDQESeUWoL4cZaMjihccwsnX5OD+ywJO0a+IDRM5noN+J1q2M dqMTw5RhK2vZbMEHCiIHhWyFJEapvj+LeISCfiQMnf2BN+MlqO02TpUsyZyQ2uypQjyttgI= -----END CERTIFICATE----- -Buypass Class 2 CA 1 -==================== ------BEGIN CERTIFICATE----- -MIIDUzCCAjugAwIBAgIBATANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU -QnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3MgQ2xhc3MgMiBDQSAxMB4XDTA2 -MTAxMzEwMjUwOVoXDTE2MTAxMzEwMjUwOVowSzELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBh -c3MgQVMtOTgzMTYzMzI3MR0wGwYDVQQDDBRCdXlwYXNzIENsYXNzIDIgQ0EgMTCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAIs8B0XY9t/mx8q6jUPFR42wWsE425KEHK8T1A9vNkYgxC7M -cXA0ojTTNy7Y3Tp3L8DrKehc0rWpkTSHIln+zNvnma+WwajHQN2lFYxuyHyXA8vmIPLXl18xoS83 -0r7uvqmtqEyeIWZDO6i88wmjONVZJMHCR3axiFyCO7srpgTXjAePzdVBHfCuuCkslFJgNJQ72uA4 -0Z0zPhX0kzLFANq1KWYOOngPIVJfAuWSeyXTkh4vFZ2B5J2O6O+JzhRMVB0cgRJNcKi+EAUXfh/R -uFdV7c27UsKwHnjCTTZoy1YmwVLBvXb3WNVyfh9EdrsAiR0WnVE1703CVu9r4Iw7DekCAwEAAaNC -MEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUP42aWYv8e3uco684sDntkHGA1sgwDgYDVR0P -AQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQAVGn4TirnoB6NLJzKyQJHyIdFkhb5jatLPgcIV -1Xp+DCmsNx4cfHZSldq1fyOhKXdlyTKdqC5Wq2B2zha0jX94wNWZUYN/Xtm+DKhQ7SLHrQVMdvvt -7h5HZPb3J31cKA9FxVxiXqaakZG3Uxcu3K1gnZZkOb1naLKuBctN518fV4bVIJwo+28TOPX2EZL2 -fZleHwzoq0QkKXJAPTZSr4xYkHPB7GEseaHsh7U/2k3ZIQAw3pDaDtMaSKk+hQsUi4y8QZ5q9w5w -wDX3OaJdZtB7WZ+oRxKaJyOkLY4ng5IgodcVf/EuGO70SH8vf/GhGLWhC5SgYiAynB321O+/TIho ------END CERTIFICATE----- - -EBG Elektronik Sertifika Hizmet Sa\xC4\x9Flay\xc4\xb1\x63\xc4\xb1s\xc4\xb1 -========================================================================== ------BEGIN CERTIFICATE----- -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 ------END CERTIFICATE----- - certSIGN ROOT CA ================ -----BEGIN CERTIFICATE----- @@ -1819,34 +1722,6 @@ IPVVYpbtbZNQvOSqeK3Zywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm 66+KAQ== -----END CERTIFICATE----- -Juur-SK -======= ------BEGIN CERTIFICATE----- -MIIE5jCCA86gAwIBAgIEO45L/DANBgkqhkiG9w0BAQUFADBdMRgwFgYJKoZIhvcNAQkBFglwa2lA -c2suZWUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKExlBUyBTZXJ0aWZpdHNlZXJpbWlza2Vza3VzMRAw -DgYDVQQDEwdKdXVyLVNLMB4XDTAxMDgzMDE0MjMwMVoXDTE2MDgyNjE0MjMwMVowXTEYMBYGCSqG -SIb3DQEJARYJcGtpQHNrLmVlMQswCQYDVQQGEwJFRTEiMCAGA1UEChMZQVMgU2VydGlmaXRzZWVy -aW1pc2tlc2t1czEQMA4GA1UEAxMHSnV1ci1TSzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAIFxNj4zB9bjMI0TfncyRsvPGbJgMUaXhvSYRqTCZUXP00B841oiqBB4M8yIsdOBSvZiF3tf -TQou0M+LI+5PAk676w7KvRhj6IAcjeEcjT3g/1tf6mTll+g/mX8MCgkzABpTpyHhOEvWgxutr2TC -+Rx6jGZITWYfGAriPrsfB2WThbkasLnE+w0R9vXW+RvHLCu3GFH+4Hv2qEivbDtPL+/40UceJlfw -UR0zlv/vWT3aTdEVNMfqPxZIe5EcgEMPPbgFPtGzlc3Yyg/CQ2fbt5PgIoIuvvVoKIO5wTtpeyDa -Tpxt4brNj3pssAki14sL2xzVWiZbDcDq5WDQn/413z8CAwEAAaOCAawwggGoMA8GA1UdEwEB/wQF -MAMBAf8wggEWBgNVHSAEggENMIIBCTCCAQUGCisGAQQBzh8BAQEwgfYwgdAGCCsGAQUFBwICMIHD -HoHAAFMAZQBlACAAcwBlAHIAdABpAGYAaQBrAGEAYQB0ACAAbwBuACAAdgDkAGwAagBhAHMAdABh -AHQAdQBkACAAQQBTAC0AaQBzACAAUwBlAHIAdABpAGYAaQB0AHMAZQBlAHIAaQBtAGkAcwBrAGUA -cwBrAHUAcwAgAGEAbABhAG0ALQBTAEsAIABzAGUAcgB0AGkAZgBpAGsAYQBhAHQAaQBkAGUAIABr -AGkAbgBuAGkAdABhAG0AaQBzAGUAawBzMCEGCCsGAQUFBwIBFhVodHRwOi8vd3d3LnNrLmVlL2Nw -cy8wKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL3d3dy5zay5lZS9qdXVyL2NybC8wHQYDVR0OBBYE -FASqekej5ImvGs8KQKcYP2/v6X2+MB8GA1UdIwQYMBaAFASqekej5ImvGs8KQKcYP2/v6X2+MA4G -A1UdDwEB/wQEAwIB5jANBgkqhkiG9w0BAQUFAAOCAQEAe8EYlFOiCfP+JmeaUOTDBS8rNXiRTHyo -ERF5TElZrMj3hWVcRrs7EKACr81Ptcw2Kuxd/u+gkcm2k298gFTsxwhwDY77guwqYHhpNjbRxZyL -abVAyJRld/JXIWY7zoVAtjNjGr95HvxcHdMdkxuLDF2FvZkwMhgJkVLpfKG6/2SSmuz+Ne6ML678 -IIbsSt4beDI3poHSna9aEhbKmVv8b20OxaAehsmR0FyYgl9jDIpaq9iVpszLita/ZEuOyoqysOkh -Mp6qqIWYNIE5ITuoOlIyPfZrN4YGWhWY3PARZv40ILcD9EEQfTmEeZZyY7aWAuVrua0ZTbvGRNs2 -yyqcjg== ------END CERTIFICATE----- - Hongkong Post Root CA 1 ======================= -----BEGIN CERTIFICATE----- @@ -2310,41 +2185,6 @@ wk01+dIL8hf2rGbVJLJP0RyZwG71fet0BLj5TXcJ17TPBzAJ8bgAVtkXFhYKK4bfjwEZGuW7gmP/ vgt2Fl43N+bYdJeimUV5 -----END CERTIFICATE----- -Root CA Generalitat Valenciana -============================== ------BEGIN CERTIFICATE----- -MIIGizCCBXOgAwIBAgIEO0XlaDANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJFUzEfMB0GA1UE -ChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJR1ZBMScwJQYDVQQDEx5Sb290 -IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwHhcNMDEwNzA2MTYyMjQ3WhcNMjEwNzAxMTUyMjQ3 -WjBoMQswCQYDVQQGEwJFUzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UE -CxMGUEtJR1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGKqtXETcvIorKA3Qdyu0togu8M1JAJke+WmmmO3I2 -F0zo37i7L3bhQEZ0ZQKQUgi0/6iMweDHiVYQOTPvaLRfX9ptI6GJXiKjSgbwJ/BXufjpTjJ3Cj9B -ZPPrZe52/lSqfR0grvPXdMIKX/UIKFIIzFVd0g/bmoGlu6GzwZTNVOAydTGRGmKy3nXiz0+J2ZGQ -D0EbtFpKd71ng+CT516nDOeB0/RSrFOyA8dEJvt55cs0YFAQexvba9dHq198aMpunUEDEO5rmXte -JajCq+TA81yc477OMUxkHl6AovWDfgzWyoxVjr7gvkkHD6MkQXpYHYTqWBLI4bft75PelAgxAgMB -AAGjggM7MIIDNzAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9vY3NwLnBraS5n -dmEuZXMwEgYDVR0TAQH/BAgwBgEB/wIBAjCCAjQGA1UdIASCAiswggInMIICIwYKKwYBBAG/VQIB -ADCCAhMwggHoBggrBgEFBQcCAjCCAdoeggHWAEEAdQB0AG8AcgBpAGQAYQBkACAAZABlACAAQwBl -AHIAdABpAGYAaQBjAGEAYwBpAPMAbgAgAFIAYQDtAHoAIABkAGUAIABsAGEAIABHAGUAbgBlAHIA -YQBsAGkAdABhAHQAIABWAGEAbABlAG4AYwBpAGEAbgBhAC4ADQAKAEwAYQAgAEQAZQBjAGwAYQBy -AGEAYwBpAPMAbgAgAGQAZQAgAFAAcgDhAGMAdABpAGMAYQBzACAAZABlACAAQwBlAHIAdABpAGYA -aQBjAGEAYwBpAPMAbgAgAHEAdQBlACAAcgBpAGcAZQAgAGUAbAAgAGYAdQBuAGMAaQBvAG4AYQBt -AGkAZQBuAHQAbwAgAGQAZQAgAGwAYQAgAHAAcgBlAHMAZQBuAHQAZQAgAEEAdQB0AG8AcgBpAGQA -YQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEAYwBpAPMAbgAgAHMAZQAgAGUAbgBjAHUAZQBu -AHQAcgBhACAAZQBuACAAbABhACAAZABpAHIAZQBjAGMAaQDzAG4AIAB3AGUAYgAgAGgAdAB0AHAA -OgAvAC8AdwB3AHcALgBwAGsAaQAuAGcAdgBhAC4AZQBzAC8AYwBwAHMwJQYIKwYBBQUHAgEWGWh0 -dHA6Ly93d3cucGtpLmd2YS5lcy9jcHMwHQYDVR0OBBYEFHs100DSHHgZZu90ECjcPk+yeAT8MIGV -BgNVHSMEgY0wgYqAFHs100DSHHgZZu90ECjcPk+yeAT8oWykajBoMQswCQYDVQQGEwJFUzEfMB0G -A1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJR1ZBMScwJQYDVQQDEx5S -b290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmGCBDtF5WgwDQYJKoZIhvcNAQEFBQADggEBACRh -TvW1yEICKrNcda3FbcrnlD+laJWIwVTAEGmiEi8YPyVQqHxK6sYJ2fR1xkDar1CdPaUWu20xxsdz -Ckj+IHLtb8zog2EWRpABlUt9jppSCS/2bxzkoXHPjCpaF3ODR00PNvsETUlR4hTJZGH71BTg9J63 -NI8KJr2XXPR5OkowGcytT6CYirQxlyric21+eLj4iIlPsSKRZEv1UN4D2+XFducTZnV+ZfsBn5OH -iJ35Rld8TWCvmHMTI6QgkYH60GFmuH3Rr9ZvHmw96RH9qfmCIoaZM3Fa6hlXPZHNqcCjbgcTpsnt -+GijnsNacgmHKNHEc8RzGF9QdRYxn7fofMM= ------END CERTIFICATE----- - TWCA Root Certification Authority ================================= -----BEGIN CERTIFICATE----- @@ -4064,3 +3904,140 @@ YVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9n JEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJ m+kXQ99b21/+jh5Xos1AnX5iItreGCc= -----END CERTIFICATE----- + +AC RAIZ FNMT-RCM +================ +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsxCzAJBgNVBAYT +AkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTAeFw0wODEw +MjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJD +TTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBALpxgHpMhm5/yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcf +qQgfBBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAzWHFctPVr +btQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxFtBDXaEAUwED653cXeuYL +j2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z374jNUUeAlz+taibmSXaXvMiwzn15Cou +08YfxGyqxRxqAQVKL9LFwag0Jl1mpdICIfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mw +WsXmo8RZZUc1g16p6DULmbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnT +tOmlcYF7wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peSMKGJ +47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2ZSysV4999AeU14EC +ll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMetUqIJ5G+GR4of6ygnXYMgrwTJbFaa +i0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FPd9xf3E6Jobd2Sn9R2gzL+HYJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1o +dHRwOi8vd3d3LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1RXxlDPiyN8+s +D8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYMLVN0V2Ue1bLdI4E7pWYjJ2cJ +j+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrT +Qfv6MooqtyuGC2mDOL7Nii4LcK2NJpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW ++YJF1DngoABd15jmfZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7 +Ixjp6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp1txyM/1d +8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B9kiABdcPUXmsEKvU7ANm +5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wokRqEIr9baRRmW1FMdW4R58MD3R++Lj8UG +rp1MYp3/RgT408m2ECVAdf4WqslKYIYvuu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- + +Amazon Root CA 1 +================ +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsFADA5MQswCQYD +VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAxMB4XDTE1 +MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv +bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBALJ4gHHKeNXjca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgH +FzZM9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qwIFAGbHrQ +gLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6VOujw5H5SNz/0egwLX0t +dHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L93FcXmn/6pUCyziKrlA4b9v7LWIbxcce +VOF34GfID5yHI9Y/QCB/IIDEgEw+OyQmjgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3 +DQEBCwUAA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDIU5PM +CCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUsN+gDS63pYaACbvXy +8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vvo/ufQJVtMVT8QtPHRh8jrdkPSHCa +2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2 +xJNDd2ZhwLnoQdeXeGADbkpyrqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- + +Amazon Root CA 2 +================ +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwFADA5MQswCQYD +VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAyMB4XDTE1 +MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv +bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBAK2Wny2cSkxKgXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4 +kHbZW0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg1dKmSYXp +N+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K8nu+NQWpEjTj82R0Yiw9 +AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvd +fLC6HM783k81ds8P+HgfajZRRidhW+mez/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAEx +kv8LV/SasrlX6avvDXbR8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSS +btqDT6ZjmUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz7Mt0 +Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6+XUyo05f7O0oYtlN +c/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI0u1ufm8/0i2BWSlmy5A5lREedCf+ +3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSw +DPBMMPQFWAJI/TPlUq9LhONmUjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oA +A7CXDpO8Wqj2LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kSk5Nrp+gvU5LE +YFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl7uxMMne0nxrpS10gxdr9HIcW +xkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygmbtmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQ +gj9sAq+uEjonljYE1x2igGOpm/HlurR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbW +aQbLU8uz/mtBzUF+fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoV +Yh63n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE76KlXIx3 +KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H9jVlpNMKVv/1F2Rs76gi +JUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT4PsJYGw= +-----END CERTIFICATE----- + +Amazon Root CA 3 +================ +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5MQswCQYDVQQG +EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAzMB4XDTE1MDUy +NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ +MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZB +f8ANm+gBG1bG8lKlui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjr +Zt6jQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSrttvXBp43 +rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkrBqWTrBqYaGFy+uGh0Psc +eGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteMYyRIHN8wfdVoOw== +-----END CERTIFICATE----- + +Amazon Root CA 4 +================ +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5MQswCQYDVQQG +EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSA0MB4XDTE1MDUy +NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ +MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN +/sGKe0uoe0ZLY7Bi9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri +83BkM6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WBMAoGCCqGSM49BAMDA2gA +MGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlwCkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1 +AE47xDqUEpHJWEadIRNyp4iciuRMStuW1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- + +LuxTrust Global Root 2 +====================== +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/settings/templates/apps.php b/settings/templates/apps.php index 99d648c6284..0adf5dfcc6f 100644 --- a/settings/templates/apps.php +++ b/settings/templates/apps.php @@ -3,7 +3,6 @@ style('settings', 'settings'); vendor_script( 'core', [ - 'handlebars/handlebars', 'marked/marked.min', ] ); diff --git a/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php new file mode 100644 index 00000000000..1317a07447d --- /dev/null +++ b/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php @@ -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); + } +} diff --git a/tests/lib/AppFramework/Utility/ControllerMethodReflectorTest.php b/tests/lib/AppFramework/Utility/ControllerMethodReflectorTest.php index 644245e1967..3c43e7f9219 100644 --- a/tests/lib/AppFramework/Utility/ControllerMethodReflectorTest.php +++ b/tests/lib/AppFramework/Utility/ControllerMethodReflectorTest.php @@ -75,18 +75,32 @@ class ControllerMethodReflectorTest extends \Test\TestCase { $this->assertTrue($reader->hasAnnotation('Annotation')); } + /** + * @Annotation(parameter=value) + */ + public function testGetAnnotationParameterSingle() { + $reader = new ControllerMethodReflector(); + $reader->reflect( + __CLASS__, + __FUNCTION__ + ); + + $this->assertSame('value', $reader->getAnnotationParameter('Annotation', 'parameter')); + } /** - * @Annotation parameter + * @Annotation(parameter1=value1, parameter2=value2,parameter3=value3) */ - public function testGetAnnotationParameter(){ + public function testGetAnnotationParameterMultiple() { $reader = new ControllerMethodReflector(); $reader->reflect( - '\Test\AppFramework\Utility\ControllerMethodReflectorTest', - 'testGetAnnotationParameter' + __CLASS__, + __FUNCTION__ ); - $this->assertSame('parameter', $reader->getAnnotationParameter('Annotation')); + $this->assertSame('value1', $reader->getAnnotationParameter('Annotation', 'parameter1')); + $this->assertSame('value2', $reader->getAnnotationParameter('Annotation', 'parameter2')); + $this->assertSame('value3', $reader->getAnnotationParameter('Annotation', 'parameter3')); } /** diff --git a/tests/lib/Security/Bruteforce/ThrottlerTest.php b/tests/lib/Security/Bruteforce/ThrottlerTest.php index 02d5b701679..9679d0c1759 100644 --- a/tests/lib/Security/Bruteforce/ThrottlerTest.php +++ b/tests/lib/Security/Bruteforce/ThrottlerTest.php @@ -76,51 +76,6 @@ class ThrottlerTest extends TestCase { $this->assertLessThan(2, $cutoff->s); } - public function testSubnet() { - // IPv4 - $this->assertSame( - '64.233.191.254/32', - $this->invokePrivate($this->throttler, 'getIPv4Subnet', ['64.233.191.254', 32]) - ); - $this->assertSame( - '64.233.191.252/30', - $this->invokePrivate($this->throttler, 'getIPv4Subnet', ['64.233.191.254', 30]) - ); - $this->assertSame( - '64.233.191.240/28', - $this->invokePrivate($this->throttler, 'getIPv4Subnet', ['64.233.191.254', 28]) - ); - $this->assertSame( - '64.233.191.0/24', - $this->invokePrivate($this->throttler, 'getIPv4Subnet', ['64.233.191.254', 24]) - ); - $this->assertSame( - '64.233.188.0/22', - $this->invokePrivate($this->throttler, 'getIPv4Subnet', ['64.233.191.254', 22]) - ); - // IPv6 - $this->assertSame( - '2001:db8:85a3::8a2e:370:7334/127', - $this->invokePrivate($this->throttler, 'getIPv6Subnet', ['2001:0db8:85a3:0000:0000:8a2e:0370:7334', 127]) - ); - $this->assertSame( - '2001:db8:85a3::8a2e:370:7300/120', - $this->invokePrivate($this->throttler, 'getIPv6Subnet', ['2001:0db8:85a3:0000:0000:8a2e:0370:7300', 120]) - ); - $this->assertSame( - '2001:db8:85a3::/64', - $this->invokePrivate($this->throttler, 'getIPv6Subnet', ['2001:0db8:85a3:0000:0000:8a2e:0370:7334', 64]) - ); - $this->assertSame( - '2001:db8:85a3::/48', - $this->invokePrivate($this->throttler, 'getIPv6Subnet', ['2001:0db8:85a3:0000:0000:8a2e:0370:7334', 48]) - ); - $this->assertSame( - '2001:db8:8500::/40', - $this->invokePrivate($this->throttler, 'getIPv6Subnet', ['2001:0db8:85a3:0000:0000:8a2e:0370:7334', 40]) - ); - } - public function dataIsIPWhitelisted() { return [ [ diff --git a/tests/lib/Security/Normalizer/IpAddressTest.php b/tests/lib/Security/Normalizer/IpAddressTest.php new file mode 100644 index 00000000000..36a48f601d3 --- /dev/null +++ b/tests/lib/Security/Normalizer/IpAddressTest.php @@ -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'))); + } +} diff --git a/tests/lib/Security/RateLimiting/Backend/MemoryCacheTest.php b/tests/lib/Security/RateLimiting/Backend/MemoryCacheTest.php new file mode 100644 index 00000000000..34c326e72e1 --- /dev/null +++ b/tests/lib/Security/RateLimiting/Backend/MemoryCacheTest.php @@ -0,0 +1,146 @@ +<?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->timeFactory + ->expects($this->once()) + ->method('getTime') + ->willReturn(123); + + $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', 100); + } + + public function testRegisterAttempt() { + $this->timeFactory + ->expects($this->once()) + ->method('getTime') + ->willReturn(129); + + $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([ + '87', + '123', + '123', + '124', + '129', + ]) + ); + + $this->memoryCache->registerAttempt('Method', 'User', 100); + } +} diff --git a/tests/lib/Security/RateLimiting/LimiterTest.php b/tests/lib/Security/RateLimiting/LimiterTest.php new file mode 100644 index 00000000000..80b63ebb391 --- /dev/null +++ b/tests/lib/Security/RateLimiting/LimiterTest.php @@ -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); + } +} |