Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 100 |
|
0.00% |
0 / 10 |
CRAP | |
0.00% |
0 / 1 |
Server | |
0.00% |
0 / 100 |
|
0.00% |
0 / 10 |
930 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
hasInternet | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
create | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
setLimit | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
handshake | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
72 | |||
run | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
90 | |||
shutdown | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
connectClient | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
disconnectClient | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
unmask | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | /** |
3 | * Jingga |
4 | * |
5 | * PHP Version 8.1 |
6 | * |
7 | * @package phpOMS\Socket\Server |
8 | * @copyright Dennis Eichhorn |
9 | * @license OMS License 2.0 |
10 | * @version 1.0.0 |
11 | * @link https://jingga.app |
12 | */ |
13 | declare(strict_types=1); |
14 | |
15 | namespace phpOMS\Socket\Server; |
16 | |
17 | use phpOMS\Account\NullAccount; |
18 | use phpOMS\Application\ApplicationAbstract; |
19 | use phpOMS\Message\Socket\PacketManager; |
20 | use phpOMS\Socket\Client\ClientConnection; |
21 | use phpOMS\Socket\SocketAbstract; |
22 | |
23 | /** |
24 | * Server class. |
25 | * |
26 | * @package phpOMS\Socket\Server |
27 | * @license OMS License 2.0 |
28 | * @link https://jingga.app |
29 | * @since 1.0.0 |
30 | */ |
31 | class Server extends SocketAbstract |
32 | { |
33 | /** |
34 | * Socket connection limit. |
35 | * |
36 | * @var int |
37 | * @since 1.0.0 |
38 | */ |
39 | private $limit = 10; |
40 | |
41 | /** |
42 | * Client connections. |
43 | * |
44 | * @var array |
45 | * @since 1.0.0 |
46 | */ |
47 | private $conn = []; |
48 | |
49 | /** |
50 | * Packet manager. |
51 | * |
52 | * @var PacketManager |
53 | * @since 1.0.0 |
54 | */ |
55 | private $packetManager = null; |
56 | |
57 | private $clientManager = null; |
58 | |
59 | private $verbose = true; |
60 | |
61 | /** |
62 | * Socket application. |
63 | * |
64 | * @var ApplicationAbstract |
65 | * @since 1.0.0 |
66 | */ |
67 | private $app = null; |
68 | |
69 | /** |
70 | * Constructor. |
71 | * |
72 | * @param ApplicationAbstract $app Socket application |
73 | * |
74 | * @since 1.0.0 |
75 | */ |
76 | public function __construct(ApplicationAbstract $app) |
77 | { |
78 | $this->app = $app; |
79 | $this->clientManager = new ClientManager(); |
80 | $this->packetManager = new PacketManager($this->app->router, $this->app->dispatcher); |
81 | } |
82 | |
83 | /** |
84 | * Has internet connection. |
85 | * |
86 | * @return bool |
87 | * |
88 | * @since 1.0.0 |
89 | */ |
90 | public static function hasInternet() : bool |
91 | { |
92 | $connected = @\fsockopen("www.google.com", 80); |
93 | |
94 | if ($connected) { |
95 | \fclose($connected); |
96 | |
97 | return true; |
98 | } else { |
99 | return false; |
100 | } |
101 | } |
102 | |
103 | /** |
104 | * {@inheritdoc} |
105 | */ |
106 | public function create(string $ip, int $port) : void |
107 | { |
108 | $this->app->logger->info('Creating socket...'); |
109 | parent::create($ip, $port); |
110 | $this->app->logger->info('Binding socket...'); |
111 | \socket_bind($this->sock, $this->ip, $this->port); |
112 | } |
113 | |
114 | /** |
115 | * Set connection limit. |
116 | * |
117 | * @param int $limit Connection limit |
118 | * |
119 | * @return void |
120 | * |
121 | * @since 1.0.0 |
122 | */ |
123 | public function setLimit(int $limit) : void |
124 | { |
125 | $this->limit = $limit; |
126 | } |
127 | |
128 | /** |
129 | * Perform client-server handshake during login |
130 | * |
131 | * @param mixed $client Client |
132 | * @param mixed $headers Header |
133 | * |
134 | * @return bool |
135 | * |
136 | * @since 1.0.0 |
137 | */ |
138 | public function handshake($client, $headers) : bool |
139 | { |
140 | // todo: different handshake for normal tcp connection |
141 | if ($client !== null) { |
142 | return true; |
143 | } |
144 | |
145 | if (\preg_match("/Sec-WebSocket-Version: (.*)\r\n/", $headers, $match) === false) { |
146 | return false; |
147 | } |
148 | |
149 | $version = (int) ($match[1] ?? -1); |
150 | |
151 | if ($version !== 13) { |
152 | return false; |
153 | } |
154 | |
155 | if (\preg_match("/GET (.*) HTTP/", $headers, $match)) { |
156 | $root = $match[1]; |
157 | } |
158 | |
159 | if (\preg_match("/Host: (.*)\r\n/", $headers, $match)) { |
160 | $host = $match[1]; |
161 | } |
162 | |
163 | if (\preg_match("/Origin: (.*)\r\n/", $headers, $match)) { |
164 | $origin = $match[1]; |
165 | } |
166 | |
167 | $key = ''; |
168 | if (\preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $headers, $match)) { |
169 | $key = $match[1]; |
170 | } |
171 | |
172 | $acceptKey = $key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; |
173 | $acceptKey = \base64_encode(\sha1($acceptKey, true)); |
174 | $upgrade = "HTTP/1.1 101 Switching Protocols\r\n" . |
175 | "Upgrade: websocket\r\n" . |
176 | "Connection: Upgrade\r\n" . |
177 | "Sec-WebSocket-Accept: ${acceptKey}" . |
178 | "\r\n\r\n"; |
179 | \socket_write($client->getSocket(), $upgrade); |
180 | $client->setHandshake(true); |
181 | |
182 | return true; |
183 | } |
184 | |
185 | /** |
186 | * {@inheritdoc} |
187 | */ |
188 | public function run() : void |
189 | { |
190 | $this->app->logger->info('Start listening...'); |
191 | @\socket_listen($this->sock); |
192 | @\socket_set_nonblock($this->sock); |
193 | $this->conn[] = $this->sock; |
194 | |
195 | $this->app->logger->info('Is running...'); |
196 | while ($this->run) { |
197 | $read = $this->conn; |
198 | |
199 | $write = null; |
200 | $except = null; |
201 | |
202 | if (\socket_select($read, $write, $except, 0) < 1) { |
203 | // error |
204 | // socket_last_error(); |
205 | // socket_strerror(socket_last_error()); |
206 | // socket_clear_error(); |
207 | $a = 2; |
208 | } |
209 | |
210 | foreach ($read as $key => $socket) { |
211 | if ($this->sock === $socket) { |
212 | $newc = @\socket_accept($this->sock); |
213 | $this->connectClient($newc); |
214 | } else { |
215 | $client = $this->clientManager->getBySocket($socket); |
216 | $data = @\socket_read($socket, 1024, \PHP_NORMAL_READ); |
217 | |
218 | if ($data === false) { |
219 | \socket_close($socket); |
220 | } |
221 | |
222 | $data = \is_string($data) ? \trim($data) : ''; |
223 | |
224 | if (!$client->getHandshake()) { |
225 | $this->app->logger->debug('Doing handshake...'); |
226 | if ($this->handshake($client, $data)) { |
227 | $client->setHandshake(true); |
228 | $this->app->logger->debug('Handshake succeeded.'); |
229 | } else { |
230 | $this->app->logger->debug('Handshake failed.'); |
231 | $this->disconnectClient($client); |
232 | } |
233 | } else { |
234 | $this->packetManager->handle($data, $client); |
235 | } |
236 | } |
237 | } |
238 | } |
239 | $this->app->logger->info('Is shutdown...'); |
240 | |
241 | $this->close(); |
242 | } |
243 | |
244 | /** |
245 | * Perform server shutdown |
246 | * |
247 | * @param mixed $request Request |
248 | * |
249 | * @return void |
250 | * |
251 | * @since 1.0.0 |
252 | */ |
253 | public function shutdown($request) : void |
254 | { |
255 | $msg = 'shutdown' . "\n"; |
256 | \socket_write($this->clientManager->get($request->header->account)->getSocket(), $msg, \strlen($msg)); |
257 | |
258 | $this->run = false; |
259 | } |
260 | |
261 | /** |
262 | * Connect a client |
263 | * |
264 | * @param mixed $socket Socket |
265 | * |
266 | * @return void |
267 | * |
268 | * @since 1.0.0 |
269 | */ |
270 | public function connectClient($socket) : void |
271 | { |
272 | $this->app->logger->debug('Connecting client...'); |
273 | $this->app->accountManager->add(new NullAccount(1)); |
274 | $this->clientManager->add($client = new ClientConnection(new NullAccount(1), $socket)); |
275 | |
276 | $this->conn[$client->getId()] = $socket; |
277 | |
278 | $this->app->logger->debug('Connected client.'); |
279 | } |
280 | |
281 | /** |
282 | * Disconnect a client |
283 | * |
284 | * @param mixed $client Client |
285 | * |
286 | * @return void |
287 | * |
288 | * @since 1.0.0 |
289 | */ |
290 | public function disconnectClient($client) : void |
291 | { |
292 | $this->app->logger->debug('Disconnecting client...'); |
293 | $client->setConnected(false); |
294 | $client->setHandshake(false); |
295 | \socket_shutdown($client->getSocket(), 2); |
296 | \socket_close($client->getSocket()); |
297 | |
298 | if (isset($this->conn[$client->getId()])) { |
299 | unset($this->conn[$client->getId()]); |
300 | } |
301 | |
302 | $this->clientManager->remove($client->getId()); |
303 | $this->app->logger->debug('Disconnected client.'); |
304 | } |
305 | |
306 | /** |
307 | * Unmask payload |
308 | * |
309 | * @param mixed $payload Payload |
310 | * |
311 | * @return string |
312 | * |
313 | * @since 1.0.0 |
314 | */ |
315 | private function unmask($payload) : string |
316 | { |
317 | $length = \ord($payload[1]) & 127; |
318 | if ($length == 126) { |
319 | $masks = \substr($payload, 4, 4); |
320 | $data = \substr($payload, 8); |
321 | } elseif ($length == 127) { |
322 | $masks = \substr($payload, 10, 4); |
323 | $data = \substr($payload, 14); |
324 | } else { |
325 | $masks = \substr($payload, 2, 4); |
326 | $data = \substr($payload, 6); |
327 | } |
328 | $text = ''; |
329 | $dataLength = \strlen($data); |
330 | for ($i = 0; $i < $dataLength; ++$i) { |
331 | $text .= $data[$i] ^ $masks[$i % 4]; |
332 | } |
333 | |
334 | return $text; |
335 | } |
336 | } |