Passed
Push — master ( da3f2a...c03036 )
by Shahrad
01:59
created

WSClientTrait   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 241
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 103
c 1
b 0
f 0
dl 0
loc 241
rs 9.6
wmc 35

7 Methods

Rating   Name   Duplication   Size   Complexity  
A getPayloadLength() 0 13 3
A generateKey() 0 10 2
B receiveFragment() 0 57 9
A getPayloadData() 0 27 5
A validateResponse() 0 17 3
A getHostUri() 0 10 3
B sendFragment() 0 47 10
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\WebSocketConfig;
9
10
/**
11
 * WSClientTrait class
12
 *
13
 * @link    https://github.com/shahradelahi/easy-http
14
 * @author  Arthur Kushman (https://github.com/arthurkushman)
15
 * @license https://github.com/shahradelahi/easy-http/blob/master/LICENSE (MIT License)
16
 */
17
trait WSClientTrait
18
{
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");
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...
31
		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...
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)));
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...
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
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...
76
		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...
77
			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...
78
				$data = $this->read(2); // 126: Payload is a 16-bit unsigned int
0 ignored issues
show
Bug introduced by
It seems like read() 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

78
				/** @scrutinizer ignore-call */ 
79
    $data = $this->read(2); // 126: Payload is a 16-bit unsigned int
Loading history...
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;
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...
146
		}
147
148
		$payloadLength = $this->getPayloadLength($data);
149
		$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

149
		$payload = $this->getPayloadData($data, /** @scrutinizer ignore-type */ $payloadLength);
Loading history...
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;
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...
156
				$payload = substr($payload, 2);
157
158
				if (!$this->isClosing) {
159
					$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

159
					$this->/** @scrutinizer ignore-call */ 
160
            send($statusBin . 'Close acknowledged: ' . $status,
Loading history...
160
						CommonsContract::EVENT_TYPE_CLOSE); // Respond.
161
				}
162
			}
163
164
			if ($this->isClosing) {
165
				$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...
166
			}
167
168
			fclose($this->socket);
169
			$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...
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;
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...
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) {
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...
209
			$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...
210
			$frameHeadBin .= sprintf('%064b', $payloadLen);
211
		} 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...
212
			$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...
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));
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

222
			$frame .= chr(/** @scrutinizer ignore-type */ bindec($binstr));
Loading history...
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];
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...
237
		}
238
239
		$this->write($frame);
0 ignored issues
show
Bug introduced by
It seems like write() 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

239
		$this->/** @scrutinizer ignore-call */ 
240
         write($frame);
Loading history...
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++) {
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...
254
			$key .= $chars[random_int(0, $chLen - 1)];
255
		}
256
257
		return base64_encode($key);
258
	}
259
260
}