Completed
Push — master ( 2d58e0...41ca8e )
by Arthur
01:35
created

WebSocketServer::looping()   F

Complexity

Conditions 24
Paths 489

Size

Total Lines 102
Code Lines 56

Duplication

Lines 25
Ratio 24.51 %

Importance

Changes 0
Metric Value
dl 25
loc 102
rs 3.0503
c 0
b 0
f 0
cc 24
eloc 56
nc 489
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace WSSC;
4
5
use WSSC\Components\Connection;
6
use WSSC\Contracts\CommonsContract;
7
use WSSC\Contracts\WebSocketMessageContract;
8
use WSSC\Contracts\WebSocketServerContract;
9
use WSSC\Exceptions\WebSocketException;
10
11
/**
12
 * Create by Arthur Kushman
13
 */
14
class WebSocketServer implements WebSocketServerContract, CommonsContract
15
{
16
17
    private $clients = [];
18
    // set any template You need ex.: GET /subscription/messenger/token
19
    private $pathParams = [];
20
    private $config;
21
    private $handshakes = [];
22
    private $headersUpgrade = [];
23
    private $totalClients = 0;
24
    private $maxClients = 1;
25
    private $handler;
26
    private $connImpl;
27
    private $cureentConn;
28
    // for the very 1st time must be true
29
    private $stepRecursion = true;
30
31
    const MAX_BYTES_READ = 8192;
32
    const HEADER_BYTES_READ = 1024;
33
    // must be the time for interaction between each client
34
    const STREAM_SELECT_TIMEOUT = 3600;
35
    // stream non-blocking 
36
    const NON_BLOCK = 0;
37
    // max clients to fork another process
38
    const MAX_CLIENTS_REMAINDER_FORK = 1000;
39
    const PROC_TITLE = 'php-wss';
40
41
    /**
42
     * WebSocketServer constructor.
43
     * @param WebSocketMessageContract $handler
44
     * @param array $config
45
     */
46
    public function __construct(
47
        WebSocketMessageContract $handler,
48
        $config = [
49
            'host' => self::DEFAULT_HOST,
50
            'port' => self::DEFAULT_PORT,
51
        ]
52
    )
53
    {
54
        ini_set('default_socket_timeout', 5); // this should be >= 5 sec, otherwise there will be broken pipe - tested
55
        $this->handler = $handler;
56
        $this->config = $config;
57
        $this->connImpl = new Connection();
58
    }
59
60
    /**
61
     * Runs main process - Anscestor with server socket on TCP
62
     */
63
    public function run()
64
    {
65
        $errno = NULL;
66
        $errorMessage = '';
67
68
        $server = stream_socket_server("tcp://{$this->config['host']}:{$this->config['port']}", $errno, $errorMessage);
69
        if ($server === false) {
70
            die('Could not bind to socket: ' . $errno . ' - ' . $errorMessage . PHP_EOL);
71
        }
72
        @cli_set_process_title(self::PROC_TITLE);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
73
        $this->eventLoop($server);
74
    }
75
76
    /**
77
     * Recursive event loop that input intu recusion by remainder = 0 - thus when N users,
78
     * and when forks equals true which prevents it from infinite recursive iterations
79
     *
80
     * @param resource $server server connection
81
     * @param bool $fork flag to fork or run event loop
82
     */
83
    private function eventLoop($server, bool $fork = false)
84
    {
85
        if ($fork === true) {
86
            $pid = pcntl_fork();
87
88
            if ($pid) { // run eventLoop in parent        
89
                @cli_set_process_title(self::PROC_TITLE);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
90
                $this->eventLoop($server);
91
            }
92
        } else {
93
            $this->looping($server);
94
        }
95
    }
96
97
    /**
98
     * @param resource $server
99
     */
100
    private function looping($server)
101
    {
102
        while (true) {
103
            $this->totalClients = count($this->clients) + 1;
104
105
            // maxClients prevents process fork on count down
106
            if ($this->totalClients > $this->maxClients) {
107
                $this->maxClients = $this->totalClients;
108
            }
109
110
            if ($this->totalClients !== 0 // avoid 0 process creation
111
                && $this->totalClients % self::MAX_CLIENTS_REMAINDER_FORK === 0 // only when N is there
112
                && true === $this->stepRecursion // only once
113
                && $this->maxClients === $this->totalClients // only if stack grows
114
            ) {
115
                $this->stepRecursion = false;
116
                $this->eventLoop($server, true);
117
            }
118
119
            if ($this->totalClients !== 0 && $this->totalClients % self::MAX_CLIENTS_REMAINDER_FORK === 0 && $this->maxClients > $this->totalClients) { // there is less connection for amount of processes at this moment
120
                exit(1);
121
            }
122
123
            //prepare readable sockets
124
            $readSocks = $this->clients;
125
            $readSocks[] = $server;
126
127
            //start reading and use a large timeout
128
            if (!stream_select($readSocks, $write, $except, self::STREAM_SELECT_TIMEOUT)) {
129
                die('something went wrong while selecting');
130
            }
131
132
            //new client
133
            if (in_array($server, $readSocks)) {
134
                $newClient = stream_socket_accept($server, 0); // must be 0 to non-block
135
                if ($newClient) {
136
                    // print remote client information, ip and port number
137
                    //                        $socketName = stream_socket_get_name($newClient, true);
0 ignored issues
show
Unused Code Comprehensibility introduced by
54% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
138
                    // important to read from headers here coz later client will change and there will be only msgs on pipe
139
                    $headers = fread($newClient, self::HEADER_BYTES_READ);
140
                    if (empty($this->handler->pathParams[0]) === false) {
0 ignored issues
show
Bug introduced by
Accessing pathParams on the interface WSSC\Contracts\WebSocketMessageContract suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
141
                        $this->setPathParams($headers);
142
                    }
143
                    $this->clients[] = $newClient;
144
                    $this->stepRecursion = true; // set on new client coz of remainder % is always 0
145
                    // trigger OPEN event
146
                    $this->handler->onOpen($this->connImpl->getConnection($newClient));
147
                    $this->handshake($newClient, $headers);
148
                }
149
                //delete the server socket from the read sockets
150
                unset($readSocks[array_search($server, $readSocks)]);
151
            }
152
153
            //message from existing client
154
            foreach ($readSocks as $kSock => $sock) {
155
                $data = $this->decode(fread($sock, self::MAX_BYTES_READ));
156
                $dataType = $data['type'];
157
                $dataPayload = $data['payload'];
158
                // to manipulate connection through send/close methods via handler, specified in IConnection
159
                $this->cureentConn = $this->connImpl->getConnection($sock);
160
                if (empty($data) || $dataType === self::EVENT_TYPE_CLOSE) { // close event triggered from client - browser tab or close socket event
161
                    // trigger CLOSE event
162
                    try {
163
                        $this->handler->onClose($this->cureentConn);
164
                    } catch (WebSocketException $e) {
165
                        $e->printStack();
166
                    }
167
                    // to avoid event leaks
168
                    unset($this->clients[array_search($sock, $this->clients)], $readSocks[$kSock]);
169
                    continue;
170
                }
171
172 View Code Duplication
                if ($dataType === self::EVENT_TYPE_TEXT) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
173
                    // trigger MESSAGE event
174
                    try {
175
                        echo 'trigger MESSAGE event';
176
                        $this->handler->onMessage($this->cureentConn, $dataPayload);
177
                    } catch (WebSocketException $e) {
178
                        $e->printStack();
179
                    }
180
                }
181
182 View Code Duplication
                if ($dataType === self::EVENT_TYPE_PING) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
183
                    // trigger PING event
184
                    try {
185
                        $this->handler->onPing($this->cureentConn, $dataPayload);
186
                    } catch (WebSocketException $e) {
187
                        $e->printStack();
188
                    }
189
                }
190
191 View Code Duplication
                if ($dataType === self::EVENT_TYPE_PONG) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
192
                    // trigger PONG event
193
                    try {
194
                        $this->handler->onPong($this->cureentConn, $dataPayload);
195
                    } catch (WebSocketException $e) {
196
                        $e->printStack();
197
                    }
198
                }
199
            }
200
        }
201
    }
202
203
    /**
204
     * Message frames decoder
205
     *
206
     * @param string $data
207
     * @return mixed null on empty data|false on improper data|array - on success
208
     */
209
    private function decode($data)
210
    {
211
        if (empty($data)) {
212
            return NULL; // close has been sent
213
        }
214
215
        $unmaskedPayload = '';
216
        $decodedData = [];
217
218
        // estimate frame type:
219
        $firstByteBinary = sprintf('%08b', ord($data[0]));
220
        $secondByteBinary = sprintf('%08b', ord($data[1]));
221
        $opcode = bindec(substr($firstByteBinary, 4, 4));
222
        $isMasked = $secondByteBinary[0] === '1';
223
        $payloadLength = ord($data[1]) & self::MASK_127;
224
225
        // unmasked frame is received:
226
        if (!$isMasked) {
227
            return ['type' => '', 'payload' => '', 'error' => self::ERR_PROTOCOL];
228
        }
229
230
        switch ($opcode) {
231
            // text frame:
232
            case self::DECODE_TEXT:
233
                $decodedData['type'] = self::EVENT_TYPE_TEXT;
234
                break;
235
            case self::DECODE_BINARY:
236
                $decodedData['type'] = self::EVENT_TYPE_BINARY;
237
                break;
238
            // connection close frame:
239
            case self::DECODE_CLOSE:
240
                $decodedData['type'] = self::EVENT_TYPE_CLOSE;
241
                break;
242
            // ping frame:
243
            case self::DECODE_PING:
244
                $decodedData['type'] = self::EVENT_TYPE_PING;
245
                break;
246
            // pong frame:
247
            case self::DECODE_PONG:
248
                $decodedData['type'] = self::EVENT_TYPE_PONG;
249
                break;
250
            default:
251
                return ['type' => '', 'payload' => '', 'error' => self::ERR_UNKNOWN_OPCODE];
252
        }
253
254
        if ($payloadLength === self::MASK_126) {
255
            $mask = substr($data, 4, 4);
256
            $payloadOffset = self::PAYLOAD_OFFSET_8;
257
            $dataLength = bindec(sprintf('%08b', ord($data[2])) . sprintf('%08b', ord($data[3]))) + $payloadOffset;
258
        } elseif ($payloadLength === self::MASK_127) {
259
            $mask = substr($data, 10, 4);
260
            $payloadOffset = self::PAYLOAD_OFFSET_14;
261
            $tmp = '';
262
            for ($i = 0; $i < 8; $i++) {
263
                $tmp .= sprintf('%08b', ord($data[$i + 2]));
264
            }
265
            $dataLength = bindec($tmp) + $payloadOffset;
266
            unset($tmp);
267
        } else {
268
            $mask = substr($data, 2, 4);
269
            $payloadOffset = self::PAYLOAD_OFFSET_6;
270
            $dataLength = $payloadLength + $payloadOffset;
271
        }
272
273
        /**
274
         * We have to check for large frames here. socket_recv cuts at 1024 bytes
275
         * so if websocket-frame is > 1024 bytes we have to wait until whole
276
         * data is transferd.
277
         */
278
        if (strlen($data) < $dataLength) {
279
            return false;
280
        }
281
282
        if ($isMasked) {
283
            for ($i = $payloadOffset; $i < $dataLength; $i++) {
284
                $j = $i - $payloadOffset;
285
                if (isset($data[$i])) {
286
                    $unmaskedPayload .= $data[$i] ^ $mask[$j % 4];
287
                }
288
            }
289
            $decodedData['payload'] = $unmaskedPayload;
290
        } else {
291
            $payloadOffset -= 4;
292
            $decodedData['payload'] = substr($data, $payloadOffset);
293
        }
294
295
        return $decodedData;
296
    }
297
298
    /**
299
     * Handshakes/upgrade and key parse
300
     *
301
     * @param resource $client Source client socket to write
302
     * @param string $headers Headers that client has been sent
303
     * @return string   socket handshake key (Sec-WebSocket-Key)| false on parse error
304
     */
305
    private function handshake($client, string $headers) : string
306
    {
307
        $match = [];
308
        $key = empty($this->handshakes[(int)$client]) ? 0 : $this->handshakes[(int)$client];
0 ignored issues
show
Unused Code introduced by
$key is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
309
        preg_match(self::SEC_WEBSOCKET_KEY_PTRN, $headers, $match);
310
        if (empty($match[1])) {
311
            return false;
312
        }
313
314
        $key = $match[1];
315
        $this->handshakes[(int)$client] = $key;
316
317
        // sending header according to WebSocket Protocol
318
        $secWebSocketAccept = base64_encode(sha1(trim($key) . self::HEADER_WEBSOCKET_ACCEPT_HASH, true));
319
        $this->setHeadersUpgrade($secWebSocketAccept);
320
        $upgradeHeaders = $this->getHeadersUpgrade();
321
322
        fwrite($client, $upgradeHeaders);
323
        return $key;
324
    }
325
326
    /**
327
     * Sets an array of headers needed to upgrade server/client connection
328
     *
329
     * @param string $secWebSocketAccept base64 encoded Sec-WebSocket-Accept header
330
     */
331
    private function setHeadersUpgrade($secWebSocketAccept)
332
    {
333
        $this->headersUpgrade = [
334
            self::HEADERS_UPGRADE_KEY              => self::HEADERS_UPGRADE_VALUE,
335
            self::HEADERS_CONNECTION_KEY           => self::HEADERS_CONNECTION_VALUE,
336
            self::HEADERS_SEC_WEBSOCKET_ACCEPT_KEY => ' ' . $secWebSocketAccept // the space before key is really important
337
        ];
338
    }
339
340
    /**
341
     * Retreives headers from an array of headers to upgrade server/client connection
342
     *
343
     * @return string   Headers to Upgrade communication connection
344
     */
345
    private function getHeadersUpgrade() : string
346
    {
347
        $handShakeHeaders = self::HEADER_HTTP1_1 . self::HEADERS_EOL;
348
        if (empty($this->headersUpgrade)) {
349
            die('Headers array is not set' . PHP_EOL);
350
        }
351
        foreach ($this->headersUpgrade as $key => $header) {
352
            $handShakeHeaders .= $key . ':' . $header . self::HEADERS_EOL;
353
            if ($key === self::HEADERS_SEC_WEBSOCKET_ACCEPT_KEY) { // add additional EOL fo Sec-WebSocket-Accept
354
                $handShakeHeaders .= self::HEADERS_EOL;
355
            }
356
        }
357
        return $handShakeHeaders;
358
    }
359
360
    /**
361
     * Parses parameters from GET on web-socket client connection before handshake
362
     *
363
     * @param string $headers
364
     */
365
    private function setPathParams(string $headers) : void
366
    {
367
        /** @var WebSocketMessageContract $handler */
368
        if (empty($this->handler->pathParams) === false) {
0 ignored issues
show
Bug introduced by
Accessing pathParams on the interface WSSC\Contracts\WebSocketMessageContract suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
369
            $matches = [];
370
            preg_match('/GET\s(.*?)\s/', $headers, $matches);
371
            $left = $matches[1];
372
            foreach ($this->handler->pathParams as $k => $param) {
0 ignored issues
show
Bug introduced by
Accessing pathParams on the interface WSSC\Contracts\WebSocketMessageContract suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
373
                if (empty($this->handler->pathParams[$k + 1]) && strpos($left, '/', 1) === false) {
0 ignored issues
show
Bug introduced by
Accessing pathParams on the interface WSSC\Contracts\WebSocketMessageContract suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
374
                    // do not eat last char if there is no / at the end
375
                    $this->handler->pathParams[$param] = substr($left, strpos($left, '/') + 1);
0 ignored issues
show
Bug introduced by
Accessing pathParams on the interface WSSC\Contracts\WebSocketMessageContract suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
376
                } else {
377
                    // eat both slashes
378
                    $this->handler->pathParams[$param] = substr($left, strpos($left, '/') + 1, strpos($left, '/', 1) - 1);
0 ignored issues
show
Bug introduced by
Accessing pathParams on the interface WSSC\Contracts\WebSocketMessageContract suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
379
                }
380
                // clear the declaration of parsed param
381
                unset($this->handler->pathParams[array_search($param, $this->handler->pathParams, false)]);
382
                $left = substr($left, strpos($left, '/', 1));
383
            }
384
        }
385
    }
386
387
}
388