Coverage Report

Created: 2026-10-28 06:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/httrack/src/htsdns_selftest.c
Line
Count
Source
1
/* ------------------------------------------------------------ */
2
/*
3
HTTrack Website Copier, Offline Browser for Windows and Unix
4
Copyright (C) 2026 Xavier Roche and other contributors
5
6
SPDX-License-Identifier: GPL-3.0-or-later
7
8
This program is free software: you can redistribute it and/or modify
9
it under the terms of the GNU General Public License as published by
10
the Free Software Foundation, either version 3 of the License, or
11
(at your option) any later version.
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, see <http://www.gnu.org/licenses/>.
20
21
Ethical use: we kindly ask that you NOT use this software to harvest email
22
addresses or to collect any other private information about people. Doing so
23
would dishonor our work and waste the many hours we have spent on it.
24
25
Please visit our Website: http://www.httrack.com
26
*/
27
28
/* ------------------------------------------------------------ */
29
/* File: htsdns_selftest.c subroutines:                         */
30
/*       in-process self-test for the DNS resolver and cache    */
31
/* Author: Xavier Roche                                         */
32
/* ------------------------------------------------------------ */
33
34
/* Routes the resolver through a scripted getaddrinfo (hts_resolver_backend)
35
   instead of the network, so resolution and the DNS cache are testable for a
36
   fixed set of scenarios (IPv4/IPv6/dual-stack, errors, family filter,
37
   cache reuse) with no live DNS. */
38
39
#define HTS_INTERNAL_BYTECODE
40
41
#include "htsdns_selftest.h"
42
43
#include "htsselftest_int.h"
44
45
#include <setjmp.h>
46
#include <stdio.h>
47
#include <string.h>
48
#include <time.h>
49
50
#if HTS_INET6 != 0
51
52
/* IPV6_resolver: 0 = v4+v6, 1 = v4 only, 2 = v6 only (htscoremain -@i). */
53
extern int IPV6_resolver;
54
55
/* One scripted host: either a getaddrinfo error, or an ordered address list. */
56
typedef struct mock_addr {
57
  int family;             /* AF_INET / AF_INET6 */
58
  unsigned char addr[16]; /* 4 (v4) or 16 (v6) meaningful bytes */
59
} mock_addr;
60
61
typedef struct mock_host {
62
  const char *name;
63
  int gai_err; /* non-zero: getaddrinfo returns this */
64
  int naddr;
65
  mock_addr addr[6];
66
  int calls;   /* times the backend resolved this host */
67
  int slow_ms; /* non-zero: block this long, as a black-hole resolver would */
68
} mock_host;
69
70
/* Long enough to outlast the 1s --timeout the bounded resolve is checked
71
   against, short enough to keep the self-test quick. The elapsed bounds below
72
   sit halfway between a prompt return and this, so they are measured on
73
   mtime_monotonic(), because an NTP correction mid-test would otherwise eat
74
   most of that margin. */
75
0
#define MOCK_SLOW_MS 3000
76
77
static mock_host mock_hosts[] = {
78
    {"v4only.test", 0, 1, {{AF_INET, {1, 2, 3, 4}}}, 0},
79
    {"v6only.test", 0, 1, {{AF_INET6, {0x20, 0x01, 0x0d, 0xb8, [15] = 1}}}, 0},
80
    /* dual stack, IPv6 first (RFC 6724 order) then IPv4 */
81
    {"dual.test",
82
     0,
83
     2,
84
     {{AF_INET6, {0x20, 0x01, 0x0d, 0xb8, [15] = 2}}, {AF_INET, {5, 6, 7, 8}}},
85
     0},
86
    /* dual stack, IPv4 first: distinguishes "keep the first address" from
87
       "prefer a family", so the selection contract is actually pinned. */
88
    {"dual4.test",
89
     0,
90
     2,
91
     {{AF_INET, {9, 10, 11, 12}},
92
      {AF_INET6, {0x20, 0x01, 0x0d, 0xb8, [15] = 3}}},
93
     0},
94
    /* more addresses than HTS_MAXADDRNUM: the list must clamp to the cap. */
95
    {"many.test",
96
     0,
97
     6,
98
     {{AF_INET, {10, 0, 0, 1}},
99
      {AF_INET, {10, 0, 0, 2}},
100
      {AF_INET, {10, 0, 0, 3}},
101
      {AF_INET, {10, 0, 0, 4}},
102
      {AF_INET, {10, 0, 0, 5}},
103
      {AF_INET, {10, 0, 0, 6}}},
104
     0},
105
    {"nodns.test", EAI_NONAME, 0, {{0}}, 0},
106
    /* the outage case: the resolver cannot answer, until the test says it can
107
       (EAI_AGAIN, not EAI_NONAME, which is an answer about the name) */
108
    {"flaky.test", EAI_AGAIN, 1, {{AF_INET, {192, 0, 2, 7}}}, 0},
109
    /* the control: the resolver answers, and the answer is "no such host" */
110
    {"dead.test", EAI_NONAME, 0, {{0}}, 0},
111
    /* the same outage, seen across a backward clock step */
112
    {"stepped.test", EAI_AGAIN, 1, {{AF_INET, {192, 0, 2, 8}}}, 0},
113
    /* resolves, but only well after --timeout: the #606 wedge */
114
    {"slow.test", 0, 1, {{AF_INET, {127, 0, 0, 9}}}, 0, MOCK_SLOW_MS},
115
    /* a second one, so a cancelled resolve cannot cache an answer the next
116
       case then reads instead of asking for */
117
    {"slow2.test", 0, 1, {{AF_INET, {127, 0, 0, 10}}}, 0, MOCK_SLOW_MS},
118
    /* two more for the cached entry point, checked with the mirror running and
119
       then stopped: neither case may read the other's cached answer */
120
    {"slow3.test", 0, 1, {{AF_INET, {127, 0, 0, 11}}}, 0, MOCK_SLOW_MS},
121
    {"slow4.test", 0, 1, {{AF_INET, {127, 0, 0, 12}}}, 0, MOCK_SLOW_MS},
122
    /* answers at once, but the backend below never lets the worker say so */
123
    {"cut.test", 0, 1, {{AF_INET, {127, 0, 0, 13}}}, 0},
124
};
125
126
/* Serializes mock_host bookkeeping: a timed-out resolve is abandoned, so its
127
   worker is still inside the backend while the test reads the counters. */
128
static htsmutex mock_lock = HTSMUTEX_INIT;
129
static int mock_finished = 0; /* backend calls that have returned */
130
/* Uses of the backend that can no longer read hts_resolver: a worker reads it
131
   again to free its answer, long after getaddrinfo() returned, so that is what
132
   a swap has to wait for. */
133
static int mock_released = 0;
134
135
0
static mock_host *mock_find(const char *name) {
136
0
  for (size_t i = 0; i < sizeof(mock_hosts) / sizeof(mock_hosts[0]); i++) {
137
0
    if (strcmp(mock_hosts[i].name, name) == 0)
138
0
      return &mock_hosts[i];
139
0
  }
140
0
  return NULL;
141
0
}
142
143
0
static void mock_reset_calls(void) {
144
0
  hts_mutexlock(&mock_lock);
145
0
  for (size_t i = 0; i < sizeof(mock_hosts) / sizeof(mock_hosts[0]); i++)
146
0
    mock_hosts[i].calls = 0;
147
0
  mock_finished = 0;
148
0
  mock_released = 0;
149
0
  hts_mutexrelease(&mock_lock);
150
0
}
151
152
0
static int mock_read_calls(const char *name) {
153
0
  int calls;
154
155
0
  hts_mutexlock(&mock_lock);
156
0
  calls = mock_find(name)->calls;
157
0
  hts_mutexrelease(&mock_lock);
158
0
  return calls;
159
0
}
160
161
/* Backend calls that have returned, ordering their writes against ours. */
162
0
static int mock_read_finished(void) {
163
0
  int n;
164
165
0
  hts_mutexlock(&mock_lock);
166
0
  n = mock_finished;
167
0
  hts_mutexrelease(&mock_lock);
168
0
  return n;
169
0
}
170
171
/* Wait for n uses of the backend to be over, ordering their writes against
172
   ours: what makes installing another backend safe. */
173
0
static void mock_wait_released(int n) {
174
0
  for (;;) {
175
0
    hts_boolean done;
176
177
0
    hts_mutexlock(&mock_lock);
178
0
    done = (mock_released >= n) ? HTS_TRUE : HTS_FALSE;
179
0
    hts_mutexrelease(&mock_lock);
180
0
    if (done)
181
0
      break;
182
0
    Sleep(10);
183
0
  }
184
0
}
185
186
/* Build one addrinfo node owning its sockaddr (freed by mock_freeaddrinfo). */
187
0
static struct addrinfo *mock_mkai(const mock_addr *a) {
188
0
  struct addrinfo *ai = calloct(1, sizeof(*ai));
189
190
0
  ai->ai_family = a->family;
191
0
  if (a->family == AF_INET) {
192
0
    struct sockaddr_in *sin = calloct(1, sizeof(*sin));
193
194
0
    sin->sin_family = AF_INET;
195
0
    memcpy(&sin->sin_addr, a->addr, 4);
196
0
    ai->ai_addr = (struct sockaddr *) sin;
197
0
    ai->ai_addrlen = sizeof(*sin);
198
0
  } else {
199
0
    struct sockaddr_in6 *sin6 = calloct(1, sizeof(*sin6));
200
201
0
    sin6->sin6_family = AF_INET6;
202
0
    memcpy(&sin6->sin6_addr, a->addr, 16);
203
0
    ai->ai_addr = (struct sockaddr *) sin6;
204
0
    ai->ai_addrlen = sizeof(*sin6);
205
0
  }
206
0
  return ai;
207
0
}
208
209
static int HTS_RESOLVER_CALL mock_getaddrinfo_(const char *node,
210
                                               const char *service,
211
                                               const struct addrinfo *hints,
212
0
                                               struct addrinfo **res) {
213
0
  mock_host *const h = mock_find(node);
214
0
  const int want = (hints != NULL) ? hints->ai_family : PF_UNSPEC;
215
0
  struct addrinfo *head = NULL, *tail = NULL;
216
217
0
  (void) service;
218
0
  *res = NULL;
219
0
  if (h == NULL)
220
0
    return EAI_NONAME;
221
0
  hts_mutexlock(&mock_lock);
222
0
  h->calls++; /* a real backend hit; a cached host skips this */
223
0
  hts_mutexrelease(&mock_lock);
224
0
  if (h->slow_ms != 0)
225
0
    Sleep(h->slow_ms);
226
0
  if (h->gai_err != 0)
227
0
    return h->gai_err;
228
0
  for (int i = 0; i < h->naddr; i++) {
229
0
    if (want != PF_UNSPEC && want != h->addr[i].family)
230
0
      continue; /* honor the requested family (v4/v6 only) */
231
0
    struct addrinfo *const ai = mock_mkai(&h->addr[i]);
232
233
0
    if (head == NULL)
234
0
      head = ai;
235
0
    else
236
0
      tail->ai_next = ai;
237
0
    tail = ai;
238
0
  }
239
0
  if (head == NULL)
240
0
    return EAI_NONAME; /* filtered to empty, as the libc resolver does */
241
0
  *res = head;
242
0
  return 0;
243
0
}
244
245
static int HTS_RESOLVER_CALL mock_getaddrinfo(const char *node,
246
                                              const char *service,
247
                                              const struct addrinfo *hints,
248
0
                                              struct addrinfo **res) {
249
0
  const int ret = mock_getaddrinfo_(node, service, hints, res);
250
251
0
  hts_mutexlock(&mock_lock);
252
0
  mock_finished++;
253
0
  if (*res == NULL) /* nothing to free, so this caller is done with us */
254
0
    mock_released++;
255
0
  hts_mutexrelease(&mock_lock);
256
0
  return ret;
257
0
}
258
259
0
static void HTS_RESOLVER_CALL mock_freeaddrinfo(struct addrinfo *res) {
260
0
  hts_mutexlock(&mock_lock);
261
0
  mock_released++;
262
0
  hts_mutexrelease(&mock_lock);
263
0
  while (res != NULL) {
264
0
    struct addrinfo *const next = res->ai_next;
265
266
0
    freet(res->ai_addr);
267
0
    freet(res);
268
0
    res = next;
269
0
  }
270
0
}
271
272
static const hts_resolver_backend mock_backend = {mock_getaddrinfo,
273
                                                  mock_freeaddrinfo};
274
275
/* Where a front end's fault recovery lands, see hts_set_thread_runner(). */
276
static jmp_buf dns_cut_jmp;
277
278
/* Counts the call, then leaves the worker body the way a recovered fault does.
279
   It frees what it asked for, because nothing downstream will. */
280
static int HTS_RESOLVER_CALL cut_getaddrinfo(const char *node,
281
                                             const char *service,
282
                                             const struct addrinfo *hints,
283
0
                                             struct addrinfo **res) {
284
0
  if (mock_getaddrinfo(node, service, hints, res) == 0)
285
0
    mock_freeaddrinfo(*res);
286
0
  *res = NULL;
287
0
  longjmp(dns_cut_jmp, 1);
288
0
  return EAI_NONAME; /* not reached */
289
0
}
290
291
static const hts_resolver_backend cut_backend = {cut_getaddrinfo,
292
                                                 mock_freeaddrinfo};
293
294
0
static void dns_cut_runner(void (*fun)(void *arg), void *arg) {
295
0
  if (setjmp(dns_cut_jmp) == 0)
296
0
    fun(arg);
297
0
}
298
299
static int failures = 0;
300
301
#define CHECK(cond)                                                            \
302
0
  do {                                                                         \
303
0
    if (!(cond)) {                                                             \
304
0
      failures++;                                                              \
305
0
      fprintf(stderr, "dns-selftest: FAIL at %s:%d: %s\n", __FILE__, __LINE__, \
306
0
              #cond);                                                          \
307
0
    }                                                                          \
308
0
  } while (0)
309
310
/* Resolve via the uncached entry point; return the address family, or
311
   AF_UNSPEC if the host did not resolve. */
312
0
static int resolve_family_nocache(const char *host) {
313
0
  SOCaddr addr;
314
0
  const char *err = NULL;
315
316
0
  if (hts_dns_resolve_nocache2(host, &addr, &err) == NULL)
317
0
    return AF_UNSPEC;
318
0
  return SOCaddr_sinfamily(addr);
319
0
}
320
321
0
int dns_selftests(httrackp *opt) {
322
0
  failures = 0;
323
0
  hts_dns_set_resolver_backend(&mock_backend);
324
325
  /* IPv4-only / IPv6-only hosts map to the right family. */
326
0
  IPV6_resolver = 0;
327
0
  CHECK(resolve_family_nocache("v4only.test") == AF_INET);
328
0
  CHECK(resolve_family_nocache("v6only.test") == AF_INET6);
329
330
  /* Dual-stack: the single-address API returns the *first* resolved address.
331
     Both orderings pin selection by position, not a family preference. The
332
     multi-address API (resolve_all, below) exposes the whole list. */
333
0
  CHECK(resolve_family_nocache("dual.test") == AF_INET6); /* v6 listed first */
334
0
  CHECK(resolve_family_nocache("dual4.test") == AF_INET); /* v4 listed first */
335
336
  /* Unknown host does not resolve. */
337
0
  CHECK(resolve_family_nocache("nodns.test") == AF_UNSPEC);
338
339
  /* Family filter (-@i4 / -@i6) selects v4 / v6 out of the dual-stack host. */
340
0
  IPV6_resolver = 1;
341
0
  CHECK(resolve_family_nocache("dual.test") == AF_INET);
342
0
  IPV6_resolver = 2;
343
0
  CHECK(resolve_family_nocache("dual.test") == AF_INET6);
344
0
  IPV6_resolver = 0;
345
346
  /* Cached driver resolves a host once and reuses the *same* address. */
347
0
  mock_reset_calls();
348
0
  {
349
0
    SOCaddr a1, a2;
350
0
    char ip1[64], ip2[64];
351
0
    const char *err = NULL;
352
353
0
    CHECK(hts_dns_resolve2(opt, "v4only.test", &a1, &err) != NULL);
354
0
    CHECK(hts_dns_resolve2(opt, "v4only.test", &a2, &err) != NULL);
355
0
    CHECK(mock_find("v4only.test")->calls == 1);
356
    /* the cache returns the right address, not merely a hit for the key */
357
0
    SOCaddr_inetntoa(ip1, sizeof(ip1), a1);
358
0
    SOCaddr_inetntoa(ip2, sizeof(ip2), a2);
359
0
    CHECK(strcmp(ip1, "1.2.3.4") == 0);
360
0
    CHECK(strcmp(ip1, ip2) == 0);
361
0
  }
362
363
  /* A negative result is cached too: a second lookup does not re-resolve. */
364
0
  {
365
0
    SOCaddr a1, a2;
366
0
    const char *err = NULL;
367
368
0
    CHECK(hts_dns_resolve2(opt, "nodns.test", &a1, &err) == NULL);
369
0
    CHECK(hts_dns_resolve2(opt, "nodns.test", &a2, &err) == NULL);
370
0
    CHECK(mock_find("nodns.test")->calls == 1); /* resolved once, then cached */
371
0
  }
372
373
  /* But a failure the resolver could not answer expires: one outage must not
374
     kill the whole crawl. The wait is minutes long and the test moves the clock
375
     rather than sleeping, so no stall expires an entry a check needs. */
376
0
  mock_reset_calls();
377
0
  hts_dns_set_negative_ttl_ms(100000);
378
0
  {
379
0
    SOCaddr a;
380
0
    const char *err = NULL;
381
382
0
    CHECK(hts_dns_resolve2(opt, "flaky.test", &a, &err) == NULL);
383
0
    CHECK(hts_dns_negative_failures(opt, "flaky.test") == 1);
384
0
    CHECK(hts_dns_resolve2(opt, "flaky.test", &a, &err) == NULL);
385
0
    CHECK(mock_read_calls("flaky.test") == 1); /* still inside the wait */
386
0
    hts_dns_test_move_clock(opt, "flaky.test", 150000);
387
    /* asking again after the wait counts, and lengthens the next one */
388
0
    CHECK(hts_dns_resolve2(opt, "flaky.test", &a, &err) == NULL);
389
0
    CHECK(mock_read_calls("flaky.test") == 2);
390
0
    CHECK(hts_dns_negative_failures(opt, "flaky.test") == 2);
391
    /* that next one is twice as long, so the same move no longer reaches it */
392
0
    hts_dns_test_move_clock(opt, "flaky.test", 150000);
393
0
    CHECK(hts_dns_resolve2(opt, "flaky.test", &a, &err) == NULL);
394
0
    CHECK(mock_read_calls("flaky.test") == 2);
395
0
    mock_find("flaky.test")->gai_err = 0; /* the network is back */
396
0
    hts_dns_test_move_clock(opt, "flaky.test", 100000); /* now past it */
397
0
    CHECK(hts_dns_resolve2(opt, "flaky.test", &a, &err) != NULL);
398
0
    CHECK(mock_read_calls("flaky.test") == 3);
399
0
    CHECK(hts_dns_negative_failures(opt, "flaky.test") == 0);
400
0
    hts_dns_test_move_clock(opt, "flaky.test", 1000000);
401
0
    CHECK(hts_dns_resolve2(opt, "flaky.test", &a, &err) != NULL);
402
0
    CHECK(mock_read_calls("flaky.test") == 3); /* the answer does not expire */
403
0
  }
404
405
  /* The stamps are wall-clock, there being no monotonic clock here: a backward
406
     step must expire them too, or it delays the retry by the size of the step,
407
     which at the ceiling means the rest of the crawl. */
408
0
  mock_reset_calls();
409
0
  {
410
0
    SOCaddr a;
411
0
    const char *err = NULL;
412
413
0
    CHECK(hts_dns_resolve2(opt, "stepped.test", &a, &err) == NULL);
414
0
    CHECK(hts_dns_resolve2(opt, "stepped.test", &a, &err) == NULL);
415
0
    CHECK(mock_read_calls("stepped.test") == 1); /* still inside the wait */
416
0
    mock_find("stepped.test")->gai_err = 0;      /* the network is back */
417
0
    hts_dns_test_move_clock(opt, "stepped.test", -600000); /* NTP steps back */
418
0
    CHECK(hts_dns_resolve2(opt, "stepped.test", &a, &err) != NULL);
419
0
    CHECK(mock_read_calls("stepped.test") == 2);
420
0
    mock_find("stepped.test")->gai_err = EAI_AGAIN; /* leave the row as found */
421
0
  }
422
423
  /* The wait doubles per consecutive failure, up to the ceiling. Table-tested
424
     rather than slept through: a resolver Android maps every failure onto,
425
     NXDOMAIN included, would otherwise re-ask a dead host all crawl. */
426
0
  {
427
0
    const int first = 1000;
428
429
0
    hts_dns_set_negative_ttl_ms(first);
430
0
    CHECK(hts_dns_negative_wait_ms(1) == first);
431
0
    CHECK(hts_dns_negative_wait_ms(2) == 2 * first);
432
0
    CHECK(hts_dns_negative_wait_ms(3) == 4 * first);
433
0
    CHECK(hts_dns_negative_wait_ms(99) == HTS_DNS_NEGATIVE_TTL_MAX_MS);
434
0
    hts_dns_set_negative_ttl_ms(500);
435
0
  }
436
437
  /* "no such host" is an answer about the name, so it stands for the crawl. */
438
0
  {
439
0
    SOCaddr a;
440
0
    const char *err = NULL;
441
442
0
    hts_dns_set_negative_ttl_ms(200);
443
0
    CHECK(hts_dns_resolve2(opt, "dead.test", &a, &err) == NULL);
444
0
    Sleep(300);
445
0
    CHECK(hts_dns_resolve2(opt, "dead.test", &a, &err) == NULL);
446
0
    CHECK(mock_read_calls("dead.test") == 1);
447
0
  }
448
0
  hts_dns_set_negative_ttl_ms(HTS_DNS_NEGATIVE_TTL_MS);
449
0
  mock_find("flaky.test")->gai_err = EAI_AGAIN; /* leave the row as found */
450
451
  /* Multi-address resolution: count and order are the connect-fallback
452
     contract. A dead first address is retried against the next, so both must be
453
     exact. */
454
0
  mock_reset_calls();
455
0
  {
456
0
    SOCaddr addrs[HTS_MAXADDRNUM];
457
0
    char ip[64];
458
0
    const char *err = NULL;
459
460
    /* dual-stack, in resolver order: [0]=v6, [1]=v4 */
461
0
    CHECK(hts_dns_resolve_all(opt, "dual.test", addrs, HTS_MAXADDRNUM, &err) ==
462
0
          2);
463
0
    CHECK(SOCaddr_sinfamily(addrs[0]) == AF_INET6);
464
0
    CHECK(SOCaddr_sinfamily(addrs[1]) == AF_INET);
465
0
    SOCaddr_inetntoa(ip, sizeof(ip), addrs[1]);
466
0
    CHECK(strcmp(ip, "5.6.7.8") == 0);
467
0
    CHECK(mock_find("dual.test")->calls ==
468
0
          1); /* one backend hit for the list */
469
470
    /* single-address host: count 1 */
471
0
    CHECK(hts_dns_resolve_all(opt, "v4only.test", addrs, HTS_MAXADDRNUM,
472
0
                              &err) == 1);
473
0
    SOCaddr_inetntoa(ip, sizeof(ip), addrs[0]);
474
0
    CHECK(strcmp(ip, "1.2.3.4") == 0);
475
476
    /* does-not-resolve: count 0 (negative), no addresses */
477
0
    CHECK(hts_dns_resolve_all(opt, "nodns.test", addrs, HTS_MAXADDRNUM, &err) ==
478
0
          0);
479
480
    /* more than the cap: the kept list is clamped to HTS_MAXADDRNUM, keeping
481
       the FIRST addresses in resolver order (not some other window) */
482
0
    CHECK(hts_dns_resolve_all(opt, "many.test", addrs, HTS_MAXADDRNUM, &err) ==
483
0
          HTS_MAXADDRNUM);
484
0
    SOCaddr_inetntoa(ip, sizeof(ip), addrs[0]);
485
0
    CHECK(strcmp(ip, "10.0.0.1") == 0);
486
0
    SOCaddr_inetntoa(ip, sizeof(ip), addrs[HTS_MAXADDRNUM - 1]);
487
0
    CHECK(strcmp(ip, "10.0.0.4") == 0);
488
489
    /* family filter still applies through the list path */
490
0
    IPV6_resolver = 1;
491
0
    CHECK(hts_dns_resolve_all(opt, "dual4.test", addrs, HTS_MAXADDRNUM, &err) ==
492
0
          1);
493
0
    CHECK(SOCaddr_sinfamily(addrs[0]) == AF_INET);
494
0
    IPV6_resolver = 0;
495
0
  }
496
497
  /* "host:port" resolves as the bare host: the cache/connect path strips
498
     ":port" before both the backend and the cache key (#181). */
499
0
  mock_reset_calls();
500
0
  {
501
0
    SOCaddr a;
502
0
    char ip[64];
503
0
    const char *err = NULL;
504
505
    /* cache miss: the backend is hit with the stripped host, not "host:port" */
506
0
    CHECK(hts_dns_resolve2(opt, "v6only.test:8080", &a, &err) != NULL);
507
0
    CHECK(mock_find("v6only.test")->calls == 1);
508
    /* the bare host shares that one cache entry (stripped key) */
509
0
    CHECK(hts_dns_resolve2(opt, "v6only.test", &a, &err) != NULL);
510
0
    CHECK(mock_find("v6only.test")->calls == 1);
511
    /* a port variant of an already-cached host also hits, right address */
512
0
    CHECK(hts_dns_resolve2(opt, "v4only.test:1234", &a, &err) != NULL);
513
0
    SOCaddr_inetntoa(ip, sizeof(ip), a);
514
0
    CHECK(strcmp(ip, "1.2.3.4") == 0);
515
0
  }
516
517
  /* newhttp_addr() must connect to the addr_index-th address, not always the
518
     first: this is what back_connect_next relies on to reach the fallback. */
519
0
  {
520
0
    htsblk r;
521
0
    int count = -1;
522
0
    T_SOC s;
523
524
0
    hts_init_htsblk(&r);
525
0
    s = newhttp_addr(opt, "dual.test", &r, 80, 0, 0, &count);
526
0
    CHECK(count == 2);
527
0
    CHECK(SOCaddr_sinfamily(r.address) == AF_INET6); /* index 0 = v6 */
528
0
    if (s != INVALID_SOCKET)
529
0
      deletesoc(s);
530
531
0
    hts_init_htsblk(&r);
532
0
    count = -1;
533
0
    s = newhttp_addr(opt, "dual.test", &r, 80, 0, 1, &count);
534
0
    CHECK(count == 2);
535
0
    CHECK(SOCaddr_sinfamily(r.address) == AF_INET); /* index 1 = v4 */
536
0
    if (s != INVALID_SOCKET)
537
0
      deletesoc(s);
538
539
    /* out-of-range index: no address selected (address stays unset) */
540
0
    hts_init_htsblk(&r);
541
0
    s = newhttp_addr(opt, "dual.test", &r, 80, 0, 2, NULL);
542
0
    CHECK(s == INVALID_SOCKET);
543
0
    if (s != INVALID_SOCKET)
544
0
      deletesoc(s);
545
0
  }
546
547
  /* A URL port outside 1..65535 must refuse the link, not fold into range and
548
     connect elsewhere (#614). *addr_count discriminates: 0 only if refused
549
     before the resolve, still 2 for one merely truncated or defaulted. */
550
0
  {
551
    /* an empty "dual.test:" means the default port (WHATWG, curl): keep it */
552
0
    static const char *const good[] = {"dual.test:1",    "dual.test:80",
553
0
                                       "dual.test:8080", "dual.test:65535",
554
0
                                       "dual.test:080",  "dual.test:"};
555
    /* 65616 and 4294967376 are load-bearing: both wrap to a plausible 80 */
556
0
    static const char *const bad[] = {
557
0
        "dual.test:0",          "dual.test:65536",      "dual.test:65616",
558
0
        "dual.test:99999",      "dual.test:2147483648", "dual.test:4294967296",
559
0
        "dual.test:4294967376", "dual.test:-1",         "dual.test:-23437",
560
0
        "dual.test:80x",        "dual.test:+80",        "dual.test: 80",
561
0
        "dual.test:0x50"};
562
0
    size_t k;
563
564
0
    for (k = 0; k < sizeof(good) / sizeof(good[0]); k++) {
565
0
      htsblk r;
566
0
      int count = -1;
567
0
      T_SOC s;
568
569
0
      hts_init_htsblk(&r);
570
0
      s = newhttp_addr(opt, good[k], &r, -1, 0, 0, &count);
571
0
      CHECK(count == 2); /* accepted: reached the resolve */
572
0
      if (s != INVALID_SOCKET)
573
0
        deletesoc(s);
574
0
    }
575
576
0
    for (k = 0; k < sizeof(bad) / sizeof(bad[0]); k++) {
577
0
      htsblk r;
578
0
      int count = -1;
579
0
      T_SOC s;
580
581
0
      hts_init_htsblk(&r);
582
0
      s = newhttp_addr(opt, bad[k], &r, -1, 0, 0, &count);
583
0
      CHECK(s == INVALID_SOCKET);
584
0
      CHECK(count == 0); /* refused before resolving, not a failed connect */
585
0
      CHECK(strstr(r.msg, "Invalid port") != NULL);
586
0
      if (s != INVALID_SOCKET)
587
0
        deletesoc(s);
588
0
    }
589
0
  }
590
591
  /* Connect-fallback decision (consumer of the multi-address list): when a
592
     stuck connect should abandon the current address for the next one. */
593
0
  {
594
    /* no fallback for the last/only candidate, whatever the elapsed time */
595
0
    CHECK(back_connect_fallback_due(0, 1, 9999, 120) == 0);
596
0
    CHECK(back_connect_fallback_due(1, 2, 9999, 120) == 0);
597
0
    CHECK(back_connect_fallback_due(3, 4, 9999, 120) == 0);
598
    /* fallback available: wait the per-candidate deadline (cap 10s here) */
599
0
    CHECK(back_connect_fallback_due(0, 2, 9, 120) == 0);
600
0
    CHECK(back_connect_fallback_due(0, 2, 10, 120) == 1);
601
0
    CHECK(back_connect_fallback_due(2, 4, 10, 120) == 1);
602
    /* a shorter slot timeout shortens the deadline (min(timeout, cap)) */
603
0
    CHECK(back_connect_fallback_due(0, 2, 4, 5) == 0);
604
0
    CHECK(back_connect_fallback_due(0, 2, 5, 5) == 1);
605
    /* no timeout management: never force a fallback */
606
0
    CHECK(back_connect_fallback_due(0, 2, 9999, 0) == 0);
607
0
  }
608
609
0
  hts_dns_set_resolver_backend(NULL);
610
0
  return failures;
611
0
}
612
613
/* Probes how long acquiring opt->state.lock takes while a resolve is in
614
   flight. hts_has_stopped() takes that same lock and mutates nothing, and the
615
   API promises it stays callable from another thread during a mirror. */
616
typedef struct lock_probe {
617
  httrackp *opt;
618
  htsmutex lock;
619
  hts_boolean done;
620
  TStamp blocked_ms;
621
} lock_probe;
622
623
0
static void lock_probe_thread(void *arg) {
624
0
  lock_probe *const p = (lock_probe *) arg;
625
0
  TStamp start;
626
627
0
  Sleep(MOCK_SLOW_MS / 10); /* let the resolve get under way first */
628
0
  start = mtime_monotonic();
629
0
  (void) hts_has_stopped(p->opt);
630
0
  hts_mutexlock(&p->lock);
631
0
  p->blocked_ms = mtime_monotonic() - start;
632
0
  p->done = HTS_TRUE;
633
0
  hts_mutexrelease(&p->lock);
634
0
}
635
636
0
int dns_timeout_selftests(httrackp *opt) {
637
0
  SOCaddr addrs[HTS_MAXADDRNUM];
638
0
  const char *err = NULL;
639
0
  lock_probe probe;
640
0
  TStamp start, elapsed;
641
0
  int count, finished;
642
643
0
  failures = 0;
644
0
  hts_dns_set_resolver_backend(&mock_backend);
645
0
  IPV6_resolver = 0;
646
0
  mock_reset_calls();
647
0
  opt->timeout = 1; /* the bound under test */
648
649
0
  memset(&probe, 0, sizeof(probe));
650
0
  probe.opt = opt;
651
0
  probe.lock = HTSMUTEX_INIT;
652
0
  hts_mutexinit(&probe.lock);
653
0
  CHECK(hts_newthread(lock_probe_thread, &probe) == 0);
654
655
0
  start = mtime_monotonic();
656
0
  count = hts_dns_resolve_all(opt, "slow.test", addrs, HTS_MAXADDRNUM, &err);
657
0
  elapsed = mtime_monotonic() - start;
658
0
  finished = mock_read_finished();
659
660
  /* the resolve returns on opt->timeout, not when the resolver deigns to
661
     answer: this is what lets --max-time and --timeout fire (#606). A backend
662
     that had not returned proves it, so the bound below need not be tight. */
663
0
  CHECK(finished == 0);
664
  /* midway to MOCK_SLOW_MS: the engine still deadlines on the wall clock, so a
665
     step moves when it fires even though the measurement no longer moves. */
666
0
  CHECK(elapsed < ((TStamp) opt->timeout * 1000 + MOCK_SLOW_MS) / 2);
667
0
  CHECK(count == 0); /* a timeout is reported as "does not resolve" */
668
669
  /* state.lock is not held across the resolve; a concurrent stop query, which
670
     the mirror API promises stays live, is not blocked behind it */
671
0
  for (;;) {
672
0
    hts_boolean done;
673
0
    TStamp blocked;
674
675
0
    hts_mutexlock(&probe.lock);
676
0
    done = probe.done;
677
0
    blocked = probe.blocked_ms;
678
0
    hts_mutexrelease(&probe.lock);
679
0
    if (done) {
680
0
      CHECK(blocked < 500);
681
0
      break;
682
0
    }
683
0
    Sleep(20);
684
0
  }
685
0
  hts_mutexfree(&probe.lock);
686
687
  /* a timeout is not an answer, so it must not be negative-cached: the host is
688
     resolved again rather than written off for the rest of the crawl */
689
0
  CHECK(mock_read_calls("slow.test") == 1);
690
0
  count = hts_dns_resolve_all(opt, "slow.test", addrs, HTS_MAXADDRNUM, &err);
691
0
  CHECK(count == 0);
692
0
  CHECK(mock_read_calls("slow.test") == 2); /* re-resolved, not cached */
693
694
  /* A raised cancel flag ends the wait where no deadline would, which is what
695
     lets a stopped FTP worker leave a black-holed resolver (#1059). */
696
0
  {
697
0
    hts_boolean cancel = HTS_TRUE;
698
699
0
    start = mtime_monotonic();
700
0
    count = hts_dns_resolve_all_bounded(opt, "slow.test", addrs, HTS_MAXADDRNUM,
701
0
                                        0, &cancel, &err);
702
0
    elapsed = mtime_monotonic() - start;
703
0
    CHECK(count == 0);
704
0
    CHECK(elapsed < MOCK_SLOW_MS / 2);
705
0
  }
706
707
  /* And an unbounded resolve nobody cancels still waits for its answer: a
708
     timeout of 0 must not be read back as a deadline of "now". */
709
0
  {
710
0
    hts_boolean cancel = HTS_FALSE;
711
712
0
    start = mtime_monotonic();
713
0
    count = hts_dns_resolve_all_bounded(opt, "slow2.test", addrs,
714
0
                                        HTS_MAXADDRNUM, 0, &cancel, &err);
715
0
    elapsed = mtime_monotonic() - start;
716
0
    CHECK(count == 1);
717
0
    CHECK(elapsed >= MOCK_SLOW_MS / 2);
718
0
  }
719
720
  /* The cached entry point takes the mirror's own stop flag as its cancel, so
721
     a ^C ends a resolve nothing else would bound (#1073). Unbounded here, to
722
     tell the stop apart from a deadline. */
723
0
  opt->timeout = 0;
724
0
  {
725
    /* control first: with the mirror running, the same call still waits */
726
0
    start = mtime_monotonic();
727
0
    count = hts_dns_resolve_all(opt, "slow3.test", addrs, HTS_MAXADDRNUM, &err);
728
0
    elapsed = mtime_monotonic() - start;
729
0
    CHECK(count == 1);
730
0
    CHECK(elapsed >= MOCK_SLOW_MS / 2);
731
732
0
    opt->state.stop = 1; /* the flag, not hts_request_stop's log line */
733
0
    start = mtime_monotonic();
734
0
    count = hts_dns_resolve_all(opt, "slow4.test", addrs, HTS_MAXADDRNUM, &err);
735
0
    elapsed = mtime_monotonic() - start;
736
0
    opt->state.stop = 0;
737
0
    CHECK(count == 0);
738
0
    CHECK(elapsed < MOCK_SLOW_MS / 2);
739
0
  }
740
741
  /* Four of the six resolves were abandoned mid-backend; wait for their
742
     workers to be out of it, freed answer included, before swapping it. */
743
0
  mock_wait_released(6);
744
745
  /* A worker a recovered fault cut short hands its verdict back at once. Left
746
     to the caller, the wait runs to the deadline, and an unbounded one runs
747
     until the user stops the mirror. */
748
0
  {
749
0
    hts_boolean cancel = HTS_FALSE;
750
751
0
    mock_reset_calls();
752
0
    hts_dns_set_resolver_backend(&cut_backend);
753
0
    CHECK(hts_set_thread_runner(dns_cut_runner) == NULL);
754
0
    start = mtime_monotonic();
755
0
    count = hts_dns_resolve_all_bounded(opt, "cut.test", addrs, HTS_MAXADDRNUM,
756
0
                                        MOCK_SLOW_MS / 1000, &cancel, &err);
757
0
    elapsed = mtime_monotonic() - start;
758
0
    hts_set_thread_runner(NULL);
759
    /* the cut worker longjmps out of the backend after freeing its answer, so
760
       it too has to be out before the swap below */
761
0
    mock_wait_released(1);
762
0
    hts_dns_set_resolver_backend(&mock_backend);
763
0
    CHECK(count == 0);
764
0
    CHECK(elapsed < MOCK_SLOW_MS / 2); /* the tail answered, not the deadline */
765
0
    CHECK(mock_read_calls("cut.test") == 1);
766
    /* no answer was had, so nothing may be cached in its place */
767
0
    count = hts_dns_resolve_all_bounded(opt, "cut.test", addrs, HTS_MAXADDRNUM,
768
0
                                        0, &cancel, &err);
769
0
    CHECK(count == 1);
770
0
    CHECK(mock_read_calls("cut.test") == 2);
771
    /* and the mirror gives up on it, see back_checkmirror() */
772
0
    CHECK(hts_worker_faulted());
773
0
    hts_worker_fault_clear();
774
0
  }
775
0
  return failures;
776
0
}
777
778
#else
779
780
int dns_selftests(httrackp *opt) {
781
  (void) opt;
782
  return 0; /* resolver seam only exists in the IPv6 build */
783
}
784
785
int dns_timeout_selftests(httrackp *opt) {
786
  (void) opt;
787
  return 0;
788
}
789
790
#endif
791
792
0
static int st_dns(httrackp *opt, int argc, char **argv) {
793
0
  const int err = dns_selftests(opt);
794
795
0
  (void) argc;
796
0
  (void) argv;
797
0
  printf("dns-selftest: %s\n", err ? "FAIL" : "OK");
798
0
  return err;
799
0
}
800
801
0
static int st_dnstimeout(httrackp *opt, int argc, char **argv) {
802
0
  const int err = dns_timeout_selftests(opt);
803
804
0
  (void) argc;
805
0
  (void) argv;
806
  printf("dns-timeout-selftest: %s\n", err ? "FAIL" : "OK");
807
0
  return err;
808
0
}
809
810
/* ------------------------------------------------------------ */
811
/* Registry: this module's tests, in the order -#test lists them. */
812
/* ------------------------------------------------------------ */
813
814
const struct selftest_entry selftests_dns[] = {
815
    {"dns", "", "DNS resolver/cache self-test", st_dns},
816
    {"dnstimeout", "", "a slow DNS resolve is bounded and holds no lock",
817
     st_dnstimeout},
818
    {NULL, NULL, NULL, NULL},
819
};