Skip to content

Commit 0f0be52

Browse files
Merge pull request #37864 from nextcloud/feat/noid/ratelimit-with-attributes
feat(ratelimit): Add Attributes support to rate limit middleware
2 parents 95c0981 + 89c3c31 commit 0f0be52

7 files changed

Lines changed: 386 additions & 144 deletions

File tree

lib/composer/composer/autoload_classmap.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,11 @@
3535
'OCP\\AppFramework\\Db\\QBMapper' => $baseDir . '/lib/public/AppFramework/Db/QBMapper.php',
3636
'OCP\\AppFramework\\Db\\TTransactional' => $baseDir . '/lib/public/AppFramework/Db/TTransactional.php',
3737
'OCP\\AppFramework\\Http' => $baseDir . '/lib/public/AppFramework/Http.php',
38+
'OCP\\AppFramework\\Http\\Attribute\\ARateLimit' => $baseDir . '/lib/public/AppFramework/Http/Attribute/ARateLimit.php',
39+
'OCP\\AppFramework\\Http\\Attribute\\AnonRateLimit' => $baseDir . '/lib/public/AppFramework/Http/Attribute/AnonRateLimit.php',
3840
'OCP\\AppFramework\\Http\\Attribute\\BruteForceProtection' => $baseDir . '/lib/public/AppFramework/Http/Attribute/BruteForceProtection.php',
3941
'OCP\\AppFramework\\Http\\Attribute\\UseSession' => $baseDir . '/lib/public/AppFramework/Http/Attribute/UseSession.php',
42+
'OCP\\AppFramework\\Http\\Attribute\\UserRateLimit' => $baseDir . '/lib/public/AppFramework/Http/Attribute/UserRateLimit.php',
4043
'OCP\\AppFramework\\Http\\ContentSecurityPolicy' => $baseDir . '/lib/public/AppFramework/Http/ContentSecurityPolicy.php',
4144
'OCP\\AppFramework\\Http\\DataDisplayResponse' => $baseDir . '/lib/public/AppFramework/Http/DataDisplayResponse.php',
4245
'OCP\\AppFramework\\Http\\DataDownloadResponse' => $baseDir . '/lib/public/AppFramework/Http/DataDownloadResponse.php',

lib/composer/composer/autoload_static.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,11 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
6868
'OCP\\AppFramework\\Db\\QBMapper' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Db/QBMapper.php',
6969
'OCP\\AppFramework\\Db\\TTransactional' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Db/TTransactional.php',
7070
'OCP\\AppFramework\\Http' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http.php',
71+
'OCP\\AppFramework\\Http\\Attribute\\ARateLimit' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/ARateLimit.php',
72+
'OCP\\AppFramework\\Http\\Attribute\\AnonRateLimit' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/AnonRateLimit.php',
7173
'OCP\\AppFramework\\Http\\Attribute\\BruteForceProtection' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/BruteForceProtection.php',
7274
'OCP\\AppFramework\\Http\\Attribute\\UseSession' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/UseSession.php',
75+
'OCP\\AppFramework\\Http\\Attribute\\UserRateLimit' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/UserRateLimit.php',
7376
'OCP\\AppFramework\\Http\\ContentSecurityPolicy' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/ContentSecurityPolicy.php',
7477
'OCP\\AppFramework\\Http\\DataDisplayResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/DataDisplayResponse.php',
7578
'OCP\\AppFramework\\Http\\DataDownloadResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/DataDownloadResponse.php',

lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php

Lines changed: 77 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
<?php
2+
3+
declare(strict_types=1);
4+
25
/**
6+
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com>
37
* @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
48
*
59
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
@@ -27,11 +31,17 @@
2731
use OC\AppFramework\Utility\ControllerMethodReflector;
2832
use OC\Security\RateLimiting\Exception\RateLimitExceededException;
2933
use OC\Security\RateLimiting\Limiter;
34+
use OCP\AppFramework\Controller;
35+
use OCP\AppFramework\Http\Attribute\AnonRateLimit;
36+
use OCP\AppFramework\Http\Attribute\ARateLimit;
37+
use OCP\AppFramework\Http\Attribute\UserRateLimit;
3038
use OCP\AppFramework\Http\DataResponse;
39+
use OCP\AppFramework\Http\Response;
3140
use OCP\AppFramework\Http\TemplateResponse;
3241
use OCP\AppFramework\Middleware;
3342
use OCP\IRequest;
3443
use OCP\IUserSession;
44+
use ReflectionMethod;
3545

3646
/**
3747
* Class RateLimitingMiddleware is the middleware responsible for implementing the
@@ -42,72 +52,98 @@
4252
* @UserRateThrottle(limit=5, period=100)
4353
* @AnonRateThrottle(limit=1, period=100)
4454
*
45-
* Those annotations above would mean that logged-in users can access the page 5
55+
* Or attributes such as:
56+
*
57+
* #[UserRateLimit(limit: 5, period: 100)]
58+
* #[AnonRateLimit(limit: 1, period: 100)]
59+
*
60+
* Both sets would mean that logged-in users can access the page 5
4661
* times within 100 seconds, and anonymous users 1 time within 100 seconds. If
4762
* only an AnonRateThrottle is specified that one will also be applied to logged-in
4863
* users.
4964
*
5065
* @package OC\AppFramework\Middleware\Security
5166
*/
5267
class RateLimitingMiddleware extends Middleware {
53-
/** @var IRequest $request */
54-
private $request;
55-
/** @var IUserSession */
56-
private $userSession;
57-
/** @var ControllerMethodReflector */
58-
private $reflector;
59-
/** @var Limiter */
60-
private $limiter;
61-
62-
/**
63-
* @param IRequest $request
64-
* @param IUserSession $userSession
65-
* @param ControllerMethodReflector $reflector
66-
* @param Limiter $limiter
67-
*/
68-
public function __construct(IRequest $request,
69-
IUserSession $userSession,
70-
ControllerMethodReflector $reflector,
71-
Limiter $limiter) {
72-
$this->request = $request;
73-
$this->userSession = $userSession;
74-
$this->reflector = $reflector;
75-
$this->limiter = $limiter;
68+
public function __construct(
69+
protected IRequest $request,
70+
protected IUserSession $userSession,
71+
protected ControllerMethodReflector $reflector,
72+
protected Limiter $limiter,
73+
) {
7674
}
7775

7876
/**
7977
* {@inheritDoc}
8078
* @throws RateLimitExceededException
8179
*/
82-
public function beforeController($controller, $methodName) {
80+
public function beforeController(Controller $controller, string $methodName): void {
8381
parent::beforeController($controller, $methodName);
84-
85-
$anonLimit = $this->reflector->getAnnotationParameter('AnonRateThrottle', 'limit');
86-
$anonPeriod = $this->reflector->getAnnotationParameter('AnonRateThrottle', 'period');
87-
$userLimit = $this->reflector->getAnnotationParameter('UserRateThrottle', 'limit');
88-
$userPeriod = $this->reflector->getAnnotationParameter('UserRateThrottle', 'period');
8982
$rateLimitIdentifier = get_class($controller) . '::' . $methodName;
90-
if ($userLimit !== '' && $userPeriod !== '' && $this->userSession->isLoggedIn()) {
91-
$this->limiter->registerUserRequest(
92-
$rateLimitIdentifier,
93-
$userLimit,
94-
$userPeriod,
95-
$this->userSession->getUser()
96-
);
97-
} elseif ($anonLimit !== '' && $anonPeriod !== '') {
83+
84+
if ($this->userSession->isLoggedIn()) {
85+
$rateLimit = $this->readLimitFromAnnotationOrAttribute($controller, $methodName, 'UserRateThrottle', UserRateLimit::class);
86+
87+
if ($rateLimit !== null) {
88+
$this->limiter->registerUserRequest(
89+
$rateLimitIdentifier,
90+
$rateLimit->getLimit(),
91+
$rateLimit->getPeriod(),
92+
$this->userSession->getUser()
93+
);
94+
return;
95+
}
96+
97+
// If not user specific rate limit is found the Anon rate limit applies!
98+
}
99+
100+
$rateLimit = $this->readLimitFromAnnotationOrAttribute($controller, $methodName, 'AnonRateThrottle', AnonRateLimit::class);
101+
102+
if ($rateLimit !== null) {
98103
$this->limiter->registerAnonRequest(
99104
$rateLimitIdentifier,
100-
$anonLimit,
101-
$anonPeriod,
105+
$rateLimit->getLimit(),
106+
$rateLimit->getPeriod(),
102107
$this->request->getRemoteAddress()
103108
);
104109
}
105110
}
106111

112+
/**
113+
* @template T of ARateLimit
114+
*
115+
* @param Controller $controller
116+
* @param string $methodName
117+
* @param string $annotationName
118+
* @param class-string<T> $attributeClass
119+
* @return ?ARateLimit
120+
*/
121+
protected function readLimitFromAnnotationOrAttribute(Controller $controller, string $methodName, string $annotationName, string $attributeClass): ?ARateLimit {
122+
$annotationLimit = $this->reflector->getAnnotationParameter($annotationName, 'limit');
123+
$annotationPeriod = $this->reflector->getAnnotationParameter($annotationName, 'period');
124+
125+
if ($annotationLimit !== '' && $annotationPeriod !== '') {
126+
return new $attributeClass(
127+
(int) $annotationLimit,
128+
(int) $annotationPeriod,
129+
);
130+
}
131+
132+
$reflectionMethod = new ReflectionMethod($controller, $methodName);
133+
$attributes = $reflectionMethod->getAttributes($attributeClass);
134+
$attribute = current($attributes);
135+
136+
if ($attribute !== false) {
137+
return $attribute->newInstance();
138+
}
139+
140+
return null;
141+
}
142+
107143
/**
108144
* {@inheritDoc}
109145
*/
110-
public function afterException($controller, $methodName, \Exception $exception) {
146+
public function afterException(Controller $controller, string $methodName, \Exception $exception): Response {
111147
if ($exception instanceof RateLimitExceededException) {
112148
if (stripos($this->request->getHeader('Accept'), 'html') === false) {
113149
$response = new DataResponse([], $exception->getCode());
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com>
7+
*
8+
* @author Joas Schilling <coding@schilljs.com>
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 OCP\AppFramework\Http\Attribute;
27+
28+
/**
29+
* Attribute for controller methods that want to limit the times a logged-in
30+
* user can call the endpoint in a given time period.
31+
*
32+
* @since 27.0.0
33+
*/
34+
abstract class ARateLimit {
35+
/**
36+
* @since 27.0.0
37+
*/
38+
public function __construct(
39+
protected int $limit,
40+
protected int $period,
41+
) {
42+
}
43+
44+
/**
45+
* @since 27.0.0
46+
*/
47+
public function getLimit(): int {
48+
return $this->limit;
49+
}
50+
51+
/**
52+
* @since 27.0.0
53+
*/
54+
public function getPeriod(): int {
55+
return $this->period;
56+
}
57+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com>
7+
*
8+
* @author Joas Schilling <coding@schilljs.com>
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 OCP\AppFramework\Http\Attribute;
27+
28+
use Attribute;
29+
30+
/**
31+
* Attribute for controller methods that want to limit the times a not logged-in
32+
* guest can call the endpoint in a given time period.
33+
*
34+
* @since 27.0.0
35+
*/
36+
#[Attribute(Attribute::TARGET_METHOD)]
37+
class AnonRateLimit extends ARateLimit {
38+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com>
7+
*
8+
* @author Joas Schilling <coding@schilljs.com>
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 OCP\AppFramework\Http\Attribute;
27+
28+
use Attribute;
29+
30+
/**
31+
* Attribute for controller methods that want to limit the times a logged-in
32+
* user can call the endpoint in a given time period.
33+
*
34+
* @since 27.0.0
35+
*/
36+
#[Attribute(Attribute::TARGET_METHOD)]
37+
class UserRateLimit extends ARateLimit {
38+
}

0 commit comments

Comments
 (0)