Passed
Push — master ( beaa4b...da3f2a )
by Shahrad
02:02
created

WSClientTrait   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 221
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 96
dl 0
loc 221
c 1
b 0
f 0
rs 9.76
wmc 33

6 Methods

Rating   Name   Duplication   Size   Complexity  
A getPayloadLength() 0 13 3
B receiveFragment() 0 57 9
A getPayloadData() 0 27 5
A validateResponse() 0 16 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\Utils\WSConfig;
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 WSConfig $config
24
	 * @param string $pathWithQuery
25
	 * @param string $key
26
	 * @throws ConnectionException
27
	 */
28
	private function validateResponse(WSConfig $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('Server sent bad upgrade response.',
43
				CommonsContract::CLIENT_INVALID_UPGRADE_RESPONSE);
44
		}
45
	}
46
47
	/**
48
	 *  Gets host uri based on protocol
49
	 *
50
	 * @param WSConfig $config
51
	 * @return string
52
	 * @throws BadUriException
53
	 */
54
	private function getHostUri(WSConfig $config): string
55
	{
56
		if (in_array($config->getScheme(), ['ws', 'wss'], true) === false) {
57
			throw new BadUriException(
58
				"Url should have scheme ws or wss, not '{$config->getScheme()}' from URI '$this->socketUrl' .",
59
				CommonsContract::CLIENT_INCORRECT_SCHEME
60
			);
61
		}
62
63
		return ($config->getScheme() === 'wss' ? 'ssl' : 'tcp') . '://' . $config->getHost();
64
	}
65
66
	/**
67
	 * @param string $data
68
	 * @return float|int
69
	 * @throws ConnectionException
70
	 */
71
	private function getPayloadLength(string $data): float|int
72
	{
73
		$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...
74
		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...
75
			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...
76
				$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

76
				/** @scrutinizer ignore-call */ 
77
    $data = $this->read(2); // 126: Payload is a 16-bit unsigned int
Loading history...
77
			} else {
78
				$data = $this->read(8); // 127: Payload is a 64-bit unsigned int
79
			}
80
			$payloadLength = bindec(self::sprintB($data));
81
		}
82
83
		return $payloadLength;
84
	}
85
86
	/**
87
	 * @param string $data
88
	 * @param int $payloadLength
89
	 * @return string
90
	 * @throws ConnectionException
91
	 */
92
	private function getPayloadData(string $data, int $payloadLength): string
93
	{
94
		// Masking?
95
		$mask = (bool)(ord($data[1]) >> 7);  // Bit 0 in byte 1
96
		$payload = '';
97
		$maskingKey = '';
98
99
		// Get masking key.
100
		if ($mask) {
101
			$maskingKey = $this->read(4);
102
		}
103
104
		// Get the actual payload, if any (might not be for e.g. close frames.
105
		if ($payloadLength > 0) {
106
			$data = $this->read($payloadLength);
107
108
			if ($mask) {
109
				// Unmask payload.
110
				for ($i = 0; $i < $payloadLength; $i++) {
111
					$payload .= ($data[$i] ^ $maskingKey[$i % 4]);
112
				}
113
			} else {
114
				$payload = $data;
115
			}
116
		}
117
118
		return $payload;
119
	}
120
121
	/**
122
	 * @return null|string
123
	 * @throws \Exception
124
	 */
125
	protected function receiveFragment(): ?string
126
	{
127
		$data = $this->read(2);
128
129
		$final = (bool)(ord($data[0]) & 1 << 7);
130
131
		$opcodeInt = ord($data[0]) & 31;
132
		$opcodeInts = array_flip(self::$opcodes);
133
		if (!array_key_exists($opcodeInt, $opcodeInts)) {
134
			throw new ConnectionException(
135
				"Bad opcode in websocket frame: $opcodeInt",
136
				CommonsContract::CLIENT_BAD_OPCODE
137
			);
138
		}
139
140
		$opcode = $opcodeInts[$opcodeInt];
141
142
		if ($opcode !== 'continuation') {
143
			$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...
144
		}
145
146
		$payloadLength = $this->getPayloadLength($data);
147
		$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

147
		$payload = $this->getPayloadData($data, /** @scrutinizer ignore-type */ $payloadLength);
Loading history...
148
149
		if ($opcode === CommonsContract::EVENT_TYPE_CLOSE) {
150
			if ($payloadLength >= 2) {
151
				$statusBin = $payload[0] . $payload[1];
152
				$status = bindec(sprintf('%08b%08b', ord($payload[0]), ord($payload[1])));
153
				$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...
154
				$payload = substr($payload, 2);
155
156
				if (!$this->isClosing) {
157
					$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

157
					$this->/** @scrutinizer ignore-call */ 
158
            send($statusBin . 'Close acknowledged: ' . $status,
Loading history...
158
						CommonsContract::EVENT_TYPE_CLOSE); // Respond.
159
				}
160
			}
161
162
			if ($this->isClosing) {
163
				$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...
164
			}
165
166
			fclose($this->socket);
167
			$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...
168
		}
169
170
		if (!$final) {
171
			$this->hugePayload .= $payload;
172
173
			return NULL;
174
		}
175
176
		if ($this->hugePayload) {
177
			$payload = $this->hugePayload .= $payload;
178
			$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...
179
		}
180
181
		return $payload;
182
	}
183
184
	/**
185
	 * @param $final
186
	 * @param $payload
187
	 * @param $opcode
188
	 * @param $masked
189
	 * @throws \Exception
190
	 */
191
	protected function sendFragment($final, $payload, $opcode, $masked): void
192
	{
193
		// Binary string for header.
194
		$frameHeadBin = '';
195
		// Write FIN, final fragment bit.
196
		$frameHeadBin .= (bool)$final ? '1' : '0';
197
		// RSV 1, 2, & 3 false and unused.
198
		$frameHeadBin .= '000';
199
		// Opcode rest of the byte.
200
		$frameHeadBin .= sprintf('%04b', self::$opcodes[$opcode]);
201
		// Use masking?
202
		$frameHeadBin .= $masked ? '1' : '0';
203
204
		// 7 bits of payload length...
205
		$payloadLen = strlen($payload);
206
		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...
207
			$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...
208
			$frameHeadBin .= sprintf('%064b', $payloadLen);
209
		} 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...
210
			$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...
211
			$frameHeadBin .= sprintf('%016b', $payloadLen);
212
		} else {
213
			$frameHeadBin .= sprintf('%07b', $payloadLen);
214
		}
215
216
		$frame = '';
217
218
		// Write frame head to frame.
219
		foreach (str_split($frameHeadBin, 8) as $binstr) {
220
			$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

220
			$frame .= chr(/** @scrutinizer ignore-type */ bindec($binstr));
Loading history...
221
		}
222
		// Handle masking
223
		if ($masked) {
224
			// generate a random mask:
225
			$mask = '';
226
			for ($i = 0; $i < 4; $i++) {
227
				$mask .= chr(random_int(0, 255));
228
			}
229
			$frame .= $mask;
230
		}
231
232
		// Append payload to frame:
233
		for ($i = 0; $i < $payloadLen; $i++) {
234
			$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...
235
		}
236
237
		$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

237
		$this->/** @scrutinizer ignore-call */ 
238
         write($frame);
Loading history...
238
	}
239
240
}