-
-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add custom cookie plugin to reject bad cookie
- Loading branch information
Showing
4 changed files
with
173 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
<?php | ||
|
||
namespace Graby\HttpClient\Plugin; | ||
|
||
use Http\Client\Common\Plugin; | ||
use Http\Client\Exception\TransferException; | ||
use Http\Message\Cookie; | ||
use Http\Message\CookieJar; | ||
use Http\Message\CookieUtil; | ||
use Http\Message\Exception\UnexpectedValueException; | ||
use Http\Promise\Promise; | ||
use Psr\Http\Message\RequestInterface; | ||
use Psr\Http\Message\ResponseInterface; | ||
|
||
/** | ||
* Handle request cookies. | ||
* | ||
* Similar to `Http\Client\Common\Plugin\CookiePlugin` but it rejects bad cookie. | ||
*/ | ||
class CookiePlugin implements Plugin | ||
{ | ||
/** | ||
* Cookie storage. | ||
* | ||
* @var CookieJar | ||
*/ | ||
private $cookieJar; | ||
|
||
public function __construct(CookieJar $cookieJar) | ||
{ | ||
$this->cookieJar = $cookieJar; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise | ||
{ | ||
$cookies = []; | ||
foreach ($this->cookieJar->getCookies() as $cookie) { | ||
if ($cookie->isExpired()) { | ||
continue; | ||
} | ||
|
||
if (!$cookie->matchDomain($request->getUri()->getHost())) { | ||
continue; | ||
} | ||
|
||
if (!$cookie->matchPath($request->getUri()->getPath())) { | ||
continue; | ||
} | ||
|
||
if ($cookie->isSecure() && ('https' !== $request->getUri()->getScheme())) { | ||
continue; | ||
} | ||
|
||
$cookies[] = sprintf('%s=%s', $cookie->getName(), $cookie->getValue()); | ||
} | ||
|
||
if (!empty($cookies)) { | ||
$request = $request->withAddedHeader('Cookie', implode('; ', array_unique($cookies))); | ||
} | ||
|
||
return $next($request)->then(function (ResponseInterface $response) use ($request) { | ||
if ($response->hasHeader('Set-Cookie')) { | ||
$setCookies = $response->getHeader('Set-Cookie'); | ||
|
||
foreach ($setCookies as $setCookie) { | ||
$cookie = $this->createCookie($request, $setCookie); | ||
|
||
// Cookie invalid do not use it | ||
if (null === $cookie) { | ||
continue; | ||
} | ||
|
||
// Restrict setting cookie from another domain | ||
if (!preg_match("/\.{$cookie->getDomain()}$/", '.' . $request->getUri()->getHost())) { | ||
continue; | ||
} | ||
|
||
$this->cookieJar->addCookie($cookie); | ||
} | ||
} | ||
|
||
return $response; | ||
}); | ||
} | ||
|
||
/** | ||
* Creates a cookie from a string. | ||
* | ||
* @throws TransferException | ||
*/ | ||
private function createCookie(RequestInterface $request, string $setCookieHeader): ?Cookie | ||
{ | ||
$parts = array_map('trim', explode(';', $setCookieHeader)); | ||
|
||
if (empty($parts) || !strpos($parts[0], '=')) { | ||
return null; | ||
} | ||
|
||
list($name, $cookieValue) = $this->createValueKey(array_shift($parts)); | ||
|
||
$maxAge = null; | ||
$expires = null; | ||
$domain = $request->getUri()->getHost(); | ||
$path = $request->getUri()->getPath(); | ||
$secure = false; | ||
$httpOnly = false; | ||
|
||
// Add the cookie pieces into the parsed data array | ||
foreach ($parts as $part) { | ||
list($key, $value) = $this->createValueKey($part); | ||
|
||
switch (strtolower($key)) { | ||
case 'expires': | ||
try { | ||
$expires = CookieUtil::parseDate((string) $value); | ||
} catch (UnexpectedValueException $e) { | ||
throw new TransferException(sprintf('Cookie header `%s` expires value `%s` could not be converted to date', $name, $value), 0, $e); | ||
} | ||
|
||
break; | ||
case 'max-age': | ||
$maxAge = (int) $value; | ||
|
||
break; | ||
case 'domain': | ||
$domain = $value; | ||
|
||
break; | ||
case 'path': | ||
$path = $value; | ||
|
||
break; | ||
case 'secure': | ||
$secure = true; | ||
|
||
break; | ||
case 'httponly': | ||
$httpOnly = true; | ||
|
||
break; | ||
} | ||
} | ||
|
||
try { | ||
return new Cookie($name, $cookieValue, $maxAge, $domain, $path, $secure, $httpOnly, $expires); | ||
} catch (\InvalidArgumentException $e) { | ||
return null; | ||
} | ||
} | ||
|
||
/** | ||
* Separates key/value pair from cookie. | ||
* | ||
* @param string $part A single cookie value in format key=value | ||
* | ||
* @return array{0:string, 1:?string} | ||
*/ | ||
private function createValueKey(string $part): array | ||
{ | ||
$parts = explode('=', $part, 2); | ||
$key = trim($parts[0]); | ||
$value = isset($parts[1]) ? trim($parts[1]) : null; | ||
|
||
return [$key, $value]; | ||
} | ||
} |