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

Controller::onOpen()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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