beyondcode /
laravel-websockets
| 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
|
|||||
| 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
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
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 |
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.