Loading src/crypto/tls.cpp +60 −0 Original line number Diff line number Diff line Loading @@ -37,6 +37,28 @@ namespace netplus { } // Helper function to extract SNI from ClientHello // ALPN (RFC 7301) extension carrying exactly one protocol -- what a server answers with. static void appendAlpnExtension(std::vector<uint8_t>& exts, const std::string& proto) { if (proto.empty() || proto.size() > 255) return; const uint16_t list_len = uint16_t(1 + proto.size()); const uint16_t ext_len = uint16_t(2 + list_len); exts.push_back(0x00); exts.push_back(0x10); exts.push_back(uint8_t(ext_len >> 8)); exts.push_back(uint8_t(ext_len & 0xFF)); exts.push_back(uint8_t(list_len >> 8)); exts.push_back(uint8_t(list_len & 0xFF)); exts.push_back(uint8_t(proto.size())); exts.insert(exts.end(), proto.begin(), proto.end()); } // The protocol a server selected, from its ALPN extension data (list_len(2) + one // len-prefixed name). Empty if malformed. static std::string parseAlpnSelection(const uint8_t* ep, size_t el) { if (el < 3) return {}; size_t list_len = (size_t(ep[0]) << 8) | ep[1]; size_t name_len = ep[2]; if (list_len + 2 != el || name_len == 0 || 3 + name_len > el) return {}; return std::string(reinterpret_cast<const char*>(ep + 3), name_len); } static bool extractSNIFromClientHello(const std::vector<uint8_t>& ch, std::string& hostname) { // ClientHello format: type(1) + length(3) + version(2) + random(32) + session_id_len(1) + ... if (ch.size() < 43) return false; Loading Loading @@ -770,6 +792,7 @@ namespace netplus { exts.push_back(0x00); exts.push_back(0x17); exts.push_back(0x00); exts.push_back(0x00); } appendAlpnExtension(exts, selected_alpn); body.push_back((exts.size() >> 8) & 0xFF); body.push_back((exts.size() >> 0) & 0xFF); body.insert(body.end(), exts.begin(), exts.end()); Loading Loading @@ -1706,6 +1729,11 @@ namespace netplus { const uint8_t* ep = p + eoff; // ALPN: a TLS 1.2 server answers here (TLS 1.3: EncryptedExtensions) if (et == 0x0010) { selected_alpn = parseAlpnSelection(ep, el); } // supported_versions (0x002b): for ServerHello it's 2 bytes selected version if (et == 0x002b && el == 2) { uint16_t sel = (uint16_t(ep[0]) << 8) | ep[1]; Loading Loading @@ -1898,6 +1926,21 @@ namespace netplus { throw e; } // ALPN: TLS 1.3 servers answer in EncryptedExtensions (type(1)+len(3)+ // extensions_length(2)+extensions). if (msg.size() >= 6) { size_t eoff = 6; size_t eend = std::min(msg.size(), 6 + ((size_t(msg[4]) << 8) | msg[5])); while (eoff + 4 <= eend) { uint16_t et = (uint16_t(msg[eoff]) << 8) | msg[eoff+1]; uint16_t el = (uint16_t(msg[eoff+2]) << 8) | msg[eoff+3]; eoff += 4; if (eoff + el > eend) break; if (et == 0x0010) selected_alpn = parseAlpnSelection(msg.data() + eoff, el); eoff += el; } } // Add to transcript handshake_transcript.insert(handshake_transcript.end(), msg.begin(), msg.end()); Loading Loading @@ -4315,6 +4358,10 @@ namespace netplus { exts.push_back(0x00); exts.push_back(0x00); } // ALPN (RFC 7301): TLS 1.2 answers in ServerHello -- it used to be negotiated // server-side but never sent back, so the client couldn't know the result. appendAlpnExtension(exts, selected_alpn); // Write extensions length (always present in TLS1.2 ServerHello!) body.push_back((exts.size() >> 8) & 0xFF); body.push_back((exts.size() >> 0) & 0xFF); Loading Loading @@ -4941,6 +4988,19 @@ namespace netplus { exts.push_back(0x00); exts.push_back(0x17); exts.push_back(0x00); exts.push_back(0x00); // ALPN (RFC 7301). client_alpn_protocols is the wire-format protocol_name_list body // (length-prefixed names, e.g. "\x02h2\x08http/1.1"). It used to be set by callers but // never sent, so every server fell back to its default (HTTP/1.1). if (!client_alpn_protocols.empty() && client_alpn_protocols.size() <= 0xFFFD) { const uint16_t list_len = uint16_t(client_alpn_protocols.size()); const uint16_t ext_len = uint16_t(2 + list_len); exts.push_back(0x00); exts.push_back(0x10); exts.push_back(uint8_t(ext_len >> 8)); exts.push_back(uint8_t(ext_len & 0xFF)); exts.push_back(uint8_t(list_len >> 8)); exts.push_back(uint8_t(list_len & 0xFF)); exts.insert(exts.end(), client_alpn_protocols.begin(), client_alpn_protocols.end()); } selected_alpn.clear(); // Mark that we offered TLS 1.3 (only when not in TLS 1.2-only mode) client_offered_tls13 = !client_tls12_only; Loading src/quic.cpp +25 −0 Original line number Diff line number Diff line Loading @@ -4945,6 +4945,31 @@ void quic::flushFlowControl() { flushBatch(); } size_t quic::openStreamCount(bool bidirectionalOnly) const { std::lock_guard<std::recursive_mutex> lock(quic_mtx()); if (!bidirectionalOnly) return _streams.size(); size_t n = 0; for (const auto &[id, st] : _streams) if ((id & 0x2) == 0) ++n; // RFC 9000 §2.1: bit 1 clear = bidirectional return n; } size_t quic::childOpenStreamCount(bool bidirectionalOnly) const { // Pin first, count after: each child's own lock is taken outside _registry_mutex, and a // child is registered under two CIDs -- count it once. std::vector<std::shared_ptr<quic>> pins; { std::shared_lock<netplus::WriterPreferringSharedMutex> lk(_registry_mutex); for (const auto &[cid, child] : _child_connections) { if (child && std::find(pins.begin(), pins.end(), child) == pins.end()) pins.push_back(child); } } size_t total = 0; for (const auto &c : pins) total += c->openStreamCount(bidirectionalOnly); return total; } bool quic::isStreamSendStopped(uint64_t stream_id) const { std::lock_guard<std::recursive_mutex> lock(quic_mtx()); auto it = _streams.find(stream_id); Loading src/socket.h +9 −0 Original line number Diff line number Diff line Loading @@ -892,6 +892,15 @@ namespace netplus { // stays readable until closeStream(). bool isStreamSendStopped(uint64_t stream_id) const; // Diagnostics: streams this connection still tracks, i.e. not yet retired (a stream // retires once both directions finished and the application called closeStream() // or resetStream()). A count that only grows means streams are leaking. // bidirectionalOnly: skip unidirectional streams (e.g. HTTP/3's control and QPACK // streams, which live as long as the connection by design). size_t openStreamCount(bool bidirectionalOnly = false) const; // Listener: openStreamCount() summed over its accepted connections. size_t childOpenStreamCount(bool bidirectionalOnly = false) const; virtual std::string selectStreamProtocol(const std::string &proto) { return ""; } // Abort a stream abnormally (RFC 9000 §19.4 RESET_STREAM). Use this Loading Loading
src/crypto/tls.cpp +60 −0 Original line number Diff line number Diff line Loading @@ -37,6 +37,28 @@ namespace netplus { } // Helper function to extract SNI from ClientHello // ALPN (RFC 7301) extension carrying exactly one protocol -- what a server answers with. static void appendAlpnExtension(std::vector<uint8_t>& exts, const std::string& proto) { if (proto.empty() || proto.size() > 255) return; const uint16_t list_len = uint16_t(1 + proto.size()); const uint16_t ext_len = uint16_t(2 + list_len); exts.push_back(0x00); exts.push_back(0x10); exts.push_back(uint8_t(ext_len >> 8)); exts.push_back(uint8_t(ext_len & 0xFF)); exts.push_back(uint8_t(list_len >> 8)); exts.push_back(uint8_t(list_len & 0xFF)); exts.push_back(uint8_t(proto.size())); exts.insert(exts.end(), proto.begin(), proto.end()); } // The protocol a server selected, from its ALPN extension data (list_len(2) + one // len-prefixed name). Empty if malformed. static std::string parseAlpnSelection(const uint8_t* ep, size_t el) { if (el < 3) return {}; size_t list_len = (size_t(ep[0]) << 8) | ep[1]; size_t name_len = ep[2]; if (list_len + 2 != el || name_len == 0 || 3 + name_len > el) return {}; return std::string(reinterpret_cast<const char*>(ep + 3), name_len); } static bool extractSNIFromClientHello(const std::vector<uint8_t>& ch, std::string& hostname) { // ClientHello format: type(1) + length(3) + version(2) + random(32) + session_id_len(1) + ... if (ch.size() < 43) return false; Loading Loading @@ -770,6 +792,7 @@ namespace netplus { exts.push_back(0x00); exts.push_back(0x17); exts.push_back(0x00); exts.push_back(0x00); } appendAlpnExtension(exts, selected_alpn); body.push_back((exts.size() >> 8) & 0xFF); body.push_back((exts.size() >> 0) & 0xFF); body.insert(body.end(), exts.begin(), exts.end()); Loading Loading @@ -1706,6 +1729,11 @@ namespace netplus { const uint8_t* ep = p + eoff; // ALPN: a TLS 1.2 server answers here (TLS 1.3: EncryptedExtensions) if (et == 0x0010) { selected_alpn = parseAlpnSelection(ep, el); } // supported_versions (0x002b): for ServerHello it's 2 bytes selected version if (et == 0x002b && el == 2) { uint16_t sel = (uint16_t(ep[0]) << 8) | ep[1]; Loading Loading @@ -1898,6 +1926,21 @@ namespace netplus { throw e; } // ALPN: TLS 1.3 servers answer in EncryptedExtensions (type(1)+len(3)+ // extensions_length(2)+extensions). if (msg.size() >= 6) { size_t eoff = 6; size_t eend = std::min(msg.size(), 6 + ((size_t(msg[4]) << 8) | msg[5])); while (eoff + 4 <= eend) { uint16_t et = (uint16_t(msg[eoff]) << 8) | msg[eoff+1]; uint16_t el = (uint16_t(msg[eoff+2]) << 8) | msg[eoff+3]; eoff += 4; if (eoff + el > eend) break; if (et == 0x0010) selected_alpn = parseAlpnSelection(msg.data() + eoff, el); eoff += el; } } // Add to transcript handshake_transcript.insert(handshake_transcript.end(), msg.begin(), msg.end()); Loading Loading @@ -4315,6 +4358,10 @@ namespace netplus { exts.push_back(0x00); exts.push_back(0x00); } // ALPN (RFC 7301): TLS 1.2 answers in ServerHello -- it used to be negotiated // server-side but never sent back, so the client couldn't know the result. appendAlpnExtension(exts, selected_alpn); // Write extensions length (always present in TLS1.2 ServerHello!) body.push_back((exts.size() >> 8) & 0xFF); body.push_back((exts.size() >> 0) & 0xFF); Loading Loading @@ -4941,6 +4988,19 @@ namespace netplus { exts.push_back(0x00); exts.push_back(0x17); exts.push_back(0x00); exts.push_back(0x00); // ALPN (RFC 7301). client_alpn_protocols is the wire-format protocol_name_list body // (length-prefixed names, e.g. "\x02h2\x08http/1.1"). It used to be set by callers but // never sent, so every server fell back to its default (HTTP/1.1). if (!client_alpn_protocols.empty() && client_alpn_protocols.size() <= 0xFFFD) { const uint16_t list_len = uint16_t(client_alpn_protocols.size()); const uint16_t ext_len = uint16_t(2 + list_len); exts.push_back(0x00); exts.push_back(0x10); exts.push_back(uint8_t(ext_len >> 8)); exts.push_back(uint8_t(ext_len & 0xFF)); exts.push_back(uint8_t(list_len >> 8)); exts.push_back(uint8_t(list_len & 0xFF)); exts.insert(exts.end(), client_alpn_protocols.begin(), client_alpn_protocols.end()); } selected_alpn.clear(); // Mark that we offered TLS 1.3 (only when not in TLS 1.2-only mode) client_offered_tls13 = !client_tls12_only; Loading
src/quic.cpp +25 −0 Original line number Diff line number Diff line Loading @@ -4945,6 +4945,31 @@ void quic::flushFlowControl() { flushBatch(); } size_t quic::openStreamCount(bool bidirectionalOnly) const { std::lock_guard<std::recursive_mutex> lock(quic_mtx()); if (!bidirectionalOnly) return _streams.size(); size_t n = 0; for (const auto &[id, st] : _streams) if ((id & 0x2) == 0) ++n; // RFC 9000 §2.1: bit 1 clear = bidirectional return n; } size_t quic::childOpenStreamCount(bool bidirectionalOnly) const { // Pin first, count after: each child's own lock is taken outside _registry_mutex, and a // child is registered under two CIDs -- count it once. std::vector<std::shared_ptr<quic>> pins; { std::shared_lock<netplus::WriterPreferringSharedMutex> lk(_registry_mutex); for (const auto &[cid, child] : _child_connections) { if (child && std::find(pins.begin(), pins.end(), child) == pins.end()) pins.push_back(child); } } size_t total = 0; for (const auto &c : pins) total += c->openStreamCount(bidirectionalOnly); return total; } bool quic::isStreamSendStopped(uint64_t stream_id) const { std::lock_guard<std::recursive_mutex> lock(quic_mtx()); auto it = _streams.find(stream_id); Loading
src/socket.h +9 −0 Original line number Diff line number Diff line Loading @@ -892,6 +892,15 @@ namespace netplus { // stays readable until closeStream(). bool isStreamSendStopped(uint64_t stream_id) const; // Diagnostics: streams this connection still tracks, i.e. not yet retired (a stream // retires once both directions finished and the application called closeStream() // or resetStream()). A count that only grows means streams are leaking. // bidirectionalOnly: skip unidirectional streams (e.g. HTTP/3's control and QPACK // streams, which live as long as the connection by design). size_t openStreamCount(bool bidirectionalOnly = false) const; // Listener: openStreamCount() summed over its accepted connections. size_t childOpenStreamCount(bool bidirectionalOnly = false) const; virtual std::string selectStreamProtocol(const std::string &proto) { return ""; } // Abort a stream abnormally (RFC 9000 §19.4 RESET_STREAM). Use this Loading