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

PowerDNS / pdns / 19741624072

27 Nov 2025 03:45PM UTC coverage: 73.086% (+0.02%) from 73.065%
19741624072

Pull #16570

github

web-flow
Merge 08a2cdb1d into f94a3f63f
Pull Request #16570: rec: rewrite all unwrap calls in web.rs

38523 of 63408 branches covered (60.75%)

Branch coverage included in aggregate %.

128044 of 164496 relevant lines covered (77.84%)

6531485.83 hits per line

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

78.63
/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)
226✔
101
  {
226✔
102
  }
226✔
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;
226✔
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
  {
79✔
153
    std::unique_ptr<DOHUnitInterface> unit(this);
79✔
154
    auto conn = d_connection.lock();
79✔
155
    if (!conn) {
79!
156
      /* the connection has been closed in the meantime */
157
      return;
×
158
    }
×
159

160
    state.du = std::move(unit);
79✔
161
    TCPResponse resp(std::move(response), std::move(state), nullptr, nullptr);
79✔
162
    resp.d_ds = downstream_;
79✔
163
    struct timeval now{};
79✔
164
    gettimeofday(&now, nullptr);
79✔
165
    conn->handleResponse(now, std::move(resp));
79✔
166
  }
79✔
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
{
255✔
190
  if (std::this_thread::get_id() != d_creatorThreadID) {
255✔
191
    handleCrossProtocolResponse(now, std::move(response));
127✔
192
    return;
127✔
193
  }
127✔
194

195
  auto& state = response.d_idstate;
128✔
196
  if (state.forwardedOverUDP) {
128✔
197
    dnsheader_aligned responseDH(response.d_buffer.data());
91✔
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)) {
91!
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
  }
91✔
222

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

226
std::unique_ptr<DOHUnitInterface> IncomingHTTP2Connection::getDOHUnit(uint32_t streamID)
227
{
226✔
228
  if (streamID > std::numeric_limits<IncomingHTTP2Connection::StreamID>::max()) {
226!
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)));
226✔
234
  return std::make_unique<IncomingDoHCrossProtocolContext>(std::move(query), std::dynamic_pointer_cast<IncomingHTTP2Connection>(shared_from_this()), streamID);
226✔
235
}
226✔
236

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

245
IncomingHTTP2Connection::IncomingHTTP2Connection(ConnectionInfo&& connectionInfo, TCPClientThreadData& threadData, const struct timeval& now) :
246
  IncomingTCPConnectionState(std::move(connectionInfo), threadData, now)
215✔
247
{
215✔
248
  nghttp2_session_callbacks* cbs = nullptr;
215✔
249
  if (nghttp2_session_callbacks_new(&cbs) != 0) {
215!
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);
215✔
253
  cbs = nullptr;
215✔
254

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

263
  nghttp2_session* sess = nullptr;
215✔
264
  if (nghttp2_session_server_new(&sess, callbacks.get(), this) != 0) {
215!
265
    throw std::runtime_error("Could 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);
215✔
269
  sess = nullptr;
215✔
270
}
215✔
271

272
bool IncomingHTTP2Connection::checkALPN()
273
{
212✔
274
  constexpr std::array<uint8_t, 2> h2ALPN{'h', '2'};
212✔
275
  const auto protocols = d_handler.getNextProtocol();
212✔
276
  if (protocols.size() == h2ALPN.size() && memcmp(protocols.data(), h2ALPN.data(), h2ALPN.size()) == 0) {
212!
277
    return true;
187✔
278
  }
187✔
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
  static const std::string data0("HTTP/1.1 400 Bad Request\r\nConnection: Close\r\n");
25✔
286

287
  std::array<char, 40> data1{};
25✔
288
  static const std::string dateformat("Date: %a, %d %h %Y %T GMT\r\n");
25✔
289
  struct tm tmval{};
25✔
290
  time_t timestamp = time(nullptr);
25✔
291
  size_t len = strftime(data1.data(), data1.size(), dateformat.data(), gmtime_r(&timestamp, &tmval));
25✔
292
  assert(len != 0);
25!
293

294
  static const std::string data2("\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✔
295

296
  d_out.insert(d_out.end(), data0.begin(), data0.end());
25✔
297
  d_out.insert(d_out.end(), data1.begin(), data1.begin() + len);
25✔
298
  d_out.insert(d_out.end(), data2.begin(), data2.end());
25✔
299
  writeToSocket(false);
25✔
300

301
  vinfolog("DoH connection from %s expected ALPN value 'h2', got '%s'", d_ci.remote.toStringWithPort(), std::string(protocols.begin(), protocols.end()));
25✔
302
  return false;
25✔
303
}
25✔
304

305
void IncomingHTTP2Connection::handleConnectionReady()
306
{
189✔
307
  constexpr std::array<nghttp2_settings_entry, 1> settings{{{NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, dnsdist::doh::MAX_INCOMING_CONCURRENT_STREAMS}}};
189✔
308
  constexpr std::array<nghttp2_settings_entry, 1> nearLimitsSettings{{{NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 1U}}};
189✔
309
  auto ret = nghttp2_submit_settings(d_session.get(), NGHTTP2_FLAG_NONE, isNearTCPLimits() ? nearLimitsSettings.data() : settings.data(), isNearTCPLimits() ? nearLimitsSettings.size() : settings.size());
189✔
310
  if (ret != 0) {
189!
311
    throw std::runtime_error("Fatal error: " + std::string(nghttp2_strerror(ret)));
×
312
  }
×
313
  d_needFlush = true;
189✔
314

315
  if (!d_inReadFunction) {
189!
316
    ret = nghttp2_session_send(d_session.get());
189✔
317
    if (ret != 0) {
189!
318
      throw std::runtime_error("Fatal error: " + std::string(nghttp2_strerror(ret)));
×
319
    }
×
320
  }
189✔
321
}
189✔
322

323
bool IncomingHTTP2Connection::hasPendingWrite() const
324
{
631✔
325
  return d_pendingWrite;
631✔
326
}
631✔
327

328
IOState IncomingHTTP2Connection::handleHandshake(const struct timeval& now)
329
{
415✔
330
  auto iostate = d_handler.tryHandshake();
415✔
331
  if (iostate == IOState::Done) {
415✔
332
    handleHandshakeDone(now);
214✔
333
    if (d_handler.isTLS()) {
214✔
334
      if (!checkALPN()) {
212✔
335
        d_connectionDied = true;
25✔
336
        stopIO();
25✔
337
        return iostate;
25✔
338
      }
25✔
339
    }
212✔
340

341
    if (d_ci.cs != nullptr && d_ci.cs->d_enableProxyProtocol && !isProxyPayloadOutsideTLS() && expectProxyProtocolFrom(d_ci.remote)) {
189!
342
      d_state = State::readingProxyProtocolHeader;
1✔
343
      d_buffer.resize(s_proxyProtocolMinimumHeaderSize);
1✔
344
      d_proxyProtocolNeed = s_proxyProtocolMinimumHeaderSize;
1✔
345
    }
1✔
346
    else {
188✔
347
      d_state = State::waitingForQuery;
188✔
348
      handleConnectionReady();
188✔
349
    }
188✔
350
  }
189✔
351
  return iostate;
390✔
352
}
415✔
353

354
class ReadFunctionGuard
355
{
356
public:
357
  ReadFunctionGuard(bool& inReadFunction) :
358
    d_inReadFunctionRef(inReadFunction)
1,142✔
359
  {
1,142✔
360
    d_inReadFunctionRef = true;
1,142✔
361
  }
1,142✔
362
  ReadFunctionGuard(ReadFunctionGuard&&) = delete;
363
  ReadFunctionGuard(const ReadFunctionGuard&) = delete;
364
  ReadFunctionGuard& operator=(ReadFunctionGuard&&) = delete;
365
  ReadFunctionGuard& operator=(const ReadFunctionGuard&) = delete;
366
  ~ReadFunctionGuard()
367
  {
1,142✔
368
    d_inReadFunctionRef = false;
1,142✔
369
  }
1,142✔
370

371
private:
372
  bool& d_inReadFunctionRef;
373
};
374

375
void IncomingHTTP2Connection::handleIO()
376
{
694✔
377
  IOState iostate = IOState::Done;
694✔
378
  struct timeval now{};
694✔
379
  gettimeofday(&now, nullptr);
694✔
380

381
  try {
694✔
382
    if (maxConnectionDurationReached(dnsdist::configuration::getCurrentRuntimeConfiguration().d_maxTCPConnectionDuration, now)) {
694!
383
      vinfolog("Terminating DoH connection from %s because it reached the maximum TCP connection duration", d_ci.remote.toStringWithPort());
×
384
      stopIO();
×
385
      d_connectionClosing = true;
×
386
      return;
×
387
    }
×
388

389
    if (d_state == State::starting) {
694✔
390
      if (d_ci.cs != nullptr && d_ci.cs->dohFrontend != nullptr) {
215!
391
        ++d_ci.cs->dohFrontend->d_httpconnects;
215✔
392
      }
215✔
393
      if (d_ci.cs != nullptr && d_ci.cs->d_enableProxyProtocol && isProxyPayloadOutsideTLS() && expectProxyProtocolFrom(d_ci.remote)) {
215!
394
        d_state = State::readingProxyProtocolHeader;
1✔
395
        d_buffer.resize(s_proxyProtocolMinimumHeaderSize);
1✔
396
        d_proxyProtocolNeed = s_proxyProtocolMinimumHeaderSize;
1✔
397
      }
1✔
398
      else {
214✔
399
        d_state = State::doingHandshake;
214✔
400
      }
214✔
401
    }
215✔
402

403
    if (d_state == State::doingHandshake) {
694✔
404
      iostate = handleHandshake(now);
414✔
405
      if (d_connectionDied) {
414✔
406
        return;
29✔
407
      }
29✔
408
    }
414✔
409

410
    if (d_state == State::readingProxyProtocolHeader) {
665✔
411
      auto status = handleProxyProtocolPayload();
2✔
412
      if (status == ProxyProtocolResult::Done) {
2!
413
        if (isProxyPayloadOutsideTLS()) {
2✔
414
          d_state = State::doingHandshake;
1✔
415
          iostate = handleHandshake(now);
1✔
416
          if (d_connectionDied) {
1!
417
            return;
×
418
          }
×
419
        }
1✔
420
        else {
1✔
421
          d_state = State::waitingForQuery;
1✔
422
          handleConnectionReady();
1✔
423
        }
1✔
424
      }
2✔
425
      else if (status == ProxyProtocolResult::Error) {
×
426
        d_connectionDied = true;
×
427
        stopIO();
×
428
        return;
×
429
      }
×
430
    }
2✔
431

432
    if (!d_inReadFunction && active() && !d_connectionClosing && (d_state == State::waitingForQuery || d_state == State::idle)) {
665!
433
      do {
1,142✔
434
        iostate = readHTTPData();
1,142✔
435
      } while (!d_inReadFunction && active() && !d_connectionClosing && iostate == IOState::Done);
1,142!
436
    }
464✔
437

438
    if (!active()) {
665✔
439
      stopIO();
180✔
440
      return;
180✔
441
    }
180✔
442
    /*
443
      So:
444
      - if we have a pending write, we need to wait until the socket becomes writable
445
        and then call handleWritableCallback
446
      - if we have NeedWrite but no pending write, we need to wait until the socket
447
        becomes writable but for handleReadableIOCallback
448
      - if we have NeedRead, or nghttp2_session_want_read, wait until the socket
449
        becomes readable and call handleReadableIOCallback
450
    */
451
    if (hasPendingWrite()) {
485✔
452
      updateIO(IOState::NeedWrite, handleWritableIOCallback);
2✔
453
    }
2✔
454
    else if (iostate == IOState::NeedWrite) {
483!
455
      updateIO(IOState::NeedWrite, handleReadableIOCallback);
×
456
    }
×
457
    else if (!d_connectionClosing) {
483✔
458
      if (nghttp2_session_want_read(d_session.get()) != 0) {
480✔
459
        updateIO(IOState::NeedRead, handleReadableIOCallback);
479✔
460
      }
479✔
461
      else {
1✔
462
        if (getConcurrentStreamsCount() == 0) {
1!
463
          d_connectionDied = true;
1✔
464
          stopIO();
1✔
465
        }
1✔
466
        else {
×
467
          updateIO(IOState::Done, handleReadableIOCallback);
×
468
        }
×
469
      }
1✔
470
    }
480✔
471
    else {
3✔
472
      if (getConcurrentStreamsCount() == 0) {
3!
473
        d_connectionDied = true;
×
474
        stopIO();
×
475
      }
×
476
      else {
3✔
477
        updateIO(IOState::Done, handleReadableIOCallback);
3✔
478
      }
3✔
479
    }
3✔
480
  }
485✔
481
  catch (const std::exception& e) {
694✔
482
    vinfolog("Exception when processing IO for incoming DoH connection from %s: %s", d_ci.remote.toStringWithPort(), e.what());
1!
483
    d_connectionDied = true;
1✔
484
    stopIO();
1✔
485
  }
1✔
486
}
694✔
487

488
void IncomingHTTP2Connection::writeToSocket(bool socketReady)
489
{
404✔
490
  try {
404✔
491
    d_needFlush = false;
404✔
492
    IOState newState = d_handler.tryWrite(d_out, d_outPos, d_out.size());
404✔
493

494
    if (newState == IOState::Done) {
404✔
495
      d_pendingWrite = false;
392✔
496
      d_out.clear();
392✔
497
      d_outPos = 0;
392✔
498
      if (active() && !d_connectionClosing) {
392!
499
        updateIO(IOState::NeedRead, handleReadableIOCallback);
390✔
500
      }
390✔
501
      else {
2✔
502
        stopIO();
2✔
503
      }
2✔
504
    }
392✔
505
    else {
12✔
506
      updateIO(newState, handleWritableIOCallback);
12✔
507
      d_pendingWrite = true;
12✔
508
    }
12✔
509
  }
404✔
510
  catch (const std::exception& e) {
404✔
511
    vinfolog("Exception while trying to write (%s) to HTTP client connection to %s: %s", (socketReady ? "ready" : "send"), d_ci.remote.toStringWithPort(), e.what());
10!
512
    handleIOError();
10✔
513
  }
10✔
514
}
404✔
515

516
ssize_t IncomingHTTP2Connection::send_callback(nghttp2_session* session, const uint8_t* data, size_t length, int flags, void* user_data)
517
{
747✔
518
  (void)session;
747✔
519
  (void)flags;
747✔
520
  auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
747✔
521
  if (conn->d_connectionDied) {
747✔
522
    return static_cast<ssize_t>(length);
6✔
523
  }
6✔
524
  // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic): nghttp2 API
525
  conn->d_out.insert(conn->d_out.end(), data, data + length);
741✔
526

527
  if (conn->d_connectionClosing || conn->d_needFlush) {
741✔
528
    conn->writeToSocket(false);
377✔
529
  }
377✔
530

531
  return static_cast<ssize_t>(length);
741✔
532
}
747✔
533

534
static const std::array<const std::string, static_cast<size_t>(NGHTTP2Headers::HeaderConstantIndexes::COUNT)> s_headerConstants{
535
  "200",
536
  ":method",
537
  "POST",
538
  ":scheme",
539
  "https",
540
  ":authority",
541
  "x-forwarded-for",
542
  ":path",
543
  "content-length",
544
  ":status",
545
  "location",
546
  "accept",
547
  "application/dns-message",
548
  "cache-control",
549
  "content-type",
550
  "application/dns-message",
551
  "user-agent",
552
  "nghttp2-" NGHTTP2_VERSION "/dnsdist",
553
  "x-forwarded-port",
554
  "x-forwarded-proto",
555
  "dns-over-udp",
556
  "dns-over-tcp",
557
  "dns-over-tls",
558
  "dns-over-http",
559
  "dns-over-https"};
560

561
static const std::string s_authorityHeaderName(":authority");
562
static const std::string s_pathHeaderName(":path");
563
static const std::string s_methodHeaderName(":method");
564
static const std::string s_schemeHeaderName(":scheme");
565
static const std::string s_xForwardedForHeaderName("x-forwarded-for");
566

567
static void addHeader(std::vector<nghttp2_nv>& headers, const std::string_view& name, bool nameIsStatic, const std::string_view& value, bool valueIsStatic)
568
{
2,405✔
569
  /* Be careful when setting NGHTTP2_NV_FLAG_NO_COPY_NAME or NGHTTP2_NV_FLAG_NO_COPY_VALUE, the corresponding name or value needs to exist until after nghttp2_session_send() has been called, not just nghttp2_submit_response(). */
570
  uint8_t flag{NGHTTP2_NV_FLAG_NONE};
2,405✔
571
  if (nameIsStatic) {
2,405✔
572
    flag |= NGHTTP2_NV_FLAG_NO_COPY_NAME;
2,218✔
573
  }
2,218✔
574
  if (valueIsStatic) {
2,405✔
575
    flag |= NGHTTP2_NV_FLAG_NO_COPY_VALUE;
1,262✔
576
  }
1,262✔
577

578
  // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast,cppcoreguidelines-pro-type-reinterpret-cast): nghttp2 API
579
  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(), flag});
2,405✔
580
}
2,405✔
581

582
void NGHTTP2Headers::addStaticHeader(std::vector<nghttp2_nv>& headers, NGHTTP2Headers::HeaderConstantIndexes nameKey, NGHTTP2Headers::HeaderConstantIndexes valueKey)
583
{
1,262✔
584
  const auto& name = s_headerConstants.at(static_cast<size_t>(nameKey));
1,262✔
585
  const auto& value = s_headerConstants.at(static_cast<size_t>(valueKey));
1,262✔
586

587
  addHeader(headers, name, true, value, true);
1,262✔
588
}
1,262✔
589

590
void NGHTTP2Headers::addCustomDynamicHeader(std::vector<nghttp2_nv>& headers, const std::string& name, const std::string_view& value)
591
{
187✔
592
  addHeader(headers, name, false, value, false);
187✔
593
}
187✔
594

595
void NGHTTP2Headers::addDynamicHeader(std::vector<nghttp2_nv>& headers, NGHTTP2Headers::HeaderConstantIndexes nameKey, const std::string_view& value)
596
{
956✔
597
  const auto& name = s_headerConstants.at(static_cast<size_t>(nameKey));
956✔
598
  addHeader(headers, name, true, value, false);
956✔
599
}
956✔
600

601
std::unordered_map<IncomingHTTP2Connection::StreamID, IncomingHTTP2Connection::PendingQuery>::iterator IncomingHTTP2Connection::getStreamContext(StreamID streamID)
602
{
1,703✔
603
  auto streamIt = d_currentStreams.find(streamID);
1,703✔
604
  if (streamIt == d_currentStreams.end()) {
1,703!
605
    /* it might have been closed by the remote end in the meantime */
606
    d_killedStreams.erase(streamID);
×
607
  }
×
608
  return streamIt;
1,703✔
609
}
1,703✔
610

611
IOState IncomingHTTP2Connection::sendResponse(const struct timeval& now, TCPResponse&& response)
612
{
147✔
613
  (void)now;
147✔
614
  if (response.d_idstate.d_streamID == -1) {
147!
615
    throw std::runtime_error("Invalid DoH stream ID while sending response");
×
616
  }
×
617
  auto streamIt = getStreamContext(response.d_idstate.d_streamID);
147✔
618
  if (streamIt == d_currentStreams.end()) {
147!
619
    /* it might have been closed by the remote end in the meantime */
620
    return hasPendingWrite() ? IOState::NeedWrite : IOState::Done;
×
621
  }
×
622
  auto& context = streamIt->second;
147✔
623

624
  uint32_t statusCode = 200U;
147✔
625
  std::string contentType;
147✔
626
  bool sendContentType = true;
147✔
627
  auto& responseBuffer = context.d_buffer;
147✔
628
  if (context.d_statusCode != 0) {
147✔
629
    responseBuffer = std::move(context.d_response);
7✔
630
    statusCode = context.d_statusCode;
7✔
631
    contentType = std::move(context.d_contentTypeOut);
7✔
632
  }
7✔
633
  else {
140✔
634
    responseBuffer = std::move(response.d_buffer);
140✔
635
  }
140✔
636

637
  auto sent = responseBuffer.size();
147✔
638
  sendResponse(response.d_idstate.d_streamID, context, statusCode, d_ci.cs->dohFrontend->d_customResponseHeaders, contentType, sendContentType);
147✔
639
  handleResponseSent(response, sent);
147✔
640

641
  return hasPendingWrite() ? IOState::NeedWrite : IOState::Done;
147!
642
}
147✔
643

644
void IncomingHTTP2Connection::notifyIOError(const struct timeval& now, TCPResponse&& response)
645
{
14✔
646
  if (std::this_thread::get_id() != d_creatorThreadID) {
14✔
647
    /* empty buffer will signal an IO error */
648
    response.d_buffer.clear();
6✔
649
    handleCrossProtocolResponse(now, std::move(response));
6✔
650
    return;
6✔
651
  }
6✔
652

653
  if (response.d_idstate.d_streamID == -1) {
8!
654
    throw std::runtime_error("Invalid DoH stream ID while handling I/O error notification");
×
655
  }
×
656

657
  auto streamIt = getStreamContext(response.d_idstate.d_streamID);
8✔
658
  if (streamIt == d_currentStreams.end()) {
8!
659
    /* it might have been closed by the remote end in the meantime */
660
    return;
×
661
  }
×
662
  auto& context = streamIt->second;
8✔
663
  context.d_buffer = std::move(response.d_buffer);
8✔
664
  sendResponse(response.d_idstate.d_streamID, context, 502, d_ci.cs->dohFrontend->d_customResponseHeaders);
8✔
665
}
8✔
666

667
bool IncomingHTTP2Connection::sendResponse(IncomingHTTP2Connection::StreamID streamID, IncomingHTTP2Connection::PendingQuery& context, uint16_t responseCode, const HeadersMap& customResponseHeaders, const std::string& contentType, bool addContentType)
668
{
186✔
669
  /* 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.
670
   */
671
  nghttp2_data_provider data_provider;
186✔
672

673
  data_provider.source.ptr = this;
186✔
674
  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 {
186✔
675
    (void)source;
186✔
676
    auto* connection = static_cast<IncomingHTTP2Connection*>(cb_data);
186✔
677
    auto& obj = connection->d_currentStreams.at(stream_id);
186✔
678
    size_t toCopy = 0;
186✔
679
    if (obj.d_queryPos < obj.d_buffer.size()) {
186!
680
      size_t remaining = obj.d_buffer.size() - obj.d_queryPos;
186✔
681
      toCopy = length > remaining ? remaining : length;
186!
682
      memcpy(buf, &obj.d_buffer.at(obj.d_queryPos), toCopy);
186✔
683
      obj.d_queryPos += toCopy;
186✔
684
    }
186✔
685

686
    if (obj.d_queryPos >= obj.d_buffer.size()) {
186!
687
      *data_flags |= NGHTTP2_DATA_FLAG_EOF;
186✔
688
      obj.d_buffer.clear();
186✔
689
      connection->d_needFlush = true;
186✔
690
    }
186✔
691
    return static_cast<ssize_t>(toCopy);
186✔
692
  };
186✔
693

694
  const auto& dohFrontend = d_ci.cs->dohFrontend;
186✔
695
  auto& responseBody = context.d_buffer;
186✔
696

697
  std::vector<nghttp2_nv> headers;
186✔
698
  std::string responseCodeStr;
186✔
699
  std::string cacheControlValue;
186✔
700
  std::string location;
186✔
701
  /* remember that dynamic header values should be kept alive
702
     until we have called nghttp2_submit_response(), at least */
703
  /* status, content-type, cache-control, content-length */
704
  headers.reserve(4);
186✔
705

706
  if (responseCode == 200) {
186✔
707
    NGHTTP2Headers::addStaticHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::STATUS_NAME, NGHTTP2Headers::HeaderConstantIndexes::OK_200_VALUE);
145✔
708
    ++dohFrontend->d_validresponses;
145✔
709
    ++dohFrontend->d_http2Stats.d_nb200Responses;
145✔
710

711
    if (addContentType) {
145!
712
      if (contentType.empty()) {
145✔
713
        NGHTTP2Headers::addStaticHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_NAME, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_VALUE);
140✔
714
      }
140✔
715
      else {
5✔
716
        NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_NAME, contentType);
5✔
717
      }
5✔
718
    }
145✔
719

720
    if (dohFrontend->d_sendCacheControlHeaders && responseBody.size() > sizeof(dnsheader)) {
145✔
721
      // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): API
722
      uint32_t minTTL = getDNSPacketMinTTL(reinterpret_cast<const char*>(responseBody.data()), responseBody.size());
139✔
723
      if (minTTL != std::numeric_limits<uint32_t>::max()) {
139✔
724
        cacheControlValue = "max-age=" + std::to_string(minTTL);
100✔
725
        NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CACHE_CONTROL_NAME, cacheControlValue);
100✔
726
      }
100✔
727
    }
139✔
728
  }
145✔
729
  else {
41✔
730
    responseCodeStr = std::to_string(responseCode);
41✔
731
    NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::STATUS_NAME, responseCodeStr);
41✔
732

733
    if (responseCode >= 300 && responseCode < 400) {
41!
734
      // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
735
      location = std::string(reinterpret_cast<const char*>(responseBody.data()), responseBody.size());
2✔
736
      NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_NAME, "text/html; charset=utf-8");
2✔
737
      NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::LOCATION_NAME, location);
2✔
738
      static const std::string s_redirectStart{"<!DOCTYPE html><TITLE>Moved</TITLE><P>The document has moved <A HREF=\""};
2✔
739
      static const std::string s_redirectEnd{"\">here</A>"};
2✔
740
      responseBody.reserve(s_redirectStart.size() + responseBody.size() + s_redirectEnd.size());
2✔
741
      responseBody.insert(responseBody.begin(), s_redirectStart.begin(), s_redirectStart.end());
2✔
742
      responseBody.insert(responseBody.end(), s_redirectEnd.begin(), s_redirectEnd.end());
2✔
743
      ++dohFrontend->d_redirectresponses;
2✔
744
    }
2✔
745
    else {
39✔
746
      ++dohFrontend->d_errorresponses;
39✔
747
      switch (responseCode) {
39✔
748
      case 400:
15✔
749
        ++dohFrontend->d_http2Stats.d_nb400Responses;
15✔
750
        break;
15✔
751
      case 403:
7✔
752
        ++dohFrontend->d_http2Stats.d_nb403Responses;
7✔
753
        break;
7✔
754
      case 500:
×
755
        ++dohFrontend->d_http2Stats.d_nb500Responses;
×
756
        break;
×
757
      case 502:
8✔
758
        ++dohFrontend->d_http2Stats.d_nb502Responses;
8✔
759
        break;
8✔
760
      default:
9✔
761
        ++dohFrontend->d_http2Stats.d_nbOtherResponses;
9✔
762
        break;
9✔
763
      }
39✔
764

765
      if (!responseBody.empty()) {
39✔
766
        NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_NAME, "text/plain; charset=utf-8");
31✔
767
      }
31✔
768
      else {
8✔
769
        static const std::string invalid{"invalid DNS query"};
8✔
770
        static const std::string notAllowed{"dns query not allowed"};
8✔
771
        static const std::string noDownstream{"no downstream server available"};
8✔
772
        static const std::string internalServerError{"Internal Server Error"};
8✔
773

774
        switch (responseCode) {
8✔
775
        case 400:
×
776
          responseBody.insert(responseBody.begin(), invalid.begin(), invalid.end());
×
777
          break;
×
778
        case 403:
×
779
          responseBody.insert(responseBody.begin(), notAllowed.begin(), notAllowed.end());
×
780
          break;
×
781
        case 502:
8!
782
          responseBody.insert(responseBody.begin(), noDownstream.begin(), noDownstream.end());
8✔
783
          break;
8✔
784
        case 500:
×
785
          /* fall-through */
786
        default:
×
787
          responseBody.insert(responseBody.begin(), internalServerError.begin(), internalServerError.end());
×
788
          break;
×
789
        }
8✔
790
      }
8✔
791
    }
39✔
792
  }
41✔
793

794
  const std::string contentLength = std::to_string(responseBody.size());
186✔
795
  NGHTTP2Headers::addDynamicHeader(headers, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_LENGTH_NAME, contentLength);
186✔
796

797
  for (const auto& [key, value] : customResponseHeaders) {
197✔
798
    NGHTTP2Headers::addCustomDynamicHeader(headers, key, value);
187✔
799
  }
187✔
800

801
  context.d_sendingResponse = true;
186✔
802
  auto ret = nghttp2_submit_response(d_session.get(), streamID, headers.data(), headers.size(), &data_provider);
186✔
803
  if (ret != 0) {
186!
804
    vinfolog("Error submitting HTTP response for stream %d: %s", streamID, nghttp2_strerror(ret));
×
805
    d_currentStreams.erase(streamID);
×
806
    return false;
×
807
  }
×
808

809
  if (!d_inReadFunction) {
186✔
810
    ret = nghttp2_session_send(d_session.get());
104✔
811
    if (ret != 0) {
104!
812
      d_currentStreams.erase(streamID);
×
813
      vinfolog("Error flushing HTTP response for stream %d: %s", streamID, nghttp2_strerror(ret));
×
814
      return false;
×
815
    }
×
816
  }
104✔
817

818
  return true;
186✔
819
}
186✔
820

821
static void processForwardedForHeader(const std::unique_ptr<HeadersMap>& headers, ComboAddress& remote)
822
{
5✔
823
  if (!headers) {
5✔
824
    return;
1✔
825
  }
1✔
826

827
  auto headerIt = headers->find(s_xForwardedForHeaderName);
4✔
828
  if (headerIt == headers->end()) {
4!
829
    return;
×
830
  }
×
831

832
  std::string_view value = headerIt->second;
4✔
833
  try {
4✔
834
    auto pos = value.rfind(',');
4✔
835
    if (pos != std::string_view::npos) {
4✔
836
      ++pos;
2✔
837
      for (; pos < value.size() && value[pos] == ' '; ++pos) {
4!
838
      }
2✔
839

840
      if (pos < value.size()) {
2!
841
        value = value.substr(pos);
2✔
842
      }
2✔
843
    }
2✔
844
    auto newRemote = ComboAddress(std::string(value));
4✔
845
    remote = newRemote;
4✔
846
  }
4✔
847
  catch (const std::exception& e) {
4✔
848
    vinfolog("Invalid X-Forwarded-For header ('%s') received from %s : %s", std::string(value), remote.toStringWithPort(), e.what());
×
849
  }
×
850
  catch (const PDNSException& e) {
4✔
851
    vinfolog("Invalid X-Forwarded-For header ('%s') received from %s : %s", std::string(value), remote.toStringWithPort(), e.reason);
×
852
  }
×
853
}
4✔
854

855
void IncomingHTTP2Connection::handleIncomingQuery(IncomingHTTP2Connection::PendingQuery&& query, IncomingHTTP2Connection::StreamID streamID)
856
{
186✔
857
  const auto handleImmediateResponse = [this, &query, streamID](uint16_t code, const std::string& reason, PacketBuffer&& response = PacketBuffer()) {
186✔
858
    if (response.empty()) {
27!
859
      query.d_buffer.clear();
27✔
860
      query.d_buffer.insert(query.d_buffer.begin(), reason.begin(), reason.end());
27✔
861
    }
27✔
862
    else {
×
863
      query.d_buffer = std::move(response);
×
864
    }
×
865
    vinfolog("Sending an immediate %d response to incoming DoH query: %s", code, reason);
27✔
866
    sendResponse(streamID, query, code, d_ci.cs->dohFrontend->d_customResponseHeaders);
27✔
867
  };
27✔
868

869
  if (query.d_method == PendingQuery::Method::Unknown || query.d_method == PendingQuery::Method::Unsupported) {
186!
870
    handleImmediateResponse(400, "DoH query not allowed because of unsupported HTTP method");
2✔
871
    return;
2✔
872
  }
2✔
873

874
  ++d_ci.cs->dohFrontend->d_http2Stats.d_nbQueries;
184✔
875

876
  if (d_ci.cs->dohFrontend->d_trustForwardedForHeader) {
184✔
877
    processForwardedForHeader(query.d_headers, d_proxiedRemote);
5✔
878

879
    /* second ACL lookup based on the updated address */
880
    if (!dnsdist::configuration::getCurrentRuntimeConfiguration().d_ACL.match(d_proxiedRemote)) {
5✔
881
      ++dnsdist::metrics::g_stats.aclDrops;
1✔
882
      vinfolog("Query from %s (%s) (DoH) dropped because of ACL", d_ci.remote.toStringWithPort(), d_proxiedRemote.toStringWithPort());
1!
883
      handleImmediateResponse(403, "DoH query not allowed because of ACL");
1✔
884
      return;
1✔
885
    }
1✔
886

887
    if (!d_ci.cs->dohFrontend->d_keepIncomingHeaders) {
4!
888
      query.d_headers.reset();
4✔
889
    }
4✔
890
  }
4✔
891

892
  if (d_ci.cs->dohFrontend->d_exactPathMatching) {
183✔
893
    if (d_ci.cs->dohFrontend->d_urls.count(query.d_path) == 0) {
180✔
894
      handleImmediateResponse(404, "there is no endpoint configured for this path");
4✔
895
      return;
4✔
896
    }
4✔
897
  }
180✔
898
  else {
3✔
899
    bool found = false;
3✔
900
    for (const auto& path : d_ci.cs->dohFrontend->d_urls) {
3✔
901
      if (boost::starts_with(query.d_path, path)) {
3✔
902
        found = true;
2✔
903
        break;
2✔
904
      }
2✔
905
    }
3✔
906
    if (!found) {
3✔
907
      handleImmediateResponse(404, "there is no endpoint configured for this path");
1✔
908
      return;
1✔
909
    }
1✔
910
  }
3✔
911

912
  /* the responses map can be updated at runtime, so we need to take a copy of
913
     the shared pointer, increasing the reference counter */
914
  auto responsesMap = d_ci.cs->dohFrontend->d_responsesMap;
178✔
915
  if (responsesMap) {
178✔
916
    for (const auto& entry : *responsesMap) {
58✔
917
      if (entry->matches(query.d_path)) {
58✔
918
        const auto& customHeaders = entry->getHeaders();
4✔
919
        query.d_buffer = entry->getContent();
4✔
920
        if (entry->getStatusCode() >= 400 && !query.d_buffer.empty()) {
4!
921
          // legacy trailing 0 from the h2o era
922
          query.d_buffer.pop_back();
4✔
923
        }
4✔
924

925
        sendResponse(streamID, query, entry->getStatusCode(), customHeaders ? *customHeaders : d_ci.cs->dohFrontend->d_customResponseHeaders, std::string(), false);
4!
926
        return;
4✔
927
      }
4✔
928
    }
58✔
929
  }
58✔
930

931
  if (query.d_buffer.empty() && query.d_method == PendingQuery::Method::Get && !query.d_queryString.empty()) {
174✔
932
    auto payload = dnsdist::doh::getPayloadFromPath(query.d_queryString);
152✔
933
    if (payload) {
152✔
934
      query.d_buffer = std::move(*payload);
148✔
935
    }
148✔
936
    else {
4✔
937
      ++d_ci.cs->dohFrontend->d_badrequests;
4✔
938
      handleImmediateResponse(400, "DoH unable to decode BASE64-URL");
4✔
939
      return;
4✔
940
    }
4✔
941
  }
152✔
942

943
  if (query.d_method == PendingQuery::Method::Get) {
170✔
944
    ++d_ci.cs->dohFrontend->d_getqueries;
150✔
945
  }
150✔
946
  else if (query.d_method == PendingQuery::Method::Post) {
20!
947
    ++d_ci.cs->dohFrontend->d_postqueries;
20✔
948
  }
20✔
949

950
  try {
170✔
951
    struct timeval now{};
170✔
952
    gettimeofday(&now, nullptr);
170✔
953
    auto processingResult = handleQuery(std::move(query.d_buffer), now, streamID);
170✔
954

955
    switch (processingResult) {
170✔
956
    case QueryProcessingResult::TooSmall:
4✔
957
      handleImmediateResponse(400, "DoH non-compliant query");
4✔
958
      break;
4✔
959
    case QueryProcessingResult::InvalidHeaders:
2✔
960
      handleImmediateResponse(400, "DoH invalid headers");
2✔
961
      break;
2✔
962
    case QueryProcessingResult::Dropped:
6✔
963
      handleImmediateResponse(403, "DoH dropped query");
6✔
964
      break;
6✔
965
    case QueryProcessingResult::NoBackend:
×
966
      handleImmediateResponse(502, "DoH no backend available");
×
967
      return;
×
968
    case QueryProcessingResult::Forwarded:
68✔
969
    case QueryProcessingResult::Asynchronous:
104✔
970
    case QueryProcessingResult::SelfAnswered:
155✔
971
      break;
155✔
972
    }
170✔
973
  }
170✔
974
  catch (const std::exception& e) {
170✔
975
    vinfolog("Exception while processing DoH query: %s", e.what());
3✔
976
    handleImmediateResponse(400, "DoH non-compliant query");
3✔
977
    return;
3✔
978
  }
3✔
979
}
170✔
980

981
int IncomingHTTP2Connection::on_frame_recv_callback(nghttp2_session* session, const nghttp2_frame* frame, void* user_data)
982
{
896✔
983
  (void)session;
896✔
984
  auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
896✔
985
  /* is this the last frame for this stream? */
986
  if ((frame->hd.type == NGHTTP2_HEADERS || frame->hd.type == NGHTTP2_DATA) && (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) != 0) {
896✔
987
    auto streamID = frame->hd.stream_id;
186✔
988
    auto stream = conn->getStreamContext(streamID);
186✔
989
    if (stream != conn->d_currentStreams.end()) {
186!
990
      conn->handleIncomingQuery(std::move(stream->second), streamID);
186✔
991
    }
186✔
992
    else {
×
993
      vinfolog("Stream %d NOT FOUND", streamID);
×
994
      return NGHTTP2_ERR_CALLBACK_FAILURE;
×
995
    }
×
996
  }
186✔
997
  else if (frame->hd.type == NGHTTP2_PING) {
710!
998
    conn->d_needFlush = true;
×
999
  }
×
1000

1001
  return 0;
896✔
1002
}
896✔
1003

1004
int IncomingHTTP2Connection::on_stream_close_callback(nghttp2_session* session, IncomingHTTP2Connection::StreamID stream_id, uint32_t error_code, void* user_data)
1005
{
186✔
1006
  (void)session;
186✔
1007
  (void)error_code;
186✔
1008
  auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
186✔
1009

1010
  auto streamIt = conn->d_currentStreams.find(stream_id);
186✔
1011
  if (streamIt == conn->d_currentStreams.end()) {
186✔
1012
    return 0;
2✔
1013
  }
2✔
1014

1015
  if (!streamIt->second.d_sendingResponse) {
184!
1016
    conn->d_killedStreams.emplace(stream_id);
×
1017
  }
×
1018

1019
  conn->d_currentStreams.erase(streamIt);
184✔
1020
  return 0;
184✔
1021
}
186✔
1022

1023
int IncomingHTTP2Connection::on_begin_headers_callback(nghttp2_session* session, const nghttp2_frame* frame, void* user_data)
1024
{
188✔
1025
  (void)session;
188✔
1026
  if (frame->hd.type != NGHTTP2_HEADERS || frame->headers.cat != NGHTTP2_HCAT_REQUEST) {
188!
1027
    return 0;
×
1028
  }
×
1029

1030
  auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
188✔
1031
  auto close_connection = [](IncomingHTTP2Connection* connection) -> int {
188✔
1032
    connection->d_connectionClosing = true;
×
1033
    connection->d_needFlush = true;
×
1034
    nghttp2_session_terminate_session(connection->d_session.get(), NGHTTP2_REFUSED_STREAM);
×
1035
    return 0;
×
1036
  };
×
1037

1038
  if (conn->getConcurrentStreamsCount() >= dnsdist::doh::MAX_INCOMING_CONCURRENT_STREAMS) {
188!
1039
    vinfolog("Too many concurrent streams on connection from %s", conn->d_ci.remote.toStringWithPort());
×
1040
    return close_connection(conn);
×
1041
  }
×
1042

1043
  auto insertPair = conn->d_currentStreams.emplace(frame->hd.stream_id, PendingQuery());
188✔
1044
  if (!insertPair.second) {
188!
1045
    /* there is a stream ID collision, something is very wrong! */
1046
    vinfolog("Stream ID collision (%d) on connection from %s", frame->hd.stream_id, conn->d_ci.remote.toStringWithPort());
×
1047
    return close_connection(conn);
×
1048
  }
×
1049

1050
  return 0;
188✔
1051
}
188✔
1052

1053
static std::string::size_type getLengthOfPathWithoutParameters(const std::string_view& path)
1054
{
188✔
1055
  auto pos = path.find('?');
188✔
1056
  if (pos == string::npos) {
188✔
1057
    return path.size();
28✔
1058
  }
28✔
1059

1060
  return pos;
160✔
1061
}
188✔
1062

1063
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)
1064
{
1,336✔
1065
  (void)session;
1,336✔
1066
  (void)flags;
1,336✔
1067
  auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
1,336✔
1068

1069
  if (frame->hd.type == NGHTTP2_HEADERS && frame->headers.cat == NGHTTP2_HCAT_REQUEST) {
1,336!
1070
    if (nghttp2_check_header_name(name, nameLen) == 0) {
1,336!
1071
      vinfolog("Invalid header name");
×
1072
      return NGHTTP2_ERR_CALLBACK_FAILURE;
×
1073
    }
×
1074

1075
#if defined(HAVE_NGHTTP2_CHECK_HEADER_VALUE_RFC9113)
1,336✔
1076
    if (nghttp2_check_header_value_rfc9113(value, valuelen) == 0) {
1,336!
1077
      vinfolog("Invalid header value");
×
1078
      return NGHTTP2_ERR_CALLBACK_FAILURE;
×
1079
    }
×
1080
#endif /* HAVE_NGHTTP2_CHECK_HEADER_VALUE_RFC9113 */
1,336✔
1081

1082
    auto headerMatches = [name, nameLen](const std::string& expected) -> bool {
4,251✔
1083
      return nameLen == expected.size() && memcmp(name, expected.data(), expected.size()) == 0;
4,251✔
1084
    };
4,251✔
1085

1086
    auto stream = conn->getStreamContext(frame->hd.stream_id);
1,336✔
1087
    if (stream == conn->d_currentStreams.end()) {
1,336!
1088
      vinfolog("Unable to match the stream ID %d to a known one!", frame->hd.stream_id);
×
1089
      return NGHTTP2_ERR_CALLBACK_FAILURE;
×
1090
    }
×
1091
    auto& query = stream->second;
1,336✔
1092
    // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): nghttp2 API
1093
    auto valueView = std::string_view(reinterpret_cast<const char*>(value), valuelen);
1,336✔
1094
    if (headerMatches(s_pathHeaderName)) {
1,336✔
1095
#if defined(HAVE_NGHTTP2_CHECK_PATH)
188✔
1096
      if (nghttp2_check_path(value, valuelen) == 0) {
188!
1097
        vinfolog("Invalid path value");
×
1098
        return NGHTTP2_ERR_CALLBACK_FAILURE;
×
1099
      }
×
1100
#endif /* HAVE_NGHTTP2_CHECK_PATH */
188✔
1101

1102
      auto pathLen = getLengthOfPathWithoutParameters(valueView);
188✔
1103
      query.d_path = valueView.substr(0, pathLen);
188✔
1104
      if (pathLen < valueView.size()) {
188✔
1105
        query.d_queryString = valueView.substr(pathLen);
160✔
1106
      }
160✔
1107
    }
188✔
1108
    else if (headerMatches(s_authorityHeaderName)) {
1,148✔
1109
      query.d_host = valueView;
188✔
1110
    }
188✔
1111
    else if (headerMatches(s_schemeHeaderName)) {
960✔
1112
      query.d_scheme = valueView;
188✔
1113
    }
188✔
1114
    else if (headerMatches(s_methodHeaderName)) {
772✔
1115
#if defined(HAVE_NGHTTP2_CHECK_METHOD)
188✔
1116
      if (nghttp2_check_method(value, valuelen) == 0) {
188!
1117
        vinfolog("Invalid method value");
×
1118
        return NGHTTP2_ERR_CALLBACK_FAILURE;
×
1119
      }
×
1120
#endif /* HAVE_NGHTTP2_CHECK_METHOD */
188✔
1121

1122
      if (valueView == "GET") {
188✔
1123
        query.d_method = PendingQuery::Method::Get;
164✔
1124
      }
164✔
1125
      else if (valueView == "POST") {
24✔
1126
        query.d_method = PendingQuery::Method::Post;
22✔
1127
      }
22✔
1128
      else {
2✔
1129
        query.d_method = PendingQuery::Method::Unsupported;
2✔
1130
        vinfolog("Unsupported method value");
2!
1131
        return 0;
2✔
1132
      }
2✔
1133
    }
188✔
1134

1135
    if (conn->d_ci.cs->dohFrontend->d_keepIncomingHeaders || (conn->d_ci.cs->dohFrontend->d_trustForwardedForHeader && headerMatches(s_xForwardedForHeaderName))) {
1,334✔
1136
      if (!query.d_headers) {
487✔
1137
        query.d_headers = std::make_unique<HeadersMap>();
72✔
1138
      }
72✔
1139
      // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): nghttp2 API
1140
      query.d_headers->insert({std::string(reinterpret_cast<const char*>(name), nameLen), std::string(valueView)});
487✔
1141
    }
487✔
1142
  }
1,334✔
1143
  return 0;
1,334✔
1144
}
1,336✔
1145

1146
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)
1147
{
26✔
1148
  (void)session;
26✔
1149
  (void)flags;
26✔
1150
  auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
26✔
1151
  auto stream = conn->getStreamContext(stream_id);
26✔
1152
  if (stream == conn->d_currentStreams.end()) {
26!
1153
    vinfolog("Unable to match the stream ID %d to a known one!", stream_id);
×
1154
    return NGHTTP2_ERR_CALLBACK_FAILURE;
×
1155
  }
×
1156
  if (len > std::numeric_limits<uint16_t>::max() || (std::numeric_limits<uint16_t>::max() - stream->second.d_buffer.size()) < len) {
26!
1157
    vinfolog("Data frame of size %d is too large for a DNS query (we already have %d)", len, stream->second.d_buffer.size());
×
1158
    return NGHTTP2_ERR_CALLBACK_FAILURE;
×
1159
  }
×
1160

1161
  // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic): nghttp2 API
1162
  stream->second.d_buffer.insert(stream->second.d_buffer.end(), data, data + len);
26✔
1163

1164
  return 0;
26✔
1165
}
26✔
1166

1167
int IncomingHTTP2Connection::on_error_callback(nghttp2_session* session, int lib_error_code, const char* msg, size_t len, void* user_data)
1168
{
2✔
1169
  (void)session;
2✔
1170
  auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
2✔
1171

1172
  vinfolog("Error in HTTP/2 connection from %s: %s (%d)", conn->d_ci.remote.toStringWithPort(), std::string(msg, len), lib_error_code);
2!
1173
  conn->d_connectionClosing = true;
2✔
1174
  conn->d_needFlush = true;
2✔
1175
  nghttp2_session_terminate_session(conn->d_session.get(), NGHTTP2_NO_ERROR);
2✔
1176

1177
  return 0;
2✔
1178
}
2✔
1179

1180
IOState IncomingHTTP2Connection::readHTTPData()
1181
{
1,142✔
1182
  if (d_inReadFunction) {
1,142!
1183
    return IOState::Done;
×
1184
  }
×
1185
  ReadFunctionGuard readGuard(d_inReadFunction);
1,142✔
1186

1187
  IOState newState = IOState::Done;
1,142✔
1188
  size_t got = 0;
1,142✔
1189
  if (d_in.size() < s_initialReceiveBufferSize) {
1,142✔
1190
    d_in.resize(std::max(s_initialReceiveBufferSize, d_in.capacity()));
988✔
1191
  }
988✔
1192
  try {
1,142✔
1193
    newState = d_handler.tryRead(d_in, got, d_in.size(), true);
1,142✔
1194
    d_in.resize(got);
1,142✔
1195

1196
    if (got > 0) {
1,142✔
1197
      /* we got something */
1198
      auto readlen = nghttp2_session_mem_recv(d_session.get(), d_in.data(), d_in.size());
689✔
1199
      /* as long as we don't require a pause by returning nghttp2_error.NGHTTP2_ERR_PAUSE from a CB,
1200
         all data should be consumed before returning */
1201
      if (readlen < 0 || static_cast<size_t>(readlen) < d_in.size()) {
689!
1202
        throw std::runtime_error("Fatal error while passing received data to nghttp2: " + std::string(nghttp2_strerror((int)readlen)));
5✔
1203
      }
5✔
1204

1205
      nghttp2_session_send(d_session.get());
684✔
1206
    }
684✔
1207
  }
1,142✔
1208
  catch (const std::exception& e) {
1,142✔
1209
    vinfolog("Exception while trying to read from HTTP client connection to %s: %s", d_ci.remote.toStringWithPort(), e.what());
178✔
1210
    handleIOError();
178✔
1211
    return IOState::Done;
178✔
1212
  }
178✔
1213
  return newState;
964✔
1214
}
1,142✔
1215

1216
void IncomingHTTP2Connection::handleReadableIOCallback([[maybe_unused]] int descriptor, FDMultiplexer::funcparam_t& param)
1217
{
383✔
1218
  auto conn = boost::any_cast<std::shared_ptr<IncomingHTTP2Connection>>(param);
383✔
1219
  conn->handleIO();
383✔
1220
}
383✔
1221

1222
void IncomingHTTP2Connection::handleWritableIOCallback([[maybe_unused]] int descriptor, FDMultiplexer::funcparam_t& param)
1223
{
2✔
1224
  auto conn = boost::any_cast<std::shared_ptr<IncomingHTTP2Connection>>(param);
2✔
1225
  conn->writeToSocket(true);
2✔
1226
}
2✔
1227

1228
void IncomingHTTP2Connection::stopIO()
1229
{
397✔
1230
  if (d_ioState) {
397✔
1231
    d_ioState->reset();
395✔
1232
  }
395✔
1233
}
397✔
1234

1235
uint32_t IncomingHTTP2Connection::getConcurrentStreamsCount() const
1236
{
191✔
1237
  return d_currentStreams.size() + d_killedStreams.size();
191✔
1238
}
191✔
1239

1240
boost::optional<struct timeval> IncomingHTTP2Connection::getIdleClientReadTTD(struct timeval now) const
1241
{
559✔
1242
  const auto& currentConfig = dnsdist::configuration::getCurrentRuntimeConfiguration();
559✔
1243
  auto idleTimeout = d_ci.cs->dohFrontend->d_idleTimeout;
559✔
1244
  if (currentConfig.d_maxTCPConnectionDuration == 0 && idleTimeout == 0) {
559!
1245
    return boost::none;
×
1246
  }
×
1247

1248
  if (currentConfig.d_maxTCPConnectionDuration > 0) {
559!
1249
    auto elapsed = now.tv_sec - d_connectionStartTime.tv_sec;
×
1250
    if (elapsed < 0 || (static_cast<size_t>(elapsed) >= currentConfig.d_maxTCPConnectionDuration)) {
×
1251
      return now;
×
1252
    }
×
1253
    auto remaining = currentConfig.d_maxTCPConnectionDuration - elapsed;
×
1254
    if (idleTimeout == 0 || remaining <= static_cast<size_t>(idleTimeout)) {
×
1255
      now.tv_sec += static_cast<time_t>(remaining);
×
1256
      return now;
×
1257
    }
×
1258
  }
×
1259

1260
  now.tv_sec += idleTimeout;
559✔
1261
  return now;
559✔
1262
}
559✔
1263

1264
void IncomingHTTP2Connection::updateIO(IOState newState, const timeval& now)
1265
{
147✔
1266
  (void)now;
147✔
1267
  updateIO(newState, newState == IOState::NeedWrite ? handleWritableIOCallback : handleReadableIOCallback);
147!
1268
}
147✔
1269

1270
void IncomingHTTP2Connection::updateIO(IOState newState, const FDMultiplexer::callbackfunc_t& callback)
1271
{
1,022✔
1272
  boost::optional<struct timeval> ttd{boost::none};
1,022✔
1273

1274
  if (newState == IOState::Async) {
1,022!
1275
    auto shared = shared_from_this();
×
1276
    updateIOForAsync(shared);
×
1277
    return;
×
1278
  }
×
1279

1280
  auto shared = std::dynamic_pointer_cast<IncomingHTTP2Connection>(shared_from_this());
1,022✔
1281
  if (!shared || !d_ioState) {
1,022!
1282
    return;
×
1283
  }
×
1284

1285
  timeval now{};
1,022✔
1286
  gettimeofday(&now, nullptr);
1,022✔
1287

1288
  if (newState == IOState::NeedRead) {
1,022✔
1289
    /* use the idle TTL if the handshake has been completed (and proxy protocol payload received, if any),
1290
       and we have processed at least one query, otherwise we use the shorter read TTL  */
1291
    if ((d_state == State::waitingForQuery || d_state == State::idle) && (d_queriesCount > 0 || d_currentQueriesCount > 0)) {
1,015!
1292
      ttd = getIdleClientReadTTD(now);
559✔
1293
    }
559✔
1294
    else {
456✔
1295
      ttd = getClientReadTTD(now);
456✔
1296
    }
456✔
1297
    d_ioState->update(newState, callback, shared, ttd);
1,015✔
1298
  }
1,015✔
1299
  else if (newState == IOState::NeedWrite) {
7✔
1300
    ttd = getClientWriteTTD(now);
4✔
1301
    d_ioState->update(newState, callback, shared, ttd);
4✔
1302
  }
4✔
1303
  else if (newState == IOState::Done) {
3!
1304
    d_ioState->reset();
3✔
1305
  }
3✔
1306
}
1,022✔
1307

1308
void IncomingHTTP2Connection::handleIOError()
1309
{
188✔
1310
  d_connectionDied = true;
188✔
1311
  d_out.clear();
188✔
1312
  d_outPos = 0;
188✔
1313
  nghttp2_session_terminate_session(d_session.get(), NGHTTP2_PROTOCOL_ERROR);
188✔
1314
  d_currentStreams.clear();
188✔
1315
  d_killedStreams.clear();
188✔
1316
  stopIO();
188✔
1317
}
188✔
1318

1319
bool IncomingHTTP2Connection::active() const
1320
{
3,492✔
1321
  return !d_connectionDied && d_ioState != nullptr;
3,492!
1322
}
3,492✔
1323

1324
#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