Passed
Push — master ( da3f2a...c03036 )
by Shahrad
01:59
created
src/Contracts/MessageContract.php 1 patch
Indentation   +6 added lines, -6 removed lines patch added patch discarded remove patch
@@ -14,11 +14,11 @@
 block discarded – undo
14 14
 interface MessageContract
15 15
 {
16 16
 
17
-	/**
18
-	 * @param string $message
19
-	 * @return void
20
-	 * @throws WebSocketException
21
-	 */
22
-	public function onMessage(string $message): void;
17
+    /**
18
+     * @param string $message
19
+     * @return void
20
+     * @throws WebSocketException
21
+     */
22
+    public function onMessage(string $message): void;
23 23
 
24 24
 }
Please login to merge, or discard this patch.
src/Contracts/WebSocketContract.php 1 patch
Indentation   +14 added lines, -14 removed lines patch added patch discarded remove patch
@@ -14,21 +14,21 @@
 block discarded – undo
14 14
 interface WebSocketContract
15 15
 {
16 16
 
17
-	/**
18
-	 * @return void
19
-	 */
20
-	public function onOpen(): void;
17
+    /**
18
+     * @return void
19
+     */
20
+    public function onOpen(): void;
21 21
 
22
-	/**
23
-	 * @param int $closeStatus
24
-	 * @return void
25
-	 */
26
-	public function onClose(int $closeStatus): void;
22
+    /**
23
+     * @param int $closeStatus
24
+     * @return void
25
+     */
26
+    public function onClose(int $closeStatus): void;
27 27
 
28
-	/**
29
-	 * @param WebSocketException $exception
30
-	 * @return void
31
-	 */
32
-	public function onError(WebSocketException $exception): void;
28
+    /**
29
+     * @param WebSocketException $exception
30
+     * @return void
31
+     */
32
+    public function onError(WebSocketException $exception): void;
33 33
 
34 34
 }
Please login to merge, or discard this patch.
src/WebSocket.php 1 patch
Indentation   +412 added lines, -412 removed lines patch added patch discarded remove patch
@@ -19,417 +19,417 @@
 block discarded – undo
19 19
 class WebSocket implements WscCommonsContract
20 20
 {
21 21
 
22
-	use WSClientTrait;
23
-
24
-	/**
25
-	 * App version
26
-	 *
27
-	 * @var string
28
-	 */
29
-	public const VERSION = 'v1.0.0';
30
-
31
-	/**
32
-	 * @var resource|bool
33
-	 */
34
-	private $socket;
35
-
36
-	/**
37
-	 * @var bool
38
-	 */
39
-	private bool $isConnected = false;
40
-
41
-	/**
42
-	 * @var bool
43
-	 */
44
-	private bool $isClosing = false;
45
-
46
-	/**
47
-	 * @var string
48
-	 */
49
-	private string $lastOpcode;
50
-
51
-	/**
52
-	 * @var float|int
53
-	 */
54
-	private float|int $closeStatus;
55
-
56
-	/**
57
-	 * @var string|null
58
-	 */
59
-	private ?string $hugePayload;
60
-
61
-	/**
62
-	 * @var array|int[]
63
-	 */
64
-	private static array $opcodes = [
65
-		CommonsContract::EVENT_TYPE_CONTINUATION => 0,
66
-		CommonsContract::EVENT_TYPE_TEXT => 1,
67
-		CommonsContract::EVENT_TYPE_BINARY => 2,
68
-		CommonsContract::EVENT_TYPE_CLOSE => 8,
69
-		CommonsContract::EVENT_TYPE_PING => 9,
70
-		CommonsContract::EVENT_TYPE_PONG => 10,
71
-	];
72
-
73
-	/**
74
-	 * @var WebSocketConfig
75
-	 */
76
-	protected WebSocketConfig $config;
77
-
78
-	/**
79
-	 * @var string
80
-	 */
81
-	protected string $socketUrl;
82
-
83
-	/**
84
-	 * @var ?SocketClient
85
-	 */
86
-	protected ?SocketClient $client = null;
87
-
88
-	/**
89
-	 * Sets parameters for Web Socket Client intercommunication
90
-	 *
91
-	 * @param SocketClient|string $clientOrUri pass the SocketClient object or the URI of the server
92
-	 * @param ?WebSocketConfig $config if you're passing the URI, you can pass the config object
93
-	 */
94
-	public function __construct(SocketClient|string $clientOrUri, ?WebSocketConfig $config = null)
95
-	{
96
-		if ($clientOrUri instanceof SocketClient) {
97
-			$this->client = $clientOrUri;
98
-		} else {
99
-			$this->connect($clientOrUri, $config === null ? new WebSocketConfig() : $config);
100
-		}
101
-	}
102
-
103
-	/**
104
-	 * @param string $socketUrl string that represents the URL of the Web Socket server. e.g. ws://localhost:1337 or wss://localhost:1337
105
-	 * @param WebSocketConfig $config The configuration for the Web Socket client
106
-	 */
107
-	public function connect(string $socketUrl, WebSocketConfig $config): void
108
-	{
109
-		try {
110
-			$this->config = $config;
111
-			$this->socketUrl = $socketUrl;
112
-			$urlParts = parse_url($this->socketUrl);
113
-
114
-			$this->config->setScheme($urlParts['scheme']);
115
-			$this->config->setHost($urlParts['host']);
116
-			$this->config->setUser($urlParts);
117
-			$this->config->setPassword($urlParts);
118
-			$this->config->setPort($urlParts);
119
-
120
-			$pathWithQuery = $this->getPathWithQuery($urlParts);
121
-			$hostUri = $this->getHostUri($this->config);
122
-
123
-			$context = $this->getStreamContext();
124
-			if ($this->config->hasProxy()) {
125
-				$this->socket = $this->proxy();
126
-			} else {
127
-				$this->socket = @stream_socket_client(
128
-					$hostUri . ':' . $this->config->getPort(),
129
-					$errno,
130
-					$errstr,
131
-					$this->config->getTimeout(),
132
-					STREAM_CLIENT_CONNECT,
133
-					$context
134
-				);
135
-			}
136
-
137
-			if ($this->socket === false) {
138
-				throw new ConnectionException(
139
-					"Could not open socket to \"{$this->config->getHost()}:{$this->config->getPort()}\": $errstr ($errno).",
140
-					CommonsContract::CLIENT_COULD_NOT_OPEN_SOCKET
141
-				);
142
-			}
143
-
144
-			stream_set_timeout($this->socket, $this->config->getTimeout());
145
-
146
-			$key = $this->generateKey();
147
-			$headers = [
148
-				'Host' => $this->config->getHost() . ':' . $this->config->getPort(),
149
-				'User-Agent' => 'Easy-Http/' . self::VERSION . ' (PHP/' . PHP_VERSION . ')',
150
-				'Connection' => 'Upgrade',
151
-				'Upgrade' => 'WebSocket',
152
-				'Sec-WebSocket-Key' => $key,
153
-				'Sec-Websocket-Version' => '13',
154
-			];
155
-
156
-			if ($this->config->getUser() || $this->config->getPassword()) {
157
-				$headers['authorization'] = 'Basic ' . base64_encode($this->config->getUser() . ':' . $this->config->getPassword()) . "\r\n";
158
-			}
159
-
160
-			if (!empty($this->config->getHeaders())) {
161
-				$headers = array_merge($headers, $this->config->getHeaders());
162
-			}
163
-
164
-			$header = $this->getHeaders($pathWithQuery, $headers);
165
-
166
-			$this->write($header);
167
-
168
-			$this->validateResponse($this->config, $pathWithQuery, $key);
169
-			$this->isConnected = true;
170
-
171
-			if ($this->client !== null) {
172
-				$this->client->setConnection($this);
173
-				$this->client->onOpen();
174
-				while ($this->isConnected()) {
175
-					if (is_string(($message = $this->receive()))) {
176
-						$this->client->onMessage($message);
177
-					}
178
-				}
179
-				$this->client->onClose($this->closeStatus);
180
-			}
181
-
182
-		} catch (\Exception $e) {
183
-			$this->client->onError(
184
-				new WebSocketException(
185
-					$e->getMessage(),
186
-					$e->getCode(),
187
-					$e
188
-				)
189
-			);
190
-		}
191
-	}
192
-
193
-	/**
194
-	 * Init a proxy connection
195
-	 *
196
-	 * @return resource|false
197
-	 * @throws \InvalidArgumentException
198
-	 * @throws ConnectionException
199
-	 */
200
-	private function proxy()
201
-	{
202
-		$sock = @stream_socket_client(
203
-			WscCommonsContract::TCP_SCHEME . $this->config->getProxyIp() . ':' . $this->config->getProxyPort(),
204
-			$errno,
205
-			$errstr,
206
-			$this->config->getTimeout(),
207
-			STREAM_CLIENT_CONNECT,
208
-			$this->getStreamContext()
209
-		);
210
-		$write = "CONNECT {$this->config->getProxyIp()}:{$this->config->getProxyPort()} HTTP/1.1\r\n";
211
-		$auth = $this->config->getProxyAuth();
212
-		if ($auth !== NULL) {
213
-			$write .= "Proxy-Authorization: Basic {$auth}\r\n";
214
-		}
215
-		$write .= "\r\n";
216
-		fwrite($sock, $write);
217
-		$resp = fread($sock, 1024);
218
-
219
-		if (preg_match(self::PROXY_MATCH_RESP, $resp) === 1) {
220
-			return $sock;
221
-		}
222
-
223
-		throw new ConnectionException('Failed to connect to the host via proxy');
224
-	}
225
-
226
-	/**
227
-	 * @return mixed
228
-	 * @throws \InvalidArgumentException
229
-	 */
230
-	private function getStreamContext(): mixed
231
-	{
232
-		if ($this->config->getContext() !== null) {
233
-			// Suppress the error since we'll catch it below
234
-			if (@get_resource_type($this->config->getContext()) === 'stream-context') {
235
-				return $this->config->getContext();
236
-			}
237
-
238
-			throw new \InvalidArgumentException(
239
-				'Stream context is invalid',
240
-				CommonsContract::CLIENT_INVALID_STREAM_CONTEXT
241
-			);
242
-		}
243
-
244
-		return stream_context_create($this->config->getContextOptions());
245
-	}
246
-
247
-	/**
248
-	 * @param mixed $urlParts
249
-	 * @return string
250
-	 */
251
-	private function getPathWithQuery(mixed $urlParts): string
252
-	{
253
-		$path = isset($urlParts['path']) ? $urlParts['path'] : '/';
254
-		$query = isset($urlParts['query']) ? $urlParts['query'] : '';
255
-		$fragment = isset($urlParts['fragment']) ? $urlParts['fragment'] : '';
256
-		$pathWithQuery = $path;
257
-		if (!empty($query)) {
258
-			$pathWithQuery .= '?' . $query;
259
-		}
260
-		if (!empty($fragment)) {
261
-			$pathWithQuery .= '#' . $fragment;
262
-		}
263
-
264
-		return $pathWithQuery;
265
-	}
266
-
267
-	/**
268
-	 * @param string $pathWithQuery
269
-	 * @param array $headers
270
-	 * @return string
271
-	 */
272
-	private function getHeaders(string $pathWithQuery, array $headers): string
273
-	{
274
-		return 'GET ' . $pathWithQuery . " HTTP/1.1\r\n"
275
-			. implode(
276
-				"\r\n",
277
-				array_map(
278
-					function ($key, $value) {
279
-						return "$key: $value";
280
-					},
281
-					array_keys($headers),
282
-					$headers
283
-				)
284
-			)
285
-			. "\r\n\r\n";
286
-	}
287
-
288
-	/**
289
-	 * @return string
290
-	 */
291
-	public function getLastOpcode(): string
292
-	{
293
-		return $this->lastOpcode;
294
-	}
295
-
296
-	/**
297
-	 * @return int
298
-	 */
299
-	public function getCloseStatus(): int
300
-	{
301
-		return $this->closeStatus;
302
-	}
303
-
304
-	/**
305
-	 * @return bool
306
-	 */
307
-	public function isConnected(): bool
308
-	{
309
-		return $this->isConnected;
310
-	}
311
-
312
-	/**
313
-	 * @param int $timeout
314
-	 * @param null $microSecs
315
-	 * @return WebSocket
316
-	 */
317
-	public function setTimeout(int $timeout, $microSecs = null): WebSocket
318
-	{
319
-		$this->config->setTimeout($timeout);
320
-		if ($this->socket && get_resource_type($this->socket) === 'stream') {
321
-			stream_set_timeout($this->socket, $timeout, $microSecs);
322
-		}
323
-
324
-		return $this;
325
-	}
326
-
327
-	/**
328
-	 * Sends message to opened socket connection client->server
329
-	 *
330
-	 * @param $payload
331
-	 * @param string $opcode
332
-	 * @throws \Exception
333
-	 */
334
-	public function send($payload, string $opcode = CommonsContract::EVENT_TYPE_TEXT): void
335
-	{
336
-		if (!$this->isConnected) {
337
-			$this->connect($this->socketUrl, new WebSocketConfig());
338
-		}
339
-
340
-		if (array_key_exists($opcode, self::$opcodes) === false) {
341
-			throw new BadOpcodeException(
342
-				"Bad opcode '$opcode'.  Try 'text' or 'binary'.",
343
-				CommonsContract::CLIENT_BAD_OPCODE
344
-			);
345
-		}
346
-
347
-		$payloadLength = strlen($payload);
348
-		$fragmentCursor = 0;
349
-
350
-		while ($payloadLength > $fragmentCursor) {
351
-			$subPayload = substr($payload, $fragmentCursor, $this->config->getFragmentSize());
352
-			$fragmentCursor += $this->config->getFragmentSize();
353
-			$final = $payloadLength <= $fragmentCursor;
354
-			$this->sendFragment($final, $subPayload, $opcode, true);
355
-			$opcode = 'continuation';
356
-		}
357
-	}
358
-
359
-	/**
360
-	 * Receives message client<-server
361
-	 *
362
-	 * @return string|null
363
-	 * @throws \Exception
364
-	 */
365
-	public function receive(): string|null
366
-	{
367
-		if (!$this->isConnected) {
368
-			$this->connect($this->socketUrl, new WebSocketConfig());
369
-		}
370
-
371
-		$this->hugePayload = '';
372
-
373
-		return $this->receiveFragment();
374
-	}
375
-
376
-	/**
377
-	 * Tell the socket to close.
378
-	 *
379
-	 * @param integer $status http://tools.ietf.org/html/rfc6455#section-7.4
380
-	 * @param string $message A closing message, max 125 bytes.
381
-	 * @return bool|null|string
382
-	 * @throws \Exception
383
-	 */
384
-	public function close(int $status = 1000, string $message = 'ttfn'): bool|null|string
385
-	{
386
-		$statusBin = sprintf('%016b', $status);
387
-		$statusStr = '';
388
-
389
-		foreach (str_split($statusBin, 8) as $binstr) {
390
-			$statusStr .= chr(bindec($binstr));
391
-		}
392
-
393
-		$this->send($statusStr . $message, CommonsContract::EVENT_TYPE_CLOSE);
394
-		$this->isClosing = true;
395
-
396
-		return $this->receive(); // Receiving a close frame will close the socket now.
397
-	}
398
-
399
-	/**
400
-	 * @param string $data
401
-	 * @throws ConnectionException
402
-	 */
403
-	protected function write(string $data): void
404
-	{
405
-		Middleware::stream_write($this->socket, $data);
406
-	}
407
-
408
-	/**
409
-	 * @param int $len
410
-	 * @return string|null
411
-	 * @throws ConnectionException
412
-	 */
413
-	protected function read(int $len): string|null
414
-	{
415
-		return Middleware::stream_read($this->socket, $len) ?: false;
416
-	}
417
-
418
-	/**
419
-	 * Helper to convert a binary to a string of '0' and '1'.
420
-	 *
421
-	 * @param string $string
422
-	 * @return string
423
-	 */
424
-	protected static function sprintB(string $string): string
425
-	{
426
-		$return = '';
427
-		$strLen = strlen($string);
428
-		for ($i = 0; $i < $strLen; $i++) {
429
-			$return .= sprintf('%08b', ord($string[$i]));
430
-		}
431
-
432
-		return $return;
433
-	}
22
+    use WSClientTrait;
23
+
24
+    /**
25
+     * App version
26
+     *
27
+     * @var string
28
+     */
29
+    public const VERSION = 'v1.0.0';
30
+
31
+    /**
32
+     * @var resource|bool
33
+     */
34
+    private $socket;
35
+
36
+    /**
37
+     * @var bool
38
+     */
39
+    private bool $isConnected = false;
40
+
41
+    /**
42
+     * @var bool
43
+     */
44
+    private bool $isClosing = false;
45
+
46
+    /**
47
+     * @var string
48
+     */
49
+    private string $lastOpcode;
50
+
51
+    /**
52
+     * @var float|int
53
+     */
54
+    private float|int $closeStatus;
55
+
56
+    /**
57
+     * @var string|null
58
+     */
59
+    private ?string $hugePayload;
60
+
61
+    /**
62
+     * @var array|int[]
63
+     */
64
+    private static array $opcodes = [
65
+        CommonsContract::EVENT_TYPE_CONTINUATION => 0,
66
+        CommonsContract::EVENT_TYPE_TEXT => 1,
67
+        CommonsContract::EVENT_TYPE_BINARY => 2,
68
+        CommonsContract::EVENT_TYPE_CLOSE => 8,
69
+        CommonsContract::EVENT_TYPE_PING => 9,
70
+        CommonsContract::EVENT_TYPE_PONG => 10,
71
+    ];
72
+
73
+    /**
74
+     * @var WebSocketConfig
75
+     */
76
+    protected WebSocketConfig $config;
77
+
78
+    /**
79
+     * @var string
80
+     */
81
+    protected string $socketUrl;
82
+
83
+    /**
84
+     * @var ?SocketClient
85
+     */
86
+    protected ?SocketClient $client = null;
87
+
88
+    /**
89
+     * Sets parameters for Web Socket Client intercommunication
90
+     *
91
+     * @param SocketClient|string $clientOrUri pass the SocketClient object or the URI of the server
92
+     * @param ?WebSocketConfig $config if you're passing the URI, you can pass the config object
93
+     */
94
+    public function __construct(SocketClient|string $clientOrUri, ?WebSocketConfig $config = null)
95
+    {
96
+        if ($clientOrUri instanceof SocketClient) {
97
+            $this->client = $clientOrUri;
98
+        } else {
99
+            $this->connect($clientOrUri, $config === null ? new WebSocketConfig() : $config);
100
+        }
101
+    }
102
+
103
+    /**
104
+     * @param string $socketUrl string that represents the URL of the Web Socket server. e.g. ws://localhost:1337 or wss://localhost:1337
105
+     * @param WebSocketConfig $config The configuration for the Web Socket client
106
+     */
107
+    public function connect(string $socketUrl, WebSocketConfig $config): void
108
+    {
109
+        try {
110
+            $this->config = $config;
111
+            $this->socketUrl = $socketUrl;
112
+            $urlParts = parse_url($this->socketUrl);
113
+
114
+            $this->config->setScheme($urlParts['scheme']);
115
+            $this->config->setHost($urlParts['host']);
116
+            $this->config->setUser($urlParts);
117
+            $this->config->setPassword($urlParts);
118
+            $this->config->setPort($urlParts);
119
+
120
+            $pathWithQuery = $this->getPathWithQuery($urlParts);
121
+            $hostUri = $this->getHostUri($this->config);
122
+
123
+            $context = $this->getStreamContext();
124
+            if ($this->config->hasProxy()) {
125
+                $this->socket = $this->proxy();
126
+            } else {
127
+                $this->socket = @stream_socket_client(
128
+                    $hostUri . ':' . $this->config->getPort(),
129
+                    $errno,
130
+                    $errstr,
131
+                    $this->config->getTimeout(),
132
+                    STREAM_CLIENT_CONNECT,
133
+                    $context
134
+                );
135
+            }
136
+
137
+            if ($this->socket === false) {
138
+                throw new ConnectionException(
139
+                    "Could not open socket to \"{$this->config->getHost()}:{$this->config->getPort()}\": $errstr ($errno).",
140
+                    CommonsContract::CLIENT_COULD_NOT_OPEN_SOCKET
141
+                );
142
+            }
143
+
144
+            stream_set_timeout($this->socket, $this->config->getTimeout());
145
+
146
+            $key = $this->generateKey();
147
+            $headers = [
148
+                'Host' => $this->config->getHost() . ':' . $this->config->getPort(),
149
+                'User-Agent' => 'Easy-Http/' . self::VERSION . ' (PHP/' . PHP_VERSION . ')',
150
+                'Connection' => 'Upgrade',
151
+                'Upgrade' => 'WebSocket',
152
+                'Sec-WebSocket-Key' => $key,
153
+                'Sec-Websocket-Version' => '13',
154
+            ];
155
+
156
+            if ($this->config->getUser() || $this->config->getPassword()) {
157
+                $headers['authorization'] = 'Basic ' . base64_encode($this->config->getUser() . ':' . $this->config->getPassword()) . "\r\n";
158
+            }
159
+
160
+            if (!empty($this->config->getHeaders())) {
161
+                $headers = array_merge($headers, $this->config->getHeaders());
162
+            }
163
+
164
+            $header = $this->getHeaders($pathWithQuery, $headers);
165
+
166
+            $this->write($header);
167
+
168
+            $this->validateResponse($this->config, $pathWithQuery, $key);
169
+            $this->isConnected = true;
170
+
171
+            if ($this->client !== null) {
172
+                $this->client->setConnection($this);
173
+                $this->client->onOpen();
174
+                while ($this->isConnected()) {
175
+                    if (is_string(($message = $this->receive()))) {
176
+                        $this->client->onMessage($message);
177
+                    }
178
+                }
179
+                $this->client->onClose($this->closeStatus);
180
+            }
181
+
182
+        } catch (\Exception $e) {
183
+            $this->client->onError(
184
+                new WebSocketException(
185
+                    $e->getMessage(),
186
+                    $e->getCode(),
187
+                    $e
188
+                )
189
+            );
190
+        }
191
+    }
192
+
193
+    /**
194
+     * Init a proxy connection
195
+     *
196
+     * @return resource|false
197
+     * @throws \InvalidArgumentException
198
+     * @throws ConnectionException
199
+     */
200
+    private function proxy()
201
+    {
202
+        $sock = @stream_socket_client(
203
+            WscCommonsContract::TCP_SCHEME . $this->config->getProxyIp() . ':' . $this->config->getProxyPort(),
204
+            $errno,
205
+            $errstr,
206
+            $this->config->getTimeout(),
207
+            STREAM_CLIENT_CONNECT,
208
+            $this->getStreamContext()
209
+        );
210
+        $write = "CONNECT {$this->config->getProxyIp()}:{$this->config->getProxyPort()} HTTP/1.1\r\n";
211
+        $auth = $this->config->getProxyAuth();
212
+        if ($auth !== NULL) {
213
+            $write .= "Proxy-Authorization: Basic {$auth}\r\n";
214
+        }
215
+        $write .= "\r\n";
216
+        fwrite($sock, $write);
217
+        $resp = fread($sock, 1024);
218
+
219
+        if (preg_match(self::PROXY_MATCH_RESP, $resp) === 1) {
220
+            return $sock;
221
+        }
222
+
223
+        throw new ConnectionException('Failed to connect to the host via proxy');
224
+    }
225
+
226
+    /**
227
+     * @return mixed
228
+     * @throws \InvalidArgumentException
229
+     */
230
+    private function getStreamContext(): mixed
231
+    {
232
+        if ($this->config->getContext() !== null) {
233
+            // Suppress the error since we'll catch it below
234
+            if (@get_resource_type($this->config->getContext()) === 'stream-context') {
235
+                return $this->config->getContext();
236
+            }
237
+
238
+            throw new \InvalidArgumentException(
239
+                'Stream context is invalid',
240
+                CommonsContract::CLIENT_INVALID_STREAM_CONTEXT
241
+            );
242
+        }
243
+
244
+        return stream_context_create($this->config->getContextOptions());
245
+    }
246
+
247
+    /**
248
+     * @param mixed $urlParts
249
+     * @return string
250
+     */
251
+    private function getPathWithQuery(mixed $urlParts): string
252
+    {
253
+        $path = isset($urlParts['path']) ? $urlParts['path'] : '/';
254
+        $query = isset($urlParts['query']) ? $urlParts['query'] : '';
255
+        $fragment = isset($urlParts['fragment']) ? $urlParts['fragment'] : '';
256
+        $pathWithQuery = $path;
257
+        if (!empty($query)) {
258
+            $pathWithQuery .= '?' . $query;
259
+        }
260
+        if (!empty($fragment)) {
261
+            $pathWithQuery .= '#' . $fragment;
262
+        }
263
+
264
+        return $pathWithQuery;
265
+    }
266
+
267
+    /**
268
+     * @param string $pathWithQuery
269
+     * @param array $headers
270
+     * @return string
271
+     */
272
+    private function getHeaders(string $pathWithQuery, array $headers): string
273
+    {
274
+        return 'GET ' . $pathWithQuery . " HTTP/1.1\r\n"
275
+            . implode(
276
+                "\r\n",
277
+                array_map(
278
+                    function ($key, $value) {
279
+                        return "$key: $value";
280
+                    },
281
+                    array_keys($headers),
282
+                    $headers
283
+                )
284
+            )
285
+            . "\r\n\r\n";
286
+    }
287
+
288
+    /**
289
+     * @return string
290
+     */
291
+    public function getLastOpcode(): string
292
+    {
293
+        return $this->lastOpcode;
294
+    }
295
+
296
+    /**
297
+     * @return int
298
+     */
299
+    public function getCloseStatus(): int
300
+    {
301
+        return $this->closeStatus;
302
+    }
303
+
304
+    /**
305
+     * @return bool
306
+     */
307
+    public function isConnected(): bool
308
+    {
309
+        return $this->isConnected;
310
+    }
311
+
312
+    /**
313
+     * @param int $timeout
314
+     * @param null $microSecs
315
+     * @return WebSocket
316
+     */
317
+    public function setTimeout(int $timeout, $microSecs = null): WebSocket
318
+    {
319
+        $this->config->setTimeout($timeout);
320
+        if ($this->socket && get_resource_type($this->socket) === 'stream') {
321
+            stream_set_timeout($this->socket, $timeout, $microSecs);
322
+        }
323
+
324
+        return $this;
325
+    }
326
+
327
+    /**
328
+     * Sends message to opened socket connection client->server
329
+     *
330
+     * @param $payload
331
+     * @param string $opcode
332
+     * @throws \Exception
333
+     */
334
+    public function send($payload, string $opcode = CommonsContract::EVENT_TYPE_TEXT): void
335
+    {
336
+        if (!$this->isConnected) {
337
+            $this->connect($this->socketUrl, new WebSocketConfig());
338
+        }
339
+
340
+        if (array_key_exists($opcode, self::$opcodes) === false) {
341
+            throw new BadOpcodeException(
342
+                "Bad opcode '$opcode'.  Try 'text' or 'binary'.",
343
+                CommonsContract::CLIENT_BAD_OPCODE
344
+            );
345
+        }
346
+
347
+        $payloadLength = strlen($payload);
348
+        $fragmentCursor = 0;
349
+
350
+        while ($payloadLength > $fragmentCursor) {
351
+            $subPayload = substr($payload, $fragmentCursor, $this->config->getFragmentSize());
352
+            $fragmentCursor += $this->config->getFragmentSize();
353
+            $final = $payloadLength <= $fragmentCursor;
354
+            $this->sendFragment($final, $subPayload, $opcode, true);
355
+            $opcode = 'continuation';
356
+        }
357
+    }
358
+
359
+    /**
360
+     * Receives message client<-server
361
+     *
362
+     * @return string|null
363
+     * @throws \Exception
364
+     */
365
+    public function receive(): string|null
366
+    {
367
+        if (!$this->isConnected) {
368
+            $this->connect($this->socketUrl, new WebSocketConfig());
369
+        }
370
+
371
+        $this->hugePayload = '';
372
+
373
+        return $this->receiveFragment();
374
+    }
375
+
376
+    /**
377
+     * Tell the socket to close.
378
+     *
379
+     * @param integer $status http://tools.ietf.org/html/rfc6455#section-7.4
380
+     * @param string $message A closing message, max 125 bytes.
381
+     * @return bool|null|string
382
+     * @throws \Exception
383
+     */
384
+    public function close(int $status = 1000, string $message = 'ttfn'): bool|null|string
385
+    {
386
+        $statusBin = sprintf('%016b', $status);
387
+        $statusStr = '';
388
+
389
+        foreach (str_split($statusBin, 8) as $binstr) {
390
+            $statusStr .= chr(bindec($binstr));
391
+        }
392
+
393
+        $this->send($statusStr . $message, CommonsContract::EVENT_TYPE_CLOSE);
394
+        $this->isClosing = true;
395
+
396
+        return $this->receive(); // Receiving a close frame will close the socket now.
397
+    }
398
+
399
+    /**
400
+     * @param string $data
401
+     * @throws ConnectionException
402
+     */
403
+    protected function write(string $data): void
404
+    {
405
+        Middleware::stream_write($this->socket, $data);
406
+    }
407
+
408
+    /**
409
+     * @param int $len
410
+     * @return string|null
411
+     * @throws ConnectionException
412
+     */
413
+    protected function read(int $len): string|null
414
+    {
415
+        return Middleware::stream_read($this->socket, $len) ?: false;
416
+    }
417
+
418
+    /**
419
+     * Helper to convert a binary to a string of '0' and '1'.
420
+     *
421
+     * @param string $string
422
+     * @return string
423
+     */
424
+    protected static function sprintB(string $string): string
425
+    {
426
+        $return = '';
427
+        $strLen = strlen($string);
428
+        for ($i = 0; $i < $strLen; $i++) {
429
+            $return .= sprintf('%08b', ord($string[$i]));
430
+        }
431
+
432
+        return $return;
433
+    }
434 434
 
435 435
 }
Please login to merge, or discard this patch.
src/Loop.php 1 patch
Indentation   +29 added lines, -29 removed lines patch added patch discarded remove patch
@@ -14,36 +14,36 @@
 block discarded – undo
14 14
 class Loop
15 15
 {
16 16
 
17
-	/**
18
-	 * Whether the loop is running or not
19
-	 *
20
-	 * @var bool
21
-	 */
22
-	protected static bool $running = false;
17
+    /**
18
+     * Whether the loop is running or not
19
+     *
20
+     * @var bool
21
+     */
22
+    protected static bool $running = false;
23 23
 
24
-	/**
25
-	 * @param callable $callback
26
-	 * @param int $interval in milliseconds
27
-	 * @return void
28
-	 */
29
-	public static function run(callable $callback, int $interval = 500): void
30
-	{
31
-		static::$running = true;
32
-		$last_hit = Toolkit::time();
33
-		while (static::$running) {
34
-			if (Toolkit::time() - $last_hit > $interval) {
35
-				$callback();
36
-				$last_hit = Toolkit::time();
37
-			}
38
-		}
39
-	}
24
+    /**
25
+     * @param callable $callback
26
+     * @param int $interval in milliseconds
27
+     * @return void
28
+     */
29
+    public static function run(callable $callback, int $interval = 500): void
30
+    {
31
+        static::$running = true;
32
+        $last_hit = Toolkit::time();
33
+        while (static::$running) {
34
+            if (Toolkit::time() - $last_hit > $interval) {
35
+                $callback();
36
+                $last_hit = Toolkit::time();
37
+            }
38
+        }
39
+    }
40 40
 
41
-	/**
42
-	 * @return void
43
-	 */
44
-	public static function stop(): void
45
-	{
46
-		static::$running = false;
47
-	}
41
+    /**
42
+     * @return void
43
+     */
44
+    public static function stop(): void
45
+    {
46
+        static::$running = false;
47
+    }
48 48
 
49 49
 }
50 50
\ No newline at end of file
Please login to merge, or discard this patch.
src/Middleware.php 1 patch
Indentation   +181 added lines, -181 removed lines patch added patch discarded remove patch
@@ -17,191 +17,191 @@
 block discarded – undo
17 17
 class Middleware
18 18
 {
19 19
 
20
-	/**
21
-	 * Create curl handler.
22
-	 *
23
-	 * @param ?string $method
24
-	 * @param string $uri
25
-	 * @param array|HttpOptions $options
26
-	 *
27
-	 * @return \CurlHandle|false
28
-	 */
29
-	public static function create_curl_handler(?string $method, string $uri, array|HttpOptions $options = []): \CurlHandle|false
30
-	{
31
-		$handler = curl_init();
32
-		if (is_resource($handler) || !$handler) {
33
-			return false;
34
-		}
35
-
36
-		if (gettype($options) === 'array') {
37
-			$options = new HttpOptions($options);
38
-		}
39
-
40
-		if (count($options->getQuery()) > 0) {
41
-			if (!str_contains($uri, '?')) {
42
-				$uri .= '?';
43
-			}
44
-			$uri .= $options->getQueryString();
45
-		}
46
-
47
-		curl_setopt($handler, CURLOPT_URL, $uri);
48
-
49
-		self::set_curl_options($method, $handler, $options);
50
-
51
-		return $handler;
52
-	}
53
-
54
-	/**
55
-	 * Setup curl options based on the given method and our options.
56
-	 *
57
-	 * @param \CurlHandle $cHandler
58
-	 * @param ?string $method
59
-	 * @param HttpOptions $options
60
-	 *
61
-	 * @return void
62
-	 */
63
-	public static function set_curl_options(?string $method, \CurlHandle $cHandler, HttpOptions $options): void
64
-	{
65
-		curl_setopt($cHandler, CURLOPT_HEADER, true);
66
-		curl_setopt($cHandler, CURLOPT_CUSTOMREQUEST, $method ?? 'GET');
67
-
68
-		# Fetch the header
69
-		$fetchedHeaders = [];
70
-		foreach ($options->getHeaders() as $header => $value) {
71
-			$fetchedHeaders[] = $header . ': ' . $value;
72
-		}
73
-
74
-		# Set headers
75
-		curl_setopt($cHandler, CURLOPT_HTTPHEADER, $fetchedHeaders ?? []);
76
-
77
-		# Add body if we have one.
78
-		if ($options->getBody()) {
79
-			curl_setopt($cHandler, CURLOPT_CUSTOMREQUEST, $method ?? 'POST');
80
-			curl_setopt($cHandler, CURLOPT_POSTFIELDS, $options->getBody());
81
-			curl_setopt($cHandler, CURLOPT_POST, true);
82
-		}
83
-
84
-		# Check for a proxy
85
-		if ($options->getProxy() != null) {
86
-			curl_setopt($cHandler, CURLOPT_PROXY, $options->getProxy()->getHost());
87
-			curl_setopt($cHandler, CURLOPT_PROXYUSERPWD, $options->getProxy()->getAuth());
88
-			if ($options->getProxy()->type !== null) {
89
-				curl_setopt($cHandler, CURLOPT_PROXYTYPE, $options->getProxy()->type);
90
-			}
91
-		}
92
-
93
-		curl_setopt($cHandler, CURLOPT_RETURNTRANSFER, true);
94
-		curl_setopt($cHandler, CURLOPT_FOLLOWLOCATION, true);
95
-
96
-		# Add and override the custom curl options.
97
-		foreach ($options->getCurlOptions() as $option => $value) {
98
-			curl_setopt($cHandler, $option, $value);
99
-		}
100
-
101
-		# if we have a timeout, set it.
102
-		curl_setopt($cHandler, CURLOPT_TIMEOUT, $options->getTimeout());
103
-
104
-		# If self-signed certs are allowed, set it.
105
-		if ((bool)getenv('HAS_SELF_SIGNED_CERT') === true) {
106
-			curl_setopt($cHandler, CURLOPT_SSL_VERIFYPEER, false);
107
-			curl_setopt($cHandler, CURLOPT_SSL_VERIFYHOST, false);
108
-		}
109
-
110
-		(new Middleware())->handle_media($cHandler, $options);
111
-	}
112
-
113
-	/**
114
-	 * Handle the media
115
-	 *
116
-	 * @param \CurlHandle $handler
117
-	 * @param HttpOptions $options
118
-	 * @return void
119
-	 */
120
-	private function handle_media(\CurlHandle $handler, HttpOptions $options): void
121
-	{
122
-		if (count($options->getMultipart()) > 0) {
123
-			curl_setopt($handler, CURLOPT_POST, true);
124
-			curl_setopt($handler, CURLOPT_CUSTOMREQUEST, 'POST');
125
-
126
-			$form_data = new FormData();
127
-			foreach ($options->getMultipart() as $key => $value) {
128
-				$form_data->addFile($key, $value);
129
-			}
130
-
131
-			$headers = [];
132
-			foreach ($options->getHeaders() as $header => $value) {
133
-				if (Toolkit::insensitiveString($header, 'content-type')) continue;
134
-				$headers[] = $header . ': ' . $value;
135
-			}
136
-			$headers[] = 'Content-Type: multipart/form-data';
137
-
138
-			curl_setopt($handler, CURLOPT_HTTPHEADER, $headers);
139
-			curl_setopt($handler, CURLOPT_POSTFIELDS, $form_data->getFiles());
140
-		}
141
-	}
142
-
143
-	/**
144
-	 * @param mixed $socket
145
-	 * @param int $len
146
-	 * @return string|null
147
-	 * @throws ConnectionException
148
-	 */
149
-	public static function stream_read(mixed $socket, int $len): string|null
150
-	{
151
-		if (!is_resource($socket)) {
152
-			throw new ConnectionException(sprintf(
153
-				'%s is not a valid resource.', $socket
154
-			));
155
-		}
156
-
157
-		$data = '';
158
-		while (($dataLen = strlen($data)) < $len) {
159
-			$buff = fread($socket, $len - $dataLen);
160
-
161
-			if ($buff === false) {
20
+    /**
21
+     * Create curl handler.
22
+     *
23
+     * @param ?string $method
24
+     * @param string $uri
25
+     * @param array|HttpOptions $options
26
+     *
27
+     * @return \CurlHandle|false
28
+     */
29
+    public static function create_curl_handler(?string $method, string $uri, array|HttpOptions $options = []): \CurlHandle|false
30
+    {
31
+        $handler = curl_init();
32
+        if (is_resource($handler) || !$handler) {
33
+            return false;
34
+        }
35
+
36
+        if (gettype($options) === 'array') {
37
+            $options = new HttpOptions($options);
38
+        }
39
+
40
+        if (count($options->getQuery()) > 0) {
41
+            if (!str_contains($uri, '?')) {
42
+                $uri .= '?';
43
+            }
44
+            $uri .= $options->getQueryString();
45
+        }
46
+
47
+        curl_setopt($handler, CURLOPT_URL, $uri);
48
+
49
+        self::set_curl_options($method, $handler, $options);
50
+
51
+        return $handler;
52
+    }
53
+
54
+    /**
55
+     * Setup curl options based on the given method and our options.
56
+     *
57
+     * @param \CurlHandle $cHandler
58
+     * @param ?string $method
59
+     * @param HttpOptions $options
60
+     *
61
+     * @return void
62
+     */
63
+    public static function set_curl_options(?string $method, \CurlHandle $cHandler, HttpOptions $options): void
64
+    {
65
+        curl_setopt($cHandler, CURLOPT_HEADER, true);
66
+        curl_setopt($cHandler, CURLOPT_CUSTOMREQUEST, $method ?? 'GET');
67
+
68
+        # Fetch the header
69
+        $fetchedHeaders = [];
70
+        foreach ($options->getHeaders() as $header => $value) {
71
+            $fetchedHeaders[] = $header . ': ' . $value;
72
+        }
73
+
74
+        # Set headers
75
+        curl_setopt($cHandler, CURLOPT_HTTPHEADER, $fetchedHeaders ?? []);
76
+
77
+        # Add body if we have one.
78
+        if ($options->getBody()) {
79
+            curl_setopt($cHandler, CURLOPT_CUSTOMREQUEST, $method ?? 'POST');
80
+            curl_setopt($cHandler, CURLOPT_POSTFIELDS, $options->getBody());
81
+            curl_setopt($cHandler, CURLOPT_POST, true);
82
+        }
83
+
84
+        # Check for a proxy
85
+        if ($options->getProxy() != null) {
86
+            curl_setopt($cHandler, CURLOPT_PROXY, $options->getProxy()->getHost());
87
+            curl_setopt($cHandler, CURLOPT_PROXYUSERPWD, $options->getProxy()->getAuth());
88
+            if ($options->getProxy()->type !== null) {
89
+                curl_setopt($cHandler, CURLOPT_PROXYTYPE, $options->getProxy()->type);
90
+            }
91
+        }
92
+
93
+        curl_setopt($cHandler, CURLOPT_RETURNTRANSFER, true);
94
+        curl_setopt($cHandler, CURLOPT_FOLLOWLOCATION, true);
95
+
96
+        # Add and override the custom curl options.
97
+        foreach ($options->getCurlOptions() as $option => $value) {
98
+            curl_setopt($cHandler, $option, $value);
99
+        }
100
+
101
+        # if we have a timeout, set it.
102
+        curl_setopt($cHandler, CURLOPT_TIMEOUT, $options->getTimeout());
103
+
104
+        # If self-signed certs are allowed, set it.
105
+        if ((bool)getenv('HAS_SELF_SIGNED_CERT') === true) {
106
+            curl_setopt($cHandler, CURLOPT_SSL_VERIFYPEER, false);
107
+            curl_setopt($cHandler, CURLOPT_SSL_VERIFYHOST, false);
108
+        }
109
+
110
+        (new Middleware())->handle_media($cHandler, $options);
111
+    }
112
+
113
+    /**
114
+     * Handle the media
115
+     *
116
+     * @param \CurlHandle $handler
117
+     * @param HttpOptions $options
118
+     * @return void
119
+     */
120
+    private function handle_media(\CurlHandle $handler, HttpOptions $options): void
121
+    {
122
+        if (count($options->getMultipart()) > 0) {
123
+            curl_setopt($handler, CURLOPT_POST, true);
124
+            curl_setopt($handler, CURLOPT_CUSTOMREQUEST, 'POST');
125
+
126
+            $form_data = new FormData();
127
+            foreach ($options->getMultipart() as $key => $value) {
128
+                $form_data->addFile($key, $value);
129
+            }
130
+
131
+            $headers = [];
132
+            foreach ($options->getHeaders() as $header => $value) {
133
+                if (Toolkit::insensitiveString($header, 'content-type')) continue;
134
+                $headers[] = $header . ': ' . $value;
135
+            }
136
+            $headers[] = 'Content-Type: multipart/form-data';
137
+
138
+            curl_setopt($handler, CURLOPT_HTTPHEADER, $headers);
139
+            curl_setopt($handler, CURLOPT_POSTFIELDS, $form_data->getFiles());
140
+        }
141
+    }
142
+
143
+    /**
144
+     * @param mixed $socket
145
+     * @param int $len
146
+     * @return string|null
147
+     * @throws ConnectionException
148
+     */
149
+    public static function stream_read(mixed $socket, int $len): string|null
150
+    {
151
+        if (!is_resource($socket)) {
152
+            throw new ConnectionException(sprintf(
153
+                '%s is not a valid resource.', $socket
154
+            ));
155
+        }
156
+
157
+        $data = '';
158
+        while (($dataLen = strlen($data)) < $len) {
159
+            $buff = fread($socket, $len - $dataLen);
160
+
161
+            if ($buff === false) {
162 162
 //				$metadata = stream_get_meta_data($socket);
163 163
 //				throw new ConnectionException(
164 164
 //					sprintf('Broken frame, read %s of stated %s bytes.  Stream state: %s', strlen($data), $len, json_encode($metadata)),
165 165
 //					CommonsContract::CLIENT_BROKEN_FRAME
166 166
 //				);
167
-				return null;
168
-			}
169
-
170
-			if ($buff === '') {
171
-				$metadata = stream_get_meta_data($socket);
172
-				throw new ConnectionException(
173
-					sprintf('Empty read; connection dead?  Stream state: %s', json_encode($metadata)),
174
-					CommonsContract::CLIENT_EMPTY_READ
175
-				);
176
-			}
177
-			$data .= $buff;
178
-		}
179
-
180
-		return $data;
181
-	}
182
-
183
-	/**
184
-	 * @param mixed $socket
185
-	 * @param string $data
186
-	 * @return bool
187
-	 * @throws ConnectionException
188
-	 */
189
-	public static function stream_write(mixed $socket, string $data): bool
190
-	{
191
-		if (!is_resource($socket)) {
192
-			throw new ConnectionException(sprintf('%s is not a valid resource.', $socket));
193
-		}
194
-
195
-		$written = fwrite($socket, $data);
196
-
197
-		if ($written < strlen($data)) {
198
-			throw new ConnectionException(
199
-				sprintf('Could only write %s out of %s bytes.', $written, strlen($data)),
200
-				CommonsContract::CLIENT_COULD_ONLY_WRITE_LESS
201
-			);
202
-		}
203
-
204
-		return true;
205
-	}
167
+                return null;
168
+            }
169
+
170
+            if ($buff === '') {
171
+                $metadata = stream_get_meta_data($socket);
172
+                throw new ConnectionException(
173
+                    sprintf('Empty read; connection dead?  Stream state: %s', json_encode($metadata)),
174
+                    CommonsContract::CLIENT_EMPTY_READ
175
+                );
176
+            }
177
+            $data .= $buff;
178
+        }
179
+
180
+        return $data;
181
+    }
182
+
183
+    /**
184
+     * @param mixed $socket
185
+     * @param string $data
186
+     * @return bool
187
+     * @throws ConnectionException
188
+     */
189
+    public static function stream_write(mixed $socket, string $data): bool
190
+    {
191
+        if (!is_resource($socket)) {
192
+            throw new ConnectionException(sprintf('%s is not a valid resource.', $socket));
193
+        }
194
+
195
+        $written = fwrite($socket, $data);
196
+
197
+        if ($written < strlen($data)) {
198
+            throw new ConnectionException(
199
+                sprintf('Could only write %s out of %s bytes.', $written, strlen($data)),
200
+                CommonsContract::CLIENT_COULD_ONLY_WRITE_LESS
201
+            );
202
+        }
203
+
204
+        return true;
205
+    }
206 206
 
207 207
 }
208 208
\ No newline at end of file
Please login to merge, or discard this patch.
src/Utils/Toolkit.php 2 patches
Indentation   +19 added lines, -19 removed lines patch added patch discarded remove patch
@@ -96,25 +96,25 @@
 block discarded – undo
96 96
         return (bool)preg_match_all('/' . $value . '/i', $string);
97 97
     }
98 98
 
99
-	/**
100
-	 * Millisecond sleep
101
-	 *
102
-	 * @param int $milliseconds The milliseconds
103
-	 * @return void
104
-	 */
105
-	public static function sleep(int $milliseconds): void
106
-	{
107
-		usleep($milliseconds * 1000);
108
-	}
99
+    /**
100
+     * Millisecond sleep
101
+     *
102
+     * @param int $milliseconds The milliseconds
103
+     * @return void
104
+     */
105
+    public static function sleep(int $milliseconds): void
106
+    {
107
+        usleep($milliseconds * 1000);
108
+    }
109 109
 
110
-	/**
111
-	 * Get current time in milliseconds
112
-	 *
113
-	 * @return int
114
-	 */
115
-	public static function time(): int
116
-	{
117
-		return (int)(microtime(true) * 1000);
118
-	}
110
+    /**
111
+     * Get current time in milliseconds
112
+     *
113
+     * @return int
114
+     */
115
+    public static function time(): int
116
+    {
117
+        return (int)(microtime(true) * 1000);
118
+    }
119 119
 
120 120
 }
121 121
\ No newline at end of file
Please login to merge, or discard this patch.
Spacing   +2 added lines, -2 removed lines patch added patch discarded remove patch
@@ -93,7 +93,7 @@  discard block
 block discarded – undo
93 93
      */
94 94
     public static function insensitiveString(string $string, string $value): bool
95 95
     {
96
-        return (bool)preg_match_all('/' . $value . '/i', $string);
96
+        return (bool) preg_match_all('/' . $value . '/i', $string);
97 97
     }
98 98
 
99 99
 	/**
@@ -114,7 +114,7 @@  discard block
 block discarded – undo
114 114
 	 */
115 115
 	public static function time(): int
116 116
 	{
117
-		return (int)(microtime(true) * 1000);
117
+		return (int) (microtime(true) * 1000);
118 118
 	}
119 119
 
120 120
 }
121 121
\ No newline at end of file
Please login to merge, or discard this patch.
src/SocketClient.php 1 patch
Indentation   +33 added lines, -33 removed lines patch added patch discarded remove patch
@@ -20,38 +20,38 @@
 block discarded – undo
20 20
 abstract class SocketClient implements WebSocketContract, MessageContract
21 21
 {
22 22
 
23
-	/**
24
-	 * @var WebSocket
25
-	 */
26
-	protected WebSocket $websocket;
27
-
28
-	/**
29
-	 * @param WebSocket $websocket
30
-	 * @return void
31
-	 */
32
-	protected function setConnection(WebSocket $websocket): void
33
-	{
34
-		$this->websocket = $websocket;
35
-	}
36
-
37
-	/**
38
-	 * @param string $name
39
-	 * @param array $arguments
40
-	 * @return mixed
41
-	 */
42
-	public function __call(string $name, array $arguments): mixed
43
-	{
44
-		if (method_exists($this, $name)) {
45
-			return call_user_func_array([$this, $name], $arguments);
46
-		}
47
-
48
-		if (method_exists($this->websocket, $name)) {
49
-			return call_user_func_array([$this->websocket, $name], $arguments);
50
-		}
51
-
52
-		throw new \BadMethodCallException(sprintf(
53
-			'Method `%s` does not exist at `%s`', $name, get_class($this)
54
-		));
55
-	}
23
+    /**
24
+     * @var WebSocket
25
+     */
26
+    protected WebSocket $websocket;
27
+
28
+    /**
29
+     * @param WebSocket $websocket
30
+     * @return void
31
+     */
32
+    protected function setConnection(WebSocket $websocket): void
33
+    {
34
+        $this->websocket = $websocket;
35
+    }
36
+
37
+    /**
38
+     * @param string $name
39
+     * @param array $arguments
40
+     * @return mixed
41
+     */
42
+    public function __call(string $name, array $arguments): mixed
43
+    {
44
+        if (method_exists($this, $name)) {
45
+            return call_user_func_array([$this, $name], $arguments);
46
+        }
47
+
48
+        if (method_exists($this->websocket, $name)) {
49
+            return call_user_func_array([$this->websocket, $name], $arguments);
50
+        }
51
+
52
+        throw new \BadMethodCallException(sprintf(
53
+            'Method `%s` does not exist at `%s`', $name, get_class($this)
54
+        ));
55
+    }
56 56
 
57 57
 }
Please login to merge, or discard this patch.
src/Traits/WSClientTrait.php 1 patch
Indentation   +239 added lines, -239 removed lines patch added patch discarded remove patch
@@ -17,244 +17,244 @@
 block discarded – undo
17 17
 trait WSClientTrait
18 18
 {
19 19
 
20
-	/**
21
-	 * Validates whether server sent valid upgrade response
22
-	 *
23
-	 * @param WebSocketConfig $config
24
-	 * @param string $pathWithQuery
25
-	 * @param string $key
26
-	 * @throws ConnectionException
27
-	 */
28
-	private function validateResponse(WebSocketConfig $config, string $pathWithQuery, string $key): void
29
-	{
30
-		$response = stream_get_line($this->socket, self::DEFAULT_RESPONSE_HEADER, "\r\n\r\n");
31
-		if (!preg_match(self::SEC_WEBSOCKET_ACCEPT_PTTRN, $response, $matches)) {
32
-			$address = $config->getScheme() . '://' . $config->getHost() . ':' . $config->getPort() . $pathWithQuery;
33
-			throw new ConnectionException(
34
-				"Connection to '{$address}' failed: Server sent invalid upgrade response:\n"
35
-				. $response, CommonsContract::CLIENT_INVALID_UPGRADE_RESPONSE
36
-			);
37
-		}
38
-
39
-		$keyAccept = trim($matches[1]);
40
-		$expectedResponse = base64_encode(pack('H*', sha1($key . self::SERVER_KEY_ACCEPT)));
41
-		if ($keyAccept !== $expectedResponse) {
42
-			throw new ConnectionException(
43
-				'Server sent bad upgrade response.',
44
-				CommonsContract::CLIENT_INVALID_UPGRADE_RESPONSE
45
-			);
46
-		}
47
-	}
48
-
49
-	/**
50
-	 *  Gets host uri based on protocol
51
-	 *
52
-	 * @param WebSocketConfig $config
53
-	 * @return string
54
-	 * @throws BadUriException
55
-	 */
56
-	private function getHostUri(WebSocketConfig $config): string
57
-	{
58
-		if (in_array($config->getScheme(), ['ws', 'wss'], true) === false) {
59
-			throw new BadUriException(
60
-				"Url should have scheme ws or wss, not '{$config->getScheme()}' from URI '$this->socketUrl' .",
61
-				CommonsContract::CLIENT_INCORRECT_SCHEME
62
-			);
63
-		}
64
-
65
-		return ($config->getScheme() === 'wss' ? 'ssl' : 'tcp') . '://' . $config->getHost();
66
-	}
67
-
68
-	/**
69
-	 * @param string $data
70
-	 * @return float|int
71
-	 * @throws ConnectionException
72
-	 */
73
-	private function getPayloadLength(string $data): float|int
74
-	{
75
-		$payloadLength = (int)ord($data[1]) & self::MASK_127; // Bits 1-7 in byte 1
76
-		if ($payloadLength > self::MASK_125) {
77
-			if ($payloadLength === self::MASK_126) {
78
-				$data = $this->read(2); // 126: Payload is a 16-bit unsigned int
79
-			} else {
80
-				$data = $this->read(8); // 127: Payload is a 64-bit unsigned int
81
-			}
82
-			$payloadLength = bindec(self::sprintB($data));
83
-		}
84
-
85
-		return $payloadLength;
86
-	}
87
-
88
-	/**
89
-	 * @param string $data
90
-	 * @param int $payloadLength
91
-	 * @return string
92
-	 * @throws ConnectionException
93
-	 */
94
-	private function getPayloadData(string $data, int $payloadLength): string
95
-	{
96
-		// Masking?
97
-		$mask = (bool)(ord($data[1]) >> 7);  // Bit 0 in byte 1
98
-		$payload = '';
99
-		$maskingKey = '';
100
-
101
-		// Get masking key.
102
-		if ($mask) {
103
-			$maskingKey = $this->read(4);
104
-		}
105
-
106
-		// Get the actual payload, if any (might not be for e.g. close frames.
107
-		if ($payloadLength > 0) {
108
-			$data = $this->read($payloadLength);
109
-
110
-			if ($mask) {
111
-				// Unmask payload.
112
-				for ($i = 0; $i < $payloadLength; $i++) {
113
-					$payload .= ($data[$i] ^ $maskingKey[$i % 4]);
114
-				}
115
-			} else {
116
-				$payload = $data;
117
-			}
118
-		}
119
-
120
-		return $payload;
121
-	}
122
-
123
-	/**
124
-	 * @return string|null
125
-	 * @throws \Exception
126
-	 */
127
-	protected function receiveFragment(): string|null
128
-	{
129
-		$data = $this->read(2);
130
-
131
-		$final = (bool)(ord($data[0]) & 1 << 7);
132
-
133
-		$opcodeInt = ord($data[0]) & 31;
134
-		$opcodeInts = array_flip(self::$opcodes);
135
-		if (!array_key_exists($opcodeInt, $opcodeInts)) {
136
-			throw new ConnectionException(
137
-				"Bad opcode in websocket frame: $opcodeInt",
138
-				CommonsContract::CLIENT_BAD_OPCODE
139
-			);
140
-		}
141
-
142
-		$opcode = $opcodeInts[$opcodeInt];
143
-
144
-		if ($opcode !== 'continuation') {
145
-			$this->lastOpcode = $opcode;
146
-		}
147
-
148
-		$payloadLength = $this->getPayloadLength($data);
149
-		$payload = $this->getPayloadData($data, $payloadLength);
150
-
151
-		if ($opcode === CommonsContract::EVENT_TYPE_CLOSE) {
152
-			if ($payloadLength >= 2) {
153
-				$statusBin = $payload[0] . $payload[1];
154
-				$status = bindec(sprintf('%08b%08b', ord($payload[0]), ord($payload[1])));
155
-				$this->closeStatus = $status;
156
-				$payload = substr($payload, 2);
157
-
158
-				if (!$this->isClosing) {
159
-					$this->send($statusBin . 'Close acknowledged: ' . $status,
160
-						CommonsContract::EVENT_TYPE_CLOSE); // Respond.
161
-				}
162
-			}
163
-
164
-			if ($this->isClosing) {
165
-				$this->isClosing = false; // A close response, all done.
166
-			}
167
-
168
-			fclose($this->socket);
169
-			$this->isConnected = false;
170
-		}
171
-
172
-		if (!$final) {
173
-			$this->hugePayload .= $payload;
174
-
175
-			return null;
176
-		}
177
-
178
-		if ($this->hugePayload) {
179
-			$payload = $this->hugePayload .= $payload;
180
-			$this->hugePayload = null;
181
-		}
182
-
183
-		return $payload;
184
-	}
185
-
186
-	/**
187
-	 * @param $final
188
-	 * @param $payload
189
-	 * @param $opcode
190
-	 * @param $masked
191
-	 * @throws \Exception
192
-	 */
193
-	protected function sendFragment($final, $payload, $opcode, $masked): void
194
-	{
195
-		// Binary string for header.
196
-		$frameHeadBin = '';
197
-		// Write FIN, final fragment bit.
198
-		$frameHeadBin .= (bool)$final ? '1' : '0';
199
-		// RSV 1, 2, & 3 false and unused.
200
-		$frameHeadBin .= '000';
201
-		// Opcode rest of the byte.
202
-		$frameHeadBin .= sprintf('%04b', self::$opcodes[$opcode]);
203
-		// Use masking?
204
-		$frameHeadBin .= $masked ? '1' : '0';
205
-
206
-		// 7 bits of payload length...
207
-		$payloadLen = strlen($payload);
208
-		if ($payloadLen > self::MAX_BYTES_READ) {
209
-			$frameHeadBin .= decbin(self::MASK_127);
210
-			$frameHeadBin .= sprintf('%064b', $payloadLen);
211
-		} else if ($payloadLen > self::MASK_125) {
212
-			$frameHeadBin .= decbin(self::MASK_126);
213
-			$frameHeadBin .= sprintf('%016b', $payloadLen);
214
-		} else {
215
-			$frameHeadBin .= sprintf('%07b', $payloadLen);
216
-		}
217
-
218
-		$frame = '';
219
-
220
-		// Write frame head to frame.
221
-		foreach (str_split($frameHeadBin, 8) as $binstr) {
222
-			$frame .= chr(bindec($binstr));
223
-		}
224
-		// Handle masking
225
-		if ($masked) {
226
-			// generate a random mask:
227
-			$mask = '';
228
-			for ($i = 0; $i < 4; $i++) {
229
-				$mask .= chr(random_int(0, 255));
230
-			}
231
-			$frame .= $mask;
232
-		}
233
-
234
-		// Append payload to frame:
235
-		for ($i = 0; $i < $payloadLen; $i++) {
236
-			$frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i];
237
-		}
238
-
239
-		$this->write($frame);
240
-	}
241
-
242
-	/**
243
-	 * Sec-WebSocket-Key generator
244
-	 *
245
-	 * @return string   the 16 character length key
246
-	 * @throws \Exception
247
-	 */
248
-	private function generateKey(): string
249
-	{
250
-		$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$&/()=[]{}0123456789';
251
-		$key = '';
252
-		$chLen = strlen($chars);
253
-		for ($i = 0; $i < self::KEY_GEN_LENGTH; $i++) {
254
-			$key .= $chars[random_int(0, $chLen - 1)];
255
-		}
256
-
257
-		return base64_encode($key);
258
-	}
20
+    /**
21
+     * Validates whether server sent valid upgrade response
22
+     *
23
+     * @param WebSocketConfig $config
24
+     * @param string $pathWithQuery
25
+     * @param string $key
26
+     * @throws ConnectionException
27
+     */
28
+    private function validateResponse(WebSocketConfig $config, string $pathWithQuery, string $key): void
29
+    {
30
+        $response = stream_get_line($this->socket, self::DEFAULT_RESPONSE_HEADER, "\r\n\r\n");
31
+        if (!preg_match(self::SEC_WEBSOCKET_ACCEPT_PTTRN, $response, $matches)) {
32
+            $address = $config->getScheme() . '://' . $config->getHost() . ':' . $config->getPort() . $pathWithQuery;
33
+            throw new ConnectionException(
34
+                "Connection to '{$address}' failed: Server sent invalid upgrade response:\n"
35
+                . $response, CommonsContract::CLIENT_INVALID_UPGRADE_RESPONSE
36
+            );
37
+        }
38
+
39
+        $keyAccept = trim($matches[1]);
40
+        $expectedResponse = base64_encode(pack('H*', sha1($key . self::SERVER_KEY_ACCEPT)));
41
+        if ($keyAccept !== $expectedResponse) {
42
+            throw new ConnectionException(
43
+                'Server sent bad upgrade response.',
44
+                CommonsContract::CLIENT_INVALID_UPGRADE_RESPONSE
45
+            );
46
+        }
47
+    }
48
+
49
+    /**
50
+     *  Gets host uri based on protocol
51
+     *
52
+     * @param WebSocketConfig $config
53
+     * @return string
54
+     * @throws BadUriException
55
+     */
56
+    private function getHostUri(WebSocketConfig $config): string
57
+    {
58
+        if (in_array($config->getScheme(), ['ws', 'wss'], true) === false) {
59
+            throw new BadUriException(
60
+                "Url should have scheme ws or wss, not '{$config->getScheme()}' from URI '$this->socketUrl' .",
61
+                CommonsContract::CLIENT_INCORRECT_SCHEME
62
+            );
63
+        }
64
+
65
+        return ($config->getScheme() === 'wss' ? 'ssl' : 'tcp') . '://' . $config->getHost();
66
+    }
67
+
68
+    /**
69
+     * @param string $data
70
+     * @return float|int
71
+     * @throws ConnectionException
72
+     */
73
+    private function getPayloadLength(string $data): float|int
74
+    {
75
+        $payloadLength = (int)ord($data[1]) & self::MASK_127; // Bits 1-7 in byte 1
76
+        if ($payloadLength > self::MASK_125) {
77
+            if ($payloadLength === self::MASK_126) {
78
+                $data = $this->read(2); // 126: Payload is a 16-bit unsigned int
79
+            } else {
80
+                $data = $this->read(8); // 127: Payload is a 64-bit unsigned int
81
+            }
82
+            $payloadLength = bindec(self::sprintB($data));
83
+        }
84
+
85
+        return $payloadLength;
86
+    }
87
+
88
+    /**
89
+     * @param string $data
90
+     * @param int $payloadLength
91
+     * @return string
92
+     * @throws ConnectionException
93
+     */
94
+    private function getPayloadData(string $data, int $payloadLength): string
95
+    {
96
+        // Masking?
97
+        $mask = (bool)(ord($data[1]) >> 7);  // Bit 0 in byte 1
98
+        $payload = '';
99
+        $maskingKey = '';
100
+
101
+        // Get masking key.
102
+        if ($mask) {
103
+            $maskingKey = $this->read(4);
104
+        }
105
+
106
+        // Get the actual payload, if any (might not be for e.g. close frames.
107
+        if ($payloadLength > 0) {
108
+            $data = $this->read($payloadLength);
109
+
110
+            if ($mask) {
111
+                // Unmask payload.
112
+                for ($i = 0; $i < $payloadLength; $i++) {
113
+                    $payload .= ($data[$i] ^ $maskingKey[$i % 4]);
114
+                }
115
+            } else {
116
+                $payload = $data;
117
+            }
118
+        }
119
+
120
+        return $payload;
121
+    }
122
+
123
+    /**
124
+     * @return string|null
125
+     * @throws \Exception
126
+     */
127
+    protected function receiveFragment(): string|null
128
+    {
129
+        $data = $this->read(2);
130
+
131
+        $final = (bool)(ord($data[0]) & 1 << 7);
132
+
133
+        $opcodeInt = ord($data[0]) & 31;
134
+        $opcodeInts = array_flip(self::$opcodes);
135
+        if (!array_key_exists($opcodeInt, $opcodeInts)) {
136
+            throw new ConnectionException(
137
+                "Bad opcode in websocket frame: $opcodeInt",
138
+                CommonsContract::CLIENT_BAD_OPCODE
139
+            );
140
+        }
141
+
142
+        $opcode = $opcodeInts[$opcodeInt];
143
+
144
+        if ($opcode !== 'continuation') {
145
+            $this->lastOpcode = $opcode;
146
+        }
147
+
148
+        $payloadLength = $this->getPayloadLength($data);
149
+        $payload = $this->getPayloadData($data, $payloadLength);
150
+
151
+        if ($opcode === CommonsContract::EVENT_TYPE_CLOSE) {
152
+            if ($payloadLength >= 2) {
153
+                $statusBin = $payload[0] . $payload[1];
154
+                $status = bindec(sprintf('%08b%08b', ord($payload[0]), ord($payload[1])));
155
+                $this->closeStatus = $status;
156
+                $payload = substr($payload, 2);
157
+
158
+                if (!$this->isClosing) {
159
+                    $this->send($statusBin . 'Close acknowledged: ' . $status,
160
+                        CommonsContract::EVENT_TYPE_CLOSE); // Respond.
161
+                }
162
+            }
163
+
164
+            if ($this->isClosing) {
165
+                $this->isClosing = false; // A close response, all done.
166
+            }
167
+
168
+            fclose($this->socket);
169
+            $this->isConnected = false;
170
+        }
171
+
172
+        if (!$final) {
173
+            $this->hugePayload .= $payload;
174
+
175
+            return null;
176
+        }
177
+
178
+        if ($this->hugePayload) {
179
+            $payload = $this->hugePayload .= $payload;
180
+            $this->hugePayload = null;
181
+        }
182
+
183
+        return $payload;
184
+    }
185
+
186
+    /**
187
+     * @param $final
188
+     * @param $payload
189
+     * @param $opcode
190
+     * @param $masked
191
+     * @throws \Exception
192
+     */
193
+    protected function sendFragment($final, $payload, $opcode, $masked): void
194
+    {
195
+        // Binary string for header.
196
+        $frameHeadBin = '';
197
+        // Write FIN, final fragment bit.
198
+        $frameHeadBin .= (bool)$final ? '1' : '0';
199
+        // RSV 1, 2, & 3 false and unused.
200
+        $frameHeadBin .= '000';
201
+        // Opcode rest of the byte.
202
+        $frameHeadBin .= sprintf('%04b', self::$opcodes[$opcode]);
203
+        // Use masking?
204
+        $frameHeadBin .= $masked ? '1' : '0';
205
+
206
+        // 7 bits of payload length...
207
+        $payloadLen = strlen($payload);
208
+        if ($payloadLen > self::MAX_BYTES_READ) {
209
+            $frameHeadBin .= decbin(self::MASK_127);
210
+            $frameHeadBin .= sprintf('%064b', $payloadLen);
211
+        } else if ($payloadLen > self::MASK_125) {
212
+            $frameHeadBin .= decbin(self::MASK_126);
213
+            $frameHeadBin .= sprintf('%016b', $payloadLen);
214
+        } else {
215
+            $frameHeadBin .= sprintf('%07b', $payloadLen);
216
+        }
217
+
218
+        $frame = '';
219
+
220
+        // Write frame head to frame.
221
+        foreach (str_split($frameHeadBin, 8) as $binstr) {
222
+            $frame .= chr(bindec($binstr));
223
+        }
224
+        // Handle masking
225
+        if ($masked) {
226
+            // generate a random mask:
227
+            $mask = '';
228
+            for ($i = 0; $i < 4; $i++) {
229
+                $mask .= chr(random_int(0, 255));
230
+            }
231
+            $frame .= $mask;
232
+        }
233
+
234
+        // Append payload to frame:
235
+        for ($i = 0; $i < $payloadLen; $i++) {
236
+            $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i];
237
+        }
238
+
239
+        $this->write($frame);
240
+    }
241
+
242
+    /**
243
+     * Sec-WebSocket-Key generator
244
+     *
245
+     * @return string   the 16 character length key
246
+     * @throws \Exception
247
+     */
248
+    private function generateKey(): string
249
+    {
250
+        $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$&/()=[]{}0123456789';
251
+        $key = '';
252
+        $chLen = strlen($chars);
253
+        for ($i = 0; $i < self::KEY_GEN_LENGTH; $i++) {
254
+            $key .= $chars[random_int(0, $chLen - 1)];
255
+        }
256
+
257
+        return base64_encode($key);
258
+    }
259 259
 
260 260
 }
261 261
\ No newline at end of file
Please login to merge, or discard this patch.