Skip to content

Commit

Permalink
Validate proxy requests in absolute-form
Browse files Browse the repository at this point in the history
  • Loading branch information
clue committed Apr 19, 2017
1 parent 8524de4 commit 708b143
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 2 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
45 changes: 45 additions & 0 deletions examples/21-http-proxy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

use React\EventLoop\Factory;
use React\Socket\Server;
use React\Http\Response;
use Psr\Http\Message\RequestInterface;
use RingCentral\Psr7;

require __DIR__ . '/../vendor/autoload.php';

$loop = Factory::create();
$socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop);

$server = new \React\Http\Server($socket, function (RequestInterface $request) {
if (strpos($request->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();
File renamed without changes.
10 changes: 10 additions & 0 deletions src/RequestHeaderParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
32 changes: 32 additions & 0 deletions tests/RequestHeaderParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
115 changes: 115 additions & 0 deletions tests/ServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit 708b143

Please sign in to comment.