Commit 8249214f authored by jan.koester's avatar jan.koester
Browse files

test

parent bb7c5bca
Loading
Loading
Loading
Loading
+9 −0
Changes for src/http.h: 9 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <memory>
#include <deque>
#include <map>
#include <chrono>

#include <netplus/socket.h>
#include <netplus/connection.h>
@@ -453,6 +454,14 @@ namespace libhttppp {
      size_t      emptyCount = 0;
      unsigned int backoffMs = 1;
      bool        finished = false;
      // Set while totalSent < contentLength and the peer's flow-control
      // window is the reason no DATA can go out (not an upstream stall).
      // Reaped by _reapStalledH2Streams() if it stays true too long —
      // guards against a peer that stops sending WINDOW_UPDATE entirely,
      // which would otherwise leave the stream (and its tempreq) parked
      // in activeStreams forever.
      bool        windowBlocked = false;
      std::chrono::steady_clock::time_point blockedSince{};
    };

    struct H2PendingIncoming {
+95 −9
Changes for src/httpd.cpp: 95 added lines, 9 removed lines.
Original line number Diff line number Diff line
@@ -66,6 +66,18 @@ static constexpr uint8_t H2_FLAG_PRIORITY = 0x20;

static constexpr size_t H2_FRAME_HEADER_LEN = 9;
static constexpr size_t H2_MAX_FRAME_SIZE = 16384;

// RFC 7540 §6.9.1: a flow-control window must never exceed 2^31-1.
static constexpr int64_t H2_MAX_WINDOW_SIZE = 0x7fffffff;

// How long a streaming response may sit blocked purely on a closed peer
// flow-control window (no WINDOW_UPDATE arriving) before we give up on
// it. Guards against a peer that silently stops crediting the window —
// without this, the stream (and its per-stream tempreq) stays parked in
// activeStreams forever and the connection looks hung from the client's
// side until it tears down the TCP connection itself.
static constexpr std::chrono::seconds H2_WINDOW_STALL_TIMEOUT{30};

static const char H2_CLIENT_PREFACE[] = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
static constexpr size_t H2_CLIENT_PREFACE_LEN = 24;

@@ -640,7 +652,13 @@ void libhttppp::HttpEvent::_resumeH2Streams(HttpRequest &cureq,
        // First: drain any buffered pending data
        while (!ss->pendingData.empty() && ss->pendingOffset < ss->pendingData.size()) {
            int32_t allowed = std::min(sw, cw);
            if (allowed <= 0) break;
            if (allowed <= 0) {
                if (!ss->windowBlocked) {
                    ss->windowBlocked = true;
                    ss->blockedSince = std::chrono::steady_clock::now();
                }
                break;
            }
            size_t remaining = ss->contentLength > ss->totalSent
                             ? ss->contentLength - ss->totalSent : 0;
            if (remaining == 0) break;
@@ -657,6 +675,7 @@ void libhttppp::HttpEvent::_resumeH2Streams(HttpRequest &cureq,
            cw -= static_cast<int32_t>(chunk_sz);
            ss->pendingOffset += chunk_sz;
            ss->totalSent += chunk_sz;
            ss->windowBlocked = false;
        }
        if (ss->pendingOffset > 0) {
            ss->pendingData.erase(0, ss->pendingOffset);
@@ -675,7 +694,13 @@ void libhttppp::HttpEvent::_resumeH2Streams(HttpRequest &cureq,
        // Fetch more data from upstream via ResponseEvent if window available
        while (ss->totalSent < ss->contentLength) {
            int32_t allowed = std::min(sw, cw);
            if (allowed <= 0) break;
            if (allowed <= 0) {
                if (!ss->windowBlocked) {
                    ss->windowBlocked = true;
                    ss->blockedSince = std::chrono::steady_clock::now();
                }
                break;
            }

            // NOTE: Do NOT reset SendData.pos here — application code
            // (e.g. file serving) uses pos as a source-file read cursor
@@ -716,6 +741,10 @@ void libhttppp::HttpEvent::_resumeH2Streams(HttpRequest &cureq,
                    allowed = std::min(sw, cw);
                    if (allowed <= 0) {
                        // Buffer remainder for next WINDOW_UPDATE
                        if (!ss->windowBlocked) {
                            ss->windowBlocked = true;
                            ss->blockedSince = std::chrono::steady_clock::now();
                        }
                        size_t buf_remaining = ss->contentLength > ss->totalSent
                                             ? ss->contentLength - ss->totalSent : 0;
                        size_t buf_len = std::min(sa - doff, buf_remaining);
@@ -741,6 +770,7 @@ void libhttppp::HttpEvent::_resumeH2Streams(HttpRequest &cureq,
                    cw -= static_cast<int32_t>(chunk_sz);
                    doff += chunk_sz;
                    ss->totalSent += chunk_sz;
                    ss->windowBlocked = false;
                }
                ss->tempreq->SendData.clear();
            } else {
@@ -785,6 +815,39 @@ void libhttppp::HttpEvent::_resumeH2Streams(HttpRequest &cureq,
    }
}

// Abort streaming responses that have been sitting blocked purely on a
// closed peer flow-control window for too long. Called on every
// Http2RequestEvent invocation (i.e. whenever any frame arrives on the
// connection) so that even a connection where one stream is permanently
// stuck still gets cleaned up as soon as something else happens on it —
// there is no separate timer driving the H2 layer.
void libhttppp::HttpEvent::_reapStalledH2Streams(HttpRequest &cureq,
                                                  std::string &out) {
    auto &streams = cureq.h2state().activeStreams;
    if (streams.empty()) return;

    auto now = std::chrono::steady_clock::now();
    std::vector<uint32_t> sids;
    sids.reserve(streams.size());
    for (auto &kv : streams) sids.push_back(kv.first);

    for (uint32_t sid : sids) {
        auto it = streams.find(sid);
        if (it == streams.end()) continue;
        auto &ss = it->second;
        if (!ss->windowBlocked) continue;
        if (now - ss->blockedSince < H2_WINDOW_STALL_TIMEOUT) continue;

        // Peer never credited the window back — give up on this stream
        // instead of leaving it (and its tempreq) parked forever.
        uint8_t rst[4] = {0, 0, 0, 0x03}; // FLOW_CONTROL_ERROR
        out += h2BuildFrame(H2_FRAME_RST_STREAM, 0, sid,
            std::string(reinterpret_cast<char*>(rst), 4));
        cureq.h2state().peerStreamWindows.erase(sid);
        streams.erase(it);
    }
}

bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon,
                                             const int tid,
                                             ULONG_PTR args,
@@ -817,6 +880,12 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon,
        out += h2ConnectionWindowBoost();
    }

    // Reap any streams that have been window-blocked past the stall
    // timeout before doing anything else — a new request coming in on an
    // otherwise-stuck connection is exactly the point where an old
    // zombie stream would never get cleaned up any other way.
    _reapStalledH2Streams(cureq, out);

    // Process all complete frames in RecvData
reprocess:
    while (off + H2_FRAME_HEADER_LEN <= cureq.RecvData.size()) {
@@ -848,13 +917,20 @@ reprocess:
                                     (static_cast<uint8_t>(data[off + soff + 3]) << 16) |
                                     (static_cast<uint8_t>(data[off + soff + 4]) << 8) |
                                     static_cast<uint8_t>(data[off + soff + 5]);
                    if (sid_s == 0x04) { // SETTINGS_INITIAL_WINDOW_SIZE
                    if (sid_s == 0x04 && sval <= H2_MAX_WINDOW_SIZE) { // SETTINGS_INITIAL_WINDOW_SIZE
                        // RFC 7540 §6.5.2: values above 2^31-1 are a
                        // FLOW_CONTROL_ERROR — reject rather than let the
                        // int32_t narrowing below turn it negative.
                        int32_t delta = static_cast<int32_t>(sval) -
                                        cureq.h2state().peerInitialStreamWindow;
                        cureq.h2state().peerInitialStreamWindow = static_cast<int32_t>(sval);
                        // Adjust all existing per-stream windows (RFC 7540 §6.9.2)
                        for (auto &kv : cureq.h2state().peerStreamWindows)
                            kv.second += delta;
                        // Adjust all existing per-stream windows (RFC 7540 §6.9.2),
                        // clamped so an existing window can't be pushed past
                        // the legal maximum either.
                        for (auto &kv : cureq.h2state().peerStreamWindows) {
                            int64_t adjusted = static_cast<int64_t>(kv.second) + delta;
                            kv.second = static_cast<int32_t>(std::min<int64_t>(adjusted, H2_MAX_WINDOW_SIZE));
                        }
                    } else if (sid_s == 0x05) { // SETTINGS_MAX_FRAME_SIZE
                        // RFC 7540 §6.5.2: value must be between 16384 and 16777215
                        if (sval >= 16384 && sval <= 16777215)
@@ -1024,14 +1100,24 @@ reprocess:
                                     (static_cast<uint8_t>(data[off + 2]) << 8) |
                                     static_cast<uint8_t>(data[off + 3]);
                if (sid == 0) {
                    cureq.h2state().peerConnWindow += static_cast<int32_t>(increment);
                    // Clamp instead of letting the int32_t wrap negative
                    // (RFC 7540 §6.9.1). An overflowed window never
                    // recovers on its own — every future send sees
                    // min(sw, cw) <= 0 and the connection stalls
                    // permanently until the client tears down the TCP
                    // connection and reconnects.
                    int64_t newWindow = static_cast<int64_t>(cureq.h2state().peerConnWindow) + increment;
                    cureq.h2state().peerConnWindow =
                        static_cast<int32_t>(std::min<int64_t>(newWindow, H2_MAX_WINDOW_SIZE));
                } else {
                    // Only update streams we're still tracking — ignore
                    // WINDOW_UPDATEs for already-completed streams to
                    // avoid creating phantom peerStreamWindows entries.
                    auto swit = cureq.h2state().peerStreamWindows.find(sid);
                    if (swit != cureq.h2state().peerStreamWindows.end())
                        swit->second += static_cast<int32_t>(increment);
                    if (swit != cureq.h2state().peerStreamWindows.end()) {
                        int64_t newWindow = static_cast<int64_t>(swit->second) + increment;
                        swit->second = static_cast<int32_t>(std::min<int64_t>(newWindow, H2_MAX_WINDOW_SIZE));
                    }
                }
                // Resume any parked streaming responses
                _resumeH2Streams(cureq, out, tid, args);
+1 −0
Changes for src/httpd.h: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -122,6 +122,7 @@ namespace libhttppp {
                               const int tid, ULONG_PTR args);
        void _resumeH2Streams(HttpRequest &cureq, std::string &out,
                              const int tid, ULONG_PTR args);
        void _reapStalledH2Streams(HttpRequest &cureq, std::string &out);
    };

    class HttpD {