Skip to content

Commit 61a69d7

Browse files
authored
Merge pull request #125 from clue-labs/protocol-version
Support HTTP/1.1 and HTTP/1.0
2 parents ae70a3a + 531c466 commit 61a69d7

File tree

5 files changed

+185
-28
lines changed

5 files changed

+185
-28
lines changed

README.md

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,10 @@ $http->on('request', function (Request $request, Response $response) {
8888

8989
See also [`Request`](#request) and [`Response`](#response) for more details.
9090

91-
If a client sends an invalid request message, it will emit an `error` event,
92-
send an HTTP error response to the client and close the connection:
91+
The `Server` supports both HTTP/1.1 and HTTP/1.0 request messages.
92+
If a client sends an invalid request message or uses an invalid HTTP protocol
93+
version, it will emit an `error` event, send an HTTP error response to the
94+
client and close the connection:
9395

9496
```php
9597
$http->on('error', function (Exception $e) {
@@ -178,6 +180,13 @@ It implements the `WritableStreamInterface`.
178180
The constructor is internal, you SHOULD NOT call this yourself.
179181
The `Server` is responsible for emitting `Request` and `Response` objects.
180182

183+
The `Response` will automatically use the same HTTP protocol version as the
184+
corresponding `Request`.
185+
186+
HTTP/1.1 responses will automatically apply chunked transfer encoding if
187+
no `Content-Length` header has been set.
188+
See [`writeHead()`](#writehead) for more details.
189+
181190
See the above usage example and the class outline for details.
182191

183192
#### writeContinue()
@@ -210,12 +219,15 @@ $http->on('request', function (Request $request, Response $response) {
210219
});
211220
```
212221

213-
Note that calling this method is strictly optional.
214-
If you do not use it, then the client MUST continue sending the request body
215-
after waiting some time.
222+
Note that calling this method is strictly optional for HTTP/1.1 responses.
223+
If you do not use it, then a HTTP/1.1 client MUST continue sending the
224+
request body after waiting some time.
216225

217-
This method MUST NOT be invoked after calling `writeHead()`.
218-
Calling this method after sending the headers will result in an `Exception`.
226+
This method MUST NOT be invoked after calling [`writeHead()`](#writehead).
227+
This method MUST NOT be invoked if this is not a HTTP/1.1 response
228+
(please check [`expectsContinue()`](#expectscontinue) as above).
229+
Calling this method after sending the headers or if this is not a HTTP/1.1
230+
response is an error that will result in an `Exception`.
219231

220232
#### writeHead()
221233

@@ -234,7 +246,7 @@ $response->end('Hello World!');
234246

235247
Calling this method more than once will result in an `Exception`.
236248

237-
Unless you specify a `Content-Length` header yourself, the response message
249+
Unless you specify a `Content-Length` header yourself, HTTP/1.1 responses
238250
will automatically use chunked transfer encoding and send the respective header
239251
(`Transfer-Encoding: chunked`) automatically. If you know the length of your
240252
body, you MAY specify it like this instead:

src/Response.php

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,27 @@
1414
* The constructor is internal, you SHOULD NOT call this yourself.
1515
* The `Server` is responsible for emitting `Request` and `Response` objects.
1616
*
17+
* The `Response` will automatically use the same HTTP protocol version as the
18+
* corresponding `Request`.
19+
*
20+
* HTTP/1.1 responses will automatically apply chunked transfer encoding if
21+
* no `Content-Length` header has been set.
22+
* See `writeHead()` for more details.
23+
*
1724
* See the usage examples and the class outline for details.
1825
*
1926
* @see WritableStreamInterface
2027
* @see Server
2128
*/
2229
class Response extends EventEmitter implements WritableStreamInterface
2330
{
31+
private $conn;
32+
private $protocolVersion;
33+
2434
private $closed = false;
2535
private $writable = true;
26-
private $conn;
2736
private $headWritten = false;
28-
private $chunkedEncoding = true;
37+
private $chunkedEncoding = false;
2938

3039
/**
3140
* The constructor is internal, you SHOULD NOT call this yourself.
@@ -36,9 +45,11 @@ class Response extends EventEmitter implements WritableStreamInterface
3645
*
3746
* @internal
3847
*/
39-
public function __construct(ConnectionInterface $conn)
48+
public function __construct(ConnectionInterface $conn, $protocolVersion = '1.1')
4049
{
4150
$this->conn = $conn;
51+
$this->protocolVersion = $protocolVersion;
52+
4253
$that = $this;
4354
$this->conn->on('end', function () use ($that) {
4455
$that->close();
@@ -87,19 +98,25 @@ public function isWritable()
8798
* });
8899
* ```
89100
*
90-
* Note that calling this method is strictly optional.
91-
* If you do not use it, then the client MUST continue sending the request body
92-
* after waiting some time.
101+
* Note that calling this method is strictly optional for HTTP/1.1 responses.
102+
* If you do not use it, then a HTTP/1.1 client MUST continue sending the
103+
* request body after waiting some time.
93104
*
94105
* This method MUST NOT be invoked after calling `writeHead()`.
95-
* Calling this method after sending the headers will result in an `Exception`.
106+
* This method MUST NOT be invoked if this is not a HTTP/1.1 response
107+
* (please check [`expectsContinue()`] as above).
108+
* Calling this method after sending the headers or if this is not a HTTP/1.1
109+
* response is an error that will result in an `Exception`.
96110
*
97111
* @return void
98112
* @throws \Exception
99113
* @see Request::expectsContinue()
100114
*/
101115
public function writeContinue()
102116
{
117+
if ($this->protocolVersion !== '1.1') {
118+
throw new \Exception('Continue requires a HTTP/1.1 message');
119+
}
103120
if ($this->headWritten) {
104121
throw new \Exception('Response head has already been written.');
105122
}
@@ -122,7 +139,7 @@ public function writeContinue()
122139
*
123140
* Calling this method more than once will result in an `Exception`.
124141
*
125-
* Unless you specify a `Content-Length` header yourself, the response message
142+
* Unless you specify a `Content-Length` header yourself, HTTP/1.1 responses
126143
* will automatically use chunked transfer encoding and send the respective header
127144
* (`Transfer-Encoding: chunked`) automatically. If you know the length of your
128145
* body, you MAY specify it like this instead:
@@ -167,11 +184,6 @@ public function writeHead($status = 200, array $headers = array())
167184

168185
$lower = array_change_key_case($headers);
169186

170-
// disable chunked encoding if content-length is given
171-
if (isset($lower['content-length'])) {
172-
$this->chunkedEncoding = false;
173-
}
174-
175187
// assign default "X-Powered-By" header as first for history reasons
176188
if (!isset($lower['x-powered-by'])) {
177189
$headers = array_merge(
@@ -180,15 +192,16 @@ public function writeHead($status = 200, array $headers = array())
180192
);
181193
}
182194

183-
// assign chunked transfer-encoding if chunked encoding is used
184-
if ($this->chunkedEncoding) {
195+
// assign chunked transfer-encoding if no 'content-length' is given for HTTP/1.1 responses
196+
if (!isset($lower['content-length']) && $this->protocolVersion === '1.1') {
185197
foreach($headers as $name => $value) {
186198
if (strtolower($name) === 'transfer-encoding') {
187199
unset($headers[$name]);
188200
}
189201
}
190202

191203
$headers['Transfer-Encoding'] = 'chunked';
204+
$this->chunkedEncoding = true;
192205
}
193206

194207
$data = $this->formatHead($status, $headers);
@@ -201,7 +214,7 @@ private function formatHead($status, array $headers)
201214
{
202215
$status = (int) $status;
203216
$text = isset(ResponseCodes::$statusTexts[$status]) ? ResponseCodes::$statusTexts[$status] : '';
204-
$data = "HTTP/1.1 $status $text\r\n";
217+
$data = "HTTP/$this->protocolVersion $status $text\r\n";
205218

206219
foreach ($headers as $name => $value) {
207220
$name = str_replace(array("\r", "\n"), '', $name);

src/Server.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@
2828
*
2929
* See also [`Request`](#request) and [`Response`](#response) for more details.
3030
*
31-
* If a client sends an invalid request message, it will emit an `error` event,
32-
* send an HTTP error response to the client and close the connection:
31+
* The `Server` supports both HTTP/1.1 and HTTP/1.0 request messages.
32+
* If a client sends an invalid request message or uses an invalid HTTP protocol
33+
* version, it will emit an `error` event, send an HTTP error response to the
34+
* client and close the connection:
3335
*
3436
* ```php
3537
* $http->on('error', function (Exception $e) {
@@ -107,7 +109,13 @@ public function handleConnection(ConnectionInterface $conn)
107109
/** @internal */
108110
public function handleRequest(ConnectionInterface $conn, Request $request)
109111
{
110-
$response = new Response($conn);
112+
// only support HTTP/1.1 and HTTP/1.0 requests
113+
if ($request->getProtocolVersion() !== '1.1' && $request->getProtocolVersion() !== '1.0') {
114+
$this->emit('error', array(new \InvalidArgumentException('Received request with invalid protocol version')));
115+
return $this->writeError($conn, 505);
116+
}
117+
118+
$response = new Response($conn, $request->getProtocolVersion());
111119
$response->on('close', array($request, 'close'));
112120

113121
if (!$this->listeners('request')) {

tests/ResponseTest.php

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,25 @@ public function testResponseShouldBeChunkedByDefault()
2626
$response->writeHead();
2727
}
2828

29+
public function testResponseShouldNotBeChunkedWhenProtocolVersionIsNot11()
30+
{
31+
$expected = '';
32+
$expected .= "HTTP/1.0 200 OK\r\n";
33+
$expected .= "X-Powered-By: React/alpha\r\n";
34+
$expected .= "\r\n";
35+
36+
$conn = $this
37+
->getMockBuilder('React\Socket\ConnectionInterface')
38+
->getMock();
39+
$conn
40+
->expects($this->once())
41+
->method('write')
42+
->with($expected);
43+
44+
$response = new Response($conn, '1.0');
45+
$response->writeHead();
46+
}
47+
2948
public function testResponseShouldBeChunkedEvenWithOtherTransferEncoding()
3049
{
3150
$expected = '';
@@ -46,7 +65,6 @@ public function testResponseShouldBeChunkedEvenWithOtherTransferEncoding()
4665
$response->writeHead(200, array('transfer-encoding' => 'custom'));
4766
}
4867

49-
5068
public function testResponseShouldNotBeChunkedWithContentLength()
5169
{
5270
$expected = '';
@@ -221,6 +239,20 @@ public function writeContinueShouldSendContinueLineBeforeRealHeaders()
221239
$response->writeHead();
222240
}
223241

242+
/**
243+
* @test
244+
* @expectedException Exception
245+
*/
246+
public function writeContinueShouldThrowForHttp10()
247+
{
248+
$conn = $this
249+
->getMockBuilder('React\Socket\ConnectionInterface')
250+
->getMock();
251+
252+
$response = new Response($conn, '1.0');
253+
$response->writeContinue();
254+
}
255+
224256
/** @test */
225257
public function shouldForwardEndDrainAndErrorEvents()
226258
{

tests/ServerTest.php

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,98 @@ function ($data) use (&$buffer) {
216216
$this->assertContains("\r\nX-Powered-By: React/alpha\r\n", $buffer);
217217
}
218218

219+
public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForHttp11()
220+
{
221+
$server = new Server($this->socket);
222+
$server->on('request', function (Request $request, Response $response) {
223+
$response->writeHead();
224+
$response->end('bye');
225+
});
226+
227+
$buffer = '';
228+
229+
$this->connection
230+
->expects($this->any())
231+
->method('write')
232+
->will(
233+
$this->returnCallback(
234+
function ($data) use (&$buffer) {
235+
$buffer .= $data;
236+
}
237+
)
238+
);
239+
240+
$this->socket->emit('connection', array($this->connection));
241+
242+
$data = "GET / HTTP/1.1\r\n\r\n";
243+
$this->connection->emit('data', array($data));
244+
245+
$this->assertContains("HTTP/1.1 200 OK\r\n", $buffer);
246+
$this->assertContains("\r\n\r\n3\r\nbye\r\n0\r\n\r\n", $buffer);
247+
}
248+
249+
public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp10()
250+
{
251+
$server = new Server($this->socket);
252+
$server->on('request', function (Request $request, Response $response) {
253+
$response->writeHead();
254+
$response->end('bye');
255+
});
256+
257+
$buffer = '';
258+
259+
$this->connection
260+
->expects($this->any())
261+
->method('write')
262+
->will(
263+
$this->returnCallback(
264+
function ($data) use (&$buffer) {
265+
$buffer .= $data;
266+
}
267+
)
268+
);
269+
270+
$this->socket->emit('connection', array($this->connection));
271+
272+
$data = "GET / HTTP/1.0\r\n\r\n";
273+
$this->connection->emit('data', array($data));
274+
275+
$this->assertContains("HTTP/1.0 200 OK\r\n", $buffer);
276+
$this->assertContains("\r\n\r\nbye", $buffer);
277+
}
278+
279+
public function testRequestInvalidHttpProtocolVersionWillEmitErrorAndSendErrorResponse()
280+
{
281+
$error = null;
282+
$server = new Server($this->socket);
283+
$server->on('error', function ($message) use (&$error) {
284+
$error = $message;
285+
});
286+
287+
$buffer = '';
288+
289+
$this->connection
290+
->expects($this->any())
291+
->method('write')
292+
->will(
293+
$this->returnCallback(
294+
function ($data) use (&$buffer) {
295+
$buffer .= $data;
296+
}
297+
)
298+
);
299+
300+
$this->socket->emit('connection', array($this->connection));
301+
302+
$data = "GET / HTTP/1.2\r\nHost: localhost\r\n\r\n";
303+
$this->connection->emit('data', array($data));
304+
305+
$this->assertInstanceOf('InvalidArgumentException', $error);
306+
307+
$this->assertContains("HTTP/1.1 505 HTTP Version Not Supported\r\n", $buffer);
308+
$this->assertContains("\r\n\r\nError 505: HTTP Version Not Supported", $buffer);
309+
}
310+
219311
public function testParserErrorEmitted()
220312
{
221313
$error = null;

0 commit comments

Comments
 (0)