From c2b68d7b495d331a2f0c6de963bd06f462472984 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Fri, 31 Mar 2017 14:55:30 +0200 Subject: [PATCH 1/4] Add ServerRequest --- src/RequestHeaderParser.php | 12 +++ src/Server.php | 33 +++++-- src/ServerRequest.php | 138 ++++++++++++++++++++++++++ tests/ServerRequestTest.php | 164 +++++++++++++++++++++++++++++++ tests/ServerTest.php | 186 ++++++++++++++++++++++++++++++++++-- 5 files changed, 517 insertions(+), 16 deletions(-) create mode 100644 src/ServerRequest.php create mode 100644 tests/ServerRequestTest.php diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 792a0604..9afdce62 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -72,6 +72,18 @@ private function parseRequest($data) } $request = g7\parse_request($headers); + $request = new ServerRequest( + $request->getMethod(), + $request->getUri(), + $request->getHeaders(), + $request->getBody(), + $request->getProtocolVersion() + ); + + $cookies = ServerRequest::parseCookie($request->getHeaderLine('Cookie')); + if ($cookies !== false) { + $request = $request->withCookieParams($cookies); + } // Do not assume this is HTTPS when this happens to be port 443 // detecting HTTPS is left up to the socket layer (TLS detection) diff --git a/src/Server.php b/src/Server.php index 4cb6d6ea..e67dc9c0 100644 --- a/src/Server.php +++ b/src/Server.php @@ -5,7 +5,6 @@ use Evenement\EventEmitter; use React\Socket\ServerInterface as SocketServerInterface; use React\Socket\ConnectionInterface; -use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use React\Promise\Promise; use RingCentral\Psr7 as Psr7Implementation; @@ -146,7 +145,7 @@ public function handleConnection(ConnectionInterface $conn) $that = $this; $parser = new RequestHeaderParser(); $listener = array($parser, 'feed'); - $parser->on('headers', function (RequestInterface $request, $bodyBuffer) use ($conn, $listener, $parser, $that) { + $parser->on('headers', function (ServerRequest $request, $bodyBuffer) use ($conn, $listener, $parser, $that) { // parsing request completed => stop feeding parser $conn->removeListener('data', $listener); @@ -170,7 +169,7 @@ public function handleConnection(ConnectionInterface $conn) } /** @internal */ - public function handleRequest(ConnectionInterface $conn, RequestInterface $request) + public function handleRequest(ConnectionInterface $conn, ServerRequest $request) { // only support HTTP/1.1 and HTTP/1.0 requests if ($request->getProtocolVersion() !== '1.1' && $request->getProtocolVersion() !== '1.0') { @@ -248,12 +247,26 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque $conn->write("HTTP/1.1 100 Continue\r\n\r\n"); } - // attach remote ip to the request as metadata - $request->remoteAddress = trim( - parse_url('tcp://' . $conn->getRemoteAddress(), PHP_URL_HOST), - '[]' + $serverParams = array( + 'request_time' => time(), + 'request_time_float' => microtime(true), + 'https' => $this->isConnectionEncrypted($conn) ? 'on' : null ); + if ($conn->getRemoteAddress() !== null) { + $remoteAddress = parse_url('tcp://' . $conn->getRemoteAddress()); + $serverParams['remote_address'] = $remoteAddress['host']; + $serverParams['remote_port'] = $remoteAddress['port']; + } + + if ($conn->getLocalAddress() !== null) { + $localAddress = parse_url('tcp://' . $conn->getLocalAddress()); + $serverParams['server_address'] = $localAddress['host']; + $serverParams['server_port'] = $localAddress['port']; + } + + $request = $request->withServerParams($serverParams); + // 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 @@ -306,7 +319,7 @@ function ($error) use ($that, $conn, $request) { } /** @internal */ - public function writeError(ConnectionInterface $conn, $code, RequestInterface $request = null) + public function writeError(ConnectionInterface $conn, $code, ServerRequest $request = null) { $message = 'Error ' . $code; if (isset(ResponseCodes::$statusTexts[$code])) { @@ -322,7 +335,7 @@ public function writeError(ConnectionInterface $conn, $code, RequestInterface $r ); if ($request === null) { - $request = new Psr7Implementation\Request('GET', '/', array(), null, '1.1'); + $request = new ServerRequest('GET', '/', array(), null, '1.1'); } $this->handleResponse($conn, $request, $response); @@ -330,7 +343,7 @@ public function writeError(ConnectionInterface $conn, $code, RequestInterface $r /** @internal */ - public function handleResponse(ConnectionInterface $connection, RequestInterface $request, ResponseInterface $response) + public function handleResponse(ConnectionInterface $connection, ServerRequest $request, ResponseInterface $response) { $response = $response->withProtocolVersion($request->getProtocolVersion()); diff --git a/src/ServerRequest.php b/src/ServerRequest.php new file mode 100644 index 00000000..2c3412d6 --- /dev/null +++ b/src/ServerRequest.php @@ -0,0 +1,138 @@ +serverParams; + } + + public function getCookieParams() + { + return $this->cookies; + } + + public function withCookieParams(array $cookies) + { + $new = clone $this; + $new->cookies = $cookies; + return $new; + } + + public function getQueryParams() + { + return $this->queryParams; + } + + public function withQueryParams(array $query) + { + $new = clone $this; + $new->queryParams = $query; + return $new; + } + + public function getUploadedFiles() + { + return $this->fileParams; + } + + public function withUploadedFiles(array $uploadedFiles) + { + $new = clone $this; + $new->fileParams = $uploadedFiles; + return $new; + } + + public function getParsedBody() + { + return $this->parsedBody; + } + + public function withParsedBody($data) + { + $new = clone $this; + $new->parsedBody = $data; + return $new; + } + + public function getAttributes() + { + return $this->attributes; + } + + public function getAttribute($name, $default = null) + { + if (!array_key_exists($name, $this->attributes)) { + return $default; + } + return $this->attributes[$name]; + } + + public function withAttribute($name, $value) + { + $new = clone $this; + $new->attributes[$name] = $value; + return $new; + } + + public function withoutAttribute($name) + { + $new = clone $this; + unset($new->attributes[$name]); + return $new; + } + + /** @internal + * Used only internal set the retrospective server params + **/ + public function withServerParams(array $serverParams) + { + $new = clone $this; + $new->serverParams= $serverParams; + return $new; + } + + /** + * @internal + * @param string $cookie + * @return boolean|mixed[] + */ + public static function parseCookie($cookie) + { + // PSR-7 `getHeadline('Cookies')` will return multiple + // cookie header coma-seperated. Multiple cookie headers + // are not allowed according to https://tools.ietf.org/html/rfc6265#section-5.4 + if (strpos($cookie, ',') !== false) { + return false; + } + + $cookieArray = explode(';', $cookie); + + $result = array(); + foreach ($cookieArray as $pair) { + $nameValuePair = explode('=', $pair, 2); + if (count($nameValuePair) === 2) { + $key = urldecode($nameValuePair[0]); + $value = urldecode($nameValuePair[1]); + + $result[$key] = $value; + } + } + + return $result; + } +} diff --git a/tests/ServerRequestTest.php b/tests/ServerRequestTest.php new file mode 100644 index 00000000..33632b8a --- /dev/null +++ b/tests/ServerRequestTest.php @@ -0,0 +1,164 @@ +request = new ServerRequest('GET', 'http://localhost'); + } + + public function testGetNoAttributes() + { + $this->assertEquals(array(), $this->request->getAttributes()); + } + + public function testWithAttribute() + { + $request = $this->request->withAttribute('hello', 'world'); + + $this->assertNotSame($request, $this->request); + $this->assertEquals(array('hello' => 'world'), $request->getAttributes()); + } + + public function testGetAttribute() + { + $request = $this->request->withAttribute('hello', 'world'); + + $this->assertNotSame($request, $this->request); + $this->assertEquals('world', $request->getAttribute('hello')); + } + + public function testGetDefaultAttribute() + { + $request = $this->request->withAttribute('hello', 'world'); + + $this->assertNotSame($request, $this->request); + $this->assertEquals(null, $request->getAttribute('hi', null)); + } + + public function testWithoutAttribute() + { + $request = $this->request->withAttribute('hello', 'world'); + $request = $request->withAttribute('test', 'nice'); + + $request = $request->withoutAttribute('hello'); + + $this->assertNotSame($request, $this->request); + $this->assertEquals(array('test' => 'nice'), $request->getAttributes()); + } + + public function testWithCookieParams() + { + $request = $this->request->withCookieParams(array('test' => 'world')); + + $this->assertNotSame($request, $this->request); + $this->assertEquals(array('test' => 'world'), $request->getCookieParams()); + } + + public function testWithQueryParams() + { + $request = $this->request->withQueryParams(array('test' => 'world')); + + $this->assertNotSame($request, $this->request); + $this->assertEquals(array('test' => 'world'), $request->getQueryParams()); + } + + public function testWithUploadedFiles() + { + $request = $this->request->withUploadedFiles(array('test' => 'world')); + + $this->assertNotSame($request, $this->request); + $this->assertEquals(array('test' => 'world'), $request->getUploadedFiles()); + } + + public function testWithParsedBody() + { + $request = $this->request->withParsedBody(array('test' => 'world')); + + $this->assertNotSame($request, $this->request); + $this->assertEquals(array('test' => 'world'), $request->getParsedBody()); + } + + public function testParseSingleCookieNameValuePairWillReturnValidArray() + { + $cookieString = 'hello=world'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(array('hello' => 'world'), $cookies); + } + + public function testParseMultipleCookieNameValuePaiWillReturnValidArray() + { + $cookieString = 'hello=world;test=abc'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(array('hello' => 'world', 'test' => 'abc'), $cookies); + } + + public function testParseMultipleCookieNameValuePairWillReturnFalse() + { + // Could be done through multiple 'Cookie' headers + // getHeaderLine('Cookie') will return a value seperated by coma + // e.g. + // GET / HTTP/1.1\r\n + // Host: test.org\r\n + // Cookie: hello=world\r\n + // Cookie: test=abc\r\n\r\n + $cookieString = 'hello=world,test=abc'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(false, $cookies); + } + + public function testOnlyFirstSetWillBeAddedToCookiesArray() + { + $cookieString = 'hello=world;hello=abc'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(array('hello' => 'abc'), $cookies); + } + + public function testOtherEqualSignsWillBeAddedToValueAndWillReturnValidArray() + { + $cookieString = 'hello=world=test=php'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(array('hello' => 'world=test=php'), $cookies); + } + + public function testSingleCookieValueInCookiesReturnsEmptyArray() + { + $cookieString = 'world'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(array(), $cookies); + } + + public function testSingleMutlipleCookieValuesReturnsEmptyArray() + { + $cookieString = 'world;test'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(array(), $cookies); + } + + public function testSingleValueIsValidInMultipleValueCookieWillReturnValidArray() + { + $cookieString = 'world;test=php'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(array('test' => 'php'), $cookies); + } + + public function testUrlEncodingForValueWillReturnValidArray() + { + $cookieString = 'hello=world%21;test=100%25%20coverage'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(array('hello' => 'world!', 'test' => '100% coverage'), $cookies); + } + + public function testUrlEncodingForKeyWillReturnValidArray() + { + $cookieString = 'react%3Bphp=is%20great'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(array('react;php' => 'is great'), $cookies); + } +} diff --git a/tests/ServerTest.php b/tests/ServerTest.php index b7ddac2f..ed763fe6 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -7,6 +7,7 @@ use React\Http\Response; use React\Stream\ReadableStream; use React\Promise\Promise; +use Psr\Http\Message\ServerRequestInterface; class ServerTest extends TestCase { @@ -73,9 +74,9 @@ public function testRequestEvent() }); $this->connection - ->expects($this->once()) + ->expects($this->any()) ->method('getRemoteAddress') - ->willReturn('127.0.0.1'); + ->willReturn('127.0.0.1:8080'); $this->socket->emit('connection', array($this->connection)); @@ -85,11 +86,13 @@ public function testRequestEvent() $this->assertSame(1, $i); $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); + + $serverParams = $requestAssertion->getServerParams(); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); $this->assertSame('http://example.com/', (string)$requestAssertion->getUri()); $this->assertSame('example.com:80', $requestAssertion->getHeaderLine('Host')); - $this->assertSame('127.0.0.1', $requestAssertion->remoteAddress); + $this->assertSame('127.0.0.1', $serverParams['remote_address']); } public function testRequestGetWithHostAndCustomPort() @@ -2219,16 +2222,187 @@ function ($data) use (&$buffer) { $this->socket->emit('connection', array($this->connection)); - $data = "GET / HTTP/1.0\r\n\r\n"; - $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); $this->assertInstanceOf('RuntimeException', $exception); } + public function testCookieWillBeAddedToServerRequest() + { + $requestValidation = null; + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestValidation) { + $requestValidation = $request; + return new Response(); + }); + + $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 .= "Cookie: hello=world\r\n"; + $data .= "\r\n"; + + $this->connection->emit('data', array($data)); + + $this->assertEquals(array('hello' => 'world'), $requestValidation->getCookieParams()); + } + + public function testMultipleCookiesWontBeAddedToServerRequest() + { + $requestValidation = null; + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestValidation) { + $requestValidation = $request; + return new Response(); + }); + + $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 .= "Cookie: hello=world\r\n"; + $data .= "Cookie: test=failed\r\n"; + $data .= "\r\n"; + + $this->connection->emit('data', array($data)); + + $this->assertEquals(array(), $requestValidation->getCookieParams()); + } + + public function testCookieWithSepeartorWillBeAddedToServerRequest() + { + $requestValidation = null; + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestValidation) { + $requestValidation = $request; + return new Response(); + }); + + $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 .= "Cookie: hello=world;test=abc\r\n"; + $data .= "\r\n"; + + $this->connection->emit('data', array($data)); + + $this->assertEquals(array('hello' => 'world', 'test' => 'abc'), $requestValidation->getCookieParams()); + } + + public function testServerRequestParams() + { + $requestValidation = null; + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestValidation) { + $requestValidation = $request; + return new Response(); + }); + + $this->connection + ->expects($this->any()) + ->method('getRemoteAddress') + ->willReturn('192.168.1.2:80'); + + $this->connection + ->expects($this->any()) + ->method('getLocalAddress') + ->willReturn('127.0.0.1:8080'); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + + $serverParams = $requestValidation->getServerParams(); + + $this->assertEquals('127.0.0.1', $serverParams['server_address']); + $this->assertEquals('8080', $serverParams['server_port']); + + $this->assertEquals('192.168.1.2', $serverParams['remote_address']); + $this->assertEquals('80', $serverParams['remote_port']); + + $this->assertNotNull($serverParams['request_time']); + $this->assertNotNull($serverParams['request_time_float']); + } + + public function testServerRequestParameterRemoteAddressNotAvailableWontAddParameter() + { + $requestValidation = null; + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestValidation) { + $requestValidation = $request; + return new Response(); + }); + + $this->connection + ->expects($this->any()) + ->method('getRemoteAddress') + ->willReturn(null); + + $this->connection + ->expects($this->any()) + ->method('getLocalAddress') + ->willReturn('127.0.0.1:8080'); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + + $serverParams = $requestValidation->getServerParams(); + + $this->assertArrayNotHasKey('remote_address', $serverParams); + $this->assertArrayNotHasKey('remote_port', $serverParams); + + $this->assertEquals('127.0.0.1', $serverParams['server_address']); + $this->assertEquals('8080', $serverParams['server_port']); + + $this->assertNotNull($serverParams['request_time']); + $this->assertNotNull($serverParams['request_time_float']); + + $this->assertEquals(null, $serverParams['https']); + } + + public function testServerRequestParameterServerAddressNotAvailableWontAddParameter() + { + $requestValidation = null; + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestValidation) { + $requestValidation = $request; + return new Response(); + }); + + $this->connection + ->expects($this->any()) + ->method('getRemoteAddress') + ->willReturn('192.168.1.2:80'); + + $this->connection + ->expects($this->any()) + ->method('getLocalAddress') + ->willReturn(null); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + + $serverParams = $requestValidation->getServerParams(); + + $this->assertArrayNotHasKey('server_address', $serverParams); + $this->assertArrayNotHasKey('server_port', $serverParams); + + $this->assertEquals('192.168.1.2', $serverParams['remote_address']); + $this->assertEquals('80', $serverParams['remote_port']); + + $this->assertNotNull($serverParams['request_time']); + $this->assertNotNull($serverParams['request_time_float']); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From 5f13ef26f0abf3c7e7cb1374abb5f1533c01157f Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Fri, 31 Mar 2017 15:02:45 +0200 Subject: [PATCH 2/4] Update examples --- examples/01-hello-world.php | 5 ++--- examples/02-count-visitors.php | 4 ++-- examples/03-stream-response.php | 4 ++-- examples/04-stream-request.php | 4 ++-- examples/05-error-handling.php | 4 ++-- examples/11-hello-world-https.php | 4 ++-- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/examples/01-hello-world.php b/examples/01-hello-world.php index ed84af11..cf047944 100644 --- a/examples/01-hello-world.php +++ b/examples/01-hello-world.php @@ -3,15 +3,14 @@ use React\EventLoop\Factory; use React\Socket\Server; use React\Http\Response; -use Psr\Http\Message\RequestInterface; -use React\Promise\Promise; +use Psr\Http\Message\ServerRequestInterface; 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) { +$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) { return new Response( 200, array( diff --git a/examples/02-count-visitors.php b/examples/02-count-visitors.php index 2c384a3b..9f69f797 100644 --- a/examples/02-count-visitors.php +++ b/examples/02-count-visitors.php @@ -3,7 +3,7 @@ use React\EventLoop\Factory; use React\Socket\Server; use React\Http\Response; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; require __DIR__ . '/../vendor/autoload.php'; @@ -11,7 +11,7 @@ $socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); $counter = 0; -$server = new \React\Http\Server($socket, function (RequestInterface $request) use (&$counter) { +$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) use (&$counter) { return new Response( 200, array('Content-Type' => 'text/plain'), diff --git a/examples/03-stream-response.php b/examples/03-stream-response.php index 5fd990e8..d6700017 100644 --- a/examples/03-stream-response.php +++ b/examples/03-stream-response.php @@ -3,15 +3,15 @@ use React\EventLoop\Factory; use React\Socket\Server; use React\Http\Response; -use Psr\Http\Message\RequestInterface; use React\Stream\ReadableStream; +use Psr\Http\Message\ServerRequestInterface; 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) use ($loop) { +$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) use ($loop) { $stream = new ReadableStream(); $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { diff --git a/examples/04-stream-request.php b/examples/04-stream-request.php index 481162ef..a9ff4e18 100644 --- a/examples/04-stream-request.php +++ b/examples/04-stream-request.php @@ -3,15 +3,15 @@ use React\EventLoop\Factory; use React\Socket\Server; use React\Http\Response; -use Psr\Http\Message\RequestInterface; use React\Promise\Promise; +use Psr\Http\Message\ServerRequestInterface; 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) { +$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) use ($request) { $contentLength = 0; $request->getBody()->on('data', function ($data) use (&$contentLength) { diff --git a/examples/05-error-handling.php b/examples/05-error-handling.php index 29f54b92..f4b95397 100644 --- a/examples/05-error-handling.php +++ b/examples/05-error-handling.php @@ -3,8 +3,8 @@ use React\EventLoop\Factory; use React\Socket\Server; use React\Http\Response; -use Psr\Http\Message\RequestInterface; use React\Promise\Promise; +use Psr\Http\Message\ServerRequestInterface; require __DIR__ . '/../vendor/autoload.php'; @@ -12,7 +12,7 @@ $socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); $count = 0; -$server = new \React\Http\Server($socket, function (RequestInterface $request) use (&$count) { +$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) use (&$count) { return new Promise(function ($resolve, $reject) use (&$count) { $count++; diff --git a/examples/11-hello-world-https.php b/examples/11-hello-world-https.php index 191e7d62..de958007 100644 --- a/examples/11-hello-world-https.php +++ b/examples/11-hello-world-https.php @@ -4,7 +4,7 @@ use React\Socket\Server; use React\Http\Response; use React\Socket\SecureServer; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; require __DIR__ . '/../vendor/autoload.php'; @@ -14,7 +14,7 @@ 'local_cert' => isset($argv[2]) ? $argv[2] : __DIR__ . '/localhost.pem' )); -$server = new \React\Http\Server($socket, function (RequestInterface $request) { +$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) { return new Response( 200, array('Content-Type' => 'text/plain'), From 16d0654cf201525a67f1b9b27af8f76a6c2d9ad9 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Fri, 31 Mar 2017 15:02:26 +0200 Subject: [PATCH 3/4] Update README --- README.md | 102 +++++++++++++++--- examples/02-client-ip.php | 27 +++++ ...unt-visitors.php => 03-count-visitors.php} | 0 ...am-response.php => 04-stream-response.php} | 0 ...ream-request.php => 05-stream-request.php} | 0 ...ror-handling.php => 06-error-handling.php} | 0 examples/07-cookies.php | 38 +++++++ src/Server.php | 41 +++++-- 8 files changed, 183 insertions(+), 25 deletions(-) create mode 100644 examples/02-client-ip.php rename examples/{02-count-visitors.php => 03-count-visitors.php} (100%) rename examples/{03-stream-response.php => 04-stream-response.php} (100%) rename examples/{04-stream-request.php => 05-stream-request.php} (100%) rename examples/{05-error-handling.php => 06-error-handling.php} (100%) create mode 100644 examples/07-cookies.php diff --git a/README.md b/README.md index 1006559f..85984a5e 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ This is an HTTP server which responds with `Hello World` to every request. $loop = React\EventLoop\Factory::create(); $socket = new React\Socket\Server(8080, $loop); -$http = new Server($socket, function (RequestInterface $request) { +$http = new Server($socket, function (ServerRequestInterface $request) { return new Response( 200, array('Content-Type' => 'text/plain'), @@ -54,7 +54,7 @@ constructor with the respective [request](#request) and ```php $socket = new React\Socket\Server(8080, $loop); -$http = new Server($socket, function (RequestInterface $request) { +$http = new Server($socket, function (ServerRequestInterface $request) { return new Response( 200, array('Content-Type' => 'text/plain'), @@ -75,7 +75,7 @@ $socket = new React\Socket\SecureServer($socket, $loop, array( 'local_cert' => __DIR__ . '/localhost.pem' )); -$http = new Server($socket, function (RequestInterface $request) { +$http = new Server($socket, function (ServerRequestInterface $request) { return new Response( 200, array('Content-Type' => 'text/plain'), @@ -137,11 +137,13 @@ connections and then processing each incoming HTTP request. The request object will be processed once the request headers have been received by the client. This request object implements the +[PSR-7 ServerRequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#321-psrhttpmessageserverrequestinterface) +which in turn extends the [PSR-7 RequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#32-psrhttpmessagerequestinterface) and will be passed to the callback function like this. - ```php -$http = new Server($socket, function (RequestInterface $request) { +```php +$http = new Server($socket, function (ServerRequestInterface $request) { $body = "The method of the request is: " . $request->getMethod(); $body .= "The requested path is: " . $request->getUri()->getPath(); @@ -153,8 +155,43 @@ $http = new Server($socket, function (RequestInterface $request) { }); ``` +The `Server` will currently add server-side parameters, analog to the `$_SERVER` variable: + +* `server_address` + The current IP address of the server +* `server_port` + The current port of the server +* `remote_address` + The IP address of the request sender +* `remote_port` + Port of the request sender +* `request_time` + Unix timestamp of the moment, the complete request header was received +* `request_time_float` + Unix timestamp in microseconds of the moment, the complete request header was + received +* `https` + Set to 'on' if the request used HTTPS, otherwise it will be set to null + +The `server` and `remote` parameters MAY missing if the client or server unexpected +closes the connection. + +```php +$http = new Server($socket, function (ServerRequestInterface $request) { + $body = "Your IP is: " . $request->getServerParams()['remote_address']; + + return new Response( + 200, + array('Content-Type' => 'text/plain'), + $body + ); +}); +``` + For more details about the request object, check out the documentation of [PSR-7 RequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#32-psrhttpmessagerequestinterface). +and +[PSR-7 ServerRequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#321-psrhttpmessageserverrequestinterface) Note that the request object will be processed once the request headers have been received. @@ -184,7 +221,7 @@ Instead, you should use the `ReactPHP ReadableStreamInterface` which gives you access to the incoming request body as the individual chunks arrive: ```php -$http = new Server($socket, function (RequestInterface $request) { +$http = new Server($socket, function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) use ($request) { $contentLength = 0; $request->getBody()->on('data', function ($data) use (&$contentLength) { @@ -248,7 +285,7 @@ Note that this value may be `null` if the request body size is unknown in advance because the request message uses chunked transfer encoding. ```php -$http = new Server($socket, function (RequestInterface $request) { +$http = new Server($socket, function (ServerRequestInterface $request) { $size = $request->getBody()->getSize(); if ($size === null) { $body = 'The request does not contain an explicit length.'; @@ -290,6 +327,41 @@ Allowed). can in fact use a streaming response body for the tunneled application data. See also [example #21](examples) for more details. +The cookies of the request, will also be added to the request object by the `Server`: + +```php +$http = new Server($socket, function (ServerRequestInterface $request) { + $key = 'react\php'; + + if (isset($request->getCookieParams()[$key])) { + $body = "Your cookie value is: " . $request->getCookieParams()[$key]; + + return new Response( + 200, + array('Content-Type' => 'text/plain'), + $body + ); + } + + return new Response( + 200, + array( + 'Content-Type' => 'text/plain', + 'Set-Cookie' => urlencode($key) . '=' . urlencode('test;more') + ), + "Your cookie has been set." + ); +}); +``` + +The above example should be executed via a web browser, a cookie will be set +on the first request contation the key-value pair 'test=php'. +On a second request to the server the cookie is accessed by the `getCookieParams()` +method, and will be written into the body. +The example uses the `urlencode()` function to encode non-alphanumeric characters. +This encoding is also used internally when decoding the name and value of cookies +(which is in line with other implementations, such as PHP's cookie functions). + ### Response The callback function passed to the constructor of the [Server](#server) @@ -308,7 +380,7 @@ but feel free to use any implemantation of the `PSR-7 ResponseInterface` you prefer. ```php -$http = new Server($socket, function (RequestInterface $request) { +$http = new Server($socket, function (ServerRequestInterface $request) { return new Response( 200, array('Content-Type' => 'text/plain'), @@ -327,7 +399,7 @@ To prevent this you SHOULD use a This example shows how such a long-term action could look like: ```php -$server = new \React\Http\Server($socket, function (RequestInterface $request) use ($loop) { +$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) use ($loop) { return new Promise(function ($resolve, $reject) use ($request, $loop) { $loop->addTimer(1.5, function() use ($loop, $resolve) { $response = new Response( @@ -355,7 +427,7 @@ Note that other implementations of the `PSR-7 ResponseInterface` likely only support string. ```php -$server = new Server($socket, function (RequestInterface $request) use ($loop) { +$server = new Server($socket, function (ServerRequestInterface $request) use ($loop) { $stream = new ReadableStream(); $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { @@ -389,7 +461,7 @@ If you know the length of your stream body, you MAY specify it like this instead ```php $stream = new ReadableStream() -$server = new Server($socket, function (RequestInterface $request) use ($loop, $stream) { +$server = new Server($socket, function (ServerRequestInterface $request) use ($loop, $stream) { return new Response( 200, array( @@ -437,7 +509,7 @@ A `Date` header will be automatically added with the system date and time if non You can add a custom `Date` header yourself like this: ```php -$server = new Server($socket, function (RequestInterface $request) { +$server = new Server($socket, function (ServerRequestInterface $request) { return new Response(200, array('Date' => date('D, d M Y H:i:s T'))); }); ``` @@ -446,7 +518,7 @@ If you don't have a appropriate clock to rely on, you should unset this header with an empty string: ```php -$server = new Server($socket, function (RequestInterface $request) { +$server = new Server($socket, function (ServerRequestInterface $request) { return new Response(200, array('Date' => '')); }); ``` @@ -455,7 +527,7 @@ Note that it will automatically assume a `X-Powered-By: react/alpha` header unless your specify a custom `X-Powered-By` header yourself: ```php -$server = new Server($socket, function (RequestInterface $request) { +$server = new Server($socket, function (ServerRequestInterface $request) { return new Response(200, array('X-Powered-By' => 'PHP 3')); }); ``` @@ -464,7 +536,7 @@ If you do not want to send this header at all, you can use an empty string as value like this: ```php -$server = new Server($socket, function (RequestInterface $request) { +$server = new Server($socket, function (ServerRequestInterface $request) { return new Response(200, array('X-Powered-By' => '')); }); ``` diff --git a/examples/02-client-ip.php b/examples/02-client-ip.php new file mode 100644 index 00000000..131e5680 --- /dev/null +++ b/examples/02-client-ip.php @@ -0,0 +1,27 @@ +getServerParams()['remote_address']; + + return new Response( + 200, + array('Content-Type' => 'text/plain'), + $body + ); +}); + +echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; + +$loop->run(); + + diff --git a/examples/02-count-visitors.php b/examples/03-count-visitors.php similarity index 100% rename from examples/02-count-visitors.php rename to examples/03-count-visitors.php diff --git a/examples/03-stream-response.php b/examples/04-stream-response.php similarity index 100% rename from examples/03-stream-response.php rename to examples/04-stream-response.php diff --git a/examples/04-stream-request.php b/examples/05-stream-request.php similarity index 100% rename from examples/04-stream-request.php rename to examples/05-stream-request.php diff --git a/examples/05-error-handling.php b/examples/06-error-handling.php similarity index 100% rename from examples/05-error-handling.php rename to examples/06-error-handling.php diff --git a/examples/07-cookies.php b/examples/07-cookies.php new file mode 100644 index 00000000..67e008bb --- /dev/null +++ b/examples/07-cookies.php @@ -0,0 +1,38 @@ +getCookieParams()[$key])) { + $body = "Your cookie value is: " . $request->getCookieParams()[$key]; + + return new Response( + 200, + array('Content-Type' => 'text/plain'), + $body + ); + } + + return new Response( + 200, + array( + 'Content-Type' => 'text/plain', + 'Set-Cookie' => urlencode($key) . '=' . urlencode('test;more') + ), + "Your cookie has been set." + ); +}); + +echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; + +$loop->run(); diff --git a/src/Server.php b/src/Server.php index e67dc9c0..8565ba64 100644 --- a/src/Server.php +++ b/src/Server.php @@ -24,7 +24,7 @@ * ```php * $socket = new React\Socket\Server(8080, $loop); * - * $http = new Server($socket, function (RequestInterface $request) { + * $http = new Server($socket, function (ServerRequestInterface $request) { * return new Response( * 200, * array('Content-Type' => 'text/plain'), @@ -45,7 +45,7 @@ * 'local_cert' => __DIR__ . '/localhost.pem' * )); * - * $http = new Server($socket, function (RequestInterface $request) { + * $http = new Server($socket, function (ServerRequestInterface $request) { * return new Response( * 200, * array('Content-Type' => 'text/plain'), @@ -80,6 +80,22 @@ * }); * ``` * + * The server will also emit an `error` event if you return an invalid + * type in the callback function or have a unhandled `Exception`. + * If your callback function throws an exception, + * the `Server` will emit a `RuntimeException` and add the thrown exception + * as previous: + * + * ```php + * $http->on('error', function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * if ($e->getPrevious() !== null) { + * $previousException = $e->getPrevious(); + * echo $previousException->getMessage() . PHP_EOL; + * } + * }); + * ``` + * * Note that the request object can also emit an error. * Check out [request](#request) for more details. * @@ -98,15 +114,17 @@ class Server extends EventEmitter * as HTTP. * * For each request, it executes the callback function passed to the - * constructor with the respective [`Request`](#request) and - * [`Response`](#response) objects: + * constructor with the respective [`request`](#request) object: * * ```php * $socket = new React\Socket\Server(8080, $loop); * - * $http = new Server($socket, function (Request $request, Response $response) { - * $response->writeHead(200, array('Content-Type' => 'text/plain')); - * $response->end("Hello World!\n"); + * $http = new Server($socket, function (ServerRequestInterface $request) { + * return new Response( + * 200, + * array('Content-Type' => 'text/plain'), + * "Hello World!\n" + * ); * }); * ``` * @@ -120,9 +138,12 @@ class Server extends EventEmitter * 'local_cert' => __DIR__ . '/localhost.pem' * )); * - * $http = new Server($socket, function (Request $request, Response $response) { - * $response->writeHead(200, array('Content-Type' => 'text/plain')); - * $response->end("Hello World!\n"); + * $http = new Server($socket, function (ServerRequestInterface $request $request) { + * return new Response( + * 200, + * array('Content-Type' => 'text/plain'), + * "Hello World!\n" + * ); * }); *``` * From 4d781d459f231d8674c902ba7e9c1a10f3415432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 19 Apr 2017 01:16:46 +0200 Subject: [PATCH 4/4] Update README.md --- README.md | 102 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 53 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 85984a5e..56abef64 100644 --- a/README.md +++ b/README.md @@ -155,26 +155,26 @@ $http = new Server($socket, function (ServerRequestInterface $request) { }); ``` -The `Server` will currently add server-side parameters, analog to the `$_SERVER` variable: +The `getServerParams(): mixed[]` method can be used to +get server-side parameters similar to the `$_SERVER` variable. +The following parameters are currently available: -* `server_address` - The current IP address of the server -* `server_port` - The current port of the server * `remote_address` The IP address of the request sender * `remote_port` Port of the request sender +* `server_address` + The IP address of the server +* `server_port` + The port of the server * `request_time` - Unix timestamp of the moment, the complete request header was received + Unix timestamp when the complete request header has been received, + as integer similar to `time()` * `request_time_float` - Unix timestamp in microseconds of the moment, the complete request header was - received + Unix timestamp when the complete request header has been received, + as float similar to `microtime(true)` * `https` - Set to 'on' if the request used HTTPS, otherwise it will be set to null - -The `server` and `remote` parameters MAY missing if the client or server unexpected -closes the connection. + Set to 'on' if the request used HTTPS, otherwise it will be set to `null` ```php $http = new Server($socket, function (ServerRequestInterface $request) { @@ -188,10 +188,49 @@ $http = new Server($socket, function (ServerRequestInterface $request) { }); ``` +See also [example #2](examples). + +The `getCookieParams(): string[]` method can be used to +get all cookies sent with the current request. + +```php +$http = new Server($socket, function (ServerRequestInterface $request) { + $key = 'react\php'; + + if (isset($request->getCookieParams()[$key])) { + $body = "Your cookie value is: " . $request->getCookieParams()[$key]; + + return new Response( + 200, + array('Content-Type' => 'text/plain'), + $body + ); + } + + return new Response( + 200, + array( + 'Content-Type' => 'text/plain', + 'Set-Cookie' => urlencode($key) . '=' . urlencode('test;more') + ), + "Your cookie has been set." + ); +}); +``` + +The above example will try to set a cookie on first access and +will try to print the cookie value on all subsequent tries. +Note how the example uses the `urlencode()` function to encode +non-alphanumeric characters. +This encoding is also used internally when decoding the name and value of cookies +(which is in line with other implementations, such as PHP's cookie functions). + +See also [example #7](examples) for more details. + For more details about the request object, check out the documentation of -[PSR-7 RequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#32-psrhttpmessagerequestinterface). -and [PSR-7 ServerRequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#321-psrhttpmessageserverrequestinterface) +and +[PSR-7 RequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#32-psrhttpmessagerequestinterface). Note that the request object will be processed once the request headers have been received. @@ -327,41 +366,6 @@ Allowed). can in fact use a streaming response body for the tunneled application data. See also [example #21](examples) for more details. -The cookies of the request, will also be added to the request object by the `Server`: - -```php -$http = new Server($socket, function (ServerRequestInterface $request) { - $key = 'react\php'; - - if (isset($request->getCookieParams()[$key])) { - $body = "Your cookie value is: " . $request->getCookieParams()[$key]; - - return new Response( - 200, - array('Content-Type' => 'text/plain'), - $body - ); - } - - return new Response( - 200, - array( - 'Content-Type' => 'text/plain', - 'Set-Cookie' => urlencode($key) . '=' . urlencode('test;more') - ), - "Your cookie has been set." - ); -}); -``` - -The above example should be executed via a web browser, a cookie will be set -on the first request contation the key-value pair 'test=php'. -On a second request to the server the cookie is accessed by the `getCookieParams()` -method, and will be written into the body. -The example uses the `urlencode()` function to encode non-alphanumeric characters. -This encoding is also used internally when decoding the name and value of cookies -(which is in line with other implementations, such as PHP's cookie functions). - ### Response The callback function passed to the constructor of the [Server](#server)