Passed
Push — master ( 623058...f84931 )
by Caen
04:14 queued 13s
created

BaseController::isRequestMadeFromLocalhost()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 11
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Hyde\RealtimeCompiler\Http;
6
7
use Desilva\Microserve\Request;
8
use Desilva\Microserve\Response;
9
use Desilva\Microserve\JsonResponse;
10
use Hyde\RealtimeCompiler\ConsoleOutput;
11
use Symfony\Component\HttpKernel\Exception\HttpException;
12
13
/**
14
 * @internal This class is not intended to be edited outside the Hyde Realtime Compiler.
15
 */
16
abstract class BaseController
17
{
18
    protected Request $request;
19
    protected ConsoleOutput $console;
20
    protected bool $withConsoleOutput = false;
21
    protected bool $withSession = false;
22
23
    abstract public function handle(): Response;
24
25
    public function __construct(?Request $request = null)
26
    {
27
        $this->request = $request ?? Request::capture();
28
29
        if ($this->withConsoleOutput && ((bool) env('HYDE_SERVER_REQUEST_OUTPUT', false)) === true) {
30
            $this->console = new ConsoleOutput();
31
        }
32
33
        if ($this->withSession) {
34
            session_start();
35
        }
36
    }
37
38
    protected function sendJsonErrorResponse(int $statusCode, string $message): JsonResponse
39
    {
40
        return new JsonResponse($statusCode, $this->matchStatusCode($statusCode), [
41
            'error' => $message,
42
        ]);
43
    }
44
45
    protected function abort(int $code, string $message): never
46
    {
47
        throw new HttpException($code, $message);
48
    }
49
50
    protected function matchStatusCode(int $statusCode): string
51
    {
52
        return match ($statusCode) {
53
            200 => 'OK',
54
            201 => 'Created',
55
            400 => 'Bad Request',
56
            403 => 'Forbidden',
57
            404 => 'Not Found',
58
            409 => 'Conflict',
59
            default => 'Internal Server Error',
60
        };
61
    }
62
63
    protected function authorizePostRequest(): void
64
    {
65
        if (! $this->isRequestMadeFromLocalhost()) {
66
            throw new HttpException(403, "Refusing to serve request from address {$_SERVER['REMOTE_ADDR']} (must be on localhost)");
67
        }
68
69
        if ($this->withSession) {
70
            if (! $this->validateCSRFToken($this->request->get('_token'))) {
71
                throw new HttpException(403, 'Invalid CSRF token');
72
            }
73
        }
74
    }
75
76
    protected function isRequestMadeFromLocalhost(): bool
77
    {
78
        // As the dashboard is not password-protected, and it can make changes to the file system,
79
        // we block any requests that are not coming from the host machine. While we are clear
80
        // in the documentation that the realtime compiler should only be used for local
81
        // development, we still want to be extra careful in case someone forgets.
82
83
        $requestIp = $_SERVER['REMOTE_ADDR'];
84
        $allowedIps = ['::1', '127.0.0.1', 'localhost'];
85
86
        return in_array($requestIp, $allowedIps, true);
87
    }
88
89
    protected function generateCSRFToken(): string
90
    {
91
        if (empty($_SESSION['csrf_token'])) {
92
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
93
        }
94
95
        return $_SESSION['csrf_token'];
96
    }
97
98
    protected function validateCSRFToken(?string $suppliedToken): bool
99
    {
100
        if ($suppliedToken === null || empty($_SESSION['csrf_token'])) {
101
            return false;
102
        }
103
104
        return hash_equals($_SESSION['csrf_token'], $suppliedToken);
105
    }
106
107
    protected function writeToConsole(string $message, string $context = 'dashboard'): void
108
    {
109
        if (isset($this->console)) {
110
            $this->console->printMessage($message, $context);
111
        }
112
    }
113
114
    protected function expectsJson(): bool
115
    {
116
        return array_change_key_case(getallheaders())['accept'] === 'application/json';
0 ignored issues
show
Bug introduced by
It seems like getallheaders() can also be of type true; however, parameter $array of array_change_key_case() does only seem to accept 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

116
        return array_change_key_case(/** @scrutinizer ignore-type */ getallheaders())['accept'] === 'application/json';
Loading history...
117
    }
118
}
119