diff --git a/src/Server.php b/src/Server.php index 1a33bcbd..c74c06a2 100644 --- a/src/Server.php +++ b/src/Server.php @@ -88,7 +88,7 @@ public function handleConnection(ConnectionInterface $conn) $that->handleRequest($conn, $request); if ($bodyBuffer !== '') { - $request->emit('data', array($bodyBuffer)); + $conn->emit('data', array($bodyBuffer)); } }); @@ -122,6 +122,15 @@ public function handleRequest(ConnectionInterface $conn, Request $request) '[]' ); + $stream = $conn; + if ($request->hasHeader('Transfer-Encoding')) { + $transferEncodingHeader = $request->getHeader('Transfer-Encoding'); + // 'chunked' must always be the final value of 'Transfer-Encoding' according to: https://tools.ietf.org/html/rfc7230#section-3.3.1 + if (strtolower(end($transferEncodingHeader)) === 'chunked') { + $stream = new ChunkedDecoder($conn); + } + } + // forward pause/resume calls to underlying connection $request->on('pause', array($conn, 'pause')); $request->on('resume', array($conn, 'resume')); @@ -133,13 +142,14 @@ public function handleRequest(ConnectionInterface $conn, Request $request) }); // forward connection events to request - $conn->on('end', function () use ($request) { - $request->emit('end'); - }); - $conn->on('data', function ($data) use ($request) { + $stream->on('data', function ($data) use ($request) { $request->emit('data', array($data)); }); + $stream->on('end', function () use ($request) { + $request->emit('end'); + }); + $this->emit('request', array($request, $response)); } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index d2ff7aef..84b33ca9 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -265,6 +265,179 @@ function ($data) use (&$buffer) { $this->assertContains("\r\n\r\nError 400: Bad Request", $buffer); } + public function testBodyDataWillBeSendViaRequestEvent() + { + $server = new Server($this->socket); + + $dataEvent = $this->expectCallableOnceWith('hello'); + $endEvent = $this->expectCallableNever(); + $closeEvent = $this->expectCallableNever(); + $errorEvent = $this->expectCallableNever(); + + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->on('data', $dataEvent); + $request->on('end', $endEvent); + $request->on('close', $closeEvent); + $request->on('error', $errorEvent); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\r\n"; + $data .= "Host: example.com:80\r\n"; + $data .= "Connection: close\r\n"; + $data .= "Content-Length: 5\r\n"; + $data .= "\r\n"; + $data .= "hello"; + + $this->connection->emit('data', array($data)); + } + + public function testChunkedEncodedRequestWillBeParsedForRequestEvent() + { + $server = new Server($this->socket); + + $dataEvent = $this->expectCallableOnceWith('hello'); + $endEvent = $this->expectCallableOnce(); + $closeEvent = $this->expectCallableNever(); + $errorEvent = $this->expectCallableNever(); + + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->on('data', $dataEvent); + $request->on('end', $endEvent); + $request->on('close', $closeEvent); + $request->on('error', $errorEvent); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\r\n"; + $data .= "Host: example.com:80\r\n"; + $data .= "Connection: close\r\n"; + $data .= "Transfer-Encoding: chunked\r\n"; + $data .= "\r\n"; + $data .= "5\r\nhello\r\n"; + $data .= "0\r\n\r\n"; + + $this->connection->emit('data', array($data)); + } + + public function testChunkedEncodedRequestAdditionalDataWontBeEmitted() + { + $server = new Server($this->socket); + + $dataEvent = $this->expectCallableOnceWith('hello'); + $endEvent = $this->expectCallableOnce(); + $closeEvent = $this->expectCallableNever(); + $errorEvent = $this->expectCallableNever(); + + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->on('data', $dataEvent); + $request->on('end', $endEvent); + $request->on('close', $closeEvent); + $request->on('error', $errorEvent); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\r\n"; + $data .= "Host: example.com:80\r\n"; + $data .= "Connection: close\r\n"; + $data .= "Transfer-Encoding: chunked\r\n"; + $data .= "\r\n"; + $data .= "5\r\nhello\r\n"; + $data .= "0\r\n\r\n"; + $data .= "2\r\nhi\r\n"; + + $this->connection->emit('data', array($data)); + } + + public function testEmptyChunkedEncodedRequest() + { + $server = new Server($this->socket); + + $dataEvent = $this->expectCallableNever(); + $endEvent = $this->expectCallableOnce(); + $closeEvent = $this->expectCallableNever(); + $errorEvent = $this->expectCallableNever(); + + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->on('data', $dataEvent); + $request->on('end', $endEvent); + $request->on('close', $closeEvent); + $request->on('error', $errorEvent); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\r\n"; + $data .= "Host: example.com:80\r\n"; + $data .= "Connection: close\r\n"; + $data .= "Transfer-Encoding: chunked\r\n"; + $data .= "\r\n"; + $data .= "0\r\n\r\n"; + + $this->connection->emit('data', array($data)); + } + + public function testChunkedIsUpperCase() + { + $server = new Server($this->socket); + + $dataEvent = $this->expectCallableOnceWith('hello'); + $endEvent = $this->expectCallableOnce(); + $closeEvent = $this->expectCallableNever(); + $errorEvent = $this->expectCallableNever(); + + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->on('data', $dataEvent); + $request->on('end', $endEvent); + $request->on('close', $closeEvent); + $request->on('error', $errorEvent); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\r\n"; + $data .= "Host: example.com:80\r\n"; + $data .= "Connection: close\r\n"; + $data .= "Transfer-Encoding: CHUNKED\r\n"; + $data .= "\r\n"; + $data .= "5\r\nhello\r\n"; + $data .= "0\r\n\r\n"; + + $this->connection->emit('data', array($data)); + } + + public function testChunkedIsMixedUpperAndLowerCase() + { + $server = new Server($this->socket); + + $dataEvent = $this->expectCallableOnceWith('hello'); + $endEvent = $this->expectCallableOnce(); + $closeEvent = $this->expectCallableNever(); + $errorEvent = $this->expectCallableNever(); + + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->on('data', $dataEvent); + $request->on('end', $endEvent); + $request->on('close', $closeEvent); + $request->on('error', $errorEvent); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\r\n"; + $data .= "Host: example.com:80\r\n"; + $data .= "Connection: close\r\n"; + $data .= "Transfer-Encoding: CHunKeD\r\n"; + $data .= "\r\n"; + $data .= "5\r\nhello\r\n"; + $data .= "0\r\n\r\n"; + + $this->connection->emit('data', array($data)); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n";