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 | | /** @} */ |