Completed
Pull Request — master (#447)
by Marcel
02:52 queued 01:28
created

Controller::handleRequest()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 30
rs 9.44
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
namespace BeyondCode\LaravelWebSockets\HttpApi\Controllers;
4
5
use BeyondCode\LaravelWebSockets\Apps\App;
6
use BeyondCode\LaravelWebSockets\QueryParameters;
7
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
8
use Exception;
9
use GuzzleHttp\Psr7\Response;
10
use GuzzleHttp\Psr7\ServerRequest;
11
use Illuminate\Http\JsonResponse;
12
use Illuminate\Http\Request;
13
use Illuminate\Support\Arr;
14
use Illuminate\Support\Collection;
15
use Psr\Http\Message\RequestInterface;
16
use Pusher\Pusher;
17
use Ratchet\ConnectionInterface;
18
use Ratchet\Http\HttpServerInterface;
19
use React\Promise\PromiseInterface;
20
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
21
use Symfony\Component\HttpKernel\Exception\HttpException;
22
23
abstract class Controller implements HttpServerInterface
24
{
25
    /** @var string */
26
    protected $requestBuffer = '';
27
28
    /** @var RequestInterface */
29
    protected $request;
30
31
    /** @var int */
32
    protected $contentLength;
33
34
    /** @var ChannelManager */
35
    protected $channelManager;
36
37
    public function __construct(ChannelManager $channelManager)
38
    {
39
        $this->channelManager = $channelManager;
40
    }
41
42
    public function onOpen(ConnectionInterface $connection, RequestInterface $request = null)
43
    {
44
        $this->request = $request;
45
46
        $this->contentLength = $this->findContentLength($request->getHeaders());
0 ignored issues
show
Bug introduced by
It seems like $request is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
47
48
        $this->requestBuffer = (string) $request->getBody();
49
50
        if (! $this->verifyContentLength()) {
51
            return;
52
        }
53
54
        $this->handleRequest($connection);
55
    }
56
57
    protected function findContentLength(array $headers): int
58
    {
59
        return Collection::make($headers)->first(function ($values, $header) {
60
            return strtolower($header) === 'content-length';
61
        })[0] ?? 0;
62
    }
63
64
    public function onMessage(ConnectionInterface $from, $msg)
65
    {
66
        $this->requestBuffer .= $msg;
67
68
        if (! $this->verifyContentLength()) {
69
            return;
70
        }
71
72
        $this->handleRequest($from);
73
    }
74
75
    protected function verifyContentLength()
76
    {
77
        return strlen($this->requestBuffer) === $this->contentLength;
78
    }
79
80
    protected function handleRequest(ConnectionInterface $connection)
81
    {
82
        $serverRequest = (new ServerRequest(
83
            $this->request->getMethod(),
84
            $this->request->getUri(),
85
            $this->request->getHeaders(),
86
            $this->requestBuffer,
87
            $this->request->getProtocolVersion()
88
        ))->withQueryParams(QueryParameters::create($this->request)->all());
89
90
        $laravelRequest = Request::createFromBase((new HttpFoundationFactory)->createRequest($serverRequest));
91
92
        $this
93
            ->ensureValidAppId($laravelRequest->appId)
94
            ->ensureValidSignature($laravelRequest);
95
96
        // Invoke the controller action
97
        $response = $this($laravelRequest);
98
99
        // Allow for async IO in the controller action
100
        if ($response instanceof PromiseInterface) {
101
            $response->then(function ($response) use ($connection) {
102
                $this->sendAndClose($connection, $response);
103
            });
104
105
            return;
106
        }
107
108
        $this->sendAndClose($connection, $response);
109
    }
110
111
    protected function sendAndClose(ConnectionInterface $connection, $response)
112
    {
113
        $connection->send(JsonResponse::create($response));
0 ignored issues
show
Deprecated Code introduced by
The method Symfony\Component\HttpFo...\JsonResponse::create() has been deprecated with message: since Symfony 5.1, use __construct() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
114
        $connection->close();
115
    }
116
117
    public function onClose(ConnectionInterface $connection)
118
    {
119
    }
120
121
    public function onError(ConnectionInterface $connection, Exception $exception)
122
    {
123
        if (! $exception instanceof HttpException) {
124
            return;
125
        }
126
127
        $response = new Response($exception->getStatusCode(), [
128
            'Content-Type' => 'application/json',
129
        ], json_encode([
130
            'error' => $exception->getMessage(),
131
        ]));
132
133
        $connection->send(\GuzzleHttp\Psr7\str($response));
134
135
        $connection->close();
136
    }
137
138
    public function ensureValidAppId(string $appId)
139
    {
140
        if (! App::findById($appId)) {
141
            throw new HttpException(401, "Unknown app id `{$appId}` provided.");
142
        }
143
144
        return $this;
145
    }
146
147
    protected function ensureValidSignature(Request $request)
148
    {
149
        /*
150
         * The `auth_signature` & `body_md5` parameters are not included when calculating the `auth_signature` value.
151
         *
152
         * The `appId`, `appKey` & `channelName` parameters are actually route parameters and are never supplied by the client.
153
         */
154
        $params = Arr::except($request->query(), ['auth_signature', 'body_md5', 'appId', 'appKey', 'channelName']);
0 ignored issues
show
Bug introduced by
It seems like $request->query() targeting Illuminate\Http\Concerns...ractsWithInput::query() can also be of type null or string; however, Illuminate\Support\Arr::except() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
155
156
        if ($request->getContent() !== '') {
157
            $params['body_md5'] = md5($request->getContent());
158
        }
159
160
        ksort($params);
161
162
        $signature = "{$request->getMethod()}\n/{$request->path()}\n".Pusher::array_implode('=', '&', $params);
163
164
        $authSignature = hash_hmac('sha256', $signature, App::findById($request->get('appId'))->secret);
165
166
        if ($authSignature !== $request->get('auth_signature')) {
167
            throw new HttpException(401, 'Invalid auth signature provided.');
168
        }
169
170
        return $this;
171
    }
172
173
    abstract public function __invoke(Request $request);
174
}
175