Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 100
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Server
0.00% covered (danger)
0.00%
0 / 100
0.00% covered (danger)
0.00%
0 / 10
930
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 hasInternet
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 create
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 setLimit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 handshake
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
72
 run
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
90
 shutdown
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 connectClient
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 disconnectClient
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 unmask
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
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 */
13declare(strict_types=1);
14
15namespace phpOMS\Socket\Server;
16
17use phpOMS\Account\NullAccount;
18use phpOMS\Application\ApplicationAbstract;
19use phpOMS\Message\Socket\PacketManager;
20use phpOMS\Socket\Client\ClientConnection;
21use 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 */
31class 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}