RateLimitingMiddleware.php 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com>
  5. * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
  6. *
  7. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  8. * @author Joas Schilling <coding@schilljs.com>
  9. * @author Lukas Reschke <lukas@statuscode.ch>
  10. *
  11. * @license GNU AGPL version 3 or any later version
  12. *
  13. * This program is free software: you can redistribute it and/or modify
  14. * it under the terms of the GNU Affero General Public License as
  15. * published by the Free Software Foundation, either version 3 of the
  16. * License, or (at your option) any later version.
  17. *
  18. * This program is distributed in the hope that it will be useful,
  19. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. * GNU Affero General Public License for more details.
  22. *
  23. * You should have received a copy of the GNU Affero General Public License
  24. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  25. *
  26. */
  27. namespace OC\AppFramework\Middleware\Security;
  28. use OC\AppFramework\Utility\ControllerMethodReflector;
  29. use OC\Security\RateLimiting\Exception\RateLimitExceededException;
  30. use OC\Security\RateLimiting\Limiter;
  31. use OCP\AppFramework\Controller;
  32. use OCP\AppFramework\Http\Attribute\AnonRateLimit;
  33. use OCP\AppFramework\Http\Attribute\ARateLimit;
  34. use OCP\AppFramework\Http\Attribute\UserRateLimit;
  35. use OCP\AppFramework\Http\DataResponse;
  36. use OCP\AppFramework\Http\Response;
  37. use OCP\AppFramework\Http\TemplateResponse;
  38. use OCP\AppFramework\Middleware;
  39. use OCP\IRequest;
  40. use OCP\ISession;
  41. use OCP\IUserSession;
  42. use ReflectionMethod;
  43. /**
  44. * Class RateLimitingMiddleware is the middleware responsible for implementing the
  45. * ratelimiting in Nextcloud.
  46. *
  47. * It parses annotations such as:
  48. *
  49. * @UserRateThrottle(limit=5, period=100)
  50. * @AnonRateThrottle(limit=1, period=100)
  51. *
  52. * Or attributes such as:
  53. *
  54. * #[UserRateLimit(limit: 5, period: 100)]
  55. * #[AnonRateLimit(limit: 1, period: 100)]
  56. *
  57. * Both sets would mean that logged-in users can access the page 5
  58. * times within 100 seconds, and anonymous users 1 time within 100 seconds. If
  59. * only an AnonRateThrottle is specified that one will also be applied to logged-in
  60. * users.
  61. *
  62. * @package OC\AppFramework\Middleware\Security
  63. */
  64. class RateLimitingMiddleware extends Middleware {
  65. public function __construct(
  66. protected IRequest $request,
  67. protected IUserSession $userSession,
  68. protected ControllerMethodReflector $reflector,
  69. protected Limiter $limiter,
  70. protected ISession $session,
  71. ) {
  72. }
  73. /**
  74. * {@inheritDoc}
  75. * @throws RateLimitExceededException
  76. */
  77. public function beforeController(Controller $controller, string $methodName): void {
  78. parent::beforeController($controller, $methodName);
  79. $rateLimitIdentifier = get_class($controller) . '::' . $methodName;
  80. if ($this->session->exists('app_api_system')) {
  81. // Bypass rate limiting for app_api
  82. return;
  83. }
  84. if ($this->userSession->isLoggedIn()) {
  85. $rateLimit = $this->readLimitFromAnnotationOrAttribute($controller, $methodName, 'UserRateThrottle', UserRateLimit::class);
  86. if ($rateLimit !== null) {
  87. $this->limiter->registerUserRequest(
  88. $rateLimitIdentifier,
  89. $rateLimit->getLimit(),
  90. $rateLimit->getPeriod(),
  91. $this->userSession->getUser()
  92. );
  93. return;
  94. }
  95. // If not user specific rate limit is found the Anon rate limit applies!
  96. }
  97. $rateLimit = $this->readLimitFromAnnotationOrAttribute($controller, $methodName, 'AnonRateThrottle', AnonRateLimit::class);
  98. if ($rateLimit !== null) {
  99. $this->limiter->registerAnonRequest(
  100. $rateLimitIdentifier,
  101. $rateLimit->getLimit(),
  102. $rateLimit->getPeriod(),
  103. $this->request->getRemoteAddress()
  104. );
  105. }
  106. }
  107. /**
  108. * @template T of ARateLimit
  109. *
  110. * @param Controller $controller
  111. * @param string $methodName
  112. * @param string $annotationName
  113. * @param class-string<T> $attributeClass
  114. * @return ?ARateLimit
  115. */
  116. protected function readLimitFromAnnotationOrAttribute(Controller $controller, string $methodName, string $annotationName, string $attributeClass): ?ARateLimit {
  117. $annotationLimit = $this->reflector->getAnnotationParameter($annotationName, 'limit');
  118. $annotationPeriod = $this->reflector->getAnnotationParameter($annotationName, 'period');
  119. if ($annotationLimit !== '' && $annotationPeriod !== '') {
  120. return new $attributeClass(
  121. (int) $annotationLimit,
  122. (int) $annotationPeriod,
  123. );
  124. }
  125. $reflectionMethod = new ReflectionMethod($controller, $methodName);
  126. $attributes = $reflectionMethod->getAttributes($attributeClass);
  127. $attribute = current($attributes);
  128. if ($attribute !== false) {
  129. return $attribute->newInstance();
  130. }
  131. return null;
  132. }
  133. /**
  134. * {@inheritDoc}
  135. */
  136. public function afterException(Controller $controller, string $methodName, \Exception $exception): Response {
  137. if ($exception instanceof RateLimitExceededException) {
  138. if (stripos($this->request->getHeader('Accept'), 'html') === false) {
  139. $response = new DataResponse([], $exception->getCode());
  140. } else {
  141. $response = new TemplateResponse(
  142. 'core',
  143. '429',
  144. [],
  145. TemplateResponse::RENDER_AS_GUEST
  146. );
  147. $response->setStatus($exception->getCode());
  148. }
  149. return $response;
  150. }
  151. throw $exception;
  152. }
  153. }