Completed
Push — master ( 0a022c...1f28ab )
by y
01:37
created

Handshake::onReadable()   B

Complexity

Conditions 9
Paths 37

Size

Total Lines 57

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 57
rs 7.3826
c 0
b 0
f 0
cc 9
nc 37
nop 0

How to fix   Long Method   

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 Helix\Socket\WebSocket;
4
5
use Throwable;
6
7
/**
8
 * Initial WebSocket connection handshake.
9
 *
10
 * https://tools.ietf.org/html/rfc6455#section-1.3
11
 */
12
class Handshake {
13
14
    const RFC_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
15
16
    /**
17
     * @var string
18
     */
19
    protected $buffer = '';
20
21
    /**
22
     * @var WebSocketClient
23
     */
24
    protected $client;
25
26
    /**
27
     * @var string[]
28
     */
29
    protected $headers = [];
30
31
    /**
32
     * The connection is closed (HTTP 413) if the received headers exceed this many bytes.
33
     *
34
     * @var int
35
     */
36
    protected $maxLength = 4096;
37
38
    /**
39
     * @var string
40
     */
41
    protected $method;
42
43
    /**
44
     * @param WebSocketClient $client
45
     */
46
    public function __construct (WebSocketClient $client) {
47
        $this->client = $client;
48
    }
49
50
    /**
51
     * @return string[]
52
     */
53
    public function getHeaders () {
54
        return $this->headers;
55
    }
56
57
    /**
58
     * @return string
59
     */
60
    public function getMethod (): string {
61
        return $this->method;
62
    }
63
64
    /**
65
     * Negotiates the initial connection.
66
     *
67
     * @return bool
68
     * @throws WebSocketError
69
     * @throws Throwable
70
     */
71
    public function onReadable (): bool {
72
        // read into the buffer
73
        $this->buffer .= $bytes = $this->client->recvAll();
74
75
        // check for peer disconnection
76
        if (!strlen($bytes)) {
77
            $this->client->close();
78
            return false;
79
        }
80
81
        // read frames from the buffer and yield
82
        try {
83
            // length check
84
            if (strlen($this->buffer) > $this->maxLength) {
85
                throw new WebSocketError(413, "{$this->client} exceeded the maximum handshake size.");
86
            }
87
            // still reading?
88
            if (false === $end = strpos($this->buffer, "\r\n\r\n")) {
89
                return false;
90
            }
91
            // parse the headers
92
            $head = explode("\r\n", substr($this->buffer, 0, $end));
93
            $this->method = array_shift($head);
94
            foreach ($head as $header) {
95
                $header = explode(':', $header, 2);
96
                if (count($header) !== 2) {
97
                    throw new WebSocketError(400, "{$this->client} sent a malformed header.");
98
                }
99
                [$key, $value] = $header;
0 ignored issues
show
Bug introduced by
The variable $key does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $value does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
100
                $key = strtolower(trim($key));
101
                $value = trim($value);
102
                if (isset($this->headers[$key])) {
103
                    $this->headers[$key] .= ', ' . $value;
104
                }
105
                else {
106
                    $this->headers[$key] = $value;
107
                }
108
            }
109
            $this->buffer = ''; // wipe the buffer
110
            $this->validate();
111
        }
112
        catch (WebSocketError $e) { // catch and respond with HTTP error and rethrow
113
            $this->client->write("HTTP/1.1 {$e->getCode()} WebSocket Handshake Failure\r\n\r\n");
114
            throw $e;
115
        }
116
        catch (Throwable $e) { // catch everything else and respond with HTTP 500 and rethrow
117
            $this->client->write("HTTP/1.1 500 WebSocket Internal Error\r\n\r\n");
118
            throw $e;
119
        }
120
121
        // send upgrade headers
122
        $this->upgrade();
123
        $this->client->write("\r\n\r\n");
124
125
        // success
126
        return true;
127
    }
128
129
    /**
130
     * Sends the connection upgrade headers.
131
     */
132
    protected function upgrade (): void {
133
        $key = base64_encode(sha1($this->headers['sec-websocket-key'] . self::RFC_GUID, true));
134
        $this->client->write(implode("\r\n", [
135
            "HTTP/1.1 101 Switching Protocols",
136
            "Connection: Upgrade",
137
            "Upgrade: websocket",
138
            "Sec-WebSocket-Accept: {$key}"
139
        ]));
140
    }
141
142
    /**
143
     * Validates the received HTTP handshake headers, or throws.
144
     *
145
     * @throws WebSocketError
146
     */
147
    protected function validate (): void {
148
        if (!(
149
            $check = 'method = http 1.1'
150
            and preg_match('/HTTP\/1\.1$/i', $this->method)
151
            and $check = 'connection = upgrade'
152
            and preg_match('/^upgrade$/i', $this->headers['connection'] ?? '')
153
            and $check = 'upgrade = websocket'
154
            and preg_match('/^websocket$/i', $this->headers['upgrade'] ?? '')
155
            and $check = 'version = 13'
156
            and ($this->headers['sec-websocket-version'] ?? '') === '13'
157
            and $check = 'key length = 16'
158
            and strlen(base64_decode($this->headers['sec-websocket-key'] ?? '')) === 16
159
        )) {
160
            throw new WebSocketError(400, "Handshake with {$this->client} failed on validation: {$check}");
161
        }
162
    }
163
}