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
![]() |
|||
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 | |||
260 | } |