RateLimitingMiddleware.php 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OC\AppFramework\Middleware\Security;
  8. use OC\AppFramework\Utility\ControllerMethodReflector;
  9. use OC\Security\RateLimiting\Exception\RateLimitExceededException;
  10. use OC\Security\RateLimiting\Limiter;
  11. use OC\User\Session;
  12. use OCP\AppFramework\Controller;
  13. use OCP\AppFramework\Http\Attribute\AnonRateLimit;
  14. use OCP\AppFramework\Http\Attribute\ARateLimit;
  15. use OCP\AppFramework\Http\Attribute\UserRateLimit;
  16. use OCP\AppFramework\Http\DataResponse;
  17. use OCP\AppFramework\Http\Response;
  18. use OCP\AppFramework\Http\TemplateResponse;
  19. use OCP\AppFramework\Middleware;
  20. use OCP\IRequest;
  21. use OCP\ISession;
  22. use OCP\IUserSession;
  23. use ReflectionMethod;
  24. /**
  25. * Class RateLimitingMiddleware is the middleware responsible for implementing the
  26. * ratelimiting in Nextcloud.
  27. *
  28. * It parses annotations such as:
  29. *
  30. * @UserRateThrottle(limit=5, period=100)
  31. * @AnonRateThrottle(limit=1, period=100)
  32. *
  33. * Or attributes such as:
  34. *
  35. * #[UserRateLimit(limit: 5, period: 100)]
  36. * #[AnonRateLimit(limit: 1, period: 100)]
  37. *
  38. * Both sets would mean that logged-in users can access the page 5
  39. * times within 100 seconds, and anonymous users 1 time within 100 seconds. If
  40. * only an AnonRateThrottle is specified that one will also be applied to logged-in
  41. * users.
  42. *
  43. * @package OC\AppFramework\Middleware\Security
  44. */
  45. class RateLimitingMiddleware extends Middleware {
  46. public function __construct(
  47. protected IRequest $request,
  48. protected IUserSession $userSession,
  49. protected ControllerMethodReflector $reflector,
  50. protected Limiter $limiter,
  51. protected ISession $session,
  52. ) {
  53. }
  54. /**
  55. * {@inheritDoc}
  56. * @throws RateLimitExceededException
  57. */
  58. public function beforeController(Controller $controller, string $methodName): void {
  59. parent::beforeController($controller, $methodName);
  60. $rateLimitIdentifier = get_class($controller) . '::' . $methodName;
  61. if ($this->userSession instanceof Session && $this->userSession->getSession()->get('app_api') === true && $this->userSession->getUser() === null) {
  62. // if userId is not specified and the request is authenticated by AppAPI, we skip the rate limit
  63. return;
  64. }
  65. if ($this->userSession->isLoggedIn()) {
  66. $rateLimit = $this->readLimitFromAnnotationOrAttribute($controller, $methodName, 'UserRateThrottle', UserRateLimit::class);
  67. if ($rateLimit !== null) {
  68. $this->limiter->registerUserRequest(
  69. $rateLimitIdentifier,
  70. $rateLimit->getLimit(),
  71. $rateLimit->getPeriod(),
  72. $this->userSession->getUser()
  73. );
  74. return;
  75. }
  76. // If not user specific rate limit is found the Anon rate limit applies!
  77. }
  78. $rateLimit = $this->readLimitFromAnnotationOrAttribute($controller, $methodName, 'AnonRateThrottle', AnonRateLimit::class);
  79. if ($rateLimit !== null) {
  80. $this->limiter->registerAnonRequest(
  81. $rateLimitIdentifier,
  82. $rateLimit->getLimit(),
  83. $rateLimit->getPeriod(),
  84. $this->request->getRemoteAddress()
  85. );
  86. }
  87. }
  88. /**
  89. * @template T of ARateLimit
  90. *
  91. * @param Controller $controller
  92. * @param string $methodName
  93. * @param string $annotationName
  94. * @param class-string<T> $attributeClass
  95. * @return ?ARateLimit
  96. */
  97. protected function readLimitFromAnnotationOrAttribute(Controller $controller, string $methodName, string $annotationName, string $attributeClass): ?ARateLimit {
  98. $annotationLimit = $this->reflector->getAnnotationParameter($annotationName, 'limit');
  99. $annotationPeriod = $this->reflector->getAnnotationParameter($annotationName, 'period');
  100. if ($annotationLimit !== '' && $annotationPeriod !== '') {
  101. return new $attributeClass(
  102. (int)$annotationLimit,
  103. (int)$annotationPeriod,
  104. );
  105. }
  106. $reflectionMethod = new ReflectionMethod($controller, $methodName);
  107. $attributes = $reflectionMethod->getAttributes($attributeClass);
  108. $attribute = current($attributes);
  109. if ($attribute !== false) {
  110. return $attribute->newInstance();
  111. }
  112. return null;
  113. }
  114. /**
  115. * {@inheritDoc}
  116. */
  117. public function afterException(Controller $controller, string $methodName, \Exception $exception): Response {
  118. if ($exception instanceof RateLimitExceededException) {
  119. if (stripos($this->request->getHeader('Accept'), 'html') === false) {
  120. $response = new DataResponse([], $exception->getCode());
  121. } else {
  122. $response = new TemplateResponse(
  123. 'core',
  124. '429',
  125. [],
  126. TemplateResponse::RENDER_AS_GUEST
  127. );
  128. $response->setStatus($exception->getCode());
  129. }
  130. return $response;
  131. }
  132. throw $exception;
  133. }
  134. }