/*
   Unix SMB/CIFS implementation.

   DNS server utils

   Copyright (C) 2010 Kai Blin
   Copyright (C) 2014 Stefan Metzmacher
   Copyright (C) 2015 Andrew Bartlett

   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 of the License, 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/>.
*/

#include "includes.h"
#include "libcli/util/ntstatus.h"
#include "libcli/util/werror.h"
#include "librpc/ndr/libndr.h"
#include "librpc/gen_ndr/ndr_dns.h"
#include "librpc/gen_ndr/ndr_dnsp.h"
#include <ldb.h>
#include "dsdb/samdb/samdb.h"
#include "dsdb/common/util.h"
#include "dns_server/dnsserver_common.h"
#include "rpc_server/dnsserver/dnsserver.h"
#include "lib/util/dlinklist.h"
#include "system/network.h"

#undef DBGC_CLASS
#define DBGC_CLASS DBGC_DNS

#undef strncasecmp

uint8_t werr_to_dns_err(WERROR werr)
{
	if (W_ERROR_EQUAL(WERR_OK, werr)) {
		return DNS_RCODE_OK;
	} else if (W_ERROR_EQUAL(DNS_ERR(FORMAT_ERROR), werr)) {
		return DNS_RCODE_FORMERR;
	} else if (W_ERROR_EQUAL(DNS_ERR(SERVER_FAILURE), werr)) {
		return DNS_RCODE_SERVFAIL;
	} else if (W_ERROR_EQUAL(DNS_ERR(NAME_ERROR), werr)) {
		return DNS_RCODE_NXDOMAIN;
	} else if (W_ERROR_EQUAL(WERR_DNS_ERROR_NAME_DOES_NOT_EXIST, werr)) {
		return DNS_RCODE_NXDOMAIN;
	} else if (W_ERROR_EQUAL(DNS_ERR(NOT_IMPLEMENTED), werr)) {
		return DNS_RCODE_NOTIMP;
	} else if (W_ERROR_EQUAL(DNS_ERR(REFUSED), werr)) {
		return DNS_RCODE_REFUSED;
	} else if (W_ERROR_EQUAL(DNS_ERR(YXDOMAIN), werr)) {
		return DNS_RCODE_YXDOMAIN;
	} else if (W_ERROR_EQUAL(DNS_ERR(YXRRSET), werr)) {
		return DNS_RCODE_YXRRSET;
	} else if (W_ERROR_EQUAL(DNS_ERR(NXRRSET), werr)) {
		return DNS_RCODE_NXRRSET;
	} else if (W_ERROR_EQUAL(DNS_ERR(NOTAUTH), werr)) {
		return DNS_RCODE_NOTAUTH;
	} else if (W_ERROR_EQUAL(DNS_ERR(NOTZONE), werr)) {
		return DNS_RCODE_NOTZONE;
	} else if (W_ERROR_EQUAL(DNS_ERR(BADKEY), werr)) {
		return DNS_RCODE_BADKEY;
	} else if (W_ERROR_EQUAL(WERR_ACCESS_DENIED, werr)) {
		return DNS_RCODE_REFUSED;
	}
	DEBUG(5, ("No mapping exists for %s\n", win_errstr(werr)));
	return DNS_RCODE_SERVFAIL;
}

WERROR dns_common_extract(struct ldb_context *samdb,
			  const struct ldb_message_element *el,
			  TALLOC_CTX *mem_ctx,
			  struct dnsp_DnssrvRpcRecord **records,
			  uint16_t *num_records)
{
	uint16_t ri;
	struct dnsp_DnssrvRpcRecord *recs;

	*records = NULL;
	*num_records = 0;

	recs = talloc_zero_array(mem_ctx, struct dnsp_DnssrvRpcRecord,
				 el->num_values);
	if (recs == NULL) {
		return WERR_NOT_ENOUGH_MEMORY;
	}
	for (ri = 0; ri < el->num_values; ri++) {
		bool am_rodc;
		int ret;
		const char *dnsHostName = NULL;
		struct ldb_val *v = &el->values[ri];
		enum ndr_err_code ndr_err;
		ndr_err = ndr_pull_struct_blob(v, recs, &recs[ri],
				(ndr_pull_flags_fn_t)ndr_pull_dnsp_DnssrvRpcRecord);
		if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
			TALLOC_FREE(recs);
			DEBUG(0, ("Failed to grab dnsp_DnssrvRpcRecord\n"));
			return DNS_ERR(SERVER_FAILURE);
		}

		/*
		 * In AD, except on an RODC (where we should list a random RWDC,
		 * we should over-stamp the MNAME with our own hostname
		 */
		if (recs[ri].wType != DNS_TYPE_SOA) {
			continue;
		}

		ret = samdb_rodc(samdb, &am_rodc);
		if (ret != LDB_SUCCESS) {
			DEBUG(0, ("Failed to confirm we are not an RODC: %s\n",
				  ldb_errstring(samdb)));
			return DNS_ERR(SERVER_FAILURE);
		}

		if (am_rodc) {
			continue;
		}

		ret = samdb_dns_host_name(samdb, &dnsHostName);
		if (ret != LDB_SUCCESS || dnsHostName == NULL) {
			DEBUG(0, ("Failed to get dnsHostName from rootDSE\n"));
			return DNS_ERR(SERVER_FAILURE);
		}

		recs[ri].data.soa.mname = talloc_strdup(recs, dnsHostName);
	}

	*records = recs;
	*num_records = el->num_values;
	return WERR_OK;
}

/*
 * Lookup a DNS record, performing an exact match.
 * i.e. DNS wild card records are not considered.
 */
WERROR dns_common_lookup(struct ldb_context *samdb,
			 TALLOC_CTX *mem_ctx,
			 struct ldb_dn *dn,
			 struct dnsp_DnssrvRpcRecord **records,
			 uint16_t *num_records,
			 bool *tombstoned)
{
	const struct timeval start = timeval_current();
	static const char * const attrs[] = {
		"dnsRecord",
		"dNSTombstoned",
		NULL
	};
	int ret;
	WERROR werr = WERR_OK;
	struct ldb_message *msg = NULL;
	struct ldb_message_element *el;

	*records = NULL;
	*num_records = 0;

	if (tombstoned != NULL) {
		*tombstoned = false;
		ret = dsdb_search_one(samdb, mem_ctx, &msg, dn,
			LDB_SCOPE_BASE, attrs, 0,
			"(objectClass=dnsNode)");
	} else {
		ret = dsdb_search_one(samdb, mem_ctx, &msg, dn,
			LDB_SCOPE_BASE, attrs, 0,
			"(&(objectClass=dnsNode)(!(dNSTombstoned=TRUE)))");
	}
	if (ret == LDB_ERR_NO_SUCH_OBJECT) {
		werr = WERR_DNS_ERROR_NAME_DOES_NOT_EXIST;
		goto exit;
	}
	if (ret != LDB_SUCCESS) {
		/* TODO: we need to check if there's a glue record we need to
		 * create a referral to */
		werr = DNS_ERR(NAME_ERROR);
		goto exit;
	}

	if (tombstoned != NULL) {
		*tombstoned = ldb_msg_find_attr_as_bool(msg,
					"dNSTombstoned", false);
	}

	el = ldb_msg_find_element(msg, "dnsRecord");
	if (el == NULL) {
		TALLOC_FREE(msg);
		/*
		 * records produced by older Samba releases
		 * keep dnsNode objects without dnsRecord and
		 * without setting dNSTombstoned=TRUE.
		 *
		 * We just pretend they're tombstones.
		 */
		if (tombstoned != NULL) {
			struct dnsp_DnssrvRpcRecord *recs;
			recs = talloc_array(mem_ctx,
					    struct dnsp_DnssrvRpcRecord,
					    1);
			if (recs == NULL) {
				werr = WERR_NOT_ENOUGH_MEMORY;
				goto exit;
			}
			recs[0] = (struct dnsp_DnssrvRpcRecord) {
				.wType = DNS_TYPE_TOMBSTONE,
				/*
				 * A value of timestamp != 0
				 * indicated that the object was already
				 * a tombstone, this will be used
				 * in dns_common_replace()
				 */
				.data.EntombedTime = 1,
			};

			*tombstoned = true;
			*records = recs;
			*num_records = 1;
			werr = WERR_OK;
			goto exit;
		} else {
			/*
			 * Because we are not looking for a tombstone
			 * in this codepath, we just pretend it does
			 * not exist at all.
			 */
			werr = WERR_DNS_ERROR_NAME_DOES_NOT_EXIST;
			goto exit;
		}
	}

	werr = dns_common_extract(samdb, el, mem_ctx, records, num_records);
	TALLOC_FREE(msg);
	if (!W_ERROR_IS_OK(werr)) {
		goto exit;
	}

	werr = WERR_OK;
exit:
	DNS_COMMON_LOG_OPERATION(
		win_errstr(werr),
		&start,
		NULL,
		dn == NULL ? NULL : ldb_dn_get_linearized(dn),
		NULL);
	return werr;
}

/*
 * Build an ldb_parse_tree node for an equality check
 *
 * Note: name is assumed to have been validated by dns_name_check
 *       so will be zero terminated and of a reasonable size.
 */
static struct ldb_parse_tree *build_equality_operation(
	TALLOC_CTX *mem_ctx,
	bool add_asterix,     /* prepend an '*' to the name          */
	const uint8_t *name,  /* the value being matched             */
	const char *attr,     /* the attribute to check name against */
	size_t size)          /* length of name                      */
{

	struct ldb_parse_tree *el = NULL;  /* Equality node being built */
	struct ldb_val *value = NULL;      /* Value the attr will be compared
					      with */
	size_t length = 0;                 /* calculated length of the value
	                                      including option '*' prefix and
					      '\0' string terminator */

	el = talloc(mem_ctx, struct ldb_parse_tree);
	if (el == NULL) {
		DBG_ERR("Unable to allocate ldb_parse_tree\n");
		return NULL;
	}

	el->operation = LDB_OP_EQUALITY;
	el->u.equality.attr = talloc_strdup(mem_ctx, attr);
	value = &el->u.equality.value;
	length = (add_asterix) ? size + 2 : size + 1;
	value->data = talloc_zero_array(el, uint8_t, length);
	if (value->data == NULL) {
		DBG_ERR("Unable to allocate value->data\n");
		TALLOC_FREE(el);
		return NULL;
	}

	value->length = length;
	if (add_asterix) {
		value->data[0] = '*';
		if (name != NULL) {
			memcpy(&value->data[1], name, size);
		}
	} else if (name != NULL) {
		memcpy(value->data, name, size);
	}
	return el;
}

/*
 * Determine the number of levels in name
 * essentially the number of '.'s in the name + 1
 *
 * name is assumed to have been validated by dns_name_check
 */
static unsigned int number_of_labels(const struct ldb_val *name) {
	int x  = 0;
	unsigned int labels = 1;
	for (x = 0; x < name->length; x++) {
		if (name->data[x] == '.') {
			labels++;
		}
	}
	return labels;
}
/*
 * Build a query that matches the target name, and any possible
 * DNS wild card entries
 *
 * Builds a parse tree equivalent to the example query.
 *
 * x.y.z -> (|(name=x.y.z)(name=\2a.y.z)(name=\2a.z)(name=\2a))
 *
 * The attribute 'name' is used as this is what the LDB index is on
 * (the RDN, being 'dc' in this use case, does not have an index in
 * the AD schema).
 *
 * Returns NULL if unable to build the query.
 *
 * The first component of the DN is assumed to be the name being looked up
 * and also that it has been validated by dns_name_check
 *
 */
#define BASE "(&(objectClass=dnsNode)(!(dNSTombstoned=TRUE))(|(a=b)(c=d)))"
static struct ldb_parse_tree *build_wildcard_query(
	TALLOC_CTX *mem_ctx,
	struct ldb_dn *dn)
{
	const struct ldb_val *name = NULL;            /* The DNS name being
							 queried */
	const char *attr = "name";                    /* The attribute name */
	struct ldb_parse_tree *query = NULL;          /* The constructed query
							 parse tree*/
	struct ldb_parse_tree *wildcard_query = NULL; /* The parse tree for the
							 name and wild card
							 entries */
	int labels = 0;         /* The number of labels in the name */

	name = ldb_dn_get_rdn_val(dn);
	if (name == NULL) {
		DBG_ERR("Unable to get domain name value\n");
		return NULL;
	}
	labels = number_of_labels(name);

	query = ldb_parse_tree(mem_ctx, BASE);
	if (query == NULL) {
		DBG_ERR("Unable to parse query %s\n", BASE);
		return NULL;
	}

	/*
	 * The 3rd element of BASE is a place holder which is replaced with
	 * the actual wild card query
	 */
	wildcard_query = query->u.list.elements[2];
	TALLOC_FREE(wildcard_query->u.list.elements);

	wildcard_query->u.list.num_elements = labels + 1;
	wildcard_query->u.list.elements = talloc_array(
		wildcard_query,
		struct ldb_parse_tree *,
		labels + 1);
	/*
	 * Build the wild card query
	 */
	{
		int x = 0;   /* current character in the name               */
		int l = 0;   /* current equality operator index in elements */
		struct ldb_parse_tree *el = NULL; /* Equality operator being
						     built */
		bool add_asterix = true;  /* prepend an '*' to the value    */
		for (l = 0, x = 0; l < labels && x < name->length; l++) {
			unsigned int size = name->length - x;
			add_asterix = (name->data[x] == '.');
			el = build_equality_operation(
				mem_ctx,
				add_asterix,
				&name->data[x],
				attr,
				size);
			if (el == NULL) {
				return NULL;  /* Reason will have been logged */
			}
			wildcard_query->u.list.elements[l] = el;

			/* skip to the start of the next label */
			x++;
			for (;x < name->length && name->data[x] != '.'; x++);
		}

		/* Add the base level "*" only query */
		el = build_equality_operation(mem_ctx, true, NULL, attr, 0);
		if (el == NULL) {
			TALLOC_FREE(query);
			return NULL;  /* Reason will have been logged */
		}
		wildcard_query->u.list.elements[l] = el;
	}
	return query;
}

/*
 * Scan the list of records matching a dns wildcard query and return the
 * best match.
 *
 * The best match is either an exact name match, or the longest wild card
 * entry returned
 *
 * i.e. name = a.b.c candidates *.b.c, *.c,        - *.b.c would be selected
 *      name = a.b.c candidates a.b.c, *.b.c, *.c  - a.b.c would be selected
 */
static struct ldb_message *get_best_match(struct ldb_dn *dn,
		                          struct ldb_result *result)
{
	int matched = 0;    /* Index of the current best match in result */
	size_t length = 0;  /* The length of the current candidate       */
	const struct ldb_val *target = NULL;    /* value we're looking for */
	const struct ldb_val *candidate = NULL; /* current candidate value */
	int x = 0;

	target = ldb_dn_get_rdn_val(dn);
	for(x = 0; x < result->count; x++) {
		candidate = ldb_dn_get_rdn_val(result->msgs[x]->dn);
		if (strncasecmp((char *) target->data,
				(char *) candidate->data,
				target->length) == 0) {
			/* Exact match stop searching and return */
			return result->msgs[x];
		}
		if (candidate->length > length) {
			matched = x;
			length  = candidate->length;
		}
	}
	return result->msgs[matched];
}

/*
 * Look up a DNS entry, if an exact match does not exist, return the
 * closest matching DNS wildcard entry if available
 *
 * Returns: LDB_ERR_NO_SUCH_OBJECT     If no matching record exists
 *          LDB_ERR_OPERATIONS_ERROR   If the query fails
 *          LDB_SUCCESS                If a matching record was retrieved
 *
 */
static int dns_wildcard_lookup(struct ldb_context *samdb,
			       TALLOC_CTX *mem_ctx,
			       struct ldb_dn *dn,
			       struct ldb_message **msg)
{
	static const char * const attrs[] = {
		"dnsRecord",
		"dNSTombstoned",
		NULL
	};
	struct ldb_dn *parent = NULL;     /* The parent dn                    */
	struct ldb_result *result = NULL; /* Results of the search            */
	int ret;                          /* Return code                      */
	struct ldb_parse_tree *query = NULL; /* The query to run              */
	struct ldb_request *request = NULL;  /* LDB request for the query op  */
	struct ldb_message *match = NULL;    /* the best matching DNS record  */
	TALLOC_CTX *frame = talloc_stackframe();

	parent = ldb_dn_get_parent(frame, dn);
	if (parent == NULL) {
		DBG_ERR("Unable to extract parent from dn\n");
		TALLOC_FREE(frame);
		return LDB_ERR_OPERATIONS_ERROR;
	}

	query = build_wildcard_query(frame, dn);
	if (query == NULL) {
		TALLOC_FREE(frame);
		return LDB_ERR_OPERATIONS_ERROR;
	}

	result = talloc_zero(mem_ctx, struct ldb_result);
	if (result == NULL) {
		TALLOC_FREE(frame);
		DBG_ERR("Unable to allocate ldb_result\n");
		return LDB_ERR_OPERATIONS_ERROR;
	}

	ret = ldb_build_search_req_ex(&request,
				      samdb,
				      frame,
				      parent,
				      LDB_SCOPE_SUBTREE,
				      query,
				      attrs,
				      NULL,
				      result,
				      ldb_search_default_callback,
				      NULL);
	if (ret != LDB_SUCCESS) {
		TALLOC_FREE(frame);
		DBG_ERR("ldb_build_search_req_ex returned %d\n", ret);
		return ret;
	}

	ret = ldb_request(samdb, request);
	if (ret != LDB_SUCCESS) {
		TALLOC_FREE(frame);
		return ret;
	}

	ret = ldb_wait(request->handle, LDB_WAIT_ALL);
	if (ret != LDB_SUCCESS) {
		TALLOC_FREE(frame);
		return ret;
	}

	if (result->count == 0) {
		TALLOC_FREE(frame);
		return LDB_ERR_NO_SUCH_OBJECT;
	}

	match = get_best_match(dn, result);
	if (match == NULL) {
		TALLOC_FREE(frame);
		return LDB_ERR_OPERATIONS_ERROR;
	}

	*msg = talloc_move(mem_ctx, &match);
	TALLOC_FREE(frame);
	return LDB_SUCCESS;
}

/*
 * Lookup a DNS record, will match DNS wild card records if an exact match
 * is not found.
 */
WERROR dns_common_wildcard_lookup(struct ldb_context *samdb,
				  TALLOC_CTX *mem_ctx,
				  struct ldb_dn *dn,
				  struct dnsp_DnssrvRpcRecord **records,
				  uint16_t *num_records)
{
	const struct timeval start = timeval_current();
	int ret;
	WERROR werr = WERR_OK;
	struct ldb_message *msg = NULL;
	struct ldb_message_element *el = NULL;
	const struct ldb_val *name = NULL;

	*records = NULL;
	*num_records = 0;

	name = ldb_dn_get_rdn_val(dn);
	if (name == NULL) {
		werr = DNS_ERR(NAME_ERROR);
		goto exit;
	}

	/* Don't look for a wildcard for @ */
	if (name->length == 1 && name->data[0] == '@') {
		werr = dns_common_lookup(samdb,
					 mem_ctx,
					 dn,
					 records,
					 num_records,
					 NULL);
		goto exit;
	}

	werr =  dns_name_check(
			mem_ctx,
			strlen((const char*)name->data),
			(const char*) name->data);
	if (!W_ERROR_IS_OK(werr)) {
		goto exit;
	}

	/*
	 * Do a point search first, then fall back to a wildcard
	 * lookup if it does not exist
	 */
	werr = dns_common_lookup(samdb,
				 mem_ctx,
				 dn,
				 records,
				 num_records,
				 NULL);
	if (!W_ERROR_EQUAL(werr, WERR_DNS_ERROR_NAME_DOES_NOT_EXIST)) {
		goto exit;
	}

	ret = dns_wildcard_lookup(samdb, mem_ctx, dn, &msg);
	if (ret == LDB_ERR_OPERATIONS_ERROR) {
		werr = DNS_ERR(SERVER_FAILURE);
		goto exit;
	}
	if (ret != LDB_SUCCESS) {
		werr = DNS_ERR(NAME_ERROR);
		goto exit;
	}

	el = ldb_msg_find_element(msg, "dnsRecord");
	if (el == NULL) {
		werr = WERR_DNS_ERROR_NAME_DOES_NOT_EXIST;
		goto exit;
	}

	werr = dns_common_extract(samdb, el, mem_ctx, records, num_records);
	TALLOC_FREE(msg);
	if (!W_ERROR_IS_OK(werr)) {
		goto exit;
	}

	werr = WERR_OK;
exit:
	DNS_COMMON_LOG_OPERATION(
		win_errstr(werr),
		&start,
		NULL,
		dn == NULL ? NULL : ldb_dn_get_linearized(dn),
		NULL);
	return werr;
}

static int rec_cmp(const struct dnsp_DnssrvRpcRecord *r1,
		   const struct dnsp_DnssrvRpcRecord *r2)
{
	if (r1->wType != r2->wType) {
		/*
		 * The records are sorted with higher types first,
		 * which puts tombstones (type 0) last.
		 */
		return NUMERIC_CMP(r2->wType, r1->wType);
	}
	/*
	 * Then we need to sort from the oldest to newest timestamp.
	 *
	 * Note that dwTimeStamp == 0 (never expiring) records come first,
	 * then the ones whose expiry is soonest.
	 */
	return NUMERIC_CMP(r1->dwTimeStamp, r2->dwTimeStamp);
}

/*
 * Check for valid DNS names. These are names which:
 *   - are non-empty
 *   - do not start with a dot
 *   - do not have any empty labels
 *   - have no more than 127 labels
 *   - are no longer than 253 characters
 *   - none of the labels exceed 63 characters
 */
WERROR dns_name_check(TALLOC_CTX *mem_ctx, size_t len, const char *name)
{
	size_t i;
	unsigned int labels    = 0;
	unsigned int label_len = 0;

	if (len == 0) {
		return WERR_DS_INVALID_DN_SYNTAX;
	}

	if (len > 1 && name[0] == '.') {
		return WERR_DS_INVALID_DN_SYNTAX;
	}

	if ((len - 1) > DNS_MAX_DOMAIN_LENGTH) {
		return WERR_DS_INVALID_DN_SYNTAX;
	}

	for (i = 0; i < len - 1; i++) {
		if (name[i] == '.' && name[i+1] == '.') {
			return WERR_DS_INVALID_DN_SYNTAX;
		}
		if (name[i] == '.') {
			labels++;
			if (labels > DNS_MAX_LABELS) {
				return WERR_DS_INVALID_DN_SYNTAX;
			}
			label_len = 0;
		} else {
			label_len++;
			if (label_len > DNS_MAX_LABEL_LENGTH) {
				return WERR_DS_INVALID_DN_SYNTAX;
			}
		}
	}

	return WERR_OK;
}

static WERROR check_name_list(TALLOC_CTX *mem_ctx, uint16_t rec_count,
			      struct dnsp_DnssrvRpcRecord *records)
{
	WERROR werr;
	uint16_t i;
	size_t len;
	struct dnsp_DnssrvRpcRecord record;

	werr = WERR_OK;
	for (i = 0; i < rec_count; i++) {
		record = records[i];

		switch (record.wType) {

		case DNS_TYPE_NS:
			len = strlen(record.data.ns);
			werr = dns_name_check(mem_ctx, len, record.data.ns);
			break;
		case DNS_TYPE_CNAME:
			len = strlen(record.data.cname);
			werr = dns_name_check(mem_ctx, len, record.data.cname);
			break;
		case DNS_TYPE_SOA:
			len = strlen(record.data.soa.mname);
			werr = dns_name_check(mem_ctx, len, record.data.soa.mname);
			if (!W_ERROR_IS_OK(werr)) {
				break;
			}
			len = strlen(record.data.soa.rname);
			werr = dns_name_check(mem_ctx, len, record.data.soa.rname);
			break;
		case DNS_TYPE_PTR:
			len = strlen(record.data.ptr);
			werr = dns_name_check(mem_ctx, len, record.data.ptr);
			break;
		case DNS_TYPE_MX:
			len = strlen(record.data.mx.nameTarget);
			werr = dns_name_check(mem_ctx, len, record.data.mx.nameTarget);
			break;
		case DNS_TYPE_SRV:
			len = strlen(record.data.srv.nameTarget);
			werr = dns_name_check(mem_ctx, len,
					      record.data.srv.nameTarget);
			break;
		/*
		 * In the default case, the record doesn't have a DN, so it
		 * must be ok.
		 */
		default:
			break;
		}

		if (!W_ERROR_IS_OK(werr)) {
			return werr;
		}
	}

	return WERR_OK;
}

bool dns_name_is_static(struct dnsp_DnssrvRpcRecord *records,
			uint16_t rec_count)
{
	int i = 0;
	for (i = 0; i < rec_count; i++) {
		if (records[i].wType == DNS_TYPE_TOMBSTONE) {
			continue;
		}

		if (records[i].wType == DNS_TYPE_SOA ||
		    records[i].dwTimeStamp == 0) {
			return true;
		}
	}
	return false;
}

/*
 * Helper function to copy a dnsp_ip4_array struct to an IP4_ARRAY struct.
 * The new structure and it's data are allocated on the supplied talloc context
 */
static struct IP4_ARRAY *copy_ip4_array(TALLOC_CTX *ctx,
					const char *name,
					struct dnsp_ip4_array array)
{

	struct IP4_ARRAY *ip4_array = NULL;
	unsigned int i;

	ip4_array = talloc_zero(ctx, struct IP4_ARRAY);
	if (ip4_array == NULL) {
		DBG_ERR("Out of memory copying property [%s]\n", name);
		return NULL;
	}

	ip4_array->AddrCount = array.addrCount;
	if (ip4_array->AddrCount == 0) {
		return ip4_array;
	}

	ip4_array->AddrArray =
	    talloc_array(ip4_array, uint32_t, ip4_array->AddrCount);
	if (ip4_array->AddrArray == NULL) {
		TALLOC_FREE(ip4_array);
		DBG_ERR("Out of memory copying property [%s] values\n", name);
		return NULL;
	}

	for (i = 0; i < ip4_array->AddrCount; i++) {
		ip4_array->AddrArray[i] = array.addrArray[i];
	}

	return ip4_array;
}

bool dns_zoneinfo_load_zone_property(struct dnsserver_zoneinfo *zoneinfo,
				     struct dnsp_DnsProperty *prop)
{
	switch (prop->id) {
	case DSPROPERTY_ZONE_TYPE:
		zoneinfo->dwZoneType = prop->data.zone_type;
		break;
	case DSPROPERTY_ZONE_ALLOW_UPDATE:
		zoneinfo->fAllowUpdate = prop->data.allow_update_flag;
		break;
	case DSPROPERTY_ZONE_NOREFRESH_INTERVAL:
		zoneinfo->dwNoRefreshInterval = prop->data.norefresh_hours;
		break;
	case DSPROPERTY_ZONE_REFRESH_INTERVAL:
		zoneinfo->dwRefreshInterval = prop->data.refresh_hours;
		break;
	case DSPROPERTY_ZONE_AGING_STATE:
		zoneinfo->fAging = prop->data.aging_enabled;
		break;
	case DSPROPERTY_ZONE_SCAVENGING_SERVERS:
		zoneinfo->aipScavengeServers = copy_ip4_array(
		    zoneinfo, "ZONE_SCAVENGING_SERVERS", prop->data.servers);
		if (zoneinfo->aipScavengeServers == NULL) {
			return false;
		}
		break;
	case DSPROPERTY_ZONE_AGING_ENABLED_TIME:
		zoneinfo->dwAvailForScavengeTime =
		    prop->data.next_scavenging_cycle_hours;
		break;
	case DSPROPERTY_ZONE_MASTER_SERVERS:
		zoneinfo->aipLocalMasters = copy_ip4_array(
		    zoneinfo, "ZONE_MASTER_SERVERS", prop->data.master_servers);
		if (zoneinfo->aipLocalMasters == NULL) {
			return false;
		}
		break;
	case DSPROPERTY_ZONE_EMPTY:
	case DSPROPERTY_ZONE_SECURE_TIME:
	case DSPROPERTY_ZONE_DELETED_FROM_HOSTNAME:
	case DSPROPERTY_ZONE_AUTO_NS_SERVERS:
	case DSPROPERTY_ZONE_DCPROMO_CONVERT:
	case DSPROPERTY_ZONE_SCAVENGING_SERVERS_DA:
	case DSPROPERTY_ZONE_MASTER_SERVERS_DA:
	case DSPROPERTY_ZONE_NS_SERVERS_DA:
	case DSPROPERTY_ZONE_NODE_DBFLAGS:
		break;
	}
	return true;
}
WERROR dns_get_zone_properties(struct ldb_context *samdb,
			       TALLOC_CTX *mem_ctx,
			       struct ldb_dn *zone_dn,
			       struct dnsserver_zoneinfo *zoneinfo)
{

	int ret, i;
	struct dnsp_DnsProperty *prop = NULL;
	struct ldb_message_element *element = NULL;
	const char *const attrs[] = {"dNSProperty", NULL};
	struct ldb_result *res = NULL;
	enum ndr_err_code err;

	ret = ldb_search(samdb,
			 mem_ctx,
			 &res,
			 zone_dn,
			 LDB_SCOPE_BASE,
			 attrs,
			 "(objectClass=dnsZone)");
	if (ret != LDB_SUCCESS) {
		DBG_ERR("dnsserver: Failed to find DNS zone: %s\n",
			ldb_dn_get_linearized(zone_dn));
		return DNS_ERR(SERVER_FAILURE);
	}

	element = ldb_msg_find_element(res->msgs[0], "dNSProperty");
	if (element == NULL) {
		return DNS_ERR(NOTZONE);
	}

	for (i = 0; i < element->num_values; i++) {
		bool valid_property;
		prop = talloc_zero(mem_ctx, struct dnsp_DnsProperty);
		if (prop == NULL) {
			return WERR_NOT_ENOUGH_MEMORY;
		}
		err = ndr_pull_struct_blob(
		    &(element->values[i]),
		    mem_ctx,
		    prop,
		    (ndr_pull_flags_fn_t)ndr_pull_dnsp_DnsProperty);
		if (!NDR_ERR_CODE_IS_SUCCESS(err)) {
			/*
			 * If we can't pull it, then there is no valid
			 * data to load into the zone, so ignore this
			 * as Microsoft does.  Windows can load an
			 * invalid property with a zero length into
			 * the dnsProperty attribute.
			 */
			continue;
		}

		valid_property =
		    dns_zoneinfo_load_zone_property(zoneinfo, prop);
		if (!valid_property) {
			return DNS_ERR(SERVER_FAILURE);
		}
	}

	return WERR_OK;
}

WERROR dns_common_replace(struct ldb_context *samdb,
			  TALLOC_CTX *mem_ctx,
			  struct ldb_dn *dn,
			  bool needs_add,
			  uint32_t serial,
			  struct dnsp_DnssrvRpcRecord *records,
			  uint16_t rec_count)
{
	const struct timeval start = timeval_current();
	struct ldb_message_element *el;
	uint16_t i;
	int ret;
	WERROR werr;
	struct ldb_message *msg = NULL;
	bool was_tombstoned = false;
	bool become_tombstoned = false;
	struct ldb_dn *zone_dn = NULL;
	struct dnsserver_zoneinfo *zoneinfo = NULL;
	uint32_t t;

	msg = ldb_msg_new(mem_ctx);
	W_ERROR_HAVE_NO_MEMORY(msg);

	msg->dn = dn;

	zone_dn = ldb_dn_copy(mem_ctx, dn);
	if (zone_dn == NULL) {
		werr = WERR_NOT_ENOUGH_MEMORY;
		goto exit;
	}
	if (!ldb_dn_remove_child_components(zone_dn, 1)) {
		werr = DNS_ERR(SERVER_FAILURE);
		goto exit;
	}
	zoneinfo = talloc(mem_ctx, struct dnsserver_zoneinfo);
	if (zoneinfo == NULL) {
		werr = WERR_NOT_ENOUGH_MEMORY;
		goto exit;
	}
	werr = dns_get_zone_properties(samdb, mem_ctx, zone_dn, zoneinfo);
	if (W_ERROR_EQUAL(DNS_ERR(NOTZONE), werr)) {
		/*
		 * We only got zoneinfo for aging so if we didn't find any
		 * properties then just disable aging and keep going.
		 */
		zoneinfo->fAging = 0;
	} else if (!W_ERROR_IS_OK(werr)) {
		goto exit;
	}

	werr = check_name_list(mem_ctx, rec_count, records);
	if (!W_ERROR_IS_OK(werr)) {
		goto exit;
	}

	ret = ldb_msg_add_empty(msg, "dnsRecord", LDB_FLAG_MOD_REPLACE, &el);
	if (ret != LDB_SUCCESS) {
		werr = DNS_ERR(SERVER_FAILURE);
		goto exit;
	}

	/*
	 * we have at least one value,
	 * which might be used for the tombstone marker
	 */
	el->values = talloc_zero_array(el, struct ldb_val, MAX(1, rec_count));
	if (el->values == NULL) {
		werr = WERR_NOT_ENOUGH_MEMORY;
		goto exit;
	}

	if (rec_count > 1) {
		/*
		 * We store a sorted list with the high wType values first
		 * that's what windows does. It also simplifies the
		 * filtering of DNS_TYPE_TOMBSTONE records
		 */
		TYPESAFE_QSORT(records, rec_count, rec_cmp);
	}

	for (i = 0; i < rec_count; i++) {
		struct ldb_val *v = &el->values[el->num_values];
		enum ndr_err_code ndr_err;

		if (records[i].wType == DNS_TYPE_TOMBSTONE) {
			/*
			 * There are two things that could be going on here.
			 *
			 * 1. We use a tombstone with EntombedTime == 0 for
			 * passing deletion messages through the stack, and
			 * this is the place we filter them out to perform
			 * that deletion.
			 *
			 * 2. This node is tombstoned, with no records except
			 * for a single tombstone, and it is just waiting to
			 * disappear. In this case, unless the caller has
			 * added a record, rec_count should be 1, and
			 * el->num_values will end up at 0, and we will make
			 * no changes. But if the caller has added a record,
			 * we need to un-tombstone the node.
			 *
			 * It is not possible to add an explicit tombstone
			 * record.
			 */
			if (records[i].data.EntombedTime != 0) {
				if (rec_count != 1 && DEBUGLVL(DBGLVL_NOTICE)) {
					DBG_NOTICE("tombstone record [%u] has "
						   "%u neighbour records.\n",
						   i, rec_count - 1);
					NDR_PRINT_DEBUG(dnsp_DnssrvRpcRecord, &records[i]);
				}
				was_tombstoned = true;
			}
			continue;
		}

		if (zoneinfo->fAging == 1 && records[i].dwTimeStamp != 0) {
			t = unix_to_dns_timestamp(time(NULL));
			if (t - records[i].dwTimeStamp >
			    zoneinfo->dwNoRefreshInterval) {
				records[i].dwTimeStamp = t;
			}
		}

		records[i].dwSerial = serial;
		ndr_err = ndr_push_struct_blob(v, el->values, &records[i],
				(ndr_push_flags_fn_t)ndr_push_dnsp_DnssrvRpcRecord);
		if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
			DEBUG(0, ("Failed to push dnsp_DnssrvRpcRecord\n"));
			werr = DNS_ERR(SERVER_FAILURE);
			goto exit;
		}
		el->num_values++;
	}

	if (needs_add) {
		/*
		 * This is a new dnsNode, which simplifies everything as we
		 * know there is nothing to delete or change. We add the
		 * records and get out.
		 */
		if (el->num_values == 0) {
			werr = WERR_OK;
			goto exit;
		}

		ret = ldb_msg_add_string(msg, "objectClass", "dnsNode");
		if (ret != LDB_SUCCESS) {
			werr = DNS_ERR(SERVER_FAILURE);
			goto exit;
		}

		ret = ldb_add(samdb, msg);
		if (ret != LDB_SUCCESS) {
			werr = DNS_ERR(SERVER_FAILURE);
			goto exit;
		}

		werr = WERR_OK;
		goto exit;
	}

	if (el->num_values == 0) {
		/*
		 * We get here if there are no records or all the records were
		 * tombstones.
		 */
		struct dnsp_DnssrvRpcRecord tbs;
		struct ldb_val *v = &el->values[el->num_values];
		enum ndr_err_code ndr_err;
		struct timeval tv;

		if (was_tombstoned) {
			/*
			 * This is already a tombstoned object.
			 * Just leave it instead of updating the time stamp.
			 */
			werr = WERR_OK;
			goto exit;
		}

		tv = timeval_current();
		tbs = (struct dnsp_DnssrvRpcRecord) {
			.wType = DNS_TYPE_TOMBSTONE,
			.dwSerial = serial,
			.data.EntombedTime = timeval_to_nttime(&tv),
		};

		ndr_err = ndr_push_struct_blob(v, el->values, &tbs,
				(ndr_push_flags_fn_t)ndr_push_dnsp_DnssrvRpcRecord);
		if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
			DEBUG(0, ("Failed to push dnsp_DnssrvRpcRecord\n"));
			werr = DNS_ERR(SERVER_FAILURE);
			goto exit;
		}
		el->num_values++;

		become_tombstoned = true;
	}

	if (was_tombstoned || become_tombstoned) {
		ret = ldb_msg_append_fmt(msg, LDB_FLAG_MOD_REPLACE,
					 "dNSTombstoned", "%s",
					 become_tombstoned ? "TRUE" : "FALSE");
		if (ret != LDB_SUCCESS) {
			werr = DNS_ERR(SERVER_FAILURE);
			goto exit;
		}
	}

	ret = ldb_modify(samdb, msg);
	if (ret != LDB_SUCCESS) {
		NTSTATUS nt = dsdb_ldb_err_to_ntstatus(ret);
		werr = ntstatus_to_werror(nt);
		goto exit;
	}

	werr = WERR_OK;
exit:
	talloc_free(msg);
	DNS_COMMON_LOG_OPERATION(
		win_errstr(werr),
		&start,
		NULL,
		dn == NULL ? NULL : ldb_dn_get_linearized(dn),
		NULL);
	return werr;
}

bool dns_name_match(const char *zone, const char *name, size_t *host_part_len)
{
	size_t zl = strlen(zone);
	size_t nl = strlen(name);
	ssize_t zi, ni;
	static const size_t fixup = 'a' - 'A';

	if (zl > nl) {
		return false;
	}

	for (zi = zl, ni = nl; zi >= 0; zi--, ni--) {
		char zc = zone[zi];
		char nc = name[ni];

		/* convert to lower case */
		if (zc >= 'A' && zc <= 'Z') {
			zc += fixup;
		}
		if (nc >= 'A' && nc <= 'Z') {
			nc += fixup;
		}

		if (zc != nc) {
			return false;
		}
	}

	if (ni >= 0) {
		if (name[ni] != '.') {
			return false;
		}

		ni--;
	}

	*host_part_len = ni+1;

	return true;
}

WERROR dns_common_name2dn(struct ldb_context *samdb,
			  struct dns_server_zone *zones,
			  TALLOC_CTX *mem_ctx,
			  const char *name,
			  struct ldb_dn **_dn)
{
	struct ldb_dn *base;
	struct ldb_dn *dn;
	const struct dns_server_zone *z;
	size_t host_part_len = 0;
	struct ldb_val host_part;
	WERROR werr;
	bool ok;
	const char *casefold = NULL;

	if (name == NULL) {
		return DNS_ERR(FORMAT_ERROR);
	}

	if (strcmp(name, "") == 0) {
		base = ldb_get_default_basedn(samdb);
		dn = ldb_dn_copy(mem_ctx, base);
		ok = ldb_dn_add_child_fmt(dn,
					  "DC=@,DC=RootDNSServers,CN=MicrosoftDNS,CN=System");
		if (ok == false) {
			TALLOC_FREE(dn);
			return WERR_NOT_ENOUGH_MEMORY;
		}

		*_dn = dn;
		return WERR_OK;
	}

	/* Check non-empty names */
	werr = dns_name_check(mem_ctx, strlen(name), name);
	if (!W_ERROR_IS_OK(werr)) {
		return werr;
	}

	for (z = zones; z != NULL; z = z->next) {
		bool match;

		match = dns_name_match(z->name, name, &host_part_len);
		if (match) {
			break;
		}
	}

	if (z == NULL) {
		return DNS_ERR(NAME_ERROR);
	}

	if (host_part_len == 0) {
		dn = ldb_dn_copy(mem_ctx, z->dn);
		ok = ldb_dn_add_child_fmt(dn, "DC=@");
		if (! ok) {
			TALLOC_FREE(dn);
			return WERR_NOT_ENOUGH_MEMORY;
		}
		*_dn = dn;
		return WERR_OK;
	}

	dn = ldb_dn_copy(mem_ctx, z->dn);
	if (dn == NULL) {
		TALLOC_FREE(dn);
		return WERR_NOT_ENOUGH_MEMORY;
	}

	host_part = data_blob_const(name, host_part_len);

	ok = ldb_dn_add_child_val(dn, "DC", host_part);

	if (ok == false) {
		TALLOC_FREE(dn);
		return WERR_NOT_ENOUGH_MEMORY;
	}

	/*
	 * Check the new DN here for validity, so as to catch errors
	 * early
	 */
	ok = ldb_dn_validate(dn);
	if (ok == false) {
		TALLOC_FREE(dn);
		return DNS_ERR(NAME_ERROR);
	}

	/*
	 * The value from this check is saved in the DN, and doing
	 * this here allows an easy return here.
	 */
	casefold = ldb_dn_get_casefold(dn);
	if (casefold == NULL) {
		TALLOC_FREE(dn);
		return DNS_ERR(NAME_ERROR);
	}

	*_dn = dn;
	return WERR_OK;
}


/*
  see if two dns records match
 */


bool dns_record_match(struct dnsp_DnssrvRpcRecord *rec1,
		      struct dnsp_DnssrvRpcRecord *rec2)
{
	int i;
	struct in6_addr rec1_in_addr6;
	struct in6_addr rec2_in_addr6;

	if (rec1->wType != rec2->wType) {
		return false;
	}

	/* see if the data matches */
	switch (rec1->wType) {
	case DNS_TYPE_A:
		return strcmp(rec1->data.ipv4, rec2->data.ipv4) == 0;
	case DNS_TYPE_AAAA: {
		int ret;

		ret = inet_pton(AF_INET6, rec1->data.ipv6, &rec1_in_addr6);
		if (ret != 1) {
			return false;
		}
		ret = inet_pton(AF_INET6, rec2->data.ipv6, &rec2_in_addr6);
		if (ret != 1) {
			return false;
		}

		return memcmp(&rec1_in_addr6, &rec2_in_addr6, sizeof(rec1_in_addr6)) == 0;
	}
	case DNS_TYPE_CNAME:
		return samba_dns_name_equal(rec1->data.cname,
					    rec2->data.cname);
	case DNS_TYPE_TXT:
		if (rec1->data.txt.count != rec2->data.txt.count) {
			return false;
		}
		for (i = 0; i < rec1->data.txt.count; i++) {
			if (strcmp(rec1->data.txt.str[i], rec2->data.txt.str[i]) != 0) {
				return false;
			}
		}
		return true;
	case DNS_TYPE_PTR:
		return samba_dns_name_equal(rec1->data.ptr, rec2->data.ptr);
	case DNS_TYPE_NS:
		return samba_dns_name_equal(rec1->data.ns, rec2->data.ns);

	case DNS_TYPE_SRV:
		return rec1->data.srv.wPriority == rec2->data.srv.wPriority &&
			rec1->data.srv.wWeight  == rec2->data.srv.wWeight &&
			rec1->data.srv.wPort    == rec2->data.srv.wPort &&
			samba_dns_name_equal(rec1->data.srv.nameTarget,
					     rec2->data.srv.nameTarget);

	case DNS_TYPE_MX:
		return rec1->data.mx.wPriority == rec2->data.mx.wPriority &&
			samba_dns_name_equal(rec1->data.mx.nameTarget,
					     rec2->data.mx.nameTarget);

	case DNS_TYPE_SOA:
		return samba_dns_name_equal(rec1->data.soa.mname,
					    rec2->data.soa.mname) &&
			samba_dns_name_equal(rec1->data.soa.rname,
					     rec2->data.soa.rname) &&
			rec1->data.soa.serial == rec2->data.soa.serial &&
			rec1->data.soa.refresh == rec2->data.soa.refresh &&
			rec1->data.soa.retry == rec2->data.soa.retry &&
			rec1->data.soa.expire == rec2->data.soa.expire &&
			rec1->data.soa.minimum == rec2->data.soa.minimum;
	case DNS_TYPE_TOMBSTONE:
		return true;
	default:
		break;
	}

	return false;
}


static int dns_common_sort_zones(struct ldb_message **m1, struct ldb_message **m2)
{
	const char *n1, *n2;
	size_t l1, l2;

	n1 = ldb_msg_find_attr_as_string(*m1, "name", NULL);
	n2 = ldb_msg_find_attr_as_string(*m2, "name", NULL);
	if (n1 == NULL || n2 == NULL) {
		if (n1 != NULL) {
			return -1;
		} else if (n2 != NULL) {
			return 1;
		} else {
			return 0;
		}
	}
	l1 = strlen(n1);
	l2 = strlen(n2);

	/* If the string lengths are not equal just sort by length */
	if (l1 != l2) {
		/* If m1 is the larger zone name, return it first */
		return NUMERIC_CMP(l2, l1);
	}

	/*TODO: We need to compare DNs here, we want the DomainDNSZones first */
	return 0;
}

NTSTATUS dns_common_zones(struct ldb_context *samdb,
			  TALLOC_CTX *mem_ctx,
			  struct ldb_dn *base_dn,
			  struct dns_server_zone **zones_ret)
{
	const struct timeval start = timeval_current();
	int ret;
	static const char * const attrs[] = { "name", NULL};
	struct ldb_result *res;
	int i;
	struct dns_server_zone *new_list = NULL;
	TALLOC_CTX *frame = talloc_stackframe();
	NTSTATUS result = NT_STATUS_OK;

	if (base_dn) {
		/* This search will work against windows */
		ret = dsdb_search(samdb, frame, &res,
				  base_dn, LDB_SCOPE_SUBTREE,
				  attrs, 0, "(objectClass=dnsZone)");
	} else {
		/* TODO: this search does not work against windows */
		ret = dsdb_search(samdb, frame, &res, NULL,
				  LDB_SCOPE_SUBTREE,
				  attrs,
				  DSDB_SEARCH_SEARCH_ALL_PARTITIONS,
				  "(objectClass=dnsZone)");
	}
	if (ret != LDB_SUCCESS) {
		TALLOC_FREE(frame);
		result = NT_STATUS_INTERNAL_DB_CORRUPTION;
		goto exit;
	}

	TYPESAFE_QSORT(res->msgs, res->count, dns_common_sort_zones);

	for (i=0; i < res->count; i++) {
		struct dns_server_zone *z;

		z = talloc_zero(mem_ctx, struct dns_server_zone);
		if (z == NULL) {
			TALLOC_FREE(frame);
			result = NT_STATUS_NO_MEMORY;
			goto exit;
		}

		z->name = ldb_msg_find_attr_as_string(res->msgs[i], "name", NULL);
		talloc_steal(z, z->name);
		z->dn = talloc_move(z, &res->msgs[i]->dn);
		/*
		 * Ignore the RootDNSServers zone and zones that we don't support yet
		 * RootDNSServers should never be returned (Windows DNS server don't)
		 * ..TrustAnchors should never be returned as is, (Windows returns
		 * TrustAnchors) and for the moment we don't support DNSSEC so we'd better
		 * not return this zone.
		 */
		if ((strcmp(z->name, "RootDNSServers") == 0) ||
		    (strcmp(z->name, "..TrustAnchors") == 0))
		{
			DEBUG(10, ("Ignoring zone %s\n", z->name));
			talloc_free(z);
			continue;
		}
		DLIST_ADD_END(new_list, z);
	}

	*zones_ret = new_list;
	TALLOC_FREE(frame);
	result = NT_STATUS_OK;
exit:
	DNS_COMMON_LOG_OPERATION(
		nt_errstr(result),
		&start,
		NULL,
		base_dn == NULL ? NULL : ldb_dn_get_linearized(base_dn),
		NULL);
	return result;
}

/*
  see if two DNS names are the same
 */
bool samba_dns_name_equal(const char *name1, const char *name2)
{
	size_t len1 = strlen(name1);
	size_t len2 = strlen(name2);

	if (len1 > 0 && name1[len1 - 1] == '.') {
		len1--;
	}
	if (len2 > 0 && name2[len2 - 1] == '.') {
		len2--;
	}
	if (len1 != len2) {
		return false;
	}
	return strncasecmp(name1, name2, len1) == 0;
}


/*
 * Convert unix time to a DNS timestamp
 * uint32 hours in the NTTIME epoch
 *
 * This uses unix_to_nt_time() which can return special flag NTTIMEs like
 * UINT64_MAX (0xFFF...) or NTTIME_MAX (0x7FF...), which will convert to
 * distant future timestamps; or 0 as a flag value, meaning a 1601 timestamp,
 * which is used to indicate a record does not expire.
 *
 * As we don't generally check for these special values in NTTIME conversions,
 * we also don't check here, but for the benefit of people encountering these
 * timestamps and searching for their origin, here is a list:
 *
 **  TIME_T_MAX
 *
 * Even if time_t is 32 bit, this will become NTTIME_MAX (a.k.a INT64_MAX,
 * 0x7fffffffffffffff) in 100ns units. That translates to 256204778 hours
 * since 1601, which converts back to 9223372008000000000 or
 * 0x7ffffff9481f1000. It will present as 30828-09-14 02:00:00, around 48
 * minutes earlier than NTTIME_MAX.
 *
 **  0, the start of the unix epoch, 1970-01-01 00:00:00
 *
 * This is converted into 0 in the Windows epoch, 1601-01-01 00:00:00 which is
 * clearly the same whether you count in 100ns units or hours. In DNS record
 * timestamps this is a flag meaning the record will never expire.
 *
 **  (time_t)-1, such as what *might* mean 1969-12-31 23:59:59
 *
 * This becomes (NTTIME)-1ULL a.k.a. UINT64_MAX, 0xffffffffffffffff thence
 * 512409557 in hours since 1601. That in turn is 0xfffffffaf2028800 or
 * 18446744052000000000 in NTTIME (rounded to the hour), which might be
 * presented as -21709551616 or -0x50dfd7800, because NTTIME is not completely
 * dedicated to being unsigned. If it gets shown as a year, it will be around
 * 60055.
 *
 **  Other negative time_t values (e.g. 1969-05-29).
 *
 * The meaning of these is somewhat undefined, but in any case they will
 * translate perfectly well into the same dates in NTTIME.
 *
 **  Notes
 *
 * There are dns timestamps that exceed the range of NTTIME (up to 488356 AD),
 * but it is not possible for this function to produce them.
 *
 * It is plausible that it was at midnight on 1601-01-01, in London, that
 * Shakespeare wrote:
 *
 *  The time is out of joint. O cursed spite
 *  That ever I was born to set it right!
 *
 * and this is why we have this epoch and time zone.
 */
uint32_t unix_to_dns_timestamp(time_t t)
{
	NTTIME nt;
	unix_to_nt_time(&nt, t);
	nt /= NTTIME_TO_HOURS;
	return (uint32_t) nt;
}

/*
 * Convert a DNS timestamp into NTTIME.
 *
 * Because DNS timestamps cover a longer time period than NTTIME, and these
 * would wrap to an arbitrary NTTIME, we saturate at NTTIME_MAX and return an
 * error in this case.
 */
NTSTATUS dns_timestamp_to_nt_time(NTTIME *_nt, uint32_t t)
{
	NTTIME nt = t;
	if (nt > NTTIME_MAX / NTTIME_TO_HOURS) {
		*_nt = NTTIME_MAX;
		return NT_STATUS_INTEGER_OVERFLOW;
	}
	*_nt = nt * NTTIME_TO_HOURS;
	return NT_STATUS_OK;
}
