1 /*
2 * Copyright (c) 2019-2021 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
net_init(void)72 static void __attribute__((constructor)) net_init(void)
73 {
74 if (!initialized) {
75 wget_thread_mutex_init(&default_dns.mutex);
76 initialized = true;
77 }
78 }
79
net_exit(void)80 static void __attribute__((destructor)) net_exit(void)
81 {
82 if (initialized) {
83 wget_thread_mutex_destroy(&default_dns.mutex);
84 initialized = false;
85 }
86 }
87
88 /**
89 * \param[out] dns Pointer to return newly allocated and initialized wget_dns instance
90 * \return WGET_E_SUCCESS if OK, WGET_E_MEMORY if out-of-memory or WGET_E_INVALID
91 * if the mutex initialization failed.
92 *
93 * Allocates and initializes a wget_dns instance.
94 */
wget_dns_init(wget_dns ** dns)95 int wget_dns_init(wget_dns **dns)
96 {
97 wget_dns *_dns = wget_calloc(1, sizeof(wget_dns));
98
99 if (!_dns)
100 return WGET_E_MEMORY;
101
102 if (wget_thread_mutex_init(&_dns->mutex)) {
103 xfree(_dns);
104 return WGET_E_INVALID;
105 }
106
107 _dns->timeout = -1;
108 *dns = _dns;
109
110 return WGET_E_SUCCESS;
111 }
112
113 /**
114 * \param[in/out] dns Pointer to wget_dns instance that will be freed and NULLified.
115 *
116 * Free the resources allocated by wget_dns_init().
117 */
wget_dns_free(wget_dns ** dns)118 void wget_dns_free(wget_dns **dns)
119 {
120 if (dns && *dns) {
121 wget_thread_mutex_destroy(&(*dns)->mutex);
122 xfree(*dns);
123 }
124 }
125
126 /**
127 * \param[in] dns The wget_dns instance to set the timeout
128 * \param[in] timeout The timeout value.
129 *
130 * Set the timeout (in milliseconds) for the DNS queries.
131 *
132 * This is the maximum time to wait until we get a response from the server.
133 *
134 * Warning: For standard getaddrinfo() a timeout can't be set in a portable way.
135 * So this functions currently is a no-op.
136 *
137 * The following two values are special:
138 *
139 * - `0`: No timeout, immediate.
140 * - `-1`: Infinite timeout. Wait indefinitely.
141 */
wget_dns_set_timeout(wget_dns * dns,int timeout)142 void wget_dns_set_timeout(wget_dns *dns, int timeout)
143 {
144 (dns ? dns : &default_dns)->timeout = timeout;
145 }
146
147 /**
148 * \param[in] dns A `wget_dns` instance, created by wget_dns_init().
149 * \param[in] cache A `wget_dns_cache` instance
150 *
151 * Enable or disable DNS caching for the DNS instance provided.
152 *
153 * The DNS cache is kept internally in memory, and is used in wget_dns_resolve() to speed up DNS queries.
154 */
wget_dns_set_cache(wget_dns * dns,wget_dns_cache * cache)155 void wget_dns_set_cache(wget_dns *dns, wget_dns_cache *cache)
156 {
157 (dns ? dns : &default_dns)->cache = cache;
158 }
159
160 /**
161 * \param[in] dns A `wget_dns` instance, created by wget_dns_init().
162 * \return 1 if DNS caching is enabled, 0 otherwise.
163 *
164 * Tells whether DNS caching is enabled or not.
165 *
166 * You can enable and disable DNS caching with wget_dns_set_caching().
167 */
wget_dns_get_cache(wget_dns * dns)168 wget_dns_cache *wget_dns_get_cache(wget_dns *dns)
169 {
170 return (dns ? dns : &default_dns)->cache;
171 }
172
173 /*
174 * Reorder address list so that addresses of the preferred family will come first.
175 */
sort_preferred(struct addrinfo * addrinfo,int preferred_family)176 static struct addrinfo *sort_preferred(struct addrinfo *addrinfo, int preferred_family)
177 {
178 struct addrinfo *preferred = NULL, *preferred_tail = NULL;
179 struct addrinfo *unpreferred = NULL, *unpreferred_tail = NULL;
180
181 for (struct addrinfo *ai = addrinfo; ai;) {
182 if (ai->ai_family == preferred_family) {
183 if (preferred_tail)
184 preferred_tail->ai_next = ai;
185 else
186 preferred = ai; // remember the head of the list
187
188 preferred_tail = ai;
189 ai = ai->ai_next;
190 preferred_tail->ai_next = NULL;
191 } else {
192 if (unpreferred_tail)
193 unpreferred_tail->ai_next = ai;
194 else
195 unpreferred = ai; // remember the head of the list
196
197 unpreferred_tail = ai;
198 ai = ai->ai_next;
199 unpreferred_tail->ai_next = NULL;
200 }
201 }
202
203 /* Merge preferred + not preferred */
204 if (preferred) {
205 preferred_tail->ai_next = unpreferred;
206 return preferred;
207 } else {
208 return unpreferred;
209 }
210 }
211
212 // we can't provide a portable way of respecting a DNS timeout
resolve(int family,int flags,const char * host,uint16_t port,struct addrinfo ** out_addr)213 static int resolve(int family, int flags, const char *host, uint16_t port, struct addrinfo **out_addr)
214 {
215 struct addrinfo hints = {
216 .ai_family = family,
217 .ai_socktype = SOCK_STREAM,
218 .ai_flags = AI_ADDRCONFIG | flags
219 };
220
221 if (port) {
222 char s_port[NI_MAXSERV];
223
224 hints.ai_flags |= AI_NUMERICSERV;
225
226 wget_snprintf(s_port, sizeof(s_port), "%hu", port);
227 debug_printf("resolving %s:%s...\n", host ? host : "", s_port);
228 return getaddrinfo(host, s_port, &hints, out_addr);
229 } else {
230 debug_printf("resolving %s...\n", host);
231 return getaddrinfo(host, NULL, &hints, out_addr);
232 }
233 }
234
235 /**
236 *
237 * \param[in] ip IP address of name
238 * \param[in] name Domain name, part of the cache's lookup key
239 * \param[in] port Port number, part of the cache's lookup key
240 * \return 0 on success, < 0 on error
241 *
242 * Assign an IP address to the name+port key in the DNS cache.
243 * The \p name should be lowercase.
244 */
wget_dns_cache_ip(wget_dns * dns,const char * ip,const char * name,uint16_t port)245 int wget_dns_cache_ip(wget_dns *dns, const char *ip, const char *name, uint16_t port)
246 {
247 int rc, family;
248 struct addrinfo *ai;
249
250 if (!dns || !dns->cache || !name)
251 return WGET_E_INVALID;
252
253 if (wget_ip_is_family(ip, WGET_NET_FAMILY_IPV4)) {
254 family = AF_INET;
255 } else if (wget_ip_is_family(ip, WGET_NET_FAMILY_IPV6)) {
256 family = AF_INET6;
257 } else
258 return WGET_E_INVALID;
259
260 if ((rc = resolve(family, AI_NUMERICHOST, ip, port, &ai)) != 0) {
261 error_printf(_("Failed to resolve %s:%d: %s\n"), ip, port, gai_strerror(rc));
262 return WGET_E_UNKNOWN;
263 }
264
265 if ((rc = wget_dns_cache_add(dns->cache, name, port, &ai)) < 0) {
266 freeaddrinfo(ai);
267 return rc;
268 }
269
270 return WGET_E_SUCCESS;
271 }
272
273 /**
274 * \param[in] dns A `wget_dns` instance, created by wget_dns_init().
275 * \param[in] host Hostname
276 * \param[in] port TCP destination port
277 * \param[in] family Protocol family AF_INET or AF_INET6
278 * \param[in] preferred_family Preferred protocol family AF_INET or AF_INET6
279 * \return A `struct addrinfo` structure (defined in libc's `<netdb.h>`). Must be freed by the caller with `wget_dns_freeaddrinfo()`.
280 *
281 * Resolve a host name into its IPv4/IPv6 address.
282 *
283 * **family**: Desired address family for the returned addresses. This will typically be `AF_INET` or `AF_INET6`,
284 * but it can be any of the values defined in `<socket.h>`. Additionally, `AF_UNSPEC` means you don't care: it will
285 * return any address family that can be used with the specified \p host and \p port. If **family** is different
286 * than `AF_UNSPEC` and the specified family is not found, _that's an error condition_ and thus wget_dns_resolve() will return NULL.
287 *
288 * **preferred_family**: Tries to resolve addresses of this family if possible. This is only honored if **family**
289 * (see point above) is `AF_UNSPEC`.
290 *
291 * The returned `addrinfo` structure must be freed with `wget_dns_freeaddrinfo()`.
292 */
wget_dns_resolve(wget_dns * dns,const char * host,uint16_t port,int family,int preferred_family)293 struct addrinfo *wget_dns_resolve(wget_dns *dns, const char *host, uint16_t port, int family, int preferred_family)
294 {
295 struct addrinfo *addrinfo = NULL;
296 int rc = 0;
297 char adr[NI_MAXHOST], sport[NI_MAXSERV];
298 long long before_millisecs = 0;
299 wget_dns_stats_data stats;
300
301 if (!dns)
302 dns = &default_dns;
303
304 if (dns->stats_callback)
305 before_millisecs = wget_get_timemillis();
306
307 // get the IP address for the server
308 for (int tries = 0, max = 3; tries < max; tries++) {
309 if (dns->cache) {
310 if ((addrinfo = wget_dns_cache_get(dns->cache, host, port)))
311 return addrinfo;
312
313 // prevent multiple address resolutions of the same host
314 wget_thread_mutex_lock(dns->mutex);
315
316 // now try again
317 if ((addrinfo = wget_dns_cache_get(dns->cache, host, port))) {
318 wget_thread_mutex_unlock(dns->mutex);
319 return addrinfo;
320 }
321 }
322
323 addrinfo = NULL;
324
325 rc = resolve(family, 0, host, port, &addrinfo);
326 if (rc == 0 || rc != EAI_AGAIN)
327 break;
328
329 if (tries < max - 1) {
330 if (dns->cache)
331 wget_thread_mutex_unlock(dns->mutex);
332 wget_millisleep(100);
333 }
334 }
335
336 if (dns->stats_callback) {
337 long long after_millisecs = wget_get_timemillis();
338 stats.dns_secs = after_millisecs - before_millisecs;
339 stats.hostname = host;
340 stats.port = port;
341 }
342
343 if (rc) {
344 error_printf(_("Failed to resolve %s (%s)\n"),
345 (host ? host : ""), gai_strerror(rc));
346
347 if (dns->cache)
348 wget_thread_mutex_unlock(dns->mutex);
349
350 if (dns->stats_callback) {
351 stats.ip = NULL;
352 dns->stats_callback(dns, &stats, dns->stats_ctx);
353 }
354
355 return NULL;
356 }
357
358 if (family == AF_UNSPEC && preferred_family != AF_UNSPEC)
359 addrinfo = sort_preferred(addrinfo, preferred_family);
360
361 if (dns->stats_callback) {
362 if (getnameinfo(addrinfo->ai_addr, addrinfo->ai_addrlen, adr, sizeof(adr), sport, sizeof(sport), NI_NUMERICHOST | NI_NUMERICSERV) == 0)
363 stats.ip = adr;
364 else
365 stats.ip = "???";
366
367 dns->stats_callback(dns, &stats, dns->stats_ctx);
368 }
369
370 /* Finally, print the address list to the debug pipe if enabled */
371 if (wget_logger_is_active(wget_get_logger(WGET_LOGGER_DEBUG))) {
372 for (struct addrinfo *ai = addrinfo; ai; ai = ai->ai_next) {
373 if ((rc = getnameinfo(ai->ai_addr, ai->ai_addrlen, adr, sizeof(adr), sport, sizeof(sport), NI_NUMERICHOST | NI_NUMERICSERV)) == 0)
374 debug_printf("has %s:%s\n", adr, sport);
375 else
376 debug_printf("has ??? (%s)\n", gai_strerror(rc));
377 }
378 }
379
380 if (dns->cache) {
381 /*
382 * In case of a race condition the already existing addrinfo is returned.
383 * The addrinfo argument given to wget_dns_cache_add() will be freed in this case.
384 */
385 rc = wget_dns_cache_add(dns->cache, host, port, &addrinfo);
386 wget_thread_mutex_unlock(dns->mutex);
387 if ( rc < 0) {
388 freeaddrinfo(addrinfo);
389 return NULL;
390 }
391 }
392
393 return addrinfo;
394 }
395
396 /**
397 * \param[in] dns A `wget_dns` instance, created by wget_dns_init().
398 * \param[in/out] addrinfo Value returned by `c`
399 *
400 * Release addrinfo, previously returned by `wget_dns_resolve()`.
401 * If the underlying \p dns uses caching, just the reference/pointer is set to %NULL.
402 */
wget_dns_freeaddrinfo(wget_dns * dns,struct addrinfo ** addrinfo)403 void wget_dns_freeaddrinfo(wget_dns *dns, struct addrinfo **addrinfo)
404 {
405 if (addrinfo && *addrinfo) {
406 if (!dns)
407 dns = &default_dns;
408
409 if (!dns->cache) {
410 freeaddrinfo(*addrinfo);
411 *addrinfo = NULL;
412 } else {
413 // addrinfo is cached and gets freed later when the DNS cache is freed
414 *addrinfo = NULL;
415 }
416 }
417 }
418
419 /**
420 * \param[in] dns A `wget_dns` instance, created by wget_dns_init().
421 * \param[in] fn A `wget_dns_stats_callback` callback function to receive resolve statistics data
422 * \param[in] ctx Context data given to \p fn
423 *
424 * Set callback function to be called once DNS statistics for a host are collected
425 */
wget_dns_set_stats_callback(wget_dns * dns,wget_dns_stats_callback * fn,void * ctx)426 void wget_dns_set_stats_callback(wget_dns *dns, wget_dns_stats_callback *fn, void *ctx)
427 {
428 if (!dns)
429 dns = &default_dns;
430
431 dns->stats_callback = fn;
432 dns->stats_ctx = ctx;
433 }
434
435 /** @} */
436