Passed
Push — devel-3.0 ( af8b21...5a06ca )
by Rubén
03:22
created

Request::analyzeUnsafeString()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 2
dl 0
loc 7
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-2018, 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 Klein\DataCollection\DataCollection;
28
use SP\Bootstrap;
29
use SP\Core\Crypt\CryptPKI;
30
use SP\Core\Crypt\Hash;
31
use SP\Core\Exceptions\SPException;
32
use SP\Util\Filter;
33
use SP\Util\Util;
34
35
/**
36
 * Clase Request para la gestión de peticiones HTTP
37
 *
38
 * @package SP
39
 */
40
final class Request
41
{
42
    /**
43
     * @var array Directorios seguros para include
44
     */
45
    const SECURE_DIRS = ['css', 'js'];
46
    /**
47
     * @var \Klein\DataCollection\HeaderDataCollection
48
     */
49
    protected $headers;
50
    /**
51
     * @var \Klein\Request
52
     */
53
    private $request;
54
    /**
55
     * @var DataCollection
56
     */
57
    private $params;
58
    /**
59
     * @var string
60
     */
61
    private $method;
62
    /**
63
     * @var bool
64
     */
65
    private $https;
66
67
    /**
68
     * Request constructor.
69
     *
70
     * @param \Klein\Request $request
71
     */
72
    public function __construct(\Klein\Request $request)
73
    {
74
        $this->request = $request;
75
        $this->headers = $this->request->headers();
76
        $this->params = $this->getParamsByMethod();
77
        $this->detectHttps();
78
    }
79
80
    /**
81
     * @return DataCollection
82
     */
83
    private function getParamsByMethod()
84
    {
85
        if ($this->request->method('GET')) {
86
            $this->method = 'GET';
87
            return $this->request->paramsGet();
88
        } else {
89
            $this->method = 'POST';
90
            return $this->request->paramsPost();
91
        }
92
    }
93
94
    /**
95
     * Detects if the connection is done through HTTPS
96
     */
97
    private function detectHttps()
98
    {
99
        $this->https = ($this->request->server()->exists('HTTPS') && $this->request->server()->get('HTTPS') !== 'off')
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: $this->https = ($this->r...RVER_PORT', 0) === 443), Probably Intended Meaning: $this->https = $this->re...RVER_PORT', 0) === 443)
Loading history...
100
            || $this->request->server()->get('SERVER_PORT', 0) === 443;
101
    }
102
103
    /**
104
     * Devuelve un nombre de archivo seguro
105
     *
106
     * @param string $file
107
     * @param string $base
108
     *
109
     * @return string
110
     */
111
    public static function getSecureAppFile(string $file, string $base = null)
112
    {
113
        return basename(self::getSecureAppPath($file, $base));
114
    }
115
116
    /**
117
     * Devolver una ruta segura para
118
     *
119
     * @param string $path
120
     * @param string $base
121
     *
122
     * @return string
123
     */
124
    public static function getSecureAppPath(string $path, string $base = null)
125
    {
126
        if ($base === null) {
127
            $base = APP_ROOT;
128
        } elseif (!in_array(basename($base), self::SECURE_DIRS, true)) {
129
            return '';
130
        }
131
132
        $realPath = realpath($base . DIRECTORY_SEPARATOR . $path);
133
134
        if ($realPath === false
135
            || strpos($realPath, $base) !== 0
136
        ) {
137
            return '';
138
        }
139
140
        return $realPath;
141
    }
142
143
    /**
144
     * @param bool $fullForwarded
145
     *
146
     * @return array|string
147
     */
148
    public function getClientAddress(bool $fullForwarded = false)
149
    {
150
        if (APP_MODULE === 'tests') {
0 ignored issues
show
introduced by
The condition SP\Http\APP_MODULE === 'tests' is always false.
Loading history...
151
            return '127.0.0.1';
152
        }
153
154
        if (($forwarded = $this->getForwardedFor()) !== null) {
155
            return $fullForwarded ? implode(',', $forwarded) : $forwarded[0];
156
        }
157
158
        return $this->request->server()->get('REMOTE_ADDR', '');
159
    }
160
161
    /**
162
     * @return string[]|null
163
     */
164
    public function getForwardedFor()
165
    {
166
        // eg: Forwarded: by=<identifier>; for=<identifier>; host=<host>; proto=<http|https>
167
        if (($forwarded = $this->headers->get('HTTP_FORWARDED')) !== null &&
168
            preg_match_all('/(?:for=([\w.:]+))|(?:for="\[([\w.:]+)\]")/i',
169
                $forwarded, $matches)
170
        ) {
171
            return array_filter(array_merge($matches[1], $matches[2]), function ($value) {
172
                return !empty($value);
173
            });
174
        }
175
176
        // eg: X-Forwarded-For: 192.0.2.43, 2001:db8:cafe::17
177
        if (($xForwarded = $this->headers->exists('HTTP_X_FORWARDED_FOR')) !== null) {
0 ignored issues
show
introduced by
The condition $xForwarded = $this->hea...ORWARDED_FOR') !== null is always true.
Loading history...
178
            $matches = preg_split('/(?<=[\w])+,\s?/i',
179
                $xForwarded,
180
                -1,
181
                PREG_SPLIT_NO_EMPTY);
182
183
            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

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

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