public function parser(Client $client, $settings = "") : \Generator { $maxHeaderSize = $client->options->maxHeaderSize; $maxBodySize = $client->options->maxBodySize; $maxStreams = $client->options->maxConcurrentStreams; // $bodyEmitSize = $client->options->ioGranularity; // redundant because data frames, which is 16 KB assert(!\defined("Aerys\\DEBUG_HTTP2") || (print "INIT\n")); $this->writeFrame($client, pack("nNnN", self::INITIAL_WINDOW_SIZE, $maxBodySize + 256, self::MAX_CONCURRENT_STREAMS, $maxStreams), self::SETTINGS, self::NOFLAG); $this->writeFrame($client, "þÿÿ", self::WINDOW_UPDATE, self::NOFLAG); // effectively disabling global flow control... $headers = []; $bodyLens = []; $table = new HPack(); $setSetting = function ($buffer) use($client, $table) { $unpacked = \unpack("nsetting/Nvalue", $buffer); // $unpacked["value"] >= 0 assert(!defined("Aerys\\DEBUG_HTTP2") || (print "SETTINGS({$unpacked["setting"]}): {$unpacked["value"]}\n")); switch ($unpacked["setting"]) { case self::MAX_HEADER_LIST_SIZE: if ($unpacked["value"] >= 4096) { return self::PROTOCOL_ERROR; // @TODO correct error?? } $table->table_resize($unpacked["value"]); break; case self::INITIAL_WINDOW_SIZE: if ($unpacked["value"] >= 1 << 31) { return self::FLOW_CONTROL_ERROR; } $client->initialWindowSize = $unpacked["value"]; break; case self::ENABLE_PUSH: if ($unpacked["value"] & ~1) { return self::PROTOCOL_ERROR; } $client->allowsPush = (bool) $unpacked["value"]; break; } }; if ($settings != "") { if (\strlen($settings) % 6 != 0) { $error = self::FRAME_SIZE_ERROR; goto connection_error; } do { if ($error = $setSetting($settings)) { goto connection_error; } $settings = substr($settings, 6); } while ($settings != ""); } $buffer = yield; $preface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"; while (\strlen($buffer) < \strlen($preface)) { $buffer .= yield; } if (\strncmp($buffer, $preface, \strlen($preface)) !== 0) { $start = \strpos($buffer, "HTTP/") + 5; if ($start < \strlen($buffer)) { ($this->emit)([HttpDriver::ERROR, ["protocol" => \substr($buffer, $start, \strpos($buffer, "\r\n", $start) - $start)], HttpDriver::BAD_VERSION], $client); } while (1) { yield; } } $buffer = \substr($buffer, \strlen($preface)); $client->remainingKeepAlives = $maxStreams; while (1) { while (\strlen($buffer) < 9) { $buffer .= yield; } $length = \unpack("N", "{$buffer}")[1]; // @TODO SETTINGS: MAX_FRAME_SIZE $type = $buffer[3]; $flags = $buffer[4]; $id = \unpack("N", substr($buffer, 5, 4))[1]; // the highest bit must be zero... but RFC does not specify what should happen when it is set to 1? /*if ($id < 0) { $id = ~$id; }*/ assert(!\defined("Aerys\\DEBUG_HTTP2") || (print "Flag: " . bin2hex($flags) . "; Type: " . bin2hex($type) . "; Stream: {$id}; Length: {$length}\n")); $buffer = \substr($buffer, 9); switch ($type) { case self::DATA: if ($id === 0) { $error = self::PROTOCOL_ERROR; goto connection_error; } if (!isset($client->bodyPromisors[$id])) { if (isset($headers[$id])) { $error = self::PROTOCOL_ERROR; goto connection_error; } else { $error = self::STREAM_CLOSED; while (\strlen($buffer) < $length) { $buffer .= yield; } $buffer = \substr($buffer, $length); goto stream_error; } } if (($flags & self::PADDED) !== "") { if ($buffer === "") { $buffer = yield; } $padding = \ord($buffer); $buffer = \substr($buffer, 1); $length--; if ($padding > $length) { $error = self::PROTOCOL_ERROR; goto connection_error; } } else { $padding = 0; } if ($length > 1 << 14) { $error = self::PROTOCOL_ERROR; goto connection_error; } if (($bodyLens[$id] ?? 0) + $length > $maxBodySize) { $error = self::ENHANCE_YOUR_CALM; while ($length) { $buffer = yield; $length -= \strlen($buffer); } $buffer = substr($buffer, \strlen($buffer) + $length); goto stream_error; } while (\strlen($buffer) < $length) { $buffer .= yield; } if (($flags & self::END_STREAM) !== "") { $bodyLens[$id] = $length; $type = HttpDriver::ENTITY_RESULT; } else { unset($bodyLens[$id]); $type = HttpDriver::ENTITY_PART; } $body = \substr($buffer, 0, $length - $padding); assert(!\defined("Aerys\\DEBUG_HTTP2") || (print "DATA({$length}): {$body}\n")); if ($body != "") { ($this->emit)([$type, ["id" => $id, "protocol" => "2.0", "body" => $body], null], $client); } $buffer = \substr($buffer, $length); continue 2; case self::HEADERS: if (isset($headers[$id])) { $error = self::PROTOCOL_ERROR; goto connection_error; } if ($client->remainingKeepAlives-- == 0) { $error = self::PROTOCOL_ERROR; goto connection_error; } if (($flags & self::PADDED) !== "") { if ($buffer == "") { $buffer = yield; } $padding = \ord($buffer); $buffer = \substr($buffer, 1); $length--; } else { $padding = 0; } if (($flags & self::PRIORITY_FLAG) !== "") { while (\strlen($buffer) < 5) { $buffer .= yield; } /* Not yet needed?! $dependency = unpack("N", $buffer)[1]; if ($dependency < 0) { $dependency = ~$dependency; $exclusive = true; } else { $exclusive = false; } $weight = $buffer[4]; */ $buffer = \substr($buffer, 5); $length -= 5; } if ($padding >= $length) { $error = self::PROTOCOL_ERROR; goto connection_error; } if ($length > $maxHeaderSize) { $error = self::ENHANCE_YOUR_CALM; while ($length) { $buffer = yield; $length -= \strlen($buffer); } $buffer = substr($buffer, \strlen($buffer) + $length); goto stream_error; } while (\strlen($buffer) < $length) { $buffer .= yield; } $packed = \substr($buffer, 0, $length - $padding); $buffer = \substr($buffer, $length); $streamEnd = ($flags & self::END_STREAM) !== ""; if (($flags & self::END_HEADERS) != "") { goto parse_headers; } else { $headers[$id] = $packed; } continue 2; case self::PRIORITY: if ($length != 5) { $error = self::FRAME_SIZE_ERROR; while (\strlen($buffer) < $length) { $buffer .= yield; } $buffer = substr($buffer, $length); goto stream_error; } if ($id === 0) { $error = self::PROTOCOL_ERROR; goto connection_error; } while (\strlen($buffer) < 5) { $buffer .= yield; } /* @TODO PRIORITY frames not yet handled?! $dependency = unpack("N", $buffer); if ($dependency < 0) { $dependency = ~$dependency; $exclusive = true; } else { $exclusive = false; } if ($dependency == 0) { $error = self::PROTOCOL_ERROR; goto connection_error; } $weight = $buffer[4]; */ $buffer = \substr($buffer, 5); assert(!defined("Aerys\\DEBUG_HTTP2") || (print "PRIORITY: - \n")); continue 2; case self::RST_STREAM: if ($length != 4) { $error = self::FRAME_SIZE_ERROR; goto connection_error; } if ($id === 0) { $error = self::PROTOCOL_ERROR; goto connection_error; } while (\strlen($buffer) < 4) { $buffer .= yield; } $error = \unpack("N", $buffer)[1]; assert(!defined("Aerys\\DEBUG_HTTP2") || (print "RST_STREAM: {$error}\n")); if (isset($client->bodyPromisors[$id])) { $client->bodyPromisors[$id]->fail(new ClientException()); unset($client->bodyPromisors[$id]); } unset($headers[$id], $bodyLens[$id], $client->streamWindow[$id], $client->streamEnd[$id], $client->streamWindowBuffer[$id]); $buffer = \substr($buffer, 4); continue 2; case self::SETTINGS: if ($id !== 0) { $error = self::PROTOCOL_ERROR; goto connection_error; } if (($flags & self::ACK) !== "") { if ($length) { $error = self::PROTOCOL_ERROR; goto connection_error; } assert(!defined("Aerys\\DEBUG_HTTP2") || (print "SETTINGS: ACK\n")); // got ACK continue 2; } elseif ($length % 6 != 0) { $error = self::FRAME_SIZE_ERROR; goto connection_error; } while ($length > 0) { while (\strlen($buffer) < 6) { $buffer .= yield; } if ($error = $setSetting($buffer)) { goto connection_error; } $buffer = \substr($buffer, 6); $length -= 6; } $this->writeFrame($client, "", self::SETTINGS, self::ACK); continue 2; // PUSH_PROMISE sent by client is a PROTOCOL_ERROR (just like undefined frame types) // PUSH_PROMISE sent by client is a PROTOCOL_ERROR (just like undefined frame types) case self::PING: if ($length != 8) { $error = self::FRAME_SIZE_ERROR; goto connection_error; } if ($id !== 0) { $error = self::PROTOCOL_ERROR; goto connection_error; } while (\strlen($buffer) < 8) { $buffer .= yield; } $data = \substr($buffer, 0, 8); if (($flags & self::ACK) !== "") { // do not resolve ping - unneeded because of keepAliveTimeout } else { $this->writeFrame($client, $data, self::PING, self::ACK); } $buffer = \substr($buffer, 8); continue 2; case self::GOAWAY: if ($id !== 0) { $error = self::PROTOCOL_ERROR; goto connection_error; } $lastId = \unpack("N", $buffer)[1]; // the highest bit must be zero... but RFC does not specify what should happen when it is set to 1? if ($lastId < 0) { $lastId = ~$lastId; } $error = \unpack("N", substr($buffer, 4, 4))[1]; $buffer = \substr($buffer, 8); $length -= 8; while (\strlen($buffer) < $length) { $buffer .= yield; } if ($error !== 0) { // ($this->emit)([HttpDriver::ERROR, ["body" => substr($buffer, 0, $length)], $error], $client); } assert(!defined("Aerys\\DEBUG_HTTP2") || (print "GOAWAY({$error}): " . substr($buffer, 0, $length) . "\n")); $client->shouldClose = true; while (1) { yield; } // keepAliveTimeout will force a close when necessary // keepAliveTimeout will force a close when necessary case self::WINDOW_UPDATE: while (\strlen($buffer) < 4) { $buffer .= yield; } if ($buffer === "") { $error = self::PROTOCOL_ERROR; if ($id) { goto stream_error; } else { goto connection_error; } } $windowSize = \unpack("N", $buffer)[1]; if ($id) { if (!isset($client->streamWindow[$id])) { $client->streamWindow[$id] = $client->initialWindowSize + $windowSize; } else { $client->streamWindow[$id] += $windowSize; } if (isset($client->streamWindowBuffer[$id])) { $this->tryDataSend($client, $id); } } else { $client->window += $windowSize; foreach ($client->streamWindowBuffer as $stream => $data) { $this->tryDataSend($client, $stream); if ($client->window == 0) { break; } } } $buffer = \substr($buffer, 4); continue 2; case self::CONTINUATION: if (!isset($headers[$id])) { $error = self::PROTOCOL_ERROR; goto connection_error; } while (\strlen($buffer) < $length) { $buffer .= yield; } $headers[$id] .= \substr($buffer, 0, $length); $buffer = \substr($buffer, $length); if (($flags & self::END_HEADERS) !== "") { $packed = $headers[$id]; unset($headers[$id]); goto parse_headers; } continue 2; default: assert(!defined("Aerys\\DEBUG_HTTP2") || (print "BAD TYPE: " . ord($type) . "\n")); $error = self::PROTOCOL_ERROR; goto connection_error; } parse_headers: $headerList = $table->decode($packed); if ($headerList === null) { $error = self::COMPRESSION_ERROR; break; } assert(!defined("Aerys\\DEBUG_HTTP2") || (print "HEADER(" . (\strlen($packed) - $padding) . "): " . implode(" | ", array_map(function ($x) { return implode(": ", $x); }, $headerList)) . "\n")); $headerArray = []; foreach ($headerList as list($name, $value)) { $headerArray[$name][] = $value; } $parseResult = ["id" => $id, "trace" => $headerList, "protocol" => "2.0", "method" => $headerArray[":method"][0], "uri" => !empty($headerArray[":authority"][0]) ? "{$headerArray[":scheme"][0]}://{$headerArray[":authority"][0]}{$headerArray[":path"][0]}" : $headerArray[":path"][0], "headers" => $headerArray, "body" => ""]; if (!isset($client->streamWindow[$id])) { $client->streamWindow[$id] = $client->initialWindowSize; } $client->streamWindowBuffer[$id] = ""; if ($streamEnd) { ($this->emit)([HttpDriver::RESULT, $parseResult, null], $client); } else { ($this->emit)([HttpDriver::ENTITY_HEADERS, $parseResult, null], $client); } continue; stream_error: $this->writeFrame($client, pack("N", $error), self::RST_STREAM, self::NOFLAG, $id); assert(!defined("Aerys\\DEBUG_HTTP2") || (print "Stream ERROR: {$error}\n")); unset($headers[$id], $bodyLens[$id]); $client->remainingKeepAlives++; continue; } connection_error: $client->shouldClose = true; $this->writeFrame($client, pack("NN", 0, $error), self::GOAWAY, self::NOFLAG); assert(!defined("Aerys\\DEBUG_HTTP2") || (print "Connection ERROR: {$error}\n")); while (1) { yield; } }