Coverage Report

Created: 2025-01-28 06:58

/src/wget2/libwget/dns.c
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Copyright (c) 2019-2024 Free Software Foundation, Inc.
3
 *
4
 * This file is part of libwget.
5
 *
6
 * Libwget is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Libwget is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU Lesser General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU Lesser General Public License
17
 * along with libwget.  If not, see <https://www.gnu.org/licenses/>.
18
 *
19
 *
20
 * resolver routines
21
 */
22
23
#include <config.h>
24
25
#include <sys/types.h>
26
#include <stddef.h>
27
#include <stdio.h>
28
#include <string.h>
29
#include <unistd.h>
30
#include <stdarg.h>
31
#include <time.h>
32
#include <errno.h>
33
#include <netdb.h>
34
#include <netinet/in.h>
35
36
#include <wget.h>
37
#include "private.h"
38
39
/**
40
 * \file
41
 * \brief Functions for resolving names/IPs
42
 * \defgroup libwget-dns DNS resolver functions
43
 *
44
 * @{
45
 *
46
 * DNS Resolver functions.
47
 *
48
 */
49
50
struct wget_dns_st
51
{
52
  wget_dns_cache
53
    *cache;
54
  wget_thread_mutex
55
    mutex;
56
  wget_dns_stats_callback
57
    *stats_callback;
58
  void
59
    *stats_ctx;
60
  wget_dns_stats_data
61
    stats;
62
  int
63
    timeout;
64
};
65
static wget_dns default_dns = {
66
  .timeout = -1,
67
};
68
69
static bool
70
  initialized;
71
72
static void dns_exit(void)
73
0
{
74
0
  if (initialized) {
75
0
    wget_thread_mutex_destroy(&default_dns.mutex);
76
0
    initialized = false;
77
0
  }
78
0
}
79
80
INITIALIZER(dns_init)
81
2
{
82
2
  if (!initialized) {
83
2
    wget_thread_mutex_init(&default_dns.mutex);
84
2
    initialized = true;
85
2
    atexit(dns_exit);
86
2
  }
87
2
}
88
89
/**
90
 * \param[out] dns Pointer to return newly allocated and initialized wget_dns instance
91
 * \return WGET_E_SUCCESS if OK, WGET_E_MEMORY if out-of-memory or WGET_E_INVALID
92
 *   if the mutex initialization failed.
93
 *
94
 * Allocates and initializes a wget_dns instance.
95
 * \p dns may be NULL for the purpose of initializing the global structures.
96
 */
97
int wget_dns_init(wget_dns **dns)
98
0
{
99
0
  dns_init();
100
101
0
  if (!dns)
102
0
    return WGET_E_SUCCESS;
103
104
0
  wget_dns *_dns = wget_calloc(1, sizeof(wget_dns));
105
106
0
  if (!_dns)
107
0
    return WGET_E_MEMORY;
108
109
0
  if (wget_thread_mutex_init(&_dns->mutex)) {
110
0
    xfree(_dns);
111
0
    return WGET_E_INVALID;
112
0
  }
113
114
0
  _dns->timeout = -1;
115
0
  *dns = _dns;
116
117
0
  return WGET_E_SUCCESS;
118
0
}
119
120
/**
121
 * \param[in/out] dns Pointer to wget_dns instance that will be freed and NULLified.
122
 *
123
 * Free the resources allocated by wget_dns_init().
124
 * \p dns may be NULL for the purpose of freeing the global structures.
125
 */
126
void wget_dns_free(wget_dns **dns)
127
0
{
128
0
  if (!dns) {
129
0
    dns_exit();
130
0
    return;
131
0
  }
132
133
0
  if (*dns) {
134
0
    wget_thread_mutex_destroy(&(*dns)->mutex);
135
0
    xfree(*dns);
136
0
  }
137
0
}
138
139
/**
140
 * \param[in] dns The wget_dns instance to set the timeout
141
 * \param[in] timeout The timeout value.
142
 *
143
 * Set the timeout (in milliseconds) for the DNS queries.
144
 *
145
 * This is the maximum time to wait until we get a response from the server.
146
 *
147
 * Warning: For standard getaddrinfo() a timeout can't be set in a portable way.
148
 * So this functions currently is a no-op.
149
 *
150
 * The following two values are special:
151
 *
152
 *  - `0`: No timeout, immediate.
153
 *  - `-1`: Infinite timeout. Wait indefinitely.
154
 */
155
void wget_dns_set_timeout(wget_dns *dns, int timeout)
156
0
{
157
0
  (dns ? dns : &default_dns)->timeout = timeout;
158
0
}
159
160
/**
161
 * \param[in] dns A `wget_dns` instance, created by wget_dns_init().
162
 * \param[in] cache A `wget_dns_cache` instance
163
 *
164
 * Enable or disable DNS caching for the DNS instance provided.
165
 *
166
 * The DNS cache is kept internally in memory, and is used in wget_dns_resolve() to speed up DNS queries.
167
 */
168
void wget_dns_set_cache(wget_dns *dns, wget_dns_cache *cache)
169
0
{
170
0
  (dns ? dns : &default_dns)->cache = cache;
171
0
}
172
173
/**
174
 * \param[in] dns A `wget_dns` instance, created by wget_dns_init().
175
 * \return 1 if DNS caching is enabled, 0 otherwise.
176
 *
177
 * Tells whether DNS caching is enabled or not.
178
 *
179
 * You can enable and disable DNS caching with wget_dns_set_caching().
180
 */
181
wget_dns_cache *wget_dns_get_cache(wget_dns *dns)
182
0
{
183
0
  return (dns ? dns : &default_dns)->cache;
184
0
}
185
186
/*
187
 * Reorder address list so that addresses of the preferred family will come first.
188
 */
189
static struct addrinfo *sort_preferred(struct addrinfo *addrinfo, int preferred_family)
190
0
{
191
0
  struct addrinfo *preferred = NULL, *preferred_tail = NULL;
192
0
  struct addrinfo *unpreferred = NULL, *unpreferred_tail = NULL;
193
194
0
  for (struct addrinfo *ai = addrinfo; ai;) {
195
0
    if (ai->ai_family == preferred_family) {
196
0
      if (preferred_tail)
197
0
        preferred_tail->ai_next = ai;
198
0
      else
199
0
        preferred = ai; // remember the head of the list
200
201
0
      preferred_tail = ai;
202
0
      ai = ai->ai_next;
203
0
      preferred_tail->ai_next = NULL;
204
0
    } else {
205
0
      if (unpreferred_tail)
206
0
        unpreferred_tail->ai_next = ai;
207
0
      else
208
0
        unpreferred = ai; // remember the head of the list
209
210
0
      unpreferred_tail = ai;
211
0
      ai = ai->ai_next;
212
0
      unpreferred_tail->ai_next = NULL;
213
0
    }
214
0
  }
215
216
  /* Merge preferred + not preferred */
217
0
  if (preferred) {
218
0
    preferred_tail->ai_next = unpreferred;
219
0
    return preferred;
220
0
  } else {
221
0
    return unpreferred;
222
0
  }
223
0
}
224
225
static int getaddrinfo_merging(const char *host, const char *s_port, struct addrinfo *hints, struct addrinfo **out_addr)
226
7.78k
{
227
7.78k
  if (!*out_addr)
228
3.89k
    return getaddrinfo(host, s_port, hints, out_addr);
229
230
  // Get to the tail of the list
231
3.89k
  struct addrinfo *ai_tail = *out_addr;
232
3.89k
  while (ai_tail->ai_next)
233
0
    ai_tail = ai_tail->ai_next;
234
235
3.89k
  return getaddrinfo(host, s_port, hints, &ai_tail->ai_next);
236
7.78k
}
237
238
// we can't provide a portable way of respecting a DNS timeout
239
static int resolve(int family, int flags, const char *host, uint16_t port, struct addrinfo **out_addr)
240
3.89k
{
241
3.89k
  struct addrinfo hints = {
242
3.89k
    .ai_family = family,
243
3.89k
    .ai_socktype = 0,
244
3.89k
    .ai_flags = AI_ADDRCONFIG | flags
245
3.89k
  };
246
3.89k
  char s_port[NI_MAXSERV];
247
248
3.89k
  *out_addr = NULL;
249
250
3.89k
  if (port) {
251
3.89k
    hints.ai_flags |= AI_NUMERICSERV;
252
253
3.89k
    wget_snprintf(s_port, sizeof(s_port), "%hu", port);
254
3.89k
    if (host) {
255
3.89k
      if (family == AF_INET6)
256
0
        debug_printf("resolving [%s]:%s...\n", host, s_port);
257
3.89k
      else
258
3.89k
        debug_printf("resolving %s:%s...\n", host, s_port);
259
3.89k
    } else
260
0
      debug_printf("resolving :%s...\n", s_port);
261
3.89k
  } else {
262
0
    debug_printf("resolving %s...\n", host);
263
0
  }
264
265
3.89k
  int ret;
266
267
  /*
268
   * .ai_socktype = 0, which would give us all the available socket types,
269
   * is not a valid option on Windows. Hence, we call getaddrinfo() twice with SOCK_STREAM
270
   * and SOCK_DGRAM, and merge the two lists.
271
   * See: https://learn.microsoft.com/en-us/windows/win32/api/ws2def/ns-ws2def-addrinfoa
272
   */
273
3.89k
  hints.ai_socktype = SOCK_STREAM;
274
3.89k
  if ((ret = getaddrinfo_merging(host, port ? s_port : NULL, &hints, out_addr)) != 0)
275
0
    return ret;
276
277
3.89k
  hints.ai_socktype = SOCK_DGRAM;
278
3.89k
  if ((ret = getaddrinfo_merging(host, port ? s_port : NULL, &hints, out_addr)) != 0) {
279
0
    if (*out_addr)
280
0
      freeaddrinfo(*out_addr);
281
0
  }
282
283
3.89k
  return ret;
284
3.89k
}
285
286
/**
287
 *
288
 * \param[in] ip IP address of name
289
 * \param[in] name Domain name, part of the cache's lookup key
290
 * \param[in] port Port number, part of the cache's lookup key
291
 * \return 0 on success, < 0 on error
292
 *
293
 * Assign an IP address to the name+port key in the DNS cache.
294
 * The \p name should be lowercase.
295
 */
296
int wget_dns_cache_ip(wget_dns *dns, const char *ip, const char *name, uint16_t port)
297
0
{
298
0
  int rc, family;
299
0
  struct addrinfo *ai;
300
301
0
  if (!dns || !dns->cache || !name)
302
0
    return WGET_E_INVALID;
303
304
0
  if (wget_ip_is_family(ip, WGET_NET_FAMILY_IPV4)) {
305
0
    family = AF_INET;
306
0
  } else if (wget_ip_is_family(ip, WGET_NET_FAMILY_IPV6)) {
307
0
    family = AF_INET6;
308
0
  } else
309
0
    return WGET_E_INVALID;
310
311
0
  if ((rc = resolve(family, AI_NUMERICHOST, ip, port, &ai)) != 0) {
312
0
    if (family == AF_INET6)
313
0
      error_printf(_("Failed to resolve '[%s]:%d': %s\n"), ip, port, gai_strerror(rc));
314
0
    else
315
0
      error_printf(_("Failed to resolve '%s:%d': %s\n"), ip, port, gai_strerror(rc));
316
0
    return WGET_E_UNKNOWN;
317
0
  }
318
319
0
  if ((rc = wget_dns_cache_add(dns->cache, name, port, &ai)) < 0) {
320
0
    freeaddrinfo(ai);
321
0
    return rc;
322
0
  }
323
324
0
  return WGET_E_SUCCESS;
325
0
}
326
327
/**
328
 * \param[in] dns A `wget_dns` instance, created by wget_dns_init().
329
 * \param[in] host Hostname
330
 * \param[in] port TCP destination port
331
 * \param[in] family Protocol family AF_INET or AF_INET6
332
 * \param[in] preferred_family Preferred protocol family AF_INET or AF_INET6
333
 * \return A `struct addrinfo` structure (defined in libc's `<netdb.h>`). Must be freed by the caller with `wget_dns_freeaddrinfo()`.
334
 *
335
 * Resolve a host name into its IPv4/IPv6 address.
336
 *
337
 * **family**: Desired address family for the returned addresses. This will typically be `AF_INET` or `AF_INET6`,
338
 * but it can be any of the values defined in `<socket.h>`. Additionally, `AF_UNSPEC` means you don't care: it will
339
 * return any address family that can be used with the specified \p host and \p port. If **family** is different
340
 * than `AF_UNSPEC` and the specified family is not found, _that's an error condition_ and thus wget_dns_resolve() will return NULL.
341
 *
342
 * **preferred_family**: Tries to resolve addresses of this family if possible. This is only honored if **family**
343
 * (see point above) is `AF_UNSPEC`.
344
 *
345
 *  The returned `addrinfo` structure must be freed with `wget_dns_freeaddrinfo()`.
346
 */
347
struct addrinfo *wget_dns_resolve(wget_dns *dns, const char *host, uint16_t port, int family, int preferred_family)
348
3.89k
{
349
3.89k
  struct addrinfo *addrinfo = NULL;
350
3.89k
  int rc = 0;
351
3.89k
  char adr[NI_MAXHOST], sport[NI_MAXSERV];
352
3.89k
  long long before_millisecs = 0;
353
3.89k
  wget_dns_stats_data stats;
354
355
3.89k
  if (!dns)
356
3.89k
    dns = &default_dns;
357
358
3.89k
  if (dns->stats_callback)
359
0
    before_millisecs = wget_get_timemillis();
360
361
  // get the IP address for the server
362
3.89k
  for (int tries = 0, max = 3; tries < max; tries++) {
363
3.89k
    if (dns->cache) {
364
0
      if ((addrinfo = wget_dns_cache_get(dns->cache, host, port)))
365
0
        return addrinfo;
366
367
      // prevent multiple address resolutions of the same host
368
0
      wget_thread_mutex_lock(dns->mutex);
369
370
      // now try again
371
0
      if ((addrinfo = wget_dns_cache_get(dns->cache, host, port))) {
372
0
        wget_thread_mutex_unlock(dns->mutex);
373
0
        return addrinfo;
374
0
      }
375
0
    }
376
377
3.89k
    addrinfo = NULL;
378
379
3.89k
    rc = resolve(family, 0, host, port, &addrinfo);
380
3.89k
    if (rc == 0 || rc != EAI_AGAIN)
381
3.89k
      break;
382
383
0
    if (tries < max - 1) {
384
0
      if (dns->cache)
385
0
        wget_thread_mutex_unlock(dns->mutex);
386
0
      wget_millisleep(100);
387
0
    }
388
0
  }
389
390
3.89k
  if (dns->stats_callback) {
391
0
    long long after_millisecs = wget_get_timemillis();
392
0
    stats.dns_secs = after_millisecs - before_millisecs;
393
0
    stats.hostname = host;
394
0
    stats.port = port;
395
0
  }
396
397
3.89k
  if (rc) {
398
0
    error_printf(_("Failed to resolve '%s' (%s)\n"),
399
0
        (host ? host : ""), gai_strerror(rc));
400
401
0
    if (dns->cache)
402
0
      wget_thread_mutex_unlock(dns->mutex);
403
404
0
    if (dns->stats_callback) {
405
0
      stats.ip = NULL;
406
0
      dns->stats_callback(dns, &stats, dns->stats_ctx);
407
0
    }
408
409
0
    return NULL;
410
0
  }
411
412
3.89k
  if (family == AF_UNSPEC && preferred_family != AF_UNSPEC)
413
0
    addrinfo = sort_preferred(addrinfo, preferred_family);
414
415
3.89k
  if (dns->stats_callback) {
416
0
    if (getnameinfo(addrinfo->ai_addr, addrinfo->ai_addrlen, adr, sizeof(adr), sport, sizeof(sport), NI_NUMERICHOST | NI_NUMERICSERV) == 0)
417
0
      stats.ip = adr;
418
0
    else
419
0
      stats.ip = "???";
420
421
0
    dns->stats_callback(dns, &stats, dns->stats_ctx);
422
0
  }
423
424
  /* Finally, print the address list to the debug pipe if enabled */
425
3.89k
  if (wget_logger_is_active(wget_get_logger(WGET_LOGGER_DEBUG))) {
426
0
    for (struct addrinfo *ai = addrinfo; ai; ai = ai->ai_next) {
427
0
      if ((rc = getnameinfo(ai->ai_addr, ai->ai_addrlen, adr, sizeof(adr), sport, sizeof(sport), NI_NUMERICHOST | NI_NUMERICSERV)) == 0) {
428
0
        if (ai->ai_family == AF_INET6)
429
0
          debug_printf("has [%s]:%s\n", adr, sport);
430
0
        else
431
0
          debug_printf("has %s:%s\n", adr, sport);
432
0
      } else
433
0
        debug_printf("has ??? (%s)\n", gai_strerror(rc));
434
0
    }
435
0
  }
436
437
3.89k
  if (dns->cache) {
438
    /*
439
     * In case of a race condition the already existing addrinfo is returned.
440
     * The addrinfo argument given to wget_dns_cache_add() will be freed in this case.
441
     */
442
0
    rc = wget_dns_cache_add(dns->cache, host, port, &addrinfo);
443
0
    wget_thread_mutex_unlock(dns->mutex);
444
0
    if ( rc < 0) {
445
0
      freeaddrinfo(addrinfo);
446
0
      return NULL;
447
0
    }
448
0
  }
449
450
3.89k
  return addrinfo;
451
3.89k
}
452
453
/**
454
 * \param[in] dns A `wget_dns` instance, created by wget_dns_init().
455
 * \param[in/out] addrinfo Value returned by `c`
456
 *
457
 * Release addrinfo, previously returned by `wget_dns_resolve()`.
458
 * If the underlying \p dns uses caching, just the reference/pointer is set to %NULL.
459
 */
460
void wget_dns_freeaddrinfo(wget_dns *dns, struct addrinfo **addrinfo)
461
11.6k
{
462
11.6k
  if (addrinfo && *addrinfo) {
463
3.89k
    if (!dns)
464
3.89k
      dns = &default_dns;
465
466
3.89k
    if (!dns->cache) {
467
3.89k
      freeaddrinfo(*addrinfo);
468
3.89k
      *addrinfo = NULL;
469
3.89k
    } else {
470
      // addrinfo is cached and gets freed later when the DNS cache is freed
471
0
      *addrinfo = NULL;
472
0
    }
473
3.89k
  }
474
11.6k
}
475
476
/**
477
 * \param[in] dns A `wget_dns` instance, created by wget_dns_init().
478
 * \param[in] fn A `wget_dns_stats_callback` callback function to receive resolve statistics data
479
 * \param[in] ctx Context data given to \p fn
480
 *
481
 * Set callback function to be called once DNS statistics for a host are collected
482
 */
483
void wget_dns_set_stats_callback(wget_dns *dns, wget_dns_stats_callback *fn, void *ctx)
484
0
{
485
0
  if (!dns)
486
0
    dns = &default_dns;
487
488
0
  dns->stats_callback = fn;
489
0
  dns->stats_ctx = ctx;
490
0
}
491
492
/** @} */