Handshake::getMethod()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
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
     * [ header name => lowercase value  => actual value ]
28
     *
29
     * @var string[][]
30
     */
31
    protected $headers = [];
32
33
    /**
34
     * The connection is closed (HTTP 413) if the received headers exceed this many bytes.
35
     *
36
     * @var int
37
     */
38
    protected $maxLength = 4096;
39
40
    /**
41
     * @var string
42
     */
43
    protected $method;
44
45
    /**
46
     * @param WebSocketClient $client
47
     */
48
    public function __construct (WebSocketClient $client) {
49
        $this->client = $client;
50
    }
51
52
    /**
53
     * @return string[][]
54
     */
55
    public function getHeaders () {
56
        return $this->headers;
57
    }
58
59
    /**
60
     * @return string
61
     */
62
    public function getMethod (): string {
63
        return $this->method;
64
    }
65
66
    /**
67
     * Negotiates the initial connection.
68
     *
69
     * @return bool
70
     * @throws WebSocketError
71
     * @throws Throwable
72
     */
73
    public function onReadable (): bool {
74
        // read into the buffer
75
        $this->buffer .= $bytes = $this->client->recvAll();
76
77
        // check for peer disconnection
78
        if (!strlen($bytes)) {
79
            $this->client->close();
80
            return false;
81
        }
82
83
        // read frames from the buffer and yield
84
        try {
85
            // length check
86
            if (strlen($this->buffer) > $this->maxLength) {
87
                throw new WebSocketError(413, "{$this->client} exceeded the maximum handshake size.");
88
            }
89
            // still reading?
90
            if (false === $end = strpos($this->buffer, "\r\n\r\n")) {
91
                return false;
92
            }
93
            // parse the headers
94
            $head = explode("\r\n", substr($this->buffer, 0, $end));
95
            $this->method = array_shift($head);
96
            foreach ($head as $header) {
97
                $header = explode(':', $header, 2);
98
                if (count($header) !== 2) {
99
                    throw new WebSocketError(400, "{$this->client} sent a malformed header.");
100
                }
101
                [$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...
102
                $key = strtolower(trim($key));
103
                foreach (explode(', ', $value) as $value) {
104
                    $value = trim($value);
105
                    $this->headers[$key][strtolower($value)] = $value;
106
                }
107
            }
108
            $this->buffer = ''; // wipe the buffer
109
            $this->validate();
110
        }
111
        catch (WebSocketError $e) { // catch and respond with HTTP error and rethrow
112
            $this->client->write("HTTP/1.1 {$e->getCode()} WebSocket Handshake Failure\r\n\r\n");
113
            throw $e;
114
        }
115
        catch (Throwable $e) { // catch everything else and respond with HTTP 500 and rethrow
116
            $this->client->write("HTTP/1.1 500 WebSocket Internal Error\r\n\r\n");
117
            throw $e;
118
        }
119
120
        // send upgrade headers
121
        $this->upgrade();
122
        $this->client->write("\r\n\r\n");
123
124
        // success
125
        return true;
126
    }
127
128
    /**
129
     * Sends the connection upgrade headers.
130
     */
131
    protected function upgrade (): void {
132
        $key = base64_encode(sha1(current($this->headers['sec-websocket-key']) . self::RFC_GUID, true));
133
        $this->client->write(implode("\r\n", [
134
            "HTTP/1.1 101 Switching Protocols",
135
            "Connection: Upgrade",
136
            "Upgrade: websocket",
137
            "Sec-WebSocket-Accept: {$key}"
138
        ]));
139
    }
140
141
    /**
142
     * Validates the received HTTP handshake headers, or throws.
143
     *
144
     * @throws WebSocketError
145
     */
146
    protected function validate (): void {
147
        if (!(
148
            $check = 'method = http 1.1'
149
            and preg_match('/HTTP\/1\.1$/i', $this->method)
150
            and $check = 'connection = upgrade'
151
            and isset($this->headers['connection']['upgrade'])
152
            and $check = 'upgrade = websocket'
153
            and isset($this->headers['upgrade']['websocket'])
154
            and $check = 'version = 13'
155
            and isset($this->headers['sec-websocket-version']['13'])
156
            and $check = 'key length = 16'
157
            and strlen(base64_decode(current($this->headers['sec-websocket-key'] ?? []))) === 16
158
        )) {
159
            throw new WebSocketError(400, "Handshake with {$this->client} failed on validation: {$check}");
160
        }
161
    }
162
}