]> git.ipfire.org Git - thirdparty/pdns.git/blob - pdns/dnsdistdist/dnsdist-nghttp2-in.cc
Merge pull request #13381 from rgacogne/ddist-clean-up-nghttp2-no-doh
[thirdparty/pdns.git] / pdns / dnsdistdist / dnsdist-nghttp2-in.cc
1 /*
2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of version 2 of the GNU General Public License as
7 * published by the Free Software Foundation.
8 *
9 * In addition, for the avoidance of any doubt, permission is granted to
10 * link this program with OpenSSL and to (re)distribute the binaries
11 * produced as the result of such linking.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 */
22
23 #include "base64.hh"
24 #include "dnsdist-dnsparser.hh"
25 #include "dnsdist-nghttp2-in.hh"
26 #include "dnsdist-proxy-protocol.hh"
27 #include "dnsparser.hh"
28
29 #if defined(HAVE_DNS_OVER_HTTPS) && defined(HAVE_NGHTTP2)
30
31 #if 0
32 class IncomingDoHCrossProtocolContext : public CrossProtocolContext
33 {
34 public:
35 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))
36 {
37 }
38
39 std::optional<std::string> getHTTPPath() const override
40 {
41 return d_query.d_path;
42 }
43
44 std::optional<std::string> getHTTPScheme() const override
45 {
46 return d_query.d_scheme;
47 }
48
49 std::optional<std::string> getHTTPHost() const override
50 {
51 return d_query.d_host;
52 }
53
54 std::optional<std::string> getHTTPQueryString() const override
55 {
56 return d_query.d_queryString;
57 }
58
59 std::optional<HeadersMap> getHTTPHeaders() const override
60 {
61 if (!d_query.d_headers) {
62 return std::nullopt;
63 }
64 return *d_query.d_headers;
65 }
66
67 void handleResponse(PacketBuffer&& response, InternalQueryState&& state) override
68 {
69 auto conn = d_connection.lock();
70 if (!conn) {
71 /* the connection has been closed in the meantime */
72 return;
73 }
74 }
75
76 void handleTimeout() override
77 {
78 auto conn = d_connection.lock();
79 if (!conn) {
80 /* the connection has been closed in the meantime */
81 return;
82 }
83 }
84
85 ~IncomingDoHCrossProtocolContext() override
86 {
87 }
88
89 private:
90 std::weak_ptr<IncomingHTTP2Connection> d_connection;
91 IncomingHTTP2Connection::PendingQuery d_query;
92 IncomingHTTP2Connection::StreamID d_streamID{-1};
93 };
94 #endif
95
96 class IncomingDoHCrossProtocolContext : public DOHUnitInterface
97 {
98 public:
99 IncomingDoHCrossProtocolContext(IncomingHTTP2Connection::PendingQuery&& query, const std::shared_ptr<IncomingHTTP2Connection>& connection, IncomingHTTP2Connection::StreamID streamID) :
100 d_connection(connection), d_query(std::move(query)), d_streamID(streamID)
101 {
102 }
103 IncomingDoHCrossProtocolContext(const IncomingDoHCrossProtocolContext&) = delete;
104 IncomingDoHCrossProtocolContext(IncomingDoHCrossProtocolContext&&) = delete;
105 IncomingDoHCrossProtocolContext& operator=(const IncomingDoHCrossProtocolContext&) = delete;
106 IncomingDoHCrossProtocolContext& operator=(IncomingDoHCrossProtocolContext&&) = delete;
107
108 ~IncomingDoHCrossProtocolContext() override = default;
109
110 [[nodiscard]] std::string getHTTPPath() const override
111 {
112 return d_query.d_path;
113 }
114
115 [[nodiscard]] const std::string& getHTTPScheme() const override
116 {
117 return d_query.d_scheme;
118 }
119
120 [[nodiscard]] const std::string& getHTTPHost() const override
121 {
122 return d_query.d_host;
123 }
124
125 [[nodiscard]] std::string getHTTPQueryString() const override
126 {
127 return d_query.d_queryString;
128 }
129
130 [[nodiscard]] const HeadersMap& getHTTPHeaders() const override
131 {
132 if (!d_query.d_headers) {
133 static const HeadersMap empty{};
134 return empty;
135 }
136 return *d_query.d_headers;
137 }
138
139 void setHTTPResponse(uint16_t statusCode, PacketBuffer&& body, const std::string& contentType = "") override
140 {
141 d_query.d_statusCode = statusCode;
142 d_query.d_response = std::move(body);
143 d_query.d_contentTypeOut = contentType;
144 }
145
146 void handleUDPResponse(PacketBuffer&& response, InternalQueryState&& state, const std::shared_ptr<DownstreamState>& downstream_) override
147 {
148 std::unique_ptr<DOHUnitInterface> unit(this);
149 auto conn = d_connection.lock();
150 if (!conn) {
151 /* the connection has been closed in the meantime */
152 return;
153 }
154
155 state.du = std::move(unit);
156 TCPResponse resp(std::move(response), std::move(state), nullptr, nullptr);
157 resp.d_ds = downstream_;
158 struct timeval now
159 {
160 };
161 gettimeofday(&now, nullptr);
162 conn->handleResponse(now, std::move(resp));
163 }
164
165 void handleTimeout() override
166 {
167 std::unique_ptr<DOHUnitInterface> unit(this);
168 auto conn = d_connection.lock();
169 if (!conn) {
170 /* the connection has been closed in the meantime */
171 return;
172 }
173 struct timeval now
174 {
175 };
176 gettimeofday(&now, nullptr);
177 TCPResponse resp;
178 resp.d_idstate.d_streamID = d_streamID;
179 conn->notifyIOError(now, std::move(resp));
180 }
181
182 std::weak_ptr<IncomingHTTP2Connection> d_connection;
183 IncomingHTTP2Connection::PendingQuery d_query;
184 IncomingHTTP2Connection::StreamID d_streamID{-1};
185 };
186
187 void IncomingHTTP2Connection::handleResponse(const struct timeval& now, TCPResponse&& response)
188 {
189 if (std::this_thread::get_id() != d_creatorThreadID) {
190 handleCrossProtocolResponse(now, std::move(response));
191 return;
192 }
193
194 auto& state = response.d_idstate;
195 if (state.forwardedOverUDP) {
196 dnsheader_aligned responseDH(response.d_buffer.data());
197
198 if (responseDH.get()->tc && state.d_packet && state.d_packet->size() > state.d_proxyProtocolPayloadSize && state.d_packet->size() - state.d_proxyProtocolPayloadSize > sizeof(dnsheader)) {
199 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());
200 auto& query = *state.d_packet;
201 dnsdist::PacketMangling::editDNSHeaderFromRawPacket(&query.at(state.d_proxyProtocolPayloadSize), [origID = state.origID](dnsheader& header) {
202 /* restoring the original ID */
203 header.id = origID;
204 return true;
205 });
206
207 state.forwardedOverUDP = false;
208 bool proxyProtocolPayloadAdded = state.d_proxyProtocolPayloadSize > 0;
209 auto cpq = getCrossProtocolQuery(std::move(query), std::move(state), response.d_ds);
210 cpq->query.d_proxyProtocolPayloadAdded = proxyProtocolPayloadAdded;
211 if (g_tcpclientthreads && g_tcpclientthreads->passCrossProtocolQueryToThread(std::move(cpq))) {
212 return;
213 }
214 vinfolog("Unable to pass DoH query to a TCP worker thread after getting a TC response over UDP");
215 notifyIOError(now, std::move(response));
216 return;
217 }
218 }
219
220 IncomingTCPConnectionState::handleResponse(now, std::move(response));
221 }
222
223 std::unique_ptr<DOHUnitInterface> IncomingHTTP2Connection::getDOHUnit(uint32_t streamID)
224 {
225 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay): clang-tidy is getting confused by assert()
226 assert(streamID <= std::numeric_limits<IncomingHTTP2Connection::StreamID>::max());
227 // NOLINTNEXTLINE(*-narrowing-conversions): generic interface between DNS and DoH with different types
228 auto query = std::move(d_currentStreams.at(static_cast<IncomingHTTP2Connection::StreamID>(streamID)));
229 return std::make_unique<IncomingDoHCrossProtocolContext>(std::move(query), std::dynamic_pointer_cast<IncomingHTTP2Connection>(shared_from_this()), streamID);
230 }
231
232 void IncomingHTTP2Connection::restoreDOHUnit(std::unique_ptr<DOHUnitInterface>&& unit)
233 {
234 auto context = std::unique_ptr<IncomingDoHCrossProtocolContext>(dynamic_cast<IncomingDoHCrossProtocolContext*>(unit.release()));
235 if (context) {
236 d_currentStreams.at(context->d_streamID) = std::move(context->d_query);
237 }
238 }
239
240 IncomingHTTP2Connection::IncomingHTTP2Connection(ConnectionInfo&& connectionInfo, TCPClientThreadData& threadData, const struct timeval& now) :
241 IncomingTCPConnectionState(std::move(connectionInfo), threadData, now)
242 {
243 nghttp2_session_callbacks* cbs = nullptr;
244 if (nghttp2_session_callbacks_new(&cbs) != 0) {
245 throw std::runtime_error("Unable to create a callback object for a new incoming HTTP/2 session");
246 }
247 std::unique_ptr<nghttp2_session_callbacks, void (*)(nghttp2_session_callbacks*)> callbacks(cbs, nghttp2_session_callbacks_del);
248 cbs = nullptr;
249
250 nghttp2_session_callbacks_set_send_callback(callbacks.get(), send_callback);
251 nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks.get(), on_frame_recv_callback);
252 nghttp2_session_callbacks_set_on_stream_close_callback(callbacks.get(), on_stream_close_callback);
253 nghttp2_session_callbacks_set_on_begin_headers_callback(callbacks.get(), on_begin_headers_callback);
254 nghttp2_session_callbacks_set_on_header_callback(callbacks.get(), on_header_callback);
255 nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks.get(), on_data_chunk_recv_callback);
256 nghttp2_session_callbacks_set_error_callback2(callbacks.get(), on_error_callback);
257
258 nghttp2_session* sess = nullptr;
259 if (nghttp2_session_server_new(&sess, callbacks.get(), this) != 0) {
260 throw std::runtime_error("Coult not allocate a new incoming HTTP/2 session");
261 }
262
263 d_session = std::unique_ptr<nghttp2_session, decltype(&nghttp2_session_del)>(sess, nghttp2_session_del);
264 sess = nullptr;
265 }
266
267 bool IncomingHTTP2Connection::checkALPN()
268 {
269 constexpr std::array<uint8_t, 2> h2ALPN{'h', '2'};
270 const auto protocols = d_handler.getNextProtocol();
271 if (protocols.size() == h2ALPN.size() && memcmp(protocols.data(), h2ALPN.data(), h2ALPN.size()) == 0) {
272 return true;
273 }
274 vinfolog("DoH connection from %s expected ALPN value 'h2', got '%s'", d_ci.remote.toStringWithPort(), std::string(protocols.begin(), protocols.end()));
275 return false;
276 }
277
278 void IncomingHTTP2Connection::handleConnectionReady()
279 {
280 constexpr std::array<nghttp2_settings_entry, 1> settings{{{NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100U}}};
281 auto ret = nghttp2_submit_settings(d_session.get(), NGHTTP2_FLAG_NONE, settings.data(), settings.size());
282 if (ret != 0) {
283 throw std::runtime_error("Fatal error: " + std::string(nghttp2_strerror(ret)));
284 }
285 d_needFlush = true;
286 ret = nghttp2_session_send(d_session.get());
287 if (ret != 0) {
288 throw std::runtime_error("Fatal error: " + std::string(nghttp2_strerror(ret)));
289 }
290 }
291
292 bool IncomingHTTP2Connection::hasPendingWrite() const
293 {
294 return d_pendingWrite;
295 }
296
297 IOState IncomingHTTP2Connection::handleHandshake(const struct timeval& now)
298 {
299 auto iostate = d_handler.tryHandshake();
300 if (iostate == IOState::Done) {
301 handleHandshakeDone(now);
302 if (d_handler.isTLS()) {
303 if (!checkALPN()) {
304 d_connectionDied = true;
305 stopIO();
306 return iostate;
307 }
308 }
309
310 if (!isProxyPayloadOutsideTLS() && expectProxyProtocolFrom(d_ci.remote)) {
311 d_state = State::readingProxyProtocolHeader;
312 d_buffer.resize(s_proxyProtocolMinimumHeaderSize);
313 d_proxyProtocolNeed = s_proxyProtocolMinimumHeaderSize;
314 }
315 else {
316 d_state = State::waitingForQuery;
317 handleConnectionReady();
318 }
319 }
320 return iostate;
321 }
322
323 void IncomingHTTP2Connection::handleIO()
324 {
325 IOState iostate = IOState::Done;
326 struct timeval now
327 {
328 };
329 gettimeofday(&now, nullptr);
330
331 try {
332 if (maxConnectionDurationReached(g_maxTCPConnectionDuration, now)) {
333 vinfolog("Terminating DoH connection from %s because it reached the maximum TCP connection duration", d_ci.remote.toStringWithPort());
334 stopIO();
335 d_connectionClosing = true;
336 return;
337 }
338
339 if (d_state == State::starting) {
340 if (isProxyPayloadOutsideTLS() && expectProxyProtocolFrom(d_ci.remote)) {
341 d_state = State::readingProxyProtocolHeader;
342 d_buffer.resize(s_proxyProtocolMinimumHeaderSize);
343 d_proxyProtocolNeed = s_proxyProtocolMinimumHeaderSize;
344 }
345 else {
346 d_state = State::doingHandshake;
347 }
348 }
349
350 if (d_state == State::doingHandshake) {
351 iostate = handleHandshake(now);
352 if (d_connectionDied) {
353 return;
354 }
355 }
356
357 if (d_state == State::readingProxyProtocolHeader) {
358 auto status = handleProxyProtocolPayload();
359 if (status == ProxyProtocolResult::Done) {
360 if (isProxyPayloadOutsideTLS()) {
361 d_state = State::doingHandshake;
362 iostate = handleHandshake(now);
363 if (d_connectionDied) {
364 return;
365 }
366 }
367 else {
368 d_currentPos = 0;
369 d_proxyProtocolNeed = 0;
370 d_buffer.clear();
371 d_state = State::waitingForQuery;
372 handleConnectionReady();
373 }
374 }
375 else if (status == ProxyProtocolResult::Error) {
376 d_connectionDied = true;
377 stopIO();
378 return;
379 }
380 }
381
382 if (active() && !d_connectionClosing && (d_state == State::waitingForQuery || d_state == State::idle)) {
383 do {
384 iostate = readHTTPData();
385 } while (active() && !d_connectionClosing && iostate == IOState::Done);
386 }
387
388 if (!active()) {
389 stopIO();
390 return;
391 }
392 /*
393 So:
394 - if we have a pending write, we need to wait until the socket becomes writable
395 and then call handleWritableCallback
396 - if we have NeedWrite but no pending write, we need to wait until the socket
397 becomes writable but for handleReadableIOCallback
398 - if we have NeedRead, or nghttp2_session_want_read, wait until the socket
399 becomes readable and call handleReadableIOCallback
400 */
401 if (hasPendingWrite()) {
402 updateIO(IOState::NeedWrite, handleWritableIOCallback);
403 }
404 else if (iostate == IOState::NeedWrite) {
405 updateIO(IOState::NeedWrite, handleReadableIOCallback);
406 }
407 else if (!d_connectionClosing) {
408 if (nghttp2_session_want_read(d_session.get()) != 0) {
409 updateIO(IOState::NeedRead, handleReadableIOCallback);
410 }
411 }
412 }
413 catch (const std::exception& e) {
414 vinfolog("Exception when processing IO for incoming DoH connection from %s: %s", d_ci.remote.toStringWithPort(), e.what());
415 d_connectionDied = true;
416 stopIO();
417 }
418 }
419
420 void IncomingHTTP2Connection::writeToSocket(bool socketReady)
421 {
422 try {
423 d_needFlush = false;
424 IOState newState = d_handler.tryWrite(d_out, d_outPos, d_out.size());
425
426 if (newState == IOState::Done) {
427 d_pendingWrite = false;
428 d_out.clear();
429 d_outPos = 0;
430 if (active() && !d_connectionClosing) {
431 updateIO(IOState::NeedRead, handleReadableIOCallback);
432 }
433 else {
434 stopIO();
435 }
436 }
437 else {
438 updateIO(newState, handleWritableIOCallback);
439 d_pendingWrite = true;
440 }
441 }
442 catch (const std::exception& e) {
443 vinfolog("Exception while trying to write (%s) to HTTP client connection to %s: %s", (socketReady ? "ready" : "send"), d_ci.remote.toStringWithPort(), e.what());
444 handleIOError();
445 }
446 }
447
448 ssize_t IncomingHTTP2Connection::send_callback(nghttp2_session* session, const uint8_t* data, size_t length, int flags, void* user_data)
449 {
450 auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
451 if (conn->d_connectionDied) {
452 return static_cast<ssize_t>(length);
453 }
454 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic): nghttp2 API
455 conn->d_out.insert(conn->d_out.end(), data, data + length);
456
457 if (conn->d_connectionClosing || conn->d_needFlush) {
458 conn->writeToSocket(false);
459 }
460
461 return static_cast<ssize_t>(length);
462 }
463
464 static const std::array<const std::string, static_cast<size_t>(NGHTTP2Headers::HeaderConstantIndexes::COUNT)> s_headerConstants{
465 "200",
466 ":method",
467 "POST",
468 ":scheme",
469 "https",
470 ":authority",
471 "x-forwarded-for",
472 ":path",
473 "content-length",
474 ":status",
475 "location",
476 "accept",
477 "application/dns-message",
478 "cache-control",
479 "content-type",
480 "application/dns-message",
481 "user-agent",
482 "nghttp2-" NGHTTP2_VERSION "/dnsdist",
483 "x-forwarded-port",
484 "x-forwarded-proto",
485 "dns-over-udp",
486 "dns-over-tcp",
487 "dns-over-tls",
488 "dns-over-http",
489 "dns-over-https"};
490
491 static const std::string s_authorityHeaderName(":authority");
492 static const std::string s_pathHeaderName(":path");
493 static const std::string s_methodHeaderName(":method");
494 static const std::string s_schemeHeaderName(":scheme");
495 static const std::string s_xForwardedForHeaderName("x-forwarded-for");
496
497 void NGHTTP2Headers::addStaticHeader(std::vector<nghttp2_nv>& headers, NGHTTP2Headers::HeaderConstantIndexes nameKey, NGHTTP2Headers::HeaderConstantIndexes valueKey)
498 {
499 const auto& name = s_headerConstants.at(static_cast<size_t>(nameKey));
500 const auto& value = s_headerConstants.at(static_cast<size_t>(valueKey));
501
502 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast,cppcoreguidelines-pro-type-reinterpret-cast): nghttp2 API
503 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});
504 }
505
506 void NGHTTP2Headers::addCustomDynamicHeader(std::vector<nghttp2_nv>& headers, const std::string& name, const std::string_view& value)
507 {
508 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast,cppcoreguidelines-pro-type-reinterpret-cast): nghttp2 API
509 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});
510 }
511
512 void NGHTTP2Headers::addDynamicHeader(std::vector<nghttp2_nv>& headers, NGHTTP2Headers::HeaderConstantIndexes nameKey, const std::string_view& value)
513 {
514 const auto& name = s_headerConstants.at(static_cast<size_t>(nameKey));
515 NGHTTP2Headers::addCustomDynamicHeader(headers, name, value);
516 }
517
518 IOState IncomingHTTP2Connection::sendResponse(const struct timeval& now, TCPResponse&& response)
519 {
520 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay): clang-tidy is getting confused by assert()
521 assert(response.d_idstate.d_streamID != -1);
522 auto& context = d_currentStreams.at(response.d_idstate.d_streamID);
523
524 uint32_t statusCode = 200U;
525 std::string contentType;
526 bool sendContentType = true;
527 auto& responseBuffer = context.d_buffer;
528 if (context.d_statusCode != 0) {
529 responseBuffer = std::move(context.d_response);
530 statusCode = context.d_statusCode;
531 contentType = std::move(context.d_contentTypeOut);
532 }
533 else {
534 responseBuffer = std::move(response.d_buffer);
535 }
536
537 sendResponse(response.d_idstate.d_streamID, context, statusCode, d_ci.cs->dohFrontend->d_customResponseHeaders, contentType, sendContentType);
538 handleResponseSent(response);
539
540 return hasPendingWrite() ? IOState::NeedWrite : IOState::Done;
541 }
542
543 void IncomingHTTP2Connection::notifyIOError(const struct timeval& now, TCPResponse&& response)
544 {
545 if (std::this_thread::get_id() != d_creatorThreadID) {
546 /* empty buffer will signal an IO error */
547 response.d_buffer.clear();
548 handleCrossProtocolResponse(now, std::move(response));
549 return;
550 }
551
552 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay): clang-tidy is getting confused by assert()
553 assert(response.d_idstate.d_streamID != -1);
554 auto& context = d_currentStreams.at(response.d_idstate.d_streamID);
555 context.d_buffer = std::move(response.d_buffer);
556 sendResponse(response.d_idstate.d_streamID, context, 502, d_ci.cs->dohFrontend->d_customResponseHeaders);
557 }
558
559 bool IncomingHTTP2Connection::sendResponse(IncomingHTTP2Connection::StreamID streamID, IncomingHTTP2Connection::PendingQuery& context, uint16_t responseCode, const HeadersMap& customResponseHeaders, const std::string& contentType, bool addContentType)
560 {
561 /* 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.
562 */
563 nghttp2_data_provider data_provider;
564
565 data_provider.source.ptr = this;
566 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 {
567 auto* connection = static_cast<IncomingHTTP2Connection*>(cb_data);
568 auto& obj = connection->d_currentStreams.at(stream_id);
569 size_t toCopy = 0;
570 if (obj.d_queryPos < obj.d_buffer.size()) {
571 size_t remaining = obj.d_buffer.size() - obj.d_queryPos;
572 toCopy = length > remaining ? remaining : length;
573 memcpy(buf, &obj.d_buffer.at(obj.d_queryPos), toCopy);
574 obj.d_queryPos += toCopy;
575 }
576
577 if (obj.d_queryPos >= obj.d_buffer.size()) {
578 *data_flags |= NGHTTP2_DATA_FLAG_EOF;
579 obj.d_buffer.clear();
580 connection->d_needFlush = true;
581 }
582 return static_cast<ssize_t>(toCopy);
583 };
584
585 const auto& dohFrontend = d_ci.cs->dohFrontend;
586 auto& responseBody = context.d_buffer;
587
588 std::vector<nghttp2_nv> headers;
589 std::string responseCodeStr;
590 std::string cacheControlValue;
591 std::string location;
592 /* remember that dynamic header values should be kept alive
593 until we have called nghttp2_submit_response(), at least */
594 /* status, content-type, cache-control, content-length */
595 headers.reserve(4);
596
597 if (responseCode == 200) {
598 NGHTTP2Headers::addStaticHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::STATUS_NAME, NGHTTP2Headers::HeaderConstantIndexes::OK_200_VALUE);
599 ++dohFrontend->d_validresponses;
600 ++dohFrontend->d_http2Stats.d_nb200Responses;
601
602 if (addContentType) {
603 if (contentType.empty()) {
604 NGHTTP2Headers::addStaticHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_NAME, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_VALUE);
605 }
606 else {
607 NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_NAME, contentType);
608 }
609 }
610
611 if (dohFrontend->d_sendCacheControlHeaders && responseBody.size() > sizeof(dnsheader)) {
612 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): API
613 uint32_t minTTL = getDNSPacketMinTTL(reinterpret_cast<const char*>(responseBody.data()), responseBody.size());
614 if (minTTL != std::numeric_limits<uint32_t>::max()) {
615 cacheControlValue = "max-age=" + std::to_string(minTTL);
616 NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CACHE_CONTROL_NAME, cacheControlValue);
617 }
618 }
619 }
620 else {
621 responseCodeStr = std::to_string(responseCode);
622 NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::STATUS_NAME, responseCodeStr);
623
624 if (responseCode >= 300 && responseCode < 400) {
625 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
626 location = std::string(reinterpret_cast<const char*>(responseBody.data()), responseBody.size());
627 NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_NAME, "text/html; charset=utf-8");
628 NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::LOCATION_NAME, location);
629 static const std::string s_redirectStart{"<!DOCTYPE html><TITLE>Moved</TITLE><P>The document has moved <A HREF=\""};
630 static const std::string s_redirectEnd{"\">here</A>"};
631 responseBody.reserve(s_redirectStart.size() + responseBody.size() + s_redirectEnd.size());
632 responseBody.insert(responseBody.begin(), s_redirectStart.begin(), s_redirectStart.end());
633 responseBody.insert(responseBody.end(), s_redirectEnd.begin(), s_redirectEnd.end());
634 ++dohFrontend->d_redirectresponses;
635 }
636 else {
637 ++dohFrontend->d_errorresponses;
638 switch (responseCode) {
639 case 400:
640 ++dohFrontend->d_http2Stats.d_nb400Responses;
641 break;
642 case 403:
643 ++dohFrontend->d_http2Stats.d_nb403Responses;
644 break;
645 case 500:
646 ++dohFrontend->d_http2Stats.d_nb500Responses;
647 break;
648 case 502:
649 ++dohFrontend->d_http2Stats.d_nb502Responses;
650 break;
651 default:
652 ++dohFrontend->d_http2Stats.d_nbOtherResponses;
653 break;
654 }
655
656 if (!responseBody.empty()) {
657 NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_NAME, "text/plain; charset=utf-8");
658 }
659 else {
660 static const std::string invalid{"invalid DNS query"};
661 static const std::string notAllowed{"dns query not allowed"};
662 static const std::string noDownstream{"no downstream server available"};
663 static const std::string internalServerError{"Internal Server Error"};
664
665 switch (responseCode) {
666 case 400:
667 responseBody.insert(responseBody.begin(), invalid.begin(), invalid.end());
668 break;
669 case 403:
670 responseBody.insert(responseBody.begin(), notAllowed.begin(), notAllowed.end());
671 break;
672 case 502:
673 responseBody.insert(responseBody.begin(), noDownstream.begin(), noDownstream.end());
674 break;
675 case 500:
676 /* fall-through */
677 default:
678 responseBody.insert(responseBody.begin(), internalServerError.begin(), internalServerError.end());
679 break;
680 }
681 }
682 }
683 }
684
685 const std::string contentLength = std::to_string(responseBody.size());
686 NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_LENGTH_NAME, contentLength);
687
688 for (const auto& [key, value] : customResponseHeaders) {
689 NGHTTP2Headers::addCustomDynamicHeader(headers, key, value);
690 }
691
692 auto ret = nghttp2_submit_response(d_session.get(), streamID, headers.data(), headers.size(), &data_provider);
693 if (ret != 0) {
694 d_currentStreams.erase(streamID);
695 vinfolog("Error submitting HTTP response for stream %d: %s", streamID, nghttp2_strerror(ret));
696 return false;
697 }
698
699 ret = nghttp2_session_send(d_session.get());
700 if (ret != 0) {
701 d_currentStreams.erase(streamID);
702 vinfolog("Error flushing HTTP response for stream %d: %s", streamID, nghttp2_strerror(ret));
703 return false;
704 }
705
706 return true;
707 }
708
709 static void processForwardedForHeader(const std::unique_ptr<HeadersMap>& headers, ComboAddress& remote)
710 {
711 if (!headers) {
712 return;
713 }
714
715 auto headerIt = headers->find(s_xForwardedForHeaderName);
716 if (headerIt == headers->end()) {
717 return;
718 }
719
720 std::string_view value = headerIt->second;
721 try {
722 auto pos = value.rfind(',');
723 if (pos != std::string_view::npos) {
724 ++pos;
725 for (; pos < value.size() && value[pos] == ' '; ++pos) {
726 }
727
728 if (pos < value.size()) {
729 value = value.substr(pos);
730 }
731 }
732 auto newRemote = ComboAddress(std::string(value));
733 remote = newRemote;
734 }
735 catch (const std::exception& e) {
736 vinfolog("Invalid X-Forwarded-For header ('%s') received from %s : %s", std::string(value), remote.toStringWithPort(), e.what());
737 }
738 catch (const PDNSException& e) {
739 vinfolog("Invalid X-Forwarded-For header ('%s') received from %s : %s", std::string(value), remote.toStringWithPort(), e.reason);
740 }
741 }
742
743 static std::optional<PacketBuffer> getPayloadFromPath(const std::string_view& path)
744 {
745 std::optional<PacketBuffer> result{std::nullopt};
746
747 if (path.size() <= 5) {
748 return result;
749 }
750
751 auto pos = path.find("?dns=");
752 if (pos == string::npos) {
753 pos = path.find("&dns=");
754 }
755
756 if (pos == string::npos) {
757 return result;
758 }
759
760 // need to base64url decode this
761 string sdns;
762 const size_t payloadSize = path.size() - pos - 5;
763 size_t neededPadding = 0;
764 switch (payloadSize % 4) {
765 case 2:
766 neededPadding = 2;
767 break;
768 case 3:
769 neededPadding = 1;
770 break;
771 }
772 sdns.reserve(payloadSize + neededPadding);
773 sdns = path.substr(pos + 5);
774 for (auto& entry : sdns) {
775 switch (entry) {
776 case '-':
777 entry = '+';
778 break;
779 case '_':
780 entry = '/';
781 break;
782 }
783 }
784
785 if (neededPadding != 0) {
786 // re-add padding that may have been missing
787 sdns.append(neededPadding, '=');
788 }
789
790 PacketBuffer decoded;
791 /* rough estimate so we hopefully don't need a new allocation later */
792 /* We reserve at few additional bytes to be able to add EDNS later */
793 const size_t estimate = ((sdns.size() * 3) / 4);
794 decoded.reserve(estimate);
795 if (B64Decode(sdns, decoded) < 0) {
796 return result;
797 }
798
799 result = std::move(decoded);
800 return result;
801 }
802
803 void IncomingHTTP2Connection::handleIncomingQuery(IncomingHTTP2Connection::PendingQuery&& query, IncomingHTTP2Connection::StreamID streamID)
804 {
805 const auto handleImmediateResponse = [this, &query, streamID](uint16_t code, const std::string& reason, PacketBuffer&& response = PacketBuffer()) {
806 if (response.empty()) {
807 query.d_buffer.clear();
808 query.d_buffer.insert(query.d_buffer.begin(), reason.begin(), reason.end());
809 }
810 else {
811 query.d_buffer = std::move(response);
812 }
813 vinfolog("Sending an immediate %d response to incoming DoH query: %s", code, reason);
814 sendResponse(streamID, query, code, d_ci.cs->dohFrontend->d_customResponseHeaders);
815 };
816
817 if (query.d_method == PendingQuery::Method::Unknown || query.d_method == PendingQuery::Method::Unsupported) {
818 handleImmediateResponse(400, "DoH query not allowed because of unsupported HTTP method");
819 return;
820 }
821
822 ++d_ci.cs->dohFrontend->d_http2Stats.d_nbQueries;
823
824 if (d_ci.cs->dohFrontend->d_trustForwardedForHeader) {
825 processForwardedForHeader(query.d_headers, d_proxiedRemote);
826
827 /* second ACL lookup based on the updated address */
828 auto& holders = d_threadData.holders;
829 if (!holders.acl->match(d_proxiedRemote)) {
830 ++dnsdist::metrics::g_stats.aclDrops;
831 vinfolog("Query from %s (%s) (DoH) dropped because of ACL", d_ci.remote.toStringWithPort(), d_proxiedRemote.toStringWithPort());
832 handleImmediateResponse(403, "DoH query not allowed because of ACL");
833 return;
834 }
835
836 if (!d_ci.cs->dohFrontend->d_keepIncomingHeaders) {
837 query.d_headers.reset();
838 }
839 }
840
841 if (d_ci.cs->dohFrontend->d_exactPathMatching) {
842 if (d_ci.cs->dohFrontend->d_urls.count(query.d_path) == 0) {
843 handleImmediateResponse(404, "there is no endpoint configured for this path");
844 return;
845 }
846 }
847 else {
848 bool found = false;
849 for (const auto& path : d_ci.cs->dohFrontend->d_urls) {
850 if (boost::starts_with(query.d_path, path)) {
851 found = true;
852 break;
853 }
854 }
855 if (!found) {
856 handleImmediateResponse(404, "there is no endpoint configured for this path");
857 return;
858 }
859 }
860
861 /* the responses map can be updated at runtime, so we need to take a copy of
862 the shared pointer, increasing the reference counter */
863 auto responsesMap = d_ci.cs->dohFrontend->d_responsesMap;
864 if (responsesMap) {
865 for (const auto& entry : *responsesMap) {
866 if (entry->matches(query.d_path)) {
867 const auto& customHeaders = entry->getHeaders();
868 query.d_buffer = entry->getContent();
869 if (entry->getStatusCode() >= 400 && !query.d_buffer.empty()) {
870 // legacy trailing 0 from the h2o era
871 query.d_buffer.pop_back();
872 }
873
874 sendResponse(streamID, query, entry->getStatusCode(), customHeaders ? *customHeaders : d_ci.cs->dohFrontend->d_customResponseHeaders, std::string(), false);
875 return;
876 }
877 }
878 }
879
880 if (query.d_buffer.empty() && query.d_method == PendingQuery::Method::Get && !query.d_queryString.empty()) {
881 auto payload = getPayloadFromPath(query.d_queryString);
882 if (payload) {
883 query.d_buffer = std::move(*payload);
884 }
885 else {
886 ++d_ci.cs->dohFrontend->d_badrequests;
887 handleImmediateResponse(400, "DoH unable to decode BASE64-URL");
888 return;
889 }
890 }
891
892 if (query.d_method == PendingQuery::Method::Get) {
893 ++d_ci.cs->dohFrontend->d_getqueries;
894 }
895 else if (query.d_method == PendingQuery::Method::Post) {
896 ++d_ci.cs->dohFrontend->d_postqueries;
897 }
898
899 try {
900 struct timeval now
901 {
902 };
903 gettimeofday(&now, nullptr);
904 auto processingResult = handleQuery(std::move(query.d_buffer), now, streamID);
905
906 switch (processingResult) {
907 case QueryProcessingResult::TooSmall:
908 handleImmediateResponse(400, "DoH non-compliant query");
909 break;
910 case QueryProcessingResult::InvalidHeaders:
911 handleImmediateResponse(400, "DoH invalid headers");
912 break;
913 case QueryProcessingResult::Dropped:
914 handleImmediateResponse(403, "DoH dropped query");
915 break;
916 case QueryProcessingResult::NoBackend:
917 handleImmediateResponse(502, "DoH no backend available");
918 return;
919 case QueryProcessingResult::Forwarded:
920 case QueryProcessingResult::Asynchronous:
921 case QueryProcessingResult::SelfAnswered:
922 break;
923 }
924 }
925 catch (const std::exception& e) {
926 vinfolog("Exception while processing DoH query: %s", e.what());
927 handleImmediateResponse(400, "DoH non-compliant query");
928 return;
929 }
930 }
931
932 int IncomingHTTP2Connection::on_frame_recv_callback(nghttp2_session* session, const nghttp2_frame* frame, void* user_data)
933 {
934 auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
935 /* is this the last frame for this stream? */
936 if ((frame->hd.type == NGHTTP2_HEADERS || frame->hd.type == NGHTTP2_DATA) && (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) != 0) {
937 auto streamID = frame->hd.stream_id;
938 auto stream = conn->d_currentStreams.find(streamID);
939 if (stream != conn->d_currentStreams.end()) {
940 conn->handleIncomingQuery(std::move(stream->second), streamID);
941 }
942 else {
943 vinfolog("Stream %d NOT FOUND", streamID);
944 return NGHTTP2_ERR_CALLBACK_FAILURE;
945 }
946 }
947
948 return 0;
949 }
950
951 int IncomingHTTP2Connection::on_stream_close_callback(nghttp2_session* session, IncomingHTTP2Connection::StreamID stream_id, uint32_t error_code, void* user_data)
952 {
953 auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
954
955 conn->d_currentStreams.erase(stream_id);
956 return 0;
957 }
958
959 int IncomingHTTP2Connection::on_begin_headers_callback(nghttp2_session* session, const nghttp2_frame* frame, void* user_data)
960 {
961 if (frame->hd.type != NGHTTP2_HEADERS || frame->headers.cat != NGHTTP2_HCAT_REQUEST) {
962 return 0;
963 }
964
965 auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
966 auto insertPair = conn->d_currentStreams.emplace(frame->hd.stream_id, PendingQuery());
967 if (!insertPair.second) {
968 /* there is a stream ID collision, something is very wrong! */
969 vinfolog("Stream ID collision (%d) on connection from %d", frame->hd.stream_id, conn->d_ci.remote.toStringWithPort());
970 conn->d_connectionClosing = true;
971 conn->d_needFlush = true;
972 nghttp2_session_terminate_session(conn->d_session.get(), NGHTTP2_NO_ERROR);
973 auto ret = nghttp2_session_send(conn->d_session.get());
974 if (ret != 0) {
975 vinfolog("Error flushing HTTP response for stream %d from %s: %s", frame->hd.stream_id, conn->d_ci.remote.toStringWithPort(), nghttp2_strerror(ret));
976 return NGHTTP2_ERR_CALLBACK_FAILURE;
977 }
978
979 return 0;
980 }
981
982 return 0;
983 }
984
985 static std::string::size_type getLengthOfPathWithoutParameters(const std::string_view& path)
986 {
987 auto pos = path.find('?');
988 if (pos == string::npos) {
989 return path.size();
990 }
991
992 return pos;
993 }
994
995 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)
996 {
997 auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
998
999 if (frame->hd.type == NGHTTP2_HEADERS && frame->headers.cat == NGHTTP2_HCAT_REQUEST) {
1000 if (nghttp2_check_header_name(name, nameLen) == 0) {
1001 vinfolog("Invalid header name");
1002 return NGHTTP2_ERR_CALLBACK_FAILURE;
1003 }
1004
1005 #if HAVE_NGHTTP2_CHECK_HEADER_VALUE_RFC9113
1006 if (nghttp2_check_header_value_rfc9113(value, valuelen) == 0) {
1007 vinfolog("Invalid header value");
1008 return NGHTTP2_ERR_CALLBACK_FAILURE;
1009 }
1010 #endif /* HAVE_NGHTTP2_CHECK_HEADER_VALUE_RFC9113 */
1011
1012 auto headerMatches = [name, nameLen](const std::string& expected) -> bool {
1013 return nameLen == expected.size() && memcmp(name, expected.data(), expected.size()) == 0;
1014 };
1015
1016 auto stream = conn->d_currentStreams.find(frame->hd.stream_id);
1017 if (stream == conn->d_currentStreams.end()) {
1018 vinfolog("Unable to match the stream ID %d to a known one!", frame->hd.stream_id);
1019 return NGHTTP2_ERR_CALLBACK_FAILURE;
1020 }
1021 auto& query = stream->second;
1022 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): nghttp2 API
1023 auto valueView = std::string_view(reinterpret_cast<const char*>(value), valuelen);
1024 if (headerMatches(s_pathHeaderName)) {
1025 #if HAVE_NGHTTP2_CHECK_PATH
1026 if (nghttp2_check_path(value, valuelen) == 0) {
1027 vinfolog("Invalid path value");
1028 return NGHTTP2_ERR_CALLBACK_FAILURE;
1029 }
1030 #endif /* HAVE_NGHTTP2_CHECK_PATH */
1031
1032 auto pathLen = getLengthOfPathWithoutParameters(valueView);
1033 query.d_path = valueView.substr(0, pathLen);
1034 if (pathLen < valueView.size()) {
1035 query.d_queryString = valueView.substr(pathLen);
1036 }
1037 }
1038 else if (headerMatches(s_authorityHeaderName)) {
1039 query.d_host = valueView;
1040 }
1041 else if (headerMatches(s_schemeHeaderName)) {
1042 query.d_scheme = valueView;
1043 }
1044 else if (headerMatches(s_methodHeaderName)) {
1045 #if HAVE_NGHTTP2_CHECK_METHOD
1046 if (nghttp2_check_method(value, valuelen) == 0) {
1047 vinfolog("Invalid method value");
1048 return NGHTTP2_ERR_CALLBACK_FAILURE;
1049 }
1050 #endif /* HAVE_NGHTTP2_CHECK_METHOD */
1051
1052 if (valueView == "GET") {
1053 query.d_method = PendingQuery::Method::Get;
1054 }
1055 else if (valueView == "POST") {
1056 query.d_method = PendingQuery::Method::Post;
1057 }
1058 else {
1059 query.d_method = PendingQuery::Method::Unsupported;
1060 vinfolog("Unsupported method value");
1061 return 0;
1062 }
1063 }
1064
1065 if (conn->d_ci.cs->dohFrontend->d_keepIncomingHeaders || (conn->d_ci.cs->dohFrontend->d_trustForwardedForHeader && headerMatches(s_xForwardedForHeaderName))) {
1066 if (!query.d_headers) {
1067 query.d_headers = std::make_unique<HeadersMap>();
1068 }
1069 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): nghttp2 API
1070 query.d_headers->insert({std::string(reinterpret_cast<const char*>(name), nameLen), std::string(valueView)});
1071 }
1072 }
1073 return 0;
1074 }
1075
1076 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)
1077 {
1078 auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
1079 auto stream = conn->d_currentStreams.find(stream_id);
1080 if (stream == conn->d_currentStreams.end()) {
1081 vinfolog("Unable to match the stream ID %d to a known one!", stream_id);
1082 return NGHTTP2_ERR_CALLBACK_FAILURE;
1083 }
1084 if (len > std::numeric_limits<uint16_t>::max() || (std::numeric_limits<uint16_t>::max() - stream->second.d_buffer.size()) < len) {
1085 vinfolog("Data frame of size %d is too large for a DNS query (we already have %d)", len, stream->second.d_buffer.size());
1086 return NGHTTP2_ERR_CALLBACK_FAILURE;
1087 }
1088
1089 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic): nghttp2 API
1090 stream->second.d_buffer.insert(stream->second.d_buffer.end(), data, data + len);
1091
1092 return 0;
1093 }
1094
1095 int IncomingHTTP2Connection::on_error_callback(nghttp2_session* session, int lib_error_code, const char* msg, size_t len, void* user_data)
1096 {
1097 auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
1098
1099 vinfolog("Error in HTTP/2 connection from %d: %s", conn->d_ci.remote.toStringWithPort(), std::string(msg, len));
1100 conn->d_connectionClosing = true;
1101 conn->d_needFlush = true;
1102 nghttp2_session_terminate_session(conn->d_session.get(), NGHTTP2_NO_ERROR);
1103 auto ret = nghttp2_session_send(conn->d_session.get());
1104 if (ret != 0) {
1105 vinfolog("Error flushing HTTP response on connection from %s: %s", conn->d_ci.remote.toStringWithPort(), nghttp2_strerror(ret));
1106 return NGHTTP2_ERR_CALLBACK_FAILURE;
1107 }
1108
1109 return 0;
1110 }
1111
1112 IOState IncomingHTTP2Connection::readHTTPData()
1113 {
1114 IOState newState = IOState::Done;
1115 size_t got = 0;
1116 if (d_in.size() < s_initialReceiveBufferSize) {
1117 d_in.resize(std::max(s_initialReceiveBufferSize, d_in.capacity()));
1118 }
1119 try {
1120 newState = d_handler.tryRead(d_in, got, d_in.size(), true);
1121 d_in.resize(got);
1122
1123 if (got > 0) {
1124 /* we got something */
1125 auto readlen = nghttp2_session_mem_recv(d_session.get(), d_in.data(), d_in.size());
1126 /* as long as we don't require a pause by returning nghttp2_error.NGHTTP2_ERR_PAUSE from a CB,
1127 all data should be consumed before returning */
1128 if (readlen < 0 || static_cast<size_t>(readlen) < d_in.size()) {
1129 throw std::runtime_error("Fatal error while passing received data to nghttp2: " + std::string(nghttp2_strerror((int)readlen)));
1130 }
1131
1132 nghttp2_session_send(d_session.get());
1133 }
1134 }
1135 catch (const std::exception& e) {
1136 vinfolog("Exception while trying to read from HTTP client connection to %s: %s", d_ci.remote.toStringWithPort(), e.what());
1137 handleIOError();
1138 return IOState::Done;
1139 }
1140 return newState;
1141 }
1142
1143 void IncomingHTTP2Connection::handleReadableIOCallback([[maybe_unused]] int descriptor, FDMultiplexer::funcparam_t& param)
1144 {
1145 auto conn = boost::any_cast<std::shared_ptr<IncomingHTTP2Connection>>(param);
1146 conn->handleIO();
1147 }
1148
1149 void IncomingHTTP2Connection::handleWritableIOCallback([[maybe_unused]] int descriptor, FDMultiplexer::funcparam_t& param)
1150 {
1151 auto conn = boost::any_cast<std::shared_ptr<IncomingHTTP2Connection>>(param);
1152 conn->writeToSocket(true);
1153 }
1154
1155 void IncomingHTTP2Connection::stopIO()
1156 {
1157 d_ioState->reset();
1158 }
1159
1160 uint32_t IncomingHTTP2Connection::getConcurrentStreamsCount() const
1161 {
1162 return d_currentStreams.size();
1163 }
1164
1165 boost::optional<struct timeval> IncomingHTTP2Connection::getIdleClientReadTTD(struct timeval now) const
1166 {
1167 auto idleTimeout = d_ci.cs->dohFrontend->d_idleTimeout;
1168 if (g_maxTCPConnectionDuration == 0 && idleTimeout == 0) {
1169 return boost::none;
1170 }
1171
1172 if (g_maxTCPConnectionDuration > 0) {
1173 auto elapsed = now.tv_sec - d_connectionStartTime.tv_sec;
1174 if (elapsed < 0 || (static_cast<size_t>(elapsed) >= g_maxTCPConnectionDuration)) {
1175 return now;
1176 }
1177 auto remaining = g_maxTCPConnectionDuration - elapsed;
1178 if (idleTimeout == 0 || remaining <= static_cast<size_t>(idleTimeout)) {
1179 now.tv_sec += static_cast<time_t>(remaining);
1180 return now;
1181 }
1182 }
1183
1184 now.tv_sec += idleTimeout;
1185 return now;
1186 }
1187
1188 void IncomingHTTP2Connection::updateIO(IOState newState, const FDMultiplexer::callbackfunc_t& callback)
1189 {
1190 boost::optional<struct timeval> ttd{boost::none};
1191
1192 auto shared = std::dynamic_pointer_cast<IncomingHTTP2Connection>(shared_from_this());
1193 if (shared) {
1194 struct timeval now
1195 {
1196 };
1197 gettimeofday(&now, nullptr);
1198
1199 if (newState == IOState::NeedRead) {
1200 /* use the idle TTL if the handshake has been completed (and proxy protocol payload received, if any),
1201 and we have processed at least one query, otherwise we use the shorter read TTL */
1202 if ((d_state == State::waitingForQuery || d_state == State::idle) && (d_queriesCount > 0 || d_currentQueriesCount > 0)) {
1203 ttd = getIdleClientReadTTD(now);
1204 }
1205 else {
1206 ttd = getClientReadTTD(now);
1207 }
1208 d_ioState->update(newState, callback, shared, ttd);
1209 }
1210 else if (newState == IOState::NeedWrite) {
1211 ttd = getClientWriteTTD(now);
1212 d_ioState->update(newState, callback, shared, ttd);
1213 }
1214 }
1215 }
1216
1217 void IncomingHTTP2Connection::handleIOError()
1218 {
1219 d_connectionDied = true;
1220 d_out.clear();
1221 d_outPos = 0;
1222 nghttp2_session_terminate_session(d_session.get(), NGHTTP2_PROTOCOL_ERROR);
1223 d_currentStreams.clear();
1224 stopIO();
1225 }
1226
1227 bool IncomingHTTP2Connection::active() const
1228 {
1229 return !d_connectionDied && d_ioState != nullptr;
1230 }
1231
1232 #endif /* HAVE_DNS_OVER_HTTPS && HAVE_NGHTTP2 */