/* This file is part of Mailfromd.
   Copyright (C) 2005-2022 Sergey Poznyakoff

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3, or (at your option)
   any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>. */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <sys/types.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
#include <adns.h>
#include <mailutils/alloc.h>
#include <mailutils/argcv.h>
#include <mailutils/io.h>
#include <mailutils/stream.h>
#include <mailutils/cstr.h>
#include <mailutils/wordsplit.h>
#include <mailutils/assoc.h>

#include "libmf.h"
#include "dns.h"

#define DEFAULT_QFLAGS \
	(adns_qf_quoteok_cname|adns_qf_cname_loose|adns_qf_quoteok_query)

static mu_debug_handle_t debug_handle;
static adns_state state;

static void
dns_log_cb(adns_state ads, void *logfndata, const char *fmt, va_list al)
{
/* FIXME: Could have used just:
     mu_diag_vprintf(MU_DIAG_DEBUG, fmt, al);
   but it will emit \e<N> directives in the middle of the string, which
   upsets the mailutils' logstream implementation.

   A possible work over would be to use logfndata to select between
   mu_diag_vprintf,mu_diag_cont_vprintf or appropriate mu_debug_log_
   call.

   For the time being, a simplified approach is used: */
	mu_stream_vprintf(mu_strerr, fmt, al);
}

void
dnsbase_init(void)
{
	if (!debug_handle)
		debug_handle = mu_debug_register_category("dns");
}

void
dnsbase_real_init(char *configtext)
{
	int rc;
	int flags;
	mu_debug_level_t lev;
	
	flags = adns_if_nosigpipe;
	if (mu_debug_get_category_level(debug_handle, &lev) == 0
	    && (lev & MU_DEBUG_LEVEL_MASK(MU_DEBUG_TRACE9)))
		flags |= adns_if_debug;
	rc = adns_init_logfn(&state, flags, configtext, dns_log_cb, NULL);
	if (rc) {
		mu_diag_funcall(MU_DIAG_ERROR, "adns_init", NULL, rc);
		exit(1);
	}
}

void
dnsbase_file_init(char *filename)
{
	if (!filename)
		dnsbase_real_init(NULL);
	else {
		mu_stream_t str;
		mu_off_t sz;
		int rc;
		char *cfg;
		
		rc = mu_file_stream_create(&str, filename, MU_STREAM_READ);
		if (rc) {
			mu_diag_funcall(MU_DIAG_ERROR, "mu_file_stream_create",
					filename, rc);
			return;
		}
		rc = mu_stream_size(str, &sz);
		if (rc) {
			mu_diag_funcall(MU_DIAG_ERROR, "mu_stream_size",
					filename, rc);
			mu_stream_destroy(&str);
			return;
		}

		if (sz > ((size_t)~0)) {
			mu_error(_("%s too big"), filename);
			mu_stream_destroy(&str);
			return;
		}
		
		cfg = mu_alloc(sz + 1);

		rc = mu_stream_read(str, cfg, sz, NULL);
		mu_stream_destroy(&str);
		if (rc) {
			mu_diag_funcall(MU_DIAG_ERROR, "mu_stream_read",
					filename, rc);
			return;
		}
		cfg[sz] = 0;
		dnsbase_real_init(cfg);
		free(cfg);
	}
}

static adns_state
get_state(void)
{
	if (!state)
		dnsbase_real_init(NULL);
	return state;
}

static inline size_t
dns_reply_elsize(struct dns_reply *reply)
{
	switch (reply->type) {
	case dns_reply_ip:
		return sizeof(reply->data.ip[0]);
	case dns_reply_str:
		return sizeof(reply->data.str[0]);
	}
	abort();
}

void
dns_reply_init(struct dns_reply *reply, dns_reply_type type, size_t count)
{
	reply->type = type;
	reply->count = count;
	reply->maxcount = count;
	if (count)
		reply->data.ptr = mu_calloc(count, dns_reply_elsize(reply));
	else
		reply->data.ptr = NULL;
}

void
dns_reply_ip_push(struct dns_reply *reply, void *item)
{
	if (reply->count == reply->maxcount) 
		reply->data.ip = mu_2nrealloc(reply->data.ip,
					      &reply->maxcount,
					      sizeof(reply->data.ip[0]));
	reply->data.ip[reply->count++] = *(GACOPYZ_UINT32_T*)item;
}

void
dns_reply_str_push(struct dns_reply *reply, void *item)
{
	if (reply->count == reply->maxcount) 
		reply->data.str = mu_2nrealloc(reply->data.ip,
					       &reply->maxcount,
					       sizeof(reply->data.str[0]));
	reply->data.str[reply->count++] = item;
}

void
dns_reply_push(struct dns_reply *reply, void *item)
{
	switch (reply->type) {
	case dns_reply_ip:
		dns_reply_ip_push(reply, item);
		break;
	case dns_reply_str:
		dns_reply_str_push(reply, item);
		break;
	default:
		abort();
	}
}

void
dns_reply_free(struct dns_reply *reply)
{
	int i;
	
	switch (reply->type) {
	case dns_reply_str:
		for (i = 0; i < reply->count; i++)
			free(reply->data.str[i]);
		free(reply->data.str);
		break;
	case dns_reply_ip:
		free(reply->data.ip);
		break;
	}
}	

int
dns_str_is_ipv4(const char *addr)
{
        int dot_count;
        int digit_count;

        dot_count = 0;
        digit_count = 0;
        while (*addr != 0) {
                if (*addr == '.') {
                        if (++dot_count > 4)
                                return 0;
                        digit_count = 0;
                } else if (!(isdigit(*addr) && ++digit_count <= 3)) {
                        return 0;
                }
                addr++;
        }
	
        return dot_count == 3;
}

static int
errno_to_dns_status(int e)
{
	switch (e) {
	case 0:
		return dns_success;
	case EAGAIN:
#ifdef EINPROGRESS
	case EINPROGRESS:
#endif
#ifdef ETIMEDOUT
	case ETIMEDOUT:
#endif
		return dns_temp_failure;
	default:
		return dns_failure;
	}
}

/* Table of correspondence between ADNS status codes and dns status.
   Values are increased by 1 to be able to tell whether the entry is
   initialized or not. */
int adns_to_dns_tab[] = {
#define STAT(s) ((s)+1)	
	[adns_s_ok]                  = STAT(dns_success),
	
	[adns_s_nomemory]            = STAT(dns_failure),
	[adns_s_unknownrrtype]       = STAT(dns_failure),
	[adns_s_systemfail]          = STAT(dns_failure),

	/* remotely induced errors), detected locally */
	[adns_s_timeout]             = STAT(dns_temp_failure),
	[adns_s_allservfail]         = STAT(dns_temp_failure),
	[adns_s_norecurse]           = STAT(dns_temp_failure),
	[adns_s_invalidresponse]     = STAT(dns_failure),
	[adns_s_unknownformat]       = STAT(dns_failure),

	/* remotely induced errors), reported by remote server to us */
	[adns_s_rcodeservfail]       = STAT(dns_not_found),
	[adns_s_rcodeformaterror]    = STAT(dns_not_found),
	[adns_s_rcodenotimplemented] = STAT(dns_not_found),
	[adns_s_rcoderefused]        = STAT(dns_not_found),
	[adns_s_rcodeunknown]        = STAT(dns_not_found),

	/* remote configuration errors */
	[adns_s_inconsistent]        = STAT(dns_not_found),
	[adns_s_prohibitedcname]     = STAT(dns_not_found),
	[adns_s_answerdomaininvalid] = STAT(dns_not_found),
	[adns_s_answerdomaintoolong] = STAT(dns_not_found),
	[adns_s_invaliddata]         = STAT(dns_not_found),
 
	/* permanent problems with the query */
	[adns_s_querydomainwrong]    = STAT(dns_failure),
	[adns_s_querydomaininvalid]  = STAT(dns_failure),
	[adns_s_querydomaintoolong]  = STAT(dns_failure),
 
	/* permanent errors */
	[adns_s_nxdomain]            = STAT(dns_not_found),
	[adns_s_nodata]              = STAT(dns_not_found),
#undef STAT	
};

/* Convert ADNS status code E to DNS status. */
static int
adns_to_dns_status(int e)
{
	int r;

	/* If it is negative, fail right away */
	if (e < 0)
		return dns_failure;
	/* If it is not in table, it still can be a valid, but unhandled
	   value */
	if (e >= MU_ARRAY_SIZE(adns_to_dns_tab))
		return e < adns_s_max_permfail ? dns_not_found : dns_failure;
	/* Now, look up in the table */
	if ((r = adns_to_dns_tab[e]) > 0)
		return r - 1;
	/* If not found in table, use adns_s_max_ constants to decide the
	   error class.
	*/
	if (e < adns_s_max_localfail)
		return dns_failure;
	if (e < adns_s_max_remotefail)
		return dns_not_found;
	if (e < adns_s_max_tempfail)
		return dns_temp_failure;
	if (e < adns_s_max_misconfig)
		return dns_not_found;
	if (e < adns_s_max_misquery)
		return dns_not_found;
	return dns_not_found;
}

dns_status
soa_check(const char *name, int ip, struct dns_reply *reply)
{
	dns_status status = dns_failure;
	int rc;
	adns_answer *ans;

	rc = adns_synchronous(get_state(), name, adns_r_soa_raw,
			      DEFAULT_QFLAGS,
			      &ans);
	if (rc)
		return errno_to_dns_status(rc);
	status = adns_to_dns_status(ans->status);
	if (status == dns_success) {
		if (ip) {
			status = a_lookup(ans->rrs.soa->mname, reply);
		} else {
			dns_reply_init(reply, dns_reply_str, 1);
			reply->data.str[0] = mu_strdup (ans->rrs.soa->mname);
		}
		free(ans);
	}
	return status;
}

static dns_status
dns_reply_resolve(struct dns_reply *reply)
{
	size_t i;
	struct dns_reply res;
	
	dns_reply_init(&res, dns_reply_ip, 0);
	for (i = 0; i < reply->count; i++) {
		struct dns_reply r;
		dns_status stat = a_lookup(reply->data.str[i], &r);
		if (stat == dns_success) {
			size_t n;
			for (n = 0; n < r.count; n++) {
				dns_reply_push(&res, &r.data.ip[n]);
			}
			dns_reply_free(&r);
		}
	}
	dns_reply_free(reply);
	*reply = res;
	if (res.count == 0) 
		return dns_not_found;
	return dns_success;
}

/* Return MX records for the given HOST. */
dns_status
mx_lookup(const char *host, int resolve, struct dns_reply *reply)
{
	dns_status status = dns_failure;
	int rc;
	adns_answer *ans;
	int i;

	rc = adns_synchronous(get_state(), host, adns_r_mx,
			      DEFAULT_QFLAGS,
			      &ans);
	if (rc)
		return errno_to_dns_status(rc);
	status = adns_to_dns_status(ans->status);
	if (status != dns_success)
		return status;
	
	dns_reply_init(reply, dns_reply_str, ans->nrrs);
	for (i = 0; i < ans->nrrs; i++)
		reply->data.str[i] = mu_strdup(ans->rrs.inthostaddr[i].ha.host);
	free(ans);

	if (resolve)
		status = dns_reply_resolve(reply);
	
	return status;
}

typedef char IPBUF[3*4+3+1];

int
dns_reverse_ipstr(const char *ipstr, char *revipstr)
{
	int i;
	const char *p;
	char *q;

	q = revipstr + strlen(ipstr);
	*q = 0;
	for (i = 0, p = ipstr; *p && i < 4; i++) {
		int len;
			
		for (len = 0; p[len] && p[len] != '.'; len++)
			;
		q -= len;
		memcpy(q, p, len);
		if (q > revipstr)
			*--q = '.';
		p += len;
		if (*p == '.')
			p++;
	}
	
	return *p || i != 4;
}

dns_status
dns_resolve_ipstr(const char *ipstr, const char *domain, char **hbuf)
{
	dns_status status = dns_failure;
	int rc;
	adns_answer *ans;
	char *name;
	adns_rrtype type;
	
	if (!domain || strcasecmp(domain, "in-addr.arpa") == 0) {
		IPBUF ipbuf;
		if (!dns_str_is_ipv4(ipstr))
			return dns_failure;
                if (dns_reverse_ipstr(ipstr, ipbuf))
                        return dns_failure;
		mu_asprintf(&name, "%s.in-addr.arpa", ipbuf);
		type = adns_r_ptr_raw;
        } else {
		mu_asprintf(&name, "%s.%s", ipstr, domain);
		type = adns_r_a;
        }

	rc = adns_synchronous(get_state(), name, type,
			      DEFAULT_QFLAGS,
			      &ans);
	free(name);
	if (rc)
		return errno_to_dns_status(rc);
	status = adns_to_dns_status(ans->status);
	if (status == dns_success) {
		if (ans->type == adns_r_ptr_raw) {
			*hbuf = mu_strdup(ans->rrs.str[0]);
		} else {
			*hbuf = mu_strdup(inet_ntoa(ans->rrs.inaddr[0]));
		}
	}
	free(ans);
	return status;
}

dns_status
dns_resolve_hostname(const char *host, char **ipbuf)
{
	dns_status status = dns_failure;
	int rc;
	adns_answer *ans;
	
	rc = adns_synchronous(get_state(), host, adns_r_a,
			      DEFAULT_QFLAGS,
			      &ans);
	if (rc)
		return errno_to_dns_status(rc);
	status = adns_to_dns_status(ans->status);
	if (status == dns_success)
		*ipbuf = mu_strdup(inet_ntoa(ans->rrs.inaddr[0]));
	free(ans);
	return status;
}
	

dns_status
a_lookup(const char *host, struct dns_reply *reply)
{
	dns_status status = dns_failure;
	int rc;
	adns_answer *ans;
	
	rc = adns_synchronous(get_state(), host, adns_r_a,
			      DEFAULT_QFLAGS,
			      &ans);
	if (rc)
		return errno_to_dns_status(rc);
	status = adns_to_dns_status(ans->status);
	if (status == dns_success) {
		int i;
		dns_reply_init(reply, dns_reply_ip, ans->nrrs);
		for (i = 0; i < ans->nrrs; i++)
			reply->data.ip[i] = ans->rrs.inaddr[i].s_addr;
	}
	free(ans);
	return status;
}

dns_status
ptr_lookup(struct in_addr ip, struct dns_reply *reply)
{
	dns_status status = dns_failure;
	int rc;
	adns_answer *ans;
	char *name;

	ip.s_addr = ntohl(ip.s_addr);
	mu_asprintf(&name, "%d.%d.%d.%d.in-addr.arpa",
		    ip.s_addr & 0xff,
		    (ip.s_addr >> 8) & 0xff,
		    (ip.s_addr >> 16) & 0xff,
		    (ip.s_addr >> 24) & 0xff);
	rc = adns_synchronous(get_state(), name, adns_r_ptr_raw,
			      DEFAULT_QFLAGS,
			      &ans);
	free(name);
	if (rc)
		return errno_to_dns_status(rc);
	status = adns_to_dns_status(ans->status);
	if (status == dns_success) {
		int i;
		dns_reply_init(reply, dns_reply_str, ans->nrrs);
		for (i = 0; i < ans->nrrs; i++)
			reply->data.str[i] = mu_strdup(ans->rrs.str[i]);
	}
	free(ans);
	return status;
}

dns_status
txt_lookup(const char *name, struct dns_reply *reply)
{
	dns_status status = dns_failure;
	int rc;
	adns_answer *ans;

	rc = adns_synchronous(get_state(), name, adns_r_txt,
			      DEFAULT_QFLAGS,
			      &ans);
	if (rc)
		return errno_to_dns_status(rc);
	status = adns_to_dns_status(ans->status);
	if (status == dns_success) {
		int i;
		dns_reply_init(reply, dns_reply_str, ans->nrrs);
		for (i = 0; i < ans->nrrs; i++) {
			size_t l = 0;
			int j;
			for (j = 0; ans->rrs.manyistr[i][j].i > 0; j++)
				l += ans->rrs.manyistr[i][j].i;
			reply->data.str[i] = mu_alloc(l + 1);
			reply->data.str[i][0] = 0;
			l = 0;
			for (j = 0; ans->rrs.manyistr[i][j].i > 0; j++) {
				memcpy(reply->data.str[i] + l,
				       ans->rrs.manyistr[i][j].str,
				       ans->rrs.manyistr[i][j].i);
				l += ans->rrs.manyistr[i][j].i;
			}
			reply->data.str[i][l] = 0;
		}
	}
	free(ans);
	return status;
}

#define VSPF1_STR "v=spf1"
#define VSPF1_LEN (sizeof(VSPF1_STR)-1)

dns_status
spf_lookup(const char *domain, char **rec)
{
	dns_status status;
	struct dns_reply reply;

	status = txt_lookup(domain, &reply);
	if (status == dns_success) {
		int i;
		int found = -1;
		
		status = dns_not_found;
		
		for (i = 0; i < reply.count; i++) {
			if (mu_c_strncasecmp(reply.data.str[i],
					     VSPF1_STR, VSPF1_LEN) == 0
			    && (reply.data.str[i][VSPF1_LEN] == 0
				|| mu_isspace(reply.data.str[i][VSPF1_LEN]))) {
				if (found == -1) {
					found = i;
					status = dns_success;
				} else {
					found = -1;
					status = dns_too_many;
					break;
				}
			}
		}

		if (status == dns_success)
			*rec = mu_strdup(reply.data.str[found]);
				
		dns_reply_free(&reply);
	}
	return status;
}

dns_status
dkim_lookup(const char *domain, const char *sel, char ***retval)
{
	dns_status status;
	struct dns_reply reply;
	char *dk;
	
	if (mu_asprintf(&dk, "%s._domainkey.%s", sel, domain))
		mu_alloc_die();
	status = txt_lookup(dk, &reply);
	free(dk);
	if (status == dns_success) {
		int i;
		char **rv = mu_calloc(reply.count + 1, sizeof(*rv));

		for (i = 0; i < reply.count; i++) {
			rv[i] = mu_strdup(reply.data.str[i]);
		}
		*retval = rv;
		dns_reply_free(&reply);
	}
	return status;
}

/* rfc4408, chapter 5.5 */
dns_status
ptr_validate(const char *ipstr, struct dns_reply *reply)
{
	struct in_addr ip;
	size_t i;
	dns_status status;
	struct dns_reply ptr_reply;
	dns_status result = dns_not_found;
	
	if (!inet_aton(ipstr, &ip))
		return dns_failure;
	
	status = ptr_lookup(ip, &ptr_reply);
		
	if (status != dns_success)
		return status;

	if (reply)
		dns_reply_init(reply, dns_reply_str, 0);
	for (i = 0; i < ptr_reply.count; i++) {
		struct dns_reply r;
		status = a_lookup(ptr_reply.data.str[i], &r);
		if (status == dns_success) {
			size_t k;

			for (k = 0; k < r.count; k++) {
				if (r.data.ip[k] == ip.s_addr) {
					result = dns_success;
					if (reply)
						dns_reply_push(reply,
							       mu_strdup(ptr_reply.data.str[i]));
					break;
				}
			}
			dns_reply_free(&r);
		}
	}
	dns_reply_free(&ptr_reply);
	
	return result;
}


mf_status
dns_to_mf_status(dns_status stat)
{
	return (mf_status) stat;
}

dns_status
mf_to_dns_status(mf_status stat)
{
	return (dns_status) stat;
}

mf_status
resolve_ipstr_domain(const char *ipstr, const char *domain, char **phbuf)
{
	char *hbuf;
	dns_status dstat;
		
	mu_debug(debug_handle, MU_DEBUG_TRACE8,
		 ("Getting canonical name for %s", ipstr));

	dstat = dns_resolve_ipstr(ipstr, domain, &hbuf);

	switch (dstat) {
	case dns_success:
		mu_debug(debug_handle, MU_DEBUG_TRACE8, 
			 ("%s resolved to %s", ipstr, hbuf));
		*phbuf = hbuf;
		break;

	default:
		mu_debug(debug_handle, MU_DEBUG_TRACE8,
			 ("%s not resolved", ipstr));
	}
	return dns_to_mf_status(dstat);
}

mf_status
resolve_ipstr(const char *ipstr, char **phbuf)	
{
	return resolve_ipstr_domain(ipstr, NULL, phbuf);
}

mf_status
resolve_hostname(const char *host, char **pipbuf)	
{
	char *ipbuf;
	dns_status dstat;

	mu_debug(debug_handle, MU_DEBUG_TRACE8,
		 ("Getting IP address for %s", host));
		
	dstat = dns_resolve_hostname(host, &ipbuf);
	switch (dstat) {
	case dns_success:
		mu_debug(debug_handle, MU_DEBUG_TRACE8, 
			 ("%s resolved to %s", host, ipbuf));
		*pipbuf = ipbuf;
		break;
			
	default:
		mu_debug(debug_handle, MU_DEBUG_TRACE8,
			 ("%s not resolved", host));
	}
	return dns_to_mf_status(dstat);
}

/* Return NS records for the given DOMAIN. */
dns_status
ns_lookup(const char *domain, int resolve, struct dns_reply *reply)
{
	dns_status status = dns_failure;
	int rc;
	adns_answer *ans;
	int i;

	rc = adns_synchronous(get_state(), domain, adns_r_ns_raw,
			      DEFAULT_QFLAGS,
			      &ans);
	if (rc)
		return errno_to_dns_status(rc);
	status = adns_to_dns_status(ans->status);
	if (status != dns_success)
		return status;
	
	dns_reply_init(reply, dns_reply_str, ans->nrrs);
	for (i = 0; i < ans->nrrs; i++)
		reply->data.str[i] = mu_strdup(ans->rrs.str[i]);
	free(ans);

	if (resolve)
		status = dns_reply_resolve(reply);
	
	return status;
}
