/*
 * Copyright (c) 2014-2020 by Farsight Security, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* Note that cygwin has a crippled libresolv that does not
 * include the not so recent ns_initparse() function, etc.
 * This AS info functionality is thus not available
 * in cygwin.
 */

/* external. */

/* asprintf() does not appear on linux without this */
#define _GNU_SOURCE

#include <arpa/inet.h>
#include <arpa/nameser.h>
#include <netinet/in.h>
#include <errno.h>
#include <netdb.h>
#include <resolv.h>
#include <stdlib.h>
#include <string.h>
#include "globals.h"

#ifndef CRIPPLED_LIBC  /* must be after globals.h - which includes defs.h */

#include "asinfo.h"

/* private. */

static struct __res_state res;

/* forward. */

static char *asinfo_from_ipv4(const char *, char **, char **);
#ifdef asinfo_ipv6
static const char *asinfo_from_ipv6(const char *, char **, char **);
#endif
static char *asinfo_from_dns(const char *, char **, char **);
static const char *keep_best(char **, char **, char *, char *);

/* public. */

/* asinfo_from_rr(rrtype, rdata, asnum, cidr) -- find ASINFO for A/AAAA string
 *
 * return NULL on success, or else, reason (malloc'd string) for failure.
 *
 * side effect: on success, *asnum and *cidr will be heap-allocated strings.
 */
char *
asinfo_from_rr(const char *rrtype, const char *rdata,
	       char **asnum, char **cidr)
{
	if (asinfo_lookup) {
		if (strcmp(rrtype, "A") == 0)
			return asinfo_from_ipv4(rdata, asnum, cidr);
#ifdef asinfo_ipv6
		if (strcmp(rrtype, "AAAA") == 0)
			return asinfo_from_ipv6(rdata, asnum, cidr);
#endif
	}
	return NULL;
}

/* asinfo_domain_exists(domain) -- verify DNS-level existence of a domain
 *
 * return boolean -- does this domain exist in some form?
 */
bool
asinfo_domain_exists(const char *domain) {
	u_char buf[NS_PACKETSZ];

	return res_query(domain, ns_c_in, ns_t_txt, buf, sizeof buf) > 0 ||
		_res.res_h_errno != HOST_NOT_FOUND;
}

/* asinfo_shutdown() -- deallocate underlying library's heap resources
 */
void
asinfo_shutdown(void) {
	if ((res.options & RES_INIT) != 0)
		res_nclose(&res);
}

/* static. */

/* asinfo_from_ipv4(addr, asnum, cidr) -- prepare and use ASINFO IPv4 name
 *
 * return NULL on success, or else, reason (malloc'd string) for failure.
 *
 * side effect: on success, *asnum and *cidr will be heap-allocated strings.
 */
static char *
asinfo_from_ipv4(const char *addr, char **asnum, char **cidr) {
	u_char a4[32/8];
	char *dname;

	if (inet_pton(AF_INET, addr, a4) < 0)
		return strdup(strerror(errno));
	int n = asprintf(&dname, "%d.%d.%d.%d.%s",
			 a4[3], a4[2], a4[1], a4[0], asinfo_domain);
	if (n < 0)
		return strdup(strerror(errno));
	char *result = asinfo_from_dns(dname, asnum, cidr);
	free(dname);
	return result;
}

#ifdef asinfo_ipv6
/* asinfo_from_ipv6(addr, asnum, cidr) -- prepare and use ASINFO IPv6 name
 *
 * return NULL on success, or else, reason (malloc'd string) for failure.
 *
 * side effect: on success, *asnum and *cidr will be heap-allocated strings.
 *
 * NOTE WELL: this is a placeholder, since no ASINFO source has working IPv6.
 */
static char *
asinfo_from_ipv6(const char *addr, char **asnum, char **cidr) {
	char *result, *dname, *p;
	u_char a6[128/8];
	int i;

	if (inet_pton(AF_INET6, addr, &a6) < 0)
		return strdup(strerror(errno));
	dname = malloc(strlen(asinfo_domain) + (128/4)*2);
	if (dname == NULL)
		return strdup(strerror(errno));
	result = NULL;
	p = dname;
	for (i = (128/8) - 1; i >= 0; i--) {
		int n = sprintf(p, "%x.%x.", a6[i] & 0xf, a6[i] >> 4);
		if (n < 0) {
			result = strdup(strerror(errno));
			break;
		}
		p += n;
	}
	if (result == NULL) {
		strcpy(p, asinfo_domain);
		result = asinfo_from_dns(dname, asnum, cidr);
	}
	p = NULL;
	free(dname);
	return result;
}
#endif

/* asinfo_from_dns(dname, asnum, cidr) -- retrieve and parse a ASINFO DNS TXT
 *
 * return NULL on success, or else, reason (malloc'd string) for failure.
 *
 * side effect: on success, *asnum and *cidr will be heap-allocated strings.
 */
static char *
asinfo_from_dns(const char *dname, char **asnum, char **cidr) {
	u_char buf[NS_PACKETSZ];
	int n, an, rrn, rcode;
	char *result;
	ns_msg msg;
	ns_rr rr;

	DEBUG(1, true, "asinfo_from_dns(%s)\n", dname);
	if ((res.options & RES_INIT) == 0)
		res_ninit(&res);
	n = res_nquery(&res, dname, ns_c_in, ns_t_txt, buf, sizeof buf);
	if (n < 0) {
		if (res.res_h_errno == HOST_NOT_FOUND)
			return NULL;
		else
			return strdup(hstrerror(res.res_h_errno));
	}
	if (ns_initparse(buf, n, &msg) < 0)
		return strdup(strerror(errno));
	rcode = ns_msg_getflag(msg, ns_f_rcode);
	if (rcode != ns_r_noerror) {
		if (asprintf(&result, "DNS RCODE 0x%x", rcode) < 0)
			return strdup(strerror(errno));
		return result;
	}
	an = ns_msg_count(msg, ns_s_an);
	if (an == 0)
		return strdup("ANCOUNT == 0");
	/* some ASINFO data sources return multiple TXT RR's, each having
	 * a prefix length measured in bits. we will select the best
	 * (longest match) prefix offered.
	 */
	for (result = NULL, rrn = 0; result == NULL && rrn < an; rrn++) {
		const u_char *rdata;
		int rdlen, ntxt;
		char *txt[3];

		if (ns_parserr(&msg, ns_s_an, rrn, &rr) < 0) {
			result = strdup(strerror(errno));
			break;
		}
		if (ns_rr_type(rr) != ns_t_txt)
			goto next_rr;
		rdata = ns_rr_rdata(rr);
		rdlen = ns_rr_rdlen(rr);
		ntxt = 0;
		while (rdlen > 0) {
			/* no current ASINFO source has a TXT schema having
			 * more than three TXT segments (<character-strings>).
			 */
			if (ntxt == 3) {
				result = strdup("len(TXT[]) > 3");
				break;
			}
			n = *rdata++;
			rdlen--;
			if (n > rdlen) {
				result = strdup("TXT overrun");
				break;
			}
			txt[ntxt] = strndup((const char *)rdata, (size_t)n);
			if (txt[ntxt] == NULL) {
				result = strdup("strndup FAIL");
				break;
			}
			DEBUG(2, true, "TXT[%d] \"%s\"\n", ntxt, txt[ntxt]);
			rdata += n;
			rdlen -= n;
			ntxt++;
		}

		if (result == NULL) {
			const int seplen = sizeof " | " - 1;
			const char *t1 = NULL, *t2 = NULL;

			if (ntxt == 1 &&
			    (t1 = strstr(txt[0], " | ")) != NULL &&
			    (t2 = strstr(t1 + seplen, " | ")) != NULL)
			{
				/* team-cymru.com format:
				 *
				 * one TXT segment per TXT RR, having
				 * internal structure of vertical bar (|)
				 * separated fields, of which the first
				 * two are our desired output values
				 * (AS number or path or set; CIDR prefix).
				 */
				char *new_asnum, *new_cidr;
				new_asnum = strndup(txt[0], (size_t)
						    (t1 - txt[0]));
				new_cidr = strndup(t1 + seplen, (size_t)
						   (t2 - (t1 + seplen)));
				t1 = t2 = NULL;
				const char *t = keep_best(asnum, cidr,
							  new_asnum,
							  new_cidr);
				if (t != NULL)
					result = strdup(t);
			} else if (ntxt == 3) {
				/* routeviews.org format:
				 *
				 * three TXT segments per TXT RR, which are
				 * the AS number or path or set, and the
				 * prefix mantissa, and the prefix length.
				 * we use the first directly, and combine
				 * the second and third to form CIDR prefix.
				 */
				char *new_asnum, *new_cidr;
				if (asprintf(&new_cidr, "%s/%s",
					     txt[1], txt[2]) >= 0)
				{
					new_asnum = strdup(txt[0]);
					const char *t = keep_best(asnum, cidr,
								  new_asnum,
								  new_cidr);
					if (t != NULL)
						result = strdup(t);
				} else {
					result = strdup(strerror(errno));
				}
			} else {
				result = strdup("unrecognized TXT format");
			}
		}
		for (n = 0; n < ntxt; n++) {
			free(txt[n]);
			txt[n] = NULL;
		}
 next_rr:;
	}
	return result;
}

/* keep_best(asnum, cidr, new_asnum, new_cidr) -- select/keep "best" ASINFO
 *
 * return NULL on success, or else, reason (string) for failure.
 *
 * side effect: on success, *asnum and *cidr will be heap-allocated strings.
 */
static const char *
keep_best(char **asnum, char **cidr, char *new_asnum, char *new_cidr) {
	if (*asnum != NULL && *cidr != NULL) {
		int pfxlen = -1, new_pfxlen = -1;
		char *cp;

		if ((cp = strchr(*cidr, '/')) == NULL ||
		    (pfxlen = atoi(cp+1)) <= 0 || pfxlen > 128)
			return "bad CIDR syntax (old)";
		if ((cp = strchr(new_cidr, '/')) == NULL ||
		    (new_pfxlen = atoi(cp+1)) <= 0 || new_pfxlen > 128)
			return "bad CIDR syntax (new)";
		if (new_pfxlen <= pfxlen) {
			free(new_asnum);
			free(new_cidr);
			return NULL;
		}
		free(*asnum);
		*asnum = NULL;
		free(*cidr);
		*cidr = NULL;
	}
	if (strcmp(new_asnum, "4294967295") == 0) {
		/* in routeviews.org, this is how they signal "unknown". */
		free(new_asnum);
		free(new_cidr);
	} else {
		*asnum = new_asnum;
		*cidr = new_cidr;
	}
	return NULL;
}
#endif /*CRIPPLED_LIBC*/
