diff --git a/README.md b/README.md index 1006559f..939f474c 100644 --- a/README.md +++ b/README.md @@ -271,7 +271,13 @@ $http = new Server($socket, function (RequestInterface $request) { Note that the server supports *any* request method (including custom and non- standard ones) and all request-target formats defined in the HTTP specs for each -respective method. +respective method, including *normal* `origin-form` requests as well as +proxy requests in `absolute-form` and `authority-form`. +The `getUri(): UriInterface` method can be used to get the effective request +URI which provides you access to individiual URI components. +Note that (depending on the given `request-target`) certain URI components may +or may not be present, for example the `getPath(): string` method will return +an empty string for requests in `asterisk-form` or `authority-form`. You can use `getMethod(): string` and `getRequestTarget(): string` to check this is an accepted request and may want to reject other requests with an appropriate error code, such as `400` (Bad Request) or `405` (Method Not @@ -431,7 +437,7 @@ to the message if the same request would have used an (unconditional) `GET`. response for tunneled application data. This implies that that a `2xx` (Successful) response to a `CONNECT` request can in fact use a streaming response body for the tunneled application data. - See also [example #21](examples) for more details. + See also [example #22](examples) for more details. A `Date` header will be automatically added with the system date and time if none is given. You can add a custom `Date` header yourself like this: diff --git a/examples/21-http-proxy.php b/examples/21-http-proxy.php new file mode 100644 index 00000000..720f51fe --- /dev/null +++ b/examples/21-http-proxy.php @@ -0,0 +1,45 @@ +getRequestTarget(), '://') === false) { + return new Response( + 400, + array('Content-Type' => 'text/plain'), + 'This is a plain HTTP proxy' + ); + } + + // prepare outgoing client request by updating request-target and Host header + $host = (string)$request->getUri()->withScheme('')->withPath('')->withQuery(''); + $target = (string)$request->getUri()->withScheme('')->withHost('')->withPort(null); + if ($target === '') { + $target = $request->getMethod() === 'OPTIONS' ? '*' : '/'; + } + $outgoing = $request->withRequestTarget($target)->withHeader('Host', $host); + + // pseudo code only: simply dump the outgoing request as a string + // left up as an exercise: use an HTTP client to send the outgoing request + // and forward the incoming response to the original client request + return new Response( + 200, + array('Content-Type' => 'text/plain'), + Psr7\str($outgoing) + ); +}); + +//$server->on('error', 'printf'); + +echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; + +$loop->run(); diff --git a/examples/21-connect-proxy.php b/examples/22-connect-proxy.php similarity index 100% rename from examples/21-connect-proxy.php rename to examples/22-connect-proxy.php diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 792a0604..29d23a9a 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -89,6 +89,16 @@ private function parseRequest($data) )->withRequestTarget($originalTarget); } + // ensure absolute-form request-target contains a valid URI + if (strpos($request->getRequestTarget(), '://') !== false) { + $parts = parse_url($request->getRequestTarget()); + + // make sure value contains valid host component (IP or hostname), but no fragment + if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'http' || isset($parts['fragment'])) { + throw new \InvalidArgumentException('Invalid absolute-form request-target'); + } + } + return array($request, $bodyBuffer); } } diff --git a/tests/RequestHeaderParserTest.php b/tests/RequestHeaderParserTest.php index dfc25f4f..f0422361 100644 --- a/tests/RequestHeaderParserTest.php +++ b/tests/RequestHeaderParserTest.php @@ -167,6 +167,38 @@ public function testGuzzleRequestParseException() $this->assertSame(0, count($parser->listeners('error'))); } + public function testInvalidAbsoluteFormSchemeEmitsError() + { + $error = null; + + $parser = new RequestHeaderParser(); + $parser->on('headers', $this->expectCallableNever()); + $parser->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $parser->feed("GET tcp://example.com:80/ HTTP/1.0\r\n\r\n"); + + $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); + } + + public function testInvalidAbsoluteFormWithFragmentEmitsError() + { + $error = null; + + $parser = new RequestHeaderParser(); + $parser->on('headers', $this->expectCallableNever()); + $parser->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $parser->feed("GET http://example.com:80/#home HTTP/1.0\r\n\r\n"); + + $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; diff --git a/tests/ServerTest.php b/tests/ServerTest.php index b7ddac2f..40aafc96 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -267,6 +267,121 @@ public function testRequestNonConnectWithAuthorityRequestTargetWillReject() $this->connection->emit('data', array($data)); } + public function testRequestAbsoluteEvent() + { + $requestAssertion = null; + + $server = new Server($this->socket, function (RequestInterface $request) use (&$requestAssertion) { + $requestAssertion = $request; + return new Response(); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET http://example.com/test HTTP/1.1\r\nHost: example.com\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('GET', $requestAssertion->getMethod()); + $this->assertSame('http://example.com/test', $requestAssertion->getRequestTarget()); + $this->assertEquals('http://example.com/test', $requestAssertion->getUri()); + $this->assertSame('/test', $requestAssertion->getUri()->getPath()); + //$this->assertSame(array(), $requestAssertion->getQueryParams()); + + $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); + } + + public function testRequestAbsoluteAddsMissingHostEvent() + { + $requestAssertion = null; + + $server = new Server($this->socket, function (RequestInterface $request) use (&$requestAssertion) { + $requestAssertion = $request; + return new Response(); + }); + $server->on('error', 'printf'); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET http://example.com:8080/test HTTP/1.0\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('GET', $requestAssertion->getMethod()); + $this->assertSame('http://example.com:8080/test', $requestAssertion->getRequestTarget()); + $this->assertEquals('http://example.com:8080/test', $requestAssertion->getUri()); + $this->assertSame('/test', $requestAssertion->getUri()->getPath()); + //$this->assertSame(array(), $requestAssertion->getQueryParams()); + + $this->assertSame('example.com:8080', $requestAssertion->getHeaderLine('Host')); + } + + public function testRequestAbsoluteNonMatchingHostWillBePassedAsIs() + { + $requestAssertion = null; + + $server = new Server($this->socket, function (RequestInterface $request) use (&$requestAssertion) { + $requestAssertion = $request; + return new Response(); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET http://example.com/test HTTP/1.1\r\nHost: other.example.org\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('GET', $requestAssertion->getMethod()); + $this->assertSame('http://example.com/test', $requestAssertion->getRequestTarget()); + $this->assertEquals('http://example.com/test', $requestAssertion->getUri()); + $this->assertSame('/test', $requestAssertion->getUri()->getPath()); + $this->assertSame('other.example.org', $requestAssertion->getHeaderLine('Host')); + } + + public function testRequestOptionsAsteriskEvent() + { + $requestAssertion = null; + + $server = new Server($this->socket, function (RequestInterface $request) use (&$requestAssertion) { + $requestAssertion = $request; + return new Response(); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "OPTIONS * HTTP/1.1\r\nHost: example.com\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('OPTIONS', $requestAssertion->getMethod()); + $this->assertSame('*', $requestAssertion->getRequestTarget()); + $this->assertEquals('http://example.com', $requestAssertion->getUri()); + $this->assertSame('', $requestAssertion->getUri()->getPath()); + $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); + } + + public function testRequestOptionsAbsoluteEvent() + { + $requestAssertion = null; + + $server = new Server($this->socket, function (RequestInterface $request) use (&$requestAssertion) { + $requestAssertion = $request; + return new Response(); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "OPTIONS http://example.com HTTP/1.1\r\nHost: example.com\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('OPTIONS', $requestAssertion->getMethod()); + $this->assertSame('http://example.com', $requestAssertion->getRequestTarget()); + $this->assertEquals('http://example.com', $requestAssertion->getUri()); + $this->assertSame('', $requestAssertion->getUri()->getPath()); + $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); + } + public function testRequestPauseWillbeForwardedToConnection() { $server = new Server($this->socket, function (RequestInterface $request) {