Completed
Pull Request — master (#447)
by Alexandru
01:40
created

Controller::ensureValidSignature()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 9.536
c 0
b 0
f 0
cc 3
nc 4
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
    /**
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\WebSockets\Channels\ChannelManager
51
     */
52
    protected $channelManager;
53
54
    /**
55
     * Initialize the request.
56
     *
57
     * @param  \BeyondCode\LaravelWebSockets\WebSockets\Channels\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
        $this->sendAndClose($connection, $response);
196
    }
197
198
    /**
199
     * Send the response and close the connection.
200
     *
201
     * @param  \Ratchet\ConnectionInterface  $connection
202
     * @param  mixed  $response
203
     * @return void
204
     */
205
    protected function sendAndClose(ConnectionInterface $connection, $response)
206
    {
207
        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...
208
    }
209
210
    /**
211
     * Ensure app existence.
212
     *
213
     * @param  mixed  $appId
214
     * @return $this
215
     * @throws \Symfony\Component\HttpKernel\Exception\HttpException
216
     */
217
    public function ensureValidAppId($appId)
218
    {
219
        if (! App::findById($appId)) {
220
            throw new HttpException(401, "Unknown app id `{$appId}` provided.");
221
        }
222
223
        return $this;
224
    }
225
226
    /**
227
     * Ensure signature integrity coming from an
228
     * authorized application.
229
     *
230
     * @param  \GuzzleHttp\Psr7\ServerRequest  $request
231
     * @return $this
232
     * @throws \Symfony\Component\HttpKernel\Exception\HttpException
233
     */
234
    protected function ensureValidSignature(Request $request)
235
    {
236
        /*
237
         * The `auth_signature` & `body_md5` parameters are not included when calculating the `auth_signature` value.
238
         * The `appId`, `appKey` & `channelName` parameters are actually route parameters and are never supplied by the client.
239
         */
240
        $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...
241
242
        if ($request->getContent() !== '') {
243
            $params['body_md5'] = md5($request->getContent());
244
        }
245
246
        ksort($params);
247
248
        $signature = "{$request->getMethod()}\n/{$request->path()}\n".Pusher::array_implode('=', '&', $params);
249
250
        $authSignature = hash_hmac('sha256', $signature, App::findById($request->get('appId'))->secret);
251
252
        if ($authSignature !== $request->get('auth_signature')) {
253
            throw new HttpException(401, 'Invalid auth signature provided.');
254
        }
255
256
        return $this;
257
    }
258
259
    /**
260
     * Handle the incoming request.
261
     *
262
     * @param  \Illuminate\Http\Request  $request
263
     * @return void
264
     */
265
    abstract public function __invoke(Request $request);
266
}
267