• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PowerDNS / pdns / 17235120617

26 Aug 2025 10:17AM UTC coverage: 65.959% (-0.02%) from 65.977%
17235120617

Pull #16016

github

web-flow
Merge d1e0ec6fc into 9eeac00a7
Pull Request #16016: auth: random doc nits

42117 of 92446 branches covered (45.56%)

Branch coverage included in aggregate %.

128034 of 165518 relevant lines covered (77.35%)

5925196.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.6
/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
#include "dnsdist-nghttp2-in.hh"
23

24
#if defined(HAVE_DNS_OVER_HTTPS) && defined(HAVE_NGHTTP2)
25

26
#include "dnsdist-dnsparser.hh"
27
#include "dnsdist-doh-common.hh"
28
#include "dnsdist-proxy-protocol.hh"
29
#include "dnsparser.hh"
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)
217✔
101
  {
217✔
102
  }
217✔
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;
217✔
109

110
  [[nodiscard]] std::string getHTTPPath() const override
111
  {
67✔
112
    return d_query.d_path;
67✔
113
  }
67✔
114

115
  [[nodiscard]] const std::string& getHTTPScheme() const override
116
  {
5✔
117
    return d_query.d_scheme;
5✔
118
  }
5✔
119

120
  [[nodiscard]] const std::string& getHTTPHost() const override
121
  {
5✔
122
    return d_query.d_host;
5✔
123
  }
5✔
124

125
  [[nodiscard]] std::string getHTTPQueryString() const override
126
  {
5✔
127
    return d_query.d_queryString;
5✔
128
  }
5✔
129

130
  [[nodiscard]] const HeadersMap& getHTTPHeaders() const override
131
  {
39✔
132
    if (!d_query.d_headers) {
39!
133
      static const HeadersMap empty{};
×
134
      return empty;
×
135
    }
×
136
    return *d_query.d_headers;
39✔
137
  }
39✔
138

139
  [[nodiscard]] std::shared_ptr<TCPQuerySender> getQuerySender() const override
140
  {
1✔
141
    return std::dynamic_pointer_cast<TCPQuerySender>(d_connection.lock());
1✔
142
  }
1✔
143

144
  void setHTTPResponse(uint16_t statusCode, PacketBuffer&& body, const std::string& contentType = "") override
145
  {
7✔
146
    d_query.d_statusCode = statusCode;
7✔
147
    d_query.d_response = std::move(body);
7✔
148
    d_query.d_contentTypeOut = contentType;
7✔
149
  }
7✔
150

151
  void handleUDPResponse(PacketBuffer&& response, InternalQueryState&& state, const std::shared_ptr<DownstreamState>& downstream_) override
152
  {
78✔
153
    std::unique_ptr<DOHUnitInterface> unit(this);
78✔
154
    auto conn = d_connection.lock();
78✔
155
    if (!conn) {
78!
156
      /* the connection has been closed in the meantime */
157
      return;
×
158
    }
×
159

160
    state.du = std::move(unit);
78✔
161
    TCPResponse resp(std::move(response), std::move(state), nullptr, nullptr);
78✔
162
    resp.d_ds = downstream_;
78✔
163
    struct timeval now{};
78✔
164
    gettimeofday(&now, nullptr);
78✔
165
    conn->handleResponse(now, std::move(resp));
78✔
166
  }
78✔
167

168
  void handleTimeout() override
169
  {
×
170
    std::unique_ptr<DOHUnitInterface> unit(this);
×
171
    auto conn = d_connection.lock();
×
172
    if (!conn) {
×
173
      /* the connection has been closed in the meantime */
174
      return;
×
175
    }
×
176
    struct timeval now{};
×
177
    gettimeofday(&now, nullptr);
×
178
    TCPResponse resp;
×
179
    resp.d_idstate.d_streamID = d_streamID;
×
180
    conn->notifyIOError(now, std::move(resp));
×
181
  }
×
182

183
  std::weak_ptr<IncomingHTTP2Connection> d_connection;
184
  IncomingHTTP2Connection::PendingQuery d_query;
185
  IncomingHTTP2Connection::StreamID d_streamID{-1};
186
};
187

188
void IncomingHTTP2Connection::handleResponse(const struct timeval& now, TCPResponse&& response)
189
{
253✔
190
  if (std::this_thread::get_id() != d_creatorThreadID) {
253✔
191
    handleCrossProtocolResponse(now, std::move(response));
126✔
192
    return;
126✔
193
  }
126✔
194

195
  auto& state = response.d_idstate;
127✔
196
  if (state.forwardedOverUDP) {
127✔
197
    dnsheader_aligned responseDH(response.d_buffer.data());
90✔
198

199
    if (responseDH.get()->tc && state.d_packet && state.d_packet->size() > state.d_proxyProtocolPayloadSize && state.d_packet->size() - state.d_proxyProtocolPayloadSize > sizeof(dnsheader)) {
90!
200
      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());
4✔
201
      auto& query = *state.d_packet;
4✔
202
      dnsdist::PacketMangling::editDNSHeaderFromRawPacket(&query.at(state.d_proxyProtocolPayloadSize), [origID = state.origID](dnsheader& header) {
4✔
203
        /* restoring the original ID */
204
        header.id = origID;
4✔
205
        return true;
4✔
206
      });
4✔
207

208
      state.forwardedOverUDP = false;
4✔
209
      bool proxyProtocolPayloadAdded = state.d_proxyProtocolPayloadSize > 0;
4✔
210
      auto cpq = getCrossProtocolQuery(std::move(query), std::move(state), response.d_ds);
4✔
211
      /* 'd_packet' buffer moved by InternalQuery constructor, need re-association */
212
      cpq->query.d_idstate.d_packet = std::make_unique<PacketBuffer>(cpq->query.d_buffer);
4✔
213
      cpq->query.d_proxyProtocolPayloadAdded = proxyProtocolPayloadAdded;
4✔
214
      if (g_tcpclientthreads && g_tcpclientthreads->passCrossProtocolQueryToThread(std::move(cpq))) {
4!
215
        return;
4✔
216
      }
4✔
217
      vinfolog("Unable to pass DoH query to a TCP worker thread after getting a TC response over UDP");
×
218
      notifyIOError(now, std::move(response));
×
219
      return;
×
220
    }
4✔
221
  }
90✔
222

223
  IncomingTCPConnectionState::handleResponse(now, std::move(response));
123✔
224
}
123✔
225

226
std::unique_ptr<DOHUnitInterface> IncomingHTTP2Connection::getDOHUnit(uint32_t streamID)
227
{
217✔
228
  if (streamID > std::numeric_limits<IncomingHTTP2Connection::StreamID>::max()) {
217!
229
    throw std::runtime_error("Invalid stream ID while retrieving DoH unit");
×
230
  }
×
231

232
  // NOLINTNEXTLINE(*-narrowing-conversions): generic interface between DNS and DoH with different types
233
  auto query = std::move(d_currentStreams.at(static_cast<IncomingHTTP2Connection::StreamID>(streamID)));
217✔
234
  return std::make_unique<IncomingDoHCrossProtocolContext>(std::move(query), std::dynamic_pointer_cast<IncomingHTTP2Connection>(shared_from_this()), streamID);
217✔
235
}
217✔
236

237
void IncomingHTTP2Connection::restoreDOHUnit(std::unique_ptr<DOHUnitInterface>&& unit)
238
{
118✔
239
  auto context = std::unique_ptr<IncomingDoHCrossProtocolContext>(dynamic_cast<IncomingDoHCrossProtocolContext*>(unit.release()));
118✔
240
  if (context) {
118!
241
    d_currentStreams.at(context->d_streamID) = std::move(context->d_query);
118✔
242
  }
118✔
243
}
118✔
244

245
IncomingHTTP2Connection::IncomingHTTP2Connection(ConnectionInfo&& connectionInfo, TCPClientThreadData& threadData, const struct timeval& now) :
246
  IncomingTCPConnectionState(std::move(connectionInfo), threadData, now)
206✔
247
{
206✔
248
  nghttp2_session_callbacks* cbs = nullptr;
206✔
249
  if (nghttp2_session_callbacks_new(&cbs) != 0) {
206!
250
    throw std::runtime_error("Unable to create a callback object for a new incoming HTTP/2 session");
×
251
  }
×
252
  std::unique_ptr<nghttp2_session_callbacks, void (*)(nghttp2_session_callbacks*)> callbacks(cbs, nghttp2_session_callbacks_del);
206✔
253
  cbs = nullptr;
206✔
254

255
  nghttp2_session_callbacks_set_send_callback(callbacks.get(), send_callback);
206✔
256
  nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks.get(), on_frame_recv_callback);
206✔
257
  nghttp2_session_callbacks_set_on_stream_close_callback(callbacks.get(), on_stream_close_callback);
206✔
258
  nghttp2_session_callbacks_set_on_begin_headers_callback(callbacks.get(), on_begin_headers_callback);
206✔
259
  nghttp2_session_callbacks_set_on_header_callback(callbacks.get(), on_header_callback);
206✔
260
  nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks.get(), on_data_chunk_recv_callback);
206✔
261
  nghttp2_session_callbacks_set_error_callback2(callbacks.get(), on_error_callback);
206✔
262

263
  nghttp2_session* sess = nullptr;
206✔
264
  if (nghttp2_session_server_new(&sess, callbacks.get(), this) != 0) {
206!
265
    throw std::runtime_error("Coult not allocate a new incoming HTTP/2 session");
×
266
  }
×
267

268
  d_session = std::unique_ptr<nghttp2_session, decltype(&nghttp2_session_del)>(sess, nghttp2_session_del);
206✔
269
  sess = nullptr;
206✔
270
}
206✔
271

272
bool IncomingHTTP2Connection::checkALPN()
273
{
203✔
274
  constexpr std::array<uint8_t, 2> h2ALPN{'h', '2'};
203✔
275
  const auto protocols = d_handler.getNextProtocol();
203✔
276
  if (protocols.size() == h2ALPN.size() && memcmp(protocols.data(), h2ALPN.data(), h2ALPN.size()) == 0) {
203!
277
    return true;
178✔
278
  }
178✔
279

280
  constexpr std::array<uint8_t, 8> http11ALPN{'h', 't', 't', 'p', '/', '1', '.', '1'};
25✔
281
  if (protocols.size() == http11ALPN.size() && memcmp(protocols.data(), http11ALPN.data(), http11ALPN.size()) == 0) {
25!
282
    ++d_ci.cs->dohFrontend->d_http1Stats.d_nbQueries;
2✔
283
  }
2✔
284

285
  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");
25✔
286
  d_out.insert(d_out.end(), data.begin(), data.end());
25✔
287
  writeToSocket(false);
25✔
288

289
  vinfolog("DoH connection from %s expected ALPN value 'h2', got '%s'", d_ci.remote.toStringWithPort(), std::string(protocols.begin(), protocols.end()));
25✔
290
  return false;
25✔
291
}
203✔
292

293
void IncomingHTTP2Connection::handleConnectionReady()
294
{
180✔
295
  constexpr std::array<nghttp2_settings_entry, 1> settings{{{NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, dnsdist::doh::MAX_INCOMING_CONCURRENT_STREAMS}}};
180✔
296
  constexpr std::array<nghttp2_settings_entry, 1> nearLimitsSettings{{{NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 1U}}};
180✔
297
  auto ret = nghttp2_submit_settings(d_session.get(), NGHTTP2_FLAG_NONE, isNearTCPLimits() ? nearLimitsSettings.data() : settings.data(), isNearTCPLimits() ? nearLimitsSettings.size() : settings.size());
180✔
298
  if (ret != 0) {
180!
299
    throw std::runtime_error("Fatal error: " + std::string(nghttp2_strerror(ret)));
×
300
  }
×
301
  d_needFlush = true;
180✔
302
  ret = nghttp2_session_send(d_session.get());
180✔
303
  if (ret != 0) {
180!
304
    throw std::runtime_error("Fatal error: " + std::string(nghttp2_strerror(ret)));
×
305
  }
×
306
}
180✔
307

308
bool IncomingHTTP2Connection::hasPendingWrite() const
309
{
617✔
310
  return d_pendingWrite;
617✔
311
}
617✔
312

313
IOState IncomingHTTP2Connection::handleHandshake(const struct timeval& now)
314
{
402✔
315
  auto iostate = d_handler.tryHandshake();
402✔
316
  if (iostate == IOState::Done) {
402✔
317
    handleHandshakeDone(now);
205✔
318
    if (d_handler.isTLS()) {
205✔
319
      if (!checkALPN()) {
203✔
320
        d_connectionDied = true;
25✔
321
        stopIO();
25✔
322
        return iostate;
25✔
323
      }
25✔
324
    }
203✔
325

326
    if (d_ci.cs != nullptr && d_ci.cs->d_enableProxyProtocol && !isProxyPayloadOutsideTLS() && expectProxyProtocolFrom(d_ci.remote)) {
180!
327
      d_state = State::readingProxyProtocolHeader;
1✔
328
      d_buffer.resize(s_proxyProtocolMinimumHeaderSize);
1✔
329
      d_proxyProtocolNeed = s_proxyProtocolMinimumHeaderSize;
1✔
330
    }
1✔
331
    else {
179✔
332
      d_state = State::waitingForQuery;
179✔
333
      handleConnectionReady();
179✔
334
    }
179✔
335
  }
180✔
336
  return iostate;
377✔
337
}
402✔
338

339
class ReadFunctionGuard
340
{
341
public:
342
  ReadFunctionGuard(bool& inReadFunction) :
343
    d_inReadFunctionRef(inReadFunction)
1,109✔
344
  {
1,109✔
345
    d_inReadFunctionRef = true;
1,109✔
346
  }
1,109✔
347
  ReadFunctionGuard(ReadFunctionGuard&&) = delete;
348
  ReadFunctionGuard(const ReadFunctionGuard&) = delete;
349
  ReadFunctionGuard& operator=(ReadFunctionGuard&&) = delete;
350
  ReadFunctionGuard& operator=(const ReadFunctionGuard&) = delete;
351
  ~ReadFunctionGuard()
352
  {
1,109✔
353
    d_inReadFunctionRef = false;
1,109✔
354
  }
1,109✔
355

356
private:
357
  bool& d_inReadFunctionRef;
358
};
359

360
void IncomingHTTP2Connection::handleIO()
361
{
677✔
362
  IOState iostate = IOState::Done;
677✔
363
  struct timeval now{};
677✔
364
  gettimeofday(&now, nullptr);
677✔
365

366
  try {
677✔
367
    if (maxConnectionDurationReached(dnsdist::configuration::getCurrentRuntimeConfiguration().d_maxTCPConnectionDuration, now)) {
677!
368
      vinfolog("Terminating DoH connection from %s because it reached the maximum TCP connection duration", d_ci.remote.toStringWithPort());
×
369
      stopIO();
×
370
      d_connectionClosing = true;
×
371
      return;
×
372
    }
×
373

374
    if (d_state == State::starting) {
677✔
375
      if (d_ci.cs != nullptr && d_ci.cs->dohFrontend != nullptr) {
206!
376
        ++d_ci.cs->dohFrontend->d_httpconnects;
206✔
377
      }
206✔
378
      if (d_ci.cs != nullptr && d_ci.cs->d_enableProxyProtocol && isProxyPayloadOutsideTLS() && expectProxyProtocolFrom(d_ci.remote)) {
206!
379
        d_state = State::readingProxyProtocolHeader;
1✔
380
        d_buffer.resize(s_proxyProtocolMinimumHeaderSize);
1✔
381
        d_proxyProtocolNeed = s_proxyProtocolMinimumHeaderSize;
1✔
382
      }
1✔
383
      else {
205✔
384
        d_state = State::doingHandshake;
205✔
385
      }
205✔
386
    }
206✔
387

388
    if (d_state == State::doingHandshake) {
677✔
389
      iostate = handleHandshake(now);
401✔
390
      if (d_connectionDied) {
401✔
391
        return;
28✔
392
      }
28✔
393
    }
401✔
394

395
    if (d_state == State::readingProxyProtocolHeader) {
649✔
396
      auto status = handleProxyProtocolPayload();
2✔
397
      if (status == ProxyProtocolResult::Done) {
2!
398
        if (isProxyPayloadOutsideTLS()) {
2✔
399
          d_state = State::doingHandshake;
1✔
400
          iostate = handleHandshake(now);
1✔
401
          if (d_connectionDied) {
1!
402
            return;
×
403
          }
×
404
        }
1✔
405
        else {
1✔
406
          d_state = State::waitingForQuery;
1✔
407
          handleConnectionReady();
1✔
408
        }
1✔
409
      }
2✔
410
      else if (status == ProxyProtocolResult::Error) {
×
411
        d_connectionDied = true;
×
412
        stopIO();
×
413
        return;
×
414
      }
×
415
    }
2✔
416

417
    if (!d_inReadFunction && active() && !d_connectionClosing && (d_state == State::waitingForQuery || d_state == State::idle)) {
649!
418
      do {
1,109✔
419
        iostate = readHTTPData();
1,109✔
420
      } while (!d_inReadFunction && active() && !d_connectionClosing && iostate == IOState::Done);
1,109!
421
    }
452✔
422

423
    if (!active()) {
649✔
424
      stopIO();
173✔
425
      return;
173✔
426
    }
173✔
427
    /*
428
      So:
429
      - if we have a pending write, we need to wait until the socket becomes writable
430
        and then call handleWritableCallback
431
      - if we have NeedWrite but no pending write, we need to wait until the socket
432
        becomes writable but for handleReadableIOCallback
433
      - if we have NeedRead, or nghttp2_session_want_read, wait until the socket
434
        becomes readable and call handleReadableIOCallback
435
    */
436
    if (hasPendingWrite()) {
476✔
437
      updateIO(IOState::NeedWrite, handleWritableIOCallback);
1✔
438
    }
1✔
439
    else if (iostate == IOState::NeedWrite) {
475!
440
      updateIO(IOState::NeedWrite, handleReadableIOCallback);
×
441
    }
×
442
    else if (!d_connectionClosing) {
475✔
443
      if (nghttp2_session_want_read(d_session.get()) != 0) {
472✔
444
        updateIO(IOState::NeedRead, handleReadableIOCallback);
471✔
445
      }
471✔
446
      else {
1✔
447
        if (getConcurrentStreamsCount() == 0) {
1!
448
          d_connectionDied = true;
1✔
449
          stopIO();
1✔
450
        }
1✔
451
        else {
×
452
          updateIO(IOState::Done, handleReadableIOCallback);
×
453
        }
×
454
      }
1✔
455
    }
472✔
456
    else {
3✔
457
      if (getConcurrentStreamsCount() == 0) {
3!
458
        d_connectionDied = true;
×
459
        stopIO();
×
460
      }
×
461
      else {
3✔
462
        updateIO(IOState::Done, handleReadableIOCallback);
3✔
463
      }
3✔
464
    }
3✔
465
  }
476✔
466
  catch (const std::exception& e) {
677✔
467
    vinfolog("Exception when processing IO for incoming DoH connection from %s: %s", d_ci.remote.toStringWithPort(), e.what());
1!
468
    d_connectionDied = true;
1✔
469
    stopIO();
1✔
470
  }
1✔
471
}
677✔
472

473
void IncomingHTTP2Connection::writeToSocket(bool socketReady)
474
{
387✔
475
  try {
387✔
476
    d_needFlush = false;
387✔
477
    IOState newState = d_handler.tryWrite(d_out, d_outPos, d_out.size());
387✔
478

479
    if (newState == IOState::Done) {
387✔
480
      d_pendingWrite = false;
379✔
481
      d_out.clear();
379✔
482
      d_outPos = 0;
379✔
483
      if (active() && !d_connectionClosing) {
379!
484
        updateIO(IOState::NeedRead, handleReadableIOCallback);
377✔
485
      }
377✔
486
      else {
2✔
487
        stopIO();
2✔
488
      }
2✔
489
    }
379✔
490
    else {
8✔
491
      updateIO(newState, handleWritableIOCallback);
8✔
492
      d_pendingWrite = true;
8✔
493
    }
8✔
494
  }
387✔
495
  catch (const std::exception& e) {
387✔
496
    vinfolog("Exception while trying to write (%s) to HTTP client connection to %s: %s", (socketReady ? "ready" : "send"), d_ci.remote.toStringWithPort(), e.what());
7!
497
    handleIOError();
7✔
498
  }
7✔
499
}
387✔
500

501
ssize_t IncomingHTTP2Connection::send_callback(nghttp2_session* session, const uint8_t* data, size_t length, int flags, void* user_data)
502
{
715✔
503
  (void)session;
715✔
504
  (void)flags;
715✔
505
  auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
715✔
506
  if (conn->d_connectionDied) {
715✔
507
    return static_cast<ssize_t>(length);
4✔
508
  }
4✔
509
  // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic): nghttp2 API
510
  conn->d_out.insert(conn->d_out.end(), data, data + length);
711✔
511

512
  if (conn->d_connectionClosing || conn->d_needFlush) {
711✔
513
    conn->writeToSocket(false);
361✔
514
  }
361✔
515

516
  return static_cast<ssize_t>(length);
711✔
517
}
715✔
518

519
static const std::array<const std::string, static_cast<size_t>(NGHTTP2Headers::HeaderConstantIndexes::COUNT)> s_headerConstants{
520
  "200",
521
  ":method",
522
  "POST",
523
  ":scheme",
524
  "https",
525
  ":authority",
526
  "x-forwarded-for",
527
  ":path",
528
  "content-length",
529
  ":status",
530
  "location",
531
  "accept",
532
  "application/dns-message",
533
  "cache-control",
534
  "content-type",
535
  "application/dns-message",
536
  "user-agent",
537
  "nghttp2-" NGHTTP2_VERSION "/dnsdist",
538
  "x-forwarded-port",
539
  "x-forwarded-proto",
540
  "dns-over-udp",
541
  "dns-over-tcp",
542
  "dns-over-tls",
543
  "dns-over-http",
544
  "dns-over-https"};
545

546
static const std::string s_authorityHeaderName(":authority");
547
static const std::string s_pathHeaderName(":path");
548
static const std::string s_methodHeaderName(":method");
549
static const std::string s_schemeHeaderName(":scheme");
550
static const std::string s_xForwardedForHeaderName("x-forwarded-for");
551

552
void NGHTTP2Headers::addStaticHeader(std::vector<nghttp2_nv>& headers, NGHTTP2Headers::HeaderConstantIndexes nameKey, NGHTTP2Headers::HeaderConstantIndexes valueKey)
553
{
1,082✔
554
  const auto& name = s_headerConstants.at(static_cast<size_t>(nameKey));
1,082✔
555
  const auto& value = s_headerConstants.at(static_cast<size_t>(valueKey));
1,082✔
556

557
  // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast,cppcoreguidelines-pro-type-reinterpret-cast): nghttp2 API
558
  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});
1,082✔
559
}
1,082✔
560

561
void NGHTTP2Headers::addCustomDynamicHeader(std::vector<nghttp2_nv>& headers, const std::string& name, const std::string_view& value)
562
{
1,027✔
563
  // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast,cppcoreguidelines-pro-type-reinterpret-cast): nghttp2 API
564
  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});
1,027✔
565
}
1,027✔
566

567
void NGHTTP2Headers::addDynamicHeader(std::vector<nghttp2_nv>& headers, NGHTTP2Headers::HeaderConstantIndexes nameKey, const std::string_view& value)
568
{
840✔
569
  const auto& name = s_headerConstants.at(static_cast<size_t>(nameKey));
840✔
570
  NGHTTP2Headers::addCustomDynamicHeader(headers, name, value);
840✔
571
}
840✔
572

573
std::unordered_map<IncomingHTTP2Connection::StreamID, IncomingHTTP2Connection::PendingQuery>::iterator IncomingHTTP2Connection::getStreamContext(StreamID streamID)
574
{
1,631✔
575
  auto streamIt = d_currentStreams.find(streamID);
1,631✔
576
  if (streamIt == d_currentStreams.end()) {
1,631!
577
    /* it might have been closed by the remote end in the meantime */
578
    d_killedStreams.erase(streamID);
×
579
  }
×
580
  return streamIt;
1,631✔
581
}
1,631✔
582

583
IOState IncomingHTTP2Connection::sendResponse(const struct timeval& now, TCPResponse&& response)
584
{
142✔
585
  (void)now;
142✔
586
  if (response.d_idstate.d_streamID == -1) {
142!
587
    throw std::runtime_error("Invalid DoH stream ID while sending response");
×
588
  }
×
589
  auto streamIt = getStreamContext(response.d_idstate.d_streamID);
142✔
590
  if (streamIt == d_currentStreams.end()) {
142!
591
    /* it might have been closed by the remote end in the meantime */
592
    return hasPendingWrite() ? IOState::NeedWrite : IOState::Done;
×
593
  }
×
594
  auto& context = streamIt->second;
142✔
595

596
  uint32_t statusCode = 200U;
142✔
597
  std::string contentType;
142✔
598
  bool sendContentType = true;
142✔
599
  auto& responseBuffer = context.d_buffer;
142✔
600
  if (context.d_statusCode != 0) {
142✔
601
    responseBuffer = std::move(context.d_response);
7✔
602
    statusCode = context.d_statusCode;
7✔
603
    contentType = std::move(context.d_contentTypeOut);
7✔
604
  }
7✔
605
  else {
135✔
606
    responseBuffer = std::move(response.d_buffer);
135✔
607
  }
135✔
608

609
  auto sent = responseBuffer.size();
142✔
610
  sendResponse(response.d_idstate.d_streamID, context, statusCode, d_ci.cs->dohFrontend->d_customResponseHeaders, contentType, sendContentType);
142✔
611
  handleResponseSent(response, sent);
142✔
612

613
  return hasPendingWrite() ? IOState::NeedWrite : IOState::Done;
142✔
614
}
142✔
615

616
void IncomingHTTP2Connection::notifyIOError(const struct timeval& now, TCPResponse&& response)
617
{
13✔
618
  if (std::this_thread::get_id() != d_creatorThreadID) {
13✔
619
    /* empty buffer will signal an IO error */
620
    response.d_buffer.clear();
6✔
621
    handleCrossProtocolResponse(now, std::move(response));
6✔
622
    return;
6✔
623
  }
6✔
624

625
  if (response.d_idstate.d_streamID == -1) {
7!
626
    throw std::runtime_error("Invalid DoH stream ID while handling I/O error notification");
×
627
  }
×
628

629
  auto streamIt = getStreamContext(response.d_idstate.d_streamID);
7✔
630
  if (streamIt == d_currentStreams.end()) {
7!
631
    /* it might have been closed by the remote end in the meantime */
632
    return;
×
633
  }
×
634
  auto& context = streamIt->second;
7✔
635
  context.d_buffer = std::move(response.d_buffer);
7✔
636
  sendResponse(response.d_idstate.d_streamID, context, 502, d_ci.cs->dohFrontend->d_customResponseHeaders);
7✔
637
}
7✔
638

639
bool IncomingHTTP2Connection::sendResponse(IncomingHTTP2Connection::StreamID streamID, IncomingHTTP2Connection::PendingQuery& context, uint16_t responseCode, const HeadersMap& customResponseHeaders, const std::string& contentType, bool addContentType)
640
{
179✔
641
  /* 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.
642
   */
643
  nghttp2_data_provider data_provider;
179✔
644

645
  data_provider.source.ptr = this;
179✔
646
  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 {
179✔
647
    (void)source;
179✔
648
    auto* connection = static_cast<IncomingHTTP2Connection*>(cb_data);
179✔
649
    auto& obj = connection->d_currentStreams.at(stream_id);
179✔
650
    size_t toCopy = 0;
179✔
651
    if (obj.d_queryPos < obj.d_buffer.size()) {
179!
652
      size_t remaining = obj.d_buffer.size() - obj.d_queryPos;
179✔
653
      toCopy = length > remaining ? remaining : length;
179!
654
      memcpy(buf, &obj.d_buffer.at(obj.d_queryPos), toCopy);
179✔
655
      obj.d_queryPos += toCopy;
179✔
656
    }
179✔
657

658
    if (obj.d_queryPos >= obj.d_buffer.size()) {
179!
659
      *data_flags |= NGHTTP2_DATA_FLAG_EOF;
179✔
660
      obj.d_buffer.clear();
179✔
661
      connection->d_needFlush = true;
179✔
662
    }
179✔
663
    return static_cast<ssize_t>(toCopy);
179✔
664
  };
179✔
665

666
  const auto& dohFrontend = d_ci.cs->dohFrontend;
179✔
667
  auto& responseBody = context.d_buffer;
179✔
668

669
  std::vector<nghttp2_nv> headers;
179✔
670
  std::string responseCodeStr;
179✔
671
  std::string cacheControlValue;
179✔
672
  std::string location;
179✔
673
  /* remember that dynamic header values should be kept alive
674
     until we have called nghttp2_submit_response(), at least */
675
  /* status, content-type, cache-control, content-length */
676
  headers.reserve(4);
179✔
677

678
  if (responseCode == 200) {
179✔
679
    NGHTTP2Headers::addStaticHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::STATUS_NAME, NGHTTP2Headers::HeaderConstantIndexes::OK_200_VALUE);
140✔
680
    ++dohFrontend->d_validresponses;
140✔
681
    ++dohFrontend->d_http2Stats.d_nb200Responses;
140✔
682

683
    if (addContentType) {
140!
684
      if (contentType.empty()) {
140✔
685
        NGHTTP2Headers::addStaticHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_NAME, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_VALUE);
135✔
686
      }
135✔
687
      else {
5✔
688
        NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_NAME, contentType);
5✔
689
      }
5✔
690
    }
140✔
691

692
    if (dohFrontend->d_sendCacheControlHeaders && responseBody.size() > sizeof(dnsheader)) {
140✔
693
      // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): API
694
      uint32_t minTTL = getDNSPacketMinTTL(reinterpret_cast<const char*>(responseBody.data()), responseBody.size());
134✔
695
      if (minTTL != std::numeric_limits<uint32_t>::max()) {
134✔
696
        cacheControlValue = "max-age=" + std::to_string(minTTL);
96✔
697
        NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CACHE_CONTROL_NAME, cacheControlValue);
96✔
698
      }
96✔
699
    }
134✔
700
  }
140✔
701
  else {
39✔
702
    responseCodeStr = std::to_string(responseCode);
39✔
703
    NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::STATUS_NAME, responseCodeStr);
39✔
704

705
    if (responseCode >= 300 && responseCode < 400) {
39!
706
      // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
707
      location = std::string(reinterpret_cast<const char*>(responseBody.data()), responseBody.size());
2✔
708
      NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_NAME, "text/html; charset=utf-8");
2✔
709
      NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::LOCATION_NAME, location);
2✔
710
      static const std::string s_redirectStart{"<!DOCTYPE html><TITLE>Moved</TITLE><P>The document has moved <A HREF=\""};
2✔
711
      static const std::string s_redirectEnd{"\">here</A>"};
2✔
712
      responseBody.reserve(s_redirectStart.size() + responseBody.size() + s_redirectEnd.size());
2✔
713
      responseBody.insert(responseBody.begin(), s_redirectStart.begin(), s_redirectStart.end());
2✔
714
      responseBody.insert(responseBody.end(), s_redirectEnd.begin(), s_redirectEnd.end());
2✔
715
      ++dohFrontend->d_redirectresponses;
2✔
716
    }
2✔
717
    else {
37✔
718
      ++dohFrontend->d_errorresponses;
37✔
719
      switch (responseCode) {
37✔
720
      case 400:
15✔
721
        ++dohFrontend->d_http2Stats.d_nb400Responses;
15✔
722
        break;
15✔
723
      case 403:
6✔
724
        ++dohFrontend->d_http2Stats.d_nb403Responses;
6✔
725
        break;
6✔
726
      case 500:
×
727
        ++dohFrontend->d_http2Stats.d_nb500Responses;
×
728
        break;
×
729
      case 502:
7✔
730
        ++dohFrontend->d_http2Stats.d_nb502Responses;
7✔
731
        break;
7✔
732
      default:
9✔
733
        ++dohFrontend->d_http2Stats.d_nbOtherResponses;
9✔
734
        break;
9✔
735
      }
37✔
736

737
      if (!responseBody.empty()) {
37✔
738
        NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_NAME, "text/plain; charset=utf-8");
30✔
739
      }
30✔
740
      else {
7✔
741
        static const std::string invalid{"invalid DNS query"};
7✔
742
        static const std::string notAllowed{"dns query not allowed"};
7✔
743
        static const std::string noDownstream{"no downstream server available"};
7✔
744
        static const std::string internalServerError{"Internal Server Error"};
7✔
745

746
        switch (responseCode) {
7✔
747
        case 400:
×
748
          responseBody.insert(responseBody.begin(), invalid.begin(), invalid.end());
×
749
          break;
×
750
        case 403:
×
751
          responseBody.insert(responseBody.begin(), notAllowed.begin(), notAllowed.end());
×
752
          break;
×
753
        case 502:
7!
754
          responseBody.insert(responseBody.begin(), noDownstream.begin(), noDownstream.end());
7✔
755
          break;
7✔
756
        case 500:
×
757
          /* fall-through */
758
        default:
×
759
          responseBody.insert(responseBody.begin(), internalServerError.begin(), internalServerError.end());
×
760
          break;
×
761
        }
7✔
762
      }
7✔
763
    }
37✔
764
  }
39✔
765

766
  const std::string contentLength = std::to_string(responseBody.size());
179✔
767
  NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_LENGTH_NAME, contentLength);
179✔
768

769
  for (const auto& [key, value] : customResponseHeaders) {
192✔
770
    NGHTTP2Headers::addCustomDynamicHeader(headers, key, value);
187✔
771
  }
187✔
772

773
  context.d_sendingResponse = true;
179✔
774
  auto ret = nghttp2_submit_response(d_session.get(), streamID, headers.data(), headers.size(), &data_provider);
179✔
775
  if (ret != 0) {
179!
776
    vinfolog("Error submitting HTTP response for stream %d: %s", streamID, nghttp2_strerror(ret));
×
777
    d_currentStreams.erase(streamID);
×
778
    return false;
×
779
  }
×
780

781
  ret = nghttp2_session_send(d_session.get());
179✔
782
  if (ret != 0) {
179!
783
    vinfolog("Error flushing HTTP response for stream %d: %s", streamID, nghttp2_strerror(ret));
×
784
    d_currentStreams.erase(streamID);
×
785
    return false;
×
786
  }
×
787

788
  return true;
179✔
789
}
179✔
790

791
static void processForwardedForHeader(const std::unique_ptr<HeadersMap>& headers, ComboAddress& remote)
792
{
5✔
793
  if (!headers) {
5✔
794
    return;
1✔
795
  }
1✔
796

797
  auto headerIt = headers->find(s_xForwardedForHeaderName);
4✔
798
  if (headerIt == headers->end()) {
4!
799
    return;
×
800
  }
×
801

802
  std::string_view value = headerIt->second;
4✔
803
  try {
4✔
804
    auto pos = value.rfind(',');
4✔
805
    if (pos != std::string_view::npos) {
4✔
806
      ++pos;
2✔
807
      for (; pos < value.size() && value[pos] == ' '; ++pos) {
4!
808
      }
2✔
809

810
      if (pos < value.size()) {
2!
811
        value = value.substr(pos);
2✔
812
      }
2✔
813
    }
2✔
814
    auto newRemote = ComboAddress(std::string(value));
4✔
815
    remote = newRemote;
4✔
816
  }
4✔
817
  catch (const std::exception& e) {
4✔
818
    vinfolog("Invalid X-Forwarded-For header ('%s') received from %s : %s", std::string(value), remote.toStringWithPort(), e.what());
×
819
  }
×
820
  catch (const PDNSException& e) {
4✔
821
    vinfolog("Invalid X-Forwarded-For header ('%s') received from %s : %s", std::string(value), remote.toStringWithPort(), e.reason);
×
822
  }
×
823
}
4✔
824

825
void IncomingHTTP2Connection::handleIncomingQuery(IncomingHTTP2Connection::PendingQuery&& query, IncomingHTTP2Connection::StreamID streamID)
826
{
179✔
827
  const auto handleImmediateResponse = [this, &query, streamID](uint16_t code, const std::string& reason, PacketBuffer&& response = PacketBuffer()) {
179✔
828
    if (response.empty()) {
26!
829
      query.d_buffer.clear();
26✔
830
      query.d_buffer.insert(query.d_buffer.begin(), reason.begin(), reason.end());
26✔
831
    }
26✔
832
    else {
×
833
      query.d_buffer = std::move(response);
×
834
    }
×
835
    vinfolog("Sending an immediate %d response to incoming DoH query: %s", code, reason);
26✔
836
    sendResponse(streamID, query, code, d_ci.cs->dohFrontend->d_customResponseHeaders);
26✔
837
  };
26✔
838

839
  if (query.d_method == PendingQuery::Method::Unknown || query.d_method == PendingQuery::Method::Unsupported) {
179!
840
    handleImmediateResponse(400, "DoH query not allowed because of unsupported HTTP method");
2✔
841
    return;
2✔
842
  }
2✔
843

844
  ++d_ci.cs->dohFrontend->d_http2Stats.d_nbQueries;
177✔
845

846
  if (d_ci.cs->dohFrontend->d_trustForwardedForHeader) {
177✔
847
    processForwardedForHeader(query.d_headers, d_proxiedRemote);
5✔
848

849
    /* second ACL lookup based on the updated address */
850
    if (!dnsdist::configuration::getCurrentRuntimeConfiguration().d_ACL.match(d_proxiedRemote)) {
5✔
851
      ++dnsdist::metrics::g_stats.aclDrops;
1✔
852
      vinfolog("Query from %s (%s) (DoH) dropped because of ACL", d_ci.remote.toStringWithPort(), d_proxiedRemote.toStringWithPort());
1!
853
      handleImmediateResponse(403, "DoH query not allowed because of ACL");
1✔
854
      return;
1✔
855
    }
1✔
856

857
    if (!d_ci.cs->dohFrontend->d_keepIncomingHeaders) {
4!
858
      query.d_headers.reset();
4✔
859
    }
4✔
860
  }
4✔
861

862
  if (d_ci.cs->dohFrontend->d_exactPathMatching) {
176✔
863
    if (d_ci.cs->dohFrontend->d_urls.count(query.d_path) == 0) {
173✔
864
      handleImmediateResponse(404, "there is no endpoint configured for this path");
4✔
865
      return;
4✔
866
    }
4✔
867
  }
173✔
868
  else {
3✔
869
    bool found = false;
3✔
870
    for (const auto& path : d_ci.cs->dohFrontend->d_urls) {
3✔
871
      if (boost::starts_with(query.d_path, path)) {
3✔
872
        found = true;
2✔
873
        break;
2✔
874
      }
2✔
875
    }
3✔
876
    if (!found) {
3✔
877
      handleImmediateResponse(404, "there is no endpoint configured for this path");
1✔
878
      return;
1✔
879
    }
1✔
880
  }
3✔
881

882
  /* the responses map can be updated at runtime, so we need to take a copy of
883
     the shared pointer, increasing the reference counter */
884
  auto responsesMap = d_ci.cs->dohFrontend->d_responsesMap;
171✔
885
  if (responsesMap) {
171✔
886
    for (const auto& entry : *responsesMap) {
58✔
887
      if (entry->matches(query.d_path)) {
58✔
888
        const auto& customHeaders = entry->getHeaders();
4✔
889
        query.d_buffer = entry->getContent();
4✔
890
        if (entry->getStatusCode() >= 400 && !query.d_buffer.empty()) {
4!
891
          // legacy trailing 0 from the h2o era
892
          query.d_buffer.pop_back();
4✔
893
        }
4✔
894

895
        sendResponse(streamID, query, entry->getStatusCode(), customHeaders ? *customHeaders : d_ci.cs->dohFrontend->d_customResponseHeaders, std::string(), false);
4!
896
        return;
4✔
897
      }
4✔
898
    }
58✔
899
  }
58✔
900

901
  if (query.d_buffer.empty() && query.d_method == PendingQuery::Method::Get && !query.d_queryString.empty()) {
167✔
902
    auto payload = dnsdist::doh::getPayloadFromPath(query.d_queryString);
150✔
903
    if (payload) {
150✔
904
      query.d_buffer = std::move(*payload);
146✔
905
    }
146✔
906
    else {
4✔
907
      ++d_ci.cs->dohFrontend->d_badrequests;
4✔
908
      handleImmediateResponse(400, "DoH unable to decode BASE64-URL");
4✔
909
      return;
4✔
910
    }
4✔
911
  }
150✔
912

913
  if (query.d_method == PendingQuery::Method::Get) {
163✔
914
    ++d_ci.cs->dohFrontend->d_getqueries;
148✔
915
  }
148✔
916
  else if (query.d_method == PendingQuery::Method::Post) {
15!
917
    ++d_ci.cs->dohFrontend->d_postqueries;
15✔
918
  }
15✔
919

920
  try {
163✔
921
    struct timeval now{};
163✔
922
    gettimeofday(&now, nullptr);
163✔
923
    auto processingResult = handleQuery(std::move(query.d_buffer), now, streamID);
163✔
924

925
    switch (processingResult) {
163✔
926
    case QueryProcessingResult::TooSmall:
4✔
927
      handleImmediateResponse(400, "DoH non-compliant query");
4✔
928
      break;
4✔
929
    case QueryProcessingResult::InvalidHeaders:
2✔
930
      handleImmediateResponse(400, "DoH invalid headers");
2✔
931
      break;
2✔
932
    case QueryProcessingResult::Dropped:
5✔
933
      handleImmediateResponse(403, "DoH dropped query");
5✔
934
      break;
5✔
935
    case QueryProcessingResult::NoBackend:
×
936
      handleImmediateResponse(502, "DoH no backend available");
×
937
      return;
×
938
    case QueryProcessingResult::Forwarded:
66✔
939
    case QueryProcessingResult::Asynchronous:
102✔
940
    case QueryProcessingResult::SelfAnswered:
149✔
941
      break;
149✔
942
    }
163✔
943
  }
163✔
944
  catch (const std::exception& e) {
163✔
945
    vinfolog("Exception while processing DoH query: %s", e.what());
3✔
946
    handleImmediateResponse(400, "DoH non-compliant query");
3✔
947
    return;
3✔
948
  }
3✔
949
}
163✔
950

951
int IncomingHTTP2Connection::on_frame_recv_callback(nghttp2_session* session, const nghttp2_frame* frame, void* user_data)
952
{
866✔
953
  (void)session;
866✔
954
  auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
866✔
955
  /* is this the last frame for this stream? */
956
  if ((frame->hd.type == NGHTTP2_HEADERS || frame->hd.type == NGHTTP2_DATA) && (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) != 0) {
866✔
957
    auto streamID = frame->hd.stream_id;
179✔
958
    auto stream = conn->getStreamContext(streamID);
179✔
959
    if (stream != conn->d_currentStreams.end()) {
179!
960
      conn->handleIncomingQuery(std::move(stream->second), streamID);
179✔
961
    }
179✔
962
    else {
×
963
      vinfolog("Stream %d NOT FOUND", streamID);
×
964
      return NGHTTP2_ERR_CALLBACK_FAILURE;
×
965
    }
×
966
  }
179✔
967
  else if (frame->hd.type == NGHTTP2_PING) {
687!
968
    conn->d_needFlush = true;
×
969
  }
×
970

971
  return 0;
866✔
972
}
866✔
973

974
int IncomingHTTP2Connection::on_stream_close_callback(nghttp2_session* session, IncomingHTTP2Connection::StreamID stream_id, uint32_t error_code, void* user_data)
975
{
179✔
976
  (void)session;
179✔
977
  (void)error_code;
179✔
978
  auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
179✔
979

980
  auto streamIt = conn->d_currentStreams.find(stream_id);
179✔
981
  if (streamIt == conn->d_currentStreams.end()) {
179✔
982
    return 0;
1✔
983
  }
1✔
984

985
  if (!streamIt->second.d_sendingResponse) {
178!
986
    conn->d_killedStreams.emplace(stream_id);
×
987
  }
×
988

989
  conn->d_currentStreams.erase(streamIt);
178✔
990
  return 0;
178✔
991
}
179✔
992

993
int IncomingHTTP2Connection::on_begin_headers_callback(nghttp2_session* session, const nghttp2_frame* frame, void* user_data)
994
{
181✔
995
  (void)session;
181✔
996
  if (frame->hd.type != NGHTTP2_HEADERS || frame->headers.cat != NGHTTP2_HCAT_REQUEST) {
181!
997
    return 0;
×
998
  }
×
999

1000
  auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
181✔
1001
  auto close_connection = [](IncomingHTTP2Connection* connection, int32_t streamID, const ComboAddress& remote) -> int {
181✔
1002
    connection->d_connectionClosing = true;
×
1003
    connection->d_needFlush = true;
×
1004
    nghttp2_session_terminate_session(connection->d_session.get(), NGHTTP2_REFUSED_STREAM);
×
1005
    auto ret = nghttp2_session_send(connection->d_session.get());
×
1006
    if (ret != 0) {
×
1007
      vinfolog("Error flushing HTTP response for stream %d from %s: %s", streamID, remote.toStringWithPort(), nghttp2_strerror(ret));
×
1008
      return NGHTTP2_ERR_CALLBACK_FAILURE;
×
1009
    }
×
1010

1011
    return 0;
×
1012
  };
×
1013

1014
  if (conn->getConcurrentStreamsCount() >= dnsdist::doh::MAX_INCOMING_CONCURRENT_STREAMS) {
181!
1015
    vinfolog("Too many concurrent streams on connection from %s", conn->d_ci.remote.toStringWithPort());
×
1016
    return close_connection(conn, frame->hd.stream_id, conn->d_ci.remote);
×
1017
  }
×
1018

1019
  auto insertPair = conn->d_currentStreams.emplace(frame->hd.stream_id, PendingQuery());
181✔
1020
  if (!insertPair.second) {
181!
1021
    /* there is a stream ID collision, something is very wrong! */
1022
    vinfolog("Stream ID collision (%d) on connection from %s", frame->hd.stream_id, conn->d_ci.remote.toStringWithPort());
×
1023
    return close_connection(conn, frame->hd.stream_id, conn->d_ci.remote);
×
1024
  }
×
1025

1026
  return 0;
181✔
1027
}
181✔
1028

1029
static std::string::size_type getLengthOfPathWithoutParameters(const std::string_view& path)
1030
{
181✔
1031
  auto pos = path.find('?');
181✔
1032
  if (pos == string::npos) {
181✔
1033
    return path.size();
23✔
1034
  }
23✔
1035

1036
  return pos;
158✔
1037
}
181✔
1038

1039
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)
1040
{
1,282✔
1041
  (void)session;
1,282✔
1042
  (void)flags;
1,282✔
1043
  auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
1,282✔
1044

1045
  if (frame->hd.type == NGHTTP2_HEADERS && frame->headers.cat == NGHTTP2_HCAT_REQUEST) {
1,282!
1046
    if (nghttp2_check_header_name(name, nameLen) == 0) {
1,282!
1047
      vinfolog("Invalid header name");
×
1048
      return NGHTTP2_ERR_CALLBACK_FAILURE;
×
1049
    }
×
1050

1051
#if defined(HAVE_NGHTTP2_CHECK_HEADER_VALUE_RFC9113)
1,282✔
1052
    if (nghttp2_check_header_value_rfc9113(value, valuelen) == 0) {
1,282!
1053
      vinfolog("Invalid header value");
×
1054
      return NGHTTP2_ERR_CALLBACK_FAILURE;
×
1055
    }
×
1056
#endif /* HAVE_NGHTTP2_CHECK_HEADER_VALUE_RFC9113 */
1,282✔
1057

1058
    auto headerMatches = [name, nameLen](const std::string& expected) -> bool {
4,077✔
1059
      return nameLen == expected.size() && memcmp(name, expected.data(), expected.size()) == 0;
4,077✔
1060
    };
4,077✔
1061

1062
    auto stream = conn->getStreamContext(frame->hd.stream_id);
1,282✔
1063
    if (stream == conn->d_currentStreams.end()) {
1,282!
1064
      vinfolog("Unable to match the stream ID %d to a known one!", frame->hd.stream_id);
×
1065
      return NGHTTP2_ERR_CALLBACK_FAILURE;
×
1066
    }
×
1067
    auto& query = stream->second;
1,282✔
1068
    // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): nghttp2 API
1069
    auto valueView = std::string_view(reinterpret_cast<const char*>(value), valuelen);
1,282✔
1070
    if (headerMatches(s_pathHeaderName)) {
1,282✔
1071
#if defined(HAVE_NGHTTP2_CHECK_PATH)
181✔
1072
      if (nghttp2_check_path(value, valuelen) == 0) {
181!
1073
        vinfolog("Invalid path value");
×
1074
        return NGHTTP2_ERR_CALLBACK_FAILURE;
×
1075
      }
×
1076
#endif /* HAVE_NGHTTP2_CHECK_PATH */
181✔
1077

1078
      auto pathLen = getLengthOfPathWithoutParameters(valueView);
181✔
1079
      query.d_path = valueView.substr(0, pathLen);
181✔
1080
      if (pathLen < valueView.size()) {
181✔
1081
        query.d_queryString = valueView.substr(pathLen);
158✔
1082
      }
158✔
1083
    }
181✔
1084
    else if (headerMatches(s_authorityHeaderName)) {
1,101✔
1085
      query.d_host = valueView;
181✔
1086
    }
181✔
1087
    else if (headerMatches(s_schemeHeaderName)) {
920✔
1088
      query.d_scheme = valueView;
181✔
1089
    }
181✔
1090
    else if (headerMatches(s_methodHeaderName)) {
739✔
1091
#if defined(HAVE_NGHTTP2_CHECK_METHOD)
181✔
1092
      if (nghttp2_check_method(value, valuelen) == 0) {
181!
1093
        vinfolog("Invalid method value");
×
1094
        return NGHTTP2_ERR_CALLBACK_FAILURE;
×
1095
      }
×
1096
#endif /* HAVE_NGHTTP2_CHECK_METHOD */
181✔
1097

1098
      if (valueView == "GET") {
181✔
1099
        query.d_method = PendingQuery::Method::Get;
162✔
1100
      }
162✔
1101
      else if (valueView == "POST") {
19✔
1102
        query.d_method = PendingQuery::Method::Post;
17✔
1103
      }
17✔
1104
      else {
2✔
1105
        query.d_method = PendingQuery::Method::Unsupported;
2✔
1106
        vinfolog("Unsupported method value");
2!
1107
        return 0;
2✔
1108
      }
2✔
1109
    }
181✔
1110

1111
    if (conn->d_ci.cs->dohFrontend->d_keepIncomingHeaders || (conn->d_ci.cs->dohFrontend->d_trustForwardedForHeader && headerMatches(s_xForwardedForHeaderName))) {
1,280✔
1112
      if (!query.d_headers) {
487✔
1113
        query.d_headers = std::make_unique<HeadersMap>();
72✔
1114
      }
72✔
1115
      // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): nghttp2 API
1116
      query.d_headers->insert({std::string(reinterpret_cast<const char*>(name), nameLen), std::string(valueView)});
487✔
1117
    }
487✔
1118
  }
1,280✔
1119
  return 0;
1,280✔
1120
}
1,282✔
1121

1122
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)
1123
{
21✔
1124
  (void)session;
21✔
1125
  (void)flags;
21✔
1126
  auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
21✔
1127
  auto stream = conn->getStreamContext(stream_id);
21✔
1128
  if (stream == conn->d_currentStreams.end()) {
21!
1129
    vinfolog("Unable to match the stream ID %d to a known one!", stream_id);
×
1130
    return NGHTTP2_ERR_CALLBACK_FAILURE;
×
1131
  }
×
1132
  if (len > std::numeric_limits<uint16_t>::max() || (std::numeric_limits<uint16_t>::max() - stream->second.d_buffer.size()) < len) {
21!
1133
    vinfolog("Data frame of size %d is too large for a DNS query (we already have %d)", len, stream->second.d_buffer.size());
×
1134
    return NGHTTP2_ERR_CALLBACK_FAILURE;
×
1135
  }
×
1136

1137
  // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic): nghttp2 API
1138
  stream->second.d_buffer.insert(stream->second.d_buffer.end(), data, data + len);
21✔
1139

1140
  return 0;
21✔
1141
}
21✔
1142

1143
int IncomingHTTP2Connection::on_error_callback(nghttp2_session* session, int lib_error_code, const char* msg, size_t len, void* user_data)
1144
{
2✔
1145
  (void)session;
2✔
1146
  auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
2✔
1147

1148
  vinfolog("Error in HTTP/2 connection from %s: %s (%d)", conn->d_ci.remote.toStringWithPort(), std::string(msg, len), lib_error_code);
2!
1149
  conn->d_connectionClosing = true;
2✔
1150
  conn->d_needFlush = true;
2✔
1151
  nghttp2_session_terminate_session(conn->d_session.get(), NGHTTP2_NO_ERROR);
2✔
1152
  auto ret = nghttp2_session_send(conn->d_session.get());
2✔
1153
  if (ret != 0) {
2!
1154
    vinfolog("Error flushing HTTP response on connection from %s: %s", conn->d_ci.remote.toStringWithPort(), nghttp2_strerror(ret));
×
1155
    return NGHTTP2_ERR_CALLBACK_FAILURE;
×
1156
  }
×
1157

1158
  return 0;
2✔
1159
}
2✔
1160

1161
IOState IncomingHTTP2Connection::readHTTPData()
1162
{
1,109✔
1163
  if (d_inReadFunction) {
1,109!
1164
    return IOState::Done;
×
1165
  }
×
1166
  ReadFunctionGuard readGuard(d_inReadFunction);
1,109✔
1167

1168
  IOState newState = IOState::Done;
1,109✔
1169
  size_t got = 0;
1,109✔
1170
  if (d_in.size() < s_initialReceiveBufferSize) {
1,109✔
1171
    d_in.resize(std::max(s_initialReceiveBufferSize, d_in.capacity()));
957✔
1172
  }
957✔
1173
  try {
1,109✔
1174
    newState = d_handler.tryRead(d_in, got, d_in.size(), true);
1,109✔
1175
    d_in.resize(got);
1,109✔
1176

1177
    if (got > 0) {
1,109✔
1178
      /* we got something */
1179
      auto readlen = nghttp2_session_mem_recv(d_session.get(), d_in.data(), d_in.size());
666✔
1180
      /* as long as we don't require a pause by returning nghttp2_error.NGHTTP2_ERR_PAUSE from a CB,
1181
         all data should be consumed before returning */
1182
      if (readlen < 0 || static_cast<size_t>(readlen) < d_in.size()) {
666!
1183
        throw std::runtime_error("Fatal error while passing received data to nghttp2: " + std::string(nghttp2_strerror((int)readlen)));
5✔
1184
      }
5✔
1185

1186
      nghttp2_session_send(d_session.get());
661✔
1187
    }
661✔
1188
  }
1,109✔
1189
  catch (const std::exception& e) {
1,109✔
1190
    vinfolog("Exception while trying to read from HTTP client connection to %s: %s", d_ci.remote.toStringWithPort(), e.what());
172✔
1191
    handleIOError();
172✔
1192
    return IOState::Done;
172✔
1193
  }
172✔
1194
  return newState;
937✔
1195
}
1,109✔
1196

1197
void IncomingHTTP2Connection::handleReadableIOCallback([[maybe_unused]] int descriptor, FDMultiplexer::funcparam_t& param)
1198
{
376✔
1199
  auto conn = boost::any_cast<std::shared_ptr<IncomingHTTP2Connection>>(param);
376✔
1200
  conn->handleIO();
376✔
1201
}
376✔
1202

1203
void IncomingHTTP2Connection::handleWritableIOCallback([[maybe_unused]] int descriptor, FDMultiplexer::funcparam_t& param)
1204
{
1✔
1205
  auto conn = boost::any_cast<std::shared_ptr<IncomingHTTP2Connection>>(param);
1✔
1206
  conn->writeToSocket(true);
1✔
1207
}
1✔
1208

1209
void IncomingHTTP2Connection::stopIO()
1210
{
381✔
1211
  if (d_ioState) {
381✔
1212
    d_ioState->reset();
380✔
1213
  }
380✔
1214
}
381✔
1215

1216
uint32_t IncomingHTTP2Connection::getConcurrentStreamsCount() const
1217
{
184✔
1218
  return d_currentStreams.size() + d_killedStreams.size();
184✔
1219
}
184✔
1220

1221
boost::optional<struct timeval> IncomingHTTP2Connection::getIdleClientReadTTD(struct timeval now) const
1222
{
547✔
1223
  const auto& currentConfig = dnsdist::configuration::getCurrentRuntimeConfiguration();
547✔
1224
  auto idleTimeout = d_ci.cs->dohFrontend->d_idleTimeout;
547✔
1225
  if (currentConfig.d_maxTCPConnectionDuration == 0 && idleTimeout == 0) {
547!
1226
    return boost::none;
×
1227
  }
×
1228

1229
  if (currentConfig.d_maxTCPConnectionDuration > 0) {
547!
1230
    auto elapsed = now.tv_sec - d_connectionStartTime.tv_sec;
×
1231
    if (elapsed < 0 || (static_cast<size_t>(elapsed) >= currentConfig.d_maxTCPConnectionDuration)) {
×
1232
      return now;
×
1233
    }
×
1234
    auto remaining = currentConfig.d_maxTCPConnectionDuration - elapsed;
×
1235
    if (idleTimeout == 0 || remaining <= static_cast<size_t>(idleTimeout)) {
×
1236
      now.tv_sec += static_cast<time_t>(remaining);
×
1237
      return now;
×
1238
    }
×
1239
  }
×
1240

1241
  now.tv_sec += idleTimeout;
547✔
1242
  return now;
547✔
1243
}
547✔
1244

1245
void IncomingHTTP2Connection::updateIO(IOState newState, const timeval& now)
1246
{
141✔
1247
  (void)now;
141✔
1248
  updateIO(newState, newState == IOState::NeedWrite ? handleWritableIOCallback : handleReadableIOCallback);
141✔
1249
}
141✔
1250

1251
void IncomingHTTP2Connection::updateIO(IOState newState, const FDMultiplexer::callbackfunc_t& callback)
1252
{
993✔
1253
  boost::optional<struct timeval> ttd{boost::none};
993✔
1254

1255
  if (newState == IOState::Async) {
993!
1256
    auto shared = shared_from_this();
×
1257
    updateIOForAsync(shared);
×
1258
    return;
×
1259
  }
×
1260

1261
  auto shared = std::dynamic_pointer_cast<IncomingHTTP2Connection>(shared_from_this());
993✔
1262
  if (!shared || !d_ioState) {
993!
1263
    return;
×
1264
  }
×
1265

1266
  timeval now{};
993✔
1267
  gettimeofday(&now, nullptr);
993✔
1268

1269
  if (newState == IOState::NeedRead) {
993✔
1270
    /* use the idle TTL if the handshake has been completed (and proxy protocol payload received, if any),
1271
       and we have processed at least one query, otherwise we use the shorter read TTL  */
1272
    if ((d_state == State::waitingForQuery || d_state == State::idle) && (d_queriesCount > 0 || d_currentQueriesCount > 0)) {
987!
1273
      ttd = getIdleClientReadTTD(now);
547✔
1274
    }
547✔
1275
    else {
440✔
1276
      ttd = getClientReadTTD(now);
440✔
1277
    }
440✔
1278
    d_ioState->update(newState, callback, shared, ttd);
987✔
1279
  }
987✔
1280
  else if (newState == IOState::NeedWrite) {
6✔
1281
    ttd = getClientWriteTTD(now);
3✔
1282
    d_ioState->update(newState, callback, shared, ttd);
3✔
1283
  }
3✔
1284
  else if (newState == IOState::Done) {
3!
1285
    d_ioState->reset();
3✔
1286
  }
3✔
1287
}
993✔
1288

1289
void IncomingHTTP2Connection::handleIOError()
1290
{
179✔
1291
  d_connectionDied = true;
179✔
1292
  d_out.clear();
179✔
1293
  d_outPos = 0;
179✔
1294
  nghttp2_session_terminate_session(d_session.get(), NGHTTP2_PROTOCOL_ERROR);
179✔
1295
  d_currentStreams.clear();
179✔
1296
  d_killedStreams.clear();
179✔
1297
  stopIO();
179✔
1298
}
179✔
1299

1300
bool IncomingHTTP2Connection::active() const
1301
{
3,392✔
1302
  return !d_connectionDied && d_ioState != nullptr;
3,392!
1303
}
3,392✔
1304

1305
#endif /* HAVE_DNS_OVER_HTTPS && HAVE_NGHTTP2 */
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc