1
0
mirror of https://github.com/rkd77/elinks.git synced 2024-12-04 14:46:47 -05:00
elinks/src/network/dns.c
2022-05-04 16:18:46 +02:00

578 lines
13 KiB
C

/* Domain Name System Resolver Department */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#ifdef HAVE_NETDB_H
#include <netdb.h> /* OS/2 needs this after sys/types.h */
#endif
#ifdef HAVE_SYS_SOCKET_H
#include <sys/socket.h> /* OS/2 needs this after sys/types.h */
#endif
#ifdef HAVE_FCNTL_H
#include <fcntl.h> /* OS/2 needs this after sys/types.h */
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#ifdef HAVE_WS2TCPIP_H
#include <ws2tcpip.h> /* socklen_t for MinGW */
#endif
/* Go and say 'thanks' to BSD. */
#ifdef HAVE_NETINET_IN_H
#include <netinet/in.h>
#endif
#ifdef HAVE_ARPA_INET_H
#include <arpa/inet.h>
#endif
#include "elinks.h"
#include "config/options.h"
#include "main/select.h"
#include "network/dns.h"
#include "osdep/osdep.h"
#include "protocol/uri.h"
#include "util/error.h"
#include "util/memory.h"
#include "util/time.h"
#ifdef WIN32
#define NO_ASYNC_LOOKUP
#endif
struct dnsentry {
LIST_HEAD(struct dnsentry);
struct sockaddr_storage *addr; /* Pointer to array of addresses. */
int addrno; /* Adress array length. */
timeval_T creation_time; /* Creation time; let us do timeouts. */
char name[1]; /* Associated host; XXX: Must be last. */
};
struct dnsquery {
#ifdef THREAD_SAFE_LOOKUP
struct dnsquery *next_in_queue; /* Got queued? */
#endif
dns_callback_T done; /* Used for reporting back DNS result. */
void *data; /* Private callback data. */
/* The @done callback is called with these members. Thus, when
* free()ing, *always* set pointer to NULL ! */
struct sockaddr_storage *addr; /* Reference to array of addresses. */
int addrno; /* Reference to array len. */
/* As with the two members above, when stopping a DNS query *always* set
* this pointer to NULL. */
struct dnsquery **queryref; /* Reference to callers DNS member. */
#ifndef NO_ASYNC_LOOKUP
int h; /* One end of the async thread pipe. */
#endif
char name[1]; /* Associated host; XXX: Must be last. */
};
#ifdef THREAD_SAFE_LOOKUP
static struct dnsquery *dns_queue = NULL;
#endif
static INIT_LIST_OF(struct dnsentry, dns_cache);
static void done_dns_lookup(struct dnsquery *query, enum dns_result res);
/* DNS cache management: */
static struct dnsentry *
find_in_dns_cache(char *name)
{
struct dnsentry *dnsentry;
foreach (dnsentry, dns_cache)
if (!c_strcasecmp(dnsentry->name, name)) {
move_to_top_of_list(dns_cache, dnsentry);
return dnsentry;
}
return NULL;
}
static void
add_to_dns_cache(char *name, struct sockaddr_storage *addr, int addrno)
{
int namelen = strlen(name);
struct dnsentry *dnsentry;
int size;
assert(addrno > 0);
dnsentry = (struct dnsentry *)mem_calloc(1, sizeof(*dnsentry) + namelen);
if (!dnsentry) return;
size = addrno * sizeof(*dnsentry->addr);
dnsentry->addr = (struct sockaddr_storage *)mem_alloc(size);
if (!dnsentry->addr) {
mem_free(dnsentry);
return;
}
/* calloc() sets NUL char for us. */
memcpy(dnsentry->name, name, namelen);
memcpy(dnsentry->addr, addr, size);;
dnsentry->addrno = addrno;
timeval_now(&dnsentry->creation_time);
add_to_list(dns_cache, dnsentry);
}
static void
del_dns_cache_entry(struct dnsentry *dnsentry)
{
del_from_list(dnsentry);
mem_free_if(dnsentry->addr);
mem_free(dnsentry);
}
/* Synchronous DNS lookup management: */
enum dns_result
do_real_lookup(char *name, struct sockaddr_storage **addrs, int *addrno,
int in_thread)
{
#ifdef CONFIG_IPV6
struct addrinfo hint, *ai, *ai_cur;
#else
struct hostent *hostent = NULL;
#endif
int i;
if (!name || !addrs || !addrno)
return DNS_ERROR;
#ifdef CONFIG_IPV6
/* I had a strong preference for the following, but the glibc is really
* obsolete so I had to rather use much more complicated getaddrinfo().
* But we duplicate the code terribly here :|. */
/* hostent = getipnodebyname(name, AF_INET6, AI_ALL | AI_ADDRCONFIG, NULL); */
memset(&hint, 0, sizeof(hint));
hint.ai_family = AF_UNSPEC;
hint.ai_socktype = SOCK_STREAM;
if (getaddrinfo(name, NULL, &hint, &ai) != 0) return DNS_ERROR;
#else
/* Seems there are problems on Mac, so we first need to try
* gethostbyaddr(), but there are problems with gethostbyaddr on Cygwin,
* so we do not use gethostbyaddr there. */
#if defined(HAVE_GETHOSTBYADDR) && !defined(HAVE_SYS_CYGWIN_H)
{
struct in_addr inp;
#if defined(HAVE_INET_PTON)
if (is_ip_address(name, strlen(name)) && inet_pton(AF_INET, name, &inp))
hostent = gethostbyaddr(&inp, sizeof(inp), AF_INET);
#elif defined(HAVE_INET_ATON)
if (is_ip_address(name, strlen(name)) && inet_aton(name, &inp))
hostent = gethostbyaddr(&inp, sizeof(inp), AF_INET);
#endif
}
if (!hostent)
#endif
{
hostent = gethostbyname(name);
if (!hostent) return DNS_ERROR;
}
#endif
#ifdef CONFIG_IPV6
for (i = 0, ai_cur = ai; ai_cur; i++, ai_cur = ai_cur->ai_next);
#else
for (i = 0; hostent->h_addr_list[i] != NULL; i++);
#endif
/* We cannot use mem_*() in thread ("It will chew memory on OS/2 and
* BeOS because there are no locks around the memory debugging code."
* -- Mikulas). So we don't if in_thread != 0. */
*addrs = (struct sockaddr_storage *)(in_thread ? calloc(i, sizeof(**addrs))
: mem_calloc(i, sizeof(**addrs)));
if (!*addrs) return DNS_ERROR;
*addrno = i;
#ifdef CONFIG_IPV6
for (i = 0, ai_cur = ai; ai_cur; i++, ai_cur = ai_cur->ai_next) {
/* Don't use struct sockaddr_in6 here: because we
* called getaddrinfo with AF_UNSPEC, the address
* might not be for IP at all. */
struct sockaddr_storage *addr = &(*addrs)[i];
/* RFC 3493 says struct sockaddr_storage is supposed
* to be "Large enough to accommodate all supported
* protocol-specific address structures." So if
* getaddrinfo supports an address that does not fit
* in struct sockaddr_storage, then it is a bug in the
* library. In this case, fail the whole lookup, to
* make the bug more likely to be noticed. */
assert(ai_cur->ai_addrlen <= sizeof(*addr));
if_assert_failed {
freeaddrinfo(ai);
if (in_thread)
free(*addrs);
else
mem_free(*addrs);
*addrs = NULL;
*addrno = 0;
return DNS_ERROR;
}
memcpy(addr, ai_cur->ai_addr, ai_cur->ai_addrlen);
}
freeaddrinfo(ai);
#else
for (i = 0; hostent->h_addr_list[i] != NULL; i++) {
struct sockaddr_in *addr = (struct sockaddr_in *) &(*addrs)[i];
addr->sin_family = hostent->h_addrtype;
memcpy(&addr->sin_addr.s_addr, hostent->h_addr_list[i], hostent->h_length);
}
#endif
return DNS_SUCCESS;
}
/* Asynchronous DNS lookup management: */
#ifndef NO_ASYNC_LOOKUP
static enum dns_result
write_dns_data(int h, void *data, size_t datalen)
{
size_t done = 0;
do {
int w = safe_write(h, ((char *)data) + done, datalen - done);
if (w < 0) return DNS_ERROR;
done += w;
} while (done < datalen);
assert(done == datalen);
return DNS_SUCCESS;
}
static void
async_dns_writer(void *data, int h)
{
char *name = (char *) data;
struct sockaddr_storage *addrs;
int addrno, i;
if (do_real_lookup(name, &addrs, &addrno, 1) == DNS_ERROR)
return;
/* We will do blocking I/O here, however it's only local communication
* and it's supposed to be just a flash talk, so it shouldn't matter.
* And it would be incredibly more complicated and messy (and mainly
* useless) to do this in non-blocking way. */
if (set_blocking_fd(h) < 0) return;
if (write_dns_data(h, &addrno, sizeof(addrno)) == DNS_ERROR)
return;
for (i = 0; i < addrno; i++) {
struct sockaddr_storage *addr = &addrs[i];
if (write_dns_data(h, addr, sizeof(*addr)) == DNS_ERROR)
return;
}
/* We're in thread, thus we must do plain free(). */
free(addrs);
}
static enum dns_result
read_dns_data(int h, void *data, size_t datalen)
{
size_t done = 0;
do {
ssize_t r = safe_read(h, ((char *)data) + done, datalen - done);
if (r <= 0) return DNS_ERROR;
done += r;
} while (done < datalen);
assert(done == datalen);
return DNS_SUCCESS;
}
static void
async_dns_reader(struct dnsquery *query)
{
enum dns_result result = DNS_ERROR;
int i;
/* We will do blocking I/O here, however it's only local communication
* and it's supposed to be just a flash talk, so it shouldn't matter.
* And it would be incredibly more complicated and messy (and mainly
* useless) to do this in non-blocking way. */
if (set_blocking_fd(query->h) < 0) goto done;
if (read_dns_data(query->h, &query->addrno, sizeof(query->addrno)) == DNS_ERROR)
goto done;
query->addr = (struct sockaddr_storage *)mem_calloc(query->addrno, sizeof(*query->addr));
if (!query->addr) goto done;
for (i = 0; i < query->addrno; i++) {
struct sockaddr_storage *addr = &query->addr[i];
if (read_dns_data(query->h, addr, sizeof(*addr)) == DNS_ERROR)
goto done;
}
result = DNS_SUCCESS;
done:
if (result == DNS_ERROR)
mem_free_set(&query->addr, NULL);
done_dns_lookup(query, result);
}
static void
async_dns_error(struct dnsquery *query)
{
done_dns_lookup(query, DNS_ERROR);
}
static int
init_async_dns_lookup(struct dnsquery *dnsquery, int force_async)
{
if (!force_async && !get_opt_bool("connection.async_dns", NULL)) {
dnsquery->h = -1;
return 0;
}
dnsquery->h = start_thread(async_dns_writer, dnsquery->name,
strlen(dnsquery->name) + 1);
if (dnsquery->h == -1)
return 0;
set_handlers(dnsquery->h, (select_handler_T) async_dns_reader, NULL,
(select_handler_T) async_dns_error, dnsquery);
return 1;
}
static void
done_async_dns_lookup(struct dnsquery *dnsquery)
{
if (dnsquery->h == -1) return;
clear_handlers(dnsquery->h);
close(dnsquery->h);
dnsquery->h = -1;
}
#else
#define init_async_dns_lookup(dnsquery, force) (0)
#define done_async_dns_lookup(dnsquery) /* Nada. */
#endif /* NO_ASYNC_LOOKUP */
static enum dns_result
do_lookup(struct dnsquery *query, int force_async)
{
enum dns_result result;
/* DBG("starting lookup for %s", query->name); */
/* Async lookup */
if (init_async_dns_lookup(query, force_async))
return DNS_ASYNC;
/* Sync lookup */
result = do_real_lookup(query->name, &query->addr, &query->addrno, 0);
done_dns_lookup(query, result);
return result;
}
static enum dns_result
do_queued_lookup(struct dnsquery *query)
{
#ifdef THREAD_SAFE_LOOKUP
query->next_in_queue = NULL;
if (dns_queue) {
/* DBG("queuing lookup for %s", q->name); */
assertm(!dns_queue->next_in_queue, "DNS queue corrupted");
dns_queue->next_in_queue = query;
dns_queue = query;
return DNS_ERROR;
}
dns_queue = query;
#endif
/* DBG("direct lookup"); */
return do_lookup(query, 0);
}
static void
done_dns_lookup(struct dnsquery *query, enum dns_result result)
{
struct dnsentry *dnsentry;
/* DBG("end lookup %s (%d)", query->name, res); */
/* do_lookup() might start a new async thread */
done_async_dns_lookup(query);
#ifdef THREAD_SAFE_LOOKUP
if (query->next_in_queue) {
/* DBG("processing next in queue: %s", query->next_in_queue->name); */
do_lookup(query->next_in_queue, 1);
} else {
dns_queue = NULL;
}
#endif
/* Make sure the query is unregister _before_ calling any callbacks. */
*query->queryref = NULL;
/* If the callback was cleared skip to the freeing part. */
if (!query->done)
goto done;
dnsentry = find_in_dns_cache(query->name);
if (dnsentry) {
/* If the query failed, use the existing DNS cache entry even if
* it is too old. */
if (result == DNS_ERROR) {
query->done(query->data, dnsentry->addr, dnsentry->addrno);
goto done;
}
del_dns_cache_entry(dnsentry);
}
if (result == DNS_SUCCESS)
add_to_dns_cache(query->name, query->addr, query->addrno);
query->done(query->data, query->addr, query->addrno);
done:
mem_free_set(&query->addr, NULL);
mem_free(query);
}
static enum dns_result
init_dns_lookup(char *name, void **queryref,
dns_callback_T done, void *data)
{
struct dnsquery *query;
int namelen = strlen(name);
query = (struct dnsquery *)mem_calloc(1, sizeof(*query) + namelen);
if (!query) {
done(data, NULL, 0);
return DNS_ERROR;
}
query->done = done;
query->data = data;
/* calloc() sets NUL char for us. */
memcpy(query->name, name, namelen);
query->queryref = (struct dnsquery **) queryref;
*(query->queryref) = query;
return do_queued_lookup(query);
}
enum dns_result
find_host(char *name, void **queryref,
dns_callback_T done, void *data, int no_cache)
{
struct dnsentry *dnsentry;
assert(queryref);
*queryref = NULL;
if (no_cache)
return init_dns_lookup(name, queryref, done, data);
/* Check if the DNS name is in the cache. If the cache entry is too old
* do a new lookup. However, old cache entries will be used as a
* fallback if the new lookup fails. */
dnsentry = find_in_dns_cache(name);
if (dnsentry) {
timeval_T age, now, max_age;
assert(dnsentry && dnsentry->addrno > 0);
timeval_from_seconds(&max_age, DNS_CACHE_TIMEOUT);
timeval_now(&now);
timeval_sub(&age, &dnsentry->creation_time, &now);
if (timeval_cmp(&age, &max_age) <= 0) {
done(data, dnsentry->addr, dnsentry->addrno);
return DNS_SUCCESS;
}
}
return init_dns_lookup(name, queryref, done, data);
}
void
kill_dns_request(void **queryref)
{
struct dnsquery *query = (struct dnsquery *)*queryref;
assert(query);
query->done = NULL;
done_dns_lookup(query, DNS_ERROR);
}
void
shrink_dns_cache(int whole)
{
struct dnsentry *dnsentry, *next;
if (whole) {
foreachsafe (dnsentry, next, dns_cache)
del_dns_cache_entry(dnsentry);
} else {
timeval_T now, max_age;
timeval_from_seconds(&max_age, DNS_CACHE_TIMEOUT);
timeval_now(&now);
foreachsafe (dnsentry, next, dns_cache) {
timeval_T age;
timeval_sub(&age, &dnsentry->creation_time, &now);
if (timeval_cmp(&age, &max_age) > 0)
del_dns_cache_entry(dnsentry);
}
}
}