Passed
Push — master ( d048db...e350b2 )
by Shahrad
01:41
created

WSClientTrait::read()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 3
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace EasyHttp\Traits;
4
5
use EasyHttp\Contracts\CommonsContract;
6
use EasyHttp\Exceptions\BadUriException;
7
use EasyHttp\Exceptions\ConnectionException;
8
use EasyHttp\Middleware;
9
use EasyHttp\Utils\Toolkit;
10
use EasyHttp\WebSocketConfig;
11
12
/**
13
 * WSClientTrait class
14
 *
15
 * @link    https://github.com/shahradelahi/easy-http
16
 * @author  Arthur Kushman (https://github.com/arthurkushman)
17
 * @license https://github.com/shahradelahi/easy-http/blob/master/LICENSE (MIT License)
18
 */
19
trait WSClientTrait
20
{
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");
0 ignored issues
show
Bug introduced by
The constant EasyHttp\Traits\WSClient...DEFAULT_RESPONSE_HEADER was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
33
		if (!preg_match(self::SEC_WEBSOCKET_ACCEPT_PTTRN, $response, $matches)) {
0 ignored issues
show
Bug introduced by
The constant EasyHttp\Traits\WSClient..._WEBSOCKET_ACCEPT_PTTRN was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
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)));
0 ignored issues
show
Bug introduced by
The constant EasyHttp\Traits\WSClientTrait::SERVER_KEY_ACCEPT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
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
0 ignored issues
show
Bug introduced by
The constant EasyHttp\Traits\WSClientTrait::MASK_127 was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
78
		if ($payloadLength > self::MASK_125) {
0 ignored issues
show
Bug introduced by
The constant EasyHttp\Traits\WSClientTrait::MASK_125 was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
79
			if ($payloadLength === self::MASK_126) {
0 ignored issues
show
Bug introduced by
The constant EasyHttp\Traits\WSClientTrait::MASK_126 was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
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));
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type null; however, parameter $string of EasyHttp\Utils\Toolkit::sprintB() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

84
			$payloadLength = bindec(Toolkit::sprintB(/** @scrutinizer ignore-type */ $data));
Loading history...
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;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $payload could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
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;
0 ignored issues
show
Bug Best Practice introduced by
The property lastOpcode does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
151
		}
152
153
		$payloadLength = $this->getPayloadLength($data);
0 ignored issues
show
Bug introduced by
$data of type null is incompatible with the type string expected by parameter $data of EasyHttp\Traits\WSClientTrait::getPayloadLength(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

153
		$payloadLength = $this->getPayloadLength(/** @scrutinizer ignore-type */ $data);
Loading history...
154
		$payload = $this->getPayloadData($data, $payloadLength);
0 ignored issues
show
Bug introduced by
It seems like $payloadLength can also be of type double; however, parameter $payloadLength of EasyHttp\Traits\WSClientTrait::getPayloadData() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

154
		$payload = $this->getPayloadData($data, /** @scrutinizer ignore-type */ $payloadLength);
Loading history...
Bug introduced by
$data of type null is incompatible with the type string expected by parameter $data of EasyHttp\Traits\WSClientTrait::getPayloadData(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

154
		$payload = $this->getPayloadData(/** @scrutinizer ignore-type */ $data, $payloadLength);
Loading history...
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;
0 ignored issues
show
Bug Best Practice introduced by
The property closeStatus does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
161
				$payload = substr($payload, 2);
162
163
				if (!$this->isClosing) {
164
					$this->send($statusBin . 'Close acknowledged: ' . $status,
0 ignored issues
show
Bug introduced by
It seems like send() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

164
					$this->/** @scrutinizer ignore-call */ 
165
            send($statusBin . 'Close acknowledged: ' . $status,
Loading history...
165
						CommonsContract::EVENT_TYPE_CLOSE); // Respond.
166
				}
167
			}
168
169
			if ($this->isClosing) {
170
				$this->isClosing = false; // A close response, all done.
0 ignored issues
show
Bug Best Practice introduced by
The property isClosing does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
171
			}
172
173
			fclose($this->socket);
174
			$this->isConnected = false;
0 ignored issues
show
Bug Best Practice introduced by
The property isConnected does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
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;
0 ignored issues
show
Bug Best Practice introduced by
The property hugePayload does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
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) {
0 ignored issues
show
Bug introduced by
The constant EasyHttp\Traits\WSClientTrait::MAX_BYTES_READ was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
214
			$frameHeadBin .= decbin(self::MASK_127);
0 ignored issues
show
Bug introduced by
The constant EasyHttp\Traits\WSClientTrait::MASK_127 was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
215
			$frameHeadBin .= sprintf('%064b', $payloadLen);
216
		} else if ($payloadLen > self::MASK_125) {
0 ignored issues
show
Bug introduced by
The constant EasyHttp\Traits\WSClientTrait::MASK_125 was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
217
			$frameHeadBin .= decbin(self::MASK_126);
0 ignored issues
show
Bug introduced by
The constant EasyHttp\Traits\WSClientTrait::MASK_126 was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
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));
0 ignored issues
show
Bug introduced by
It seems like bindec($binstr) can also be of type double; however, parameter $codepoint of chr() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

227
			$frame .= chr(/** @scrutinizer ignore-type */ bindec($binstr));
Loading history...
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];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $mask does not seem to be defined for all execution paths leading up to this point.
Loading history...
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++) {
0 ignored issues
show
Bug introduced by
The constant EasyHttp\Traits\WSClientTrait::KEY_GEN_LENGTH was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
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()) {
0 ignored issues
show
Bug introduced by
It seems like isConnected() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

272
		if ($this->socket && $this->/** @scrutinizer ignore-call */ isConnected()) {
Loading history...
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
289
}