Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use https-scheme for request URIs if secure TLS is used #167

Merged
merged 2 commits into from
Apr 16, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"require-dev": {
"phpunit/phpunit": "^4.8.10||^5.0",
"react/socket": "^0.7"
"react/socket": "^0.7",
"clue/block-react": "^1.1"
}
}
3 changes: 2 additions & 1 deletion src/RequestHeaderParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ private function parseRequest($data)
// detecting HTTPS is left up to the socket layer (TLS detection)
if ($request->getUri()->getScheme() === 'https') {
$request = $request->withUri(
$request->getUri()->withScheme('http')->withPort(443)
$request->getUri()->withScheme('http')->withPort(443),
true
);
}

Expand Down
38 changes: 38 additions & 0 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,21 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque
'[]'
);

// Update request URI to "https" scheme if the connection is encrypted
if ($this->isConnectionEncrypted($conn)) {
// The request URI may omit default ports here, so try to parse port
// from Host header field (if possible)
$port = $request->getUri()->getPort();
if ($port === null) {
$port = parse_url('tcp://' . $request->getHeaderLine('Host'), PHP_URL_PORT);
}

$request = $request->withUri(
$request->getUri()->withScheme('https')->withPort($port),
true
);
}

$callback = $this->callback;
$promise = new Promise(function ($resolve, $reject) use ($callback, $request) {
$resolve($callback($request));
Expand Down Expand Up @@ -384,4 +399,27 @@ private function handleResponseBody(ResponseInterface $response, ConnectionInter
$connection->write(Psr7Implementation\str($response));
$stream->pipe($connection);
}

/**
* @param ConnectionInterface $conn
* @return bool
* @codeCoverageIgnore
*/
private function isConnectionEncrypted(ConnectionInterface $conn)
{
// Legacy PHP < 7 does not offer any direct access to check crypto parameters
// We work around by accessing the context options and assume that only
// secure connections *SHOULD* set the "ssl" context options by default.
if (PHP_VERSION_ID < 70000) {
$context = isset($conn->stream) ? stream_context_get_options($conn->stream) : array();

return (isset($context['ssl']) && $context['ssl']);
}

// Modern PHP 7+ offers more reliable access to check crypto parameters
// by checking stream crypto meta data that is only then made available.
$meta = isset($conn->stream) ? stream_get_meta_data($conn->stream) : array();

return (isset($meta['crypto']) && $meta['crypto']);
}
}
204 changes: 204 additions & 0 deletions tests/FunctionalServerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
<?php

namespace React\Tests\Http;

use React\Socket\Server as Socket;
use React\EventLoop\Factory;
use React\Http\Server;
use Psr\Http\Message\RequestInterface;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
use React\Stream\BufferedSink;
use Clue\React\Block;
use React\Http\Response;
use React\Socket\SecureServer;

class FunctionServerTest extends TestCase
{
public function testPlainHttpOnRandomPort()
{
$loop = Factory::create();
$socket = new Socket(0, $loop);
$connector = new Connector($loop);

$server = new Server($socket, function (RequestInterface $request) {
return new Response(200, array(), (string)$request->getUri());
});

$result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) {
$conn->write("GET / HTTP/1.0\r\nHost: " . $conn->getRemoteAddress() . "\r\n\r\n");

return BufferedSink::createPromise($conn);
});

$response = Block\await($result, $loop, 1.0);

$this->assertContains("HTTP/1.0 200 OK", $response);
$this->assertContains('http://' . $socket->getAddress() . '/', $response);

$socket->close();
}

public function testSecureHttpsOnRandomPort()
{
if (!function_exists('stream_socket_enable_crypto')) {
$this->markTestSkipped('Not supported on your platform (outdated HHVM?)');
}

$loop = Factory::create();
$socket = new Socket(0, $loop);
$socket = new SecureServer($socket, $loop, array(
'local_cert' => __DIR__ . '/../examples/localhost.pem'
));
$connector = new Connector($loop, array(
'tls' => array('verify_peer' => false)
));

$server = new Server($socket, function (RequestInterface $request) {
return new Response(200, array(), (string)$request->getUri());
});

$result = $connector->connect('tls://' . $socket->getAddress())->then(function (ConnectionInterface $conn) {
$conn->write("GET / HTTP/1.0\r\nHost: " . $conn->getRemoteAddress() . "\r\n\r\n");

return BufferedSink::createPromise($conn);
});

$response = Block\await($result, $loop, 1.0);

$this->assertContains("HTTP/1.0 200 OK", $response);
$this->assertContains('https://' . $socket->getAddress() . '/', $response);

$socket->close();
}

public function testPlainHttpOnStandardPortReturnsUriWithNoPort()
{
$loop = Factory::create();
try {
$socket = new Socket(80, $loop);
} catch (\RuntimeException $e) {
$this->markTestSkipped('Listening on port 80 failed (root and unused?)');
}
$connector = new Connector($loop);

$server = new Server($socket, function (RequestInterface $request) {
return new Response(200, array(), (string)$request->getUri());
});

$result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) {
$conn->write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n");

return BufferedSink::createPromise($conn);
});

$response = Block\await($result, $loop, 1.0);

$this->assertContains("HTTP/1.0 200 OK", $response);
$this->assertContains('http://127.0.0.1/', $response);

$socket->close();
}

public function testSecureHttpsOnStandardPortReturnsUriWithNoPort()
{
if (!function_exists('stream_socket_enable_crypto')) {
$this->markTestSkipped('Not supported on your platform (outdated HHVM?)');
}

$loop = Factory::create();
try {
$socket = new Socket(443, $loop);
} catch (\RuntimeException $e) {
$this->markTestSkipped('Listening on port 443 failed (root and unused?)');
}
$socket = new SecureServer($socket, $loop, array(
'local_cert' => __DIR__ . '/../examples/localhost.pem'
));
$connector = new Connector($loop, array(
'tls' => array('verify_peer' => false)
));

$server = new Server($socket, function (RequestInterface $request) {
return new Response(200, array(), (string)$request->getUri());
});

$result = $connector->connect('tls://' . $socket->getAddress())->then(function (ConnectionInterface $conn) {
$conn->write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n");

return BufferedSink::createPromise($conn);
});

$response = Block\await($result, $loop, 1.0);

$this->assertContains("HTTP/1.0 200 OK", $response);
$this->assertContains('https://127.0.0.1/', $response);

$socket->close();
}

public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort()
{
$loop = Factory::create();
try {
$socket = new Socket(443, $loop);
} catch (\RuntimeException $e) {
$this->markTestSkipped('Listening on port 443 failed (root and unused?)');
}
$connector = new Connector($loop);

$server = new Server($socket, function (RequestInterface $request) {
return new Response(200, array(), (string)$request->getUri());
});

$result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) {
$conn->write("GET / HTTP/1.0\r\nHost: " . $conn->getRemoteAddress() . "\r\n\r\n");

return BufferedSink::createPromise($conn);
});

$response = Block\await($result, $loop, 1.0);

$this->assertContains("HTTP/1.0 200 OK", $response);
$this->assertContains('http://127.0.0.1:443/', $response);

$socket->close();
}

public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort()
{
if (!function_exists('stream_socket_enable_crypto')) {
$this->markTestSkipped('Not supported on your platform (outdated HHVM?)');
}

$loop = Factory::create();
try {
$socket = new Socket(80, $loop);
} catch (\RuntimeException $e) {
$this->markTestSkipped('Listening on port 80 failed (root and unused?)');
}
$socket = new SecureServer($socket, $loop, array(
'local_cert' => __DIR__ . '/../examples/localhost.pem'
));
$connector = new Connector($loop, array(
'tls' => array('verify_peer' => false)
));

$server = new Server($socket, function (RequestInterface $request) {
return new Response(200, array(), (string)$request->getUri() . 'x' . $request->getHeaderLine('Host'));
});

$result = $connector->connect('tls://' . $socket->getAddress())->then(function (ConnectionInterface $conn) {
$conn->write("GET / HTTP/1.0\r\nHost: " . $conn->getRemoteAddress() . "\r\n\r\n");

return BufferedSink::createPromise($conn);
});

$response = Block\await($result, $loop, 1.0);

$this->assertContains("HTTP/1.0 200 OK", $response);
$this->assertContains('https://127.0.0.1:80/', $response);

$socket->close();
}
}