Controller   A
last analyzed

Complexity

Total Complexity 20

Size/Duplication

Total Lines 256
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 60
c 1
b 0
f 0
dl 0
loc 256
rs 10
wmc 20

11 Methods

Rating   Name   Duplication   Size   Complexity  
A onError() 0 13 2
A onOpen() 0 13 2
A ensureValidSignature() 0 24 3
A ensureValidAppId() 0 7 3
A sendAndClose() 0 3 1
A onMessage() 0 9 2
A handleRequest() 0 32 3
A __construct() 0 3 1
A findContentLength() 0 5 1
A verifyContentLength() 0 3 1
A onClose() 0 2 1
1
<?php
2
3
namespace BeyondCode\LaravelWebSockets\API;
4
5
use BeyondCode\LaravelWebSockets\Apps\App;
6
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
7
use BeyondCode\LaravelWebSockets\Server\QueryParameters;
8
use Exception;
9
use GuzzleHttp\Psr7\Message;
10
use GuzzleHttp\Psr7\Response;
11
use GuzzleHttp\Psr7\ServerRequest;
12
use Illuminate\Http\JsonResponse;
13
use Illuminate\Http\Request;
14
use Illuminate\Support\Arr;
15
use Illuminate\Support\Collection;
16
use Psr\Http\Message\RequestInterface;
17
use Pusher\Pusher;
18
use Ratchet\ConnectionInterface;
19
use Ratchet\Http\HttpServerInterface;
20
use React\Promise\PromiseInterface;
21
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
22
use Symfony\Component\HttpKernel\Exception\HttpException;
23
24
abstract class Controller implements HttpServerInterface
25
{
26
    /**
27
     * The request buffer.
28
     *
29
     * @var string
30
     */
31
    protected $requestBuffer = '';
32
33
    /**
34
     * The incoming request.
35
     *
36
     * @var \Psr\Http\Message\RequestInterface
37
     */
38
    protected $request;
39
40
    /**
41
     * The content length that will
42
     * be calculated.
43
     *
44
     * @var int
45
     */
46
    protected $contentLength;
47
48
    /**
49
     * The channel manager.
50
     *
51
     * @var \BeyondCode\LaravelWebSockets\Contracts\ChannelManager
52
     */
53
    protected $channelManager;
54
55
    /**
56
     * The app attached with this request.
57
     *
58
     * @var \BeyondCode\LaravelWebSockets\Apps\App|null
59
     */
60
    protected $app;
61
62
    /**
63
     * Initialize the request.
64
     *
65
     * @param  ChannelManager  $channelManager
66
     * @return void
67
     */
68
    public function __construct(ChannelManager $channelManager)
69
    {
70
        $this->channelManager = $channelManager;
71
    }
72
73
    /**
74
     * Handle the opened socket connection.
75
     *
76
     * @param  \Ratchet\ConnectionInterface  $connection
77
     * @param  \Psr\Http\Message\RequestInterface  $request
78
     * @return void
79
     */
80
    public function onOpen(ConnectionInterface $connection, RequestInterface $request = null)
81
    {
82
        $this->request = $request;
83
84
        $this->contentLength = $this->findContentLength($request->getHeaders());
0 ignored issues
show
Bug introduced by
The method getHeaders() does not exist on null. ( Ignorable by Annotation )

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

84
        $this->contentLength = $this->findContentLength($request->/** @scrutinizer ignore-call */ getHeaders());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
85
86
        $this->requestBuffer = (string) $request->getBody();
87
88
        if (! $this->verifyContentLength()) {
89
            return;
90
        }
91
92
        $this->handleRequest($connection);
93
    }
94
95
    /**
96
     * Handle the oncoming message and add it to buffer.
97
     *
98
     * @param  \Ratchet\ConnectionInterface  $from
99
     * @param  mixed  $msg
100
     * @return void
101
     */
102
    public function onMessage(ConnectionInterface $from, $msg)
103
    {
104
        $this->requestBuffer .= $msg;
105
106
        if (! $this->verifyContentLength()) {
107
            return;
108
        }
109
110
        $this->handleRequest($from);
111
    }
112
113
    /**
114
     * Handle the socket closing.
115
     *
116
     * @param  \Ratchet\ConnectionInterface  $connection
117
     * @return void
118
     */
119
    public function onClose(ConnectionInterface $connection)
120
    {
121
        //
122
    }
123
124
    /**
125
     * Handle the errors.
126
     *
127
     * @param  \Ratchet\ConnectionInterface  $connection
128
     * @param  Exception  $exception
129
     * @return void
130
     */
131
    public function onError(ConnectionInterface $connection, Exception $exception)
132
    {
133
        if (! $exception instanceof HttpException) {
134
            return;
135
        }
136
137
        $response = new Response($exception->getStatusCode(), [
138
            'Content-Type' => 'application/json',
139
        ], json_encode([
140
            'error' => $exception->getMessage(),
141
        ]));
142
143
        tap($connection)->send(Message::toString($response))->close();
144
    }
145
146
    /**
147
     * Get the content length from the headers.
148
     *
149
     * @param  array  $headers
150
     * @return int
151
     */
152
    protected function findContentLength(array $headers): int
153
    {
154
        return Collection::make($headers)->first(function ($values, $header) {
155
            return strtolower($header) === 'content-length';
156
        })[0] ?? 0;
157
    }
158
159
    /**
160
     * Check the content length.
161
     *
162
     * @return bool
163
     */
164
    protected function verifyContentLength()
165
    {
166
        return strlen($this->requestBuffer) === $this->contentLength;
167
    }
168
169
    /**
170
     * Handle the oncoming connection.
171
     *
172
     * @param  \Ratchet\ConnectionInterface  $connection
173
     * @return void
174
     */
175
    protected function handleRequest(ConnectionInterface $connection)
176
    {
177
        $serverRequest = (new ServerRequest(
178
            $this->request->getMethod(),
179
            $this->request->getUri(),
180
            $this->request->getHeaders(),
181
            $this->requestBuffer,
182
            $this->request->getProtocolVersion()
183
        ))->withQueryParams(QueryParameters::create($this->request)->all());
184
185
        $laravelRequest = Request::createFromBase((new HttpFoundationFactory)->createRequest($serverRequest));
186
187
        $this->ensureValidAppId($laravelRequest->get('appId'))
188
            ->ensureValidSignature($laravelRequest);
189
190
        // Invoke the controller action
191
        $response = $this($laravelRequest);
192
193
        // Allow for async IO in the controller action
194
        if ($response instanceof PromiseInterface) {
195
            $response->then(function ($response) use ($connection) {
196
                $this->sendAndClose($connection, $response);
197
            });
198
199
            return;
200
        }
201
202
        if ($response instanceof HttpException) {
203
            throw $response;
204
        }
205
206
        $this->sendAndClose($connection, $response);
207
    }
208
209
    /**
210
     * Send the response and close the connection.
211
     *
212
     * @param  \Ratchet\ConnectionInterface  $connection
213
     * @param  mixed  $response
214
     * @return void
215
     */
216
    protected function sendAndClose(ConnectionInterface $connection, $response)
217
    {
218
        tap($connection)->send(new JsonResponse($response))->close();
219
    }
220
221
    /**
222
     * Ensure app existence.
223
     *
224
     * @param  mixed  $appId
225
     * @return $this
226
     *
227
     * @throws \Symfony\Component\HttpKernel\Exception\HttpException
228
     */
229
    public function ensureValidAppId($appId)
230
    {
231
        if (! $appId || ! $this->app = App::findById($appId)) {
232
            throw new HttpException(401, "Unknown app id `{$appId}` provided.");
233
        }
234
235
        return $this;
236
    }
237
238
    /**
239
     * Ensure signature integrity coming from an
240
     * authorized application.
241
     *
242
     * @param  \GuzzleHttp\Psr7\ServerRequest  $request
243
     * @return $this
244
     *
245
     * @throws \Symfony\Component\HttpKernel\Exception\HttpException
246
     */
247
    protected function ensureValidSignature(Request $request)
248
    {
249
        // The `auth_signature` & `body_md5` parameters are not included when calculating the `auth_signature` value.
250
        // The `appId`, `appKey` & `channelName` parameters are actually route parameters and are never supplied by the client.
251
252
        $params = Arr::except($request->query(), [
0 ignored issues
show
Bug introduced by
It seems like $request->query() can also be of type string; however, parameter $array of Illuminate\Support\Arr::except() 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

252
        $params = Arr::except(/** @scrutinizer ignore-type */ $request->query(), [
Loading history...
253
            'auth_signature', 'body_md5', 'appId', 'appKey', 'channelName',
254
        ]);
255
256
        if ($request->getContent() !== '') {
257
            $params['body_md5'] = md5($request->getContent());
258
        }
259
260
        ksort($params);
261
262
        $signature = "{$request->getMethod()}\n/{$request->path()}\n".Pusher::array_implode('=', '&', $params);
263
264
        $authSignature = hash_hmac('sha256', $signature, $this->app->secret);
265
266
        if ($authSignature !== $request->get('auth_signature')) {
267
            throw new HttpException(401, 'Invalid auth signature provided.');
268
        }
269
270
        return $this;
271
    }
272
273
    /**
274
     * Handle the incoming request.
275
     *
276
     * @param  \Illuminate\Http\Request  $request
277
     * @return void
278
     */
279
    abstract public function __invoke(Request $request);
280
}
281