summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.drone.yml13
-rw-r--r--apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php2
-rw-r--r--apps/files_sharing/appinfo/routes.php2
-rw-r--r--apps/files_sharing/lib/Controller/ShareController.php2
-rw-r--r--apps/testing/appinfo/routes.php20
-rw-r--r--apps/testing/lib/Controller/RateLimitTestController.php52
-rw-r--r--apps/user_ldap/lib/Access.php22
-rw-r--r--apps/user_ldap/lib/Group_LDAP.php6
-rw-r--r--apps/user_ldap/lib/User_LDAP.php2
-rw-r--r--apps/user_ldap/tests/Group_LDAPTest.php8
-rw-r--r--apps/user_ldap/tests/User_LDAPTest.php2
-rw-r--r--build/integration/features/ratelimiting.feature58
-rw-r--r--core/Controller/LostController.php2
-rw-r--r--lib/composer/composer/autoload_classmap.php6
-rw-r--r--lib/composer/composer/autoload_static.php6
-rw-r--r--lib/private/AppFramework/DependencyInjection/DIContainer.php15
-rw-r--r--lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php134
-rw-r--r--lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php2
-rw-r--r--lib/private/AppFramework/Utility/ControllerMethodReflector.php55
-rw-r--r--lib/private/Security/Bruteforce/Throttler.php72
-rw-r--r--lib/private/Security/Normalizer/IpAddress.php106
-rw-r--r--lib/private/Security/RateLimiting/Backend/IBackend.php54
-rw-r--r--lib/private/Security/RateLimiting/Backend/MemoryCache.php116
-rw-r--r--lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php31
-rw-r--r--lib/private/Security/RateLimiting/Limiter.php106
-rw-r--r--lib/private/Server.php15
-rw-r--r--resources/config/ca-bundle.crt301
-rw-r--r--settings/templates/apps.php1
-rw-r--r--tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php283
-rw-r--r--tests/lib/AppFramework/Utility/ControllerMethodReflectorTest.php24
-rw-r--r--tests/lib/Security/Bruteforce/ThrottlerTest.php45
-rw-r--r--tests/lib/Security/Normalizer/IpAddressTest.php59
-rw-r--r--tests/lib/Security/RateLimiting/Backend/MemoryCacheTest.php146
-rw-r--r--tests/lib/Security/RateLimiting/LimiterTest.php161
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);
+ }
+}