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

WebSocket::getStreamContext()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
c 0
b 0
f 0
nc 3
nop 0
dl 0
loc 15
rs 10
1
<?php
2
3
namespace EasyHttp;
4
5
use EasyHttp\Contracts\CommonsContract;
6
use EasyHttp\Contracts\WscCommonsContract;
7
use EasyHttp\Exceptions\ConnectionException;
8
use EasyHttp\Exceptions\WebSocketException;
9
use EasyHttp\Traits\WSClientTrait;
10
use EasyHttp\Traits\WSConnectionTrait;
11
12
/**
13
 * WebSocket class
14
 *
15
 * @method bool   isConnected()    This method returns true if the connection is established.
16
 * @method int    getCloseStatus() This method returns the close status after the connection is closed.
17
 * @method string getSocketUrl()   This method returns the URL of the socket.
18
 * @method string getLastOpcode()  This method returns the last opcode.
19
 *
20
 * @link    https://github.com/shahradelahi/easy-http
21
 * @author  Shahrad Elahi (https://github.com/shahradelahi)
22
 * @license https://github.com/shahradelahi/easy-http/blob/master/LICENSE (MIT License)
23
 */
24
class WebSocket implements WscCommonsContract
25
{
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();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $config seems to never exist and therefore isset should always be false.
Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $sock can also be of type false; however, parameter $stream of fwrite() does only seem to accept resource, 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

134
		fwrite(/** @scrutinizer ignore-type */ $sock, $write);
Loading history...
135
		$resp = fread($sock, 1024);
0 ignored issues
show
Bug introduced by
It seems like $sock can also be of type false; however, parameter $stream of fread() does only seem to accept resource, 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

135
		$resp = fread(/** @scrutinizer ignore-type */ $sock, 1024);
Loading history...
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
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $microSecs is correct as it would always require null to be passed?
Loading history...
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') {
0 ignored issues
show
Bug introduced by
It seems like $this->socket can also be of type true; however, parameter $resource of get_resource_type() does only seem to accept resource, 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

219
		if ($this->socket && get_resource_type(/** @scrutinizer ignore-type */ $this->socket) === 'stream') {
Loading history...
220
			stream_set_timeout($this->socket, $timeout, $microSecs);
0 ignored issues
show
Bug introduced by
It seems like $this->socket can also be of type true; however, parameter $stream of stream_set_timeout() does only seem to accept resource, 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
			stream_set_timeout(/** @scrutinizer ignore-type */ $this->socket, $timeout, $microSecs);
Loading history...
Bug introduced by
$microSecs of type null is incompatible with the type integer expected by parameter $microseconds of stream_set_timeout(). ( Ignorable by Annotation )

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

220
			stream_set_timeout($this->socket, $timeout, /** @scrutinizer ignore-type */ $microSecs);
Loading history...
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
260
}