summaryrefslogtreecommitdiffstats
path: root/dnsdist-nghttp2-in.cc
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 21:14:51 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 21:14:51 +0000
commitbc282425088455198a7a99511c75914477d4ed32 (patch)
tree1b1fb887a634136a093deea7e4dd95d054201e7a /dnsdist-nghttp2-in.cc
parentReleasing progress-linux version 1.8.3-3~progress7.99u1. (diff)
downloaddnsdist-bc282425088455198a7a99511c75914477d4ed32.tar.xz
dnsdist-bc282425088455198a7a99511c75914477d4ed32.zip
Merging upstream version 1.9.3.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dnsdist-nghttp2-in.cc')
-rw-r--r--dnsdist-nghttp2-in.cc1187
1 files changed, 1187 insertions, 0 deletions
diff --git a/dnsdist-nghttp2-in.cc b/dnsdist-nghttp2-in.cc
new file mode 100644
index 0000000..32dc254
--- /dev/null
+++ b/dnsdist-nghttp2-in.cc
@@ -0,0 +1,1187 @@
+/*
+ * This file is part of PowerDNS or dnsdist.
+ * Copyright -- PowerDNS.COM B.V. and its contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * In addition, for the avoidance of any doubt, permission is granted to
+ * link this program with OpenSSL and to (re)distribute the binaries
+ * produced as the result of such linking.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "dnsdist-dnsparser.hh"
+#include "dnsdist-doh-common.hh"
+#include "dnsdist-nghttp2-in.hh"
+#include "dnsdist-proxy-protocol.hh"
+#include "dnsparser.hh"
+
+#if defined(HAVE_DNS_OVER_HTTPS) && defined(HAVE_NGHTTP2)
+
+#if 0
+class IncomingDoHCrossProtocolContext : public CrossProtocolContext
+{
+public:
+ IncomingDoHCrossProtocolContext(IncomingHTTP2Connection::PendingQuery&& query, std::shared_ptr<IncomingHTTP2Connection> connection, IncomingHTTP2Connection::StreamID streamID): CrossProtocolContext(std::move(query.d_buffer)), d_connection(connection), d_query(std::move(query))
+ {
+ }
+
+ std::optional<std::string> getHTTPPath() const override
+ {
+ return d_query.d_path;
+ }
+
+ std::optional<std::string> getHTTPScheme() const override
+ {
+ return d_query.d_scheme;
+ }
+
+ std::optional<std::string> getHTTPHost() const override
+ {
+ return d_query.d_host;
+ }
+
+ std::optional<std::string> getHTTPQueryString() const override
+ {
+ return d_query.d_queryString;
+ }
+
+ std::optional<HeadersMap> getHTTPHeaders() const override
+ {
+ if (!d_query.d_headers) {
+ return std::nullopt;
+ }
+ return *d_query.d_headers;
+ }
+
+ void handleResponse(PacketBuffer&& response, InternalQueryState&& state) override
+ {
+ auto conn = d_connection.lock();
+ if (!conn) {
+ /* the connection has been closed in the meantime */
+ return;
+ }
+ }
+
+ void handleTimeout() override
+ {
+ auto conn = d_connection.lock();
+ if (!conn) {
+ /* the connection has been closed in the meantime */
+ return;
+ }
+ }
+
+ ~IncomingDoHCrossProtocolContext() override
+ {
+ }
+
+private:
+ std::weak_ptr<IncomingHTTP2Connection> d_connection;
+ IncomingHTTP2Connection::PendingQuery d_query;
+ IncomingHTTP2Connection::StreamID d_streamID{-1};
+};
+#endif
+
+class IncomingDoHCrossProtocolContext : public DOHUnitInterface
+{
+public:
+ IncomingDoHCrossProtocolContext(IncomingHTTP2Connection::PendingQuery&& query, const std::shared_ptr<IncomingHTTP2Connection>& connection, IncomingHTTP2Connection::StreamID streamID) :
+ d_connection(connection), d_query(std::move(query)), d_streamID(streamID)
+ {
+ }
+ IncomingDoHCrossProtocolContext(const IncomingDoHCrossProtocolContext&) = delete;
+ IncomingDoHCrossProtocolContext(IncomingDoHCrossProtocolContext&&) = delete;
+ IncomingDoHCrossProtocolContext& operator=(const IncomingDoHCrossProtocolContext&) = delete;
+ IncomingDoHCrossProtocolContext& operator=(IncomingDoHCrossProtocolContext&&) = delete;
+
+ ~IncomingDoHCrossProtocolContext() override = default;
+
+ [[nodiscard]] std::string getHTTPPath() const override
+ {
+ return d_query.d_path;
+ }
+
+ [[nodiscard]] const std::string& getHTTPScheme() const override
+ {
+ return d_query.d_scheme;
+ }
+
+ [[nodiscard]] const std::string& getHTTPHost() const override
+ {
+ return d_query.d_host;
+ }
+
+ [[nodiscard]] std::string getHTTPQueryString() const override
+ {
+ return d_query.d_queryString;
+ }
+
+ [[nodiscard]] const HeadersMap& getHTTPHeaders() const override
+ {
+ if (!d_query.d_headers) {
+ static const HeadersMap empty{};
+ return empty;
+ }
+ return *d_query.d_headers;
+ }
+
+ void setHTTPResponse(uint16_t statusCode, PacketBuffer&& body, const std::string& contentType = "") override
+ {
+ d_query.d_statusCode = statusCode;
+ d_query.d_response = std::move(body);
+ d_query.d_contentTypeOut = contentType;
+ }
+
+ void handleUDPResponse(PacketBuffer&& response, InternalQueryState&& state, const std::shared_ptr<DownstreamState>& downstream_) override
+ {
+ std::unique_ptr<DOHUnitInterface> unit(this);
+ auto conn = d_connection.lock();
+ if (!conn) {
+ /* the connection has been closed in the meantime */
+ return;
+ }
+
+ state.du = std::move(unit);
+ TCPResponse resp(std::move(response), std::move(state), nullptr, nullptr);
+ resp.d_ds = downstream_;
+ struct timeval now
+ {
+ };
+ gettimeofday(&now, nullptr);
+ conn->handleResponse(now, std::move(resp));
+ }
+
+ void handleTimeout() override
+ {
+ std::unique_ptr<DOHUnitInterface> unit(this);
+ auto conn = d_connection.lock();
+ if (!conn) {
+ /* the connection has been closed in the meantime */
+ return;
+ }
+ struct timeval now
+ {
+ };
+ gettimeofday(&now, nullptr);
+ TCPResponse resp;
+ resp.d_idstate.d_streamID = d_streamID;
+ conn->notifyIOError(now, std::move(resp));
+ }
+
+ std::weak_ptr<IncomingHTTP2Connection> d_connection;
+ IncomingHTTP2Connection::PendingQuery d_query;
+ IncomingHTTP2Connection::StreamID d_streamID{-1};
+};
+
+void IncomingHTTP2Connection::handleResponse(const struct timeval& now, TCPResponse&& response)
+{
+ if (std::this_thread::get_id() != d_creatorThreadID) {
+ handleCrossProtocolResponse(now, std::move(response));
+ return;
+ }
+
+ auto& state = response.d_idstate;
+ if (state.forwardedOverUDP) {
+ dnsheader_aligned responseDH(response.d_buffer.data());
+
+ if (responseDH.get()->tc && state.d_packet && state.d_packet->size() > state.d_proxyProtocolPayloadSize && state.d_packet->size() - state.d_proxyProtocolPayloadSize > sizeof(dnsheader)) {
+ vinfolog("Response received from backend %s via UDP, for query %d received from %s via DoH, is truncated, retrying over TCP", response.d_ds->getNameWithAddr(), state.d_streamID, state.origRemote.toStringWithPort());
+ auto& query = *state.d_packet;
+ dnsdist::PacketMangling::editDNSHeaderFromRawPacket(&query.at(state.d_proxyProtocolPayloadSize), [origID = state.origID](dnsheader& header) {
+ /* restoring the original ID */
+ header.id = origID;
+ return true;
+ });
+
+ state.forwardedOverUDP = false;
+ bool proxyProtocolPayloadAdded = state.d_proxyProtocolPayloadSize > 0;
+ auto cpq = getCrossProtocolQuery(std::move(query), std::move(state), response.d_ds);
+ cpq->query.d_proxyProtocolPayloadAdded = proxyProtocolPayloadAdded;
+ if (g_tcpclientthreads && g_tcpclientthreads->passCrossProtocolQueryToThread(std::move(cpq))) {
+ return;
+ }
+ vinfolog("Unable to pass DoH query to a TCP worker thread after getting a TC response over UDP");
+ notifyIOError(now, std::move(response));
+ return;
+ }
+ }
+
+ IncomingTCPConnectionState::handleResponse(now, std::move(response));
+}
+
+std::unique_ptr<DOHUnitInterface> IncomingHTTP2Connection::getDOHUnit(uint32_t streamID)
+{
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay): clang-tidy is getting confused by assert()
+ assert(streamID <= std::numeric_limits<IncomingHTTP2Connection::StreamID>::max());
+ // NOLINTNEXTLINE(*-narrowing-conversions): generic interface between DNS and DoH with different types
+ auto query = std::move(d_currentStreams.at(static_cast<IncomingHTTP2Connection::StreamID>(streamID)));
+ return std::make_unique<IncomingDoHCrossProtocolContext>(std::move(query), std::dynamic_pointer_cast<IncomingHTTP2Connection>(shared_from_this()), streamID);
+}
+
+void IncomingHTTP2Connection::restoreDOHUnit(std::unique_ptr<DOHUnitInterface>&& unit)
+{
+ auto context = std::unique_ptr<IncomingDoHCrossProtocolContext>(dynamic_cast<IncomingDoHCrossProtocolContext*>(unit.release()));
+ if (context) {
+ d_currentStreams.at(context->d_streamID) = std::move(context->d_query);
+ }
+}
+
+IncomingHTTP2Connection::IncomingHTTP2Connection(ConnectionInfo&& connectionInfo, TCPClientThreadData& threadData, const struct timeval& now) :
+ IncomingTCPConnectionState(std::move(connectionInfo), threadData, now)
+{
+ nghttp2_session_callbacks* cbs = nullptr;
+ if (nghttp2_session_callbacks_new(&cbs) != 0) {
+ throw std::runtime_error("Unable to create a callback object for a new incoming HTTP/2 session");
+ }
+ std::unique_ptr<nghttp2_session_callbacks, void (*)(nghttp2_session_callbacks*)> callbacks(cbs, nghttp2_session_callbacks_del);
+ cbs = nullptr;
+
+ nghttp2_session_callbacks_set_send_callback(callbacks.get(), send_callback);
+ nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks.get(), on_frame_recv_callback);
+ nghttp2_session_callbacks_set_on_stream_close_callback(callbacks.get(), on_stream_close_callback);
+ nghttp2_session_callbacks_set_on_begin_headers_callback(callbacks.get(), on_begin_headers_callback);
+ nghttp2_session_callbacks_set_on_header_callback(callbacks.get(), on_header_callback);
+ nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks.get(), on_data_chunk_recv_callback);
+ nghttp2_session_callbacks_set_error_callback2(callbacks.get(), on_error_callback);
+
+ nghttp2_session* sess = nullptr;
+ if (nghttp2_session_server_new(&sess, callbacks.get(), this) != 0) {
+ throw std::runtime_error("Coult not allocate a new incoming HTTP/2 session");
+ }
+
+ d_session = std::unique_ptr<nghttp2_session, decltype(&nghttp2_session_del)>(sess, nghttp2_session_del);
+ sess = nullptr;
+}
+
+bool IncomingHTTP2Connection::checkALPN()
+{
+ constexpr std::array<uint8_t, 2> h2ALPN{'h', '2'};
+ const auto protocols = d_handler.getNextProtocol();
+ if (protocols.size() == h2ALPN.size() && memcmp(protocols.data(), h2ALPN.data(), h2ALPN.size()) == 0) {
+ return true;
+ }
+
+ constexpr std::array<uint8_t, 8> http11ALPN{'h', 't', 't', 'p', '/', '1', '.', '1'};
+ if (protocols.size() == http11ALPN.size() && memcmp(protocols.data(), http11ALPN.data(), http11ALPN.size()) == 0) {
+ ++d_ci.cs->dohFrontend->d_http1Stats.d_nbQueries;
+ }
+
+ const std::string data("HTTP/1.1 400 Bad Request\r\nConnection: Close\r\n\r\n<html><body>This server implements RFC 8484 - DNS Queries over HTTP, and requires HTTP/2 in accordance with section 5.2 of the RFC.</body></html>\r\n");
+ d_out.insert(d_out.end(), data.begin(), data.end());
+ writeToSocket(false);
+
+ vinfolog("DoH connection from %s expected ALPN value 'h2', got '%s'", d_ci.remote.toStringWithPort(), std::string(protocols.begin(), protocols.end()));
+ return false;
+}
+
+void IncomingHTTP2Connection::handleConnectionReady()
+{
+ constexpr std::array<nghttp2_settings_entry, 1> settings{{{NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100U}}};
+ auto ret = nghttp2_submit_settings(d_session.get(), NGHTTP2_FLAG_NONE, settings.data(), settings.size());
+ if (ret != 0) {
+ throw std::runtime_error("Fatal error: " + std::string(nghttp2_strerror(ret)));
+ }
+ d_needFlush = true;
+ ret = nghttp2_session_send(d_session.get());
+ if (ret != 0) {
+ throw std::runtime_error("Fatal error: " + std::string(nghttp2_strerror(ret)));
+ }
+}
+
+bool IncomingHTTP2Connection::hasPendingWrite() const
+{
+ return d_pendingWrite;
+}
+
+IOState IncomingHTTP2Connection::handleHandshake(const struct timeval& now)
+{
+ auto iostate = d_handler.tryHandshake();
+ if (iostate == IOState::Done) {
+ handleHandshakeDone(now);
+ if (d_handler.isTLS()) {
+ if (!checkALPN()) {
+ d_connectionDied = true;
+ stopIO();
+ return iostate;
+ }
+ }
+
+ if (d_ci.cs != nullptr && d_ci.cs->d_enableProxyProtocol && !isProxyPayloadOutsideTLS() && expectProxyProtocolFrom(d_ci.remote)) {
+ d_state = State::readingProxyProtocolHeader;
+ d_buffer.resize(s_proxyProtocolMinimumHeaderSize);
+ d_proxyProtocolNeed = s_proxyProtocolMinimumHeaderSize;
+ }
+ else {
+ d_state = State::waitingForQuery;
+ handleConnectionReady();
+ }
+ }
+ return iostate;
+}
+
+void IncomingHTTP2Connection::handleIO()
+{
+ IOState iostate = IOState::Done;
+ struct timeval now
+ {
+ };
+ gettimeofday(&now, nullptr);
+
+ try {
+ if (maxConnectionDurationReached(g_maxTCPConnectionDuration, now)) {
+ vinfolog("Terminating DoH connection from %s because it reached the maximum TCP connection duration", d_ci.remote.toStringWithPort());
+ stopIO();
+ d_connectionClosing = true;
+ return;
+ }
+
+ if (d_state == State::starting) {
+ if (d_ci.cs != nullptr && d_ci.cs->dohFrontend != nullptr) {
+ ++d_ci.cs->dohFrontend->d_httpconnects;
+ }
+ if (d_ci.cs != nullptr && d_ci.cs->d_enableProxyProtocol && isProxyPayloadOutsideTLS() && expectProxyProtocolFrom(d_ci.remote)) {
+ d_state = State::readingProxyProtocolHeader;
+ d_buffer.resize(s_proxyProtocolMinimumHeaderSize);
+ d_proxyProtocolNeed = s_proxyProtocolMinimumHeaderSize;
+ }
+ else {
+ d_state = State::doingHandshake;
+ }
+ }
+
+ if (d_state == State::doingHandshake) {
+ iostate = handleHandshake(now);
+ if (d_connectionDied) {
+ return;
+ }
+ }
+
+ if (d_state == State::readingProxyProtocolHeader) {
+ auto status = handleProxyProtocolPayload();
+ if (status == ProxyProtocolResult::Done) {
+ if (isProxyPayloadOutsideTLS()) {
+ d_state = State::doingHandshake;
+ iostate = handleHandshake(now);
+ if (d_connectionDied) {
+ return;
+ }
+ }
+ else {
+ d_currentPos = 0;
+ d_proxyProtocolNeed = 0;
+ d_buffer.clear();
+ d_state = State::waitingForQuery;
+ handleConnectionReady();
+ }
+ }
+ else if (status == ProxyProtocolResult::Error) {
+ d_connectionDied = true;
+ stopIO();
+ return;
+ }
+ }
+
+ if (active() && !d_connectionClosing && (d_state == State::waitingForQuery || d_state == State::idle)) {
+ do {
+ iostate = readHTTPData();
+ } while (active() && !d_connectionClosing && iostate == IOState::Done);
+ }
+
+ if (!active()) {
+ stopIO();
+ return;
+ }
+ /*
+ So:
+ - if we have a pending write, we need to wait until the socket becomes writable
+ and then call handleWritableCallback
+ - if we have NeedWrite but no pending write, we need to wait until the socket
+ becomes writable but for handleReadableIOCallback
+ - if we have NeedRead, or nghttp2_session_want_read, wait until the socket
+ becomes readable and call handleReadableIOCallback
+ */
+ if (hasPendingWrite()) {
+ updateIO(IOState::NeedWrite, handleWritableIOCallback);
+ }
+ else if (iostate == IOState::NeedWrite) {
+ updateIO(IOState::NeedWrite, handleReadableIOCallback);
+ }
+ else if (!d_connectionClosing) {
+ if (nghttp2_session_want_read(d_session.get()) != 0) {
+ updateIO(IOState::NeedRead, handleReadableIOCallback);
+ }
+ }
+ }
+ catch (const std::exception& e) {
+ vinfolog("Exception when processing IO for incoming DoH connection from %s: %s", d_ci.remote.toStringWithPort(), e.what());
+ d_connectionDied = true;
+ stopIO();
+ }
+}
+
+void IncomingHTTP2Connection::writeToSocket(bool socketReady)
+{
+ try {
+ d_needFlush = false;
+ IOState newState = d_handler.tryWrite(d_out, d_outPos, d_out.size());
+
+ if (newState == IOState::Done) {
+ d_pendingWrite = false;
+ d_out.clear();
+ d_outPos = 0;
+ if (active() && !d_connectionClosing) {
+ updateIO(IOState::NeedRead, handleReadableIOCallback);
+ }
+ else {
+ stopIO();
+ }
+ }
+ else {
+ updateIO(newState, handleWritableIOCallback);
+ d_pendingWrite = true;
+ }
+ }
+ catch (const std::exception& e) {
+ vinfolog("Exception while trying to write (%s) to HTTP client connection to %s: %s", (socketReady ? "ready" : "send"), d_ci.remote.toStringWithPort(), e.what());
+ handleIOError();
+ }
+}
+
+ssize_t IncomingHTTP2Connection::send_callback(nghttp2_session* session, const uint8_t* data, size_t length, int flags, void* user_data)
+{
+ auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
+ if (conn->d_connectionDied) {
+ return static_cast<ssize_t>(length);
+ }
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic): nghttp2 API
+ conn->d_out.insert(conn->d_out.end(), data, data + length);
+
+ if (conn->d_connectionClosing || conn->d_needFlush) {
+ conn->writeToSocket(false);
+ }
+
+ return static_cast<ssize_t>(length);
+}
+
+static const std::array<const std::string, static_cast<size_t>(NGHTTP2Headers::HeaderConstantIndexes::COUNT)> s_headerConstants{
+ "200",
+ ":method",
+ "POST",
+ ":scheme",
+ "https",
+ ":authority",
+ "x-forwarded-for",
+ ":path",
+ "content-length",
+ ":status",
+ "location",
+ "accept",
+ "application/dns-message",
+ "cache-control",
+ "content-type",
+ "application/dns-message",
+ "user-agent",
+ "nghttp2-" NGHTTP2_VERSION "/dnsdist",
+ "x-forwarded-port",
+ "x-forwarded-proto",
+ "dns-over-udp",
+ "dns-over-tcp",
+ "dns-over-tls",
+ "dns-over-http",
+ "dns-over-https"};
+
+static const std::string s_authorityHeaderName(":authority");
+static const std::string s_pathHeaderName(":path");
+static const std::string s_methodHeaderName(":method");
+static const std::string s_schemeHeaderName(":scheme");
+static const std::string s_xForwardedForHeaderName("x-forwarded-for");
+
+void NGHTTP2Headers::addStaticHeader(std::vector<nghttp2_nv>& headers, NGHTTP2Headers::HeaderConstantIndexes nameKey, NGHTTP2Headers::HeaderConstantIndexes valueKey)
+{
+ const auto& name = s_headerConstants.at(static_cast<size_t>(nameKey));
+ const auto& value = s_headerConstants.at(static_cast<size_t>(valueKey));
+
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast,cppcoreguidelines-pro-type-reinterpret-cast): nghttp2 API
+ headers.push_back({const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(name.c_str())), const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(value.c_str())), name.size(), value.size(), NGHTTP2_NV_FLAG_NO_COPY_NAME | NGHTTP2_NV_FLAG_NO_COPY_VALUE});
+}
+
+void NGHTTP2Headers::addCustomDynamicHeader(std::vector<nghttp2_nv>& headers, const std::string& name, const std::string_view& value)
+{
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast,cppcoreguidelines-pro-type-reinterpret-cast): nghttp2 API
+ headers.push_back({const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(name.data())), const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(value.data())), name.size(), value.size(), NGHTTP2_NV_FLAG_NO_COPY_NAME | NGHTTP2_NV_FLAG_NO_COPY_VALUE});
+}
+
+void NGHTTP2Headers::addDynamicHeader(std::vector<nghttp2_nv>& headers, NGHTTP2Headers::HeaderConstantIndexes nameKey, const std::string_view& value)
+{
+ const auto& name = s_headerConstants.at(static_cast<size_t>(nameKey));
+ NGHTTP2Headers::addCustomDynamicHeader(headers, name, value);
+}
+
+IOState IncomingHTTP2Connection::sendResponse(const struct timeval& now, TCPResponse&& response)
+{
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay): clang-tidy is getting confused by assert()
+ assert(response.d_idstate.d_streamID != -1);
+ auto& context = d_currentStreams.at(response.d_idstate.d_streamID);
+
+ uint32_t statusCode = 200U;
+ std::string contentType;
+ bool sendContentType = true;
+ auto& responseBuffer = context.d_buffer;
+ if (context.d_statusCode != 0) {
+ responseBuffer = std::move(context.d_response);
+ statusCode = context.d_statusCode;
+ contentType = std::move(context.d_contentTypeOut);
+ }
+ else {
+ responseBuffer = std::move(response.d_buffer);
+ }
+
+ sendResponse(response.d_idstate.d_streamID, context, statusCode, d_ci.cs->dohFrontend->d_customResponseHeaders, contentType, sendContentType);
+ handleResponseSent(response);
+
+ return hasPendingWrite() ? IOState::NeedWrite : IOState::Done;
+}
+
+void IncomingHTTP2Connection::notifyIOError(const struct timeval& now, TCPResponse&& response)
+{
+ if (std::this_thread::get_id() != d_creatorThreadID) {
+ /* empty buffer will signal an IO error */
+ response.d_buffer.clear();
+ handleCrossProtocolResponse(now, std::move(response));
+ return;
+ }
+
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay): clang-tidy is getting confused by assert()
+ assert(response.d_idstate.d_streamID != -1);
+ auto& context = d_currentStreams.at(response.d_idstate.d_streamID);
+ context.d_buffer = std::move(response.d_buffer);
+ sendResponse(response.d_idstate.d_streamID, context, 502, d_ci.cs->dohFrontend->d_customResponseHeaders);
+}
+
+bool IncomingHTTP2Connection::sendResponse(IncomingHTTP2Connection::StreamID streamID, IncomingHTTP2Connection::PendingQuery& context, uint16_t responseCode, const HeadersMap& customResponseHeaders, const std::string& contentType, bool addContentType)
+{
+ /* if data_prd is not NULL, it provides data which will be sent in subsequent DATA frames. In this case, a method that allows request message bodies (https://tools.ietf.org/html/rfc7231#section-4) must be specified with :method key (e.g. POST). This function does not take ownership of the data_prd. The function copies the members of the data_prd. If data_prd is NULL, HEADERS have END_STREAM set.
+ */
+ nghttp2_data_provider data_provider;
+
+ data_provider.source.ptr = this;
+ data_provider.read_callback = [](nghttp2_session*, IncomingHTTP2Connection::StreamID stream_id, uint8_t* buf, size_t length, uint32_t* data_flags, nghttp2_data_source* source, void* cb_data) -> ssize_t {
+ auto* connection = static_cast<IncomingHTTP2Connection*>(cb_data);
+ auto& obj = connection->d_currentStreams.at(stream_id);
+ size_t toCopy = 0;
+ if (obj.d_queryPos < obj.d_buffer.size()) {
+ size_t remaining = obj.d_buffer.size() - obj.d_queryPos;
+ toCopy = length > remaining ? remaining : length;
+ memcpy(buf, &obj.d_buffer.at(obj.d_queryPos), toCopy);
+ obj.d_queryPos += toCopy;
+ }
+
+ if (obj.d_queryPos >= obj.d_buffer.size()) {
+ *data_flags |= NGHTTP2_DATA_FLAG_EOF;
+ obj.d_buffer.clear();
+ connection->d_needFlush = true;
+ }
+ return static_cast<ssize_t>(toCopy);
+ };
+
+ const auto& dohFrontend = d_ci.cs->dohFrontend;
+ auto& responseBody = context.d_buffer;
+
+ std::vector<nghttp2_nv> headers;
+ std::string responseCodeStr;
+ std::string cacheControlValue;
+ std::string location;
+ /* remember that dynamic header values should be kept alive
+ until we have called nghttp2_submit_response(), at least */
+ /* status, content-type, cache-control, content-length */
+ headers.reserve(4);
+
+ if (responseCode == 200) {
+ NGHTTP2Headers::addStaticHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::STATUS_NAME, NGHTTP2Headers::HeaderConstantIndexes::OK_200_VALUE);
+ ++dohFrontend->d_validresponses;
+ ++dohFrontend->d_http2Stats.d_nb200Responses;
+
+ if (addContentType) {
+ if (contentType.empty()) {
+ NGHTTP2Headers::addStaticHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_NAME, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_VALUE);
+ }
+ else {
+ NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_NAME, contentType);
+ }
+ }
+
+ if (dohFrontend->d_sendCacheControlHeaders && responseBody.size() > sizeof(dnsheader)) {
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): API
+ uint32_t minTTL = getDNSPacketMinTTL(reinterpret_cast<const char*>(responseBody.data()), responseBody.size());
+ if (minTTL != std::numeric_limits<uint32_t>::max()) {
+ cacheControlValue = "max-age=" + std::to_string(minTTL);
+ NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CACHE_CONTROL_NAME, cacheControlValue);
+ }
+ }
+ }
+ else {
+ responseCodeStr = std::to_string(responseCode);
+ NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::STATUS_NAME, responseCodeStr);
+
+ if (responseCode >= 300 && responseCode < 400) {
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
+ location = std::string(reinterpret_cast<const char*>(responseBody.data()), responseBody.size());
+ NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_NAME, "text/html; charset=utf-8");
+ NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::LOCATION_NAME, location);
+ static const std::string s_redirectStart{"<!DOCTYPE html><TITLE>Moved</TITLE><P>The document has moved <A HREF=\""};
+ static const std::string s_redirectEnd{"\">here</A>"};
+ responseBody.reserve(s_redirectStart.size() + responseBody.size() + s_redirectEnd.size());
+ responseBody.insert(responseBody.begin(), s_redirectStart.begin(), s_redirectStart.end());
+ responseBody.insert(responseBody.end(), s_redirectEnd.begin(), s_redirectEnd.end());
+ ++dohFrontend->d_redirectresponses;
+ }
+ else {
+ ++dohFrontend->d_errorresponses;
+ switch (responseCode) {
+ case 400:
+ ++dohFrontend->d_http2Stats.d_nb400Responses;
+ break;
+ case 403:
+ ++dohFrontend->d_http2Stats.d_nb403Responses;
+ break;
+ case 500:
+ ++dohFrontend->d_http2Stats.d_nb500Responses;
+ break;
+ case 502:
+ ++dohFrontend->d_http2Stats.d_nb502Responses;
+ break;
+ default:
+ ++dohFrontend->d_http2Stats.d_nbOtherResponses;
+ break;
+ }
+
+ if (!responseBody.empty()) {
+ NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_NAME, "text/plain; charset=utf-8");
+ }
+ else {
+ static const std::string invalid{"invalid DNS query"};
+ static const std::string notAllowed{"dns query not allowed"};
+ static const std::string noDownstream{"no downstream server available"};
+ static const std::string internalServerError{"Internal Server Error"};
+
+ switch (responseCode) {
+ case 400:
+ responseBody.insert(responseBody.begin(), invalid.begin(), invalid.end());
+ break;
+ case 403:
+ responseBody.insert(responseBody.begin(), notAllowed.begin(), notAllowed.end());
+ break;
+ case 502:
+ responseBody.insert(responseBody.begin(), noDownstream.begin(), noDownstream.end());
+ break;
+ case 500:
+ /* fall-through */
+ default:
+ responseBody.insert(responseBody.begin(), internalServerError.begin(), internalServerError.end());
+ break;
+ }
+ }
+ }
+ }
+
+ const std::string contentLength = std::to_string(responseBody.size());
+ NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_LENGTH_NAME, contentLength);
+
+ for (const auto& [key, value] : customResponseHeaders) {
+ NGHTTP2Headers::addCustomDynamicHeader(headers, key, value);
+ }
+
+ auto ret = nghttp2_submit_response(d_session.get(), streamID, headers.data(), headers.size(), &data_provider);
+ if (ret != 0) {
+ d_currentStreams.erase(streamID);
+ vinfolog("Error submitting HTTP response for stream %d: %s", streamID, nghttp2_strerror(ret));
+ return false;
+ }
+
+ ret = nghttp2_session_send(d_session.get());
+ if (ret != 0) {
+ d_currentStreams.erase(streamID);
+ vinfolog("Error flushing HTTP response for stream %d: %s", streamID, nghttp2_strerror(ret));
+ return false;
+ }
+
+ return true;
+}
+
+static void processForwardedForHeader(const std::unique_ptr<HeadersMap>& headers, ComboAddress& remote)
+{
+ if (!headers) {
+ return;
+ }
+
+ auto headerIt = headers->find(s_xForwardedForHeaderName);
+ if (headerIt == headers->end()) {
+ return;
+ }
+
+ std::string_view value = headerIt->second;
+ try {
+ auto pos = value.rfind(',');
+ if (pos != std::string_view::npos) {
+ ++pos;
+ for (; pos < value.size() && value[pos] == ' '; ++pos) {
+ }
+
+ if (pos < value.size()) {
+ value = value.substr(pos);
+ }
+ }
+ auto newRemote = ComboAddress(std::string(value));
+ remote = newRemote;
+ }
+ catch (const std::exception& e) {
+ vinfolog("Invalid X-Forwarded-For header ('%s') received from %s : %s", std::string(value), remote.toStringWithPort(), e.what());
+ }
+ catch (const PDNSException& e) {
+ vinfolog("Invalid X-Forwarded-For header ('%s') received from %s : %s", std::string(value), remote.toStringWithPort(), e.reason);
+ }
+}
+
+void IncomingHTTP2Connection::handleIncomingQuery(IncomingHTTP2Connection::PendingQuery&& query, IncomingHTTP2Connection::StreamID streamID)
+{
+ const auto handleImmediateResponse = [this, &query, streamID](uint16_t code, const std::string& reason, PacketBuffer&& response = PacketBuffer()) {
+ if (response.empty()) {
+ query.d_buffer.clear();
+ query.d_buffer.insert(query.d_buffer.begin(), reason.begin(), reason.end());
+ }
+ else {
+ query.d_buffer = std::move(response);
+ }
+ vinfolog("Sending an immediate %d response to incoming DoH query: %s", code, reason);
+ sendResponse(streamID, query, code, d_ci.cs->dohFrontend->d_customResponseHeaders);
+ };
+
+ if (query.d_method == PendingQuery::Method::Unknown || query.d_method == PendingQuery::Method::Unsupported) {
+ handleImmediateResponse(400, "DoH query not allowed because of unsupported HTTP method");
+ return;
+ }
+
+ ++d_ci.cs->dohFrontend->d_http2Stats.d_nbQueries;
+
+ if (d_ci.cs->dohFrontend->d_trustForwardedForHeader) {
+ processForwardedForHeader(query.d_headers, d_proxiedRemote);
+
+ /* second ACL lookup based on the updated address */
+ auto& holders = d_threadData.holders;
+ if (!holders.acl->match(d_proxiedRemote)) {
+ ++dnsdist::metrics::g_stats.aclDrops;
+ vinfolog("Query from %s (%s) (DoH) dropped because of ACL", d_ci.remote.toStringWithPort(), d_proxiedRemote.toStringWithPort());
+ handleImmediateResponse(403, "DoH query not allowed because of ACL");
+ return;
+ }
+
+ if (!d_ci.cs->dohFrontend->d_keepIncomingHeaders) {
+ query.d_headers.reset();
+ }
+ }
+
+ if (d_ci.cs->dohFrontend->d_exactPathMatching) {
+ if (d_ci.cs->dohFrontend->d_urls.count(query.d_path) == 0) {
+ handleImmediateResponse(404, "there is no endpoint configured for this path");
+ return;
+ }
+ }
+ else {
+ bool found = false;
+ for (const auto& path : d_ci.cs->dohFrontend->d_urls) {
+ if (boost::starts_with(query.d_path, path)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ handleImmediateResponse(404, "there is no endpoint configured for this path");
+ return;
+ }
+ }
+
+ /* the responses map can be updated at runtime, so we need to take a copy of
+ the shared pointer, increasing the reference counter */
+ auto responsesMap = d_ci.cs->dohFrontend->d_responsesMap;
+ if (responsesMap) {
+ for (const auto& entry : *responsesMap) {
+ if (entry->matches(query.d_path)) {
+ const auto& customHeaders = entry->getHeaders();
+ query.d_buffer = entry->getContent();
+ if (entry->getStatusCode() >= 400 && !query.d_buffer.empty()) {
+ // legacy trailing 0 from the h2o era
+ query.d_buffer.pop_back();
+ }
+
+ sendResponse(streamID, query, entry->getStatusCode(), customHeaders ? *customHeaders : d_ci.cs->dohFrontend->d_customResponseHeaders, std::string(), false);
+ return;
+ }
+ }
+ }
+
+ if (query.d_buffer.empty() && query.d_method == PendingQuery::Method::Get && !query.d_queryString.empty()) {
+ auto payload = dnsdist::doh::getPayloadFromPath(query.d_queryString);
+ if (payload) {
+ query.d_buffer = std::move(*payload);
+ }
+ else {
+ ++d_ci.cs->dohFrontend->d_badrequests;
+ handleImmediateResponse(400, "DoH unable to decode BASE64-URL");
+ return;
+ }
+ }
+
+ if (query.d_method == PendingQuery::Method::Get) {
+ ++d_ci.cs->dohFrontend->d_getqueries;
+ }
+ else if (query.d_method == PendingQuery::Method::Post) {
+ ++d_ci.cs->dohFrontend->d_postqueries;
+ }
+
+ try {
+ struct timeval now
+ {
+ };
+ gettimeofday(&now, nullptr);
+ auto processingResult = handleQuery(std::move(query.d_buffer), now, streamID);
+
+ switch (processingResult) {
+ case QueryProcessingResult::TooSmall:
+ handleImmediateResponse(400, "DoH non-compliant query");
+ break;
+ case QueryProcessingResult::InvalidHeaders:
+ handleImmediateResponse(400, "DoH invalid headers");
+ break;
+ case QueryProcessingResult::Dropped:
+ handleImmediateResponse(403, "DoH dropped query");
+ break;
+ case QueryProcessingResult::NoBackend:
+ handleImmediateResponse(502, "DoH no backend available");
+ return;
+ case QueryProcessingResult::Forwarded:
+ case QueryProcessingResult::Asynchronous:
+ case QueryProcessingResult::SelfAnswered:
+ break;
+ }
+ }
+ catch (const std::exception& e) {
+ vinfolog("Exception while processing DoH query: %s", e.what());
+ handleImmediateResponse(400, "DoH non-compliant query");
+ return;
+ }
+}
+
+int IncomingHTTP2Connection::on_frame_recv_callback(nghttp2_session* session, const nghttp2_frame* frame, void* user_data)
+{
+ auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
+ /* is this the last frame for this stream? */
+ if ((frame->hd.type == NGHTTP2_HEADERS || frame->hd.type == NGHTTP2_DATA) && (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) != 0) {
+ auto streamID = frame->hd.stream_id;
+ auto stream = conn->d_currentStreams.find(streamID);
+ if (stream != conn->d_currentStreams.end()) {
+ conn->handleIncomingQuery(std::move(stream->second), streamID);
+ }
+ else {
+ vinfolog("Stream %d NOT FOUND", streamID);
+ return NGHTTP2_ERR_CALLBACK_FAILURE;
+ }
+ }
+
+ return 0;
+}
+
+int IncomingHTTP2Connection::on_stream_close_callback(nghttp2_session* session, IncomingHTTP2Connection::StreamID stream_id, uint32_t error_code, void* user_data)
+{
+ auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
+
+ conn->d_currentStreams.erase(stream_id);
+ return 0;
+}
+
+int IncomingHTTP2Connection::on_begin_headers_callback(nghttp2_session* session, const nghttp2_frame* frame, void* user_data)
+{
+ if (frame->hd.type != NGHTTP2_HEADERS || frame->headers.cat != NGHTTP2_HCAT_REQUEST) {
+ return 0;
+ }
+
+ auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
+ auto insertPair = conn->d_currentStreams.emplace(frame->hd.stream_id, PendingQuery());
+ if (!insertPair.second) {
+ /* there is a stream ID collision, something is very wrong! */
+ vinfolog("Stream ID collision (%d) on connection from %d", frame->hd.stream_id, conn->d_ci.remote.toStringWithPort());
+ conn->d_connectionClosing = true;
+ conn->d_needFlush = true;
+ nghttp2_session_terminate_session(conn->d_session.get(), NGHTTP2_NO_ERROR);
+ auto ret = nghttp2_session_send(conn->d_session.get());
+ if (ret != 0) {
+ vinfolog("Error flushing HTTP response for stream %d from %s: %s", frame->hd.stream_id, conn->d_ci.remote.toStringWithPort(), nghttp2_strerror(ret));
+ return NGHTTP2_ERR_CALLBACK_FAILURE;
+ }
+
+ return 0;
+ }
+
+ return 0;
+}
+
+static std::string::size_type getLengthOfPathWithoutParameters(const std::string_view& path)
+{
+ auto pos = path.find('?');
+ if (pos == string::npos) {
+ return path.size();
+ }
+
+ return pos;
+}
+
+int IncomingHTTP2Connection::on_header_callback(nghttp2_session* session, const nghttp2_frame* frame, const uint8_t* name, size_t nameLen, const uint8_t* value, size_t valuelen, uint8_t flags, void* user_data)
+{
+ auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
+
+ if (frame->hd.type == NGHTTP2_HEADERS && frame->headers.cat == NGHTTP2_HCAT_REQUEST) {
+ if (nghttp2_check_header_name(name, nameLen) == 0) {
+ vinfolog("Invalid header name");
+ return NGHTTP2_ERR_CALLBACK_FAILURE;
+ }
+
+#if HAVE_NGHTTP2_CHECK_HEADER_VALUE_RFC9113
+ if (nghttp2_check_header_value_rfc9113(value, valuelen) == 0) {
+ vinfolog("Invalid header value");
+ return NGHTTP2_ERR_CALLBACK_FAILURE;
+ }
+#endif /* HAVE_NGHTTP2_CHECK_HEADER_VALUE_RFC9113 */
+
+ auto headerMatches = [name, nameLen](const std::string& expected) -> bool {
+ return nameLen == expected.size() && memcmp(name, expected.data(), expected.size()) == 0;
+ };
+
+ auto stream = conn->d_currentStreams.find(frame->hd.stream_id);
+ if (stream == conn->d_currentStreams.end()) {
+ vinfolog("Unable to match the stream ID %d to a known one!", frame->hd.stream_id);
+ return NGHTTP2_ERR_CALLBACK_FAILURE;
+ }
+ auto& query = stream->second;
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): nghttp2 API
+ auto valueView = std::string_view(reinterpret_cast<const char*>(value), valuelen);
+ if (headerMatches(s_pathHeaderName)) {
+#if HAVE_NGHTTP2_CHECK_PATH
+ if (nghttp2_check_path(value, valuelen) == 0) {
+ vinfolog("Invalid path value");
+ return NGHTTP2_ERR_CALLBACK_FAILURE;
+ }
+#endif /* HAVE_NGHTTP2_CHECK_PATH */
+
+ auto pathLen = getLengthOfPathWithoutParameters(valueView);
+ query.d_path = valueView.substr(0, pathLen);
+ if (pathLen < valueView.size()) {
+ query.d_queryString = valueView.substr(pathLen);
+ }
+ }
+ else if (headerMatches(s_authorityHeaderName)) {
+ query.d_host = valueView;
+ }
+ else if (headerMatches(s_schemeHeaderName)) {
+ query.d_scheme = valueView;
+ }
+ else if (headerMatches(s_methodHeaderName)) {
+#if HAVE_NGHTTP2_CHECK_METHOD
+ if (nghttp2_check_method(value, valuelen) == 0) {
+ vinfolog("Invalid method value");
+ return NGHTTP2_ERR_CALLBACK_FAILURE;
+ }
+#endif /* HAVE_NGHTTP2_CHECK_METHOD */
+
+ if (valueView == "GET") {
+ query.d_method = PendingQuery::Method::Get;
+ }
+ else if (valueView == "POST") {
+ query.d_method = PendingQuery::Method::Post;
+ }
+ else {
+ query.d_method = PendingQuery::Method::Unsupported;
+ vinfolog("Unsupported method value");
+ return 0;
+ }
+ }
+
+ if (conn->d_ci.cs->dohFrontend->d_keepIncomingHeaders || (conn->d_ci.cs->dohFrontend->d_trustForwardedForHeader && headerMatches(s_xForwardedForHeaderName))) {
+ if (!query.d_headers) {
+ query.d_headers = std::make_unique<HeadersMap>();
+ }
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): nghttp2 API
+ query.d_headers->insert({std::string(reinterpret_cast<const char*>(name), nameLen), std::string(valueView)});
+ }
+ }
+ return 0;
+}
+
+int IncomingHTTP2Connection::on_data_chunk_recv_callback(nghttp2_session* session, uint8_t flags, IncomingHTTP2Connection::StreamID stream_id, const uint8_t* data, size_t len, void* user_data)
+{
+ auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
+ auto stream = conn->d_currentStreams.find(stream_id);
+ if (stream == conn->d_currentStreams.end()) {
+ vinfolog("Unable to match the stream ID %d to a known one!", stream_id);
+ return NGHTTP2_ERR_CALLBACK_FAILURE;
+ }
+ if (len > std::numeric_limits<uint16_t>::max() || (std::numeric_limits<uint16_t>::max() - stream->second.d_buffer.size()) < len) {
+ vinfolog("Data frame of size %d is too large for a DNS query (we already have %d)", len, stream->second.d_buffer.size());
+ return NGHTTP2_ERR_CALLBACK_FAILURE;
+ }
+
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic): nghttp2 API
+ stream->second.d_buffer.insert(stream->second.d_buffer.end(), data, data + len);
+
+ return 0;
+}
+
+int IncomingHTTP2Connection::on_error_callback(nghttp2_session* session, int lib_error_code, const char* msg, size_t len, void* user_data)
+{
+ auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
+
+ vinfolog("Error in HTTP/2 connection from %d: %s", conn->d_ci.remote.toStringWithPort(), std::string(msg, len));
+ conn->d_connectionClosing = true;
+ conn->d_needFlush = true;
+ nghttp2_session_terminate_session(conn->d_session.get(), NGHTTP2_NO_ERROR);
+ auto ret = nghttp2_session_send(conn->d_session.get());
+ if (ret != 0) {
+ vinfolog("Error flushing HTTP response on connection from %s: %s", conn->d_ci.remote.toStringWithPort(), nghttp2_strerror(ret));
+ return NGHTTP2_ERR_CALLBACK_FAILURE;
+ }
+
+ return 0;
+}
+
+IOState IncomingHTTP2Connection::readHTTPData()
+{
+ IOState newState = IOState::Done;
+ size_t got = 0;
+ if (d_in.size() < s_initialReceiveBufferSize) {
+ d_in.resize(std::max(s_initialReceiveBufferSize, d_in.capacity()));
+ }
+ try {
+ newState = d_handler.tryRead(d_in, got, d_in.size(), true);
+ d_in.resize(got);
+
+ if (got > 0) {
+ /* we got something */
+ auto readlen = nghttp2_session_mem_recv(d_session.get(), d_in.data(), d_in.size());
+ /* as long as we don't require a pause by returning nghttp2_error.NGHTTP2_ERR_PAUSE from a CB,
+ all data should be consumed before returning */
+ if (readlen < 0 || static_cast<size_t>(readlen) < d_in.size()) {
+ throw std::runtime_error("Fatal error while passing received data to nghttp2: " + std::string(nghttp2_strerror((int)readlen)));
+ }
+
+ nghttp2_session_send(d_session.get());
+ }
+ }
+ catch (const std::exception& e) {
+ vinfolog("Exception while trying to read from HTTP client connection to %s: %s", d_ci.remote.toStringWithPort(), e.what());
+ handleIOError();
+ return IOState::Done;
+ }
+ return newState;
+}
+
+void IncomingHTTP2Connection::handleReadableIOCallback([[maybe_unused]] int descriptor, FDMultiplexer::funcparam_t& param)
+{
+ auto conn = boost::any_cast<std::shared_ptr<IncomingHTTP2Connection>>(param);
+ conn->handleIO();
+}
+
+void IncomingHTTP2Connection::handleWritableIOCallback([[maybe_unused]] int descriptor, FDMultiplexer::funcparam_t& param)
+{
+ auto conn = boost::any_cast<std::shared_ptr<IncomingHTTP2Connection>>(param);
+ conn->writeToSocket(true);
+}
+
+void IncomingHTTP2Connection::stopIO()
+{
+ if (d_ioState) {
+ d_ioState->reset();
+ }
+}
+
+uint32_t IncomingHTTP2Connection::getConcurrentStreamsCount() const
+{
+ return d_currentStreams.size();
+}
+
+boost::optional<struct timeval> IncomingHTTP2Connection::getIdleClientReadTTD(struct timeval now) const
+{
+ auto idleTimeout = d_ci.cs->dohFrontend->d_idleTimeout;
+ if (g_maxTCPConnectionDuration == 0 && idleTimeout == 0) {
+ return boost::none;
+ }
+
+ if (g_maxTCPConnectionDuration > 0) {
+ auto elapsed = now.tv_sec - d_connectionStartTime.tv_sec;
+ if (elapsed < 0 || (static_cast<size_t>(elapsed) >= g_maxTCPConnectionDuration)) {
+ return now;
+ }
+ auto remaining = g_maxTCPConnectionDuration - elapsed;
+ if (idleTimeout == 0 || remaining <= static_cast<size_t>(idleTimeout)) {
+ now.tv_sec += static_cast<time_t>(remaining);
+ return now;
+ }
+ }
+
+ now.tv_sec += idleTimeout;
+ return now;
+}
+
+void IncomingHTTP2Connection::updateIO(IOState newState, const FDMultiplexer::callbackfunc_t& callback)
+{
+ boost::optional<struct timeval> ttd{boost::none};
+
+ auto shared = std::dynamic_pointer_cast<IncomingHTTP2Connection>(shared_from_this());
+ if (!shared || !d_ioState) {
+ return;
+ }
+
+ timeval now{};
+ gettimeofday(&now, nullptr);
+
+ if (newState == IOState::NeedRead) {
+ /* use the idle TTL if the handshake has been completed (and proxy protocol payload received, if any),
+ and we have processed at least one query, otherwise we use the shorter read TTL */
+ if ((d_state == State::waitingForQuery || d_state == State::idle) && (d_queriesCount > 0 || d_currentQueriesCount > 0)) {
+ ttd = getIdleClientReadTTD(now);
+ }
+ else {
+ ttd = getClientReadTTD(now);
+ }
+ d_ioState->update(newState, callback, shared, ttd);
+ }
+ else if (newState == IOState::NeedWrite) {
+ ttd = getClientWriteTTD(now);
+ d_ioState->update(newState, callback, shared, ttd);
+ }
+}
+
+void IncomingHTTP2Connection::handleIOError()
+{
+ d_connectionDied = true;
+ d_out.clear();
+ d_outPos = 0;
+ nghttp2_session_terminate_session(d_session.get(), NGHTTP2_PROTOCOL_ERROR);
+ d_currentStreams.clear();
+ stopIO();
+}
+
+bool IncomingHTTP2Connection::active() const
+{
+ return !d_connectionDied && d_ioState != nullptr;
+}
+
+#endif /* HAVE_DNS_OVER_HTTPS && HAVE_NGHTTP2 */