Commit 70d6715b authored by jan.koester's avatar jan.koester
Browse files

test

parent 11567b2d
Loading
Loading
Loading
Loading
+92 −15
Original line number Diff line number Diff line
@@ -205,6 +205,12 @@ const std::string & libhttppp::HttpUrl::getPath() const{
  return _path;
}

// --- TLS Session Cache (process-wide singleton) ---
netplus::TlsSessionCache& libhttppp::HttpClient::tlsSessionCache() {
    static netplus::TlsSessionCache cache;
    return cache;
}

libhttppp::HttpClient::HttpClient(const HttpUrl& desturl)
: _url(desturl){
    try {
@@ -231,6 +237,10 @@ bool libhttppp::HttpClient::tryHttp3First(){
}

void libhttppp::HttpClient::resetConnection(){
  // Reset HTTP/2 connection state on any new connection
  _h2PrefaceSent = false;
  _h2NextStreamId = 1;
  _h2Decoder.reset();
  if (_url.getProtocol() == HttpUrl::HTTP3) {
    _cltsock = std::make_unique<netplus::quic>();
    _cltsock->connect(_url.getHost(), _url.getPort(), false);
@@ -247,6 +257,20 @@ void libhttppp::HttpClient::resetConnection(){
    sslsock->getTls().client_alpn_protocols =
      std::string("\x02h2\x08http/1.1", 12);

    // --- TLS session resumption: attach cache and load saved session ---
    netplus::tls& t = sslsock->getTls();
    t.session_cache = &tlsSessionCache();

    // Build host:port key for the cache lookup
    std::string cacheKey = _url.getHost() + ":" + std::to_string(_url.getPort());
    netplus::TlsSessionCacheEntry saved;
    if (tlsSessionCache().lookup_by_host(cacheKey, saved)) {
        t.attempt_resumption = true;
        t.resumption_session_id    = saved.session_id;
        t.resumption_master_secret = saved.master_secret;
        t.resumption_cipher        = saved.cipher_suite;
    }

    _cltsock = std::move(sslsock);
    _cltsock->connect(_url.getHost(), _url.getPort(), false);
    _cltsock->setNonBlock();
@@ -254,6 +278,16 @@ void libhttppp::HttpClient::resetConnection(){
    // Check negotiated ALPN after TLS handshake
    const std::string &alpn = dynamic_cast<netplus::ssl*>(_cltsock.get())->getSelectedAlpn();
    _isH2 = (alpn == "h2");

    // --- Save session for future resumption ---
    {
        netplus::tls& tc = dynamic_cast<netplus::ssl*>(_cltsock.get())->getTls();
        if (tc.handshakeDone && !tc.is_tls13
            && !tc.client_session_id.empty() && !tc.masterSecret.empty()) {
            tlsSessionCache().store_by_host(cacheKey,
                tc.client_session_id, tc.masterSecret, tc.chosenSuite);
        }
    }
  } else {
    _cltsock = std::make_unique<netplus::tcp>(-1);
    _cltsock->connect(_url.getHost(), _url.getPort(), false);
@@ -264,6 +298,10 @@ void libhttppp::HttpClient::resetConnection(){


void libhttppp::HttpClient::reconnect(){
  // Reset HTTP/2 connection state on reconnect
  _h2PrefaceSent = false;
  _h2NextStreamId = 1;
  _h2Decoder.reset();
  if (_url.getProtocol() == HttpUrl::HTTP3) {
    _cltsock = std::make_unique<netplus::quic>();
    _cltsock->connect(_url.getHost(), _url.getPort(), false);
@@ -280,12 +318,35 @@ void libhttppp::HttpClient::reconnect(){
    sslsock->getTls().client_alpn_protocols =
      std::string("\x02h2\x08http/1.1", 12);

    // --- TLS session resumption: attach cache and load saved session ---
    netplus::tls& t = sslsock->getTls();
    t.session_cache = &tlsSessionCache();

    std::string cacheKey = _url.getHost() + ":" + std::to_string(_url.getPort());
    netplus::TlsSessionCacheEntry saved;
    if (tlsSessionCache().lookup_by_host(cacheKey, saved)) {
        t.attempt_resumption = true;
        t.resumption_session_id    = saved.session_id;
        t.resumption_master_secret = saved.master_secret;
        t.resumption_cipher        = saved.cipher_suite;
    }

    _cltsock = std::move(sslsock);
    _cltsock->connect(_url.getHost(), _url.getPort(), false);
    _cltsock->setNonBlock();

    const std::string &alpn = dynamic_cast<netplus::ssl*>(_cltsock.get())->getSelectedAlpn();
    _isH2 = (alpn == "h2");

    // --- Save session for future resumption ---
    {
        netplus::tls& tc = dynamic_cast<netplus::ssl*>(_cltsock.get())->getTls();
        if (tc.handshakeDone && !tc.is_tls13
            && !tc.client_session_id.empty() && !tc.masterSecret.empty()) {
            tlsSessionCache().store_by_host(cacheKey,
                tc.client_session_id, tc.masterSecret, tc.chosenSuite);
        }
    }
  } else {
    _cltsock = std::make_unique<netplus::tcp>(-1);
    _cltsock->connect(_url.getHost(), _url.getPort(), false);
@@ -746,10 +807,13 @@ const std::vector<char> libhttppp::HttpClient::_h2Request(
            }
        };

        // 1) Send connection preface + SETTINGS
        // 1) Send connection preface + SETTINGS (only once per connection)
        if (!_h2PrefaceSent) {
            std::string preface(H2C_CLIENT_PREFACE, H2C_CLIENT_PREFACE_LEN);
            preface += h2cBuildSettings();
            send_all(preface);
            _h2PrefaceSent = true;
        }

        // 2) Build HEADERS frame for stream 1
        std::string path = nreq.getRequestURL();
@@ -781,7 +845,8 @@ const std::vector<char> libhttppp::HttpClient::_h2Request(
        std::string hpack_block = hpack::Encoder::encodeRequestHeaders(
            method, path, scheme, auth.str(), extra);

        uint32_t stream_id = 1;
        uint32_t stream_id = _h2NextStreamId;
        _h2NextStreamId += 2;  // Client streams use odd IDs: 1, 3, 5, ...
        bool end_stream = (postBody == nullptr || postBody->empty());
        uint8_t hdr_flags = H2C_FLAG_END_HEADERS;
        if (end_stream) hdr_flags |= H2C_FLAG_END_STREAM;
@@ -810,7 +875,9 @@ const std::vector<char> libhttppp::HttpClient::_h2Request(
        bool got_response_headers = false;
        bool got_end_stream = false;

        hpack::Decoder decoder;
        // Use connection-level decoder to maintain HPACK dynamic table state
        if (!_h2Decoder) _h2Decoder = std::make_unique<hpack::Decoder>();
        hpack::Decoder &decoder = *_h2Decoder;

        // Read enough data into raw buffer
        auto ensure_bytes = [&](size_t need) {
@@ -999,18 +1066,28 @@ const std::vector<char> libhttppp::HttpClient::_h2Request(
          raw.insert(raw.end(), buf, buf + n);
          last_data = std::chrono::steady_clock::now();
        } else {
          // Pump the UDP socket: read raw datagrams and process QUIC packets
          // without consuming stream data (recvData would consume and discard it)
          // Pump the UDP socket: drain ALL available datagrams and process
          // QUIC packets. For large responses the server sends many datagrams;
          // reading only one per loop iteration would be far too slow.
          bool pumped_any = false;
          for (int pump_i = 0; pump_i < 256; ++pump_i) {
            try {
              q->pumpNetwork(MSG_DONTWAIT);
              pumped_any = true;
            } catch (netplus::NetException &e) {
              if (e.getErrorType() != netplus::NetException::Note) {
                HTTPException ee;
                ee[HTTPException::Error] << e.what();
                throw ee;
              }
              break; // EAGAIN — no more datagrams available
            }
          }
          // If we pumped datagrams, immediately retry recvStreamData
          // without waiting. Only waitRead when truly idle.
          if (pumped_any)
            continue;
        }

        size_t pos = 0;
        while (pos < raw.size()) {
+10 −0
Original line number Diff line number Diff line
@@ -91,6 +91,10 @@ namespace libhttppp {
      const std::vector<char> Post(HttpRequest &nreq,const std::vector<char> &post);
      const std::vector<char> Put(HttpRequest &nreq,const std::vector<char> &put);
      const std::vector<char> Delete(HttpRequest &nreq);

      // Shared TLS session cache — enables abbreviated TLS 1.2 handshakes
      // across reconnects to the same host.  One cache per process.
      static netplus::TlsSessionCache& tlsSessionCache();
  private:
      void resetConnection();
      void _ensureConnected();
@@ -107,6 +111,9 @@ namespace libhttppp {

      // HTTP/2 client helpers
      bool _isH2 = false;
      bool _h2PrefaceSent = false;       // true after connection preface sent
      uint32_t _h2NextStreamId = 1;     // next client-initiated stream ID (odd)
      std::unique_ptr<hpack::Decoder> _h2Decoder; // persistent HPACK decoder for connection
      const std::vector<char> _h2Request(const std::string &method,
                                          HttpRequest &nreq,
                                          const std::vector<char> *postBody = nullptr);
@@ -315,6 +322,9 @@ namespace libhttppp {
    struct H2PendingIncoming {
      std::vector<hpack::HeaderField> headers;
      std::string body;
      std::vector<uint8_t> rawHpack;      // accumulates HPACK across CONTINUATION frames
      bool headersComplete = false;       // true once END_HEADERS received
      bool endStreamOnHeaders = false;    // END_STREAM was on HEADERS frame
    };

    struct H2State {
+122 −25
Original line number Diff line number Diff line
@@ -126,16 +126,42 @@ static std::string h2ConnectionWindowBoost() {
    return h2BuildWindowUpdate(0, boost);
}

// Append DATA frames chunked to H2_MAX_FRAME_SIZE
// Append a single frame directly to 'out' without creating a temp string.
// Avoids allocating + copying a temporary std::string per frame.
static void h2AppendFrameDirect(std::string &out, uint8_t type, uint8_t flags,
                                uint32_t stream_id,
                                const char *payload, size_t payload_size) {
    size_t old_size = out.size();
    out.resize(old_size + 9 + payload_size);
    char *f = &out[old_size];
    f[0] = static_cast<char>((payload_size >> 16) & 0xff);
    f[1] = static_cast<char>((payload_size >> 8) & 0xff);
    f[2] = static_cast<char>(payload_size & 0xff);
    f[3] = static_cast<char>(type);
    f[4] = static_cast<char>(flags);
    f[5] = static_cast<char>((stream_id >> 24) & 0x7f);
    f[6] = static_cast<char>((stream_id >> 16) & 0xff);
    f[7] = static_cast<char>((stream_id >> 8) & 0xff);
    f[8] = static_cast<char>(stream_id & 0xff);
    if (payload_size > 0) {
        std::memcpy(f + 9, payload, payload_size);
    }
}

// Append DATA frames chunked to H2_MAX_FRAME_SIZE.
// Uses direct memcpy to avoid temporary string copies per chunk.
static void h2AppendDataFrames(std::string &out, uint32_t stream_id,
                               const std::string &body, bool end_stream) {
    // Pre-reserve space for all frames: 9-byte header per chunk + body data
    size_t num_chunks = (body.size() + H2_MAX_FRAME_SIZE - 1) / H2_MAX_FRAME_SIZE;
    out.reserve(out.size() + body.size() + num_chunks * H2_FRAME_HEADER_LEN);
    size_t offset = 0;
    while (offset < body.size()) {
        size_t chunk = std::min(body.size() - offset, H2_MAX_FRAME_SIZE);
        bool last_chunk = (offset + chunk >= body.size());
        uint8_t flags = (last_chunk && end_stream) ? H2_FLAG_END_STREAM : 0;
        out += h2BuildFrame(H2_FRAME_DATA, flags, stream_id,
                            body.substr(offset, chunk));
        h2AppendFrameDirect(out, H2_FRAME_DATA, flags, stream_id,
                            body.data() + offset, chunk);
        offset += chunk;
    }
}
@@ -270,6 +296,11 @@ libhttppp::HttpEvent::HttpEvent(std::vector<netplus::socket*> serversocket, int
        // Set ALPN selection and framing callbacks on SSL server sockets.
        // These are propagated to child sockets via ssl::accept().
        if (auto* sslsock = dynamic_cast<netplus::ssl*>(sock)) {
            // Attach the TLS session cache to the server socket.
            // Child sockets created by accept() inherit the cache pointer
            // so that abbreviated TLS 1.2 handshakes can be used.
            sslsock->getTls().session_cache = &_tlsSessionCache;

            // Prefer h2, fall back to http/1.1
            sslsock->setAlpnSelectCallback([](const std::vector<std::string>& protos) -> std::string {
                for (auto& p : protos) {
@@ -468,6 +499,7 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon,
    cureq._httpProtocol = 1;  // Mark connection as HTTP/2

    std::string out;
    out.reserve(4096);  // Pre-allocate to avoid repeated reallocations
    size_t off = 0;

    // Consume HTTP/2 client connection preface if present
@@ -518,16 +550,52 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon,
                hpack_len -= 5;
            }

            // Decode HPACK headers using the connection's decoder
            // (maintains dynamic table state across frames)
            if (fflags & H2_FLAG_END_HEADERS) {
                // Complete header block in this single frame
                auto decoded = cureq.h2state().hpackDecoder.decode(hpack_data, hpack_len);

                if (fflags & H2_FLAG_END_STREAM) {
                    // No body expected (GET, HEAD, etc.) — dispatch immediately
                    _dispatchH2Stream(cureq, out, sid, decoded, "", tid, args);
                } else {
                    // Body expected via DATA frames (POST, PUT, etc.)
                cureq.h2state().pendingIncoming[sid] = {std::move(decoded), ""};
                    auto &pending = cureq.h2state().pendingIncoming[sid];
                    pending.headers = std::move(decoded);
                    pending.headersComplete = true;
                }
            } else {
                // END_HEADERS not set: HPACK block continues in CONTINUATION
                // frames. Accumulate raw bytes and defer decoding.
                auto &pending = cureq.h2state().pendingIncoming[sid];
                pending.rawHpack.assign(hpack_data, hpack_data + hpack_len);
                pending.headersComplete = false;
                pending.endStreamOnHeaders = (fflags & H2_FLAG_END_STREAM) != 0;
            }
            break;
        }

        case H2_FRAME_CONTINUATION: {
            // RFC 7540 §6.10: CONTINUATION carries more HPACK data for
            // a HEADERS block whose END_HEADERS was not yet set.
            auto it = cureq.h2state().pendingIncoming.find(sid);
            if (it != cureq.h2state().pendingIncoming.end() && !it->second.headersComplete) {
                const uint8_t *cont_data = reinterpret_cast<const uint8_t*>(data + off);
                it->second.rawHpack.insert(it->second.rawHpack.end(),
                                           cont_data, cont_data + flen);
                if (fflags & H2_FLAG_END_HEADERS) {
                    // Full HPACK block received — decode now
                    it->second.headers = cureq.h2state().hpackDecoder.decode(
                        it->second.rawHpack.data(), it->second.rawHpack.size());
                    it->second.rawHpack.clear();
                    it->second.rawHpack.shrink_to_fit();
                    it->second.headersComplete = true;

                    if (it->second.endStreamOnHeaders) {
                        // No body expected — dispatch immediately
                        _dispatchH2Stream(cureq, out, sid,
                                          it->second.headers, "", tid, args);
                        cureq.h2state().pendingIncoming.erase(it);
                    }
                }
            }
            break;
        }
@@ -545,7 +613,9 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon,
                }

                if (fflags & H2_FLAG_END_STREAM) {
                    // All body data received — dispatch the request
                    // Only dispatch once headers are fully received
                    // (CONTINUATION may still be pending)
                    if (it->second.headersComplete) {
                        _dispatchH2Stream(cureq, out, sid,
                                          it->second.headers,
                                          it->second.body,
@@ -553,6 +623,7 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon,
                        cureq.h2state().pendingIncoming.erase(it);
                    }
                }
            }
            break;
        }

@@ -580,9 +651,27 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon,
        case H2_FRAME_GOAWAY:
            goto done;

        case H2_FRAME_RST_STREAM: {
            // Clean up pending state for the reset stream to prevent
            // memory leaks and stalled stream bookkeeping.
            auto it = cureq.h2state().pendingIncoming.find(sid);
            if (it != cureq.h2state().pendingIncoming.end()) {
                cureq.h2state().pendingIncoming.erase(it);
            }
            // Also remove any pending outbound responses for this stream
            auto &pr = cureq.h2state().pendingResponses;
            for (auto pit = pr.begin(); pit != pr.end(); ) {
                if (pit->streamId == sid) {
                    pit = pr.erase(pit);
                } else {
                    ++pit;
                }
            }
            break;
        }

        case H2_FRAME_WINDOW_UPDATE:
        case H2_FRAME_PRIORITY:
        case H2_FRAME_RST_STREAM:
            // Acknowledge by ignoring — these don't need explicit responses
            break;

@@ -888,16 +977,24 @@ void libhttppp::HttpEvent::ResponseEvent(netplus::con &curcon,const int tid,ULON
        if (cureq._httpProtocol == 1) {
            if (cureq._h2 && !cureq._h2->pendingResponses.empty()) {
                auto &pending = cureq._h2->pendingResponses.front();
                // Batch up to 8 DATA chunks per event loop iteration
                // for significantly higher throughput on large responses.
                std::string batch;
                batch.reserve(std::min(pending.body.size() - pending.offset,
                                       8 * H2_MAX_FRAME_SIZE) + 8 * H2_FRAME_HEADER_LEN);
                for (int i = 0; i < 8 && pending.offset < pending.body.size(); ++i) {
                    size_t remaining = pending.body.size() - pending.offset;
                if (remaining > 0) {
                    size_t chunk = std::min(remaining, H2_MAX_FRAME_SIZE);
                    bool last = (pending.offset + chunk >= pending.body.size());
                    uint8_t flags = last ? H2_FLAG_END_STREAM : 0;
                    std::string frame = h2BuildFrame(
                        H2_FRAME_DATA, flags, pending.streamId,
                        pending.body.substr(pending.offset, chunk));
                    cureq.SendData.append(frame.data(), frame.size());
                    h2AppendFrameDirect(batch, H2_FRAME_DATA, flags,
                                        pending.streamId,
                                        pending.body.data() + pending.offset, chunk);
                    pending.offset += chunk;
                    if (last) break;
                }
                if (!batch.empty()) {
                    cureq.SendData.append(batch.data(), batch.size());
                }
                if (pending.offset >= pending.body.size()) {
                    cureq._h2->pendingResponses.pop_front();
+4 −0
Original line number Diff line number Diff line
@@ -66,6 +66,10 @@ namespace libhttppp {
    protected:
        void CreateConnection(std::shared_ptr<netplus::con> &res);
        std::string _altSvcH3;   // Alt-Svc value for HTTP/3 advertisement

        // TLS session cache — shared across all accepted SSL connections
        // to enable abbreviated TLS 1.2 handshakes on client reconnection.
        netplus::TlsSessionCache _tlsSessionCache;
    private:
        // Per-stream buffer for HTTP/3: accumulates data until fin
        struct H3StreamBuffer {