|
1 | 1 | <?php |
| 2 | + |
| 3 | +declare(strict_types=1); |
| 4 | + |
2 | 5 | /** |
| 6 | + * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> |
3 | 7 | * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> |
4 | 8 | * |
5 | 9 | * @author Christoph Wurst <christoph@winzerhof-wurst.at> |
|
27 | 31 | use OC\AppFramework\Utility\ControllerMethodReflector; |
28 | 32 | use OC\Security\RateLimiting\Exception\RateLimitExceededException; |
29 | 33 | 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; |
30 | 38 | use OCP\AppFramework\Http\DataResponse; |
| 39 | +use OCP\AppFramework\Http\Response; |
31 | 40 | use OCP\AppFramework\Http\TemplateResponse; |
32 | 41 | use OCP\AppFramework\Middleware; |
33 | 42 | use OCP\IRequest; |
34 | 43 | use OCP\IUserSession; |
| 44 | +use ReflectionMethod; |
35 | 45 |
|
36 | 46 | /** |
37 | 47 | * Class RateLimitingMiddleware is the middleware responsible for implementing the |
|
42 | 52 | * @UserRateThrottle(limit=5, period=100) |
43 | 53 | * @AnonRateThrottle(limit=1, period=100) |
44 | 54 | * |
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 |
46 | 61 | * times within 100 seconds, and anonymous users 1 time within 100 seconds. If |
47 | 62 | * only an AnonRateThrottle is specified that one will also be applied to logged-in |
48 | 63 | * users. |
49 | 64 | * |
50 | 65 | * @package OC\AppFramework\Middleware\Security |
51 | 66 | */ |
52 | 67 | 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 | + ) { |
76 | 74 | } |
77 | 75 |
|
78 | 76 | /** |
79 | 77 | * {@inheritDoc} |
80 | 78 | * @throws RateLimitExceededException |
81 | 79 | */ |
82 | | - public function beforeController($controller, $methodName) { |
| 80 | + public function beforeController(Controller $controller, string $methodName): void { |
83 | 81 | 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'); |
89 | 82 | $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) { |
98 | 103 | $this->limiter->registerAnonRequest( |
99 | 104 | $rateLimitIdentifier, |
100 | | - $anonLimit, |
101 | | - $anonPeriod, |
| 105 | + $rateLimit->getLimit(), |
| 106 | + $rateLimit->getPeriod(), |
102 | 107 | $this->request->getRemoteAddress() |
103 | 108 | ); |
104 | 109 | } |
105 | 110 | } |
106 | 111 |
|
| 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 | + |
107 | 143 | /** |
108 | 144 | * {@inheritDoc} |
109 | 145 | */ |
110 | | - public function afterException($controller, $methodName, \Exception $exception) { |
| 146 | + public function afterException(Controller $controller, string $methodName, \Exception $exception): Response { |
111 | 147 | if ($exception instanceof RateLimitExceededException) { |
112 | 148 | if (stripos($this->request->getHeader('Accept'), 'html') === false) { |
113 | 149 | $response = new DataResponse([], $exception->getCode()); |
|
0 commit comments