Completed
Push — master ( e26ab3...1dd3f4 )
by Maxime
9s
created

Request::initHeaders()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 0
cts 7
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 2
crap 6
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
    private function setMethod($method) : Request
51
    {
52
        $this->method = $method;
53
54
        return $this;
55
    }
56
57
    /**
58
     * @param string $uri
59
     * @return Request
60
     */
61
    private function setUri(string $uri) : Request
62
    {
63
        $this->uri = $uri;
64
65
        return $this;
66
    }
67
68
    /**
69
     * @return string
70
     */
71
    public function getUri() : string
72
    {
73
        return $this->uri;
74
    }
75
76
    /**
77
     * @return string
78
     */
79
    public function getMethod() : string
80
    {
81
        return $this->method;
82
    }
83
84
    /**
85
     * @return int
86
     */
87
    public function getVersion() : int
88
    {
89
        return (int) $this->getHeaders()->get('Sec-WebSocket-Version');
90
    }
91
92
    /**
93
     * @param int $version
94
     * @return Request
95
     */
96
    public function setVersion(int $version) : Request
97
    {
98
        $this->addHeader('Sec-WebSocket-Version', $version);
99
100
        return $this;
101
    }
102
103
    /**
104
     * @param string $key
105
     * @return Request
106
     */
107
    public function setKey(string $key) : Request
108
    {
109
        $this->addHeader('Sec-WebSocket-Key', $key);
110
111
        return $this;
112
    }
113
114
    /**
115
     * @return string|null
116
     */
117
    public function getKey()
118
    {
119
        return $this->getHeader('Sec-WebSocket-Key');
120
    }
121
122
    /**
123
     * @param string $host
124
     * @return self
125
     */
126
    private function setHost(string $host) : Request
127
    {
128
        $this->host = $host;
129
130
        return $this;
131
    }
132
133
    /**
134
     * @param int $port
135
     * @return Request
136
     */
137
    public function setPort($port)
138
    {
139
        $this->port = $port;
140
141
        return $this;
142
    }
143
144
    /**
145
     * @return array
146
     */
147
    public function getExtensions() : array
148
    {
149
        $originalHeaders = $this->getHeaders()->get('Sec-WebSocket-Extensions');
150
        if (!\is_array($originalHeaders)) {
151
            $originalHeaders = [$originalHeaders];
152
        }
153
154
        $extensionHeaders = [];
155
        $extensions = [];
156
157
        foreach ($originalHeaders as $extensionHeader) {
158
            $extensionHeaders = \array_merge($extensionHeaders, \array_map('trim', \explode(',', $extensionHeader)));
159
        }
160
161
        foreach ($extensionHeaders as $extension) {
162
            $explodingHeader = \explode(';', $extension);
163
            $extensionName = \trim($explodingHeader[0]);
164
            $extensions[$extensionName] = [];
165
166
            if (\count($explodingHeader)) {
167
                unset($explodingHeader[0]); // removing the name of the extension
168
                foreach($explodingHeader as $variable) {
169
                    $explodeVariable = \explode('=', $variable);
170
171
                    // The value can be with or without quote. We need to remove extra quotes.
172
                    $value = \preg_replace('/^"(.+)"$/', '$1', trim($explodeVariable[1]));
173
                    $value = \str_replace('\\"', '"', $value);
174
175
                    $extensions[$extensionName][\trim($explodeVariable[0])] = $value;
176
                }
177
            }
178
        }
179
180
        return $extensions;
181
    }
182
183
    /**
184
     * @return string
185
     */
186
    public function getRequestAsString() : string
187
    {
188
        $request = mb_strtoupper($this->method) . ' ' . $this->uri . " HTTP/1.1\r\n";
189
        $request .= 'Host: ' . $this->host . ($this->port ? ':' . $this->port : '') . "\r\n";
190
        $request .= 'User-Agent: Woketo/' . Meta::VERSION . "\r\n";
191
        $request .= "Upgrade: websocket\r\n";
192
        $request .= "Connection: Upgrade\r\n";
193
194
        foreach ($this->getHeaders() as $key => $header) {
195
            $request .= $key . ': ' . $header . "\r\n";
196
        }
197
198
        $request .= "\r\n";
199
200
        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
    public static function create(string $requestString)
217
    {
218
        $request = new Request;
219
220
        $lines = \explode("\r\n", $requestString);
221
        Request::initRequest($lines[0], $request);
222
223
        unset($lines[0]);
224
        Request::initHeaders($lines, $request);
225
226
        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
        return $request;
231
    }
232
233
    /**
234
     * @param string   $uri
235
     * @param string   $host
236
     * @param int|null $port
237
     * @return Request
238
     */
239
    public static function createClientRequest(string $uri, string $host, int $port = null)
240
    {
241
        $request = new Request();
242
243
        $request
244
            ->setMethod('GET')
245
            ->setUri($uri)
246
            ->setHttpVersion(Request::HTTP_1_1)
247
            ->setHost($host)
248
        ;
249
        
250
        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
            $request->setPort($port);
252
        }
253
254
        return $request;
255
    }
256
257
    /**
258
     * @param string  $firstLine
259
     * @param Request $request
260
     * @throws HttpException
261
     */
262
    protected static function initRequest(string $firstLine, Request $request)
263
    {
264
        $httpElements = \explode(' ', $firstLine);
265
266
        if (\count($httpElements) < 3) {
267
            throw Request::createNotHttpException($firstLine);
268
        }
269
270
        $httpElements[2] = \trim($httpElements[2]);
271
        if (!\preg_match('/HTTP\/.+/', $httpElements[2])) {
272
            throw Request::createNotHttpException($firstLine);
273
        }
274
        $request->setHttpVersion($httpElements[2]);
275
276
        if (!\in_array($httpElements[0], ['POST', 'GET', 'PUT', 'DELETE'])) {
277
            throw new HttpException(
278
                \sprintf('Request not supported, only POST, GET, PUT, and DELETE are supported. "%s" received.', $httpElements[0])
279
            );
280
        }
281
282
        $request->setMethod($httpElements[0]);
283
        $request->setUri($httpElements[1]);
284
    }
285
}
286