WebSocketServer   F
last analyzed

Complexity

Total Complexity 153

Size/Duplication

Total Lines 626
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 348
dl 0
loc 626
rs 2
c 0
b 0
f 0
wmc 153

28 Methods

Rating   Name   Duplication   Size   Complexity  
A checkHost() 0 3 1
D frame() 0 65 18
A calcoffset() 0 13 4
A checkRSVBits() 0 8 2
A checkWebsocProtocol() 0 3 1
A send() 0 9 2
A processProtocol() 0 3 1
A checkOrigin() 0 3 1
D run() 0 58 20
C deframe() 0 69 12
A _tick() 0 17 6
A __construct() 0 9 5
A disconnect() 0 22 5
A tick() 0 2 1
A connect() 0 6 1
F doHandshake() 0 72 30
A stderr() 0 4 2
B split_packet() 0 36 7
A connecting() 0 2 1
A checkWebsocExtensions() 0 3 1
A stdout() 0 4 2
A applyMask() 0 17 4
A extractPayload() 0 13 4
A printHeaders() 0 11 4
A getUserBySocket() 0 9 3
B extractHeaders() 0 36 7
A processExtensions() 0 3 1
B strtohex() 0 21 7

How to fix   Complexity   

Complex Class

Complex classes like WebSocketServer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WebSocketServer, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
//require_once('./daemonize.php');
4
require_once './users.php';
5
6
abstract class WebSocketServer
7
{
8
  protected $userClass = 'WebSocketUser'; // redefine this if you want a custom user class.  The custom user class should inherit from WebSocketUser.
9
  protected $maxBufferSize;
10
  protected $master;
11
  protected $sockets = [];
12
  protected $users = [];
13
  protected $heldMessages = [];
14
  protected $interactive = true;
15
  protected $headerOriginRequired = false;
16
  protected $headerSecWebSocketProtocolRequired = false;
17
  protected $headerSecWebSocketExtensionsRequired = false;
18
19
  public function __construct($addr, $port, $bufferLength = 2048)
20
  {
21
    $this->maxBufferSize = $bufferLength;
22
    $this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die('Failed: socket_create()');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
23
    socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1) or die('Failed: socket_option()');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
24
    socket_bind($this->master, $addr, $port) or die('Failed: socket_bind()');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
25
    socket_listen($this->master, 20) or die('Failed: socket_listen()');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
26
    $this->sockets['m'] = $this->master;
27
    $this->stdout("Server started\nListening on: $addr:$port\nMaster socket: " . $this->master);
0 ignored issues
show
Bug introduced by
Are you sure $this->master of type Socket|resource can be used in concatenation? ( Ignorable by Annotation )

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

27
    $this->stdout("Server started\nListening on: $addr:$port\nMaster socket: " . /** @scrutinizer ignore-type */ $this->master);
Loading history...
28
  }
29
30
  abstract protected function process($user, $message);
31
32
  // Called immediately when the data is recieved.
33
34
  abstract protected function connected($user);
35
36
  // Called after the handshake response is sent to the client.
37
38
  abstract protected function closed($user);
39
40
  // Called after the connection is closed.
41
42
  protected function connecting($user)
0 ignored issues
show
Unused Code introduced by
The parameter $user is not used and could be removed. ( Ignorable by Annotation )

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

42
  protected function connecting(/** @scrutinizer ignore-unused */ $user)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
43
  {
44
    // Override to handle a connecting user, after the instance of the User is created, but before
45
  // the handshake has completed.
46
  }
47
48
  protected function send($user, $message)
49
  {
50
    if ($user->handshake) {
51
      $message = $this->frame($message, $user);
52
      $result = @socket_write($user->socket, $message, strlen($message));
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
53
    } else {
54
      // User has not yet performed their handshake.  Store for sending later.
55
      $holdingMessage = ['user' => $user, 'message' => $message];
56
      $this->heldMessages[] = $holdingMessage;
57
    }
58
  }
59
60
  protected function tick()
61
  {
62
    // Override this for any process that should happen periodically.  Will happen at least once
63
  // per second, but possibly more often.
64
  }
65
66
  protected function _tick()
67
  {
68
    // Core maintenance processes, such as retrying failed messages.
69
    foreach ($this->heldMessages as $key => $hm) {
70
      $found = false;
71
      foreach ($this->users as $currentUser) {
72
        if ($hm['user']->socket == $currentUser->socket) {
73
          $found = true;
74
          if ($currentUser->handshake) {
75
            unset($this->heldMessages[$key]);
76
            $this->send($currentUser, $hm['message']);
77
          }
78
        }
79
      }
80
      if (!$found) {
81
        // If they're no longer in the list of connected users, drop the message.
82
        unset($this->heldMessages[$key]);
83
      }
84
    }
85
  }
86
87
  /**
88
   * Main processing loop.
89
   */
90
  public function run()
91
  {
92
    while (true) {
93
      if (empty($this->sockets)) {
94
        $this->sockets['m'] = $this->master;
95
      }
96
      $read = $this->sockets;
97
      $write = $except = null;
98
      $this->_tick();
99
      $this->tick();
100
      @socket_select($read, $write, $except, 1);
0 ignored issues
show
Bug introduced by
$except of type null is incompatible with the type array expected by parameter $except of socket_select(). ( Ignorable by Annotation )

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

100
      @socket_select($read, $write, /** @scrutinizer ignore-type */ $except, 1);
Loading history...
Bug introduced by
$write of type null is incompatible with the type array expected by parameter $write of socket_select(). ( Ignorable by Annotation )

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

100
      @socket_select($read, /** @scrutinizer ignore-type */ $write, $except, 1);
Loading history...
Security Best Practice introduced by
It seems like you do not handle an error condition for socket_select(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

100
      /** @scrutinizer ignore-unhandled */ @socket_select($read, $write, $except, 1);

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...
101
      foreach ($read as $socket) {
102
        if ($socket == $this->master) {
103
          $client = socket_accept($socket);
104
          if ($client < 0) {
105
            $this->stderr('Failed: socket_accept()');
106
            continue;
107
          } else {
108
            $this->connect($client);
109
            $this->stdout('Client connected. ' . $client);
0 ignored issues
show
Bug introduced by
Are you sure $client of type Socket|resource can be used in concatenation? ( Ignorable by Annotation )

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

109
            $this->stdout('Client connected. ' . /** @scrutinizer ignore-type */ $client);
Loading history...
110
          }
111
        } else {
112
          $numBytes = @socket_recv($socket, $buffer, $this->maxBufferSize, 0);
113
          if (false === $numBytes) {
114
            $sockErrNo = socket_last_error($socket);
115
            switch ($sockErrNo) {
116
    case 102: // ENETRESET    -- Network dropped connection because of reset
117
    case 103: // ECONNABORTED -- Software caused connection abort
118
    case 104: // ECONNRESET   -- Connection reset by peer
119
    case 108: // ESHUTDOWN    -- Cannot send after transport endpoint shutdown -- probably more of an error on our part, if we're trying to write after the socket is closed.  Probably not a critical error, though.
120
    case 110: // ETIMEDOUT    -- Connection timed out
121
    case 111: // ECONNREFUSED -- Connection refused -- We shouldn't see this one, since we're listening... Still not a critical error.
122
    case 112: // EHOSTDOWN    -- Host is down -- Again, we shouldn't see this, and again, not critical because it's just one connection and we still want to listen to/for others.
123
    case 113: // EHOSTUNREACH -- No route to host
124
    case 121: // EREMOTEIO    -- Rempte I/O error -- Their hard drive just blew up.
125
    case 125: // ECANCELED    -- Operation canceled
126
127
    $this->stderr('Unusual disconnect on socket ' . $socket);
128
    $this->disconnect($socket, true, $sockErrNo); // disconnect before clearing error, in case someone with their own implementation wants to check for error conditions on the socket.
129
    break;
130
    default:
131
132
    $this->stderr('Socket error: ' . socket_strerror($sockErrNo));
133
    }
134
          } elseif (0 == $numBytes) {
135
            $this->disconnect($socket);
136
            $this->stderr('Client disconnected. TCP connection lost: ' . $socket);
137
          } else {
138
            $user = $this->getUserBySocket($socket);
139
            if (!$user->handshake) {
140
              $tmp = str_replace("\r", '', $buffer);
141
              if (false === strpos($tmp, "\n\n")) {
142
                continue; // If the client has not finished sending the header, then wait before sending our upgrade response.
143
              }
144
              $this->doHandshake($user, $buffer);
145
            } else {
146
              //split packet into frame and send it to deframe
147
              $this->split_packet($numBytes, $buffer, $user);
148
            }
149
          }
150
        }
151
      }
152
    }
153
  }
154
155
  protected function connect($socket)
156
  {
157
    $user = new $this->userClass(uniqid('u'), $socket);
158
    $this->users[$user->id] = $user;
159
    $this->sockets[$user->id] = $socket;
160
    $this->connecting($user);
161
  }
162
163
  protected function disconnect($socket, $triggerClosed = true, $sockErrNo = null)
164
  {
165
    $disconnectedUser = $this->getUserBySocket($socket);
166
167
    if (null !== $disconnectedUser) {
168
      unset($this->users[$disconnectedUser->id]);
169
170
      if (array_key_exists($disconnectedUser->id, $this->sockets)) {
171
        unset($this->sockets[$disconnectedUser->id]);
172
      }
173
174
      if (!is_null($sockErrNo)) {
175
        socket_clear_error($socket);
176
      }
177
178
      if ($triggerClosed) {
179
        $this->stdout('Client disconnected. ' . $disconnectedUser->socket);
180
        $this->closed($disconnectedUser);
181
        socket_close($disconnectedUser->socket);
182
      } else {
183
        $message = $this->frame('', $disconnectedUser, 'close');
184
        @socket_write($disconnectedUser->socket, $message, strlen($message));
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for socket_write(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

184
        /** @scrutinizer ignore-unhandled */ @socket_write($disconnectedUser->socket, $message, strlen($message));

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...
185
      }
186
    }
187
  }
188
189
  protected function doHandshake($user, $buffer)
190
  {
191
    $magicGUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
192
    $headers = [];
193
    $lines = explode("\n", $buffer);
194
    foreach ($lines as $line) {
195
      if (false !== strpos($line, ':')) {
196
        $header = explode(':', $line, 2);
197
        $headers[strtolower(trim($header[0]))] = trim($header[1]);
198
      } elseif (false !== stripos($line, 'get ')) {
199
        preg_match('/GET (.*) HTTP/i', $buffer, $reqResource);
200
        $headers['get'] = trim($reqResource[1]);
201
      }
202
    }
203
    if (isset($headers['get'])) {
204
      $user->requestedResource = $headers['get'];
205
    } else {
206
      // todo: fail the connection
207
      $handshakeResponse = "HTTP/1.1 405 Method Not Allowed\r\n\r\n";
208
    }
209
    if (!isset($headers['host']) || !$this->checkHost($headers['host'])) {
210
      $handshakeResponse = 'HTTP/1.1 400 Bad Request';
211
    }
212
    if (!isset($headers['upgrade']) || 'websocket' != strtolower($headers['upgrade'])) {
213
      $handshakeResponse = 'HTTP/1.1 400 Bad Request';
214
    }
215
    if (!isset($headers['connection']) || false === strpos(strtolower($headers['connection']), 'upgrade')) {
216
      $handshakeResponse = 'HTTP/1.1 400 Bad Request';
217
    }
218
    if (!isset($headers['sec-websocket-key'])) {
219
      $handshakeResponse = 'HTTP/1.1 400 Bad Request';
220
    } else {
221
    }
222
    if (!isset($headers['sec-websocket-version']) || 13 != strtolower($headers['sec-websocket-version'])) {
223
      $handshakeResponse = "HTTP/1.1 426 Upgrade Required\r\nSec-WebSocketVersion: 13";
224
    }
225
    if (($this->headerOriginRequired && !isset($headers['origin'])) || ($this->headerOriginRequired && !$this->checkOrigin($headers['origin']))) {
226
      $handshakeResponse = 'HTTP/1.1 403 Forbidden';
227
    }
228
    if (($this->headerSecWebSocketProtocolRequired && !isset($headers['sec-websocket-protocol'])) || ($this->headerSecWebSocketProtocolRequired && !$this->checkWebsocProtocol($headers['sec-websocket-protocol']))) {
229
      $handshakeResponse = 'HTTP/1.1 400 Bad Request';
230
    }
231
    if (($this->headerSecWebSocketExtensionsRequired && !isset($headers['sec-websocket-extensions'])) || ($this->headerSecWebSocketExtensionsRequired && !$this->checkWebsocExtensions($headers['sec-websocket-extensions']))) {
232
      $handshakeResponse = 'HTTP/1.1 400 Bad Request';
233
    }
234
235
    // Done verifying the _required_ headers and optionally required headers.
236
237
    if (isset($handshakeResponse)) {
238
      socket_write($user->socket, $handshakeResponse, strlen($handshakeResponse));
239
      $this->disconnect($user->socket);
240
241
      return;
242
    }
243
244
    $user->headers = $headers;
245
    $user->handshake = $buffer;
246
247
    $webSocketKeyHash = sha1($headers['sec-websocket-key'] . $magicGUID);
248
249
    $rawToken = '';
250
    for ($i = 0; $i < 20; ++$i) {
251
      $rawToken .= chr(hexdec(substr($webSocketKeyHash, $i * 2, 2)));
0 ignored issues
show
Bug introduced by
It seems like hexdec(substr($webSocketKeyHash, $i * 2, 2)) can also be of type double; however, parameter $codepoint of chr() does only seem to accept integer, 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

251
      $rawToken .= chr(/** @scrutinizer ignore-type */ hexdec(substr($webSocketKeyHash, $i * 2, 2)));
Loading history...
252
    }
253
    $handshakeToken = base64_encode($rawToken) . "\r\n";
254
255
    $subProtocol = (isset($headers['sec-websocket-protocol'])) ? $this->processProtocol($headers['sec-websocket-protocol']) : '';
256
    $extensions = (isset($headers['sec-websocket-extensions'])) ? $this->processExtensions($headers['sec-websocket-extensions']) : '';
257
258
    $handshakeResponse = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: $handshakeToken$subProtocol$extensions\r\n";
259
    socket_write($user->socket, $handshakeResponse, strlen($handshakeResponse));
260
    $this->connected($user);
261
  }
262
263
  protected function checkHost($hostName)
0 ignored issues
show
Unused Code introduced by
The parameter $hostName is not used and could be removed. ( Ignorable by Annotation )

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

263
  protected function checkHost(/** @scrutinizer ignore-unused */ $hostName)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
264
  {
265
    return true; // Override and return false if the host is not one that you would expect.
266
     // Ex: You only want to accept hosts from the my-domain.com domain,
267
     // but you receive a host from malicious-site.com instead.
268
  }
269
270
  protected function checkOrigin($origin)
0 ignored issues
show
Unused Code introduced by
The parameter $origin is not used and could be removed. ( Ignorable by Annotation )

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

270
  protected function checkOrigin(/** @scrutinizer ignore-unused */ $origin)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
271
  {
272
    return true; // Override and return false if the origin is not one that you would expect.
273
  }
274
275
  protected function checkWebsocProtocol($protocol)
0 ignored issues
show
Unused Code introduced by
The parameter $protocol is not used and could be removed. ( Ignorable by Annotation )

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

275
  protected function checkWebsocProtocol(/** @scrutinizer ignore-unused */ $protocol)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
276
  {
277
    return true; // Override and return false if a protocol is not found that you would expect.
278
  }
279
280
  protected function checkWebsocExtensions($extensions)
0 ignored issues
show
Unused Code introduced by
The parameter $extensions is not used and could be removed. ( Ignorable by Annotation )

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

280
  protected function checkWebsocExtensions(/** @scrutinizer ignore-unused */ $extensions)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
281
  {
282
    return true; // Override and return false if an extension is not found that you would expect.
283
  }
284
285
  protected function processProtocol($protocol)
0 ignored issues
show
Unused Code introduced by
The parameter $protocol is not used and could be removed. ( Ignorable by Annotation )

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

285
  protected function processProtocol(/** @scrutinizer ignore-unused */ $protocol)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
286
  {
287
    return ''; // return either "Sec-WebSocket-Protocol: SelectedProtocolFromClientList\r\n" or return an empty string.
288
     // The carriage return/newline combo must appear at the end of a non-empty string, and must not
289
     // appear at the beginning of the string nor in an otherwise empty string, or it will be considered part of
290
     // the response body, which will trigger an error in the client as it will not be formatted correctly.
291
  }
292
293
  protected function processExtensions($extensions)
0 ignored issues
show
Unused Code introduced by
The parameter $extensions is not used and could be removed. ( Ignorable by Annotation )

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

293
  protected function processExtensions(/** @scrutinizer ignore-unused */ $extensions)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
294
  {
295
    return ''; // return either "Sec-WebSocket-Extensions: SelectedExtensions\r\n" or return an empty string.
296
  }
297
298
  protected function getUserBySocket($socket)
299
  {
300
    foreach ($this->users as $user) {
301
      if ($user->socket == $socket) {
302
        return $user;
303
      }
304
    }
305
306
    return null;
307
  }
308
309
  public function stdout($message)
310
  {
311
    if ($this->interactive) {
312
      echo "$message\n";
313
    }
314
  }
315
316
  public function stderr($message)
317
  {
318
    if ($this->interactive) {
319
      echo "$message\n";
320
    }
321
  }
322
323
  protected function frame($message, $user, $messageType = 'text', $messageContinues = false)
324
  {
325
    switch ($messageType) {
326
  case 'continuous':
327
  $b1 = 0;
328
  break;
329
  case 'text':
330
  $b1 = ($user->sendingContinuous) ? 0 : 1;
331
  break;
332
  case 'binary':
333
  $b1 = ($user->sendingContinuous) ? 0 : 2;
334
  break;
335
  case 'close':
336
  $b1 = 8;
337
  break;
338
  case 'ping':
339
  $b1 = 9;
340
  break;
341
  case 'pong':
342
  $b1 = 10;
343
  break;
344
  }
345
    if ($messageContinues) {
346
      $user->sendingContinuous = true;
347
    } else {
348
      $b1 += 128;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $b1 does not seem to be defined for all execution paths leading up to this point.
Loading history...
349
      $user->sendingContinuous = false;
350
    }
351
352
    $length = strlen($message);
353
    $lengthField = '';
354
    if ($length < 126) {
355
      $b2 = $length;
356
    } elseif ($length < 65536) {
357
      $b2 = 126;
358
      $hexLength = dechex($length);
359
      //$this->stdout("Hex Length: $hexLength");
360
      if (1 == strlen($hexLength) % 2) {
361
        $hexLength = '0' . $hexLength;
362
      }
363
      $n = strlen($hexLength) - 2;
364
365
      for ($i = $n; $i >= 0; $i = $i - 2) {
366
        $lengthField = chr(hexdec(substr($hexLength, $i, 2))) . $lengthField;
0 ignored issues
show
Bug introduced by
It seems like hexdec(substr($hexLength, $i, 2)) can also be of type double; however, parameter $codepoint of chr() does only seem to accept integer, 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

366
        $lengthField = chr(/** @scrutinizer ignore-type */ hexdec(substr($hexLength, $i, 2))) . $lengthField;
Loading history...
367
      }
368
      while (strlen($lengthField) < 2) {
369
        $lengthField = chr(0) . $lengthField;
370
      }
371
    } else {
372
      $b2 = 127;
373
      $hexLength = dechex($length);
374
      if (1 == strlen($hexLength) % 2) {
375
        $hexLength = '0' . $hexLength;
376
      }
377
      $n = strlen($hexLength) - 2;
378
379
      for ($i = $n; $i >= 0; $i = $i - 2) {
380
        $lengthField = chr(hexdec(substr($hexLength, $i, 2))) . $lengthField;
381
      }
382
      while (strlen($lengthField) < 8) {
383
        $lengthField = chr(0) . $lengthField;
384
      }
385
    }
386
387
    return chr($b1) . chr($b2) . $lengthField . $message;
388
  }
389
390
  //check packet if he have more than one frame and process each frame individually
391
  protected function split_packet($length, $packet, $user)
392
  {
393
    //add PartialPacket and calculate the new $length
394
    if ($user->handlingPartialPacket) {
395
      $packet = $user->partialBuffer . $packet;
396
      $user->handlingPartialPacket = false;
397
      $length = strlen($packet);
398
    }
399
    $fullpacket = $packet;
400
    $frame_pos = 0;
401
    $frame_id = 1;
402
403
    while ($frame_pos < $length) {
404
      $headers = $this->extractHeaders($packet);
405
      $headers_size = $this->calcoffset($headers);
406
      $framesize = $headers['length'] + $headers_size;
407
408
      //split frame from packet and process it
409
      $frame = substr($fullpacket, $frame_pos, $framesize);
410
411
      if (false !== ($message = $this->deframe($frame, $user, $headers))) {
0 ignored issues
show
Unused Code introduced by
The call to WebSocketServer::deframe() has too many arguments starting with $headers. ( Ignorable by Annotation )

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

411
      if (false !== ($message = $this->/** @scrutinizer ignore-call */ deframe($frame, $user, $headers))) {

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
412
        if ($user->hasSentClose) {
413
          $this->disconnect($user->socket);
414
        } else {
415
          if ((preg_match('//u', $message)) || (2 == $headers['opcode'])) {
416
            //$this->stdout("Text msg encoded UTF-8 or Binary msg\n".$message);
417
            $this->process($user, $message);
418
          } else {
419
            $this->stderr("not UTF-8\n");
420
          }
421
        }
422
      }
423
      //get the new position also modify packet data
424
      $frame_pos += $framesize;
425
      $packet = substr($fullpacket, $frame_pos);
426
      ++$frame_id;
427
    }
428
  }
429
430
  protected function calcoffset($headers)
431
  {
432
    $offset = 2;
433
    if ($headers['hasmask']) {
434
      $offset += 4;
435
    }
436
    if ($headers['length'] > 65535) {
437
      $offset += 8;
438
    } elseif ($headers['length'] > 125) {
439
      $offset += 2;
440
    }
441
442
    return $offset;
443
  }
444
445
  protected function deframe($message, &$user)
446
  {
447
    //echo $this->strtohex($message);
448
    $headers = $this->extractHeaders($message);
449
    $pongReply = false;
450
    $willClose = false;
451
    switch ($headers['opcode']) {
452
  case 0:
453
  case 1:
454
  case 2:
455
  break;
456
  case 8:
457
  // todo: close the connection
458
  $user->hasSentClose = true;
459
460
  return '';
461
  case 9:
462
  $pongReply = true;
463
  // no break
464
  case 10:
465
  break;
466
  default:
467
  //$this->disconnect($user); // todo: fail connection
468
  $willClose = true;
469
  break;
470
  }
471
472
    /* Deal by split_packet() as now deframe() do only one frame at a time.
473
    if ($user->handlingPartialPacket) {
474
      $message = $user->partialBuffer . $message;
475
      $user->handlingPartialPacket = false;
476
      return $this->deframe($message, $user);
477
    }
478
    */
479
480
    if ($this->checkRSVBits($headers, $user)) {
481
      return false;
482
    }
483
484
    if ($willClose) {
485
      // todo: fail the connection
486
      return false;
487
    }
488
489
    $payload = $user->partialMessage . $this->extractPayload($message, $headers);
490
491
    if ($pongReply) {
492
      $reply = $this->frame($payload, $user, 'pong');
493
      socket_write($user->socket, $reply, strlen($reply));
494
495
      return false;
496
    }
497
    if ($headers['length'] > strlen($this->applyMask($headers, $payload))) {
498
      $user->handlingPartialPacket = true;
499
      $user->partialBuffer = $message;
500
501
      return false;
502
    }
503
504
    $payload = $this->applyMask($headers, $payload);
505
506
    if ($headers['fin']) {
507
      $user->partialMessage = '';
508
509
      return $payload;
510
    }
511
    $user->partialMessage = $payload;
512
513
    return false;
514
  }
515
516
  protected function extractHeaders($message)
517
  {
518
    $header = ['fin' => $message[0] & chr(128),
519
      'rsv1' => $message[0] & chr(64),
520
      'rsv2' => $message[0] & chr(32),
521
      'rsv3' => $message[0] & chr(16),
522
      'opcode' => ord($message[0]) & 15,
523
      'hasmask' => $message[1] & chr(128),
524
      'length' => 0,
525
      'mask' => '', ];
526
    $header['length'] = (ord($message[1]) >= 128) ? ord($message[1]) - 128 : ord($message[1]);
527
528
    if (126 == $header['length']) {
529
      if ($header['hasmask']) {
530
        $header['mask'] = $message[4] . $message[5] . $message[6] . $message[7];
531
      }
532
      $header['length'] = ord($message[2]) * 256
533
    + ord($message[3]);
534
    } elseif (127 == $header['length']) {
535
      if ($header['hasmask']) {
536
        $header['mask'] = $message[10] . $message[11] . $message[12] . $message[13];
537
      }
538
      $header['length'] = ord($message[2]) * 65536 * 65536 * 65536 * 256
539
    + ord($message[3]) * 65536 * 65536 * 65536
540
    + ord($message[4]) * 65536 * 65536 * 256
541
    + ord($message[5]) * 65536 * 65536
542
    + ord($message[6]) * 65536 * 256
543
    + ord($message[7]) * 65536
544
    + ord($message[8]) * 256
545
    + ord($message[9]);
546
    } elseif ($header['hasmask']) {
547
      $header['mask'] = $message[2] . $message[3] . $message[4] . $message[5];
548
    }
549
    //echo $this->strtohex($message);
550
    //$this->printHeaders($header);
551
    return $header;
552
  }
553
554
  protected function extractPayload($message, $headers)
555
  {
556
    $offset = 2;
557
    if ($headers['hasmask']) {
558
      $offset += 4;
559
    }
560
    if ($headers['length'] > 65535) {
561
      $offset += 8;
562
    } elseif ($headers['length'] > 125) {
563
      $offset += 2;
564
    }
565
566
    return substr($message, $offset);
567
  }
568
569
  protected function applyMask($headers, $payload)
570
  {
571
    $effectiveMask = '';
572
    if ($headers['hasmask']) {
573
      $mask = $headers['mask'];
574
    } else {
575
      return $payload;
576
    }
577
578
    while (strlen($effectiveMask) < strlen($payload)) {
579
      $effectiveMask .= $mask;
580
    }
581
    while (strlen($effectiveMask) > strlen($payload)) {
582
      $effectiveMask = substr($effectiveMask, 0, -1);
583
    }
584
585
    return $effectiveMask ^ $payload;
586
  }
587
588
  protected function checkRSVBits($headers, $user)
0 ignored issues
show
Unused Code introduced by
The parameter $user is not used and could be removed. ( Ignorable by Annotation )

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

588
  protected function checkRSVBits($headers, /** @scrutinizer ignore-unused */ $user)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
589
  { // override this method if you are using an extension where the RSV bits are used.
590
    if (ord($headers['rsv1']) + ord($headers['rsv2']) + ord($headers['rsv3']) > 0) {
591
      //$this->disconnect($user); // todo: fail connection
592
      return true;
593
    }
594
595
    return false;
596
  }
597
598
  protected function strtohex($str)
599
  {
600
    $strout = '';
601
    for ($i = 0; $i < strlen($str); ++$i) {
602
      $strout .= (ord($str[$i]) < 16) ? '0' . dechex(ord($str[$i])) : dechex(ord($str[$i]));
603
      $strout .= ' ';
604
      if (7 == $i % 32) {
605
        $strout .= ': ';
606
      }
607
      if (15 == $i % 32) {
608
        $strout .= ': ';
609
      }
610
      if (23 == $i % 32) {
611
        $strout .= ': ';
612
      }
613
      if (31 == $i % 32) {
614
        $strout .= "\n";
615
      }
616
    }
617
618
    return $strout . "\n";
619
  }
620
621
  protected function printHeaders($headers)
622
  {
623
    echo "Array\n(\n";
624
    foreach ($headers as $key => $value) {
625
      if ('length' == $key || 'opcode' == $key) {
626
        echo "\t[$key] => $value\n\n";
627
      } else {
628
        echo "\t[$key] => " . $this->strtohex($value) . "\n";
629
      }
630
    }
631
    echo ")\n";
632
  }
633
}
634