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;
}
}