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