Passed
Push — master ( a91d54...875978 )
by y
02:08
created

HandShake::validate()   B

Complexity

Conditions 11
Paths 2

Size

Total Lines 14
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 12
nc 2
nop 0
dl 0
loc 14
rs 7.3166
c 1
b 0
f 0

How to fix   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 Helix\Socket\WebSocket;
4
5
/**
6
 * WebSocket handshake.
7
 */
8
class HandShake {
9
10
    const RFC_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
11
12
    /**
13
     * @var string
14
     */
15
    protected $buffer = '';
16
17
    /**
18
     * @var WebSocketClient
19
     */
20
    protected $client;
21
22
    /**
23
     * @var string[]
24
     */
25
    protected $headers = [];
26
27
    /**
28
     * @var string
29
     */
30
    protected $method;
31
32
    /**
33
     * Claimed RSV bits.
34
     *
35
     * @var int
36
     */
37
    protected $rsv = 0b000;
38
39
    /**
40
     * Received handshake size limit.
41
     *
42
     * The connection is closed (HTTP 413) if the received headers exceed this many bytes.
43
     *
44
     * @var int
45
     */
46
    protected $sizeLimit = 4096;
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
     * @return int
68
     */
69
    public function getRsv (): int {
70
        return $this->rsv;
71
    }
72
73
    public function negotiate (): bool {
74
        $this->buffer .= $this->client->recvAll();
75
        try {
76
            if (strlen($this->buffer) > $this->sizeLimit) {
77
                throw new WebSocketError(413, "{$this->client} exceeded the maximum handshake size.");
78
            }
79
            if (false === $end = strpos($this->buffer, "\r\n\r\n")) {
80
                return false;
81
            }
82
            $head = explode("\r\n", substr($this->buffer, 0, $end));
83
            $this->method = array_shift($head);
84
            foreach ($head as $header) {
85
                $header = explode(':', $header, 2);
86
                if (count($header) !== 2) {
87
                    throw new WebSocketError(400, "{$this->client} sent a malformed header.");
88
                }
89
                [$key, $value] = $header;
90
                $key = strtolower(trim($key));
91
                $value = trim($value);
92
                if (isset($this->headers[$key])) {
93
                    $this->headers[$key] .= ', ' . $value;
94
                }
95
                else {
96
                    $this->headers[$key] = $value;
97
                }
98
            }
99
            $this->buffer = '';
100
            $this->validate();
101
            $this->upgrade();
102
            $this->client->write("\r\n\r\n");
103
            return true;
104
        }
105
        catch (WebSocketError $e) {
106
            $this->client->write("HTTP/1.1 {$e->getCode()}\r\n\r\n");
107
            throw $e;
108
        }
109
    }
110
111
    protected function upgrade (): void {
112
        $this->client->write(implode("\r\n", [
113
            "HTTP/1.1 101 Switching Protocols",
114
            "Connection: Upgrade",
115
            "Upgrade: websocket",
116
            "Sec-WebSocket-Accept: " . base64_encode(sha1($this->headers['sec-websocket-key'] . self::RFC_GUID, true)),
117
        ]));
118
    }
119
120
    /**
121
     * Validates the received HTTP handshake headers, or throws.
122
     */
123
    protected function validate (): void {
124
        if (!(
125
            $check = 'method = http 1.1'
126
            and preg_match('/HTTP\/1\.1$/i', $this->method)
127
            and $check = 'connection = upgrade'
128
            and preg_match('/^upgrade$/i', $this->headers['connection'] ?? '')
129
            and $check = 'upgrade = websocket'
130
            and preg_match('/^websocket$/i', $this->headers['upgrade'] ?? '')
131
            and $check = 'version = 13'
132
            and ($this->headers['sec-websocket-version'] ?? '') === '13'
133
            and $check = 'key length = 16'
134
            and strlen(base64_decode($this->headers['sec-websocket-key'] ?? '')) === 16
135
        )) {
136
            throw new WebSocketError(400, "Handshake with {$this->client} failed on validation: {$check}");
137
        }
138
    }
139
}