diff --git a/README.md b/README.md index 1006559f..56abef64 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,7 +155,81 @@ $http = new Server($socket, function (RequestInterface $request) { }); ``` +The `getServerParams(): mixed[]` method can be used to +get server-side parameters similar to the `$_SERVER` variable. +The following parameters are currently available: + +* `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 when the complete request header has been received, + as integer similar to `time()` +* `request_time_float` + 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` + +```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 + ); +}); +``` + +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 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 @@ -184,7 +260,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 +324,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.'; @@ -308,7 +384,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 +403,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 +431,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 +465,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 +513,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 +522,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 +531,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 +540,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/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-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 75% rename from examples/02-count-visitors.php rename to examples/03-count-visitors.php index 2c384a3b..9f69f797 100644 --- a/examples/02-count-visitors.php +++ b/examples/03-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/04-stream-response.php similarity index 84% rename from examples/03-stream-response.php rename to examples/04-stream-response.php index 5fd990e8..d6700017 100644 --- a/examples/03-stream-response.php +++ b/examples/04-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/05-stream-request.php similarity index 91% rename from examples/04-stream-request.php rename to examples/05-stream-request.php index 481162ef..a9ff4e18 100644 --- a/examples/04-stream-request.php +++ b/examples/05-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/06-error-handling.php similarity index 82% rename from examples/05-error-handling.php rename to examples/06-error-handling.php index 29f54b92..f4b95397 100644 --- a/examples/05-error-handling.php +++ b/examples/06-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/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/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'), 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..8565ba64 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; @@ -25,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'), @@ -46,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'), @@ -81,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. * @@ -99,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" + * ); * }); * ``` * @@ -121,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" + * ); * }); *``` * @@ -146,7 +166,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 +190,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 +268,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 +340,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 +356,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 +364,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";