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

Response::create()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 34
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
dl 0
loc 34
ccs 0
cts 25
cp 0
rs 8.439
c 0
b 0
f 0
cc 5
eloc 19
nc 7
nop 1
crap 30
1
<?php
2
/**
3
 * This file is a part of a nekland library
4
 *
5
 * (c) Nekland <[email protected]>
6
 *
7
 * For the full license, take a look to the LICENSE file
8
 * on the root directory of this project
9
 */
10
11
namespace Nekland\Woketo\Http;
12
13
14
use Nekland\Woketo\Exception\Http\HttpException;
15
use Nekland\Woketo\Exception\Http\IncompleteHttpMessageException;
16
use React\Socket\ConnectionInterface;
17
18
/**
19
 * Class Response
20
 *
21
 * @internal
22
 */
23
class Response extends AbstractHttpMessage
24
{
25
    const SWITCHING_PROTOCOLS = '101 Switching Protocols';
26
    const BAD_REQUEST = '400 Bad Request';
27
28
    /**
29
     * @var string For example "404 Not Found"
30
     */
31
    private $httpResponse;
32
33
    /**
34
     * @var int
35
     */
36
    private $statusCode;
37
38
    /**
39
     * @var string
40
     */
41
    private $reason;
42
43
    public function __construct()
44
    {
45
        $this->setHttpVersion('HTTP/1.1');
46
    }
47
48
    /**
49
     * @param string $httpResponse
50
     * @return Response
51
     */
52
    public function setHttpResponse($httpResponse)
53
    {
54
        $this->httpResponse = $httpResponse;
55
56
        return $this;
57
    }
58
59
    /**
60
     * @return int
61
     */
62
    public function getStatusCode(): int
63
    {
64
        return (int) $this->statusCode;
65
    }
66
67
    /**
68
     * @param int $statusCode
69
     * @return Response
70
     */
71
    public function setStatusCode(int $statusCode)
72
    {
73
        $this->statusCode = $statusCode;
74
75
        return $this;
76
    }
77
78
    /**
79
     * @return string
80
     */
81
    public function getReason(): string
82
    {
83
        return $this->reason;
84
    }
85
86
    /**
87
     * @param string $reason
88
     * @return Response
89
     */
90
    public function setReason(string $reason)
91
    {
92
        $this->reason = $reason;
93
94
        return $this;
95
    }
96
97
    /**
98
     * @return string
99
     */
100
    public function getAcceptKey()
101
    {
102
        return $this->getHeader('Sec-WebSocket-Accept');
103
    }
104
105
    /**
106
     * @param ConnectionInterface $stream
107
     */
108
    public function send(ConnectionInterface $stream)
109
    {
110
        $stringResponse = $this->getHttpVersion() . ' ' . $this->httpResponse . "\r\n";
111
112
        foreach ($this->getHeaders() as $name => $content) {
113
            $stringResponse .= $name . ': '. $content . "\r\n";
114
        }
115
116
        // No content to concatenate
117
        $stringResponse .= "\r\n";
118
119
        $stream->write($stringResponse);
120
    }
121
122
    public static function createSwitchProtocolResponse()
123
    {
124
        $response = new Response();
125
126
        $response->setHttpResponse(Response::SWITCHING_PROTOCOLS);
127
        $response->addHeader('Upgrade', 'websocket');
128
        $response->addHeader('Connection', 'Upgrade');
129
130
        return $response;
131
    }
132
133
    /**
134
     * @param string $data
135
     * @return Response
136
     * @throws HttpException
137
     */
138
    public static function create(string &$data) : Response
139
    {
140
        if (!\preg_match('/\\r\\n\\r\\n/', $data)) {
141
            throw new IncompleteHttpMessageException();
142
        }
143
144
        // Split response headers from potential content
145
        $exploded = explode("\r\n\r\n", $data);
146
        $responseString = '';
147
148
        if (count($exploded) > 1) {
149
            // Removes the request keep content in data reference
150
            $responseString = $exploded[0];
151
            unset($exploded[0]);
152
            $data = implode("\r\n\r\n", $exploded);
153
        }
154
155
        $response = new Response();
156
157
        $lines = \explode("\r\n", $responseString);
158
        Response::initResponse($lines[0], $response);
159
160
        unset($lines[0]);
161
        Response::initHeaders($lines, $response);
162
163
        if (strtolower($response->getHeader('Upgrade')) !== 'websocket') {
164
            throw new HttpException('Missing or wrong upgrade header.');
165
        }
166
        if (strtolower($response->getHeader('Connection')) !== 'upgrade') {
167
            throw new HttpException('Missing "Connection: Upgrade" header.');
168
        }
169
170
        return $response;
171
    }
172
173
    /**
174
     * @param string   $firstLine
175
     * @param Response $response
176
     * @throws HttpException
177
     */
178
    protected static function initResponse(string $firstLine, Response $response)
179
    {
180
        $httpElements = \explode(' ', $firstLine);
181
182
        if (!\preg_match('/HTTP\/[1-2\.]+/',$httpElements[0])) {
183
            throw Response::createNotHttpException($firstLine);
184
        }
185
        $response->setHttpVersion($httpElements[0]);
186
        unset($httpElements[0]);
187
188
        if ($httpElements[1] != 101) {
189
            throw new HttpException(
190
                sprintf('Attempted 101 response but got %s, message: %s', $httpElements[1], $firstLine)
191
            );
192
        }
193
        $response->setStatusCode($httpElements[1]);
194
        unset($httpElements[1]);
195
196
        $response->setReason(implode(' ', $httpElements));
197
    }
198
}
199