Completed
Pull Request — master (#447)
by Marcel
03:00 queued 01:29
created

Controller   A

Complexity

Total Complexity 19

Size/Duplication

Total Lines 251
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 0
Metric Value
wmc 19
lcom 1
cbo 13
dl 0
loc 251
rs 10
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A onOpen() 0 14 2
A onMessage() 0 10 2
A onClose() 0 4 1
A onError() 0 14 2
A findContentLength() 0 6 1
A verifyContentLength() 0 4 1
A handleRequest() 0 34 3
A sendAndClose() 0 4 1
A ensureValidAppId() 0 8 2
A ensureValidSignature() 0 27 3
__invoke() 0 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\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
    /**
26
     * The request buffer.
27
     *
28
     * @var string
29
     */
30
    protected $requestBuffer = '';
31
32
    /**
33
     * The incoming request.
34
     *
35
     * @var \Psr\Http\Message\RequestInterface
36
     */
37
    protected $request;
38
39
    /**
40
     * The content length that will
41
     * be calculated.
42
     *
43
     * @var int
44
     */
45
    protected $contentLength;
46
47
    /**
48
     * The channel manager.
49
     *
50
     * @var \BeyondCode\LaravelWebSockets\Contracts\ChannelManager
51
     */
52
    protected $channelManager;
53
54
    /**
55
     * Initialize the request.
56
     *
57
     * @param  ChannelManager  $channelManager
58
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
59
     */
60
    public function __construct(ChannelManager $channelManager)
61
    {
62
        $this->channelManager = $channelManager;
63
    }
64
65
    /**
66
     * Handle the opened socket connection.
67
     *
68
     * @param  \Ratchet\ConnectionInterface  $connection
69
     * @param  \Psr\Http\Message\RequestInterface  $request
70
     * @return void
71
     */
72
    public function onOpen(ConnectionInterface $connection, RequestInterface $request = null)
73
    {
74
        $this->request = $request;
75
76
        $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...
77
78
        $this->requestBuffer = (string) $request->getBody();
79
80
        if (! $this->verifyContentLength()) {
81
            return;
82
        }
83
84
        $this->handleRequest($connection);
85
    }
86
87
    /**
88
     * Handle the oncoming message and add it to buffer.
89
     *
90
     * @param  \Ratchet\ConnectionInterface  $from
91
     * @param  mixed  $msg
92
     * @return void
93
     */
94
    public function onMessage(ConnectionInterface $from, $msg)
95
    {
96
        $this->requestBuffer .= $msg;
97
98
        if (! $this->verifyContentLength()) {
99
            return;
100
        }
101
102
        $this->handleRequest($from);
103
    }
104
105
    /**
106
     * Handle the socket closing.
107
     *
108
     * @param  \Ratchet\ConnectionInterface  $connection
109
     * @return void
110
     */
111
    public function onClose(ConnectionInterface $connection)
112
    {
113
        //
114
    }
115
116
    /**
117
     * Handle the errors.
118
     *
119
     * @param  \Ratchet\ConnectionInterface  $connection
120
     * @param  Exception  $exception
121
     * @return void
122
     */
123
    public function onError(ConnectionInterface $connection, Exception $exception)
124
    {
125
        if (! $exception instanceof HttpException) {
126
            return;
127
        }
128
129
        $response = new Response($exception->getStatusCode(), [
130
            'Content-Type' => 'application/json',
131
        ], json_encode([
132
            'error' => $exception->getMessage(),
133
        ]));
134
135
        tap($connection)->send(\GuzzleHttp\Psr7\str($response))->close();
136
    }
137
138
    /**
139
     * Get the content length from the headers.
140
     *
141
     * @param  array  $headers
142
     * @return int
143
     */
144
    protected function findContentLength(array $headers): int
145
    {
146
        return Collection::make($headers)->first(function ($values, $header) {
147
            return strtolower($header) === 'content-length';
148
        })[0] ?? 0;
149
    }
150
151
    /**
152
     * Check the content length.
153
     *
154
     * @return bool
155
     */
156
    protected function verifyContentLength()
157
    {
158
        return strlen($this->requestBuffer) === $this->contentLength;
159
    }
160
161
    /**
162
     * Handle the oncoming connection.
163
     *
164
     * @param  \Ratchet\ConnectionInterface  $connection
165
     * @return void
166
     */
167
    protected function handleRequest(ConnectionInterface $connection)
168
    {
169
        $serverRequest = (new ServerRequest(
170
            $this->request->getMethod(),
171
            $this->request->getUri(),
172
            $this->request->getHeaders(),
173
            $this->requestBuffer,
174
            $this->request->getProtocolVersion()
175
        ))->withQueryParams(QueryParameters::create($this->request)->all());
176
177
        $laravelRequest = Request::createFromBase((new HttpFoundationFactory)->createRequest($serverRequest));
178
179
        $this
180
            ->ensureValidAppId($laravelRequest->appId)
181
            ->ensureValidSignature($laravelRequest);
182
183
        // Invoke the controller action
184
        $response = $this($laravelRequest);
185
186
        // Allow for async IO in the controller action
187
        if ($response instanceof PromiseInterface) {
188
            $response->then(function ($response) use ($connection) {
189
                $this->sendAndClose($connection, $response);
190
            });
191
192
            return;
193
        }
194
195
        if ($response instanceof HttpException) {
196
            throw $response;
197
        }
198
199
        $this->sendAndClose($connection, $response);
200
    }
201
202
    /**
203
     * Send the response and close the connection.
204
     *
205
     * @param  \Ratchet\ConnectionInterface  $connection
206
     * @param  mixed  $response
207
     * @return void
208
     */
209
    protected function sendAndClose(ConnectionInterface $connection, $response)
210
    {
211
        tap($connection)->send(JsonResponse::create($response))->close();
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...
212
    }
213
214
    /**
215
     * Ensure app existence.
216
     *
217
     * @param  mixed  $appId
218
     * @return $this
219
     * @throws \Symfony\Component\HttpKernel\Exception\HttpException
220
     */
221
    public function ensureValidAppId($appId)
222
    {
223
        if (! App::findById($appId)) {
224
            throw new HttpException(401, "Unknown app id `{$appId}` provided.");
225
        }
226
227
        return $this;
228
    }
229
230
    /**
231
     * Ensure signature integrity coming from an
232
     * authorized application.
233
     *
234
     * @param  \GuzzleHttp\Psr7\ServerRequest  $request
235
     * @return $this
236
     * @throws \Symfony\Component\HttpKernel\Exception\HttpException
237
     */
238
    protected function ensureValidSignature(Request $request)
239
    {
240
        // The `auth_signature` & `body_md5` parameters are not included when calculating the `auth_signature` value.
241
        // The `appId`, `appKey` & `channelName` parameters are actually route parameters and are never supplied by the client.
242
243
        $params = Arr::except($request->query(), [
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...
244
            'auth_signature', 'body_md5', 'appId', 'appKey', 'channelName',
245
        ]);
246
247
        if ($request->getContent() !== '') {
248
            $params['body_md5'] = md5($request->getContent());
249
        }
250
251
        ksort($params);
252
253
        $signature = "{$request->getMethod()}\n/{$request->path()}\n".Pusher::array_implode('=', '&', $params);
254
255
        $app = App::findById($request->get('appId'));
256
257
        $authSignature = hash_hmac('sha256', $signature, $app->secret);
258
259
        if ($authSignature !== $request->get('auth_signature')) {
260
            throw new HttpException(401, 'Invalid auth signature provided.');
261
        }
262
263
        return $this;
264
    }
265
266
    /**
267
     * Handle the incoming request.
268
     *
269
     * @param  \Illuminate\Http\Request  $request
270
     * @return void
271
     */
272
    abstract public function __invoke(Request $request);
273
}
274