[ ':authority', null], 2 => [ ':method', 'GET' ], 3 => [ ':method', 'POST'], 4 => [ ':path', '/'], 5 => [ ':path', '/index.html'], 6 => [ ':scheme', 'http'], 7 => [ ':scheme', 'https'], 8 => [ ':status', '200'], 9 => [ ':status', '204'], 10 => [ ':status', '206'], 11 => [ ':status', '304'], 12 => [ ':status', '400'], 13 => [ ':status', '404'], 14 => [ ':status', '500'], 15 => [ 'accept-charset', null], 16 => [ 'accept-encoding', 'gzip, deflate'], 17 => [ 'accept-language', null], 18 => [ 'accept-ranges', null], 19 => [ 'accept', null ], 20 => [ 'access-control-allow-origin', null], 21 => [ 'age', null], 22 => [ 'allow', null], 23 => [ 'authorization', null], 24 => [ 'cache-control', null], 25 => [ 'content-disposition', null], 26 => [ 'content-encoding', null], 27 => [ 'content-language', null], 28 => [ 'content-length', null], 29 => [ 'content-location', null], 30 => [ 'content-range', null], 31 => [ 'content-type', null], 32 => [ 'cookie', null], 33 => [ 'date', null], 34 => [ 'etag', null], 35 => [ 'expect', null], 36 => [ 'expires', null], 37 => [ 'from', null], 38 => [ 'host', null], 39 => [ 'if-match', null], 40 => [ 'if-modified-since', null], 41 => [ 'if-none-match', null], 42 => [ 'if-range', null], 43 => [ 'if-unmodified-since', null], 44 => [ 'last-modified', null], 45 => [ 'link', null], 46 => [ 'location', null], 47 => [ 'max-forwards', null], 48 => [ 'proxy-authenticate', null], 49 => [ 'proxy-authorization', null], 50 => [ 'range', null], 51 => [ 'referer', null], 52 => [ 'refresh', null], 53 => [ 'retry-after', null], 54 => [ 'server', null], 55 => [ 'set-cookie', null], 56 => [ 'strict-transport-security', null], 57 => [ 'transfer-encoding', null], 58 => [ 'user-agent', null], 59 => [ 'vary', null], 60 => [ 'via', null], 61 => [ 'www-authenticate', null], ]; private const STATIC_TABLE_SIZE = 62; // considering unused index 0 private array $dynamicTable = []; public function __construct() { if (!self::$huffman) self::$huffman = new Codec(Dictionary::createRfc7541Dictionary()); } /** * Pack a HeaderBag into a binary string. * * @param HeaderBag $headers * @return string */ public function packHeaders(HeaderBag $headers): string { $packed = ''; foreach ($headers as [$header,$value]) { if ($index = $this->lookupIndexForHeader($header,$value)) { // Indexed $packed .= chr(0x80 | ($index & 0x7F)); } elseif ($index = $this->lookupIndexForHeader($header,null)) { // Indexed literal $packed .= chr(0x40 | ($index & 0x3F)); $value = self::$huffman->encode($value); $packed .= chr(0x80 | strlen($value) & 0x7F); $packed .= $value; } else { // Literal // TODO pack literal } } return $packed; } /** * Unpack a binary string to a HeaderBag. * * @param string $raw * @return HeaderBag */ public function unpackHeaders(string $raw): HeaderBag { $headers = new HeaderBag(); $bytes = array_map("ord", str_split($raw)); while (count($bytes) > 0) { $head = array_shift($bytes); if ($head & 0x80) { // Indexed field [$header,$value] = $this->getIndexedHeader($head & 0x7F); $headers->append($header,$value); } elseif ($head & 0x40) { // Indexed with literal [$header,$value] = $this->getIndexedHeader($head & 0x3F); $valueHead = array_shift($bytes); $encoded = (bool)($valueHead & 0x80); $length = ($valueHead & 0x7F); while ($length-- > 0) { $value .= chr(array_shift($bytes)); } if ($encoded) { $value = self::$huffman->decode($value); } $headers->append($header,$value); } else { // Literal field $valueHead = array_shift($bytes); $encoded = (bool)($valueHead & 0x80); $length = ($valueHead & 0x7F); $header = ''; while ($length-- > 0) { $header .= chr(array_shift($bytes)); } if ($encoded) { $header = self::$huffman->decode($header); } $valueHead = array_shift($bytes); $encoded = (bool)($valueHead & 0x80); $length = ($valueHead & 0x7F); $value = ''; while ($length-- > 0) { $value .= chr(array_shift($bytes)); } if ($encoded) { $value = self::$huffman->decode($value); } $headers->append($header,$value); } } return $headers; } private function getIndexedHeader(int $index): array { if ($index < self::STATIC_TABLE_SIZE) return self::$staticTable[$index]; if ($index - self::STATIC_TABLE_SIZE >= count($this->dynamicTable)) { // TODO throw exception } return $this->dynamicTable[$index - self::STATIC_TABLE_SIZE]; } private function lookupIndexForHeader(string $header, ?string $value): ?int { foreach (self::$staticTable as $index=>[$iName, $iValue]) { if ($header === $iName && $value === $iValue) return $index; } foreach ($this->dynamicTable as $index=>[$iName, $iValue]) { if ($header === $iName && $value === $iValue) return $index + self::STATIC_TABLE_SIZE; } return null; } }