Request::detectHttps()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * sysPass
4
 *
5
 * @author    nuxsmin
6
 * @link      https://syspass.org
7
 * @copyright 2012-2019, Rubén Domínguez nuxsmin@$syspass.org
8
 *
9
 * This file is part of sysPass.
10
 *
11
 * sysPass is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU General Public License as published by
13
 * the Free Software Foundation, either version 3 of the License, or
14
 * (at your option) any later version.
15
 *
16
 * sysPass is distributed in the hope that it will be useful,
17
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
 * GNU General Public License for more details.
20
 *
21
 * You should have received a copy of the GNU General Public License
22
 *  along with sysPass.  If not, see <http://www.gnu.org/licenses/>.
23
 */
24
25
namespace SP\Http;
26
27
use Exception;
28
use Klein\DataCollection\DataCollection;
29
use Klein\DataCollection\HeaderDataCollection;
30
use SP\Bootstrap;
31
use SP\Core\Crypt\CryptPKI;
32
use SP\Core\Crypt\Hash;
33
use SP\Core\Exceptions\SPException;
34
use SP\Util\Filter;
35
use SP\Util\Util;
36
37
/**
38
 * Clase Request para la gestión de peticiones HTTP
39
 *
40
 * @package SP
41
 */
42
final class Request
43
{
44
    /**
45
     * @var array Directorios seguros para include
46
     */
47
    const SECURE_DIRS = ['css', 'js'];
48
    /**
49
     * @var HeaderDataCollection
50
     */
51
    private $headers;
52
    /**
53
     * @var \Klein\Request
54
     */
55
    private $request;
56
    /**
57
     * @var DataCollection
58
     */
59
    private $params;
60
    /**
61
     * @var string
62
     */
63
    private $method;
64
    /**
65
     * @var bool
66
     */
67
    private $https;
68
69
    /**
70
     * Request constructor.
71
     *
72
     * @param \Klein\Request $request
73
     */
74
    public function __construct(\Klein\Request $request)
75
    {
76
        $this->request = $request;
77
        $this->headers = $this->request->headers();
78
        $this->params = $this->getParamsByMethod();
79
        $this->detectHttps();
80
    }
81
82
    /**
83
     * @return DataCollection
84
     */
85
    private function getParamsByMethod()
86
    {
87
        if ($this->request->method('GET')) {
88
            $this->method = 'GET';
89
            return $this->request->paramsGet();
90
        } else {
91
            $this->method = 'POST';
92
            return $this->request->paramsPost();
93
        }
94
    }
95
96
    /**
97
     * Detects if the connection is done through HTTPS
98
     */
99
    private function detectHttps()
100
    {
101
        $this->https = Util::boolval($this->request->server()->get('HTTPS', 'off'))
102
            || $this->request->server()->get('SERVER_PORT', 0) === 443;
103
    }
104
105
    /**
106
     * Devuelve un nombre de archivo seguro
107
     *
108
     * @param string $file
109
     * @param string $base
110
     *
111
     * @return string
112
     */
113
    public static function getSecureAppFile(string $file, string $base = null)
114
    {
115
        return basename(self::getSecureAppPath($file, $base));
116
    }
117
118
    /**
119
     * Devolver una ruta segura para
120
     *
121
     * @param string $path
122
     * @param string $base
123
     *
124
     * @return string
125
     */
126
    public static function getSecureAppPath(string $path, string $base = null)
127
    {
128
        if ($base === null) {
129
            $base = APP_ROOT;
130
        } elseif (!in_array(basename($base), self::SECURE_DIRS, true)) {
131
            return '';
132
        }
133
134
        $realPath = realpath($base . DIRECTORY_SEPARATOR . $path);
135
136
        if ($realPath === false
137
            || strpos($realPath, $base) !== 0
138
        ) {
139
            return '';
140
        }
141
142
        return $realPath;
143
    }
144
145
    /**
146
     * @param bool $fullForwarded
147
     *
148
     * @return array|string
149
     */
150
    public function getClientAddress(bool $fullForwarded = false)
151
    {
152
        if (APP_MODULE === 'tests') {
0 ignored issues
show
introduced by
The condition SP\Http\APP_MODULE === 'tests' is always false.
Loading history...
153
            return '127.0.0.1';
154
        }
155
156
        $forwarded = $this->getForwardedFor();
157
158
        if ($forwarded !== null) {
159
            return $fullForwarded ? implode(',', $forwarded) : $forwarded[0];
160
        }
161
162
        return $this->request->server()->get('REMOTE_ADDR', '');
163
    }
164
165
    /**
166
     * @return string[]|null
167
     */
168
    public function getForwardedFor()
169
    {
170
        // eg: Forwarded: by=<identifier>; for=<identifier>; host=<host>; proto=<http|https>
171
        $forwarded = $this->headers->get('HTTP_FORWARDED');
172
173
        if ($forwarded !== null &&
174
            preg_match_all('/(?:for=([\w.:]+))|(?:for="\[([\w.:]+)\]")/i',
175
                $forwarded, $matches)
176
        ) {
177
            return array_filter(array_merge($matches[1], $matches[2]), function ($value) {
178
                return !empty($value);
179
            });
180
        }
181
182
        // eg: X-Forwarded-For: 192.0.2.43, 2001:db8:cafe::17
183
        $xForwarded = $this->headers->exists('HTTP_X_FORWARDED_FOR');
184
185
        if ($xForwarded !== null) {
186
            $matches = preg_split('/(?<=[\w])+,\s?/i',
187
                $xForwarded,
188
                -1,
189
                PREG_SPLIT_NO_EMPTY);
190
191
            if (count($matches) > 0) {
0 ignored issues
show
Bug introduced by
It seems like $matches can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

191
            if (count(/** @scrutinizer ignore-type */ $matches) > 0) {
Loading history...
192
                return $matches;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $matches returns the type false which is incompatible with the documented return type null|string[].
Loading history...
193
            }
194
        }
195
196
        return null;
197
    }
198
199
    /**
200
     * Comprobar si se realiza una recarga de la página
201
     *
202
     * @return bool
203
     */
204
    public function checkReload()
205
    {
206
        return $this->headers->get('Cache-Control') === 'max-age=0';
207
    }
208
209
    /**
210
     * @param string $param
211
     * @param string $default
212
     *
213
     * @return string|null
214
     */
215
    public function analyzeEmail(string $param, string $default = null)
216
    {
217
        if (!$this->params->exists($param)) {
218
            return $default;
219
        }
220
221
        return Filter::getEmail($this->params->get($param));
222
    }
223
224
    /**
225
     * Analizar un valor encriptado y devolverlo desencriptado
226
     *
227
     * @param string $param
228
     *
229
     * @return string
230
     */
231
    public function analyzeEncrypted(string $param)
232
    {
233
        $encryptedData = $this->analyzeString($param);
234
235
        if ($encryptedData === null) {
236
            return '';
237
        }
238
239
        try {
240
            // Desencriptar con la clave RSA
241
            $clearData = Bootstrap::getContainer()->get(CryptPKI::class)
242
                ->decryptRSA(base64_decode($encryptedData));
243
244
            // Desencriptar con la clave RSA
245
            if ($clearData === false) {
246
                logger('No RSA encrypted data from request');
247
248
                return $encryptedData;
249
            }
250
251
            return $clearData;
252
        } catch (Exception $e) {
253
            processException($e);
254
255
            return $encryptedData;
256
        }
257
    }
258
259
    /**
260
     * @param $param
261
     * @param $default
262
     *
263
     * @return string|null
264
     */
265
    public function analyzeString(string $param, string $default = null)
266
    {
267
        if (!$this->params->exists($param)) {
268
            return $default;
269
        }
270
271
        return Filter::getString($this->params->get($param));
272
    }
273
274
    /**
275
     * @param $param
276
     * @param $default
277
     *
278
     * @return string|null
279
     */
280
    public function analyzeUnsafeString(string $param, string $default = null)
281
    {
282
        if (!$this->params->exists($param)) {
283
            return $default;
284
        }
285
286
        return Filter::getRaw($this->params->get($param));
287
    }
288
289
    /**
290
     * @param string        $param
291
     * @param callable|null $mapper
292
     * @param mixed         $default
293
     *
294
     * @return array|null
295
     */
296
    public function analyzeArray(string $param, callable $mapper = null, $default = null)
297
    {
298
        $requestValue = $this->params->get($param);
299
300
        if ($requestValue !== null
301
            && is_array($requestValue)
302
        ) {
303
            if (is_callable($mapper)) {
304
                return $mapper($requestValue);
305
            }
306
307
            return array_map(function ($value) {
308
                return is_numeric($value) ? Filter::getInt($value) : Filter::getString($value);
309
            }, $requestValue);
310
        }
311
312
        return $default;
313
    }
314
315
    /**
316
     * Comprobar si la petición es en formato JSON
317
     *
318
     * @return bool
319
     */
320
    public function isJson()
321
    {
322
        return strpos($this->headers->get('Accept'), 'application/json') !== false;
323
    }
324
325
    /**
326
     * Comprobar si la petición es Ajax
327
     *
328
     * @return bool
329
     */
330
    public function isAjax()
331
    {
332
        return $this->headers->get('X-Requested-With') === 'XMLHttpRequest'
333
            || $this->analyzeInt('isAjax', 0) === 1;
334
    }
335
336
    /**
337
     * @param string $param
338
     * @param int    $default
339
     *
340
     * @return int
341
     */
342
    public function analyzeInt(string $param, int $default = null)
343
    {
344
        if (!$this->params->exists($param)) {
345
            return $default;
346
        }
347
348
        return Filter::getInt($this->params->get($param));
349
    }
350
351
    /**
352
     * @param string $file
353
     *
354
     * @return array|null
355
     */
356
    public function getFile(string $file)
357
    {
358
        return $this->request->files()->get($file);
359
    }
360
361
    /**
362
     * @param string $param
363
     * @param bool   $default
364
     *
365
     * @return bool
366
     */
367
    public function analyzeBool(string $param, bool $default = null)
368
    {
369
        if (!$this->params->exists($param)) {
370
            return (bool)$default;
371
        }
372
373
        return Util::boolval($this->params->get($param));
374
    }
375
376
    /**
377
     * @param string $key
378
     * @param string $param Checks the signature only for the given param
379
     *
380
     * @throws SPException
381
     */
382
    public function verifySignature(string $key, string $param = null)
383
    {
384
        $result = false;
385
        $hash = $this->params->get('h');
386
387
        if ($hash !== null) {
388
            // Strips out the hash param from the URI to get the
389
            // route which will be checked against the computed HMAC
390
            if ($param === null) {
391
                $uri = str_replace('&h=' . $hash, '', $this->request->uri());
392
                $uri = substr($uri, strpos($uri, '?') + 1);
393
            } else {
394
                $uri = $this->params->get($param, '');
395
            }
396
397
            $result = Hash::checkMessage($uri, $key, $hash);
398
        }
399
400
        if ($result === false) {
401
            throw new SPException(
402
                'URI string altered',
403
                SPException::ERROR,
404
                null,
405
                1
406
            );
407
        }
408
    }
409
410
    /**
411
     * Returns the URI used by the browser and checks for the protocol used
412
     *
413
     * @see https://tools.ietf.org/html/rfc7239#section-7.5
414
     * @return string
415
     */
416
    public function getHttpHost(): string
417
    {
418
        $forwarded = $this->getForwardedData();
419
420
        // Check in style of RFC 7239
421
        if (null !== $forwarded) {
422
            return strtolower($forwarded['proto'] . '://' . $forwarded['host']);
0 ignored issues
show
Bug introduced by
Are you sure $forwarded['host'] of type mixed|null|string[] can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

422
            return strtolower($forwarded['proto'] . '://' . /** @scrutinizer ignore-type */ $forwarded['host']);
Loading history...
423
        }
424
425
        $xForward = $this->getXForwardedData();
426
427
        // Check (deprecated) de facto standard
428
        if (null !== $xForward) {
429
            return strtolower($xForward['proto'] . '://' . $xForward['host']);
430
        }
431
432
        $protocol = 'http://';
433
434
        // We got called directly
435
        if ($this->https) {
436
            $protocol = 'https://';
437
        }
438
439
        return $protocol . $this->request->server()->get('HTTP_HOST');
440
    }
441
442
    /**
443
     * Devolver datos de forward RFC 7239
444
     *
445
     * @see https://tools.ietf.org/html/rfc7239#section-7.5
446
     * @return array|null
447
     */
448
    public function getForwardedData()
449
    {
450
        $forwarded = $this->getHeader('HTTP_FORWARDED');
451
452
        // Check in style of RFC 7239
453
        if (!empty($forwarded)
454
            && preg_match('/proto=(\w+);/i', $forwarded, $matchesProto)
455
            && preg_match('/host=(\w+);/i', $forwarded, $matchesHost)
456
        ) {
457
            $data = [
458
                'host ' => $matchesHost[0],
459
                'proto' => $matchesProto[0],
460
                'for' => $this->getForwardedFor()
461
            ];
462
463
            // Check if protocol and host are not empty
464
            if (!empty($data['proto']) && !empty($data['host'])) {
465
                return $data;
466
            }
467
        }
468
469
        return null;
470
    }
471
472
    /**
473
     * @param string $header
474
     *
475
     * @return string
476
     */
477
    public function getHeader(string $header): string
478
    {
479
        return $this->headers->get($header, '');
480
    }
481
482
    /**
483
     * Devolver datos de x-forward
484
     *
485
     * @return array|null
486
     */
487
    public function getXForwardedData()
488
    {
489
        $forwardedHost = $this->getHeader('HTTP_X_FORWARDED_HOST');
490
        $forwardedProto = $this->getHeader('HTTP_X_FORWARDED_PROTO');
491
492
        // Check (deprecated) de facto standard
493
        if (!empty($forwardedHost) && !empty($forwardedProto)) {
494
            $data = [
495
                'host' => trim(str_replace('"', '', $forwardedHost)),
496
                'proto' => trim(str_replace('"', '', $forwardedProto)),
497
                'for' => $this->getForwardedFor()
498
            ];
499
500
            // Check if protocol and host are not empty
501
            if (!empty($data['host']) && !empty($data['proto'])) {
502
                return $data;
503
            }
504
        }
505
506
        return null;
507
    }
508
509
    /**
510
     * @return string
511
     */
512
    public function getMethod(): string
513
    {
514
        return $this->method;
515
    }
516
517
    /**
518
     * @return bool
519
     */
520
    public function isHttps(): bool
521
    {
522
        return $this->https;
523
    }
524
525
    /**
526
     * @return int
527
     */
528
    public function getServerPort(): int
529
    {
530
        return (int)$this->request->server()->get('SERVER_PORT', 80);
531
    }
532
533
    /**
534
     * @return \Klein\Request
535
     */
536
    public function getRequest(): \Klein\Request
537
    {
538
        return $this->request;
539
    }
540
541
    /**
542
     * @param string $key
543
     *
544
     * @return string
545
     */
546
    public function getServer(string $key): string
547
    {
548
        return (string)$this->request->server()->get($key, '');
549
    }
550
}