Commit 7ff70145 authored by jan.koester's avatar jan.koester
Browse files

new api fixes

parent 2dc3ebd3
Loading
Loading
Loading
Loading
+74 −9
Original line number Diff line number Diff line
@@ -236,6 +236,67 @@ std::vector<HeaderField> Decoder::decode(const std::string &data) {
    return decode(reinterpret_cast<const uint8_t*>(data.data()), data.size());
}

std::vector<HeaderField> Decoder::decodeStateless(const uint8_t *data, size_t len) {
    Decoder tmp;
    return tmp.decode(data, len);
}

// ----------------------------------------------------------------
// Dynamic table helpers (RFC 7541 §4)
// ----------------------------------------------------------------
// Entry size = name length + value length + 32 (RFC 7541 §4.1)
static size_t entrySize(const std::string &name, const std::string &value) {
    return name.size() + value.size() + 32;
}

void Decoder::evict(size_t required) {
    while (_dynTableSize + required > _dynTableMaxSize && !_dynTable.empty()) {
        auto &back = _dynTable.back();
        _dynTableSize -= entrySize(back.name, back.value);
        _dynTable.pop_back();
    }
}

void Decoder::addEntry(const std::string &name, const std::string &value) {
    size_t sz = entrySize(name, value);
    evict(sz);
    if (sz <= _dynTableMaxSize) {
        _dynTable.push_front({name, value});
        _dynTableSize += sz;
    }
}

bool Decoder::lookupIndex(uint32_t idx, std::string &name, std::string &value) const {
    if (idx == 0) return false;
    if (idx < STATIC_TABLE_SIZE) {
        name = STATIC_TABLE[idx].name;
        value = STATIC_TABLE[idx].value;
        return true;
    }
    // Dynamic table: index 62 = first entry (front)
    size_t dynIdx = idx - STATIC_TABLE_SIZE;
    if (dynIdx < _dynTable.size()) {
        name = _dynTable[dynIdx].name;
        value = _dynTable[dynIdx].value;
        return true;
    }
    return false;
}

bool Decoder::lookupIndexName(uint32_t idx, std::string &name) const {
    if (idx == 0) return false;
    if (idx < STATIC_TABLE_SIZE) {
        name = STATIC_TABLE[idx].name;
        return true;
    }
    size_t dynIdx = idx - STATIC_TABLE_SIZE;
    if (dynIdx < _dynTable.size()) {
        name = _dynTable[dynIdx].name;
        return true;
    }
    return false;
}

std::vector<HeaderField> Decoder::decode(const uint8_t *data, size_t len) {
    std::vector<HeaderField> headers;
    size_t off = 0;
@@ -248,8 +309,9 @@ std::vector<HeaderField> Decoder::decode(const uint8_t *data, size_t len) {
            size_t int_bytes = 0;
            uint32_t idx = decodeInt(data + off, len - off, 7, int_bytes);
            off += int_bytes;
            if (idx > 0 && idx < STATIC_TABLE_SIZE) {
                headers.push_back({STATIC_TABLE[idx].name, STATIC_TABLE[idx].value});
            std::string name, value;
            if (lookupIndex(idx, name, value)) {
                headers.push_back({name, value});
            }
        } else if ((byte & 0xC0) == 0x40) {
            // Literal with incremental indexing (section 6.2.1)
@@ -257,8 +319,8 @@ std::vector<HeaderField> Decoder::decode(const uint8_t *data, size_t len) {
            uint32_t idx = decodeInt(data + off, len - off, 6, int_bytes);
            off += int_bytes;
            std::string name;
            if (idx > 0 && idx < STATIC_TABLE_SIZE) {
                name = STATIC_TABLE[idx].name;
            if (idx > 0) {
                lookupIndexName(idx, name);
            } else {
                size_t str_bytes = 0;
                name = decodeString(data + off, len - off, str_bytes);
@@ -268,15 +330,17 @@ std::vector<HeaderField> Decoder::decode(const uint8_t *data, size_t len) {
            std::string value = decodeString(data + off, len - off, str_bytes);
            off += str_bytes;
            headers.push_back({name, value, true});
            // Add to dynamic table
            addEntry(name, value);
        } else if ((byte & 0xF0) == 0x00 || (byte & 0xF0) == 0x10) {
            // Literal without indexing (section 6.2.2) or never indexed (section 6.2.3)
            uint8_t prefix = ((byte & 0xF0) == 0x10) ? 4 : 4;
            uint8_t prefix = 4;
            size_t int_bytes = 0;
            uint32_t idx = decodeInt(data + off, len - off, prefix, int_bytes);
            off += int_bytes;
            std::string name;
            if (idx > 0 && idx < STATIC_TABLE_SIZE) {
                name = STATIC_TABLE[idx].name;
            if (idx > 0) {
                lookupIndexName(idx, name);
            } else {
                size_t str_bytes = 0;
                name = decodeString(data + off, len - off, str_bytes);
@@ -289,9 +353,10 @@ std::vector<HeaderField> Decoder::decode(const uint8_t *data, size_t len) {
        } else if ((byte & 0xE0) == 0x20) {
            // Dynamic table size update (section 6.3)
            size_t int_bytes = 0;
            decodeInt(data + off, len - off, 5, int_bytes);
            uint32_t newMax = decodeInt(data + off, len - off, 5, int_bytes);
            off += int_bytes;
            // We don't maintain a dynamic table, just skip
            _dynTableMaxSize = newMax;
            evict(0); // evict entries that exceed new max
        } else {
            ++off; // skip unknown
        }
+23 −2
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@

#include <cstddef>
#include <cstdint>
#include <deque>
#include <string>
#include <vector>

@@ -60,14 +61,34 @@ private:

class Decoder {
public:
    static std::vector<HeaderField> decode(const uint8_t *data, size_t len);
    static std::vector<HeaderField> decode(const std::string &data);
    Decoder() = default;

    // Instance method: decode using (and updating) the dynamic table
    std::vector<HeaderField> decode(const uint8_t *data, size_t len);
    std::vector<HeaderField> decode(const std::string &data);

    // Static convenience (stateless, no dynamic table — for one-shot use)
    static std::vector<HeaderField> decodeStateless(const uint8_t *data, size_t len);

    // Shared utilities (also used by QPACK decoder)
    static uint32_t decodeInt(const uint8_t *data, size_t len, uint8_t prefix_bits, size_t &bytes_read);
    static std::string decodeString(const uint8_t *data, size_t len, size_t &bytes_read);
    static std::string huffmanDecode(const uint8_t *data, size_t len);

private:
    // Dynamic table (RFC 7541 §2.3.2): newest entry at front
    struct DynEntry { std::string name; std::string value; };
    std::deque<DynEntry> _dynTable;
    size_t _dynTableSize = 0;           // current size in octets
    size_t _dynTableMaxSize = 4096;     // default per RFC 7541 §6.5.2

    // Look up an index across static + dynamic tables
    bool lookupIndex(uint32_t idx, std::string &name, std::string &value) const;
    bool lookupIndexName(uint32_t idx, std::string &name) const;

    // Add an entry to the dynamic table, evicting as necessary
    void addEntry(const std::string &name, const std::string &value);
    void evict(size_t required);

    struct StaticEntry { const char *name; const char *value; };
    static const StaticEntry STATIC_TABLE[];
+1 −0
Original line number Diff line number Diff line
@@ -1553,6 +1553,7 @@ void libhttppp::HttpRequest::clear(){
  _h2HeadersSent = false;
  _h2ExpectedContentLength = 0;
  _h2BodyBytesSent = 0;
  _h2PendingResponses.clear();
}

size_t libhttppp::HttpRequest::parse() {
+13 −1
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <vector>
#include <string>
#include <memory>
#include <deque>

#include <netplus/socket.h>
#include <netplus/connection.h>
@@ -39,10 +40,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "config.h"

#include "httpdefinitions.h"
#include "hpack.h"

#pragma once

namespace libhttppp::hpack { struct HeaderField; }
namespace libhttppp::qpack { struct HeaderField; }

namespace libhttppp {
@@ -280,6 +281,17 @@ namespace libhttppp {
    size_t            _h2ExpectedContentLength = 0;
    size_t            _h2BodyBytesSent = 0;

    // Queue of pending H2 stream responses (body data waiting to be framed)
    struct H2PendingResponse {
      uint32_t    streamId;
      std::string body;       // remaining body data to send as DATA frames
      size_t      offset = 0; // how far into body we've sent
    };
    std::deque<H2PendingResponse> _h2PendingResponses;

    // Per-connection HPACK decoder (maintains dynamic table across H2 frames)
    hpack::Decoder _h2HpackDecoder;

    friend class HttpForm;
    friend class HttpResponse;
    friend class HttpEvent;
+147 −62
Original line number Diff line number Diff line
@@ -31,6 +31,9 @@
#include <cctype>
#include <string>

#include <sys/socket.h>
#include <netinet/in.h>

#include <netplus/socket.h>
#include <netplus/exception.h>

@@ -59,6 +62,7 @@ static constexpr uint8_t H2_FRAME_CONTINUATION = 0x09;
static constexpr uint8_t H2_FLAG_END_STREAM = 0x01;
static constexpr uint8_t H2_FLAG_ACK = 0x01;
static constexpr uint8_t H2_FLAG_END_HEADERS = 0x04;
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;
@@ -211,7 +215,57 @@ static std::vector<uint8_t> h3BuildResponse(uint16_t status_code,
}

libhttppp::HttpEvent::HttpEvent(std::vector<netplus::socket*> serversocket, int timeout) : netplus::event(serversocket, timeout) {
    // Set the HTTP/3 stream callback on QUIC server sockets so that
    // child connections created by accept() inherit it.  The event loop
    // never creates a con / calls ConnectEvent for QUIC children,
    // so this is the only opportunity to wire up Http3StreamEvent.
    for (auto* sock : serversocket) {
        if (auto* quicsock = dynamic_cast<netplus::quic*>(sock)) {
            quicsock->setStreamCallback([this](netplus::socket *s,
                                               uint64_t stream_id,
                                               const std::vector<uint8_t> &data,
                                               bool fin) {
                this->Http3StreamEvent(s, stream_id, data, fin);
            });

            // Extract the QUIC port so HTTP/2 responses can advertise Alt-Svc
            int h3port = -1;
            sockaddr* sa = reinterpret_cast<sockaddr*>(&quicsock->_Addr);
            if (sa->sa_family == AF_INET)
                h3port = ntohs(reinterpret_cast<sockaddr_in*>(sa)->sin_port);
            else if (sa->sa_family == AF_INET6)
                h3port = ntohs(reinterpret_cast<sockaddr_in6*>(sa)->sin6_port);
            if (h3port > 0)
                _altSvcH3 = "h3=\":" + std::to_string(h3port) + "\"; ma=86400";
        }

        // 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)) {
            // Prefer h2, fall back to http/1.1
            sslsock->setAlpnSelectCallback([](const std::vector<std::string>& protos) -> std::string {
                for (auto& p : protos) {
                    if (p == "h2") return "h2";
                }
                for (auto& p : protos) {
                    if (p == "http/1.1") return "http/1.1";
                }
                return protos.empty() ? "" : protos[0];
            });

            // Framing callback: stream_id==0 means "produce initial
            // connection data" (HTTP/2 SETTINGS frame).
            sslsock->setFramingCallback([](const std::string& alpn,
                                           uint32_t stream_id,
                                           const std::string& /*body*/,
                                           const std::string& /*content_type*/) -> std::string {
                if (alpn == "h2" && stream_id == 0) {
                    return h2BuildSettings();
                }
                return "";
            });
        }
    }
}

struct nop
@@ -246,6 +300,7 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon,
    (void)frame_cb;

    HttpRequest &cureq = dynamic_cast<HttpRequest&>(curcon);
    cureq._httpProtocol = 1;  // Mark connection as HTTP/2

    std::string out;
    size_t off = 0;
@@ -256,7 +311,7 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon,
        off += H2_CLIENT_PREFACE_LEN;
    }

    // Send server SETTINGS on first interaction
    // Send server SETTINGS if not already sent (e.g. ConnectEvent already sent it)
    if (cureq.SendData.empty()) {
        out += h2BuildSettings();
    }
@@ -288,25 +343,33 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon,
            break;

        case H2_FRAME_HEADERS: {
            // Decode HPACK headers from the HEADERS frame payload
            auto decoded = libhttppp::hpack::Decoder::decode(
                reinterpret_cast<const uint8_t*>(data + off), flen);
            // RFC 7540 §6.2: If PRIORITY flag is set, the first 5 bytes
            // are priority data (stream dependency + weight), not HPACK.
            const uint8_t *hpack_data = reinterpret_cast<const uint8_t*>(data + off);
            size_t hpack_len = flen;
            if ((fflags & H2_FLAG_PRIORITY) && hpack_len >= 5) {
                hpack_data += 5;
                hpack_len -= 5;
            }

            // Populate the HttpRequest via parseH2
            cureq.parseH2(decoded, sid);
            // Decode HPACK headers using the connection's decoder
            // (maintains dynamic table state across frames)
            auto decoded = cureq._h2HpackDecoder.decode(hpack_data, hpack_len);

            // Snapshot SendData before calling user handler
            size_t send_before = cureq.SendData.size();
            // Create a per-stream temporary HttpRequest (HTTP/2 multiplexes
            // multiple streams on one connection — each needs its own state)
            HttpRequest tempreq;
            tempreq.parseH2(decoded, sid);

            // Dispatch to user's virtual RequestEvent
            RequestEvent(cureq, tid, args);
            RequestEvent(tempreq, tid, args);

            // Read structured response info from :res-* pseudo-headers in _firstHeaderData
            auto *resValid = cureq.getHeaderData(":res-valid");
            // Read structured response info from :res-* pseudo-headers
            auto *resValid = tempreq.getHeaderData(":res-valid");
            if (resValid && resValid->getfirstValue()) {
                // Extract status code from :res-status (e.g. "200 OK" -> 200)
                uint16_t status_code = 200;
                auto *resStatus = cureq.getHeaderData(":res-status");
                auto *resStatus = tempreq.getHeaderData(":res-status");
                if (resStatus && resStatus->getfirstValue()) {
                    try { status_code = static_cast<uint16_t>(std::stoi(resStatus->getfirstValue()->getvalue())); }
                    catch (...) { status_code = 200; }
@@ -314,86 +377,96 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon,

                // Content type
                std::string content_type;
                auto *resCT = cureq.getHeaderData(":res-content-type");
                auto *resCT = tempreq.getHeaderData(":res-content-type");
                if (resCT && resCT->getfirstValue())
                    content_type = resCT->getfirstValue()->getvalue();

                // Content length
                size_t content_length = 0;
                auto *resCL = cureq.getHeaderData(":res-content-length");
                auto *resCL = tempreq.getHeaderData(":res-content-length");
                if (resCL && resCL->getfirstValue())
                    content_length = resCL->getfirstValue()->getSizetValue();

                // Capture body data from SendData
                // Capture body data from tempreq's SendData
                std::string body;
                size_t send_after = cureq.SendData.size();
                if (send_after > send_before) {
                    body.assign(cureq.SendData.data() + send_before,
                                send_after - send_before);
                    cureq.SendData.erase(
                        cureq.SendData.begin() + send_before,
                        cureq.SendData.begin() + send_after);
                if (!tempreq.SendData.empty()) {
                    body.assign(tempreq.SendData.data(), tempreq.SendData.size());
                    tempreq.SendData.clear();
                }

                bool is_streaming = (content_length > 0 &&
                                     body.size() < content_length);

                if (is_streaming) {
                    // Streaming response: buffer all data by calling ResponseEvent
                    // in a loop until all content is collected
                    cureq._h2ExpectedContentLength = content_length;
                    // Streaming response: collect all data via ResponseEvent
                    tempreq._h2ExpectedContentLength = content_length;
                    tempreq.SendData.pos = 0;

                    size_t max_iterations = content_length / 64 + 4096;
                    for (size_t i = 0;
                         i < max_iterations && body.size() < content_length;
                         ++i) {
                        size_t sb = cureq.SendData.size();
                        ResponseEvent(cureq, tid, args);
                        size_t sa = cureq.SendData.size();
                        size_t sb = tempreq.SendData.size();
                        ResponseEvent(tempreq, tid, args);
                        size_t sa = tempreq.SendData.size();
                        if (sa > sb) {
                            body.append(cureq.SendData.data() + sb, sa - sb);
                            cureq.SendData.erase(
                                cureq.SendData.begin() + sb,
                                cureq.SendData.begin() + sa);
                            body.append(tempreq.SendData.data() + sb, sa - sb);
                            tempreq.SendData.erase(
                                tempreq.SendData.begin() + sb,
                                tempreq.SendData.begin() + sa);
                        } else {
                            break; // No more data available
                        }
                    }
                    cureq.SendData.pos = 0;
                }

                // Collect extra :res-* headers (skip handled ones)
                std::vector<libhttppp::hpack::HeaderField> extra;
                for (auto *hd = cureq.getfirstHeaderData(); hd; hd = hd->nextHeaderData()) {

                // Advertise HTTP/3 via Alt-Svc so browsers discover QUIC
                if (!_altSvcH3.empty()) {
                    extra.push_back({"alt-svc", _altSvcH3});
                }

                for (auto *hd = tempreq.getfirstHeaderData(); hd; hd = hd->nextHeaderData()) {
                    const std::string &key = hd->getkey();
                    if (key.substr(0, 5) != ":res-") continue;
                    if (key == ":res-valid" || key == ":res-status" ||
                        key == ":res-content-type" || key == ":res-content-length") continue;
                    // Strip ":res-" prefix to get actual header name
                    std::string realKey = key.substr(5);
                    if (hd->getfirstValue())
                        extra.push_back({realKey, hd->getfirstValue()->getvalue()});
                }

                if (is_streaming || !body.empty()) {
                // Build HPACK-encoded HEADERS frame
                std::string hpack_out = libhttppp::hpack::Encoder::encodeResponseHeaders(
                    status_code, content_type, body.size(), extra);
                    out += h2BuildFrame(H2_FRAME_HEADERS, H2_FLAG_END_HEADERS, sid, hpack_out);

                if (body.size() <= H2_MAX_FRAME_SIZE) {
                    // Small response: send HEADERS + all DATA in one shot
                    uint8_t hdr_flags = H2_FLAG_END_HEADERS;
                    if (body.empty()) hdr_flags |= H2_FLAG_END_STREAM;
                    out += h2BuildFrame(H2_FRAME_HEADERS, hdr_flags, sid, hpack_out);
                    if (!body.empty()) {
                        h2AppendDataFrames(out, sid, body, true);
                } else {
                    out += h2BuildResponse(sid, status_code, body,
                                           content_type, extra, true);
                    }
                } else {
                    // Large response: send HEADERS + first chunk,
                    // queue remaining body for incremental sending
                    out += h2BuildFrame(H2_FRAME_HEADERS, H2_FLAG_END_HEADERS, sid, hpack_out);

                // Clean up :res-* pseudo-headers
                cureq.deldata(":res-valid");
                cureq.deldata(":res-status");
                cureq.deldata(":res-content-type");
                cureq.deldata(":res-content-length");
            }
                    // Send first chunk of DATA frames (up to H2_MAX_FRAME_SIZE)
                    std::string first_chunk = body.substr(0, H2_MAX_FRAME_SIZE);
                    out += h2BuildFrame(H2_FRAME_DATA, 0, sid, first_chunk);

            // Mark that this H2 stream is fully handled
            cureq._h2HeadersSent = false;
                    // Queue remaining body for incremental sending via ResponseEvent
                    HttpRequest::H2PendingResponse pending;
                    pending.streamId = sid;
                    pending.body = std::move(body);
                    pending.offset = H2_MAX_FRAME_SIZE;
                    cureq._h2PendingResponses.push_back(std::move(pending));
                }
            }
            break;
        }

@@ -599,14 +672,25 @@ REQUESTHANDLING:
void libhttppp::HttpEvent::ResponseEvent(netplus::con &curcon,const int tid,ULONG_PTR args){
    HttpRequest &cureq=static_cast<HttpRequest&>(curcon);
    try{
        // HTTP/2: all response data (including streaming) is handled in
        // Http2RequestEvent.  Just clean up once the network layer has
        // flushed SendData.
        // HTTP/2: connection is persistent and multiplexed.
        // Incrementally send pending H2 stream DATA frames.
        if (cureq._httpProtocol == 1) {
            if (curcon.SendData.empty()) {
                cureq.SendData.pos = 0;
                cureq._httpProtocol = 0;
                cureq.clear();
            if (!cureq._h2PendingResponses.empty()) {
                auto &pending = cureq._h2PendingResponses.front();
                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());
                    pending.offset += chunk;
                }
                if (pending.offset >= pending.body.size()) {
                    cureq._h2PendingResponses.pop_front();
                }
            }
            return;
        }
@@ -637,6 +721,7 @@ void libhttppp::HttpEvent::ConnectEvent(netplus::con &curcon,const int tid,ULONG
            this->Http3StreamEvent(sock, stream_id, data, fin);
        });
    }

    ConnectEvent(static_cast<HttpRequest&>(curcon), tid, args);
}

@@ -724,10 +809,11 @@ libhttppp::HttpD::HttpD(int argc, char** argv) {
                netplus::ssl::CertificateBundle bundle;
                bundle.cert = x509;
                bundle.privateKeyDer = keydata;
                bundle.rsa_key = netplus::rsa(keydata);
                
                _certBundle[httpaddr] = bundle;
                auto tss = std::make_unique<netplus::ssl>(_certBundle, httpaddr, port, maxconnections, -1);
                auto qss = std::make_unique<netplus::ssl>(_certBundle, httpaddr, port, maxconnections, -1);
                auto qss = std::make_unique<netplus::quic>(_certBundle, httpaddr, port, maxconnections, -1);
                _ServerSockets.push_back(std::move(tss));
                _ServerSockets.push_back(std::move(qss));
        }else{
@@ -782,12 +868,11 @@ libhttppp::HttpD::HttpD(const std::string& httpaddr, int port,int maxconnections
                keyfile.close();
                
                bundle.privateKeyDer = keydata;

                // Let ssl socket handle RSA key loading from privateKeyDer
                bundle.rsa_key = netplus::rsa(keydata);
                
                _certBundle[httpaddr] = bundle;
                auto tss = std::make_unique<netplus::ssl>(_certBundle, httpaddr, port, maxconnections, -1);
                auto qss = std::make_unique<netplus::ssl>(_certBundle, httpaddr, port, maxconnections, -1);
                auto qss = std::make_unique<netplus::quic>(_certBundle, httpaddr, port, maxconnections, -1);

                _ServerSockets.push_back(std::move(tss));
                _ServerSockets.push_back(std::move(qss));
Loading