Request   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 264
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 96.67%

Importance

Changes 0
Metric Value
wmc 31
lcom 1
cbo 2
dl 0
loc 264
ccs 87
cts 90
cp 0.9667
rs 9.92
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 1 1
A setMethod() 0 6 1
A setUri() 0 6 1
A getUri() 0 4 1
A getMethod() 0 4 1
A getVersion() 0 4 1
A setVersion() 0 6 1
A setKey() 0 6 1
A getKey() 0 4 1
A setHost() 0 6 1
A setPort() 0 6 1
B getExtensions() 0 35 6
A getRequestAsString() 0 16 3
A __toString() 0 4 1
A create() 0 16 4
A createClientRequest() 0 17 2
A initRequest() 0 23 4
1
<?php
2
3
/**
4
 * This file is a part of a nekland library
5
 *
6
 * (c) Nekland <[email protected]>
7
 *
8
 * For the full license, take a look to the LICENSE file
9
 * on the root directory of this project
10
 */
11
12
namespace Nekland\Woketo\Http;
13
14
use Nekland\Woketo\Exception\Http\HttpException;
15
use Nekland\Woketo\Meta;
16
17
/**
18
 * Class Request
19
 *
20
 * @internal
21
 */
22
class Request extends AbstractHttpMessage
23
{
24
    const HTTP_1_1 = '1.1';
25
    /**
26
     * @var string
27
     */
28
    private $method;
29
30
    /**
31
     * @var string
32
     */
33
    private $uri;
34
35
    /**
36
     * @var string
37
     */
38
    private $host;
39
40
    /**
41
     * @var int
42
     */
43
    private $port;
44
45
    private function __construct() {}
46
47
    /**
48
     * @return Request
49
     */
50 14
    private function setMethod($method) : Request
51
    {
52 14
        $this->method = $method;
53
54 14
        return $this;
55
    }
56
57
    /**
58
     * @param string $uri
59
     * @return Request
60
     */
61 14
    private function setUri(string $uri) : Request
62
    {
63 14
        $this->uri = $uri;
64
65 14
        return $this;
66
    }
67
68
    /**
69
     * @return string
70
     */
71 9
    public function getUri() : string
72
    {
73 9
        return $this->uri;
74
    }
75
76
    /**
77
     * @return string
78
     */
79 6
    public function getMethod() : string
80
    {
81 6
        return $this->method;
82
    }
83
84
    /**
85
     * @return int
86
     */
87 1
    public function getVersion() : int
88
    {
89 1
        return (int) $this->getHeaders()->get('Sec-WebSocket-Version');
90
    }
91
92
    /**
93
     * @param int $version
94
     * @return Request
95
     */
96 4
    public function setVersion(int $version) : Request
97
    {
98 4
        $this->addHeader('Sec-WebSocket-Version', $version);
99
100 4
        return $this;
101
    }
102
103
    /**
104
     * @param string $key
105
     * @return Request
106
     */
107 4
    public function setKey(string $key) : Request
108
    {
109 4
        $this->addHeader('Sec-WebSocket-Key', $key);
110
111 4
        return $this;
112
    }
113
114
    /**
115
     * @return string|null
116
     */
117 3
    public function getKey()
118
    {
119 3
        return $this->getHeader('Sec-WebSocket-Key');
120
    }
121
122
    /**
123
     * @param string $host
124
     * @return self
125
     */
126 4
    private function setHost(string $host) : Request
127
    {
128 4
        $this->host = $host;
129
130 4
        return $this;
131
    }
132
133
    /**
134
     * @param int $port
135
     * @return Request
136
     */
137 4
    public function setPort($port)
138
    {
139 4
        $this->port = $port;
140
141 4
        return $this;
142
    }
143
144
    /**
145
     * @return array
146
     */
147 2
    public function getExtensions() : array
148
    {
149 2
        $originalHeaders = $this->getHeaders()->get('Sec-WebSocket-Extensions');
150 2
        if (!\is_array($originalHeaders)) {
151 1
            $originalHeaders = [$originalHeaders];
152
        }
153
154 2
        $extensionHeaders = [];
155 2
        $extensions = [];
156
157 2
        foreach ($originalHeaders as $extensionHeader) {
158 2
            $extensionHeaders = \array_merge($extensionHeaders, \array_map('trim', \explode(',', $extensionHeader)));
159
        }
160
161 2
        foreach ($extensionHeaders as $extension) {
162 2
            $explodingHeader = \explode(';', $extension);
163 2
            $extensionName = \trim($explodingHeader[0]);
164 2
            $extensions[$extensionName] = [];
165
166 2
            if (\count($explodingHeader)) {
167 2
                unset($explodingHeader[0]); // removing the name of the extension
168 2
                foreach($explodingHeader as $variable) {
169 1
                    $explodeVariable = \explode('=', $variable);
170
171
                    // The value can be with or without quote. We need to remove extra quotes.
172 1
                    $value = \preg_replace('/^"(.+)"$/', '$1', trim($explodeVariable[1]));
173 1
                    $value = \str_replace('\\"', '"', $value);
174
175 1
                    $extensions[$extensionName][\trim($explodeVariable[0])] = $value;
176
                }
177
            }
178
        }
179
180 2
        return $extensions;
181
    }
182
183
    /**
184
     * @return string
185
     */
186 4
    public function getRequestAsString() : string
187
    {
188 4
        $request = mb_strtoupper($this->method) . ' ' . $this->uri . " HTTP/1.1\r\n";
189 4
        $request .= 'Host: ' . $this->host . ($this->port ? ':' . $this->port : '') . "\r\n";
190 4
        $request .= 'User-Agent: Woketo/' . Meta::VERSION . "\r\n";
191 4
        $request .= "Upgrade: websocket\r\n";
192 4
        $request .= "Connection: Upgrade\r\n";
193
194 4
        foreach ($this->getHeaders() as $key => $header) {
195 4
            $request .= $key . ': ' . $header . "\r\n";
196
        }
197
198 4
        $request .= "\r\n";
199
200 4
        return $request;
201
    }
202
203
    /**
204
     * @return string
205
     */
206
    public function __toString()
207
    {
208
        return $this->getRequestAsString();
209
    }
210
211
    /**
212
     * @param string $requestString
213
     * @return Request
214
     * @throws HttpException
215
     */
216 13
    public static function create(string $requestString)
217
    {
218 13
        $request = new Request;
219
220 13
        $lines = \explode("\r\n", $requestString);
221 13
        Request::initRequest($lines[0], $request);
222
223 10
        unset($lines[0]);
224 10
        Request::initHeaders($lines, $request);
225
226 10
        if (empty($request->getHeaders()->get('Sec-WebSocket-Key')) || empty($request->getHeaders()->get('Upgrade')) || \strtolower($request->getHeaders()->get('Upgrade')) !== 'websocket') {
227
            throw new HttpException(sprintf("The request is not a websocket upgrade request, received:\n%s", $requestString));
228
        }
229
230 10
        return $request;
231
    }
232
233
    /**
234
     * @param string   $uri
235
     * @param string   $host
236
     * @param int|null $port
237
     * @return Request
238
     */
239 4
    public static function createClientRequest(string $uri, string $host, int $port = null)
240
    {
241 4
        $request = new Request();
242
243
        $request
244 4
            ->setMethod('GET')
245 4
            ->setUri($uri)
246 4
            ->setHttpVersion(Request::HTTP_1_1)
247 4
            ->setHost($host)
248
        ;
249
250 4
        if ($port) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $port of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
251 3
            $request->setPort($port);
252
        }
253
254 4
        return $request;
255
    }
256
257
    /**
258
     * @param string  $firstLine
259
     * @param Request $request
260
     * @throws HttpException
261
     */
262 13
    protected static function initRequest(string $firstLine, Request $request)
263
    {
264 13
        $httpElements = \explode(' ', $firstLine);
265
266 13
        if (\count($httpElements) < 3) {
267 1
            throw Request::createNotHttpException($firstLine);
268
        }
269
270 12
        $httpElements[2] = \trim($httpElements[2]);
271 12
        if (!\preg_match('/HTTP\/.+/', $httpElements[2])) {
272 1
            throw Request::createNotHttpException($firstLine);
273
        }
274 11
        $request->setHttpVersion($httpElements[2]);
275
276 11
        if (!\in_array($httpElements[0], ['POST', 'GET', 'PUT', 'DELETE'])) {
277 1
            throw new HttpException(
278 1
                \sprintf('Request not supported, only POST, GET, PUT, and DELETE are supported. "%s" received.', $httpElements[0])
279
            );
280
        }
281
282 10
        $request->setMethod($httpElements[0]);
283 10
        $request->setUri($httpElements[1]);
284 10
    }
285
}
286