Aerys\Http1Driver::parser PHP Method

parser() public method

public parser ( Client $client ) : Generator
$client Client
return Generator
    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;
        }
    }