Passed
Push — master ( d048db...e350b2 )
by Shahrad
01:41
created
src/Middleware.php 1 patch
Indentation   +187 added lines, -187 removed lines patch added patch discarded remove patch
@@ -17,192 +17,192 @@
 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. Datatype: %s', $socket, gettype($socket)
154
-			));
155
-		}
156
-
157
-		$data = '';
158
-		while (($dataLen = strlen($data)) < $len) {
159
-			$buff = fread($socket, $len - $dataLen);
160
-
161
-			if ($buff === false) {
162
-				return null;
163
-			}
164
-
165
-			if ($buff === '') {
166
-				$metadata = stream_get_meta_data($socket);
167
-				throw new ConnectionException(
168
-					sprintf('Empty read; connection dead?  Stream state: %s', json_encode($metadata)),
169
-					CommonsContract::CLIENT_EMPTY_READ
170
-				);
171
-			}
172
-			$data .= $buff;
173
-		}
174
-
175
-		return $data;
176
-	}
177
-
178
-	/**
179
-	 * @param mixed $socket
180
-	 * @param string $data
181
-	 * @return bool
182
-	 * @throws ConnectionException
183
-	 */
184
-	public static function stream_write(mixed $socket, string $data): bool
185
-	{
186
-		if (!is_resource($socket)) {
187
-			throw new ConnectionException(sprintf(
188
-				'%s is not a valid resource. Datatype: %s', $socket, gettype($socket)
189
-			));
190
-		}
191
-
192
-		$written = fwrite($socket, $data);
193
-
194
-		if ($written < strlen($data)) {
195
-			throw new ConnectionException(
196
-				sprintf('Could only write %s out of %s bytes.', $written, strlen($data)),
197
-				CommonsContract::CLIENT_COULD_ONLY_WRITE_LESS
198
-			);
199
-		}
200
-
201
-		return true;
202
-	}
203
-
204
-	/**
205
-	 * Stream connect
206
-	 */
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. Datatype: %s', $socket, gettype($socket)
154
+            ));
155
+        }
156
+
157
+        $data = '';
158
+        while (($dataLen = strlen($data)) < $len) {
159
+            $buff = fread($socket, $len - $dataLen);
160
+
161
+            if ($buff === false) {
162
+                return null;
163
+            }
164
+
165
+            if ($buff === '') {
166
+                $metadata = stream_get_meta_data($socket);
167
+                throw new ConnectionException(
168
+                    sprintf('Empty read; connection dead?  Stream state: %s', json_encode($metadata)),
169
+                    CommonsContract::CLIENT_EMPTY_READ
170
+                );
171
+            }
172
+            $data .= $buff;
173
+        }
174
+
175
+        return $data;
176
+    }
177
+
178
+    /**
179
+     * @param mixed $socket
180
+     * @param string $data
181
+     * @return bool
182
+     * @throws ConnectionException
183
+     */
184
+    public static function stream_write(mixed $socket, string $data): bool
185
+    {
186
+        if (!is_resource($socket)) {
187
+            throw new ConnectionException(sprintf(
188
+                '%s is not a valid resource. Datatype: %s', $socket, gettype($socket)
189
+            ));
190
+        }
191
+
192
+        $written = fwrite($socket, $data);
193
+
194
+        if ($written < strlen($data)) {
195
+            throw new ConnectionException(
196
+                sprintf('Could only write %s out of %s bytes.', $written, strlen($data)),
197
+                CommonsContract::CLIENT_COULD_ONLY_WRITE_LESS
198
+            );
199
+        }
200
+
201
+        return true;
202
+    }
203
+
204
+    /**
205
+     * Stream connect
206
+     */
207 207
 
208 208
 }
209 209
\ No newline at end of file
Please login to merge, or discard this patch.
src/Traits/WSClientTrait.php 1 patch
Indentation   +266 added lines, -266 removed lines patch added patch discarded remove patch
@@ -19,271 +19,271 @@
 block discarded – undo
19 19
 trait WSClientTrait
20 20
 {
21 21
 
22
-	/**
23
-	 * Validates whether server sent valid upgrade response
24
-	 *
25
-	 * @param WebSocketConfig $config
26
-	 * @param string $pathWithQuery
27
-	 * @param string $key
28
-	 * @throws ConnectionException
29
-	 */
30
-	private function validateResponse(WebSocketConfig $config, string $pathWithQuery, string $key): void
31
-	{
32
-		$response = stream_get_line($this->socket, self::DEFAULT_RESPONSE_HEADER, "\r\n\r\n");
33
-		if (!preg_match(self::SEC_WEBSOCKET_ACCEPT_PTTRN, $response, $matches)) {
34
-			$address = $config->getScheme() . '://' . $config->getHost() . ':' . $config->getPort() . $pathWithQuery;
35
-			throw new ConnectionException(
36
-				"Connection to '{$address}' failed: Server sent invalid upgrade response:\n"
37
-				. $response, CommonsContract::CLIENT_INVALID_UPGRADE_RESPONSE
38
-			);
39
-		}
40
-
41
-		$keyAccept = trim($matches[1]);
42
-		$expectedResponse = base64_encode(pack('H*', sha1($key . self::SERVER_KEY_ACCEPT)));
43
-		if ($keyAccept !== $expectedResponse) {
44
-			throw new ConnectionException(
45
-				'Server sent bad upgrade response.',
46
-				CommonsContract::CLIENT_INVALID_UPGRADE_RESPONSE
47
-			);
48
-		}
49
-	}
50
-
51
-	/**
52
-	 *  Gets host uri based on protocol
53
-	 *
54
-	 * @param WebSocketConfig $config
55
-	 * @return string
56
-	 * @throws BadUriException
57
-	 */
58
-	private function getHostUri(WebSocketConfig $config): string
59
-	{
60
-		if (in_array($config->getScheme(), ['ws', 'wss'], true) === false) {
61
-			throw new BadUriException(
62
-				"Url should have scheme ws or wss, not '{$config->getScheme()}' from URI '$this->socketUrl' .",
63
-				CommonsContract::CLIENT_INCORRECT_SCHEME
64
-			);
65
-		}
66
-
67
-		return ($config->getScheme() === 'wss' ? 'ssl' : 'tcp') . '://' . $config->getHost();
68
-	}
69
-
70
-	/**
71
-	 * @param string $data
72
-	 * @return float|int
73
-	 * @throws ConnectionException
74
-	 */
75
-	private function getPayloadLength(string $data): float|int
76
-	{
77
-		$payloadLength = (int)ord($data[1]) & self::MASK_127; // Bits 1-7 in byte 1
78
-		if ($payloadLength > self::MASK_125) {
79
-			if ($payloadLength === self::MASK_126) {
80
-				$data = $this->read(2); // 126: Payload is a 16-bit unsigned int
81
-			} else {
82
-				$data = $this->read(8); // 127: Payload is a 64-bit unsigned int
83
-			}
84
-			$payloadLength = bindec(Toolkit::sprintB($data));
85
-		}
86
-
87
-		return $payloadLength;
88
-	}
89
-
90
-	/**
91
-	 * @param string $data
92
-	 * @param int $payloadLength
93
-	 * @return string
94
-	 * @throws ConnectionException
95
-	 */
96
-	private function getPayloadData(string $data, int $payloadLength): string
97
-	{
98
-		// Masking?
99
-		$mask = (bool)(ord($data[1]) >> 7);  // Bit 0 in byte 1
100
-		$payload = '';
101
-		$maskingKey = '';
102
-
103
-		// Get masking key.
104
-		if ($mask) {
105
-			$maskingKey = $this->read(4);
106
-		}
107
-
108
-		// Get the actual payload, if any (might not be for e.g. close frames.
109
-		if ($payloadLength > 0) {
110
-			$data = $this->read($payloadLength);
111
-
112
-			if ($mask) {
113
-				// Unmask payload.
114
-				for ($i = 0; $i < $payloadLength; $i++) {
115
-					$payload .= ($data[$i] ^ $maskingKey[$i % 4]);
116
-				}
117
-			} else {
118
-				$payload = $data;
119
-			}
120
-		}
121
-
122
-		return $payload;
123
-	}
124
-
125
-	/**
126
-	 * @return string|null
127
-	 * @throws \Exception
128
-	 */
129
-	protected function receiveFragment(): string|null
130
-	{
131
-		$data = $this->read(2);
132
-		if (is_string($data) === false) {
133
-			return null;
134
-		}
135
-
136
-		$final = (bool)(ord($data[0]) & 1 << 7);
137
-
138
-		$opcodeInt = ord($data[0]) & 31;
139
-		$opcodeInts = array_flip(self::$opcodes);
140
-		if (!array_key_exists($opcodeInt, $opcodeInts)) {
141
-			throw new ConnectionException(
142
-				"Bad opcode in websocket frame: $opcodeInt",
143
-				CommonsContract::CLIENT_BAD_OPCODE
144
-			);
145
-		}
146
-
147
-		$opcode = $opcodeInts[$opcodeInt];
148
-
149
-		if ($opcode !== 'continuation') {
150
-			$this->lastOpcode = $opcode;
151
-		}
152
-
153
-		$payloadLength = $this->getPayloadLength($data);
154
-		$payload = $this->getPayloadData($data, $payloadLength);
155
-
156
-		if ($opcode === CommonsContract::EVENT_TYPE_CLOSE) {
157
-			if ($payloadLength >= 2) {
158
-				$statusBin = $payload[0] . $payload[1];
159
-				$status = bindec(sprintf('%08b%08b', ord($payload[0]), ord($payload[1])));
160
-				$this->closeStatus = $status;
161
-				$payload = substr($payload, 2);
162
-
163
-				if (!$this->isClosing) {
164
-					$this->send($statusBin . 'Close acknowledged: ' . $status,
165
-						CommonsContract::EVENT_TYPE_CLOSE); // Respond.
166
-				}
167
-			}
168
-
169
-			if ($this->isClosing) {
170
-				$this->isClosing = false; // A close response, all done.
171
-			}
172
-
173
-			fclose($this->socket);
174
-			$this->isConnected = false;
175
-		}
176
-
177
-		if (!$final) {
178
-			$this->hugePayload .= $payload;
179
-
180
-			return null;
181
-		}
182
-
183
-		if ($this->hugePayload) {
184
-			$payload = $this->hugePayload .= $payload;
185
-			$this->hugePayload = null;
186
-		}
187
-
188
-		return $payload;
189
-	}
190
-
191
-	/**
192
-	 * @param $final
193
-	 * @param $payload
194
-	 * @param $opcode
195
-	 * @param $masked
196
-	 * @throws \Exception
197
-	 */
198
-	protected function sendFragment($final, $payload, $opcode, $masked): void
199
-	{
200
-		// Binary string for header.
201
-		$frameHeadBin = '';
202
-		// Write FIN, final fragment bit.
203
-		$frameHeadBin .= (bool)$final ? '1' : '0';
204
-		// RSV 1, 2, & 3 false and unused.
205
-		$frameHeadBin .= '000';
206
-		// Opcode rest of the byte.
207
-		$frameHeadBin .= sprintf('%04b', self::$opcodes[$opcode]);
208
-		// Use masking?
209
-		$frameHeadBin .= $masked ? '1' : '0';
210
-
211
-		// 7 bits of payload length...
212
-		$payloadLen = strlen($payload);
213
-		if ($payloadLen > self::MAX_BYTES_READ) {
214
-			$frameHeadBin .= decbin(self::MASK_127);
215
-			$frameHeadBin .= sprintf('%064b', $payloadLen);
216
-		} else if ($payloadLen > self::MASK_125) {
217
-			$frameHeadBin .= decbin(self::MASK_126);
218
-			$frameHeadBin .= sprintf('%016b', $payloadLen);
219
-		} else {
220
-			$frameHeadBin .= sprintf('%07b', $payloadLen);
221
-		}
222
-
223
-		$frame = '';
224
-
225
-		// Write frame head to frame.
226
-		foreach (str_split($frameHeadBin, 8) as $binstr) {
227
-			$frame .= chr(bindec($binstr));
228
-		}
229
-		// Handle masking
230
-		if ($masked) {
231
-			// generate a random mask:
232
-			$mask = '';
233
-			for ($i = 0; $i < 4; $i++) {
234
-				$mask .= chr(random_int(0, 255));
235
-			}
236
-			$frame .= $mask;
237
-		}
238
-
239
-		// Append payload to frame:
240
-		for ($i = 0; $i < $payloadLen; $i++) {
241
-			$frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i];
242
-		}
243
-
244
-		$this->write($frame);
245
-	}
246
-
247
-	/**
248
-	 * Sec-WebSocket-Key generator
249
-	 *
250
-	 * @return string   the 16 character length key
251
-	 * @throws \Exception
252
-	 */
253
-	private function generateKey(): string
254
-	{
255
-		$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$&/()=[]{}0123456789';
256
-		$key = '';
257
-		$chLen = strlen($chars);
258
-		for ($i = 0; $i < self::KEY_GEN_LENGTH; $i++) {
259
-			$key .= $chars[random_int(0, $chLen - 1)];
260
-		}
261
-
262
-		return base64_encode($key);
263
-	}
264
-
265
-	/**
266
-	 * @param int $len
267
-	 * @return string|null
268
-	 * @throws ConnectionException
269
-	 */
270
-	protected function read(int $len): string|null
271
-	{
272
-		if ($this->socket && $this->isConnected()) {
273
-			return Middleware::stream_read($this->socket, $len);
274
-		}
275
-
276
-		return null;
277
-	}
278
-
279
-	/**
280
-	 * @param string $data
281
-	 * @return void
282
-	 * @throws ConnectionException
283
-	 */
284
-	protected function write(string $data): void
285
-	{
286
-		Middleware::stream_write($this->socket, $data);
287
-	}
22
+    /**
23
+     * Validates whether server sent valid upgrade response
24
+     *
25
+     * @param WebSocketConfig $config
26
+     * @param string $pathWithQuery
27
+     * @param string $key
28
+     * @throws ConnectionException
29
+     */
30
+    private function validateResponse(WebSocketConfig $config, string $pathWithQuery, string $key): void
31
+    {
32
+        $response = stream_get_line($this->socket, self::DEFAULT_RESPONSE_HEADER, "\r\n\r\n");
33
+        if (!preg_match(self::SEC_WEBSOCKET_ACCEPT_PTTRN, $response, $matches)) {
34
+            $address = $config->getScheme() . '://' . $config->getHost() . ':' . $config->getPort() . $pathWithQuery;
35
+            throw new ConnectionException(
36
+                "Connection to '{$address}' failed: Server sent invalid upgrade response:\n"
37
+                . $response, CommonsContract::CLIENT_INVALID_UPGRADE_RESPONSE
38
+            );
39
+        }
40
+
41
+        $keyAccept = trim($matches[1]);
42
+        $expectedResponse = base64_encode(pack('H*', sha1($key . self::SERVER_KEY_ACCEPT)));
43
+        if ($keyAccept !== $expectedResponse) {
44
+            throw new ConnectionException(
45
+                'Server sent bad upgrade response.',
46
+                CommonsContract::CLIENT_INVALID_UPGRADE_RESPONSE
47
+            );
48
+        }
49
+    }
50
+
51
+    /**
52
+     *  Gets host uri based on protocol
53
+     *
54
+     * @param WebSocketConfig $config
55
+     * @return string
56
+     * @throws BadUriException
57
+     */
58
+    private function getHostUri(WebSocketConfig $config): string
59
+    {
60
+        if (in_array($config->getScheme(), ['ws', 'wss'], true) === false) {
61
+            throw new BadUriException(
62
+                "Url should have scheme ws or wss, not '{$config->getScheme()}' from URI '$this->socketUrl' .",
63
+                CommonsContract::CLIENT_INCORRECT_SCHEME
64
+            );
65
+        }
66
+
67
+        return ($config->getScheme() === 'wss' ? 'ssl' : 'tcp') . '://' . $config->getHost();
68
+    }
69
+
70
+    /**
71
+     * @param string $data
72
+     * @return float|int
73
+     * @throws ConnectionException
74
+     */
75
+    private function getPayloadLength(string $data): float|int
76
+    {
77
+        $payloadLength = (int)ord($data[1]) & self::MASK_127; // Bits 1-7 in byte 1
78
+        if ($payloadLength > self::MASK_125) {
79
+            if ($payloadLength === self::MASK_126) {
80
+                $data = $this->read(2); // 126: Payload is a 16-bit unsigned int
81
+            } else {
82
+                $data = $this->read(8); // 127: Payload is a 64-bit unsigned int
83
+            }
84
+            $payloadLength = bindec(Toolkit::sprintB($data));
85
+        }
86
+
87
+        return $payloadLength;
88
+    }
89
+
90
+    /**
91
+     * @param string $data
92
+     * @param int $payloadLength
93
+     * @return string
94
+     * @throws ConnectionException
95
+     */
96
+    private function getPayloadData(string $data, int $payloadLength): string
97
+    {
98
+        // Masking?
99
+        $mask = (bool)(ord($data[1]) >> 7);  // Bit 0 in byte 1
100
+        $payload = '';
101
+        $maskingKey = '';
102
+
103
+        // Get masking key.
104
+        if ($mask) {
105
+            $maskingKey = $this->read(4);
106
+        }
107
+
108
+        // Get the actual payload, if any (might not be for e.g. close frames.
109
+        if ($payloadLength > 0) {
110
+            $data = $this->read($payloadLength);
111
+
112
+            if ($mask) {
113
+                // Unmask payload.
114
+                for ($i = 0; $i < $payloadLength; $i++) {
115
+                    $payload .= ($data[$i] ^ $maskingKey[$i % 4]);
116
+                }
117
+            } else {
118
+                $payload = $data;
119
+            }
120
+        }
121
+
122
+        return $payload;
123
+    }
124
+
125
+    /**
126
+     * @return string|null
127
+     * @throws \Exception
128
+     */
129
+    protected function receiveFragment(): string|null
130
+    {
131
+        $data = $this->read(2);
132
+        if (is_string($data) === false) {
133
+            return null;
134
+        }
135
+
136
+        $final = (bool)(ord($data[0]) & 1 << 7);
137
+
138
+        $opcodeInt = ord($data[0]) & 31;
139
+        $opcodeInts = array_flip(self::$opcodes);
140
+        if (!array_key_exists($opcodeInt, $opcodeInts)) {
141
+            throw new ConnectionException(
142
+                "Bad opcode in websocket frame: $opcodeInt",
143
+                CommonsContract::CLIENT_BAD_OPCODE
144
+            );
145
+        }
146
+
147
+        $opcode = $opcodeInts[$opcodeInt];
148
+
149
+        if ($opcode !== 'continuation') {
150
+            $this->lastOpcode = $opcode;
151
+        }
152
+
153
+        $payloadLength = $this->getPayloadLength($data);
154
+        $payload = $this->getPayloadData($data, $payloadLength);
155
+
156
+        if ($opcode === CommonsContract::EVENT_TYPE_CLOSE) {
157
+            if ($payloadLength >= 2) {
158
+                $statusBin = $payload[0] . $payload[1];
159
+                $status = bindec(sprintf('%08b%08b', ord($payload[0]), ord($payload[1])));
160
+                $this->closeStatus = $status;
161
+                $payload = substr($payload, 2);
162
+
163
+                if (!$this->isClosing) {
164
+                    $this->send($statusBin . 'Close acknowledged: ' . $status,
165
+                        CommonsContract::EVENT_TYPE_CLOSE); // Respond.
166
+                }
167
+            }
168
+
169
+            if ($this->isClosing) {
170
+                $this->isClosing = false; // A close response, all done.
171
+            }
172
+
173
+            fclose($this->socket);
174
+            $this->isConnected = false;
175
+        }
176
+
177
+        if (!$final) {
178
+            $this->hugePayload .= $payload;
179
+
180
+            return null;
181
+        }
182
+
183
+        if ($this->hugePayload) {
184
+            $payload = $this->hugePayload .= $payload;
185
+            $this->hugePayload = null;
186
+        }
187
+
188
+        return $payload;
189
+    }
190
+
191
+    /**
192
+     * @param $final
193
+     * @param $payload
194
+     * @param $opcode
195
+     * @param $masked
196
+     * @throws \Exception
197
+     */
198
+    protected function sendFragment($final, $payload, $opcode, $masked): void
199
+    {
200
+        // Binary string for header.
201
+        $frameHeadBin = '';
202
+        // Write FIN, final fragment bit.
203
+        $frameHeadBin .= (bool)$final ? '1' : '0';
204
+        // RSV 1, 2, & 3 false and unused.
205
+        $frameHeadBin .= '000';
206
+        // Opcode rest of the byte.
207
+        $frameHeadBin .= sprintf('%04b', self::$opcodes[$opcode]);
208
+        // Use masking?
209
+        $frameHeadBin .= $masked ? '1' : '0';
210
+
211
+        // 7 bits of payload length...
212
+        $payloadLen = strlen($payload);
213
+        if ($payloadLen > self::MAX_BYTES_READ) {
214
+            $frameHeadBin .= decbin(self::MASK_127);
215
+            $frameHeadBin .= sprintf('%064b', $payloadLen);
216
+        } else if ($payloadLen > self::MASK_125) {
217
+            $frameHeadBin .= decbin(self::MASK_126);
218
+            $frameHeadBin .= sprintf('%016b', $payloadLen);
219
+        } else {
220
+            $frameHeadBin .= sprintf('%07b', $payloadLen);
221
+        }
222
+
223
+        $frame = '';
224
+
225
+        // Write frame head to frame.
226
+        foreach (str_split($frameHeadBin, 8) as $binstr) {
227
+            $frame .= chr(bindec($binstr));
228
+        }
229
+        // Handle masking
230
+        if ($masked) {
231
+            // generate a random mask:
232
+            $mask = '';
233
+            for ($i = 0; $i < 4; $i++) {
234
+                $mask .= chr(random_int(0, 255));
235
+            }
236
+            $frame .= $mask;
237
+        }
238
+
239
+        // Append payload to frame:
240
+        for ($i = 0; $i < $payloadLen; $i++) {
241
+            $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i];
242
+        }
243
+
244
+        $this->write($frame);
245
+    }
246
+
247
+    /**
248
+     * Sec-WebSocket-Key generator
249
+     *
250
+     * @return string   the 16 character length key
251
+     * @throws \Exception
252
+     */
253
+    private function generateKey(): string
254
+    {
255
+        $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$&/()=[]{}0123456789';
256
+        $key = '';
257
+        $chLen = strlen($chars);
258
+        for ($i = 0; $i < self::KEY_GEN_LENGTH; $i++) {
259
+            $key .= $chars[random_int(0, $chLen - 1)];
260
+        }
261
+
262
+        return base64_encode($key);
263
+    }
264
+
265
+    /**
266
+     * @param int $len
267
+     * @return string|null
268
+     * @throws ConnectionException
269
+     */
270
+    protected function read(int $len): string|null
271
+    {
272
+        if ($this->socket && $this->isConnected()) {
273
+            return Middleware::stream_read($this->socket, $len);
274
+        }
275
+
276
+        return null;
277
+    }
278
+
279
+    /**
280
+     * @param string $data
281
+     * @return void
282
+     * @throws ConnectionException
283
+     */
284
+    protected function write(string $data): void
285
+    {
286
+        Middleware::stream_write($this->socket, $data);
287
+    }
288 288
 
289 289
 }
290 290
\ No newline at end of file
Please login to merge, or discard this patch.
src/Traits/WSConnectionTrait.php 1 patch
Indentation   +246 added lines, -246 removed lines patch added patch discarded remove patch
@@ -18,251 +18,251 @@
 block discarded – undo
18 18
 trait WSConnectionTrait
19 19
 {
20 20
 
21
-	/**
22
-	 * @var callable|null
23
-	 */
24
-	public $onOpen = null;
25
-
26
-	/**
27
-	 * @var callable|null
28
-	 */
29
-	public $onClose = null;
30
-
31
-	/**
32
-	 * @var callable|null
33
-	 */
34
-	public $onError = null;
35
-
36
-	/**
37
-	 * @var callable|null
38
-	 */
39
-	public $onMessage = null;
40
-
41
-	/**
42
-	 * @var callable|null
43
-	 */
44
-	public $onWhile = null;
45
-
46
-	/**
47
-	 * @var bool
48
-	 */
49
-	private bool $isConnected = false;
50
-
51
-	/**
52
-	 * @var bool
53
-	 */
54
-	private bool $isClosing = false;
55
-
56
-	/**
57
-	 * Default headers
58
-	 *
59
-	 * @var array
60
-	 */
61
-	private array $defaultHeaders = [
62
-		'Connection' => 'Upgrade',
63
-		'Upgrade' => 'WebSocket',
64
-		'Sec-Websocket-Version' => '13',
65
-	];
66
-
67
-	/**
68
-	 * @param string $socketUrl string that represents the URL of the Web Socket server. e.g. ws://localhost:1337 or wss://localhost:1337
69
-	 * @param ?WebSocketConfig $config The configuration for the Web Socket client
70
-	 */
71
-	public function connect(string $socketUrl, ?WebSocketConfig $config = null): void
72
-	{
73
-		try {
74
-			$this->config = $config ?? new WebSocketConfig();
75
-			$this->socketUrl = $socketUrl;
76
-			$urlParts = parse_url($this->socketUrl);
77
-
78
-			$this->config->setScheme($urlParts['scheme']);
79
-			$this->config->setHost($urlParts['host']);
80
-			$this->config->setUser($urlParts);
81
-			$this->config->setPassword($urlParts);
82
-			$this->config->setPort($urlParts);
83
-
84
-			$pathWithQuery = $this->getPathWithQuery($urlParts);
85
-			$hostUri = $this->getHostUri($this->config);
86
-
87
-			$context = $this->getStreamContext();
88
-			if ($this->config->hasProxy()) {
89
-				$this->socket = $this->proxy();
90
-			} else {
91
-				$this->socket = @stream_socket_client(
92
-					$hostUri . ':' . $this->config->getPort(),
93
-					$errno,
94
-					$errstr,
95
-					$this->config->getTimeout(),
96
-					STREAM_CLIENT_CONNECT,
97
-					$context
98
-				);
99
-			}
100
-
101
-			if ($this->socket === false) {
102
-				throw new ConnectionException(
103
-					"Could not open socket to \"{$this->config->getHost()}:{$this->config->getPort()}\": $errstr ($errno).",
104
-					CommonsContract::CLIENT_COULD_NOT_OPEN_SOCKET
105
-				);
106
-			}
107
-
108
-			stream_set_timeout($this->socket, $this->config->getTimeout());
109
-
110
-			$key = $this->generateKey();
111
-			$headers = array_merge($this->defaultHeaders, [
112
-				'Host' => $this->config->getHost() . ':' . $this->config->getPort(),
113
-				'User-Agent' => 'Easy-Http/' . self::VERSION . ' (PHP/' . PHP_VERSION . ')',
114
-				'Sec-WebSocket-Key' => $key,
115
-			]);
116
-
117
-			if ($this->config->getUser() || $this->config->getPassword()) {
118
-				$headers['authorization'] = 'Basic ' . base64_encode($this->config->getUser() . ':' . $this->config->getPassword()) . "\r\n";
119
-			}
120
-
121
-			if (!empty($this->config->getHeaders())) {
122
-				$headers = array_merge($headers, $this->config->getHeaders());
123
-			}
124
-
125
-			$header = $this->getHeaders($pathWithQuery, $headers);
126
-
127
-			$this->write($header);
128
-
129
-			$this->validateResponse($this->config, $pathWithQuery, $key);
130
-			$this->isConnected = true;
131
-			$this->whileIsConnected();
132
-
133
-		} catch (\Exception $e) {
134
-			$this->safeCall($this->onError, $this, new WebSocketException(
135
-				$e->getMessage(),
136
-				$e->getCode(),
137
-				$e->getPrevious()
138
-			));
139
-		}
140
-	}
141
-
142
-	/**
143
-	 * Reconnect to the Web Socket server
144
-	 *
145
-	 * @return void
146
-	 * @throws \Exception
147
-	 */
148
-	public function reconnect(): void
149
-	{
150
-		if ($this->isConnected) {
151
-			$this->close();
152
-		}
153
-
154
-		$this->connect($this->socketUrl, $this->config);
155
-	}
156
-
157
-	/**
158
-	 * @return void
159
-	 * @throws WebSocketException|\Exception
160
-	 */
161
-	private function whileIsConnected(): void
162
-	{
163
-		$this->safeCall($this->onOpen, $this);
164
-
165
-		while ($this->isConnected()) {
166
-			$this->safeCall($this->onWhile, $this);
167
-
168
-			if (is_string(($message = $this->receive()))) {
169
-				$this->safeCall($this->onMessage, $this, $message);
170
-			}
171
-		}
172
-
173
-		$this->safeCall($this->onClose, $this, $this->closeStatus);
174
-	}
175
-
176
-	/**
177
-	 * Execute events with safety of exceptions
178
-	 *
179
-	 * @param callable|null $callback
180
-	 * @param mixed ...$args
181
-	 * @return void
182
-	 */
183
-	private function safeCall(?callable $callback, ...$args): void
184
-	{
185
-		if (is_callable($callback) && $callback) {
186
-			call_user_func($callback, ...$args);
187
-		}
188
-	}
189
-
190
-	/**
191
-	 * Sends message to opened socket connection client->server
192
-	 *
193
-	 * @param $payload
194
-	 * @param string $opcode
195
-	 * @throws \Exception
196
-	 */
197
-	public function send($payload, string $opcode = CommonsContract::EVENT_TYPE_TEXT): void
198
-	{
199
-		if (!$this->isConnected) {
200
-			throw new \Exception(
201
-				"Can't send message. Connection is not established.",
202
-				CommonsContract::CLIENT_CONNECTION_NOT_ESTABLISHED
203
-			);
204
-		}
205
-
206
-		if (array_key_exists($opcode, self::$opcodes) === false) {
207
-			throw new BadOpcodeException(
208
-				sprintf("Bad opcode '%s'.  Try 'text' or 'binary'.", $opcode),
209
-				CommonsContract::CLIENT_BAD_OPCODE
210
-			);
211
-		}
212
-
213
-		$payloadLength = strlen($payload);
214
-		$fragmentCursor = 0;
215
-
216
-		while ($payloadLength > $fragmentCursor) {
217
-			$subPayload = substr($payload, $fragmentCursor, $this->config->getFragmentSize());
218
-			$fragmentCursor += $this->config->getFragmentSize();
219
-			$final = $payloadLength <= $fragmentCursor;
220
-			$this->sendFragment($final, $subPayload, $opcode, true);
221
-			$opcode = 'continuation';
222
-		}
223
-	}
224
-
225
-	/**
226
-	 * Receives message client<-server
227
-	 *
228
-	 * @return string|null
229
-	 * @throws \Exception
230
-	 */
231
-	public function receive(): string|null
232
-	{
233
-		if (!$this->isConnected && $this->isClosing === false) {
234
-			throw new WebSocketException(
235
-				"Your unexpectedly disconnected from the server",
236
-				CommonsContract::CLIENT_CONNECTION_NOT_ESTABLISHED
237
-			);
238
-		}
239
-
240
-		$this->hugePayload = '';
241
-
242
-		return $this->receiveFragment();
243
-	}
244
-
245
-	/**
246
-	 * Tell the socket to close.
247
-	 *
248
-	 * @param integer $status http://tools.ietf.org/html/rfc6455#section-7.4
249
-	 * @param string $message A closing message, max 125 bytes.
250
-	 * @return bool|null|string
251
-	 * @throws \Exception
252
-	 */
253
-	public function close(int $status = 1000, string $message = 'ttfn'): bool|null|string
254
-	{
255
-		$statusBin = sprintf('%016b', $status);
256
-		$statusStr = '';
257
-
258
-		foreach (str_split($statusBin, 8) as $binstr) {
259
-			$statusStr .= chr(bindec($binstr));
260
-		}
261
-
262
-		$this->send($statusStr . $message, CommonsContract::EVENT_TYPE_CLOSE);
263
-		$this->isClosing = true;
264
-
265
-		return $this->receive(); // Receiving a close frame will close the socket now.
266
-	}
21
+    /**
22
+     * @var callable|null
23
+     */
24
+    public $onOpen = null;
25
+
26
+    /**
27
+     * @var callable|null
28
+     */
29
+    public $onClose = null;
30
+
31
+    /**
32
+     * @var callable|null
33
+     */
34
+    public $onError = null;
35
+
36
+    /**
37
+     * @var callable|null
38
+     */
39
+    public $onMessage = null;
40
+
41
+    /**
42
+     * @var callable|null
43
+     */
44
+    public $onWhile = null;
45
+
46
+    /**
47
+     * @var bool
48
+     */
49
+    private bool $isConnected = false;
50
+
51
+    /**
52
+     * @var bool
53
+     */
54
+    private bool $isClosing = false;
55
+
56
+    /**
57
+     * Default headers
58
+     *
59
+     * @var array
60
+     */
61
+    private array $defaultHeaders = [
62
+        'Connection' => 'Upgrade',
63
+        'Upgrade' => 'WebSocket',
64
+        'Sec-Websocket-Version' => '13',
65
+    ];
66
+
67
+    /**
68
+     * @param string $socketUrl string that represents the URL of the Web Socket server. e.g. ws://localhost:1337 or wss://localhost:1337
69
+     * @param ?WebSocketConfig $config The configuration for the Web Socket client
70
+     */
71
+    public function connect(string $socketUrl, ?WebSocketConfig $config = null): void
72
+    {
73
+        try {
74
+            $this->config = $config ?? new WebSocketConfig();
75
+            $this->socketUrl = $socketUrl;
76
+            $urlParts = parse_url($this->socketUrl);
77
+
78
+            $this->config->setScheme($urlParts['scheme']);
79
+            $this->config->setHost($urlParts['host']);
80
+            $this->config->setUser($urlParts);
81
+            $this->config->setPassword($urlParts);
82
+            $this->config->setPort($urlParts);
83
+
84
+            $pathWithQuery = $this->getPathWithQuery($urlParts);
85
+            $hostUri = $this->getHostUri($this->config);
86
+
87
+            $context = $this->getStreamContext();
88
+            if ($this->config->hasProxy()) {
89
+                $this->socket = $this->proxy();
90
+            } else {
91
+                $this->socket = @stream_socket_client(
92
+                    $hostUri . ':' . $this->config->getPort(),
93
+                    $errno,
94
+                    $errstr,
95
+                    $this->config->getTimeout(),
96
+                    STREAM_CLIENT_CONNECT,
97
+                    $context
98
+                );
99
+            }
100
+
101
+            if ($this->socket === false) {
102
+                throw new ConnectionException(
103
+                    "Could not open socket to \"{$this->config->getHost()}:{$this->config->getPort()}\": $errstr ($errno).",
104
+                    CommonsContract::CLIENT_COULD_NOT_OPEN_SOCKET
105
+                );
106
+            }
107
+
108
+            stream_set_timeout($this->socket, $this->config->getTimeout());
109
+
110
+            $key = $this->generateKey();
111
+            $headers = array_merge($this->defaultHeaders, [
112
+                'Host' => $this->config->getHost() . ':' . $this->config->getPort(),
113
+                'User-Agent' => 'Easy-Http/' . self::VERSION . ' (PHP/' . PHP_VERSION . ')',
114
+                'Sec-WebSocket-Key' => $key,
115
+            ]);
116
+
117
+            if ($this->config->getUser() || $this->config->getPassword()) {
118
+                $headers['authorization'] = 'Basic ' . base64_encode($this->config->getUser() . ':' . $this->config->getPassword()) . "\r\n";
119
+            }
120
+
121
+            if (!empty($this->config->getHeaders())) {
122
+                $headers = array_merge($headers, $this->config->getHeaders());
123
+            }
124
+
125
+            $header = $this->getHeaders($pathWithQuery, $headers);
126
+
127
+            $this->write($header);
128
+
129
+            $this->validateResponse($this->config, $pathWithQuery, $key);
130
+            $this->isConnected = true;
131
+            $this->whileIsConnected();
132
+
133
+        } catch (\Exception $e) {
134
+            $this->safeCall($this->onError, $this, new WebSocketException(
135
+                $e->getMessage(),
136
+                $e->getCode(),
137
+                $e->getPrevious()
138
+            ));
139
+        }
140
+    }
141
+
142
+    /**
143
+     * Reconnect to the Web Socket server
144
+     *
145
+     * @return void
146
+     * @throws \Exception
147
+     */
148
+    public function reconnect(): void
149
+    {
150
+        if ($this->isConnected) {
151
+            $this->close();
152
+        }
153
+
154
+        $this->connect($this->socketUrl, $this->config);
155
+    }
156
+
157
+    /**
158
+     * @return void
159
+     * @throws WebSocketException|\Exception
160
+     */
161
+    private function whileIsConnected(): void
162
+    {
163
+        $this->safeCall($this->onOpen, $this);
164
+
165
+        while ($this->isConnected()) {
166
+            $this->safeCall($this->onWhile, $this);
167
+
168
+            if (is_string(($message = $this->receive()))) {
169
+                $this->safeCall($this->onMessage, $this, $message);
170
+            }
171
+        }
172
+
173
+        $this->safeCall($this->onClose, $this, $this->closeStatus);
174
+    }
175
+
176
+    /**
177
+     * Execute events with safety of exceptions
178
+     *
179
+     * @param callable|null $callback
180
+     * @param mixed ...$args
181
+     * @return void
182
+     */
183
+    private function safeCall(?callable $callback, ...$args): void
184
+    {
185
+        if (is_callable($callback) && $callback) {
186
+            call_user_func($callback, ...$args);
187
+        }
188
+    }
189
+
190
+    /**
191
+     * Sends message to opened socket connection client->server
192
+     *
193
+     * @param $payload
194
+     * @param string $opcode
195
+     * @throws \Exception
196
+     */
197
+    public function send($payload, string $opcode = CommonsContract::EVENT_TYPE_TEXT): void
198
+    {
199
+        if (!$this->isConnected) {
200
+            throw new \Exception(
201
+                "Can't send message. Connection is not established.",
202
+                CommonsContract::CLIENT_CONNECTION_NOT_ESTABLISHED
203
+            );
204
+        }
205
+
206
+        if (array_key_exists($opcode, self::$opcodes) === false) {
207
+            throw new BadOpcodeException(
208
+                sprintf("Bad opcode '%s'.  Try 'text' or 'binary'.", $opcode),
209
+                CommonsContract::CLIENT_BAD_OPCODE
210
+            );
211
+        }
212
+
213
+        $payloadLength = strlen($payload);
214
+        $fragmentCursor = 0;
215
+
216
+        while ($payloadLength > $fragmentCursor) {
217
+            $subPayload = substr($payload, $fragmentCursor, $this->config->getFragmentSize());
218
+            $fragmentCursor += $this->config->getFragmentSize();
219
+            $final = $payloadLength <= $fragmentCursor;
220
+            $this->sendFragment($final, $subPayload, $opcode, true);
221
+            $opcode = 'continuation';
222
+        }
223
+    }
224
+
225
+    /**
226
+     * Receives message client<-server
227
+     *
228
+     * @return string|null
229
+     * @throws \Exception
230
+     */
231
+    public function receive(): string|null
232
+    {
233
+        if (!$this->isConnected && $this->isClosing === false) {
234
+            throw new WebSocketException(
235
+                "Your unexpectedly disconnected from the server",
236
+                CommonsContract::CLIENT_CONNECTION_NOT_ESTABLISHED
237
+            );
238
+        }
239
+
240
+        $this->hugePayload = '';
241
+
242
+        return $this->receiveFragment();
243
+    }
244
+
245
+    /**
246
+     * Tell the socket to close.
247
+     *
248
+     * @param integer $status http://tools.ietf.org/html/rfc6455#section-7.4
249
+     * @param string $message A closing message, max 125 bytes.
250
+     * @return bool|null|string
251
+     * @throws \Exception
252
+     */
253
+    public function close(int $status = 1000, string $message = 'ttfn'): bool|null|string
254
+    {
255
+        $statusBin = sprintf('%016b', $status);
256
+        $statusStr = '';
257
+
258
+        foreach (str_split($statusBin, 8) as $binstr) {
259
+            $statusStr .= chr(bindec($binstr));
260
+        }
261
+
262
+        $this->send($statusStr . $message, CommonsContract::EVENT_TYPE_CLOSE);
263
+        $this->isClosing = true;
264
+
265
+        return $this->receive(); // Receiving a close frame will close the socket now.
266
+    }
267 267
 
268 268
 }
269 269
\ No newline at end of file
Please login to merge, or discard this patch.
src/Utils/Toolkit.php 2 patches
Indentation   +34 added lines, -34 removed lines patch added patch discarded remove patch
@@ -96,42 +96,42 @@
 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
-	/**
121
-	 * Helper to convert a binary to a string of '0' and '1'.
122
-	 *
123
-	 * @param string $string
124
-	 * @return string
125
-	 */
126
-	public static function sprintB(string $string): string
127
-	{
128
-		$return = '';
129
-		$strLen = strlen($string);
130
-		for ($i = 0; $i < $strLen; $i++) {
131
-			$return .= sprintf('%08b', ord($string[$i]));
132
-		}
120
+    /**
121
+     * Helper to convert a binary to a string of '0' and '1'.
122
+     *
123
+     * @param string $string
124
+     * @return string
125
+     */
126
+    public static function sprintB(string $string): string
127
+    {
128
+        $return = '';
129
+        $strLen = strlen($string);
130
+        for ($i = 0; $i < $strLen; $i++) {
131
+            $return .= sprintf('%08b', ord($string[$i]));
132
+        }
133 133
 
134
-		return $return;
135
-	}
134
+        return $return;
135
+    }
136 136
 
137 137
 }
138 138
\ 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
 	/**
Please login to merge, or discard this patch.
src/WebSocket.php 1 patch
Indentation   +232 added lines, -232 removed lines patch added patch discarded remove patch
@@ -24,237 +24,237 @@
 block discarded – undo
24 24
 class WebSocket implements WscCommonsContract
25 25
 {
26 26
 
27
-	use WSClientTrait;
28
-	use WSConnectionTrait;
29
-
30
-	/**
31
-	 * App version
32
-	 *
33
-	 * @var string
34
-	 */
35
-	public const VERSION = 'v1.2.0';
36
-
37
-	/**
38
-	 * @var resource|bool
39
-	 */
40
-	private $socket;
41
-
42
-	/**
43
-	 * @var string
44
-	 */
45
-	private string $lastOpcode;
46
-
47
-	/**
48
-	 * @var float|int
49
-	 */
50
-	private float|int $closeStatus;
51
-
52
-	/**
53
-	 * @var string|null
54
-	 */
55
-	private ?string $hugePayload;
56
-
57
-	/**
58
-	 * @var WebSocketConfig
59
-	 */
60
-	protected WebSocketConfig $config;
61
-
62
-	/**
63
-	 * @var string
64
-	 */
65
-	protected string $socketUrl;
66
-
67
-	/**
68
-	 * @var array|int[]
69
-	 */
70
-	private static array $opcodes = [
71
-		CommonsContract::EVENT_TYPE_CONTINUATION => 0,
72
-		CommonsContract::EVENT_TYPE_TEXT => 1,
73
-		CommonsContract::EVENT_TYPE_BINARY => 2,
74
-		CommonsContract::EVENT_TYPE_CLOSE => 8,
75
-		CommonsContract::EVENT_TYPE_PING => 9,
76
-		CommonsContract::EVENT_TYPE_PONG => 10,
77
-	];
78
-
79
-	/**
80
-	 * Sets parameters for Web Socket Client intercommunication
81
-	 *
82
-	 * @param ?SocketClient $client leave it empty if you want to use default socket client
83
-	 */
84
-	public function __construct(?SocketClient $client = null)
85
-	{
86
-		if ($client instanceof SocketClient) {
87
-
88
-			$this->onOpen = function ($socket) use ($client) {
89
-				$client->onOpen($socket);
90
-			};
91
-
92
-			$this->onClose = function ($socket, int $closeStatus) use ($client) {
93
-				$client->onClose($socket, $closeStatus);
94
-			};
95
-
96
-			$this->onError = function ($socket, WebSocketException $exception) use ($client) {
97
-				$client->onError($socket, $exception);
98
-			};
99
-
100
-			$this->onMessage = function ($socket, string $message) use ($client) {
101
-				$client->onMessage($socket, $message);
102
-			};
103
-		}
104
-
105
-		$this->config = $config ?? new WebSocketConfig();
106
-	}
107
-
108
-	/**
109
-	 * Init a proxy connection
110
-	 *
111
-	 * @return resource|false
112
-	 * @throws \InvalidArgumentException
113
-	 * @throws ConnectionException
114
-	 */
115
-	private function proxy()
116
-	{
117
-		$sock = @stream_socket_client(
118
-			WscCommonsContract::TCP_SCHEME . $this->config->getProxyIp() . ':' . $this->config->getProxyPort(),
119
-			$errno,
120
-			$errstr,
121
-			$this->config->getTimeout(),
122
-			STREAM_CLIENT_CONNECT,
123
-			$this->getStreamContext()
124
-		);
125
-
126
-		$write = "CONNECT {$this->config->getProxyIp()}:{$this->config->getProxyPort()} HTTP/1.1\r\n";
127
-		$auth = $this->config->getProxyAuth();
128
-
129
-		if ($auth !== NULL) {
130
-			$write .= "Proxy-Authorization: Basic {$auth}\r\n";
131
-		}
132
-
133
-		$write .= "\r\n";
134
-		fwrite($sock, $write);
135
-		$resp = fread($sock, 1024);
136
-
137
-		if (preg_match(self::PROXY_MATCH_RESP, $resp) === 1) {
138
-			return $sock;
139
-		}
140
-
141
-		throw new ConnectionException('Failed to connect to the host via proxy');
142
-	}
143
-
144
-	/**
145
-	 * @return mixed
146
-	 * @throws \InvalidArgumentException
147
-	 */
148
-	private function getStreamContext(): mixed
149
-	{
150
-		if ($this->config->getContext() !== null) {
151
-			// Suppress the error since we'll catch it below
152
-			if (@get_resource_type($this->config->getContext()) === 'stream-context') {
153
-				return $this->config->getContext();
154
-			}
155
-
156
-			throw new \InvalidArgumentException(
157
-				'Stream context is invalid',
158
-				CommonsContract::CLIENT_INVALID_STREAM_CONTEXT
159
-			);
160
-		}
161
-
162
-		return stream_context_create($this->config->getContextOptions());
163
-	}
164
-
165
-	/**
166
-	 * @param mixed $urlParts
167
-	 *
168
-	 * @return string
169
-	 */
170
-	private function getPathWithQuery(mixed $urlParts): string
171
-	{
172
-		$path = $urlParts['path'] ?? '/';
173
-		$query = $urlParts['query'] ?? '';
174
-		$fragment = $urlParts['fragment'] ?? '';
175
-		$pathWithQuery = $path;
176
-
177
-		if (!empty($query)) {
178
-			$pathWithQuery .= '?' . $query;
179
-		}
180
-
181
-		if (!empty($fragment)) {
182
-			$pathWithQuery .= '#' . $fragment;
183
-		}
184
-
185
-		return $pathWithQuery;
186
-	}
187
-
188
-	/**
189
-	 * @param string $pathWithQuery
190
-	 * @param array $headers
191
-	 *
192
-	 * @return string
193
-	 */
194
-	private function getHeaders(string $pathWithQuery, array $headers): string
195
-	{
196
-		return 'GET ' . $pathWithQuery . " HTTP/1.1\r\n"
197
-			. implode(
198
-				"\r\n",
199
-				array_map(
200
-					function ($key, $value) {
201
-						return "$key: $value";
202
-					},
203
-					array_keys($headers),
204
-					$headers
205
-				)
206
-			)
207
-			. "\r\n\r\n";
208
-	}
209
-
210
-	/**
211
-	 * @param int $timeout
212
-	 * @param null $microSecs
213
-	 *
214
-	 * @return void
215
-	 */
216
-	public function setTimeout(int $timeout, $microSecs = null): void
217
-	{
218
-		$this->config->setTimeout($timeout);
219
-		if ($this->socket && get_resource_type($this->socket) === 'stream') {
220
-			stream_set_timeout($this->socket, $timeout, $microSecs);
221
-		}
222
-	}
223
-
224
-	/**
225
-	 * @param string $name
226
-	 * @param array $arguments
227
-	 *
228
-	 * @return mixed
229
-	 * @throws \Exception
230
-	 */
231
-	public function __call(string $name, array $arguments): mixed
232
-	{
233
-		if (property_exists($this, $name)) {
234
-			return $this->$name;
235
-		}
236
-
237
-		if (method_exists($this, $name)) {
238
-			return call_user_func_array([$this, $name], $arguments);
239
-		}
240
-
241
-		if (str_starts_with($name, 'get')) {
242
-			$property = lcfirst(substr($name, 3));
243
-
244
-			if (property_exists($this, $property)) {
245
-				return $this->{$property};
246
-			}
247
-		}
248
-
249
-		if (str_starts_with($name, 'set')) {
250
-			$property = lcfirst(substr($name, 3));
251
-			if (property_exists($this, $property)) {
252
-				$this->{$property} = $arguments[0];
253
-				return $this;
254
-			}
255
-		}
256
-
257
-		throw new \Exception(sprintf("Method '%s' does not exist.", $name));
258
-	}
27
+    use WSClientTrait;
28
+    use WSConnectionTrait;
29
+
30
+    /**
31
+     * App version
32
+     *
33
+     * @var string
34
+     */
35
+    public const VERSION = 'v1.2.0';
36
+
37
+    /**
38
+     * @var resource|bool
39
+     */
40
+    private $socket;
41
+
42
+    /**
43
+     * @var string
44
+     */
45
+    private string $lastOpcode;
46
+
47
+    /**
48
+     * @var float|int
49
+     */
50
+    private float|int $closeStatus;
51
+
52
+    /**
53
+     * @var string|null
54
+     */
55
+    private ?string $hugePayload;
56
+
57
+    /**
58
+     * @var WebSocketConfig
59
+     */
60
+    protected WebSocketConfig $config;
61
+
62
+    /**
63
+     * @var string
64
+     */
65
+    protected string $socketUrl;
66
+
67
+    /**
68
+     * @var array|int[]
69
+     */
70
+    private static array $opcodes = [
71
+        CommonsContract::EVENT_TYPE_CONTINUATION => 0,
72
+        CommonsContract::EVENT_TYPE_TEXT => 1,
73
+        CommonsContract::EVENT_TYPE_BINARY => 2,
74
+        CommonsContract::EVENT_TYPE_CLOSE => 8,
75
+        CommonsContract::EVENT_TYPE_PING => 9,
76
+        CommonsContract::EVENT_TYPE_PONG => 10,
77
+    ];
78
+
79
+    /**
80
+     * Sets parameters for Web Socket Client intercommunication
81
+     *
82
+     * @param ?SocketClient $client leave it empty if you want to use default socket client
83
+     */
84
+    public function __construct(?SocketClient $client = null)
85
+    {
86
+        if ($client instanceof SocketClient) {
87
+
88
+            $this->onOpen = function ($socket) use ($client) {
89
+                $client->onOpen($socket);
90
+            };
91
+
92
+            $this->onClose = function ($socket, int $closeStatus) use ($client) {
93
+                $client->onClose($socket, $closeStatus);
94
+            };
95
+
96
+            $this->onError = function ($socket, WebSocketException $exception) use ($client) {
97
+                $client->onError($socket, $exception);
98
+            };
99
+
100
+            $this->onMessage = function ($socket, string $message) use ($client) {
101
+                $client->onMessage($socket, $message);
102
+            };
103
+        }
104
+
105
+        $this->config = $config ?? new WebSocketConfig();
106
+    }
107
+
108
+    /**
109
+     * Init a proxy connection
110
+     *
111
+     * @return resource|false
112
+     * @throws \InvalidArgumentException
113
+     * @throws ConnectionException
114
+     */
115
+    private function proxy()
116
+    {
117
+        $sock = @stream_socket_client(
118
+            WscCommonsContract::TCP_SCHEME . $this->config->getProxyIp() . ':' . $this->config->getProxyPort(),
119
+            $errno,
120
+            $errstr,
121
+            $this->config->getTimeout(),
122
+            STREAM_CLIENT_CONNECT,
123
+            $this->getStreamContext()
124
+        );
125
+
126
+        $write = "CONNECT {$this->config->getProxyIp()}:{$this->config->getProxyPort()} HTTP/1.1\r\n";
127
+        $auth = $this->config->getProxyAuth();
128
+
129
+        if ($auth !== NULL) {
130
+            $write .= "Proxy-Authorization: Basic {$auth}\r\n";
131
+        }
132
+
133
+        $write .= "\r\n";
134
+        fwrite($sock, $write);
135
+        $resp = fread($sock, 1024);
136
+
137
+        if (preg_match(self::PROXY_MATCH_RESP, $resp) === 1) {
138
+            return $sock;
139
+        }
140
+
141
+        throw new ConnectionException('Failed to connect to the host via proxy');
142
+    }
143
+
144
+    /**
145
+     * @return mixed
146
+     * @throws \InvalidArgumentException
147
+     */
148
+    private function getStreamContext(): mixed
149
+    {
150
+        if ($this->config->getContext() !== null) {
151
+            // Suppress the error since we'll catch it below
152
+            if (@get_resource_type($this->config->getContext()) === 'stream-context') {
153
+                return $this->config->getContext();
154
+            }
155
+
156
+            throw new \InvalidArgumentException(
157
+                'Stream context is invalid',
158
+                CommonsContract::CLIENT_INVALID_STREAM_CONTEXT
159
+            );
160
+        }
161
+
162
+        return stream_context_create($this->config->getContextOptions());
163
+    }
164
+
165
+    /**
166
+     * @param mixed $urlParts
167
+     *
168
+     * @return string
169
+     */
170
+    private function getPathWithQuery(mixed $urlParts): string
171
+    {
172
+        $path = $urlParts['path'] ?? '/';
173
+        $query = $urlParts['query'] ?? '';
174
+        $fragment = $urlParts['fragment'] ?? '';
175
+        $pathWithQuery = $path;
176
+
177
+        if (!empty($query)) {
178
+            $pathWithQuery .= '?' . $query;
179
+        }
180
+
181
+        if (!empty($fragment)) {
182
+            $pathWithQuery .= '#' . $fragment;
183
+        }
184
+
185
+        return $pathWithQuery;
186
+    }
187
+
188
+    /**
189
+     * @param string $pathWithQuery
190
+     * @param array $headers
191
+     *
192
+     * @return string
193
+     */
194
+    private function getHeaders(string $pathWithQuery, array $headers): string
195
+    {
196
+        return 'GET ' . $pathWithQuery . " HTTP/1.1\r\n"
197
+            . implode(
198
+                "\r\n",
199
+                array_map(
200
+                    function ($key, $value) {
201
+                        return "$key: $value";
202
+                    },
203
+                    array_keys($headers),
204
+                    $headers
205
+                )
206
+            )
207
+            . "\r\n\r\n";
208
+    }
209
+
210
+    /**
211
+     * @param int $timeout
212
+     * @param null $microSecs
213
+     *
214
+     * @return void
215
+     */
216
+    public function setTimeout(int $timeout, $microSecs = null): void
217
+    {
218
+        $this->config->setTimeout($timeout);
219
+        if ($this->socket && get_resource_type($this->socket) === 'stream') {
220
+            stream_set_timeout($this->socket, $timeout, $microSecs);
221
+        }
222
+    }
223
+
224
+    /**
225
+     * @param string $name
226
+     * @param array $arguments
227
+     *
228
+     * @return mixed
229
+     * @throws \Exception
230
+     */
231
+    public function __call(string $name, array $arguments): mixed
232
+    {
233
+        if (property_exists($this, $name)) {
234
+            return $this->$name;
235
+        }
236
+
237
+        if (method_exists($this, $name)) {
238
+            return call_user_func_array([$this, $name], $arguments);
239
+        }
240
+
241
+        if (str_starts_with($name, 'get')) {
242
+            $property = lcfirst(substr($name, 3));
243
+
244
+            if (property_exists($this, $property)) {
245
+                return $this->{$property};
246
+            }
247
+        }
248
+
249
+        if (str_starts_with($name, 'set')) {
250
+            $property = lcfirst(substr($name, 3));
251
+            if (property_exists($this, $property)) {
252
+                $this->{$property} = $arguments[0];
253
+                return $this;
254
+            }
255
+        }
256
+
257
+        throw new \Exception(sprintf("Method '%s' does not exist.", $name));
258
+    }
259 259
 
260 260
 }
261 261
\ No newline at end of file
Please login to merge, or discard this patch.