public function parser(Client $client) : \Generator
{
$maxHeaderSize = $client->options->maxHeaderSize;
$bodyEmitSize = $client->options->ioGranularity;
$buffer = "";
do {
// break potential references
unset($traceBuffer, $protocol, $method, $uri, $headers);
$client->streamWindow = $client->options->maxBodySize;
$traceBuffer = null;
$headers = [];
$contentLength = null;
$isChunked = false;
$protocol = null;
$uri = null;
$method = null;
$parseResult = ["id" => 0, "trace" => &$traceBuffer, "protocol" => &$protocol, "method" => &$method, "uri" => &$uri, "headers" => &$headers, "body" => ""];
if ($client->pendingResponses) {
$client->parserEmitLock = true;
do {
if (\strlen($buffer) > $maxHeaderSize + $client->streamWindow) {
\Amp\disable($client->readWatcher);
$buffer .= yield;
if (!($client->isDead & Client::CLOSED_RD)) {
\Amp\enable($client->readWatcher);
}
break;
}
$buffer .= yield;
} while ($client->pendingResponses);
$client->parserEmitLock = false;
}
while (1) {
$buffer = \ltrim($buffer, "\r\n");
if ($headerPos = \strpos($buffer, "\r\n\r\n")) {
$startLineAndHeaders = \substr($buffer, 0, $headerPos + 2);
$buffer = (string) \substr($buffer, $headerPos + 4);
break;
} elseif (\strlen($buffer) > $maxHeaderSize) {
$error = "Bad Request: header size violation";
break 2;
}
$buffer .= yield;
}
$startLineEndPos = \strpos($startLineAndHeaders, "\n");
$startLine = \rtrim(substr($startLineAndHeaders, 0, $startLineEndPos), "\r\n");
$rawHeaders = \substr($startLineAndHeaders, $startLineEndPos + 1);
$traceBuffer = $startLineAndHeaders;
if (!($method = \strtok($startLine, " "))) {
$error = "Bad Request: invalid request line";
break;
}
if (!($uri = \strtok(" "))) {
$error = "Bad Request: invalid request line";
break;
}
$protocol = \strtok(" ");
if (stripos($protocol, "HTTP/") !== 0) {
$error = "Bad Request: invalid request line";
break;
}
$protocol = \substr($protocol, 5);
if ($protocol != "1.1" && $protocol != "1.0") {
// @TODO eventually add an option to disable HTTP/2.0 support???
if ($protocol == "2.0") {
$client->httpDriver = $this->http2;
$client->streamWindow = [];
$client->requestParser = $client->httpDriver->parser($client);
$client->requestParser->send("{$startLineAndHeaders}\r\n{$buffer}");
return;
} else {
$error = HttpDriver::BAD_VERSION;
break;
}
}
if ($rawHeaders) {
if (\strpos($rawHeaders, "\n ") || \strpos($rawHeaders, "\n\t")) {
$error = "Bad Request: multi-line headers deprecated by RFC 7230";
break;
}
if (!\preg_match_all(self::HEADER_REGEX, $rawHeaders, $matches)) {
$error = "Bad Request: header syntax violation";
break;
}
list(, $fields, $values) = $matches;
$headers = [];
foreach ($fields as $index => $field) {
$headers[$field][] = $values[$index];
}
if ($headers) {
$headers = \array_change_key_case($headers);
}
$contentLength = $headers["content-length"][0] ?? null;
if (isset($headers["transfer-encoding"])) {
$value = $headers["transfer-encoding"][0];
$isChunked = (bool) \strcasecmp($value, "identity");
}
// @TODO validate that the bytes in matched headers match the raw input. If not there is a syntax error.
}
if ($method == "HEAD" || $method == "TRACE" || $method == "OPTIONS" || $contentLength === 0) {
// No body allowed for these messages
$hasBody = false;
} else {
$hasBody = $isChunked || $contentLength;
}
if (!$hasBody) {
($this->parseEmitter)($client, HttpDriver::RESULT, $parseResult, null);
continue;
}
($this->parseEmitter)($client, HttpDriver::ENTITY_HEADERS, $parseResult, null);
$body = "";
if ($isChunked) {
$bodySize = 0;
while (1) {
while (false === ($lineEndPos = \strpos($buffer, "\r\n"))) {
$buffer .= yield;
}
$line = \substr($buffer, 0, $lineEndPos);
$buffer = \substr($buffer, $lineEndPos + 2);
$hex = \trim(\ltrim($line, "0")) ?: 0;
$chunkLenRemaining = \hexdec($hex);
if ($lineEndPos === 0 || $hex != \dechex($chunkLenRemaining)) {
$error = "Bad Request: hex chunk size expected";
break 2;
}
if ($chunkLenRemaining === 0) {
while (!isset($buffer[1])) {
$buffer .= yield;
}
$firstTwoBytes = \substr($buffer, 0, 2);
if ($firstTwoBytes === "\r\n") {
$buffer = \substr($buffer, 2);
break;
// finished ($is_chunked loop)
}
do {
if ($trailerSize = \strpos($buffer, "\r\n\r\n")) {
$trailers = \substr($buffer, 0, $trailerSize + 2);
$buffer = \substr($buffer, $trailerSize + 4);
} else {
$buffer .= yield;
$trailerSize = \strlen($buffer);
$trailers = null;
}
if ($maxHeaderSize > 0 && $trailerSize > $maxHeaderSize) {
$error = "Trailer headers too large";
break 3;
}
} while (!isset($trailers));
if (\strpos($trailers, "\n ") || \strpos($trailers, "\n\t")) {
$error = "Bad Request: multi-line trailers deprecated by RFC 7230";
break 2;
}
if (!\preg_match_all(self::HEADER_REGEX, $trailers, $matches)) {
$error = "Bad Request: trailer syntax violation";
break 2;
}
list(, $fields, $values) = $matches;
$trailers = [];
foreach ($fields as $index => $field) {
$trailers[$field][] = $values[$index];
}
if ($trailers) {
$trailers = \array_change_key_case($trailers);
foreach (["transfer-encoding", "content-length", "trailer"] as $remove) {
unset($trailers[$remove]);
}
if ($trailers) {
$headers = \array_merge($headers, $trailers);
}
}
break;
// finished ($is_chunked loop)
} elseif ($bodySize + $chunkLenRemaining > $client->streamWindow) {
do {
$remaining = $client->streamWindow - $bodySize;
$chunkLenRemaining -= $remaining - \strlen($body);
$body .= $buffer;
$bodyBufferSize = \strlen($body);
while ($bodyBufferSize < $remaining) {
if ($bodyBufferSize >= $bodyEmitSize) {
($this->parseEmitter)($client, HttpDriver::ENTITY_PART, ["id" => 0, "body" => $body], null);
$body = '';
$bodySize += $bodyBufferSize;
$remaining -= $bodyBufferSize;
}
$body .= yield;
$bodyBufferSize = \strlen($body);
}
if ($remaining) {
($this->parseEmitter)($client, HttpDriver::ENTITY_PART, ["id" => 0, "body" => substr($body, 0, $remaining)], null);
$buffer = substr($body, $remaining);
$body = "";
$bodySize += $remaining;
}
if (!$client->pendingResponses) {
return;
}
if ($bodySize != $client->streamWindow) {
continue;
}
($this->parseEmitter)($client, HttpDriver::SIZE_WARNING, ["id" => 0], null);
$client->parserEmitLock = true;
\Amp\disable($client->readWatcher);
$yield = yield;
if ($yield === false) {
$client->shouldClose = true;
while (1) {
yield;
}
}
\Amp\enable($client->readWatcher);
$client->parserEmitLock = false;
} while ($client->streamWindow < $bodySize + $chunkLenRemaining);
}
$bodyBufferSize = 0;
while (1) {
$bufferLen = \strlen($buffer);
// These first two (extreme) edge cases prevent errors where the packet boundary ends after
// the \r and before the \n at the end of a chunk.
if ($bufferLen === $chunkLenRemaining || $bufferLen === $chunkLenRemaining + 1) {
$buffer .= yield;
continue;
} elseif ($bufferLen >= $chunkLenRemaining + 2) {
$body .= substr($buffer, 0, $chunkLenRemaining);
$buffer = substr($buffer, $chunkLenRemaining + 2);
$bodyBufferSize += $chunkLenRemaining;
} else {
$body .= $buffer;
$bodyBufferSize += $bufferLen;
$chunkLenRemaining -= $bufferLen;
}
if ($bodyBufferSize >= $bodyEmitSize) {
($this->parseEmitter)($client, HttpDriver::ENTITY_PART, ["id" => 0, "body" => $body], null);
$body = '';
$bodySize += $bodyBufferSize;
$bodyBufferSize = 0;
}
if ($bufferLen >= $chunkLenRemaining + 2) {
$chunkLenRemaining = null;
continue 2;
// next chunk ($is_chunked loop)
} else {
$buffer = yield;
}
}
}
if ($body != "") {
($this->parseEmitter)($client, HttpDriver::ENTITY_PART, ["id" => 0, "body" => $body], null);
}
} else {
$bodySize = 0;
while (true) {
$bound = \min($contentLength, $client->streamWindow);
$bodyBufferSize = \strlen($buffer);
while ($bodySize + $bodyBufferSize < $bound) {
if ($bodyBufferSize >= $bodyEmitSize) {
($this->parseEmitter)($client, HttpDriver::ENTITY_PART, ["id" => 0, "body" => $buffer], null);
$buffer = '';
$bodySize += $bodyBufferSize;
}
$buffer .= yield;
$bodyBufferSize = \strlen($buffer);
}
$remaining = $bound - $bodySize;
if ($remaining) {
($this->parseEmitter)($client, HttpDriver::ENTITY_PART, ["id" => 0, "body" => substr($buffer, 0, $remaining)], null);
$buffer = substr($buffer, $remaining);
$bodySize = $bound;
}
if ($client->streamWindow < $contentLength) {
if (!$client->pendingResponses) {
return;
}
($this->parseEmitter)($client, HttpDriver::SIZE_WARNING, ["id" => 0], null);
$client->parserEmitLock = true;
\Amp\disable($client->readWatcher);
$yield = yield;
if ($yield === false) {
$client->shouldClose = true;
while (1) {
yield;
}
}
\Amp\enable($client->readWatcher);
$client->parserEmitLock = false;
} else {
break;
}
}
}
$client->streamWindow = $client->options->maxBodySize;
($this->parseEmitter)($client, HttpDriver::ENTITY_RESULT, $parseResult, null);
} while (true);
// An error occurred...
// stop parsing here ...
($this->parseEmitter)($client, HttpDriver::ERROR, $parseResult, $error);
while (1) {
yield;
}
}