Commit 3d2d9a16 authored by jan.koester's avatar jan.koester
Browse files

test

parent cb2e4a25
Loading
Loading
Loading
Loading
+111 −52
Changes for src/http/HttpProxyServer.cpp: 111 added lines, 52 removed lines.
Original line number Diff line number Diff line
@@ -181,6 +181,52 @@ namespace {
// Below this, gzip/brotli framing overhead eats whatever the algorithm saves -- matches
// nginx's gzip_min_length default.
constexpr size_t kMinCompressibleBytes = 256;

// A GET response whose declared Content-Length exceeds this is relayed chunk by chunk (the
// same path Range responses take) instead of being buffered whole for compression. Buffering
// a multi-GB download (an ISO image, say) used to mean holding all of it in memory before the
// client saw a single byte -- slow at best, std::bad_alloc at worst.
constexpr size_t kMaxBufferedBody = 8 * 1024 * 1024;

// Hard ceiling for a buffered body whose size wasn't known up front (chunked / read-until-EOF
// responses have no Content-Length to compare against kMaxBufferedBody): fail the request
// with a 502 instead of letting an unbounded upstream body exhaust memory.
constexpr size_t kMaxUnknownLengthBody = 256 * 1024 * 1024;

bool isChunkedResponse(const libhttppp::HttpResponse &resp) {
    try {
        for (auto *v = resp.getTransferEncoding(); v; v = v->nextvalue()) {
            std::string val = v->getvalue();
            std::transform(val.begin(), val.end(), val.begin(),
                           [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
            if (val == "chunked") return true;
        }
    } catch (...) {
    }
    return false;
}

// True when GetStream()'s response carries a trustworthy Content-Length (present, not
// overridden by chunked framing -- mirrors libhttppp's own GetStream mode selection) that is
// too large to buffer.
bool exceedsBufferLimit(const libhttppp::HttpResponse &resp) {
    if (!resp.getHeaderData("content-length") || isChunkedResponse(resp)) return false;
    return resp.getContentLength() > kMaxBufferedBody;
}

// Drains whatever body GetStream() left pending into one buffer.
std::vector<char> drainStreamedBody(libhttppp::HttpClient &client) {
    std::vector<char> body;
    char chunk[65536];
    for (;;) {
        size_t n = client.readBodyChunk(chunk, sizeof(chunk)); // blocking; 0 = body complete
        if (n == 0) break;
        if (body.size() + n > kMaxUnknownLengthBody)
            throw std::runtime_error("upstream response body too large to buffer");
        body.insert(body.end(), chunk, chunk + n);
    }
    return body;
}
} // namespace

namespace {
@@ -596,6 +642,58 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int
            outreq.setHeaderData("x-forwarded-proto")->push_back(_tlsActive ? "https" : "http");

            std::vector<char> body;
            // Relays a fully buffered upstream response (status, headers, compression).
            // `last` is HttpClient::lastResponse() for the buffering methods, or GetStream()'s
            // own response for a small GET (GetStream() never populates lastResponse()).
            auto relayBuffered = [&](const libhttppp::HttpResponse *last) {
                libhttppp::HttpResponse curres;
                int status = 200;
                if (last) {
                    status = last->getStatusCode();
                    // HttpResponse::parseH2/parseH3 clear the reason phrase entirely (H2/H3
                    // responses only carry a numeric ":status" pseudo-header) -- setState()
                    // throws on an empty string, and an HTTP/1.1 upstream's reason phrase alone
                    // (e.g. "OK", see HttpResponse::parse) would drop the code from our own
                    // status line, so always lead with the numeric code ourselves.
                    std::string reason = last->getState();
                    curres.setState(forwardedResponseState(status, reason));
                    curres.setContentType(last->getContentType());

                    // Forward every upstream response header to the client except hop-by-hop
                    // ones and the handful applyResponseCompression() owns exclusively below --
                    // without this, Set-Cookie, Location, Cache-Control, WWW-Authenticate, and
                    // any application-specific header were silently dropped (only status/
                    // content-type/content-encoding ever reached the client), breaking e.g. any
                    // upstream that sets a session cookie. Mirrors kHopByHop on the
                    // request-forwarding side above.
                    static const std::set<std::string> kResponseSkip = {
                        "content-type", "content-length", "content-encoding", "vary",
                        "connection", "transfer-encoding", "keep-alive", "upgrade", "trailer",
                    };
                    for (auto *hd = last->getfirstHeaderData(); hd; hd = hd->nextHeaderData()) {
                        const std::string &key = hd->getkey();
                        if (key.empty() || key[0] == ':' || kResponseSkip.count(key)) continue;
                        auto *out = curres.setHeaderData(key);
                        for (auto *v = hd->getfirstValue(); v; v = v->nextvalue()) {
                            out->push_back(v->getvalue());
                        }
                    }

                    std::string upstreamEncoding;
                    if (auto *hd = last->getHeaderData("content-encoding")) {
                        if (auto *v = hd->getfirstValue()) upstreamEncoding = v->getvalue();
                    }
                    applyResponseCompression(curreq, upstreamEncoding, body, curres);
                } else {
                    curres.setState(HTTP200);
                }
                curres.send(curreq, body, static_cast<int>(body.size()));
                // A pooled backend answering with a real HTTP response (even a 4xx/5xx from the
                // application itself) counts as "up" -- only a transport-level failure (caught
                // below) or a 5xx from the backend counts against it for passive health.
                upstreamOk = status < 500;
            };

            if (method == "POST" || method == "PUT" || method == "PATCH") {
                outreq.setRequestType(method == "POST" ? POSTREQUEST
                                       : method == "PUT" ? PUTREQUEST
@@ -646,7 +744,14 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int
                // byte-range response is inherently uncompressible-as-a-whole (the range
                // refers to the uncompressed representation), and neither app's media routes
                // send a Content-Encoding on these responses anyway.
                if (curreq.getHeaderData("range")) {
                //
                // The same applies to any response too large to buffer (exceedsBufferLimit):
                // every GET goes through GetStream() so the size is known before any body is
                // read, and only small (or unknown-length) bodies get drained and buffered for
                // the compression path below. A large one keeps whatever Content-Encoding the
                // upstream chose, forwarded unchanged.
                libhttppp::HttpResponse streamResp = client.GetStream(outreq);
                if (curreq.getHeaderData("range") || exceedsBufferLimit(streamResp)) {
                    // H1 vs H2/H3 need genuinely different delivery mechanisms below, not
                    // just different plumbing -- see the two branches' own comments for why.
                    // getRequestVersion() is set from real parsed values for every protocol
@@ -655,7 +760,6 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int
                    // so this is a reliable, already-public way to tell them apart here.
                    bool isH1 = curreq.getRequestVersion().rfind("HTTP/1", 0) == 0;

                    libhttppp::HttpResponse streamResp = client.GetStream(outreq);
                    int rangeStatus = streamResp.getStatusCode();

                    libhttppp::HttpResponse curres;
@@ -729,7 +833,7 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int
                            try {
                                n = client.readBodyChunk(chunk, toRead); // blocking; 0 = body complete
                            } catch (const std::exception &e) {
                                std::cerr << "[http] streaming range response failed after "
                                std::cerr << "[http] streaming response failed after "
                                          << (rangeContentLength - remaining) << "/"
                                          << rangeContentLength << " bytes: " << e.what() << std::endl;
                                deliveryOk = false;
@@ -787,55 +891,10 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int
                    return;
                }

                body = client.Get(outreq, /*maxTries=*/1);
                body = drainStreamedBody(client);
                relayBuffered(&streamResp);
            }

            libhttppp::HttpResponse curres;
            int status = 200;
            if (auto *last = client.lastResponse()) {
                status = last->getStatusCode();
                // HttpResponse::parseH2/parseH3 clear the reason phrase entirely (H2/H3
                // responses only carry a numeric ":status" pseudo-header) -- setState()
                // throws on an empty string, and an HTTP/1.1 upstream's reason phrase alone
                // (e.g. "OK", see HttpResponse::parse) would drop the code from our own
                // status line, so always lead with the numeric code ourselves.
                std::string reason = last->getState();
                curres.setState(forwardedResponseState(status, reason));
                curres.setContentType(relayedContentType(*last));

                // Forward every upstream response header to the client except hop-by-hop
                // ones and the handful applyResponseCompression() owns exclusively below --
                // without this, Set-Cookie, Location, Cache-Control, WWW-Authenticate, and
                // any application-specific header were silently dropped (only status/
                // content-type/content-encoding ever reached the client), breaking e.g. any
                // upstream that sets a session cookie. Mirrors kHopByHop on the
                // request-forwarding side above.
                static const std::set<std::string> kResponseSkip = {
                    "content-type", "content-length", "content-encoding", "vary",
                    "connection", "transfer-encoding", "keep-alive", "upgrade", "trailer",
                };
                for (auto *hd = last->getfirstHeaderData(); hd; hd = hd->nextHeaderData()) {
                    const std::string &key = hd->getkey();
                    if (key.empty() || key[0] == ':' || kResponseSkip.count(key)) continue;
                    auto *out = curres.setHeaderData(key);
                    for (auto *v = hd->getfirstValue(); v; v = v->nextvalue()) {
                        out->push_back(v->getvalue());
                    }
                }

                std::string upstreamEncoding;
                if (auto *hd = last->getHeaderData("content-encoding")) {
                    if (auto *v = hd->getfirstValue()) upstreamEncoding = v->getvalue();
                }
                applyResponseCompression(curreq, upstreamEncoding, body, curres);
            } else {
                curres.setState(HTTP200);
            }
            curres.send(curreq, body, static_cast<int>(body.size()));
            // A pooled backend answering with a real HTTP response (even a 4xx/5xx from the
            // application itself) counts as "up" -- only a transport-level failure (caught
            // below) or a 5xx from the backend counts against it for passive health.
            upstreamOk = status < 500;
            if (method != "GET") relayBuffered(client.lastResponse());
        } catch (const std::exception &e) {
            sendError(curreq, HTTP502, std::string("upstream error: ") + e.what());
        }
@@ -868,7 +927,7 @@ void HttpProxyController::ResponseEvent(libhttppp::HttpRequest &curreq, const in
            try {
                n = streaming.client->readBodyChunkNonBlocking(chunk, toRead);
            } catch (const std::exception &e) {
                std::cerr << "[http] streaming range response failed after "
                std::cerr << "[http] streaming response failed after "
                          << streaming.produced << "/" << streaming.send_size
                          << " bytes: " << e.what() << std::endl;
                n = static_cast<size_t>(-1);