Loading src/hpack.cpp +74 −9 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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) Loading @@ -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); Loading @@ -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); Loading @@ -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 } Loading src/hpack.h +23 −2 Original line number Diff line number Diff line Loading @@ -29,6 +29,7 @@ #include <cstddef> #include <cstdint> #include <deque> #include <string> #include <vector> Loading Loading @@ -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[]; Loading src/http.cpp +1 −0 Original line number Diff line number Diff line Loading @@ -1553,6 +1553,7 @@ void libhttppp::HttpRequest::clear(){ _h2HeadersSent = false; _h2ExpectedContentLength = 0; _h2BodyBytesSent = 0; _h2PendingResponses.clear(); } size_t libhttppp::HttpRequest::parse() { Loading src/http.h +13 −1 Original line number Diff line number Diff line Loading @@ -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> Loading @@ -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 { Loading Loading @@ -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; Loading src/httpd.cpp +147 −62 Original line number Diff line number Diff line Loading @@ -31,6 +31,9 @@ #include <cctype> #include <string> #include <sys/socket.h> #include <netinet/in.h> #include <netplus/socket.h> #include <netplus/exception.h> Loading Loading @@ -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; Loading Loading @@ -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 Loading Loading @@ -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; Loading @@ -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(); } Loading Loading @@ -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; } Loading @@ -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; } Loading Loading @@ -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; } Loading Loading @@ -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); } Loading Loading @@ -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{ Loading Loading @@ -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 Loading
src/hpack.cpp +74 −9 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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) Loading @@ -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); Loading @@ -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); Loading @@ -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 } Loading
src/hpack.h +23 −2 Original line number Diff line number Diff line Loading @@ -29,6 +29,7 @@ #include <cstddef> #include <cstdint> #include <deque> #include <string> #include <vector> Loading Loading @@ -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[]; Loading
src/http.cpp +1 −0 Original line number Diff line number Diff line Loading @@ -1553,6 +1553,7 @@ void libhttppp::HttpRequest::clear(){ _h2HeadersSent = false; _h2ExpectedContentLength = 0; _h2BodyBytesSent = 0; _h2PendingResponses.clear(); } size_t libhttppp::HttpRequest::parse() { Loading
src/http.h +13 −1 Original line number Diff line number Diff line Loading @@ -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> Loading @@ -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 { Loading Loading @@ -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; Loading
src/httpd.cpp +147 −62 Original line number Diff line number Diff line Loading @@ -31,6 +31,9 @@ #include <cctype> #include <string> #include <sys/socket.h> #include <netinet/in.h> #include <netplus/socket.h> #include <netplus/exception.h> Loading Loading @@ -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; Loading Loading @@ -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 Loading Loading @@ -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; Loading @@ -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(); } Loading Loading @@ -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; } Loading @@ -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; } Loading Loading @@ -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; } Loading Loading @@ -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); } Loading Loading @@ -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{ Loading Loading @@ -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