From 30058c1b003f08b7edb2bf243f3e155dbb2d080a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D1=85=D0=B0=D0=B8=D0=BB=20=D0=9A=D1=80=D0=B0?= =?UTF-8?q?=D1=81=D0=B8=D0=BB=D1=8C=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2?= Date: Wed, 12 Oct 2016 11:35:01 +0300 Subject: [PATCH 1/2] RFC6265 compliant date parsing. [RFC6265 section 5.1.1](https://tools.ietf.org/html/rfc6265#section-5.1.1) describes algorithm which should be used to parse a cookie-date. Ported from here: https://github.com/salesforce/tough-cookie/blob/master/lib/cookie.js --- spec/Plugin/CookiePluginSpec.php | 23 +++++ src/Plugin/CookiePlugin.php | 160 ++++++++++++++++++++++++++++++- 2 files changed, 182 insertions(+), 1 deletion(-) diff --git a/spec/Plugin/CookiePluginSpec.php b/spec/Plugin/CookiePluginSpec.php index 1851876..ce3646f 100644 --- a/spec/Plugin/CookiePluginSpec.php +++ b/spec/Plugin/CookiePluginSpec.php @@ -14,6 +14,9 @@ class CookiePluginSpec extends ObjectBehavior { + /** + * @var CookieJar + */ private $cookieJar; function let() @@ -180,4 +183,24 @@ function it_throws_exception_on_invalid_expires_date( $promise->shouldReturnAnInstanceOf('Http\Client\Promise\HttpRejectedPromise'); $promise->shouldThrow('Http\Client\Exception\TransferException')->duringWait(); } + + function it_support_rfc6265(RequestInterface $request, ResponseInterface $response, UriInterface $uri) + { + $next = function () use ($response) { + return new HttpFulfilledPromise($response->getWrappedObject()); + }; + + $response->hasHeader('Set-Cookie')->willReturn(true); + $response->getHeader('Set-Cookie')->willReturn([ + 'cookie=value; expires=Tuesday, 31 Mar 99 07:42:12 GMT; Max-Age=60; path=/; domain=test.com; secure; HttpOnly' + ]); + + $request->getUri()->willReturn($uri); + $uri->getHost()->willReturn('test.com'); + $uri->getPath()->willReturn('/'); + + $promise = $this->handleRequest($request, $next, function () {}); + $promise->shouldHaveType('Http\Promise\Promise'); + $promise->wait()->shouldReturnAnInstanceOf('Psr\Http\Message\ResponseInterface'); + } } diff --git a/src/Plugin/CookiePlugin.php b/src/Plugin/CookiePlugin.php index af306e5..df0403d 100644 --- a/src/Plugin/CookiePlugin.php +++ b/src/Plugin/CookiePlugin.php @@ -114,7 +114,7 @@ private function createCookie(RequestInterface $request, $setCookie) switch (strtolower($key)) { case 'expires': - $expires = \DateTime::createFromFormat(\DateTime::COOKIE, $value); + $expires = $this->parseExpires($value); if (true !== ($expires instanceof \DateTime)) { throw new TransferException( @@ -167,4 +167,162 @@ private function createValueKey($part) return [$key, $value]; } + + /** + * Parses cookie "expires" value. + * + * @param string $expires + * + * @return \DateTime|false + * + * @see https://tools.ietf.org/html/rfc6265#section-5.1.1 + * @see https://github.com/salesforce/tough-cookie/blob/master/lib/cookie.js + */ + private function parseExpires($expires) + { + /* + * RFC6265 5.1.1: + * 2. Process each date-token sequentially in the order the date-tokens + * appear in the cookie-date + */ + $tokens = preg_split('/[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]/', $expires); + if (!is_array($tokens)) { + return false; + } + + $hour = null; + $minutes = null; + $seconds = null; + $day = null; + $month = null; + $year = null; + + foreach ($tokens as $token) { + $token = trim($token); + if ('' === $token) { + continue; + } + + /* + * 2.1. If the found-time flag is not set and the token matches the time + * production, set the found-time flag and set the hour- value, + * minute-value, and second-value to the numbers denoted by the digits in + * the date-token, respectively. Skip the remaining sub-steps and continue + * to the next date-token. + */ + if (null === $seconds) { + preg_match('/^(\d{1,2})\D*:(\d{1,2})\D*:(\d{1,2})\D*$/', $token, $match); + if (count($match) > 0) { + $hour = (int) $match[1]; + $minutes = (int) $match[2]; + $seconds = (int) $match[3]; + /* + * [fail if] + * - the hour-value is greater than 23, + * - the minute-value is greater than 59, or + * - the second-value is greater than 59. + */ + if ($hour > 23 || $minutes > 59 || $seconds > 59) { + return false; + } + + continue; + } + } + + /* + * 2.2. If the found-day-of-month flag is not set and the date-token matches + * the day-of-month production, set the found-day-of- month flag and set + * the day-of-month-value to the number denoted by the date-token. Skip + * the remaining sub-steps and continue to the next date-token. + */ + if (null === $day) { + preg_match('/^(\d{1,2})\D*$/', $token, $match); + if (count($match) > 0) { + $day = (int) $match[1]; + /* + * [fail if] the day-of-month-value is less than 1 or greater than 31 + */ + if ($day < 1 || $day > 31) { + return false; + } + continue; + } + } + + /* + * 2.3. If the found-month flag is not set and the date-token matches the + * month production, set the found-month flag and set the month-value to + * the month denoted by the date-token. Skip the remaining sub-steps and + * continue to the next date-token. + */ + if (null === $month) { + preg_match('/^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)/i', $token, $match); + if (count($match) > 0) { + $month = array_search( + strtolower($match[1]), + [ + 'jan', + 'feb', + 'mar', + 'apr', + 'may', + 'jun', + 'jul', + 'aug', + 'sep', + 'oct', + 'nov', + 'dec' + ], + true + ); + continue; + } + } + + /* + * 2.4. If the found-year flag is not set and the date-token matches the year + * production, set the found-year flag and set the year-value to the number + * denoted by the date-token. Skip the remaining sub-steps and continue to + * the next date-token. + */ + if (null === $year) { + preg_match(' /^(\d{2}|\d{4})$/', $token, $match); + if (count($match) > 0) { + $year = (int) $match[1]; + /* + * 3. If the year-value is greater than or equal to 70 and less + * than or equal to 99, increment the year-value by 1900. + * 4. If the year-value is greater than or equal to 0 and less + * than or equal to 69, increment the year-value by 2000. + */ + if (70 <= $year && $year <= 99) { + $year += 1900; + } elseif (0 <= $year && $year <= 69) { + $year += 2000; + } + + if ($year < 1601) { + return false; // 5. ... the year-value is less than 1601 + } + } + } + } + + if (null === $seconds || null === $day || null === $month || null === $year) { + /* + * 5. ... at least one of the found-day-of-month, found-month, found- + * year, or found-time flags is not set, + */ + return false; + } + + // UTC/GMT format required by cookies. + $time = new \DateTime('now', new \DateTimeZone('UTC')); + $time->setDate($year, $month, $day); + $time->setTime($hour, $minutes, $seconds); + + return $time; + } } From 8aa00477af681dbe05a424db0243b463e6d8fb5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D1=85=D0=B0=D0=B8=D0=BB=20=D0=9A=D1=80=D0=B0?= =?UTF-8?q?=D1=81=D0=B8=D0=BB=D1=8C=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2?= Date: Wed, 12 Oct 2016 11:47:05 +0300 Subject: [PATCH 2/2] Remove forgotten comment. --- spec/Plugin/CookiePluginSpec.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/spec/Plugin/CookiePluginSpec.php b/spec/Plugin/CookiePluginSpec.php index ce3646f..21cb2f5 100644 --- a/spec/Plugin/CookiePluginSpec.php +++ b/spec/Plugin/CookiePluginSpec.php @@ -14,9 +14,6 @@ class CookiePluginSpec extends ObjectBehavior { - /** - * @var CookieJar - */ private $cookieJar; function let()