Skip to content

RFC6265 compliant date parsing. #46

New issue

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

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

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions spec/Plugin/CookiePluginSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,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');
}
}
160 changes: 159 additions & 1 deletion src/Plugin/CookiePlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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;
}
}