diff --git a/CMakeLists.txt b/CMakeLists.txt index 8111b49d44..dbe557f083 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ option(BUILD_BROTLI "Build Brotli" ON) option(BUILD_YAML_CONFIG "Build yaml config" ON) option(USE_SUBMODULE "Use trantor as a submodule" ON) option(USE_STATIC_LIBS_ONLY "Use only static libraries as dependencies" OFF) +option(BUILD_HTTP3 "Build with HTTP/3 (QUIC) support via ngtcp2/nghttp3" OFF) include(CMakeDependentOption) CMAKE_DEPENDENT_OPTION(BUILD_POSTGRESQL "Build with postgresql support" ON "BUILD_ORM" OFF) @@ -252,6 +253,34 @@ if (BUILD_BROTLI) endif (Brotli_FOUND) endif (BUILD_BROTLI) +if (BUILD_HTTP3) + find_package(PkgConfig QUIET) + if (PkgConfig_FOUND) + pkg_check_modules(NGTCP2 IMPORTED_TARGET libngtcp2) + # ngtcp2 v1.x renamed crypto_quictls to crypto_ossl — try both + pkg_check_modules(NGTCP2_CRYPTO IMPORTED_TARGET libngtcp2_crypto_quictls) + if (NOT NGTCP2_CRYPTO_FOUND) + pkg_check_modules(NGTCP2_CRYPTO IMPORTED_TARGET libngtcp2_crypto_ossl) + endif() + pkg_check_modules(NGHTTP3 IMPORTED_TARGET libnghttp3) + endif() + if (NGTCP2_FOUND AND NGTCP2_CRYPTO_FOUND AND NGHTTP3_FOUND) + message(STATUS "HTTP/3 (QUIC) support enabled") + message(STATUS " ngtcp2: ${NGTCP2_VERSION}") + message(STATUS " ngtcp2_crypto: ${NGTCP2_CRYPTO_VERSION}") + message(STATUS " nghttp3: ${NGHTTP3_VERSION}") + target_compile_definitions(${PROJECT_NAME} PUBLIC DROGON_HAS_HTTP3=1) + target_link_libraries(${PROJECT_NAME} PRIVATE + PkgConfig::NGTCP2 + PkgConfig::NGTCP2_CRYPTO + PkgConfig::NGHTTP3) + else() + message(WARNING "HTTP/3 requested but ngtcp2/nghttp3 not found. " + "Install libngtcp2, libngtcp2_crypto_quictls, and libnghttp3. " + "HTTP/3 support will be disabled.") + endif() +endif (BUILD_HTTP3) + set(DROGON_SOURCES lib/src/AOPAdvice.cc lib/src/AccessLogger.cc @@ -502,6 +531,19 @@ if (NOT Hiredis_FOUND) lib/src/RedisClientManager.h) endif (NOT Hiredis_FOUND) +if (BUILD_HTTP3 AND NGTCP2_FOUND AND NGTCP2_CRYPTO_FOUND AND NGHTTP3_FOUND) + set(DROGON_SOURCES + ${DROGON_SOURCES} + lib/src/QuicServer.cc + lib/src/QuicConnection.cc + lib/src/Http3Handler.cc) + set(private_headers + ${private_headers} + lib/src/QuicServer.h + lib/src/QuicConnection.h + lib/src/Http3Handler.h) +endif() + if (BUILD_TESTING) add_subdirectory(nosql_lib/redis/tests) endif (BUILD_TESTING) diff --git a/lib/inc/drogon/HttpTypes.h b/lib/inc/drogon/HttpTypes.h index 63407fd4c1..b8829902e3 100644 --- a/lib/inc/drogon/HttpTypes.h +++ b/lib/inc/drogon/HttpTypes.h @@ -93,7 +93,8 @@ enum class Version { kUnknown = 0, kHttp10, - kHttp11 + kHttp11, + kHttp3 }; enum ContentType diff --git a/lib/src/Http3AltSvcMiddleware.h b/lib/src/Http3AltSvcMiddleware.h new file mode 100644 index 0000000000..efb4dd5b8d --- /dev/null +++ b/lib/src/Http3AltSvcMiddleware.h @@ -0,0 +1,83 @@ +/** + * + * @file Http3AltSvcMiddleware.h + * @author S Bala Vignesh + * + * Copyright 2026, S Bala Vignesh. All rights reserved. + * https://github.com/drogonframework/drogon + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Drogon + * + */ + +#pragma once + +#ifdef DROGON_HAS_HTTP3 + +#include +#include + +namespace drogon +{ + +/** + * @brief Middleware that injects the Alt-Svc header into HTTP/1.1 and HTTP/2 + * responses to advertise HTTP/3 support to clients. + * + * When a browser receives an "Alt-Svc: h3=\":443\"; ma=86400" header, + * it knows it can upgrade to HTTP/3 for subsequent requests. + * + * Usage in config.json: + * @code + * { + * "middlewares": [ + * { + * "name": "drogon::Http3AltSvcMiddleware", + * "config": { + * "port": 443 + * } + * } + * ] + * } + * @endcode + */ +class Http3AltSvcMiddleware + : public HttpMiddleware +{ + public: + Http3AltSvcMiddleware() = default; + + /** + * @brief Set the QUIC port for the Alt-Svc header. + * @param port UDP port where HTTP/3 is available + */ + void setPort(uint16_t port) + { + altSvcValue_ = "h3=\":" + std::to_string(port) + "\"; ma=86400"; + } + + void invoke(const HttpRequestPtr &req, + MiddlewareNextCallback &&nextCb, + MiddlewareCallback &&mcb) override + { + auto altSvc = altSvcValue_; + nextCb([altSvc = std::move(altSvc), + mcb = std::move(mcb)](const HttpResponsePtr &resp) { + // Only inject Alt-Svc on non-HTTP/3 responses + if (resp && resp->getHeader("alt-svc").empty()) + { + resp->addHeader("alt-svc", altSvc); + } + mcb(resp); + }); + } + + private: + std::string altSvcValue_{"h3=\":443\"; ma=86400"}; +}; + +} // namespace drogon + +#endif // DROGON_HAS_HTTP3 diff --git a/lib/src/Http3Handler.cc b/lib/src/Http3Handler.cc new file mode 100644 index 0000000000..a1a05541c2 --- /dev/null +++ b/lib/src/Http3Handler.cc @@ -0,0 +1,120 @@ +/** + * + * @file Http3Handler.cc + * @author S Bala Vignesh + * + * Copyright 2026, S Bala Vignesh. All rights reserved. + * https://github.com/drogonframework/drogon + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Drogon + * + */ + +#ifdef DROGON_HAS_HTTP3 + +#include "Http3Handler.h" +#include "HttpUtils.h" +#include +#include + +namespace drogon +{ +namespace Http3Handler +{ + +HttpRequestImplPtr createRequest(const std::string &method, + const std::string &path, + const std::string &authority, + const std::string &scheme) +{ + auto req = std::make_shared(nullptr); + req->setMethod(methodFromString(method)); + req->setPath(path); + req->addHeader("host", authority); + req->addHeader(":scheme", scheme); + req->setVersion(drogon::Version::kHttp3); + return req; +} + +SerializedHeaders serializeResponseHeaders(const HttpResponsePtr &resp) +{ + SerializedHeaders result; + + // Status pseudo-header + result.statusStr = std::to_string(resp->statusCode()); + + // Build nghttp3_nv for :status + nghttp3_nv statusNv; + statusNv.name = reinterpret_cast( + const_cast(":status")); + statusNv.namelen = 7; + statusNv.value = reinterpret_cast( + const_cast(result.statusStr.c_str())); + statusNv.valuelen = result.statusStr.size(); + statusNv.flags = NGHTTP3_NV_FLAG_NONE; + result.nva.push_back(statusNv); + + // Regular headers + auto respImpl = + std::dynamic_pointer_cast(resp); + if (respImpl) + { + for (auto &[name, value] : respImpl->headers()) + { + result.headerStorage.emplace_back(name, value); + auto &stored = result.headerStorage.back(); + + nghttp3_nv nv; + nv.name = reinterpret_cast( + const_cast(stored.first.c_str())); + nv.namelen = stored.first.size(); + nv.value = reinterpret_cast( + const_cast(stored.second.c_str())); + nv.valuelen = stored.second.size(); + nv.flags = NGHTTP3_NV_FLAG_NONE; + result.nva.push_back(nv); + } + } + + return result; +} + +std::pair getResponseBody( + const HttpResponsePtr &resp) +{ + auto body = resp->body(); + return {reinterpret_cast(body.data()), body.size()}; +} + +HttpMethod methodFromString(const std::string &method) +{ + if (method == "GET") + return HttpMethod::Get; + if (method == "POST") + return HttpMethod::Post; + if (method == "PUT") + return HttpMethod::Put; + if (method == "DELETE") + return HttpMethod::Delete; + if (method == "PATCH") + return HttpMethod::Patch; + if (method == "HEAD") + return HttpMethod::Head; + if (method == "OPTIONS") + return HttpMethod::Options; + return HttpMethod::Invalid; +} + +std::string getAltSvcHeaderValue(uint16_t port) +{ + std::ostringstream oss; + oss << "h3=\":" << port << "\"; ma=86400"; + return oss.str(); +} + +} // namespace Http3Handler +} // namespace drogon + +#endif // DROGON_HAS_HTTP3 diff --git a/lib/src/Http3Handler.h b/lib/src/Http3Handler.h new file mode 100644 index 0000000000..0358b31250 --- /dev/null +++ b/lib/src/Http3Handler.h @@ -0,0 +1,96 @@ +/** + * + * @file Http3Handler.h + * @author S Bala Vignesh + * + * Copyright 2026, S Bala Vignesh. All rights reserved. + * https://github.com/drogonframework/drogon + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Drogon + * + */ + +#pragma once + +#ifdef DROGON_HAS_HTTP3 + +#include "HttpRequestImpl.h" +#include "HttpResponseImpl.h" +#include +#include +#include +#include + +namespace drogon +{ + +/** + * @brief Http3Handler provides utility functions for converting between + * HTTP/3 (nghttp3) representations and Drogon's HttpRequest/HttpResponse. + * + * It acts as the "glue" between the QUIC/HTTP/3 protocol layer and + * Drogon's existing request processing pipeline. + */ +namespace Http3Handler +{ + +/** + * @brief Create a Drogon HttpRequestImpl from HTTP/3 pseudo-headers. + * @param method The HTTP method (e.g., "GET", "POST") + * @param path The request path + * @param authority The authority (host) header + * @param scheme The scheme ("https") + * @return A new HttpRequestImpl ready for header population + */ +HttpRequestImplPtr createRequest(const std::string &method, + const std::string &path, + const std::string &authority, + const std::string &scheme); + +/** + * @brief Serialize an HttpResponse into nghttp3 header name-value pairs + * for transmission over HTTP/3. + * @param resp The response to serialize + * @return A vector of nghttp3_nv header pairs + * @note The returned nv pairs reference memory in the returned strings. + * The caller must keep the strings alive until headers are sent. + */ +struct SerializedHeaders +{ + std::vector nva; + // Storage for header strings to keep them alive + std::string statusStr; + std::vector> headerStorage; +}; + +SerializedHeaders serializeResponseHeaders(const HttpResponsePtr &resp); + +/** + * @brief Get the response body as a contiguous buffer. + * @param resp The response + * @return Pointer and length of the body data + */ +std::pair getResponseBody( + const HttpResponsePtr &resp); + +/** + * @brief Convert an HTTP method string to Drogon's HttpMethod enum. + * @param method The method string (e.g., "GET") + * @return The corresponding HttpMethod + */ +HttpMethod methodFromString(const std::string &method); + +/** + * @brief Get the Alt-Svc header value for advertising HTTP/3 support. + * @param port The UDP port for HTTP/3 + * @return The Alt-Svc header value string (e.g., 'h3=":443"') + */ +std::string getAltSvcHeaderValue(uint16_t port); + +} // namespace Http3Handler + +} // namespace drogon + +#endif // DROGON_HAS_HTTP3 diff --git a/lib/src/HttpRequestImpl.cc b/lib/src/HttpRequestImpl.cc index 68ab040a28..7565960fab 100644 --- a/lib/src/HttpRequestImpl.cc +++ b/lib/src/HttpRequestImpl.cc @@ -626,6 +626,12 @@ const char *HttpRequestImpl::versionString() const result = "HTTP/1.1"; break; +#ifdef DROGON_HAS_HTTP3 + case Version::kHttp3: + result = "HTTP/3"; + break; +#endif + default: break; } diff --git a/lib/src/HttpResponseImpl.cc b/lib/src/HttpResponseImpl.cc index 7a36ceeba7..9958c8a21d 100644 --- a/lib/src/HttpResponseImpl.cc +++ b/lib/src/HttpResponseImpl.cc @@ -108,6 +108,12 @@ const char *HttpResponseImpl::versionString() const result = "HTTP/1.1"; break; +#ifdef DROGON_HAS_HTTP3 + case Version::kHttp3: + result = "HTTP/3"; + break; +#endif + default: break; } diff --git a/lib/src/ListenerManager.cc b/lib/src/ListenerManager.cc index 5b7483280b..7c069a74d3 100644 --- a/lib/src/ListenerManager.cc +++ b/lib/src/ListenerManager.cc @@ -57,12 +57,14 @@ void ListenerManager::addListener( const std::string &certFile, const std::string &keyFile, bool useOldTLS, - const std::vector> &sslConfCmds) + const std::vector> &sslConfCmds, + bool enableHttp3) { if (useSSL && !utils::supportsTls()) LOG_ERROR << "Can't use SSL without OpenSSL found in your system"; listeners_.emplace_back( - ip, port, useSSL, certFile, keyFile, useOldTLS, sslConfCmds); + ip, port, useSSL, certFile, keyFile, useOldTLS, sslConfCmds, + enableHttp3); } std::vector ListenerManager::getListeners() const @@ -194,6 +196,44 @@ void ListenerManager::createListeners( } } #endif + +#ifdef DROGON_HAS_HTTP3 + // Create QuicServers for listeners with HTTP/3 enabled + for (auto const &listener : listeners_) + { + if (!listener.enableHttp3_) + continue; + + auto cert = listener.certFile_; + auto key = listener.keyFile_; + if (cert.empty()) + cert = globalCertFile; + if (key.empty()) + key = globalKeyFile; + + if (cert.empty() || key.empty()) + { + LOG_ERROR << "HTTP/3 requires TLS certificate and key files"; + continue; + } + + auto ip = listener.ip_; + bool isIpv6 = (ip.find(':') != std::string::npos); + InetAddress listenAddress(ip, listener.port_, isIpv6); + + auto quicServer = std::make_shared( + HttpAppFrameworkImpl::instance().getLoop(), + listenAddress, + cert, + key); + + quicServer->setIoLoops(ioLoops); + quicServers_.push_back(quicServer); + + LOG_INFO << "HTTP/3 (QUIC) listener configured on " + << listenAddress.toIpPort(); + } +#endif } void ListenerManager::startListening() @@ -202,6 +242,12 @@ void ListenerManager::startListening() { server->start(); } +#ifdef DROGON_HAS_HTTP3 + for (auto &quicServer : quicServers_) + { + quicServer->start(); + } +#endif } void ListenerManager::stopListening() @@ -210,6 +256,13 @@ void ListenerManager::stopListening() { serverPtr->stop(); } +#ifdef DROGON_HAS_HTTP3 + for (auto &quicServer : quicServers_) + { + quicServer->stop(); + } + quicServers_.clear(); +#endif if (listeningThread_) { auto loop = listeningThread_->getLoop(); diff --git a/lib/src/ListenerManager.h b/lib/src/ListenerManager.h index 6cbabe145b..ac3ecfdb9a 100644 --- a/lib/src/ListenerManager.h +++ b/lib/src/ListenerManager.h @@ -23,6 +23,10 @@ #include #include "impl_forwards.h" +#ifdef DROGON_HAS_HTTP3 +#include "QuicServer.h" +#endif + namespace trantor { class InetAddress; @@ -41,7 +45,8 @@ class ListenerManager : public trantor::NonCopyable const std::string &keyFile = "", bool useOldTLS = false, const std::vector> - &sslConfCmds = {}); + &sslConfCmds = {}, + bool enableHttp3 = false); std::vector getListeners() const; void createListeners( const std::string &globalCertFile, @@ -79,14 +84,16 @@ class ListenerManager : public trantor::NonCopyable std::string certFile, std::string keyFile, bool useOldTLS, - std::vector> sslConfCmds) + std::vector> sslConfCmds, + bool enableHttp3 = false) : ip_(std::move(ip)), port_(port), useSSL_(useSSL), certFile_(std::move(certFile)), keyFile_(std::move(keyFile)), useOldTLS_(useOldTLS), - sslConfCmds_(std::move(sslConfCmds)) + sslConfCmds_(std::move(sslConfCmds)), + enableHttp3_(enableHttp3) { } @@ -97,10 +104,14 @@ class ListenerManager : public trantor::NonCopyable std::string keyFile_; bool useOldTLS_; std::vector> sslConfCmds_; + bool enableHttp3_{false}; }; std::vector listeners_; std::vector> servers_; +#ifdef DROGON_HAS_HTTP3 + std::vector> quicServers_; +#endif // should have value when and only when on OS that one port can only be // listened by one thread diff --git a/lib/src/QuicConnection.cc b/lib/src/QuicConnection.cc new file mode 100644 index 0000000000..9aa659430c --- /dev/null +++ b/lib/src/QuicConnection.cc @@ -0,0 +1,1003 @@ +/** + * + * @file QuicConnection.cc + * @author S Bala Vignesh + * + * Copyright 2026, S Bala Vignesh. All rights reserved. + * https://github.com/drogonframework/drogon + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Drogon + * + */ + +#ifdef DROGON_HAS_HTTP3 + +#include "QuicConnection.h" +#include "Http3Handler.h" +#include +#include +#include +#include +#include +#include + +namespace +{ + +/** + * @brief Get current timestamp in nanoseconds for ngtcp2. + * ngtcp2 requires timestamps in nanoseconds from a monotonic clock. + */ +ngtcp2_tstamp quicTimestamp() +{ + auto now = std::chrono::steady_clock::now(); + return static_cast( + std::chrono::duration_cast( + now.time_since_epoch()) + .count()); +} + +} // anonymous namespace + +namespace drogon +{ + +QuicConnection::QuicConnection(QuicServer *server, + trantor::EventLoop *loop, + const ngtcp2_cid &dcid, + const ngtcp2_cid &scid, + const struct sockaddr *remoteAddr, + socklen_t remoteAddrLen, + const struct sockaddr *localAddr, + socklen_t localAddrLen, + uint32_t version, + SSL_CTX *sslCtx) + : server_(server), + loop_(loop), + dcid_(dcid), + scid_(scid), + remoteAddrLen_(remoteAddrLen), + localAddrLen_(localAddrLen), + version_(version) +{ + memcpy(&remoteAddr_, remoteAddr, remoteAddrLen); + memcpy(&localAddr_, localAddr, localAddrLen); + + // Create SSL object + ssl_ = SSL_new(sslCtx); + if (!ssl_) + { + LOG_ERROR << "SSL_new failed"; + return; + } + + SSL_set_accept_state(ssl_); + +#ifdef DROGON_NGTCP2_CRYPTO_QUICTLS + // Legacy quictls: setup ngtcp2_crypto_conn_ref so TLS callbacks + // can find our ngtcp2_conn. + connRef_.get_conn = [](ngtcp2_crypto_conn_ref *ref) -> ngtcp2_conn * { + auto *self = static_cast(ref->user_data); + return self->conn_; + }; + connRef_.user_data = this; + SSL_set_app_data(ssl_, &connRef_); +#elif defined(DROGON_NGTCP2_CRYPTO_OSSL) + // Modern ossl: configure the SSL session for QUIC server use + ngtcp2_crypto_ossl_configure_server_session(ssl_); +#endif +} + +QuicConnection::~QuicConnection() +{ + if (retransTimerId_ != trantor::InvalidTimerId) + { + loop_->invalidateTimer(retransTimerId_); + } + if (h3conn_) + { + nghttp3_conn_del(h3conn_); + } + if (conn_) + { + ngtcp2_conn_del(conn_); + } + if (ssl_) + { + SSL_free(ssl_); + } +} + +bool QuicConnection::init() +{ + // Setup ngtcp2 callbacks + ngtcp2_callbacks callbacks; + memset(&callbacks, 0, sizeof(callbacks)); + + // Note: ngtcp2_crypto_quictls_configure_server_context is called once + // in QuicServer::initTlsContext(), not per-connection. + + callbacks.recv_stream_data = onRecvStreamData; + callbacks.acked_stream_data_offset = onAckedStreamDataOffset; + callbacks.stream_open = onStreamOpen; + callbacks.stream_close = onStreamClose; + callbacks.extend_max_remote_streams_bidi = onExtendMaxStreamsBidi; + callbacks.extend_max_stream_data = onExtendMaxStreamData; + callbacks.rand = onRandCallback; + callbacks.get_new_connection_id = onGetNewConnectionId; + callbacks.handshake_completed = onHandshakeCompleted; + + // Use ngtcp2_crypto callbacks for TLS + callbacks.recv_crypto_data = ngtcp2_crypto_recv_crypto_data_cb; + callbacks.encrypt = ngtcp2_crypto_encrypt_cb; + callbacks.decrypt = ngtcp2_crypto_decrypt_cb; + callbacks.hp_mask = ngtcp2_crypto_hp_mask_cb; + callbacks.recv_retry = ngtcp2_crypto_recv_retry_cb; + callbacks.update_key = ngtcp2_crypto_update_key_cb; + callbacks.delete_crypto_aead_ctx = ngtcp2_crypto_delete_crypto_aead_ctx_cb; + callbacks.delete_crypto_cipher_ctx = + ngtcp2_crypto_delete_crypto_cipher_ctx_cb; + callbacks.get_path_challenge_data = + ngtcp2_crypto_get_path_challenge_data_cb; + callbacks.version_negotiation = ngtcp2_crypto_version_negotiation_cb; + + // Setup connection settings (separate from transport params in modern + // ngtcp2) + ngtcp2_settings settings; + ngtcp2_settings_default(&settings); + settings.initial_ts = quicTimestamp(); + settings.log_printf = nullptr; // Enable for debug: ngtcp2_log_printf + + // Transport parameters (separate struct in modern ngtcp2) + ngtcp2_transport_params params; + ngtcp2_transport_params_default(¶ms); + params.initial_max_data = 1024 * 1024; // 1MB + params.initial_max_stream_data_bidi_local = 256 * 1024; // 256KB + params.initial_max_stream_data_bidi_remote = 256 * 1024; + params.initial_max_stream_data_uni = 256 * 1024; + params.initial_max_streams_bidi = 100; + params.initial_max_streams_uni = 3; + params.max_idle_timeout = 30 * NGTCP2_SECONDS; + + // Generate stateless reset token + server_->generateStatelessResetToken( + params.stateless_reset_token, scid_); + params.stateless_reset_token_present = 1; + + // Path + ngtcp2_path path; + path.local.addr = reinterpret_cast(&localAddr_); + path.local.addrlen = localAddrLen_; + path.remote.addr = reinterpret_cast(&remoteAddr_); + path.remote.addrlen = remoteAddrLen_; + + // Create ngtcp2 server connection. + // Note: ngtcp2_conn_server_new is a macro that expands to + // ngtcp2_conn_server_new_versioned with version constants. + int rv = ngtcp2_conn_server_new( + &conn_, + &dcid_, + &scid_, + &path, + version_, + &callbacks, + &settings, + ¶ms, + nullptr, // mem allocator + this); // user_data + + if (rv != 0) + { + LOG_ERROR << "ngtcp2_conn_server_new failed: " + << ngtcp2_strerror(rv); + return false; + } + + // Set the TLS native handle so ngtcp2 can drive the TLS handshake + ngtcp2_conn_set_tls_native_handle(conn_, ssl_); + + LOG_TRACE << "QUIC connection initialized, version: 0x" + << std::hex << version_ << std::dec; + + return true; +} + +// ---- ngtcp2 Callbacks ---- + +int QuicConnection::onRecvStreamData(ngtcp2_conn *conn, + uint32_t flags, + int64_t stream_id, + uint64_t offset, + const uint8_t *data, + size_t datalen, + void *user_data, + void *stream_user_data) +{ + auto *self = static_cast(user_data); + (void)conn; + (void)offset; + (void)stream_user_data; + + if (self->h3conn_) + { + auto nconsumed = nghttp3_conn_read_stream( + self->h3conn_, + stream_id, + data, + datalen, + flags & NGTCP2_STREAM_DATA_FLAG_FIN); + + if (nconsumed < 0) + { + LOG_ERROR << "nghttp3_conn_read_stream failed: " + << nghttp3_strerror(static_cast(nconsumed)); + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + ngtcp2_conn_extend_max_stream_offset( + self->conn_, + stream_id, + static_cast(nconsumed)); + ngtcp2_conn_extend_max_offset( + self->conn_, + static_cast(nconsumed)); + } + + return 0; +} + +int QuicConnection::onAckedStreamDataOffset(ngtcp2_conn *conn, + int64_t stream_id, + uint64_t offset, + uint64_t datalen, + void *user_data, + void *stream_user_data) +{ + auto *self = static_cast(user_data); + (void)conn; + (void)offset; + (void)stream_user_data; + + if (self->h3conn_) + { + int rv = nghttp3_conn_add_ack_offset( + self->h3conn_, stream_id, datalen); + if (rv != 0) + { + LOG_ERROR << "nghttp3_conn_add_ack_offset failed: " + << nghttp3_strerror(rv); + return NGTCP2_ERR_CALLBACK_FAILURE; + } + } + + return 0; +} + +int QuicConnection::onStreamOpen(ngtcp2_conn *conn, + int64_t stream_id, + void *user_data) +{ + (void)conn; + (void)stream_id; + (void)user_data; + return 0; +} + +int QuicConnection::onStreamClose(ngtcp2_conn *conn, + uint32_t flags, + int64_t stream_id, + uint64_t app_error_code, + void *user_data, + void *stream_user_data) +{ + auto *self = static_cast(user_data); + (void)conn; + (void)flags; + (void)stream_user_data; + + if (self->h3conn_) + { + int rv = nghttp3_conn_close_stream( + self->h3conn_, stream_id, app_error_code); + if (rv != 0 && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) + { + LOG_ERROR << "nghttp3_conn_close_stream failed: " + << nghttp3_strerror(rv); + return NGTCP2_ERR_CALLBACK_FAILURE; + } + } + + // Clean up stream data + self->streams_.erase(stream_id); + self->pendingResponses_.erase(stream_id); + + return 0; +} + +int QuicConnection::onExtendMaxStreamsBidi(ngtcp2_conn *conn, + uint64_t max_streams, + void *user_data) +{ + (void)conn; + (void)max_streams; + (void)user_data; + return 0; +} + +int QuicConnection::onExtendMaxStreamData(ngtcp2_conn *conn, + int64_t stream_id, + uint64_t max_data, + void *user_data, + void *stream_user_data) +{ + auto *self = static_cast(user_data); + (void)conn; + (void)max_data; + (void)stream_user_data; + + if (self->h3conn_) + { + int rv = nghttp3_conn_unblock_stream(self->h3conn_, stream_id); + if (rv != 0 && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) + { + LOG_ERROR << "nghttp3_conn_unblock_stream failed"; + return NGTCP2_ERR_CALLBACK_FAILURE; + } + } + + return 0; +} + +void QuicConnection::onRandCallback(uint8_t *dest, + size_t destlen, + const ngtcp2_rand_ctx *rand_ctx) +{ + (void)rand_ctx; + RAND_bytes(dest, destlen); +} + +int QuicConnection::onGetNewConnectionId(ngtcp2_conn *conn, + ngtcp2_cid *cid, + uint8_t *token, + size_t cidlen, + void *user_data) +{ + auto *self = static_cast(user_data); + (void)conn; + + cid->datalen = cidlen; + RAND_bytes(cid->data, cidlen); + + // Generate stateless reset token for this CID + self->server_->generateStatelessResetToken(token, *cid); + + // Associate new CID with this connection in server + self->server_->connections_[*cid] = self->shared_from_this(); + + return 0; +} + +int QuicConnection::onHandshakeCompleted(ngtcp2_conn *conn, + void *user_data) +{ + auto *self = static_cast(user_data); + (void)conn; + + self->handshakeCompleted_ = true; + LOG_INFO << "QUIC handshake completed"; + + // Setup HTTP/3 now that the handshake is done + int rv = self->setupHttp3Connection(); + if (rv != 0) + { + LOG_ERROR << "Failed to setup HTTP/3 connection"; + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} + +// ---- nghttp3 Callbacks ---- + +int QuicConnection::onH3RecvHeader(nghttp3_conn *conn, + int64_t stream_id, + int32_t token, + nghttp3_rcbuf *name, + nghttp3_rcbuf *value, + uint8_t flags, + void *user_data, + void *stream_user_data) +{ + auto *self = static_cast(user_data); + (void)conn; + (void)token; + (void)flags; + (void)stream_user_data; + + auto nameVec = nghttp3_rcbuf_get_buf(name); + auto valueVec = nghttp3_rcbuf_get_buf(value); + + std::string headerName( + reinterpret_cast(nameVec.base), nameVec.len); + std::string headerValue( + reinterpret_cast(valueVec.base), valueVec.len); + + auto &stream = self->streams_[stream_id]; + + // Create request on first header + if (!stream.request) + { + stream.request = + std::make_shared(nullptr); + stream.request->setVersion(drogon::Version::kHttp3); + // HTTP/3 is always encrypted (QUIC uses TLS 1.3) + stream.request->setSecure(true); + // Set peer/local addresses for logging and IP-based middleware + if (self->remoteAddr_.sa.sa_family == AF_INET6) + { + stream.request->setPeerAddr( + trantor::InetAddress(self->remoteAddr_.in6)); + stream.request->setLocalAddr( + trantor::InetAddress(self->localAddr_.in6)); + } + else + { + stream.request->setPeerAddr( + trantor::InetAddress(self->remoteAddr_.in)); + stream.request->setLocalAddr( + trantor::InetAddress(self->localAddr_.in)); + } + } + + // Handle pseudo-headers + if (headerName == ":method") + { + stream.request->setMethod( + Http3Handler::methodFromString(headerValue)); + } + else if (headerName == ":path") + { + stream.request->setPath(headerValue); + } + else if (headerName == ":authority") + { + stream.request->addHeader("host", headerValue); + } + else if (headerName == ":scheme") + { + // Store scheme info if needed + } + else + { + // Regular header + stream.request->addHeader(headerName, headerValue); + } + + return 0; +} + +int QuicConnection::onH3EndHeaders(nghttp3_conn *conn, + int64_t stream_id, + int fin, + void *user_data, + void *stream_user_data) +{ + auto *self = static_cast(user_data); + (void)conn; + (void)fin; + (void)stream_user_data; + + auto it = self->streams_.find(stream_id); + if (it != self->streams_.end()) + { + it->second.headersComplete = true; + } + + return 0; +} + +int QuicConnection::onH3RecvData(nghttp3_conn *conn, + int64_t stream_id, + const uint8_t *data, + size_t datalen, + void *user_data, + void *stream_user_data) +{ + auto *self = static_cast(user_data); + (void)conn; + (void)stream_user_data; + + auto it = self->streams_.find(stream_id); + if (it != self->streams_.end()) + { + it->second.body.append( + reinterpret_cast(data), datalen); + } + + return 0; +} + +int QuicConnection::onH3EndStream(nghttp3_conn *conn, + int64_t stream_id, + void *user_data, + void *stream_user_data) +{ + auto *self = static_cast(user_data); + (void)conn; + (void)stream_user_data; + + auto it = self->streams_.find(stream_id); + if (it == self->streams_.end() || !it->second.request) + { + return 0; + } + + auto &stream = it->second; + + // Set the body on the request + if (!stream.body.empty()) + { + stream.request->setBody(stream.body); + } + + LOG_TRACE << "HTTP/3 request received: " + << stream.request->methodString() << " " + << stream.request->path(); + + // Dispatch through the same pipeline as HTTP/1.1 + if (self->requestCallback_) + { + auto weakSelf = self->weak_from_this(); + auto capturedStreamId = stream_id; + + self->requestCallback_( + stream.request, + [weakSelf, capturedStreamId](const HttpResponsePtr &resp) { + auto self2 = weakSelf.lock(); + if (self2) + { + self2->sendResponse(capturedStreamId, resp); + } + }); + } + + return 0; +} + +int QuicConnection::onH3StopSending(nghttp3_conn *conn, + int64_t stream_id, + uint64_t app_error_code, + void *user_data, + void *stream_user_data) +{ + auto *self = static_cast(user_data); + (void)conn; + (void)stream_user_data; + + ngtcp2_conn_shutdown_stream_read( + self->conn_, 0, stream_id, app_error_code); + return 0; +} + +int QuicConnection::onH3ResetStream(nghttp3_conn *conn, + int64_t stream_id, + uint64_t app_error_code, + void *user_data, + void *stream_user_data) +{ + auto *self = static_cast(user_data); + (void)conn; + (void)stream_user_data; + + ngtcp2_conn_shutdown_stream_write( + self->conn_, 0, stream_id, app_error_code); + return 0; +} + +int QuicConnection::onH3AckedStreamData(nghttp3_conn *conn, + int64_t stream_id, + uint64_t datalen, + void *user_data, + void *stream_user_data) +{ + auto *self = static_cast(user_data); + (void)conn; + (void)stream_user_data; + + auto it = self->pendingResponses_.find(stream_id); + if (it != self->pendingResponses_.end()) + { + // Track acknowledged data for potential cleanup + // For now, cleanup happens on stream close + } + + return 0; +} + +// ---- Internal Methods ---- + +int QuicConnection::setupHttp3Connection() +{ + if (h3conn_) + { + return 0; // Already setup + } + + nghttp3_callbacks h3Callbacks; + memset(&h3Callbacks, 0, sizeof(h3Callbacks)); + + h3Callbacks.recv_header = onH3RecvHeader; + h3Callbacks.end_headers = onH3EndHeaders; + h3Callbacks.recv_data = onH3RecvData; + h3Callbacks.end_stream = onH3EndStream; + h3Callbacks.stop_sending = onH3StopSending; + h3Callbacks.reset_stream = onH3ResetStream; + h3Callbacks.acked_stream_data = onH3AckedStreamData; + + nghttp3_settings h3Settings; + nghttp3_settings_default(&h3Settings); + h3Settings.qpack_max_dtable_capacity = 4096; + h3Settings.qpack_blocked_streams = 100; + + int rv = nghttp3_conn_server_new( + &h3conn_, + &h3Callbacks, + &h3Settings, + nullptr, // mem allocator + this); // user_data + + if (rv != 0) + { + LOG_ERROR << "nghttp3_conn_server_new failed: " + << nghttp3_strerror(rv); + return -1; + } + + // Bind control and QPACK streams + // These are unidirectional streams required by HTTP/3 + int64_t ctrlStreamId, qpackEncStreamId, qpackDecStreamId; + + rv = ngtcp2_conn_open_uni_stream(conn_, &ctrlStreamId, nullptr); + if (rv != 0) + { + LOG_ERROR << "Failed to open control stream"; + return -1; + } + + rv = ngtcp2_conn_open_uni_stream(conn_, &qpackEncStreamId, nullptr); + if (rv != 0) + { + LOG_ERROR << "Failed to open QPACK encoder stream"; + return -1; + } + + rv = ngtcp2_conn_open_uni_stream(conn_, &qpackDecStreamId, nullptr); + if (rv != 0) + { + LOG_ERROR << "Failed to open QPACK decoder stream"; + return -1; + } + + rv = nghttp3_conn_bind_control_stream(h3conn_, ctrlStreamId); + if (rv != 0) + { + LOG_ERROR << "nghttp3_conn_bind_control_stream failed: " + << nghttp3_strerror(rv); + return -1; + } + + rv = nghttp3_conn_bind_qpack_streams( + h3conn_, qpackEncStreamId, qpackDecStreamId); + if (rv != 0) + { + LOG_ERROR << "nghttp3_conn_bind_qpack_streams failed: " + << nghttp3_strerror(rv); + return -1; + } + + LOG_TRACE << "HTTP/3 session initialized (ctrl=" << ctrlStreamId + << ", qenc=" << qpackEncStreamId + << ", qdec=" << qpackDecStreamId << ")"; + + return 0; +} + +void QuicConnection::sendResponse(int64_t streamId, + const HttpResponsePtr &resp) +{ + if (!h3conn_ || !conn_) + { + return; + } + + // Serialize headers + auto serialized = Http3Handler::serializeResponseHeaders(resp); + + // Get body + auto [bodyData, bodyLen] = Http3Handler::getResponseBody(resp); + + // Store pending response data + auto &pending = pendingResponses_[streamId]; + if (bodyLen > 0) + { + pending.body.assign( + reinterpret_cast(bodyData), bodyLen); + } + + // Create nghttp3 data reader if there's a body + nghttp3_data_reader dataReader; + nghttp3_data_reader *dataReaderPtr = nullptr; + + if (bodyLen > 0) + { + dataReader.read_data = [](nghttp3_conn *conn, + int64_t stream_id, + nghttp3_vec *vec, + size_t veccnt, + uint32_t *pflags, + void *user_data, + void *stream_user_data) -> nghttp3_ssize { + auto *self = static_cast(user_data); + (void)conn; + (void)stream_user_data; + + auto it = self->pendingResponses_.find(stream_id); + if (it == self->pendingResponses_.end()) + { + *pflags = NGHTTP3_DATA_FLAG_EOF; + return 0; + } + + auto &resp = it->second; + if (resp.bodySent >= resp.body.size()) + { + *pflags = NGHTTP3_DATA_FLAG_EOF; + return 0; + } + + size_t remaining = resp.body.size() - resp.bodySent; + vec[0].base = reinterpret_cast( + const_cast(resp.body.data() + resp.bodySent)); + vec[0].len = remaining; + resp.bodySent += remaining; + + *pflags = NGHTTP3_DATA_FLAG_EOF; + return 1; + }; + dataReaderPtr = &dataReader; + } + + // Submit response headers + int rv = nghttp3_conn_submit_response( + h3conn_, + streamId, + serialized.nva.data(), + serialized.nva.size(), + dataReaderPtr); + + if (rv != 0) + { + LOG_ERROR << "nghttp3_conn_submit_response failed: " + << nghttp3_strerror(rv); + return; + } + + LOG_TRACE << "HTTP/3 response submitted for stream " << streamId + << " (status " << resp->statusCode() + << ", body " << bodyLen << " bytes)"; + + // Trigger write + onWrite(); +} + +int QuicConnection::onRead(const ngtcp2_pkt_info *pi, + const uint8_t *data, + size_t datalen, + const struct sockaddr *remoteAddr, + socklen_t remoteAddrLen) +{ + ngtcp2_path path; + path.local.addr = reinterpret_cast(&localAddr_); + path.local.addrlen = localAddrLen_; + + ngtcp2_sockaddr_union tmpRemote; + memcpy(&tmpRemote, remoteAddr, remoteAddrLen); + path.remote.addr = reinterpret_cast(&tmpRemote); + path.remote.addrlen = remoteAddrLen; + + int rv = ngtcp2_conn_read_pkt( + conn_, &path, pi, data, datalen, quicTimestamp()); + + if (rv != 0) + { + LOG_ERROR << "ngtcp2_conn_read_pkt failed: " + << ngtcp2_strerror(rv); + if (!ngtcp2_err_is_fatal(rv)) + { + return 0; + } + return -1; + } + + updateTimer(); + return 0; +} + +int QuicConnection::onWrite() +{ + return writePackets(); +} + +int QuicConnection::writePackets() +{ + if (draining_) + { + return 0; + } + + ngtcp2_path_storage ps; + ngtcp2_path_storage_zero(&ps); + + ngtcp2_pkt_info pi; + + for (;;) + { + int64_t streamId = -1; + nghttp3_vec vec[16]; + nghttp3_ssize sveccnt = 0; + int fin = 0; + + // Ask nghttp3 if it has data to send + if (h3conn_) + { + sveccnt = nghttp3_conn_writev_stream( + h3conn_, + &streamId, + &fin, + vec, + 16); + + if (sveccnt < 0) + { + LOG_ERROR << "nghttp3_conn_writev_stream failed: " + << nghttp3_strerror(static_cast(sveccnt)); + return -1; + } + } + + uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE; + if (fin) + { + flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + } + + ngtcp2_vec ntVec[16]; + for (nghttp3_ssize i = 0; i < sveccnt; ++i) + { + ntVec[i].base = vec[i].base; + ntVec[i].len = vec[i].len; + } + + ngtcp2_ssize ndatalen = 0; + auto nwrite = ngtcp2_conn_writev_stream( + conn_, + &ps.path, + &pi, + sendBuf_.data(), + sendBuf_.size(), + &ndatalen, + flags, + streamId, + reinterpret_cast(ntVec), + static_cast(sveccnt), + quicTimestamp()); + + if (nwrite < 0) + { + if (nwrite == NGTCP2_ERR_WRITE_MORE) + { + // ngtcp2 consumed ndatalen bytes of stream data but needs + // more before it can produce a packet. Tell nghttp3. + if (h3conn_ && streamId >= 0 && ndatalen >= 0) + { + nghttp3_conn_add_write_offset( + h3conn_, streamId, ndatalen); + } + continue; + } + LOG_ERROR << "ngtcp2_conn_writev_stream failed: " + << ngtcp2_strerror(static_cast(nwrite)); + return -1; + } + + if (nwrite == 0) + { + // No more data to send + break; + } + + // Send the packet + server_->sendPacket( + reinterpret_cast(&remoteAddr_), + remoteAddrLen_, + sendBuf_.data(), + static_cast(nwrite)); + + // Tell nghttp3 how much stream data was consumed by ngtcp2 + if (h3conn_ && streamId >= 0 && ndatalen >= 0) + { + nghttp3_conn_add_write_offset( + h3conn_, streamId, ndatalen); + } + } + + updateTimer(); + return 0; +} + +int QuicConnection::handleExpiry() +{ + int rv = ngtcp2_conn_handle_expiry(conn_, quicTimestamp()); + if (rv != 0) + { + LOG_ERROR << "ngtcp2_conn_handle_expiry failed: " + << ngtcp2_strerror(rv); + return -1; + } + return onWrite(); +} + +void QuicConnection::updateTimer() +{ + if (retransTimerId_ != trantor::InvalidTimerId) + { + loop_->invalidateTimer(retransTimerId_); + retransTimerId_ = trantor::InvalidTimerId; + } + + auto expiry = ngtcp2_conn_get_expiry(conn_); + auto now = quicTimestamp(); + + if (expiry <= now) + { + // Already expired, handle immediately + loop_->queueInLoop([weak = weak_from_this()] { + auto self = weak.lock(); + if (self) + { + if (self->handleExpiry() != 0) + { + self->server_->removeConnection(self->scid()); + } + } + }); + return; + } + + // Schedule timer + double delaySec = + static_cast(expiry - now) / NGTCP2_SECONDS; + if (delaySec < 0.001) + { + delaySec = 0.001; + } + + retransTimerId_ = loop_->runAfter( + delaySec, + [weak = weak_from_this()] { + auto self = weak.lock(); + if (self) + { + if (self->handleExpiry() != 0) + { + self->server_->removeConnection(self->scid()); + } + } + }); +} + +} // namespace drogon + +#endif // DROGON_HAS_HTTP3 diff --git a/lib/src/QuicConnection.h b/lib/src/QuicConnection.h new file mode 100644 index 0000000000..5c53ff8323 --- /dev/null +++ b/lib/src/QuicConnection.h @@ -0,0 +1,306 @@ +/** + * + * @file QuicConnection.h + * @author S Bala Vignesh + * + * Copyright 2026, S Bala Vignesh. All rights reserved. + * https://github.com/drogonframework/drogon + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Drogon + * + */ + +#pragma once + +#ifdef DROGON_HAS_HTTP3 + +#include "QuicServer.h" +#include "HttpRequestImpl.h" +#include "HttpResponseImpl.h" + +#include +#include +#include +#include +// ngtcp2_crypto backend header is included via QuicServer.h +#include +#include +#include +#include +#include +#include +#include +#include + +namespace drogon +{ + +/** + * @brief Represents a single QUIC connection with an HTTP/3 session. + * + * Each QuicConnection wraps: + * - ngtcp2_conn: QUIC protocol state machine + * - nghttp3_conn: HTTP/3 framing layer + * - SSL: TLS 1.3 session for QUIC encryption + * + * Incoming HTTP/3 requests are converted into HttpRequestImpl objects + * and dispatched through the same callback pipeline as HTTP/1.1. + */ +class QuicConnection : trantor::NonCopyable, + public std::enable_shared_from_this +{ + public: + QuicConnection(QuicServer *server, + trantor::EventLoop *loop, + const ngtcp2_cid &dcid, + const ngtcp2_cid &scid, + const struct sockaddr *remoteAddr, + socklen_t remoteAddrLen, + const struct sockaddr *localAddr, + socklen_t localAddrLen, + uint32_t version, + SSL_CTX *sslCtx); + + ~QuicConnection(); + + /** + * @brief Initialize the QUIC connection and TLS handshake. + * @return true on success + */ + bool init(); + + /** + * @brief Feed a received UDP packet into this connection. + * @param pi Packet info (ECN, etc.) + * @param data Packet data + * @param datalen Packet length + * @param remoteAddr Remote address + * @param remoteAddrLen Remote address length + * @return 0 on success, negative on error + */ + int onRead(const ngtcp2_pkt_info *pi, + const uint8_t *data, + size_t datalen, + const struct sockaddr *remoteAddr, + socklen_t remoteAddrLen); + + /** + * @brief Write pending QUIC packets to the UDP socket. + * @return 0 on success, negative on error + */ + int onWrite(); + + /** + * @brief Handle timer expiry for retransmission. + * @return 0 on success, negative on error + */ + int handleExpiry(); + + /** + * @brief Check if the connection is in draining state. + */ + bool isDraining() const + { + return draining_; + } + + /** + * @brief Get the source CID (used as key in server's map). + */ + const ngtcp2_cid &scid() const + { + return scid_; + } + + /** + * @brief Set the request callback for HTTP/3 requests. + */ + void setRequestCallback(QuicRequestCallback cb) + { + requestCallback_ = std::move(cb); + } + + private: + // ---- ngtcp2 callbacks (static, dispatched to instance) ---- + + static int onRecvStreamData(ngtcp2_conn *conn, + uint32_t flags, + int64_t stream_id, + uint64_t offset, + const uint8_t *data, + size_t datalen, + void *user_data, + void *stream_user_data); + + static int onAckedStreamDataOffset(ngtcp2_conn *conn, + int64_t stream_id, + uint64_t offset, + uint64_t datalen, + void *user_data, + void *stream_user_data); + + static int onStreamOpen(ngtcp2_conn *conn, + int64_t stream_id, + void *user_data); + + static int onStreamClose(ngtcp2_conn *conn, + uint32_t flags, + int64_t stream_id, + uint64_t app_error_code, + void *user_data, + void *stream_user_data); + + static int onExtendMaxStreamsBidi(ngtcp2_conn *conn, + uint64_t max_streams, + void *user_data); + + static int onExtendMaxStreamData(ngtcp2_conn *conn, + int64_t stream_id, + uint64_t max_data, + void *user_data, + void *stream_user_data); + + static void onRandCallback(uint8_t *dest, + size_t destlen, + const ngtcp2_rand_ctx *rand_ctx); + + static int onGetNewConnectionId(ngtcp2_conn *conn, + ngtcp2_cid *cid, + uint8_t *token, + size_t cidlen, + void *user_data); + + static int onHandshakeCompleted(ngtcp2_conn *conn, void *user_data); + + // ---- nghttp3 callbacks ---- + + static int onH3RecvHeader(nghttp3_conn *conn, + int64_t stream_id, + int32_t token, + nghttp3_rcbuf *name, + nghttp3_rcbuf *value, + uint8_t flags, + void *user_data, + void *stream_user_data); + + static int onH3EndHeaders(nghttp3_conn *conn, + int64_t stream_id, + int fin, + void *user_data, + void *stream_user_data); + + static int onH3RecvData(nghttp3_conn *conn, + int64_t stream_id, + const uint8_t *data, + size_t datalen, + void *user_data, + void *stream_user_data); + + static int onH3EndStream(nghttp3_conn *conn, + int64_t stream_id, + void *user_data, + void *stream_user_data); + + static int onH3StopSending(nghttp3_conn *conn, + int64_t stream_id, + uint64_t app_error_code, + void *user_data, + void *stream_user_data); + + static int onH3ResetStream(nghttp3_conn *conn, + int64_t stream_id, + uint64_t app_error_code, + void *user_data, + void *stream_user_data); + + static int onH3AckedStreamData(nghttp3_conn *conn, + int64_t stream_id, + uint64_t datalen, + void *user_data, + void *stream_user_data); + + // ---- Internal helpers ---- + + /** + * @brief Initialize the HTTP/3 session on this connection. + */ + int setupHttp3Connection(); + + /** + * @brief Send an HTTP/3 response for a given stream. + */ + void sendResponse(int64_t streamId, const HttpResponsePtr &resp); + + /** + * @brief Write outgoing QUIC packets to UDP. + */ + int writePackets(); + + /** + * @brief Schedule the retransmission timer. + */ + void updateTimer(); + + // Server back-pointer + QuicServer *server_; + trantor::EventLoop *loop_; + + // QUIC connection + ngtcp2_conn *conn_{nullptr}; + ngtcp2_cid dcid_; + ngtcp2_cid scid_; + + // TLS + SSL *ssl_{nullptr}; + ngtcp2_crypto_conn_ref connRef_; // Required by ngtcp2_crypto_quictls + + // HTTP/3 connection + nghttp3_conn *h3conn_{nullptr}; + + // Remote/local addresses + ngtcp2_sockaddr_union remoteAddr_; + socklen_t remoteAddrLen_; + ngtcp2_sockaddr_union localAddr_; + socklen_t localAddrLen_; + + // QUIC version + uint32_t version_; + + // Connection state + bool draining_{false}; + bool handshakeCompleted_{false}; + + // Timer for retransmissions + trantor::TimerId retransTimerId_{trantor::InvalidTimerId}; + + // HTTP/3 stream state + struct H3Stream + { + HttpRequestImplPtr request; + std::string body; + bool headersComplete{false}; + }; + std::unordered_map streams_; + + // Response data pending send + struct PendingResponse + { + std::string headers; + std::string body; + size_t headersSent{0}; + size_t bodySent{0}; + }; + std::unordered_map pendingResponses_; + + // Transmit buffer + std::array sendBuf_; + + // Request callback + QuicRequestCallback requestCallback_; +}; + +} // namespace drogon + +#endif // DROGON_HAS_HTTP3 diff --git a/lib/src/QuicServer.cc b/lib/src/QuicServer.cc new file mode 100644 index 0000000000..8af8884c09 --- /dev/null +++ b/lib/src/QuicServer.cc @@ -0,0 +1,568 @@ +/** + * + * @file QuicServer.cc + * @author S Bala Vignesh + * + * Copyright 2026, S Bala Vignesh. All rights reserved. + * https://github.com/drogonframework/drogon + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Drogon + * + */ + +#ifdef DROGON_HAS_HTTP3 + +#include "QuicServer.h" +#include "QuicConnection.h" +#include +#include +#include +#include + +#ifndef _WIN32 +#include +#include +#include +#include +#include +#else +#include +#include +#endif + +namespace drogon +{ + +QuicServer::QuicServer(trantor::EventLoop *loop, + const trantor::InetAddress &listenAddr, + const std::string &certPath, + const std::string &keyPath) + : loop_(loop), listenAddr_(listenAddr) +{ + // Generate random static secret for stateless reset tokens + RAND_bytes(staticSecret_.data(), staticSecret_.size()); + + if (!initTlsContext(certPath, keyPath)) + { + LOG_FATAL << "Failed to initialize TLS context for QUIC"; + abort(); + } + + LOG_INFO << "QuicServer created for " << listenAddr.toIpPort(); +} + +QuicServer::~QuicServer() +{ + stop(); + if (sslCtx_) + { + SSL_CTX_free(sslCtx_); + sslCtx_ = nullptr; + } +} + +bool QuicServer::initTlsContext(const std::string &certPath, + const std::string &keyPath) +{ + sslCtx_ = SSL_CTX_new(TLS_server_method()); + if (!sslCtx_) + { + LOG_ERROR << "SSL_CTX_new failed"; + return false; + } + + // Require TLS 1.3 for QUIC + SSL_CTX_set_min_proto_version(sslCtx_, TLS1_3_VERSION); + SSL_CTX_set_max_proto_version(sslCtx_, TLS1_3_VERSION); + + // Load certificate + if (SSL_CTX_use_certificate_chain_file(sslCtx_, certPath.c_str()) != 1) + { + LOG_ERROR << "Failed to load certificate: " << certPath; + return false; + } + + // Load private key + if (SSL_CTX_use_PrivateKey_file( + sslCtx_, keyPath.c_str(), SSL_FILETYPE_PEM) != 1) + { + LOG_ERROR << "Failed to load private key: " << keyPath; + return false; + } + + // Set ALPN for h3 (mandatory for QUIC — RFC 9001 Section 8.1) + static const uint8_t h3Alpn[] = {2, 'h', '3'}; + SSL_CTX_set_alpn_select_cb( + sslCtx_, + [](SSL *, + const unsigned char **out, + unsigned char *outlen, + const unsigned char *in, + unsigned int inlen, + void *) -> int { + if (SSL_select_next_proto( + const_cast(out), + outlen, + h3Alpn, + sizeof(h3Alpn), + in, + inlen) != OPENSSL_NPN_NEGOTIATED) + { + // QUIC mandates ALPN — must fail hard + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + return SSL_TLSEXT_ERR_OK; + }, + nullptr); + + // Enable early data (0-RTT) + SSL_CTX_set_max_early_data(sslCtx_, UINT32_MAX); + + // Configure TLS for QUIC — API differs between ngtcp2 backends +#ifdef DROGON_NGTCP2_CRYPTO_QUICTLS + // Legacy quictls backend: configure at SSL_CTX level + if (ngtcp2_crypto_quictls_configure_server_context(sslCtx_) != 0) + { + LOG_ERROR << "ngtcp2_crypto_quictls_configure_server_context failed"; + return false; + } +#elif defined(DROGON_NGTCP2_CRYPTO_OSSL) + // Modern ossl backend: initialize the library once. + // Per-session configuration is done in QuicConnection::init(). + if (ngtcp2_crypto_ossl_init() != 0) + { + LOG_ERROR << "ngtcp2_crypto_ossl_init failed"; + return false; + } +#endif + + LOG_INFO << "TLS context initialized for QUIC (TLS 1.3, h3 ALPN)"; + return true; +} + +void QuicServer::start() +{ + loop_->assertInLoopThread(); + + // Create UDP socket + int domain = listenAddr_.isIpV6() ? AF_INET6 : AF_INET; + udpFd_ = ::socket(domain, SOCK_DGRAM, 0); + if (udpFd_ < 0) + { + LOG_FATAL << "Failed to create UDP socket: " << strerror(errno); + abort(); + } + + // Set non-blocking +#ifndef _WIN32 + int flags = ::fcntl(udpFd_, F_GETFL, 0); + ::fcntl(udpFd_, F_SETFL, flags | O_NONBLOCK); +#else + u_long mode = 1; + ioctlsocket(udpFd_, FIONBIO, &mode); +#endif + + // Allow address reuse + int optval = 1; + ::setsockopt( + udpFd_, SOL_SOCKET, SO_REUSEADDR, (const char *)&optval, sizeof(optval)); + + // Enable ECN (Explicit Congestion Notification) if available +#ifdef IP_RECVTOS + ::setsockopt( + udpFd_, IPPROTO_IP, IP_RECVTOS, (const char *)&optval, sizeof(optval)); +#endif +#ifdef IPV6_RECVTCLASS + if (domain == AF_INET6) + { + ::setsockopt( + udpFd_, IPPROTO_IPV6, IPV6_RECVTCLASS, + (const char *)&optval, sizeof(optval)); + } +#endif + + // Enable pktinfo for local address detection +#ifdef IP_PKTINFO + ::setsockopt( + udpFd_, IPPROTO_IP, IP_PKTINFO, (const char *)&optval, sizeof(optval)); +#endif +#ifdef IPV6_RECVPKTINFO + if (domain == AF_INET6) + { + ::setsockopt( + udpFd_, IPPROTO_IPV6, IPV6_RECVPKTINFO, + (const char *)&optval, sizeof(optval)); + } +#endif + + // Bind + auto sa = listenAddr_.getSockAddr(); + socklen_t salen = + domain == AF_INET6 ? sizeof(struct sockaddr_in6) + : sizeof(struct sockaddr_in); + + if (::bind(udpFd_, reinterpret_cast(&sa), + salen) < 0) + { + LOG_FATAL << "Failed to bind UDP socket to " + << listenAddr_.toIpPort() << ": " << strerror(errno); +#ifndef _WIN32 + ::close(udpFd_); +#else + closesocket(udpFd_); +#endif + udpFd_ = -1; + abort(); + } + + // Register with event loop via Channel + udpChannel_ = std::make_unique(loop_, udpFd_); + udpChannel_->setReadCallback([this] { onRead(); }); + udpChannel_->enableReading(); + + LOG_INFO << "QuicServer listening on " << listenAddr_.toIpPort() + << " (HTTP/3, UDP)"; +} + +void QuicServer::stop() +{ + if (udpChannel_) + { + udpChannel_->disableAll(); + udpChannel_->remove(); + udpChannel_.reset(); + } + if (udpFd_ >= 0) + { +#ifndef _WIN32 + ::close(udpFd_); +#else + closesocket(udpFd_); +#endif + udpFd_ = -1; + } + connections_.clear(); +} + +void QuicServer::onRead() +{ + struct sockaddr_storage remoteAddrStorage; + socklen_t remoteAddrLen = sizeof(remoteAddrStorage); + + for (;;) + { + auto nread = ::recvfrom( + udpFd_, + reinterpret_cast(recvBuf_.data()), + recvBuf_.size(), + 0, + reinterpret_cast(&remoteAddrStorage), + &remoteAddrLen); + + if (nread < 0) + { +#ifndef _WIN32 + if (errno == EAGAIN || errno == EWOULDBLOCK) +#else + if (WSAGetLastError() == WSAEWOULDBLOCK) +#endif + { + break; + } + LOG_ERROR << "recvfrom error: " << strerror(errno); + break; + } + + if (nread == 0) + { + break; + } + + // Parse the packet header to get the DCID + ngtcp2_version_cid vc; + int rv = ngtcp2_pkt_decode_version_cid( + &vc, + recvBuf_.data(), + static_cast(nread), + NGTCP2_MAX_CIDLEN); + + if (rv < 0) + { + LOG_TRACE << "Failed to decode QUIC packet version/CID"; + continue; + } + + // Check if we support this version + if (vc.version != 0 && + !ngtcp2_is_supported_version(vc.version)) + { + // Send version negotiation + ngtcp2_pkt_hd hd; + hd.version = vc.version; + memcpy(hd.dcid.data, vc.dcid, vc.dcidlen); + hd.dcid.datalen = vc.dcidlen; + memcpy(hd.scid.data, vc.scid, vc.scidlen); + hd.scid.datalen = vc.scidlen; + + sendVersionNegotiation( + hd, + reinterpret_cast(&remoteAddrStorage), + remoteAddrLen); + continue; + } + + // Look up existing connection by DCID + ngtcp2_cid dcid; + ngtcp2_cid_init(&dcid, vc.dcid, vc.dcidlen); + + auto *conn = findConnection(dcid); + if (conn) + { + // Feed packet to existing connection + ngtcp2_pkt_info pi; + memset(&pi, 0, sizeof(pi)); + + if (conn->onRead( + &pi, + recvBuf_.data(), + static_cast(nread), + reinterpret_cast(&remoteAddrStorage), + remoteAddrLen) != 0) + { + removeConnection(conn->scid()); + } + else + { + conn->onWrite(); + } + } + else + { + // New connection - only accept Initial packets + ngtcp2_pkt_hd hd; + int rv2 = ngtcp2_accept(&hd, recvBuf_.data(), + static_cast(nread)); + if (rv2 < 0) + { + LOG_TRACE << "ngtcp2_accept failed, not an Initial packet"; + continue; + } + + // Get local address + struct sockaddr_storage localAddrStorage; + socklen_t localAddrLen = sizeof(localAddrStorage); + ::getsockname( + udpFd_, + reinterpret_cast(&localAddrStorage), + &localAddrLen); + + ngtcp2_cid scid; + ngtcp2_cid_init(&scid, hd.scid.data, hd.scid.datalen); + + auto *newConn = createConnection( + dcid, + scid, + reinterpret_cast(&remoteAddrStorage), + remoteAddrLen, + reinterpret_cast(&localAddrStorage), + localAddrLen, + hd.version); + + if (newConn) + { + ngtcp2_pkt_info pi; + memset(&pi, 0, sizeof(pi)); + + if (newConn->onRead( + &pi, + recvBuf_.data(), + static_cast(nread), + reinterpret_cast( + &remoteAddrStorage), + remoteAddrLen) != 0) + { + removeConnection(newConn->scid()); + } + else + { + newConn->onWrite(); + } + } + } + } +} + +QuicConnection *QuicServer::createConnection( + const ngtcp2_cid &dcid, + const ngtcp2_cid &scid, + const struct sockaddr *remoteAddr, + socklen_t remoteAddrLen, + const struct sockaddr *localAddr, + socklen_t localAddrLen, + uint32_t version) +{ + // Generate a new server CID + ngtcp2_cid serverCid; + serverCid.datalen = NGTCP2_MAX_CIDLEN; + RAND_bytes(serverCid.data, serverCid.datalen); + + auto conn = std::make_shared( + this, + loop_, + dcid, + serverCid, + remoteAddr, + remoteAddrLen, + localAddr, + localAddrLen, + version, + sslCtx_); + + conn->setRequestCallback(requestCallback_); + + if (!conn->init()) + { + LOG_ERROR << "Failed to initialize QUIC connection"; + return nullptr; + } + + auto *rawPtr = conn.get(); + connections_[serverCid] = std::move(conn); + + // Log the new connection (handle both IPv4 and IPv6) + if (remoteAddr->sa_family == AF_INET6) + { + LOG_INFO << "New QUIC connection from " + << trantor::InetAddress( + *reinterpret_cast( + remoteAddr)) + .toIpPort(); + } + else + { + LOG_INFO << "New QUIC connection from " + << trantor::InetAddress( + *reinterpret_cast( + remoteAddr)) + .toIpPort(); + } + + return rawPtr; +} + +QuicConnection *QuicServer::findConnection(const ngtcp2_cid &dcid) +{ + auto it = connections_.find(dcid); + if (it != connections_.end()) + { + return it->second.get(); + } + return nullptr; +} + +void QuicServer::removeConnection(const ngtcp2_cid &dcid) +{ + auto it = connections_.find(dcid); + if (it != connections_.end()) + { + LOG_INFO << "QUIC connection closed"; + connections_.erase(it); + } +} + +ssize_t QuicServer::sendPacket(const struct sockaddr *remoteAddr, + socklen_t remoteAddrLen, + const uint8_t *data, + size_t datalen) +{ + auto nwrite = ::sendto( + udpFd_, + reinterpret_cast(data), + datalen, + 0, + remoteAddr, + remoteAddrLen); + + if (nwrite < 0) + { + LOG_ERROR << "sendto error: " << strerror(errno); + } + + return nwrite; +} + +void QuicServer::sendVersionNegotiation(const ngtcp2_pkt_hd &hd, + const struct sockaddr *remoteAddr, + socklen_t remoteAddrLen) +{ + std::array buf; + + // Supported QUIC versions to offer in Version Negotiation + static const uint32_t supportedVersions[] = { + NGTCP2_PROTO_VER_V1, + NGTCP2_PROTO_VER_V2, + }; + + auto nwrite = ngtcp2_pkt_write_version_negotiation( + buf.data(), + buf.size(), + 0, // unused + hd.scid.data, + hd.scid.datalen, + hd.dcid.data, + hd.dcid.datalen, + supportedVersions, + sizeof(supportedVersions) / sizeof(supportedVersions[0])); + + if (nwrite > 0) + { + sendPacket(remoteAddr, remoteAddrLen, + buf.data(), static_cast(nwrite)); + } +} + +void QuicServer::sendRetry(const ngtcp2_pkt_hd &hd, + const struct sockaddr *remoteAddr, + socklen_t remoteAddrLen) +{ + ngtcp2_cid newScid; + newScid.datalen = NGTCP2_MAX_CIDLEN; + RAND_bytes(newScid.data, newScid.datalen); + + std::array token; + // In production: generate a proper retry token + RAND_bytes(token.data(), token.size()); + + std::array buf; + auto nwrite = ngtcp2_crypto_write_retry( + buf.data(), + buf.size(), + hd.version, + &hd.scid, + &newScid, + &hd.dcid, + token.data(), + token.size()); + + if (nwrite > 0) + { + sendPacket(remoteAddr, remoteAddrLen, + buf.data(), static_cast(nwrite)); + } +} + +bool QuicServer::generateStatelessResetToken(uint8_t *token, + const ngtcp2_cid &cid) +{ + return ngtcp2_crypto_generate_stateless_reset_token( + token, + staticSecret_.data(), + staticSecret_.size(), + &cid) == 0; +} + +} // namespace drogon + +#endif // DROGON_HAS_HTTP3 diff --git a/lib/src/QuicServer.h b/lib/src/QuicServer.h new file mode 100644 index 0000000000..31d88135a3 --- /dev/null +++ b/lib/src/QuicServer.h @@ -0,0 +1,247 @@ +/** + * + * @file QuicServer.h + * @author S Bala Vignesh + * + * Copyright 2026, S Bala Vignesh. All rights reserved. + * https://github.com/drogonframework/drogon + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Drogon + * + */ + +#pragma once + +#ifdef DROGON_HAS_HTTP3 + +#include +#include +#include +#include +#include +#include +#include +// ngtcp2 v1.x renamed crypto_quictls to crypto_ossl +#if __has_include() +#include +#define DROGON_NGTCP2_CRYPTO_QUICTLS 1 +#elif __has_include() +#include +#define DROGON_NGTCP2_CRYPTO_OSSL 1 +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace drogon +{ +class QuicConnection; +class HttpRequestImpl; +class HttpResponse; + +using HttpRequestImplPtr = std::shared_ptr; +using HttpResponsePtr = std::shared_ptr; + +/** + * @brief Callback for processing HTTP requests received over QUIC. + * The same signature as the existing HTTP/1.1 path so we can reuse + * the routing pipeline. + */ +using QuicRequestCallback = + std::function &&)>; + +/** + * @brief QuicServer manages a UDP socket and dispatches incoming QUIC + * packets to QuicConnection instances. It integrates with Trantor's + * event loop via Channel for non-blocking I/O. + * + * Architecture: + * UDP Socket (Channel) -> QuicServer -> QuicConnection (per client) + * -> ngtcp2 (QUIC) + * -> nghttp3 (HTTP/3) + * -> HttpRequest pipeline + */ +class QuicServer : trantor::NonCopyable +{ + public: + /** + * @brief Construct a QuicServer. + * @param loop The event loop to run in. + * @param listenAddr The address to bind the UDP socket to. + * @param certPath Path to the TLS certificate file. + * @param keyPath Path to the TLS private key file. + */ + QuicServer(trantor::EventLoop *loop, + const trantor::InetAddress &listenAddr, + const std::string &certPath, + const std::string &keyPath); + + ~QuicServer(); + + /** + * @brief Start listening for QUIC connections. + */ + void start(); + + /** + * @brief Stop and close the server. + */ + void stop(); + + /** + * @brief Set the callback for processing HTTP/3 requests. + * This should be set to HttpServer::onHttpRequest to reuse + * the existing routing pipeline. + */ + void setRequestCallback(QuicRequestCallback cb) + { + requestCallback_ = std::move(cb); + } + + /** + * @brief Set the IO event loops for distributing connections. + */ + void setIoLoops(const std::vector &ioLoops) + { + ioLoops_ = ioLoops; + } + + /** + * @brief Get the listen address. + */ + const trantor::InetAddress &address() const + { + return listenAddr_; + } + + private: + /** + * @brief Called when the UDP socket is readable. + * Reads packets and dispatches them to connections. + */ + void onRead(); + + /** + * @brief Create a new QUIC connection for an incoming initial packet. + */ + QuicConnection *createConnection(const ngtcp2_cid &dcid, + const ngtcp2_cid &scid, + const struct sockaddr *remoteAddr, + socklen_t remoteAddrLen, + const struct sockaddr *localAddr, + socklen_t localAddrLen, + uint32_t version); + + /** + * @brief Look up an existing connection by destination CID. + */ + QuicConnection *findConnection(const ngtcp2_cid &dcid); + + /** + * @brief Remove a connection (called when it closes). + */ + void removeConnection(const ngtcp2_cid &dcid); + + /** + * @brief Send a UDP packet. + */ + ssize_t sendPacket(const struct sockaddr *remoteAddr, + socklen_t remoteAddrLen, + const uint8_t *data, + size_t datalen); + + /** + * @brief Send a version negotiation packet. + */ + void sendVersionNegotiation(const ngtcp2_pkt_hd &hd, + const struct sockaddr *remoteAddr, + socklen_t remoteAddrLen); + + /** + * @brief Send a stateless retry packet. + */ + void sendRetry(const ngtcp2_pkt_hd &hd, + const struct sockaddr *remoteAddr, + socklen_t remoteAddrLen); + + /** + * @brief Initialize the SSL context for QUIC/TLS 1.3. + */ + bool initTlsContext(const std::string &certPath, + const std::string &keyPath); + + /** + * @brief Generate a stateless reset token for a CID. + */ + bool generateStatelessResetToken(uint8_t *token, + const ngtcp2_cid &cid); + + // Event loop + trantor::EventLoop *loop_; + trantor::InetAddress listenAddr_; + + // UDP socket and Channel + int udpFd_{-1}; + std::unique_ptr udpChannel_; + + // TLS context + SSL_CTX *sslCtx_{nullptr}; + + // Connection map: DCID -> QuicConnection + struct CidHash + { + size_t operator()(const ngtcp2_cid &cid) const + { + // FNV-1a hash over the CID bytes + size_t hash = 14695981039346656037ULL; + for (size_t i = 0; i < cid.datalen; ++i) + { + hash ^= static_cast(cid.data[i]); + hash *= 1099511628211ULL; + } + return hash; + } + }; + + struct CidEqual + { + bool operator()(const ngtcp2_cid &a, const ngtcp2_cid &b) const + { + return ngtcp2_cid_eq(&a, &b); + } + }; + + std::unordered_map, + CidHash, + CidEqual> + connections_; + + // Static secret for stateless reset tokens + std::array staticSecret_; + + // Request callback (shared with HttpServer) + QuicRequestCallback requestCallback_; + + // IO loops for distributing work + std::vector ioLoops_; + size_t nextLoopIdx_{0}; + + // Receive buffer + std::array recvBuf_; + + friend class QuicConnection; +}; + +} // namespace drogon + +#endif // DROGON_HAS_HTTP3