diff --git a/README.md b/README.md index 7551cda7..331082b3 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,7 @@ For more details about the request object, check out the documentation of and [PSR-7 RequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#32-psrhttpmessagerequestinterface). -> Currently the cookies and uploaded files are not added by the +> Currently the uploaded files are not added by the `Server`, but you can add these parameters by yourself using the given methods. The next versions of this project will cover these features. @@ -378,6 +378,43 @@ Allowed). can in fact use a streaming response body for the tunneled application data. See also [example #21](examples) for more details. +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 #6](examples) for more details. + ### Response The callback function passed to the constructor of the [Server](#server) diff --git a/examples/06-cookie-handling.php b/examples/06-cookie-handling.php new file mode 100644 index 00000000..67e008bb --- /dev/null +++ b/examples/06-cookie-handling.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/RequestHeaderParser.php b/src/RequestHeaderParser.php index bb438b37..d7fdc1d2 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -126,6 +126,11 @@ private function parseRequest($data) $request = $request->withQueryParams($queryParams); } + $cookies = ServerRequest::parseCookie($request->getHeaderLine('Cookie')); + if ($cookies !== false) { + $request = $request->withCookieParams($cookies); + } + // re-apply actual request target from above if ($originalTarget !== null) { $uri = $request->getUri()->withPath(''); diff --git a/src/ServerRequest.php b/src/ServerRequest.php index ea36a5a9..0f31628f 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -117,4 +117,35 @@ public function withoutAttribute($name) unset($new->attributes[$name]); return $new; } + + /** + * @internal + * @param string $cookie + * @return boolean|mixed[] + */ + public static function parseCookie($cookie) + { + // PSR-7 `getHeaderLine('Cookies')` will return multiple + // cookie header comma-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) { + $pair = trim($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 index 14edf3a8..a2c68a4a 100644 --- a/tests/ServerRequestTest.php +++ b/tests/ServerRequestTest.php @@ -105,4 +105,88 @@ public function testServerRequestParameter() $this->assertEquals('1.0', $request->getProtocolVersion()); $this->assertEquals('127.0.0.1', $serverParams['SERVER_ADDR']); } + + 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 comma + // 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); + } + + public function testCookieWithoutSpaceAfterSeparatorWillBeAccepted() + { + $cookieString = 'hello=world;react=php'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(array('hello' => 'world', 'react' => 'php'), $cookies); + } } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index a0dd0f5b..450b90cc 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -2390,6 +2390,68 @@ public function testQueryParametersWillBeAddedToRequest() $this->assertEquals('bar', $queryParams['test']); } + 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 testCookieWithSeparatorWillBeAddedToServerRequest() + { + $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()); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n";