RateLimitingMiddleware.php 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  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\IUserSession;
  41. use ReflectionMethod;
  42. /**
  43. * Class RateLimitingMiddleware is the middleware responsible for implementing the
  44. * ratelimiting in Nextcloud.
  45. *
  46. * It parses annotations such as:
  47. *
  48. * @UserRateThrottle(limit=5, period=100)
  49. * @AnonRateThrottle(limit=1, period=100)
  50. *
  51. * Or attributes such as:
  52. *
  53. * #[UserRateLimit(limit: 5, period: 100)]
  54. * #[AnonRateLimit(limit: 1, period: 100)]
  55. *
  56. * Both sets would mean that logged-in users can access the page 5
  57. * times within 100 seconds, and anonymous users 1 time within 100 seconds. If
  58. * only an AnonRateThrottle is specified that one will also be applied to logged-in
  59. * users.
  60. *
  61. * @package OC\AppFramework\Middleware\Security
  62. */
  63. class RateLimitingMiddleware extends Middleware {
  64. public function __construct(
  65. protected IRequest $request,
  66. protected IUserSession $userSession,
  67. protected ControllerMethodReflector $reflector,
  68. protected Limiter $limiter,
  69. ) {
  70. }
  71. /**
  72. * {@inheritDoc}
  73. * @throws RateLimitExceededException
  74. */
  75. public function beforeController(Controller $controller, string $methodName): void {
  76. parent::beforeController($controller, $methodName);
  77. $rateLimitIdentifier = get_class($controller) . '::' . $methodName;
  78. if ($this->userSession->isLoggedIn()) {
  79. $rateLimit = $this->readLimitFromAnnotationOrAttribute($controller, $methodName, 'UserRateThrottle', UserRateLimit::class);
  80. if ($rateLimit !== null) {
  81. $this->limiter->registerUserRequest(
  82. $rateLimitIdentifier,
  83. $rateLimit->getLimit(),
  84. $rateLimit->getPeriod(),
  85. $this->userSession->getUser()
  86. );
  87. return;
  88. }
  89. // If not user specific rate limit is found the Anon rate limit applies!
  90. }
  91. $rateLimit = $this->readLimitFromAnnotationOrAttribute($controller, $methodName, 'AnonRateThrottle', AnonRateLimit::class);
  92. if ($rateLimit !== null) {
  93. $this->limiter->registerAnonRequest(
  94. $rateLimitIdentifier,
  95. $rateLimit->getLimit(),
  96. $rateLimit->getPeriod(),
  97. $this->request->getRemoteAddress()
  98. );
  99. }
  100. }
  101. /**
  102. * @template T of ARateLimit
  103. *
  104. * @param Controller $controller
  105. * @param string $methodName
  106. * @param string $annotationName
  107. * @param class-string<T> $attributeClass
  108. * @return ?ARateLimit
  109. */
  110. protected function readLimitFromAnnotationOrAttribute(Controller $controller, string $methodName, string $annotationName, string $attributeClass): ?ARateLimit {
  111. $annotationLimit = $this->reflector->getAnnotationParameter($annotationName, 'limit');
  112. $annotationPeriod = $this->reflector->getAnnotationParameter($annotationName, 'period');
  113. if ($annotationLimit !== '' && $annotationPeriod !== '') {
  114. return new $attributeClass(
  115. (int) $annotationLimit,
  116. (int) $annotationPeriod,
  117. );
  118. }
  119. $reflectionMethod = new ReflectionMethod($controller, $methodName);
  120. $attributes = $reflectionMethod->getAttributes($attributeClass);
  121. $attribute = current($attributes);
  122. if ($attribute !== false) {
  123. return $attribute->newInstance();
  124. }
  125. return null;
  126. }
  127. /**
  128. * {@inheritDoc}
  129. */
  130. public function afterException(Controller $controller, string $methodName, \Exception $exception): Response {
  131. if ($exception instanceof RateLimitExceededException) {
  132. if (stripos($this->request->getHeader('Accept'), 'html') === false) {
  133. $response = new DataResponse([], $exception->getCode());
  134. } else {
  135. $response = new TemplateResponse(
  136. 'core',
  137. '429',
  138. [],
  139. TemplateResponse::RENDER_AS_GUEST
  140. );
  141. $response->setStatus($exception->getCode());
  142. }
  143. return $response;
  144. }
  145. throw $exception;
  146. }
  147. }