Passed
Push — master ( c9e6a9...dccfe4 )
by Morris
18:54
created

Request::passesCSRFCheck()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 14
nc 6
nop 0
dl 0
loc 22
rs 9.2222
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
/**
4
 * @copyright Copyright (c) 2016, ownCloud, Inc.
5
 *
6
 * @author Bart Visscher <[email protected]>
7
 * @author Bernhard Posselt <[email protected]>
8
 * @author Christoph Wurst <[email protected]>
9
 * @author coderkun <[email protected]>
10
 * @author Joas Schilling <[email protected]>
11
 * @author Juan Pablo Villafáñez <[email protected]>
12
 * @author Jörn Friedrich Dreyer <[email protected]>
13
 * @author Lukas Reschke <[email protected]>
14
 * @author Mitar <[email protected]>
15
 * @author Morris Jobke <[email protected]>
16
 * @author Robin Appelman <[email protected]>
17
 * @author Robin McCorkell <[email protected]>
18
 * @author Roeland Jago Douma <[email protected]>
19
 * @author Thomas Müller <[email protected]>
20
 * @author Thomas Tanghus <[email protected]>
21
 * @author Vincent Petry <[email protected]>
22
 *
23
 * @license AGPL-3.0
24
 *
25
 * This code is free software: you can redistribute it and/or modify
26
 * it under the terms of the GNU Affero General Public License, version 3,
27
 * as published by the Free Software Foundation.
28
 *
29
 * This program is distributed in the hope that it will be useful,
30
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
31
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32
 * GNU Affero General Public License for more details.
33
 *
34
 * You should have received a copy of the GNU Affero General Public License, version 3,
35
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
36
 *
37
 */
38
39
namespace OC\AppFramework\Http;
40
41
use OC\Security\CSRF\CsrfToken;
42
use OC\Security\CSRF\CsrfTokenManager;
43
use OC\Security\TrustedDomainHelper;
44
use OCP\IConfig;
45
use OCP\IRequest;
46
use OCP\Security\ICrypto;
47
use OCP\Security\ISecureRandom;
48
49
/**
50
 * Class for accessing variables in the request.
51
 * This class provides an immutable object with request variables.
52
 *
53
 * @property mixed[] cookies
54
 * @property mixed[] env
55
 * @property mixed[] files
56
 * @property string method
57
 * @property mixed[] parameters
58
 * @property mixed[] server
59
 */
60
class Request implements \ArrayAccess, \Countable, IRequest {
61
62
	const USER_AGENT_IE = '/(MSIE)|(Trident)/';
63
	// Microsoft Edge User Agent from https://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx
64
	const USER_AGENT_MS_EDGE = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/[0-9.]+ (Mobile Safari|Safari)\/[0-9.]+ Edge\/[0-9.]+$/';
65
	// Firefox User Agent from https://developer.mozilla.org/en-US/docs/Web/HTTP/Gecko_user_agent_string_reference
66
	const USER_AGENT_FIREFOX = '/^Mozilla\/5\.0 \([^)]+\) Gecko\/[0-9.]+ Firefox\/[0-9.]+$/';
67
	// Chrome User Agent from https://developer.chrome.com/multidevice/user-agent
68
	const USER_AGENT_CHROME = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\)( Ubuntu Chromium\/[0-9.]+|) Chrome\/[0-9.]+ (Mobile Safari|Safari)\/[0-9.]+$/';
69
	// Safari User Agent from http://www.useragentstring.com/pages/Safari/
70
	const USER_AGENT_SAFARI = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Version\/[0-9.]+ Safari\/[0-9.A-Z]+$/';
71
	// Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent
72
	const USER_AGENT_ANDROID_MOBILE_CHROME = '#Android.*Chrome/[.0-9]*#';
73
	const USER_AGENT_FREEBOX = '#^Mozilla/5\.0$#';
74
	const REGEX_LOCALHOST = '/^(127\.0\.0\.1|localhost|::1)$/';
75
76
	/**
77
	 * @deprecated use \OCP\IRequest::USER_AGENT_CLIENT_IOS instead
78
	 */
79
	const USER_AGENT_OWNCLOUD_IOS = '/^Mozilla\/5\.0 \(iOS\) (ownCloud|Nextcloud)\-iOS.*$/';
80
	/**
81
	 * @deprecated use \OCP\IRequest::USER_AGENT_CLIENT_ANDROID instead
82
	 */
83
	const USER_AGENT_OWNCLOUD_ANDROID = '/^Mozilla\/5\.0 \(Android\) ownCloud\-android.*$/';
84
	/**
85
	 * @deprecated use \OCP\IRequest::USER_AGENT_CLIENT_DESKTOP instead
86
	 */
87
	const USER_AGENT_OWNCLOUD_DESKTOP = '/^Mozilla\/5\.0 \([A-Za-z ]+\) (mirall|csyncoC)\/.*$/';
88
89
	protected $inputStream;
90
	protected $content;
91
	protected $items = [];
92
	protected $allowedKeys = [
93
		'get',
94
		'post',
95
		'files',
96
		'server',
97
		'env',
98
		'cookies',
99
		'urlParams',
100
		'parameters',
101
		'method',
102
		'requesttoken',
103
	];
104
	/** @var ISecureRandom */
105
	protected $secureRandom;
106
	/** @var IConfig */
107
	protected $config;
108
	/** @var string */
109
	protected $requestId = '';
110
	/** @var ICrypto */
111
	protected $crypto;
112
	/** @var CsrfTokenManager|null */
113
	protected $csrfTokenManager;
114
115
	/** @var bool */
116
	protected $contentDecoded = false;
117
118
	/**
119
	 * @param array $vars An associative array with the following optional values:
120
	 *        - array 'urlParams' the parameters which were matched from the URL
121
	 *        - array 'get' the $_GET array
122
	 *        - array|string 'post' the $_POST array or JSON string
123
	 *        - array 'files' the $_FILES array
124
	 *        - array 'server' the $_SERVER array
125
	 *        - array 'env' the $_ENV array
126
	 *        - array 'cookies' the $_COOKIE array
127
	 *        - string 'method' the request method (GET, POST etc)
128
	 *        - string|false 'requesttoken' the requesttoken or false when not available
129
	 * @param ISecureRandom $secureRandom
130
	 * @param IConfig $config
131
	 * @param CsrfTokenManager|null $csrfTokenManager
132
	 * @param string $stream
133
	 * @see http://www.php.net/manual/en/reserved.variables.php
134
	 */
135
	public function __construct(array $vars= [],
136
								ISecureRandom $secureRandom = null,
137
								IConfig $config,
138
								CsrfTokenManager $csrfTokenManager = null,
139
								string $stream = 'php://input') {
140
		$this->inputStream = $stream;
141
		$this->items['params'] = [];
142
		$this->secureRandom = $secureRandom;
143
		$this->config = $config;
144
		$this->csrfTokenManager = $csrfTokenManager;
145
146
		if(!array_key_exists('method', $vars)) {
147
			$vars['method'] = 'GET';
148
		}
149
150
		foreach($this->allowedKeys as $name) {
151
			$this->items[$name] = isset($vars[$name])
152
				? $vars[$name]
153
				: [];
154
		}
155
156
		$this->items['parameters'] = array_merge(
157
			$this->items['get'],
158
			$this->items['post'],
159
			$this->items['urlParams'],
160
			$this->items['params']
161
		);
162
163
	}
164
	/**
165
	 * @param array $parameters
166
	 */
167
	public function setUrlParameters(array $parameters) {
168
		$this->items['urlParams'] = $parameters;
169
		$this->items['parameters'] = array_merge(
170
			$this->items['parameters'],
171
			$this->items['urlParams']
172
		);
173
	}
174
175
	/**
176
	 * Countable method
177
	 * @return int
178
	 */
179
	public function count(): int {
180
		return \count($this->items['parameters']);
181
	}
182
183
	/**
184
	* ArrayAccess methods
185
	*
186
	* Gives access to the combined GET, POST and urlParams arrays
187
	*
188
	* Examples:
189
	*
190
	* $var = $request['myvar'];
191
	*
192
	* or
193
	*
194
	* if(!isset($request['myvar']) {
195
	* 	// Do something
196
	* }
197
	*
198
	* $request['myvar'] = 'something'; // This throws an exception.
199
	*
200
	* @param string $offset The key to lookup
201
	* @return boolean
202
	*/
203
	public function offsetExists($offset): bool {
204
		return isset($this->items['parameters'][$offset]);
205
	}
206
207
	/**
208
	 * @see offsetExists
209
	 * @param string $offset
210
	 * @return mixed
211
	 */
212
	public function offsetGet($offset) {
213
		return isset($this->items['parameters'][$offset])
214
			? $this->items['parameters'][$offset]
215
			: null;
216
	}
217
218
	/**
219
	 * @see offsetExists
220
	 * @param string $offset
221
	 * @param mixed $value
222
	 */
223
	public function offsetSet($offset, $value) {
224
		throw new \RuntimeException('You cannot change the contents of the request object');
225
	}
226
227
	/**
228
	 * @see offsetExists
229
	 * @param string $offset
230
	 */
231
	public function offsetUnset($offset) {
232
		throw new \RuntimeException('You cannot change the contents of the request object');
233
	}
234
235
	/**
236
	 * Magic property accessors
237
	 * @param string $name
238
	 * @param mixed $value
239
	 */
240
	public function __set($name, $value) {
241
		throw new \RuntimeException('You cannot change the contents of the request object');
242
	}
243
244
	/**
245
	* Access request variables by method and name.
246
	* Examples:
247
	*
248
	* $request->post['myvar']; // Only look for POST variables
249
	* $request->myvar; or $request->{'myvar'}; or $request->{$myvar}
250
	* Looks in the combined GET, POST and urlParams array.
251
	*
252
	* If you access e.g. ->post but the current HTTP request method
253
	* is GET a \LogicException will be thrown.
254
	*
255
	* @param string $name The key to look for.
256
	* @throws \LogicException
257
	* @return mixed|null
258
	*/
259
	public function __get($name) {
260
		switch($name) {
261
			case 'put':
262
			case 'patch':
263
			case 'get':
264
			case 'post':
265
				if($this->method !== strtoupper($name)) {
266
					throw new \LogicException(sprintf('%s cannot be accessed in a %s request.', $name, $this->method));
267
				}
268
				return $this->getContent();
269
			case 'files':
270
			case 'server':
271
			case 'env':
272
			case 'cookies':
273
			case 'urlParams':
274
			case 'method':
275
				return isset($this->items[$name])
276
					? $this->items[$name]
277
					: null;
278
			case 'parameters':
279
			case 'params':
280
				return $this->getContent();
281
			default;
282
				return isset($this[$name])
283
					? $this[$name]
284
					: null;
285
		}
286
	}
287
288
	/**
289
	 * @param string $name
290
	 * @return bool
291
	 */
292
	public function __isset($name) {
293
		if (\in_array($name, $this->allowedKeys, true)) {
294
			return true;
295
		}
296
		return isset($this->items['parameters'][$name]);
297
	}
298
299
	/**
300
	 * @param string $id
301
	 */
302
	public function __unset($id) {
303
		throw new \RuntimeException('You cannot change the contents of the request object');
304
	}
305
306
	/**
307
	 * Returns the value for a specific http header.
308
	 *
309
	 * This method returns null if the header did not exist.
310
	 *
311
	 * @param string $name
312
	 * @return string
313
	 */
314
	public function getHeader(string $name): string {
315
316
		$name = strtoupper(str_replace('-', '_',$name));
317
		if (isset($this->server['HTTP_' . $name])) {
318
			return $this->server['HTTP_' . $name];
319
		}
320
321
		// There's a few headers that seem to end up in the top-level
322
		// server array.
323
		switch ($name) {
324
			case 'CONTENT_TYPE' :
325
			case 'CONTENT_LENGTH' :
326
				if (isset($this->server[$name])) {
327
					return $this->server[$name];
328
				}
329
				break;
330
			case 'REMOTE_ADDR' :
331
				if (isset($this->server[$name])) {
332
					return $this->server[$name];
333
				}
334
				break;
335
		}
336
337
		return '';
338
	}
339
340
	/**
341
	 * Lets you access post and get parameters by the index
342
	 * In case of json requests the encoded json body is accessed
343
	 *
344
	 * @param string $key the key which you want to access in the URL Parameter
345
	 *                     placeholder, $_POST or $_GET array.
346
	 *                     The priority how they're returned is the following:
347
	 *                     1. URL parameters
348
	 *                     2. POST parameters
349
	 *                     3. GET parameters
350
	 * @param mixed $default If the key is not found, this value will be returned
351
	 * @return mixed the content of the array
352
	 */
353
	public function getParam(string $key, $default = null) {
354
		return isset($this->parameters[$key])
355
			? $this->parameters[$key]
356
			: $default;
357
	}
358
359
	/**
360
	 * Returns all params that were received, be it from the request
361
	 * (as GET or POST) or throuh the URL by the route
362
	 * @return array the array with all parameters
363
	 */
364
	public function getParams(): array {
365
		return is_array($this->parameters) ? $this->parameters : [];
0 ignored issues
show
introduced by
The condition is_array($this->parameters) is always true.
Loading history...
366
	}
367
368
	/**
369
	 * Returns the method of the request
370
	 * @return string the method of the request (POST, GET, etc)
371
	 */
372
	public function getMethod(): string {
373
		return $this->method;
374
	}
375
376
	/**
377
	 * Shortcut for accessing an uploaded file through the $_FILES array
378
	 * @param string $key the key that will be taken from the $_FILES array
379
	 * @return array the file in the $_FILES element
380
	 */
381
	public function getUploadedFile(string $key) {
382
		return isset($this->files[$key]) ? $this->files[$key] : null;
383
	}
384
385
	/**
386
	 * Shortcut for getting env variables
387
	 * @param string $key the key that will be taken from the $_ENV array
388
	 * @return array the value in the $_ENV element
389
	 */
390
	public function getEnv(string $key) {
391
		return isset($this->env[$key]) ? $this->env[$key] : null;
392
	}
393
394
	/**
395
	 * Shortcut for getting cookie variables
396
	 * @param string $key the key that will be taken from the $_COOKIE array
397
	 * @return string the value in the $_COOKIE element
398
	 */
399
	public function getCookie(string $key) {
400
		return isset($this->cookies[$key]) ? $this->cookies[$key] : null;
401
	}
402
403
	/**
404
	 * Returns the request body content.
405
	 *
406
	 * If the HTTP request method is PUT and the body
407
	 * not application/x-www-form-urlencoded or application/json a stream
408
	 * resource is returned, otherwise an array.
409
	 *
410
	 * @return array|string|resource The request body content or a resource to read the body stream.
411
	 *
412
	 * @throws \LogicException
413
	 */
414
	protected function getContent() {
415
		// If the content can't be parsed into an array then return a stream resource.
416
		if ($this->method === 'PUT'
417
			&& $this->getHeader('Content-Length') !== '0'
418
			&& $this->getHeader('Content-Length') !== ''
419
			&& strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') === false
420
			&& strpos($this->getHeader('Content-Type'), 'application/json') === false
421
		) {
422
			if ($this->content === false) {
423
				throw new \LogicException(
424
					'"put" can only be accessed once if not '
425
					. 'application/x-www-form-urlencoded or application/json.'
426
				);
427
			}
428
			$this->content = false;
429
			return fopen($this->inputStream, 'rb');
0 ignored issues
show
Bug Best Practice introduced by
The expression return fopen($this->inputStream, 'rb') could also return false which is incompatible with the documented return type array|resource|string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
430
		} else {
431
			$this->decodeContent();
432
			return $this->items['parameters'];
433
		}
434
	}
435
436
	/**
437
	 * Attempt to decode the content and populate parameters
438
	 */
439
	protected function decodeContent() {
440
		if ($this->contentDecoded) {
441
			return;
442
		}
443
		$params = [];
444
445
		// 'application/json' must be decoded manually.
446
		if (strpos($this->getHeader('Content-Type'), 'application/json') !== false) {
447
			$params = json_decode(file_get_contents($this->inputStream), true);
448
			if($params !== null && \count($params) > 0) {
449
				$this->items['params'] = $params;
450
				if($this->method === 'POST') {
451
					$this->items['post'] = $params;
452
				}
453
			}
454
455
		// Handle application/x-www-form-urlencoded for methods other than GET
456
		// or post correctly
457
		} elseif($this->method !== 'GET'
458
				&& $this->method !== 'POST'
459
				&& strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') !== false) {
460
461
			parse_str(file_get_contents($this->inputStream), $params);
462
			if(\is_array($params)) {
463
				$this->items['params'] = $params;
464
			}
465
		}
466
467
		if (\is_array($params)) {
468
			$this->items['parameters'] = array_merge($this->items['parameters'], $params);
469
		}
470
		$this->contentDecoded = true;
471
	}
472
473
474
	/**
475
	 * Checks if the CSRF check was correct
476
	 * @return bool true if CSRF check passed
477
	 */
478
	public function passesCSRFCheck(): bool {
479
		if($this->csrfTokenManager === null) {
480
			return false;
481
		}
482
483
		if(!$this->passesStrictCookieCheck()) {
484
			return false;
485
		}
486
487
		if (isset($this->items['get']['requesttoken'])) {
488
			$token = $this->items['get']['requesttoken'];
489
		} elseif (isset($this->items['post']['requesttoken'])) {
490
			$token = $this->items['post']['requesttoken'];
491
		} elseif (isset($this->items['server']['HTTP_REQUESTTOKEN'])) {
492
			$token = $this->items['server']['HTTP_REQUESTTOKEN'];
493
		} else {
494
			//no token found.
495
			return false;
496
		}
497
		$token = new CsrfToken($token);
498
499
		return $this->csrfTokenManager->isTokenValid($token);
500
	}
501
502
	/**
503
	 * Whether the cookie checks are required
504
	 *
505
	 * @return bool
506
	 */
507
	private function cookieCheckRequired(): bool {
508
		if ($this->getHeader('OCS-APIREQUEST')) {
509
			return false;
510
		}
511
		if($this->getCookie(session_name()) === null && $this->getCookie('nc_token') === null) {
0 ignored issues
show
introduced by
The condition $this->getCookie(session_name()) === null is always false.
Loading history...
512
			return false;
513
		}
514
515
		return true;
516
	}
517
518
	/**
519
	 * Wrapper around session_get_cookie_params
520
	 *
521
	 * @return array
522
	 */
523
	public function getCookieParams(): array {
524
		return session_get_cookie_params();
525
	}
526
527
	/**
528
	 * Appends the __Host- prefix to the cookie if applicable
529
	 *
530
	 * @param string $name
531
	 * @return string
532
	 */
533
	protected function getProtectedCookieName(string $name): string {
534
		$cookieParams = $this->getCookieParams();
535
		$prefix = '';
536
		if($cookieParams['secure'] === true && $cookieParams['path'] === '/') {
537
			$prefix = '__Host-';
538
		}
539
540
		return $prefix.$name;
541
	}
542
543
	/**
544
	 * Checks if the strict cookie has been sent with the request if the request
545
	 * is including any cookies.
546
	 *
547
	 * @return bool
548
	 * @since 9.1.0
549
	 */
550
	public function passesStrictCookieCheck(): bool {
551
		if(!$this->cookieCheckRequired()) {
552
			return true;
553
		}
554
555
		$cookieName = $this->getProtectedCookieName('nc_sameSiteCookiestrict');
556
		if($this->getCookie($cookieName) === 'true'
557
			&& $this->passesLaxCookieCheck()) {
558
			return true;
559
		}
560
		return false;
561
	}
562
563
	/**
564
	 * Checks if the lax cookie has been sent with the request if the request
565
	 * is including any cookies.
566
	 *
567
	 * @return bool
568
	 * @since 9.1.0
569
	 */
570
	public function passesLaxCookieCheck(): bool {
571
		if(!$this->cookieCheckRequired()) {
572
			return true;
573
		}
574
575
		$cookieName = $this->getProtectedCookieName('nc_sameSiteCookielax');
576
		if($this->getCookie($cookieName) === 'true') {
577
			return true;
578
		}
579
		return false;
580
	}
581
582
583
	/**
584
	 * Returns an ID for the request, value is not guaranteed to be unique and is mostly meant for logging
585
	 * If `mod_unique_id` is installed this value will be taken.
586
	 * @return string
587
	 */
588
	public function getId(): string {
589
		if(isset($this->server['UNIQUE_ID'])) {
590
			return $this->server['UNIQUE_ID'];
591
		}
592
593
		if(empty($this->requestId)) {
594
			$validChars = ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS;
595
			$this->requestId = $this->secureRandom->generate(20, $validChars);
596
		}
597
598
		return $this->requestId;
599
	}
600
601
	/**
602
	 * Checks if given $remoteAddress matches given $trustedProxy.
603
	 * If $trustedProxy is an IPv4 IP range given in CIDR notation, true will be returned if
604
	 * $remoteAddress is an IPv4 address within that IP range.
605
	 * Otherwise $remoteAddress will be compared to $trustedProxy literally and the result
606
	 * will be returned.
607
	 * @return boolean true if $remoteAddress matches $trustedProxy, false otherwise
608
	 */
609
	protected function matchesTrustedProxy($trustedProxy, $remoteAddress) {
610
		$cidrre = '/^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\/([0-9]{1,2})$/';
611
612
		if (preg_match($cidrre, $trustedProxy, $match)) {
613
			$net = $match[1];
614
			$shiftbits = min(32, max(0, 32 - intval($match[2])));
615
			$netnum = ip2long($net) >> $shiftbits;
616
			$ipnum = ip2long($remoteAddress) >> $shiftbits;
617
618
			return $ipnum === $netnum;
619
		}
620
621
		return $trustedProxy === $remoteAddress;
622
	}
623
624
	/**
625
	 * Checks if given $remoteAddress matches any entry in the given array $trustedProxies.
626
	 * For details regarding what "match" means, refer to `matchesTrustedProxy`.
627
	 * @return boolean true if $remoteAddress matches any entry in $trustedProxies, false otherwise
628
	 */
629
	protected function isTrustedProxy($trustedProxies, $remoteAddress) {
630
		foreach ($trustedProxies as $tp) {
631
			if ($this->matchesTrustedProxy($tp, $remoteAddress)) {
632
				return true;
633
			}
634
		}
635
636
		return false;
637
	}
638
639
	/**
640
	 * Returns the remote address, if the connection came from a trusted proxy
641
	 * and `forwarded_for_headers` has been configured then the IP address
642
	 * specified in this header will be returned instead.
643
	 * Do always use this instead of $_SERVER['REMOTE_ADDR']
644
	 * @return string IP address
645
	 */
646
	public function getRemoteAddress(): string {
647
		$remoteAddress = isset($this->server['REMOTE_ADDR']) ? $this->server['REMOTE_ADDR'] : '';
648
		$trustedProxies = $this->config->getSystemValue('trusted_proxies', []);
649
650
		if(\is_array($trustedProxies) && $this->isTrustedProxy($trustedProxies, $remoteAddress)) {
651
			$forwardedForHeaders = $this->config->getSystemValue('forwarded_for_headers', [
652
				'HTTP_X_FORWARDED_FOR'
653
				// only have one default, so we cannot ship an insecure product out of the box
654
			]);
655
656
			foreach($forwardedForHeaders as $header) {
657
				if(isset($this->server[$header])) {
658
					foreach(explode(',', $this->server[$header]) as $IP) {
659
						$IP = trim($IP);
660
						if (filter_var($IP, FILTER_VALIDATE_IP) !== false) {
661
							return $IP;
662
						}
663
					}
664
				}
665
			}
666
		}
667
668
		return $remoteAddress;
669
	}
670
671
	/**
672
	 * Check overwrite condition
673
	 * @param string $type
674
	 * @return bool
675
	 */
676
	private function isOverwriteCondition(string $type = ''): bool {
677
		$regex = '/' . $this->config->getSystemValue('overwritecondaddr', '')  . '/';
678
		$remoteAddr = isset($this->server['REMOTE_ADDR']) ? $this->server['REMOTE_ADDR'] : '';
679
		return $regex === '//' || preg_match($regex, $remoteAddr) === 1
680
		|| $type !== 'protocol';
681
	}
682
683
	/**
684
	 * Returns the server protocol. It respects one or more reverse proxies servers
685
	 * and load balancers
686
	 * @return string Server protocol (http or https)
687
	 */
688
	public function getServerProtocol(): string {
689
		if($this->config->getSystemValue('overwriteprotocol') !== ''
690
			&& $this->isOverwriteCondition('protocol')) {
691
			return $this->config->getSystemValue('overwriteprotocol');
692
		}
693
694
		if (isset($this->server['HTTP_X_FORWARDED_PROTO'])) {
695
			if (strpos($this->server['HTTP_X_FORWARDED_PROTO'], ',') !== false) {
696
				$parts = explode(',', $this->server['HTTP_X_FORWARDED_PROTO']);
697
				$proto = strtolower(trim($parts[0]));
698
			} else {
699
				$proto = strtolower($this->server['HTTP_X_FORWARDED_PROTO']);
700
			}
701
702
			// Verify that the protocol is always HTTP or HTTPS
703
			// default to http if an invalid value is provided
704
			return $proto === 'https' ? 'https' : 'http';
705
		}
706
707
		if (isset($this->server['HTTPS'])
708
			&& $this->server['HTTPS'] !== null
709
			&& $this->server['HTTPS'] !== 'off'
710
			&& $this->server['HTTPS'] !== '') {
711
			return 'https';
712
		}
713
714
		return 'http';
715
	}
716
717
	/**
718
	 * Returns the used HTTP protocol.
719
	 *
720
	 * @return string HTTP protocol. HTTP/2, HTTP/1.1 or HTTP/1.0.
721
	 */
722
	public function getHttpProtocol(): string {
723
		$claimedProtocol = $this->server['SERVER_PROTOCOL'];
724
725
		if (\is_string($claimedProtocol)) {
726
			$claimedProtocol = strtoupper($claimedProtocol);
727
		}
728
729
		$validProtocols = [
730
			'HTTP/1.0',
731
			'HTTP/1.1',
732
			'HTTP/2',
733
		];
734
735
		if(\in_array($claimedProtocol, $validProtocols, true)) {
736
			return $claimedProtocol;
737
		}
738
739
		return 'HTTP/1.1';
740
	}
741
742
	/**
743
	 * Returns the request uri, even if the website uses one or more
744
	 * reverse proxies
745
	 * @return string
746
	 */
747
	public function getRequestUri(): string {
748
		$uri = isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : '';
749
		if($this->config->getSystemValue('overwritewebroot') !== '' && $this->isOverwriteCondition()) {
750
			$uri = $this->getScriptName() . substr($uri, \strlen($this->server['SCRIPT_NAME']));
751
		}
752
		return $uri;
753
	}
754
755
	/**
756
	 * Get raw PathInfo from request (not urldecoded)
757
	 * @throws \Exception
758
	 * @return string Path info
759
	 */
760
	public function getRawPathInfo(): string {
761
		$requestUri = isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : '';
762
		// remove too many leading slashes - can be caused by reverse proxy configuration
763
		if (strpos($requestUri, '/') === 0) {
764
			$requestUri = '/' . ltrim($requestUri, '/');
765
		}
766
767
		$requestUri = preg_replace('%/{2,}%', '/', $requestUri);
768
769
		// Remove the query string from REQUEST_URI
770
		if ($pos = strpos($requestUri, '?')) {
771
			$requestUri = substr($requestUri, 0, $pos);
772
		}
773
774
		$scriptName = $this->server['SCRIPT_NAME'];
775
		$pathInfo = $requestUri;
776
777
		// strip off the script name's dir and file name
778
		// FIXME: Sabre does not really belong here
779
		list($path, $name) = \Sabre\Uri\split($scriptName);
780
		if (!empty($path)) {
781
			if($path === $pathInfo || strpos($pathInfo, $path.'/') === 0) {
782
				$pathInfo = substr($pathInfo, \strlen($path));
783
			} else {
784
				throw new \Exception("The requested uri($requestUri) cannot be processed by the script '$scriptName')");
785
			}
786
		}
787
		if ($name === null) {
788
			$name = '';
789
		}
790
791
		if (strpos($pathInfo, '/'.$name) === 0) {
792
			$pathInfo = substr($pathInfo, \strlen($name) + 1);
793
		}
794
		if ($name !== '' && strpos($pathInfo, $name) === 0) {
795
			$pathInfo = substr($pathInfo, \strlen($name));
796
		}
797
		if($pathInfo === false || $pathInfo === '/'){
798
			return '';
799
		} else {
800
			return $pathInfo;
801
		}
802
	}
803
804
	/**
805
	 * Get PathInfo from request
806
	 * @throws \Exception
807
	 * @return string|false Path info or false when not found
808
	 */
809
	public function getPathInfo() {
810
		$pathInfo = $this->getRawPathInfo();
811
		// following is taken from \Sabre\HTTP\URLUtil::decodePathSegment
812
		$pathInfo = rawurldecode($pathInfo);
813
		$encoding = mb_detect_encoding($pathInfo, ['UTF-8', 'ISO-8859-1']);
814
815
		switch($encoding) {
816
			case 'ISO-8859-1' :
817
				$pathInfo = utf8_encode($pathInfo);
818
		}
819
		// end copy
820
821
		return $pathInfo;
822
	}
823
824
	/**
825
	 * Returns the script name, even if the website uses one or more
826
	 * reverse proxies
827
	 * @return string the script name
828
	 */
829
	public function getScriptName(): string {
830
		$name = $this->server['SCRIPT_NAME'];
831
		$overwriteWebRoot =  $this->config->getSystemValue('overwritewebroot');
832
		if ($overwriteWebRoot !== '' && $this->isOverwriteCondition()) {
833
			// FIXME: This code is untestable due to __DIR__, also that hardcoded path is really dangerous
834
			$serverRoot = str_replace('\\', '/', substr(__DIR__, 0, -\strlen('lib/private/appframework/http/')));
835
			$suburi = str_replace('\\', '/', substr(realpath($this->server['SCRIPT_FILENAME']), \strlen($serverRoot)));
836
			$name = '/' . ltrim($overwriteWebRoot . $suburi, '/');
837
		}
838
		return $name;
839
	}
840
841
	/**
842
	 * Checks whether the user agent matches a given regex
843
	 * @param array $agent array of agent names
844
	 * @return bool true if at least one of the given agent matches, false otherwise
845
	 */
846
	public function isUserAgent(array $agent): bool {
847
		if (!isset($this->server['HTTP_USER_AGENT'])) {
848
			return false;
849
		}
850
		foreach ($agent as $regex) {
851
			if (preg_match($regex, $this->server['HTTP_USER_AGENT'])) {
852
				return true;
853
			}
854
		}
855
		return false;
856
	}
857
858
	/**
859
	 * Returns the unverified server host from the headers without checking
860
	 * whether it is a trusted domain
861
	 * @return string Server host
862
	 */
863
	public function getInsecureServerHost(): string {
864
		$host = 'localhost';
865
		if (isset($this->server['HTTP_X_FORWARDED_HOST'])) {
866
			if (strpos($this->server['HTTP_X_FORWARDED_HOST'], ',') !== false) {
867
				$parts = explode(',', $this->server['HTTP_X_FORWARDED_HOST']);
868
				$host = trim(current($parts));
869
			} else {
870
				$host = $this->server['HTTP_X_FORWARDED_HOST'];
871
			}
872
		} else {
873
			if (isset($this->server['HTTP_HOST'])) {
874
				$host = $this->server['HTTP_HOST'];
875
			} else if (isset($this->server['SERVER_NAME'])) {
876
				$host = $this->server['SERVER_NAME'];
877
			}
878
		}
879
		return $host;
880
	}
881
882
883
	/**
884
	 * Returns the server host from the headers, or the first configured
885
	 * trusted domain if the host isn't in the trusted list
886
	 * @return string Server host
887
	 */
888
	public function getServerHost(): string {
889
		// overwritehost is always trusted
890
		$host = $this->getOverwriteHost();
891
		if ($host !== null) {
892
			return $host;
893
		}
894
895
		// get the host from the headers
896
		$host = $this->getInsecureServerHost();
897
898
		// Verify that the host is a trusted domain if the trusted domains
899
		// are defined
900
		// If no trusted domain is provided the first trusted domain is returned
901
		$trustedDomainHelper = new TrustedDomainHelper($this->config);
902
		if ($trustedDomainHelper->isTrustedDomain($host)) {
903
			return $host;
904
		} else {
905
			$trustedList = $this->config->getSystemValue('trusted_domains', []);
906
			if(!empty($trustedList)) {
907
				return $trustedList[0];
908
			} else {
909
				return '';
910
			}
911
		}
912
	}
913
914
	/**
915
	 * Returns the overwritehost setting from the config if set and
916
	 * if the overwrite condition is met
917
	 * @return string|null overwritehost value or null if not defined or the defined condition
918
	 * isn't met
919
	 */
920
	private function getOverwriteHost() {
921
		if($this->config->getSystemValue('overwritehost') !== '' && $this->isOverwriteCondition()) {
922
			return $this->config->getSystemValue('overwritehost');
923
		}
924
		return null;
925
	}
926
927
}
928