Passed
Push — master ( 95c098...0f0be5 )
by Joas
16:23 queued 12s
created

RateLimitingMiddleware   A

Complexity

Total Complexity 12

Size/Duplication

Total Lines 97
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 42
dl 0
loc 97
rs 10
c 0
b 0
f 0
wmc 12

4 Methods

Rating   Name   Duplication   Size   Complexity  
A beforeController() 0 28 4
A afterException() 0 18 3
A __construct() 0 6 1
A readLimitFromAnnotationOrAttribute() 0 20 4
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright Copyright (c) 2023 Joas Schilling <[email protected]>
7
 * @copyright Copyright (c) 2017 Lukas Reschke <[email protected]>
8
 *
9
 * @author Christoph Wurst <[email protected]>
10
 * @author Joas Schilling <[email protected]>
11
 * @author Lukas Reschke <[email protected]>
12
 *
13
 * @license GNU AGPL version 3 or any later version
14
 *
15
 * This program is free software: you can redistribute it and/or modify
16
 * it under the terms of the GNU Affero General Public License as
17
 * published by the Free Software Foundation, either version 3 of the
18
 * License, or (at your option) any later version.
19
 *
20
 * This program is distributed in the hope that it will be useful,
21
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
 * GNU Affero General Public License for more details.
24
 *
25
 * You should have received a copy of the GNU Affero General Public License
26
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
27
 *
28
 */
29
namespace OC\AppFramework\Middleware\Security;
30
31
use OC\AppFramework\Utility\ControllerMethodReflector;
32
use OC\Security\RateLimiting\Exception\RateLimitExceededException;
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;
38
use OCP\AppFramework\Http\DataResponse;
39
use OCP\AppFramework\Http\Response;
40
use OCP\AppFramework\Http\TemplateResponse;
41
use OCP\AppFramework\Middleware;
42
use OCP\IRequest;
43
use OCP\IUserSession;
44
use ReflectionMethod;
45
46
/**
47
 * Class RateLimitingMiddleware is the middleware responsible for implementing the
48
 * ratelimiting in Nextcloud.
49
 *
50
 * It parses annotations such as:
51
 *
52
 * @UserRateThrottle(limit=5, period=100)
53
 * @AnonRateThrottle(limit=1, period=100)
54
 *
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
61
 * times within 100 seconds, and anonymous users 1 time within 100 seconds. If
62
 * only an AnonRateThrottle is specified that one will also be applied to logged-in
63
 * users.
64
 *
65
 * @package OC\AppFramework\Middleware\Security
66
 */
67
class RateLimitingMiddleware extends Middleware {
68
	public function __construct(
69
		protected IRequest $request,
70
		protected IUserSession $userSession,
71
		protected ControllerMethodReflector $reflector,
72
		protected Limiter $limiter,
73
	) {
74
	}
75
76
	/**
77
	 * {@inheritDoc}
78
	 * @throws RateLimitExceededException
79
	 */
80
	public function beforeController(Controller $controller, string $methodName): void {
81
		parent::beforeController($controller, $methodName);
82
		$rateLimitIdentifier = get_class($controller) . '::' . $methodName;
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) {
103
			$this->limiter->registerAnonRequest(
104
				$rateLimitIdentifier,
105
				$rateLimit->getLimit(),
106
				$rateLimit->getPeriod(),
107
				$this->request->getRemoteAddress()
108
			);
109
		}
110
	}
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
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
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
143
	/**
144
	 * {@inheritDoc}
145
	 */
146
	public function afterException(Controller $controller, string $methodName, \Exception $exception): Response {
147
		if ($exception instanceof RateLimitExceededException) {
148
			if (stripos($this->request->getHeader('Accept'), 'html') === false) {
149
				$response = new DataResponse([], $exception->getCode());
150
			} else {
151
				$response = new TemplateResponse(
152
					'core',
153
					'429',
154
					[],
155
					TemplateResponse::RENDER_AS_GUEST
156
				);
157
				$response->setStatus($exception->getCode());
158
			}
159
160
			return $response;
161
		}
162
163
		throw $exception;
164
	}
165
}
166