Skip to content

Commit fd71f49

Browse files
authored
Merge pull request #28819 from nextcloud/backport/28728/stable19
[stable19] Add database ratelimiting backend
2 parents 358ac8f + f53ae86 commit fd71f49

File tree

10 files changed

+223
-70
lines changed

10 files changed

+223
-70
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OC\Core\Migrations;
6+
7+
use Closure;
8+
use OCP\DB\ISchemaWrapper;
9+
use OCP\Migration\IOutput;
10+
use OCP\Migration\SimpleMigrationStep;
11+
12+
class Version23000Date20210906132259 extends SimpleMigrationStep {
13+
private const TABLE_NAME = 'ratelimit_entries';
14+
15+
/**
16+
* @param IOutput $output
17+
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
18+
* @param array $options
19+
* @return null|ISchemaWrapper
20+
*/
21+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
22+
/** @var ISchemaWrapper $schema */
23+
$schema = $schemaClosure();
24+
25+
$hasTable = $schema->hasTable(self::TABLE_NAME);
26+
27+
if (!$hasTable) {
28+
$table = $schema->createTable(self::TABLE_NAME);
29+
$table->addColumn('hash', 'string', [
30+
'notnull' => true,
31+
'length' => 128,
32+
]);
33+
$table->addColumn('delete_after', 'datetime', [
34+
'notnull' => true,
35+
]);
36+
$table->addIndex(['hash'], 'ratelimit_hash');
37+
$table->addIndex(['delete_after'], 'ratelimit_delete_after');
38+
return $schema;
39+
}
40+
41+
return null;
42+
}
43+
}

lib/composer/composer/autoload_classmap.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,7 @@
866866
'OC\\Core\\Migrations\\Version18000Date20191014105105' => $baseDir . '/core/Migrations/Version18000Date20191014105105.php',
867867
'OC\\Core\\Migrations\\Version18000Date20191204114856' => $baseDir . '/core/Migrations/Version18000Date20191204114856.php',
868868
'OC\\Core\\Migrations\\Version19000Date20200211083441' => $baseDir . '/core/Migrations/Version19000Date20200211083441.php',
869+
'OC\\Core\\Migrations\\Version23000Date20210906132259' => $baseDir . '/core/Migrations/Version23000Date20210906132259.php',
869870
'OC\\Core\\Notification\\RemoveLinkSharesNotifier' => $baseDir . '/core/Notification/RemoveLinkSharesNotifier.php',
870871
'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php',
871872
'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php',
@@ -1229,8 +1230,9 @@
12291230
'OC\\Security\\IdentityProof\\Manager' => $baseDir . '/lib/private/Security/IdentityProof/Manager.php',
12301231
'OC\\Security\\IdentityProof\\Signer' => $baseDir . '/lib/private/Security/IdentityProof/Signer.php',
12311232
'OC\\Security\\Normalizer\\IpAddress' => $baseDir . '/lib/private/Security/Normalizer/IpAddress.php',
1233+
'OC\\Security\\RateLimiting\\Backend\\DatabaseBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php',
12321234
'OC\\Security\\RateLimiting\\Backend\\IBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/IBackend.php',
1233-
'OC\\Security\\RateLimiting\\Backend\\MemoryCache' => $baseDir . '/lib/private/Security/RateLimiting/Backend/MemoryCache.php',
1235+
'OC\\Security\\RateLimiting\\Backend\\MemoryCacheBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php',
12341236
'OC\\Security\\RateLimiting\\Exception\\RateLimitExceededException' => $baseDir . '/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php',
12351237
'OC\\Security\\RateLimiting\\Limiter' => $baseDir . '/lib/private/Security/RateLimiting/Limiter.php',
12361238
'OC\\Security\\SecureRandom' => $baseDir . '/lib/private/Security/SecureRandom.php',

lib/composer/composer/autoload_static.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
895895
'OC\\Core\\Migrations\\Version18000Date20191014105105' => __DIR__ . '/../../..' . '/core/Migrations/Version18000Date20191014105105.php',
896896
'OC\\Core\\Migrations\\Version18000Date20191204114856' => __DIR__ . '/../../..' . '/core/Migrations/Version18000Date20191204114856.php',
897897
'OC\\Core\\Migrations\\Version19000Date20200211083441' => __DIR__ . '/../../..' . '/core/Migrations/Version19000Date20200211083441.php',
898+
'OC\\Core\\Migrations\\Version23000Date20210906132259' => __DIR__ . '/../../..' . '/core/Migrations/Version23000Date20210906132259.php',
898899
'OC\\Core\\Notification\\RemoveLinkSharesNotifier' => __DIR__ . '/../../..' . '/core/Notification/RemoveLinkSharesNotifier.php',
899900
'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php',
900901
'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php',
@@ -1258,8 +1259,9 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
12581259
'OC\\Security\\IdentityProof\\Manager' => __DIR__ . '/../../..' . '/lib/private/Security/IdentityProof/Manager.php',
12591260
'OC\\Security\\IdentityProof\\Signer' => __DIR__ . '/../../..' . '/lib/private/Security/IdentityProof/Signer.php',
12601261
'OC\\Security\\Normalizer\\IpAddress' => __DIR__ . '/../../..' . '/lib/private/Security/Normalizer/IpAddress.php',
1262+
'OC\\Security\\RateLimiting\\Backend\\DatabaseBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php',
12611263
'OC\\Security\\RateLimiting\\Backend\\IBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/IBackend.php',
1262-
'OC\\Security\\RateLimiting\\Backend\\MemoryCache' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/MemoryCache.php',
1264+
'OC\\Security\\RateLimiting\\Backend\\MemoryCacheBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php',
12631265
'OC\\Security\\RateLimiting\\Exception\\RateLimitExceededException' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php',
12641266
'OC\\Security\\RateLimiting\\Limiter' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Limiter.php',
12651267
'OC\\Security\\SecureRandom' => __DIR__ . '/../../..' . '/lib/private/Security/SecureRandom.php',
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright Copyright (c) 2021 Lukas Reschke <lukas@statuscode.ch>
7+
*
8+
* @author Lukas Reschke <lukas@statuscode.ch>
9+
*
10+
* @license GNU AGPL version 3 or any later version
11+
*
12+
* This program is free software: you can redistribute it and/or modify
13+
* it under the terms of the GNU Affero General Public License as
14+
* published by the Free Software Foundation, either version 3 of the
15+
* License, or (at your option) any later version.
16+
*
17+
* This program is distributed in the hope that it will be useful,
18+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
* GNU Affero General Public License for more details.
21+
*
22+
* You should have received a copy of the GNU Affero General Public License
23+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
24+
*
25+
*/
26+
namespace OC\Security\RateLimiting\Backend;
27+
28+
use OCP\AppFramework\Utility\ITimeFactory;
29+
use OCP\DB\QueryBuilder\IQueryBuilder;
30+
use OCP\IDBConnection;
31+
32+
class DatabaseBackend implements IBackend {
33+
private const TABLE_NAME = 'ratelimit_entries';
34+
35+
/** @var IDBConnection */
36+
private $dbConnection;
37+
/** @var ITimeFactory */
38+
private $timeFactory;
39+
40+
/**
41+
* @param IDBConnection $dbConnection
42+
* @param ITimeFactory $timeFactory
43+
*/
44+
public function __construct(
45+
IDBConnection $dbConnection,
46+
ITimeFactory $timeFactory
47+
) {
48+
$this->dbConnection = $dbConnection;
49+
$this->timeFactory = $timeFactory;
50+
}
51+
52+
/**
53+
* @param string $methodIdentifier
54+
* @param string $userIdentifier
55+
* @return string
56+
*/
57+
private function hash(string $methodIdentifier,
58+
string $userIdentifier): string {
59+
return hash('sha512', $methodIdentifier . $userIdentifier);
60+
}
61+
62+
/**
63+
* @param string $identifier
64+
* @return int
65+
*/
66+
private function getExistingAttemptCount(
67+
string $identifier
68+
): int {
69+
$currentTime = $this->timeFactory->getDateTime();
70+
71+
$qb = $this->dbConnection->getQueryBuilder();
72+
$qb->delete(self::TABLE_NAME)
73+
->where(
74+
$qb->expr()->lte('delete_after', $qb->createNamedParameter($currentTime, IQueryBuilder::PARAM_DATE))
75+
)
76+
->execute();
77+
78+
$qb = $this->dbConnection->getQueryBuilder();
79+
$qb->select($qb->func()->count('*', 'attempts'))
80+
->from(self::TABLE_NAME)
81+
->where(
82+
$qb->expr()->eq('hash', $qb->createNamedParameter($identifier, IQueryBuilder::PARAM_STR))
83+
)
84+
->andWhere(
85+
$qb->expr()->gte('delete_after', $qb->createNamedParameter($currentTime, IQueryBuilder::PARAM_DATE))
86+
);
87+
88+
$cursor = $qb->execute();
89+
$row = $cursor->fetch();
90+
$cursor->closeCursor();
91+
92+
return (int)$row['attempts'];
93+
}
94+
95+
/**
96+
* {@inheritDoc}
97+
*/
98+
public function getAttempts(string $methodIdentifier,
99+
string $userIdentifier): int {
100+
$identifier = $this->hash($methodIdentifier, $userIdentifier);
101+
return $this->getExistingAttemptCount($identifier);
102+
}
103+
104+
/**
105+
* {@inheritDoc}
106+
*/
107+
public function registerAttempt(string $methodIdentifier,
108+
string $userIdentifier,
109+
int $period) {
110+
$identifier = $this->hash($methodIdentifier, $userIdentifier);
111+
$deleteAfter = $this->timeFactory->getDateTime()->add(new \DateInterval("PT{$period}S"));
112+
113+
$qb = $this->dbConnection->getQueryBuilder();
114+
115+
$qb->insert(self::TABLE_NAME)
116+
->values([
117+
'hash' => $qb->createNamedParameter($identifier, IQueryBuilder::PARAM_STR),
118+
'delete_after' => $qb->createNamedParameter($deleteAfter, IQueryBuilder::PARAM_DATE),
119+
])
120+
->execute();
121+
}
122+
}

lib/private/Security/RateLimiting/Backend/IBackend.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,14 @@
3636
*/
3737
interface IBackend {
3838
/**
39-
* Gets the amount of attempts within the last specified seconds
39+
* Gets the number of attempts for the specified method
4040
*
4141
* @param string $methodIdentifier Identifier for the method
4242
* @param string $userIdentifier Identifier for the user
43-
* @param int $seconds Seconds to look back at
4443
* @return int
4544
*/
4645
public function getAttempts(string $methodIdentifier,
47-
string $userIdentifier,
48-
int $seconds): int;
46+
string $userIdentifier): int;
4947

5048
/**
5149
* Registers an attempt

lib/private/Security/RateLimiting/Backend/MemoryCache.php renamed to lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@
3434
use OCP\ICacheFactory;
3535

3636
/**
37-
* Class MemoryCache uses the configured distributed memory cache for storing
37+
* Class MemoryCacheBackend uses the configured distributed memory cache for storing
3838
* rate limiting data.
3939
*
4040
* @package OC\Security\RateLimiting\Backend
4141
*/
42-
class MemoryCache implements IBackend {
42+
class MemoryCacheBackend implements IBackend {
4343
/** @var ICache */
4444
private $cache;
4545
/** @var ITimeFactory */
@@ -87,16 +87,14 @@ private function getExistingAttempts(string $identifier): array {
8787
* {@inheritDoc}
8888
*/
8989
public function getAttempts(string $methodIdentifier,
90-
string $userIdentifier,
91-
int $seconds): int {
90+
string $userIdentifier): int {
9291
$identifier = $this->hash($methodIdentifier, $userIdentifier);
9392
$existingAttempts = $this->getExistingAttempts($identifier);
9493

9594
$count = 0;
9695
$currentTime = $this->timeFactory->getTime();
97-
/** @var array $existingAttempts */
98-
foreach ($existingAttempts as $attempt) {
99-
if (($attempt + $seconds) > $currentTime) {
96+
foreach ($existingAttempts as $expirationTime) {
97+
if ($expirationTime > $currentTime) {
10098
$count++;
10199
}
102100
}
@@ -114,16 +112,16 @@ public function registerAttempt(string $methodIdentifier,
114112
$existingAttempts = $this->getExistingAttempts($identifier);
115113
$currentTime = $this->timeFactory->getTime();
116114

117-
// Unset all attempts older than $period
118-
foreach ($existingAttempts as $key => $attempt) {
119-
if (($attempt + $period) < $currentTime) {
115+
// Unset all attempts that are already expired
116+
foreach ($existingAttempts as $key => $expirationTime) {
117+
if ($expirationTime < $currentTime) {
120118
unset($existingAttempts[$key]);
121119
}
122120
}
123121
$existingAttempts = array_values($existingAttempts);
124122

125123
// Store the new attempt
126-
$existingAttempts[] = (string)$currentTime;
124+
$existingAttempts[] = (string)($currentTime + $period);
127125
$this->cache->set($identifier, json_encode($existingAttempts));
128126
}
129127
}

lib/private/Security/RateLimiting/Limiter.php

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,23 +30,17 @@
3030
use OC\Security\Normalizer\IpAddress;
3131
use OC\Security\RateLimiting\Backend\IBackend;
3232
use OC\Security\RateLimiting\Exception\RateLimitExceededException;
33-
use OCP\AppFramework\Utility\ITimeFactory;
3433
use OCP\IUser;
3534

3635
class Limiter {
3736
/** @var IBackend */
3837
private $backend;
39-
/** @var ITimeFactory */
40-
private $timeFactory;
4138

4239
/**
43-
* @param ITimeFactory $timeFactory
4440
* @param IBackend $backend
4541
*/
46-
public function __construct(ITimeFactory $timeFactory,
47-
IBackend $backend) {
42+
public function __construct(IBackend $backend) {
4843
$this->backend = $backend;
49-
$this->timeFactory = $timeFactory;
5044
}
5145

5246
/**
@@ -60,12 +54,12 @@ private function register(string $methodIdentifier,
6054
string $userIdentifier,
6155
int $period,
6256
int $limit): void {
63-
$existingAttempts = $this->backend->getAttempts($methodIdentifier, $userIdentifier, $period);
57+
$existingAttempts = $this->backend->getAttempts($methodIdentifier, $userIdentifier);
6458
if ($existingAttempts >= $limit) {
6559
throw new RateLimitExceededException();
6660
}
6761

68-
$this->backend->registerAttempt($methodIdentifier, $userIdentifier, $this->timeFactory->getTime());
62+
$this->backend->registerAttempt($methodIdentifier, $userIdentifier, $period);
6963
}
7064

7165
/**

lib/private/Server.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -756,10 +756,21 @@ public function __construct($webRoot, \OC\Config $config) {
756756
$this->registerDeprecatedAlias('Search', ISearch::class);
757757

758758
$this->registerService(\OC\Security\RateLimiting\Backend\IBackend::class, function ($c) {
759-
return new \OC\Security\RateLimiting\Backend\MemoryCache(
760-
$this->getMemCacheFactory(),
761-
new \OC\AppFramework\Utility\TimeFactory()
762-
);
759+
$cacheFactory = $this->query(ICacheFactory::class);
760+
761+
if ($cacheFactory->isAvailable()) {
762+
$backend = new \OC\Security\RateLimiting\Backend\MemoryCacheBackend(
763+
$this->query(ICacheFactory::class),
764+
new \OC\AppFramework\Utility\TimeFactory()
765+
);
766+
} else {
767+
$backend = new \OC\Security\RateLimiting\Backend\DatabaseBackend(
768+
$this->query(IDBConnection::class),
769+
new \OC\AppFramework\Utility\TimeFactory()
770+
);
771+
}
772+
773+
return $backend;
763774
});
764775

765776
$this->registerService(\OCP\Security\ISecureRandom::class, function ($c) {

0 commit comments

Comments
 (0)