Initial Commit

This commit is contained in:
David Stone
2024-11-30 18:24:12 -07:00
commit e8f7955c1c
5432 changed files with 1397750 additions and 0 deletions

View File

@ -0,0 +1,322 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Http\Cookie;
/**
* Cookie attributes as defined in https://tools.ietf.org/html/rfc6265.
*
* @link https://tools.ietf.org/html/rfc6265
*/
final class CookieAttributes
{
public const SAMESITE_NONE = 'None';
public const SAMESITE_LAX = 'Lax';
public const SAMESITE_STRICT = 'Strict';
/**
* @return CookieAttributes No cookie attributes.
*
* @see self::default()
*/
public static function empty() : self
{
$new = new self();
$new->httpOnly = \false;
return $new;
}
/**
* @return CookieAttributes Default cookie attributes, which means httpOnly is enabled by default.
*
* @see self::empty()
*/
public static function default() : self
{
return new self();
}
/** @var string */
private $path = '';
/** @var string */
private $domain = '';
/** @var int|null */
private $maxAge;
/** @var \DateTimeImmutable */
private $expiry;
/** @var bool */
private $secure = \false;
/** @var bool */
private $httpOnly = \true;
/** @var string|null */
private $sameSite;
private function __construct()
{
// only allow creation via named constructors
}
/**
* @param string $path Cookie path.
*
* @return self Cloned instance with the specified operation applied. Cloned instance with the specified operation
* applied.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.4
*/
public function withPath(string $path) : self
{
$new = clone $this;
$new->path = $path;
return $new;
}
/**
* @param string $domain Cookie domain.
*
* @return self Cloned instance with the specified operation applied.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.3
*/
public function withDomain(string $domain) : self
{
$new = clone $this;
$new->domain = $domain;
return $new;
}
/**
* @param string $sameSite Cookie SameSite attribute value.
*
* @return self Cloned instance with the specified operation applied.
*
* @link https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-5.3.7
*/
public function withSameSite(string $sameSite) : self
{
$normalizedValue = \ucfirst(\strtolower($sameSite));
if (!\in_array($normalizedValue, [self::SAMESITE_NONE, self::SAMESITE_LAX, self::SAMESITE_STRICT], \true)) {
throw new \Error("Invalid SameSite attribute: " . $sameSite);
}
$new = clone $this;
$new->sameSite = $normalizedValue;
return $new;
}
/**
* @return self Cloned instance with the specified operation applied.
*
* @link https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-5.3.7
*/
public function withoutSameSite() : self
{
$new = clone $this;
$new->sameSite = null;
return $new;
}
/**
* Applies the given maximum age to the cookie.
*
* @param int $maxAge Cookie maximum age.
*
* @return self Cloned instance with the specified operation applied.
*
* @see self::withoutMaxAge()
* @see self::withExpiry()
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.2
*/
public function withMaxAge(int $maxAge) : self
{
$new = clone $this;
$new->maxAge = $maxAge;
return $new;
}
/**
* Removes any max-age information.
*
* @return self Cloned instance with the specified operation applied.
*
* @see self::withMaxAge()
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.2
*/
public function withoutMaxAge() : self
{
$new = clone $this;
$new->maxAge = null;
return $new;
}
/**
* Applies the given expiry to the cookie.
*
* @return self Cloned instance with the specified operation applied.
*
* @see self::withMaxAge()
* @see self::withoutExpiry()
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.1
*/
public function withExpiry(\DateTimeInterface $date) : self
{
$new = clone $this;
if ($date instanceof \DateTimeImmutable) {
$new->expiry = $date;
} elseif ($date instanceof \DateTime) {
$new->expiry = \DateTimeImmutable::createFromMutable($date);
} else {
$new->expiry = new \DateTimeImmutable("@" . $date->getTimestamp());
}
return $new;
}
/**
* Removes any expiry information.
*
* @return self Cloned instance with the specified operation applied.
*
* @see self::withExpiry()
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.1
*/
public function withoutExpiry() : self
{
$new = clone $this;
$new->expiry = null;
return $new;
}
/**
* @return self Cloned instance with the specified operation applied.
*
* @see self::withoutSecure()
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.5
*/
public function withSecure() : self
{
$new = clone $this;
$new->secure = \true;
return $new;
}
/**
* @return self Cloned instance with the specified operation applied.
*
* @see self::withSecure()
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.5
*/
public function withoutSecure() : self
{
$new = clone $this;
$new->secure = \false;
return $new;
}
/**
* @return self Cloned instance with the specified operation applied.
*
* @see self::withoutHttpOnly()
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.6
*/
public function withHttpOnly() : self
{
$new = clone $this;
$new->httpOnly = \true;
return $new;
}
/**
* @return self Cloned instance with the specified operation applied.
*
* @see self::withHttpOnly()
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.6
*/
public function withoutHttpOnly() : self
{
$new = clone $this;
$new->httpOnly = \false;
return $new;
}
/**
* @return string Cookie path.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.4
*/
public function getPath() : string
{
return $this->path;
}
/**
* @return string Cookie domain.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.3
*/
public function getDomain() : string
{
return $this->domain;
}
/**
* @return string Cookie domain.
*
* @link https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-5.3.7
*/
public function getSameSite() : ?string
{
return $this->sameSite;
}
/**
* @return int|null Cookie maximum age in seconds or `null` if no value is set.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.2
*/
public function getMaxAge() : ?int
{
return $this->maxAge;
}
/**
* @return \DateTimeImmutable|null Cookie expiry or `null` if no value is set.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.2
*/
public function getExpiry() : ?\DateTimeImmutable
{
return $this->expiry;
}
/**
* @return bool Whether the secure flag is enabled or not.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.5
*/
public function isSecure() : bool
{
return $this->secure;
}
/**
* @return bool Whether the httpOnly flag is enabled or not.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.6
*/
public function isHttpOnly() : bool
{
return $this->httpOnly;
}
/**
* @return string Representation of the cookie attributes appended to key=value in a 'set-cookie' header.
*/
public function __toString() : string
{
$string = '';
if ($this->expiry) {
$string .= '; Expires=' . \gmdate('D, j M Y G:i:s T', $this->expiry->getTimestamp());
}
if ($this->maxAge) {
$string .= '; Max-Age=' . $this->maxAge;
}
if ('' !== $this->path) {
$string .= '; Path=' . $this->path;
}
if ('' !== $this->domain) {
$string .= '; Domain=' . $this->domain;
}
if ($this->secure) {
$string .= '; Secure';
}
if ($this->httpOnly) {
$string .= '; HttpOnly';
}
if ($this->sameSite !== null) {
$string .= '; SameSite=' . $this->sameSite;
}
return $string;
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Http\Cookie;
final class InvalidCookieException extends \Exception
{
public function __construct(string $message)
{
parent::__construct($message);
}
}

View File

@ -0,0 +1,107 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Http\Cookie;
/**
* A cookie as sent in a request's 'cookie' header, so without any attributes.
*
* This class does not deal with encoding of arbitrary names and values. If you want to use arbitrary values, please use
* an encoding mechanism like Base64 or URL encoding.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.4
*/
final class RequestCookie
{
/** @var string */
private $name;
/** @var string */
private $value;
/**
* Parses the cookies from a 'cookie' header.
*
* Note: Parsing is aborted if there's an invalid value and no cookies are returned.
*
* @param string $string Valid 'cookie' header line.
*
* @return RequestCookie[]
*/
public static function fromHeader(string $string) : array
{
$cookies = \explode(";", $string);
$result = [];
try {
foreach ($cookies as $cookie) {
// Ignore zero-length cookie.
if (\trim($cookie) === '') {
continue;
}
$parts = \explode('=', $cookie, 2);
if (2 !== \count($parts)) {
return [];
}
list($name, $value) = $parts;
// We can safely trim quotes, as they're not allowed within cookie values
$result[] = new self(\trim($name), \trim($value, " \t\""));
}
} catch (InvalidCookieException $e) {
return [];
}
return $result;
}
/**
* @param string $name Cookie name in its decoded form.
* @param string $value Cookie value in its decoded form.
*
* @throws InvalidCookieException If name or value is invalid.
*/
public function __construct(string $name, string $value = '')
{
if (!\preg_match('(^[^()<>@,;:\\\\"/[\\]?={}\\x01-\\x20\\x7F]*+$)', $name)) {
throw new InvalidCookieException("Invalid cookie name: '{$name}'");
}
if (!\preg_match('(^[\\x21\\x23-\\x2B\\x2D-\\x3A\\x3C-\\x5B\\x5D-\\x7E]*+$)', $value)) {
throw new InvalidCookieException("Invalid cookie value: '{$value}'");
}
$this->name = $name;
$this->value = $value;
}
/**
* @return string Name of the cookie.
*/
public function getName() : string
{
return $this->name;
}
public function withName(string $name) : self
{
if (!\preg_match('(^[^()<>@,;:\\\\"/[\\]?={}\\x01-\\x20\\x7F]++$)', $name)) {
throw new InvalidCookieException("Invalid cookie name: '{$name}'");
}
$clone = clone $this;
$clone->name = $name;
return $clone;
}
/**
* @return string Value of the cookie.
*/
public function getValue() : string
{
return $this->value;
}
public function withValue(string $value) : self
{
if (!\preg_match('(^[\\x21\\x23-\\x2B\\x2D-\\x3A\\x3C-\\x5B\\x5D-\\x7E]*+$)', $value)) {
throw new InvalidCookieException("Invalid cookie value: '{$value}'");
}
$clone = clone $this;
$clone->value = $value;
return $clone;
}
/**
* @return string Representation of the cookie as in a 'cookie' header.
*/
public function __toString() : string
{
return $this->name . '=' . $this->value;
}
}

View File

@ -0,0 +1,306 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Http\Cookie;
/**
* A cookie as sent in a response's 'set-cookie' header, so with attributes.
*
* This class does not deal with encoding of arbitrary names and values. If you want to use arbitrary values, please use
* an encoding mechanism like Base64 or URL encoding.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2
*/
final class ResponseCookie
{
private static $dateFormats = ['D, d M Y H:i:s T', 'D, d-M-y H:i:s T', 'D, d-M-Y H:i:s T', 'D, d-m-y H:i:s T', 'D, d-m-Y H:i:s T', 'D M j G:i:s Y', 'D M d H:i:s Y T'];
/**
* Parses a cookie from a 'set-cookie' header.
*
* @param string $string Valid 'set-cookie' header line.
*
* @return self|null Returns a `ResponseCookie` instance on success and `null` on failure.
*/
public static function fromHeader(string $string) : ?self
{
$parts = \array_map("trim", \explode(";", $string));
$nameValue = \explode("=", \array_shift($parts), 2);
if (\count($nameValue) !== 2) {
return null;
}
list($name, $value) = $nameValue;
$name = \trim($name);
$value = \trim($value, " \t\n\r\x00\v\"");
if ($name === "") {
return null;
}
// httpOnly must default to false for parsing
$meta = CookieAttributes::empty();
$unknownAttributes = [];
foreach ($parts as $part) {
$pieces = \array_map('trim', \explode('=', $part, 2));
$key = \strtolower($pieces[0]);
if (1 === \count($pieces)) {
switch ($key) {
case 'secure':
$meta = $meta->withSecure();
break;
case 'httponly':
$meta = $meta->withHttpOnly();
break;
default:
$unknownAttributes[] = $part;
break;
}
} else {
switch ($key) {
case 'expires':
$time = self::parseDate($pieces[1]);
if ($time === null) {
break;
// break is correct, see https://tools.ietf.org/html/rfc6265#section-5.2.1
}
$meta = $meta->withExpiry($time);
break;
case 'max-age':
$maxAge = \trim($pieces[1]);
// This also allows +1.42, but avoids a more complicated manual check
if (!\is_numeric($maxAge)) {
break;
// break is correct, see https://tools.ietf.org/html/rfc6265#section-5.2.2
}
$meta = $meta->withMaxAge($maxAge);
break;
case 'path':
$meta = $meta->withPath($pieces[1]);
break;
case 'domain':
$meta = $meta->withDomain($pieces[1]);
break;
case 'samesite':
$normalizedValue = \ucfirst(\strtolower($pieces[1]));
if (!\in_array($normalizedValue, [CookieAttributes::SAMESITE_NONE, CookieAttributes::SAMESITE_LAX, CookieAttributes::SAMESITE_STRICT], \true)) {
$unknownAttributes[] = $part;
} else {
$meta = $meta->withSameSite($normalizedValue);
}
break;
default:
$unknownAttributes[] = $part;
break;
}
}
}
try {
$cookie = new self($name, $value, $meta);
$cookie->unknownAttributes = $unknownAttributes;
return $cookie;
} catch (InvalidCookieException $e) {
return null;
}
}
/**
* @param string $date Formatted cookie date
*
* @return \DateTimeImmutable|null Parsed date.
*/
private static function parseDate(string $date) : ?\DateTimeImmutable
{
foreach (self::$dateFormats as $dateFormat) {
if ($parsedDate = \DateTimeImmutable::createFromFormat($dateFormat, $date, new \DateTimeZone('GMT'))) {
return $parsedDate;
}
}
return null;
}
/** @var string[] */
private $unknownAttributes = [];
/** @var string */
private $name;
/** @var string */
private $value;
/** @var CookieAttributes */
private $attributes;
/**
* @param string $name Name of the cookie.
* @param string $value Value of the cookie.
* @param CookieAttributes $attributes Attributes of the cookie.
*
* @throws InvalidCookieException If name or value is invalid.
*/
public function __construct(string $name, string $value = '', CookieAttributes $attributes = null)
{
if (!\preg_match('(^[^()<>@,;:\\\\"/[\\]?={}\\x01-\\x20\\x7F]++$)', $name)) {
throw new InvalidCookieException("Invalid cookie name: '{$name}'");
}
if (!\preg_match('(^[\\x21\\x23-\\x2B\\x2D-\\x3A\\x3C-\\x5B\\x5D-\\x7E]*+$)', $value)) {
throw new InvalidCookieException("Invalid cookie value: '{$value}'");
}
$this->name = $name;
$this->value = $value;
$this->attributes = $attributes ?? CookieAttributes::default();
}
/**
* @return string Name of the cookie.
*/
public function getName() : string
{
return $this->name;
}
public function withName(string $name) : self
{
if (!\preg_match('(^[^()<>@,;:\\\\"/[\\]?={}\\x01-\\x20\\x7F]++$)', $name)) {
throw new InvalidCookieException("Invalid cookie name: '{$name}'");
}
$clone = clone $this;
$clone->name = $name;
return $clone;
}
/**
* @return string Value of the cookie.
*/
public function getValue() : string
{
return $this->value;
}
public function withValue(string $value) : self
{
if (!\preg_match('(^[\\x21\\x23-\\x2B\\x2D-\\x3A\\x3C-\\x5B\\x5D-\\x7E]*+$)', $value)) {
throw new InvalidCookieException("Invalid cookie value: '{$value}'");
}
$clone = clone $this;
$clone->value = $value;
return $clone;
}
/**
* @return \DateTimeImmutable|null Expiry if set, otherwise `null`.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.1
*/
public function getExpiry() : ?\DateTimeImmutable
{
return $this->attributes->getExpiry();
}
public function withExpiry(\DateTimeInterface $expiry) : self
{
return $this->withAttributes($this->attributes->withExpiry($expiry));
}
public function withoutExpiry() : self
{
return $this->withAttributes($this->attributes->withoutExpiry());
}
/**
* @return int|null Max-Age if set, otherwise `null`.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.2
*/
public function getMaxAge() : ?int
{
return $this->attributes->getMaxAge();
}
public function withMaxAge(int $maxAge) : self
{
return $this->withAttributes($this->attributes->withMaxAge($maxAge));
}
public function withoutMaxAge() : self
{
return $this->withAttributes($this->attributes->withoutMaxAge());
}
/**
* @return string Cookie path.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.4
*/
public function getPath() : string
{
return $this->attributes->getPath();
}
public function withPath(string $path) : self
{
return $this->withAttributes($this->attributes->withPath($path));
}
/**
* @return string Cookie domain.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.3
*/
public function getDomain() : string
{
return $this->attributes->getDomain();
}
public function withDomain(string $domain) : self
{
return $this->withAttributes($this->attributes->withDomain($domain));
}
/**
* @return bool Whether the secure flag is enabled or not.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.5
*/
public function isSecure() : bool
{
return $this->attributes->isSecure();
}
public function withSecure() : self
{
return $this->withAttributes($this->attributes->withSecure());
}
public function withoutSecure() : self
{
return $this->withAttributes($this->attributes->withoutSecure());
}
/**
* @return bool Whether the httpOnly flag is enabled or not.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.6
*/
public function isHttpOnly() : bool
{
return $this->attributes->isHttpOnly();
}
public function withHttpOnly() : self
{
return $this->withAttributes($this->attributes->withHttpOnly());
}
public function withoutHttpOnly() : self
{
return $this->withAttributes($this->attributes->withoutHttpOnly());
}
public function withSameSite(string $sameSite) : self
{
return $this->withAttributes($this->attributes->withSameSite($sameSite));
}
public function withoutSameSite() : self
{
return $this->withAttributes($this->attributes->withoutSameSite());
}
public function getSameSite() : ?string
{
return $this->attributes->getSameSite();
}
/**
* @return CookieAttributes All cookie attributes.
*/
public function getAttributes() : CookieAttributes
{
return $this->attributes;
}
public function withAttributes(CookieAttributes $attributes) : self
{
$clone = clone $this;
$clone->attributes = $attributes;
return $clone;
}
/**
* @return string Representation of the cookie as in a 'set-cookie' header.
*/
public function __toString() : string
{
$line = $this->name . '=' . $this->value;
$line .= $this->attributes;
$unknownAttributes = \implode('; ', $this->unknownAttributes);
if ($unknownAttributes !== '') {
$line .= '; ' . $unknownAttributes;
}
return $line;
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Http\Http2;
final class Http2ConnectionException extends \Exception
{
public function __construct(string $message, int $code, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,485 @@
<?php
/** @noinspection PhpUnusedPrivateFieldInspection */
/** @noinspection PhpDocSignatureInspection */
/** @noinspection PhpUnhandledExceptionInspection */
namespace WP_Ultimo\Dependencies\Amp\Http\Http2;
use WP_Ultimo\Dependencies\Amp\Http\HPack;
final class Http2Parser
{
public const PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
private const DEFAULT_MAX_FRAME_SIZE = 1 << 14;
private const HEADER_NAME_REGEX = '/^[\\x21-\\x40\\x5b-\\x7e]+$/';
public const KNOWN_RESPONSE_PSEUDO_HEADERS = [":status" => \true];
public const KNOWN_REQUEST_PSEUDO_HEADERS = [":method" => \true, ":authority" => \true, ":path" => \true, ":scheme" => \true];
// SETTINGS Flags - https://http2.github.io/http2-spec/#rfc.section.6.5
public const ACK = 0x1;
// HEADERS Flags - https://http2.github.io/http2-spec/#rfc.section.6.2
public const NO_FLAG = 0x0;
public const END_STREAM = 0x1;
public const END_HEADERS = 0x4;
public const PADDED = 0x8;
public const PRIORITY_FLAG = 0x20;
// Frame Types - https://http2.github.io/http2-spec/#rfc.section.11.2
public const DATA = 0x0;
public const HEADERS = 0x1;
public const PRIORITY = 0x2;
public const RST_STREAM = 0x3;
public const SETTINGS = 0x4;
public const PUSH_PROMISE = 0x5;
public const PING = 0x6;
public const GOAWAY = 0x7;
public const WINDOW_UPDATE = 0x8;
public const CONTINUATION = 0x9;
// Settings
public const HEADER_TABLE_SIZE = 0x1;
// 1 << 12
public const ENABLE_PUSH = 0x2;
// 1
public const MAX_CONCURRENT_STREAMS = 0x3;
// INF
public const INITIAL_WINDOW_SIZE = 0x4;
// 1 << 16 - 1
public const MAX_FRAME_SIZE = 0x5;
// 1 << 14
public const MAX_HEADER_LIST_SIZE = 0x6;
// INF
// Error codes
public const GRACEFUL_SHUTDOWN = 0x0;
public const PROTOCOL_ERROR = 0x1;
public const INTERNAL_ERROR = 0x2;
public const FLOW_CONTROL_ERROR = 0x3;
public const SETTINGS_TIMEOUT = 0x4;
public const STREAM_CLOSED = 0x5;
public const FRAME_SIZE_ERROR = 0x6;
public const REFUSED_STREAM = 0x7;
public const CANCEL = 0x8;
public const COMPRESSION_ERROR = 0x9;
public const CONNECT_ERROR = 0xa;
public const ENHANCE_YOUR_CALM = 0xb;
public const INADEQUATE_SECURITY = 0xc;
public const HTTP_1_1_REQUIRED = 0xd;
public static function getFrameName(int $type) : string
{
$names = [self::DATA => 'DATA', self::HEADERS => 'HEADERS', self::PRIORITY => 'PRIORITY', self::RST_STREAM => 'RST_STREAM', self::SETTINGS => 'SETTINGS', self::PUSH_PROMISE => 'PUSH_PROMISE', self::PING => 'PING', self::GOAWAY => 'GOAWAY', self::WINDOW_UPDATE => 'WINDOW_UPDATE', self::CONTINUATION => 'CONTINUATION'];
return $names[$type] ?? '0x' . \bin2hex(\chr($type));
}
public static function logDebugFrame(string $action, int $frameType, int $frameFlags, int $streamId, int $frameLength) : bool
{
$env = \getenv("AMP_DEBUG_HTTP2_FRAMES") ?: "0";
if ($env !== "0" && $env !== "false" || \defined("AMP_DEBUG_HTTP2_FRAMES") && \AMP_DEBUG_HTTP2_FRAMES) {
\fwrite(\STDERR, $action . ' ' . self::getFrameName($frameType) . ' <flags = ' . \bin2hex(\chr($frameFlags)) . ', stream = ' . $streamId . ', length = ' . $frameLength . '>' . "\r\n");
}
return \true;
}
/** @var string */
private $buffer = '';
/** @var int */
private $bufferOffset = 0;
/** @var int */
private $headerSizeLimit = self::DEFAULT_MAX_FRAME_SIZE;
// Should be configurable?
/** @var bool */
private $continuationExpected = \false;
/** @var int */
private $headerFrameType = 0;
/** @var string */
private $headerBuffer = '';
/** @var int */
private $headerStream = 0;
/** @var HPack */
private $hpack;
/** @var Http2Processor */
private $handler;
/** @var int */
private $receivedFrameCount = 0;
/** @var int */
private $receivedByteCount = 0;
public function __construct(Http2Processor $handler)
{
$this->hpack = new HPack();
$this->handler = $handler;
}
public function getReceivedByteCount() : int
{
return $this->receivedByteCount;
}
public function getReceivedFrameCount() : int
{
return $this->receivedFrameCount;
}
public function parse(string $settings = null) : \Generator
{
if ($settings !== null) {
$this->parseSettings($settings, \strlen($settings), self::NO_FLAG, 0);
}
$this->buffer = yield;
while (\true) {
$frameHeader = (yield from $this->consume(9));
['length' => $frameLength, 'type' => $frameType, 'flags' => $frameFlags, 'id' => $streamId] = \unpack('Nlength/ctype/cflags/Nid', "\x00" . $frameHeader);
$streamId &= 0x7fffffff;
$frameBuffer = $frameLength === 0 ? '' : (yield from $this->consume($frameLength));
$this->receivedFrameCount++;
\assert(self::logDebugFrame('recv', $frameType, $frameFlags, $streamId, $frameLength));
try {
// Do we want to allow increasing the maximum frame size?
if ($frameLength > self::DEFAULT_MAX_FRAME_SIZE) {
throw new Http2ConnectionException("Frame size limit exceeded", self::FRAME_SIZE_ERROR);
}
if ($this->continuationExpected && $frameType !== self::CONTINUATION) {
throw new Http2ConnectionException("Expected continuation frame", self::PROTOCOL_ERROR);
}
switch ($frameType) {
case self::DATA:
$this->parseDataFrame($frameBuffer, $frameLength, $frameFlags, $streamId);
break;
case self::PUSH_PROMISE:
$this->parsePushPromise($frameBuffer, $frameLength, $frameFlags, $streamId);
break;
case self::HEADERS:
$this->parseHeaders($frameBuffer, $frameLength, $frameFlags, $streamId);
break;
case self::PRIORITY:
$this->parsePriorityFrame($frameBuffer, $frameLength, $streamId);
break;
case self::RST_STREAM:
$this->parseStreamReset($frameBuffer, $frameLength, $streamId);
break;
case self::SETTINGS:
$this->parseSettings($frameBuffer, $frameLength, $frameFlags, $streamId);
break;
case self::PING:
$this->parsePing($frameBuffer, $frameLength, $frameFlags, $streamId);
break;
case self::GOAWAY:
$this->parseGoAway($frameBuffer, $frameLength, $streamId);
break;
case self::WINDOW_UPDATE:
$this->parseWindowUpdate($frameBuffer, $frameLength, $streamId);
break;
case self::CONTINUATION:
$this->parseContinuation($frameBuffer, $frameFlags, $streamId);
break;
default:
// Ignore and discard unknown frame per spec
break;
}
} catch (Http2StreamException $exception) {
$this->handler->handleStreamException($exception);
} catch (Http2ConnectionException $exception) {
$this->handler->handleConnectionException($exception);
throw $exception;
}
}
}
private function consume(int $bytes) : \Generator
{
$bufferEnd = $this->bufferOffset + $bytes;
while (\strlen($this->buffer) < $bufferEnd) {
$this->buffer .= yield;
}
$consumed = \substr($this->buffer, $this->bufferOffset, $bytes);
if ($bufferEnd > 2048) {
$this->buffer = \substr($this->buffer, $bufferEnd);
$this->bufferOffset = 0;
} else {
$this->bufferOffset += $bytes;
}
$this->receivedByteCount += $bytes;
return $consumed;
}
private function parseDataFrame(string $frameBuffer, int $frameLength, int $frameFlags, int $streamId) : void
{
$isPadded = $frameFlags & self::PADDED;
$headerLength = $isPadded ? 1 : 0;
if ($frameLength < $headerLength) {
$this->throwInvalidFrameSizeError();
}
$header = $headerLength === 0 ? '' : \substr($frameBuffer, 0, $headerLength);
$padding = $isPadded ? \ord($header[0]) : 0;
if ($streamId === 0) {
$this->throwInvalidZeroStreamIdError();
}
if ($frameLength - $headerLength - $padding < 0) {
$this->throwInvalidPaddingError();
}
$data = \substr($frameBuffer, $headerLength, $frameLength - $headerLength - $padding);
$this->handler->handleData($streamId, $data);
if ($frameFlags & self::END_STREAM) {
$this->handler->handleStreamEnd($streamId);
}
}
/** @see https://http2.github.io/http2-spec/#rfc.section.6.6 */
private function parsePushPromise(string $frameBuffer, int $frameLength, int $frameFlags, int $streamId) : void
{
$isPadded = $frameFlags & self::PADDED;
$headerLength = $isPadded ? 5 : 4;
if ($frameLength < $headerLength) {
$this->throwInvalidFrameSizeError();
}
$header = \substr($frameBuffer, 0, $headerLength);
$padding = $isPadded ? \ord($header[0]) : 0;
$pushId = \unpack("N", $header)[1] & 0x7fffffff;
if ($frameLength - $headerLength - $padding < 0) {
$this->throwInvalidPaddingError();
}
$this->headerFrameType = self::PUSH_PROMISE;
$this->pushHeaderBlockFragment($pushId, \substr($frameBuffer, $headerLength, $frameLength - $headerLength - $padding));
if ($frameFlags & self::END_HEADERS) {
$this->continuationExpected = \false;
[$pseudo, $headers] = $this->parseHeaderBuffer();
$this->handler->handlePushPromise($streamId, $pushId, $pseudo, $headers);
} else {
$this->continuationExpected = \true;
}
if ($frameFlags & self::END_STREAM) {
$this->handler->handleStreamEnd($streamId);
}
}
private function parseHeaderBuffer() : array
{
if ($this->headerStream === 0) {
throw new Http2ConnectionException('Invalid stream ID 0 for header block', self::PROTOCOL_ERROR);
}
if ($this->headerBuffer === '') {
throw new Http2StreamException('Invalid empty header section', $this->headerStream, self::PROTOCOL_ERROR);
}
$decoded = $this->hpack->decode($this->headerBuffer, $this->headerSizeLimit);
if ($decoded === null) {
throw new Http2ConnectionException("Compression error in headers", self::COMPRESSION_ERROR);
}
$headers = [];
$pseudo = [];
foreach ($decoded as [$name, $value]) {
if (!\preg_match(self::HEADER_NAME_REGEX, $name)) {
throw new Http2StreamException("Invalid header field name", $this->headerStream, self::PROTOCOL_ERROR);
}
if ($name[0] === ':') {
if (!empty($headers)) {
throw new Http2ConnectionException("Pseudo header after other headers", self::PROTOCOL_ERROR);
}
if (isset($pseudo[$name])) {
throw new Http2ConnectionException("Repeat pseudo header", self::PROTOCOL_ERROR);
}
$pseudo[$name] = $value;
continue;
}
$headers[$name][] = $value;
}
$this->headerBuffer = '';
$this->headerStream = 0;
return [$pseudo, $headers];
}
private function pushHeaderBlockFragment(int $streamId, string $buffer) : void
{
if ($this->headerStream !== 0 && $this->headerStream !== $streamId) {
throw new Http2ConnectionException("Expected CONTINUATION frame for stream ID " . $this->headerStream, self::PROTOCOL_ERROR);
}
$this->headerStream = $streamId;
$this->headerBuffer .= $buffer;
}
/** @see https://http2.github.io/http2-spec/#HEADERS */
private function parseHeaders(string $frameBuffer, int $frameLength, int $frameFlags, int $streamId) : void
{
if ($streamId === 0) {
$this->throwInvalidZeroStreamIdError();
}
$headerLength = 0;
$isPadded = $frameFlags & self::PADDED;
$isPriority = $frameFlags & self::PRIORITY_FLAG;
if ($isPadded) {
$headerLength++;
}
if ($isPriority) {
$headerLength += 5;
}
if ($frameLength < $headerLength) {
$this->throwInvalidFrameSizeError();
}
$header = \substr($frameBuffer, 0, $headerLength);
$padding = $isPadded ? \ord($header[0]) : 0;
if ($isPriority) {
['parent' => $parent, 'weight' => $weight] = \unpack("Nparent/cweight", $header, $isPadded ? 1 : 0);
$parent &= 0x7fffffff;
if ($parent === $streamId) {
$this->throwInvalidRecursiveDependency($streamId);
}
$this->handler->handlePriority($streamId, $parent, $weight + 1);
}
if ($frameLength - $headerLength - $padding < 0) {
$this->throwInvalidPaddingError();
}
$this->headerFrameType = self::HEADERS;
$this->pushHeaderBlockFragment($streamId, \substr($frameBuffer, $headerLength, $frameLength - $headerLength - $padding));
$ended = $frameFlags & self::END_STREAM;
if ($frameFlags & self::END_HEADERS) {
$this->continuationExpected = \false;
$headersTooLarge = \strlen($this->headerBuffer) > $this->headerSizeLimit;
[$pseudo, $headers] = $this->parseHeaderBuffer();
// This must happen after the parsing, otherwise we loose the connection state and must close the whole
// connection, which is not what we want here…
if ($headersTooLarge) {
throw new Http2StreamException("Headers exceed maximum configured size of {$this->headerSizeLimit} bytes", $streamId, self::ENHANCE_YOUR_CALM);
}
$this->handler->handleHeaders($streamId, $pseudo, $headers, $ended);
} else {
$this->continuationExpected = \true;
}
if ($ended) {
$this->handler->handleStreamEnd($streamId);
}
}
private function parsePriorityFrame(string $frameBuffer, int $frameLength, int $streamId) : void
{
if ($frameLength !== 5) {
$this->throwInvalidFrameSizeError();
}
['parent' => $parent, 'weight' => $weight] = \unpack("Nparent/cweight", $frameBuffer);
if ($exclusive = $parent & 0x80000000) {
$parent &= 0x7fffffff;
}
if ($streamId === 0) {
$this->throwInvalidZeroStreamIdError();
}
if ($parent === $streamId) {
$this->throwInvalidRecursiveDependency($streamId);
}
$this->handler->handlePriority($streamId, $parent, $weight + 1);
}
private function parseStreamReset(string $frameBuffer, int $frameLength, int $streamId) : void
{
if ($frameLength !== 4) {
$this->throwInvalidFrameSizeError();
}
if ($streamId === 0) {
$this->throwInvalidZeroStreamIdError();
}
$errorCode = \unpack('N', $frameBuffer)[1];
$this->handler->handleStreamReset($streamId, $errorCode);
}
private function parseSettings(string $frameBuffer, int $frameLength, int $frameFlags, int $streamId) : void
{
if ($streamId !== 0) {
$this->throwInvalidNonZeroStreamIdError();
}
if ($frameFlags & self::ACK) {
if ($frameLength) {
$this->throwInvalidFrameSizeError();
}
return;
// Got ACK, nothing to do
}
if ($frameLength % 6 !== 0) {
$this->throwInvalidFrameSizeError();
}
if ($frameLength > 60) {
// Even with room for a few future options, sending that a big SETTINGS frame is just about
// wasting our processing time. We declare this a protocol error.
throw new Http2ConnectionException("Excessive SETTINGS frame", self::PROTOCOL_ERROR);
}
$settings = [];
while ($frameLength > 0) {
['key' => $key, 'value' => $value] = \unpack("nkey/Nvalue", $frameBuffer);
if ($value < 0) {
throw new Http2ConnectionException("Invalid setting: {$value}", self::PROTOCOL_ERROR);
}
$settings[$key] = $value;
$frameBuffer = \substr($frameBuffer, 6);
$frameLength -= 6;
}
$this->handler->handleSettings($settings);
}
/** @see https://http2.github.io/http2-spec/#rfc.section.6.7 */
private function parsePing(string $frameBuffer, int $frameLength, int $frameFlags, int $streamId) : void
{
if ($frameLength !== 8) {
$this->throwInvalidFrameSizeError();
}
if ($streamId !== 0) {
$this->throwInvalidNonZeroStreamIdError();
}
if ($frameFlags & self::ACK) {
$this->handler->handlePong($frameBuffer);
} else {
$this->handler->handlePing($frameBuffer);
}
}
/** @see https://http2.github.io/http2-spec/#rfc.section.6.8 */
private function parseGoAway(string $frameBuffer, int $frameLength, int $streamId) : void
{
if ($frameLength < 8) {
$this->throwInvalidFrameSizeError();
}
if ($streamId !== 0) {
$this->throwInvalidNonZeroStreamIdError();
}
['last' => $lastId, 'error' => $error] = \unpack("Nlast/Nerror", $frameBuffer);
$this->handler->handleShutdown($lastId & 0x7fffffff, $error);
}
/** @see https://http2.github.io/http2-spec/#rfc.section.6.9 */
private function parseWindowUpdate(string $frameBuffer, int $frameLength, int $streamId) : void
{
if ($frameLength !== 4) {
$this->throwInvalidFrameSizeError();
}
$windowSize = \unpack('N', $frameBuffer)[1];
if ($windowSize === 0) {
if ($streamId) {
throw new Http2StreamException("Invalid zero window update value", $streamId, self::PROTOCOL_ERROR);
}
throw new Http2ConnectionException("Invalid zero window update value", self::PROTOCOL_ERROR);
}
if ($streamId) {
$this->handler->handleStreamWindowIncrement($streamId, $windowSize);
} else {
$this->handler->handleConnectionWindowIncrement($windowSize);
}
}
/** @see https://http2.github.io/http2-spec/#rfc.section.6.10 */
private function parseContinuation(string $frameBuffer, int $frameFlags, int $streamId) : void
{
if ($streamId !== $this->headerStream) {
throw new Http2ConnectionException("Invalid CONTINUATION frame stream ID", self::PROTOCOL_ERROR);
}
if ($this->headerBuffer === '') {
throw new Http2ConnectionException("Unexpected CONTINUATION frame for stream ID " . $this->headerStream, self::PROTOCOL_ERROR);
}
$this->pushHeaderBlockFragment($streamId, $frameBuffer);
$ended = $frameFlags & self::END_STREAM;
if ($frameFlags & self::END_HEADERS) {
$this->continuationExpected = \false;
$isPush = $this->headerFrameType === self::PUSH_PROMISE;
$pushId = $this->headerStream;
[$pseudo, $headers] = $this->parseHeaderBuffer();
if ($isPush) {
$this->handler->handlePushPromise($streamId, $pushId, $pseudo, $headers);
} else {
$this->handler->handleHeaders($streamId, $pseudo, $headers, $ended);
}
}
if ($ended) {
$this->handler->handleStreamEnd($streamId);
}
}
private function throwInvalidFrameSizeError() : void
{
throw new Http2ConnectionException("Invalid frame length", self::PROTOCOL_ERROR);
}
private function throwInvalidRecursiveDependency(int $streamId) : void
{
throw new Http2ConnectionException("Invalid recursive dependency for stream {$streamId}", self::PROTOCOL_ERROR);
}
private function throwInvalidPaddingError() : void
{
throw new Http2ConnectionException("Padding greater than length", self::PROTOCOL_ERROR);
}
private function throwInvalidZeroStreamIdError() : void
{
throw new Http2ConnectionException("Invalid zero stream ID", self::PROTOCOL_ERROR);
}
private function throwInvalidNonZeroStreamIdError() : void
{
throw new Http2ConnectionException("Invalid non-zero stream ID", self::PROTOCOL_ERROR);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Http\Http2;
interface Http2Processor
{
public function handlePong(string $data) : void;
public function handlePing(string $data) : void;
public function handleShutdown(int $lastId, int $error) : void;
public function handleStreamWindowIncrement(int $streamId, int $windowSize) : void;
public function handleConnectionWindowIncrement(int $windowSize) : void;
public function handleHeaders(int $streamId, array $pseudo, array $headers, bool $streamEnded) : void;
public function handlePushPromise(int $streamId, int $pushId, array $pseudo, array $headers) : void;
public function handlePriority(int $streamId, int $parentId, int $weight) : void;
public function handleStreamReset(int $streamId, int $errorCode) : void;
public function handleStreamException(Http2StreamException $exception) : void;
public function handleConnectionException(Http2ConnectionException $exception) : void;
public function handleData(int $streamId, string $data) : void;
public function handleSettings(array $settings) : void;
public function handleStreamEnd(int $streamId) : void;
}

View File

@ -0,0 +1,18 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Http\Http2;
final class Http2StreamException extends \Exception
{
/** @var int */
private $streamId;
public function __construct(string $message, int $streamId, int $code, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->streamId = $streamId;
}
public function getStreamId() : int
{
return $this->streamId;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Http;
final class InvalidHeaderException extends \Exception
{
/**
* Thrown on header injection attempts.
*
* @param string $reason Reason that can be used as HTTP response reason.
*/
public function __construct(string $reason)
{
parent::__construct($reason);
}
}

174
dependencies/amphp/http/src/Message.php vendored Normal file
View File

@ -0,0 +1,174 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Http;
/**
* Base class for HTTP request and response messages.
*/
abstract class Message
{
private const HEADER_LOWER = ['Accept' => 'accept', 'accept' => 'accept', 'Accept-Encoding' => 'accept-encoding', 'accept-encoding' => 'accept-encoding', 'Accept-Language' => 'accept-language', 'accept-language' => 'accept-language', 'Connection' => 'connection', 'connection' => 'connection', 'Cookie' => 'cookie', 'cookie' => 'cookie', 'Host' => 'host', 'host' => 'host', 'Sec-Fetch-Dest' => 'sec-fetch-dest', 'sec-fetch-dest' => 'sec-fetch-dest', 'Sec-Fetch-Mode' => 'sec-fetch-mode', 'sec-fetch-mode' => 'sec-fetch-mode', 'Sec-Fetch-Site' => 'sec-fetch-site', 'sec-fetch-site' => 'sec-fetch-site', 'Sec-Fetch-User' => 'sec-fetch-user', 'sec-fetch-user' => 'sec-fetch-user', 'Upgrade-Insecure-Requests' => 'upgrade-insecure-requests', 'upgrade-insecure-requests' => 'upgrade-insecure-requests', 'User-Agent' => 'user-agent', 'user-agent' => 'user-agent'];
/** @var string[][] */
private $headers = [];
/** @var string[][] */
private $headerCase = [];
/**
* Returns the headers as a string-indexed array of arrays of strings or an empty array if no headers
* have been set.
*
* @return string[][]
*/
public function getHeaders() : array
{
return $this->headers;
}
/**
* Returns the headers as list of [field, name] pairs in the original casing provided by the application or server.
*/
public final function getRawHeaders() : array
{
$headers = [];
foreach ($this->headers as $lcName => $values) {
$size = \count($values);
for ($i = 0; $i < $size; $i++) {
$headers[] = [$this->headerCase[$lcName][$i], $values[$i]];
}
}
return $headers;
}
/**
* Returns the array of values for the given header or an empty array if the header does not exist.
*
* @return string[]
*/
public function getHeaderArray(string $name) : array
{
return $this->headers[self::HEADER_LOWER[$name] ?? \strtolower($name)] ?? [];
}
/**
* Returns the value of the given header. If multiple headers are present for the named header, only the first
* header value will be returned. Use getHeaderArray() to return an array of all values for the particular header.
* Returns null if the header does not exist.
*
* @return string|null
*/
public function getHeader(string $name)
{
return $this->headers[self::HEADER_LOWER[$name] ?? \strtolower($name)][0] ?? null;
}
/**
* Sets the headers from the given array.
*
* @param string[]|string[][] $headers
*/
protected function setHeaders(array $headers)
{
// Ensure this is an atomic operation, either all headers are set or none.
$before = $this->headers;
$beforeCase = $this->headerCase;
try {
foreach ($headers as $name => $value) {
$this->setHeader($name, $value);
}
} catch (\Throwable $e) {
$this->headers = $before;
$this->headerCase = $beforeCase;
throw $e;
}
}
/**
* Sets the named header to the given value.
*
* @param string|string[] $value
*
* @throws \Error If the header name or value is invalid.
*/
protected function setHeader(string $name, $value)
{
\assert($this->isNameValid($name), "Invalid header name");
if (\is_array($value)) {
if (!$value) {
$this->removeHeader($name);
return;
}
$value = \array_values(\array_map("strval", $value));
} else {
$value = [(string) $value];
}
\assert($this->isValueValid($value), "Invalid header value");
$lcName = self::HEADER_LOWER[$name] ?? \strtolower($name);
$this->headers[$lcName] = $value;
$this->headerCase[$lcName] = [];
foreach ($value as $_) {
$this->headerCase[$lcName][] = $name;
}
}
/**
* Adds the value to the named header, or creates the header with the given value if it did not exist.
*
* @param string|string[] $value
*
* @throws \Error If the header name or value is invalid.
*/
protected function addHeader(string $name, $value)
{
\assert($this->isNameValid($name), "Invalid header name");
if (\is_array($value)) {
if (!$value) {
return;
}
$value = \array_values(\array_map("strval", $value));
} else {
$value = [(string) $value];
}
\assert($this->isValueValid($value), "Invalid header value");
$lcName = self::HEADER_LOWER[$name] ?? \strtolower($name);
if (isset($this->headers[$lcName])) {
$this->headers[$lcName] = \array_merge($this->headers[$lcName], $value);
foreach ($value as $_) {
$this->headerCase[$lcName][] = $name;
}
} else {
$this->headers[$lcName] = $value;
foreach ($value as $_) {
$this->headerCase[$lcName][] = $name;
}
}
}
/**
* Removes the given header if it exists.
*/
protected function removeHeader(string $name)
{
$lcName = self::HEADER_LOWER[$name] ?? \strtolower($name);
unset($this->headers[$lcName], $this->headerCase[$lcName]);
}
/**
* Checks if given header exists.
*/
public function hasHeader(string $name) : bool
{
return isset($this->headers[self::HEADER_LOWER[$name] ?? \strtolower($name)]);
}
private function isNameValid(string $name) : bool
{
return (bool) \preg_match('/^[A-Za-z0-9`~!#$%^&_|\'\\-*+.]+$/', $name);
}
/**
* Determines if the given value is a valid header value.
*
* @param string[] $values
*
* @throws \Error If the given value cannot be converted to a string and is not an array of values that can be
* converted to strings.
*/
private function isValueValid(array $values) : bool
{
foreach ($values as $value) {
if (\preg_match("/[^\t\r\n -~\x80-\xfe]|\r\n/", $value)) {
return \false;
}
}
return \true;
}
}

121
dependencies/amphp/http/src/Rfc7230.php vendored Normal file
View File

@ -0,0 +1,121 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Http;
/**
* @link https://tools.ietf.org/html/rfc7230
* @link https://tools.ietf.org/html/rfc2616
* @link https://tools.ietf.org/html/rfc5234
*/
final class Rfc7230
{
// We make use of possessive modifiers, which gives a slight performance boost
public const HEADER_NAME_REGEX = "(^([^()<>@,;:\\\"/[\\]?={}\x01- ]++)\$)";
public const HEADER_VALUE_REGEX = "(^[ \t]*+((?:[ \t]*+[!-~\x80-\xff]++)*+)[ \t]*+\$)";
public const HEADER_REGEX = "(^([^()<>@,;:\\\"/[\\]?={}\x01- ]++):[ \t]*+((?:[ \t]*+[!-~\x80-\xff]++)*+)[ \t]*+\r\n)m";
public const HEADER_FOLD_REGEX = "(\r\n[ \t]++)";
/**
* Parses headers according to RFC 7230 and 2616.
*
* Allows empty header values, as HTTP/1.0 allows that.
*
* @return array Associative array mapping header names to arrays of values.
*
* @throws InvalidHeaderException If invalid headers have been passed.
*/
public static function parseHeaders(string $rawHeaders) : array
{
$headers = [];
foreach (self::parseRawHeaders($rawHeaders) as $header) {
// Unfortunately, we can't avoid the \strtolower() calls due to \array_change_key_case() behavior
// when equal headers are present with different casing, e.g. 'set-cookie' and 'Set-Cookie'.
// Accessing headers directly instead of using foreach (... as list(...)) is slightly faster.
$headers[\strtolower($header[0])][] = $header[1];
}
return $headers;
}
/**
* Parses headers according to RFC 7230 and 2616.
*
* Allows empty header values, as HTTP/1.0 allows that.
*
* @return array List of [field, value] header pairs.
*
* @throws InvalidHeaderException If invalid headers have been passed.
*/
public static function parseRawHeaders(string $rawHeaders) : array
{
// Ensure that the last line also ends with a newline, this is important.
\assert(\substr($rawHeaders, -2) === "\r\n", "Argument 1 must end with CRLF: " . \bin2hex($rawHeaders));
/** @var array[] $matches */
$count = \preg_match_all(self::HEADER_REGEX, $rawHeaders, $matches, \PREG_SET_ORDER);
// If these aren't the same, then one line didn't match and there's an invalid header.
if ($count !== \substr_count($rawHeaders, "\n")) {
// Folding is deprecated, see https://tools.ietf.org/html/rfc7230#section-3.2.4
if (\preg_match(self::HEADER_FOLD_REGEX, $rawHeaders)) {
throw new InvalidHeaderException("Invalid header syntax: Obsolete line folding");
}
throw new InvalidHeaderException("Invalid header syntax");
}
$headers = [];
foreach ($matches as $match) {
// We avoid a call to \trim() here due to the regex.
// Accessing matches directly instead of using foreach (... as list(...)) is slightly faster.
$headers[] = [$match[1], $match[2]];
}
return $headers;
}
/**
* Format headers in to their on-the-wire format.
*
* Headers are always validated syntactically. This protects against response splitting and header injection
* attacks.
*
* @param array $headers Headers in a format as returned by {@see parseHeaders()}.
*
* @return string Formatted headers.
*
* @throws InvalidHeaderException If header names or values are invalid.
*/
public static function formatHeaders(array $headers) : string
{
$headerList = [];
foreach ($headers as $name => $values) {
foreach ($values as $value) {
// PHP casts integer-like keys to integers
$headerList[] = [(string) $name, $value];
}
}
return self::formatRawHeaders($headerList);
}
/**
* Format headers in to their on-the-wire HTTP/1 format.
*
* Headers are always validated syntactically. This protects against response splitting and header injection
* attacks.
*
* @param array $headers List of headers in [field, value] format as returned by {@see Message::getRawHeaders()}.
*
* @return string Formatted headers.
*
* @throws InvalidHeaderException If header names or values are invalid.
*/
public static function formatRawHeaders(array $headers) : string
{
$buffer = "";
$lines = 0;
foreach ($headers as [$name, $value]) {
// Ignore any HTTP/2 pseudo headers
if (($name[0] ?? '') === ':') {
continue;
}
$buffer .= "{$name}: {$value}\r\n";
$lines++;
}
$count = \preg_match_all(self::HEADER_REGEX, $buffer);
if ($lines !== $count || $lines !== \substr_count($buffer, "\n")) {
throw new InvalidHeaderException("Invalid headers");
}
return $buffer;
}
}

80
dependencies/amphp/http/src/Status.php vendored Normal file
View File

@ -0,0 +1,80 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Http;
/**
* @link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
*/
final class Status
{
const CONTINUE = 100;
const SWITCHING_PROTOCOLS = 101;
const PROCESSING = 102;
const EARLY_HINTS = 103;
const OK = 200;
const CREATED = 201;
const ACCEPTED = 202;
const NON_AUTHORITATIVE_INFORMATION = 203;
const NO_CONTENT = 204;
const RESET_CONTENT = 205;
const PARTIAL_CONTENT = 206;
const MULTI_STATUS = 207;
const ALREADY_REPORTED = 208;
const IM_USED = 226;
const MULTIPLE_CHOICES = 300;
const MOVED_PERMANENTLY = 301;
const FOUND = 302;
const SEE_OTHER = 303;
const NOT_MODIFIED = 304;
const USE_PROXY = 305;
const TEMPORARY_REDIRECT = 307;
const PERMANENT_REDIRECT = 308;
const BAD_REQUEST = 400;
const UNAUTHORIZED = 401;
const PAYMENT_REQUIRED = 402;
const FORBIDDEN = 403;
const NOT_FOUND = 404;
const METHOD_NOT_ALLOWED = 405;
const NOT_ACCEPTABLE = 406;
const PROXY_AUTHENTICATION_REQUIRED = 407;
const REQUEST_TIMEOUT = 408;
const CONFLICT = 409;
const GONE = 410;
const LENGTH_REQUIRED = 411;
const PRECONDITION_FAILED = 412;
const PAYLOAD_TOO_LARGE = 413;
const URI_TOO_LONG = 414;
const UNSUPPORTED_MEDIA_TYPE = 415;
const RANGE_NOT_SATISFIABLE = 416;
const EXPECTATION_FAILED = 417;
const MISDIRECTED_REQUEST = 421;
const UNPROCESSABLE_ENTITY = 422;
const LOCKED = 423;
const FAILED_DEPENDENCY = 424;
const UPGRADE_REQUIRED = 426;
const PRECONDITION_REQUIRED = 428;
const TOO_MANY_REQUESTS = 429;
const REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
const UNAVAILABLE_FOR_LEGAL_REASONS = 451;
const INTERNAL_SERVER_ERROR = 500;
const NOT_IMPLEMENTED = 501;
const BAD_GATEWAY = 502;
const SERVICE_UNAVAILABLE = 503;
const GATEWAY_TIMEOUT = 504;
const HTTP_VERSION_NOT_SUPPORTED = 505;
const VARIANT_ALSO_NEGOTIATES = 506;
const INSUFFICIENT_STORAGE = 507;
const LOOP_DETECTED = 508;
const NOT_EXTENDED = 510;
const NETWORK_AUTHENTICATION_REQUIRED = 511;
// @codeCoverageIgnoreStart
private function __construct()
{
// forbid instances
}
// @codeCoverageIgnoreEnd
public static function getReason(int $code) : string
{
return [100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', 103 => 'Early Hints', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-Status', 208 => 'Already Reported', 226 => 'IM Used', 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 307 => 'Temporary Redirect', 308 => 'Permanent Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Timeout', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Payload Too Large', 414 => 'URI Too Long', 415 => 'Unsupported Media Type', 416 => 'Range Not Satisfiable', 417 => 'Expectation Failed', 421 => 'Misdirected Request', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons', 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 510 => 'Not Extended', 511 => 'Network Authentication Required'][$code] ?? '';
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Http;
/**
* @see https://tools.ietf.org/html/rfc7230#section-3.2.6
*/
function parseFieldValueComponents(Message $message, string $headerName) : ?array
{
$header = \implode(', ', $message->getHeaderArray($headerName));
if ($header === '') {
return [];
}
\preg_match_all('(([^"=,]+)(?:=(?:"((?:[^\\\\"]|\\\\.)*)"|([^,"]*)))?,?\\s*)', $header, $matches, \PREG_SET_ORDER);
$totalMatchedLength = 0;
$pairs = [];
foreach ($matches as $match) {
$totalMatchedLength += \strlen($match[0]);
$key = \trim($match[1]);
$value = ($match[2] ?? '') . \trim($match[3] ?? '');
if (($match[2] ?? '') !== '') {
// decode escaped characters
$value = \preg_replace('(\\\\(.))', '\\1', $value);
}
$pairs[] = [$key, $value];
}
if ($totalMatchedLength !== \strlen($header)) {
return null;
// parse error
}
return $pairs;
}
/**
* @param array $pairs Output of {@code parseFieldValueComponents}. Keys are handled case-insensitively.
*
* @return array|null Map of keys to values or {@code null} if incompatible duplicates are found.
*/
function createFieldValueComponentMap(?array $pairs) : ?array
{
if ($pairs === null) {
return null;
}
$map = [];
foreach ($pairs as $pair) {
\assert(\count($pair) === 2);
\assert(\is_string($pair[0]));
\assert(\is_string($pair[1]));
}
foreach ($pairs as [$key, $value]) {
$key = \strtolower($key);
if (isset($map[$key]) && $map[$key] !== $value) {
return null;
// incompatible duplicates
}
$map[$key] = $value;
}
return $map;
}
/**
* Format timestamp in seconds as an HTTP date header.
*
* @param int|null $timestamp Timestamp to format, current time if `null`.
*
* @return string Formatted date header value.
*/
function formatDateHeader(?int $timestamp = null) : string
{
static $cachedTimestamp, $cachedFormattedDate;
$timestamp = $timestamp ?? \time();
if ($cachedTimestamp === $timestamp) {
return $cachedFormattedDate;
}
return $cachedFormattedDate = \gmdate("D, d M Y H:i:s", $cachedTimestamp = $timestamp) . " GMT";
}