/*
 * Copyright (C) 2014 Maui Systems Ltd, Scotland, contact@maui-systems.co.uk.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the
 * above copyright notice and this permission notice appear in all
 * copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND MAUI SYSTEMS LTD DISCLAIMS ALL
 * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL MAUI SYSTEMS LTD  BE
 * LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR
 * ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
 * WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
 * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
 * SOFTWARE.
 */

/*
 * Copyright (C) 2011,2014  Internet Systems Consortium, Inc. ("ISC")
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
 * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
 * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
 * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
 * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 * PERFORMANCE OF THIS SOFTWARE.
 */

/*
 * BIND 9 DLZ MySQL module with support for dynamic DNS (DDNS)
 *
 * Adapted from code contributed by Marty Lee, Maui Systems Ltd.
 *
 * See README for database schema and usage details.
 */

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <inttypes.h>
#include <pthread.h>
#include <netdb.h>
#include <ifaddrs.h>

#include <mysql/mysql.h>
#include <mysql/errmsg.h>

#include <dlz_minimal.h>
#include <dlz_list.h>
#include <dlz_pthread.h>

/*
 * The SQL queries that will be used for lookups and updates are defined
 * here.  They will be processed into queries by the build_query()
 * function.
 *
 * NOTE: Despite appearances, these do NOT use printf-style formatting.
 * "%s", with no modifiers, is the only supported directive.
 */

/*
 * Get the NS RRset for a zone
 * Arguments: zone-name
 */
#define Q_GETNS \
	"SELECT d.data FROM ZoneData d, Zones z " \
	"WHERE UPPER(d.type) = 'NS' AND LOWER(z.domain) = LOWER('%s') " \
	"AND z.id = d.zone_id"

/*
 * Get a list of zones (ignoring writable or not)
 * Arguments: (none)
 */
#define Q_GETZONES "SELECT LOWER(domain), serial FROM Zones"

/*
 * Find a specific zone
 * Arguments: zone-name
 */
#define Q_FINDZONE \
	"SELECT id FROM Zones WHERE LOWER(domain) = LOWER('%s')"

/*
 * Get SOA data from zone apex
 * Arguments: zone-name
 */
#define Q_GETSOA \
	"SELECT host, admin, serial, refresh, retry, expire, minimum, ttl " \
	"FROM Zones WHERE LOWER(domain) = LOWER('%s')"

/*
 * Get other data from zone apex
 * Arguments: zone-name, zone-name (repeated)
 */
#define Q_GETAPEX \
	"SELECT d.type, d.data, d.ttl FROM ZoneData d, Zones z " \
	"WHERE LOWER(z.domain) = LOWER('%s') AND z.id = d.zone_id " \
	"AND LOWER(d.name) IN (LOWER('%s'), '', '@') "\
	"ORDER BY UPPER(d.type) ASC"

/*
 * Get data from non-apex nodes
 * Arguments: zone-name, node-name (relative to zone name)
 */
#define Q_GETNODE \
	"SELECT d.type, d.data, d.ttl FROM ZoneData d, Zones z " \
	"WHERE LOWER(z.domain) = LOWER('%s') AND z.id = d.zone_id " \
	"AND LOWER(d.name) = LOWER('%s') " \
	"ORDER BY UPPER(d.type) ASC"

/*
 * Get all data from a zone, for AXFR
 * Arguments: zone-name
 */
#define Q_GETALL \
	"SELECT d.name, d.type, d.data, d.ttl FROM ZoneData d, Zones z " \
	"WHERE LOWER(z.domain) = LOWER('%s') AND z.id = d.zone_id"

/*
 * Get SOA serial number for a zone.
 * Arguments: zone-name
 */
#define Q_GETSERIAL \
	"SELECT serial FROM Zones WHERE domain = '%s'"

/*
 * Determine whether a zone is writeable, and if so, retrieve zone_id
 * Arguments: zone-name
 */
#define Q_WRITEABLE \
	"SELECT id FROM Zones WHERE " \
	"LOWER(domain) = LOWER('%s') AND writeable = 1"

/*
 * Insert data into zone (other than SOA)
 * Arguments: zone-id (from Q_WRITEABLE), node-name (relative to zone-name),
 * 	      rrtype, rdata text, TTL (in text format)
 */
#define I_DATA \
	"INSERT INTO ZoneData (zone_id, name, type, data, ttl) " \
	"VALUES (%s, LOWER('%s'), UPPER('%s'), '%s', %s)"

/*
 * Update SOA serial number for a zone
 * Arguments: new serial number (in text format), zone-id (from Q_WRITEABLE)
 */
#define U_SERIAL \
	"UPDATE Zones SET serial = %s WHERE id = %s"

/*
 * Delete a specific record (non-SOA) from a zone
 *
 * Arguments: node-name (relative to zone-name), zone-id (from Q_WRITEABLE),
 * 	      rrtype, rdata text, TTL (in text format).
 */
#define D_RECORD \
	"DELETE FROM ZoneData WHERE zone_id = %s AND " \
	"LOWER(name) = LOWER('%s') AND UPPER(type) = UPPER('%s') AND " \
	"data = '%s' AND ttl = %s"

/*
 * Delete an entire rrset from a zone
 * Arguments: node-name (relative to zone-name), zone-id (from Q_WRITEABLE),
 * 	      rrtype.
 */
#define D_RRSET \
	"DELETE FROM ZoneData WHERE zone_id = %s AND " \
	"LOWER(name) = LOWER('%s') AND UPPER(type) = UPPER('%s')"

/*
 * Number of concurrent database connections we support
 * - equivalent to maxmium number of concurrent transactions
 *   that can be 'in-flight' + 1
 */
#define MAX_DBI 16

typedef struct mysql_record {
	char zone[255];
	char name[100];
	char type[10];
	char data[200];
	char ttl[10];
} mysql_record_t;

typedef struct mysql_instance {
	int id;
	MYSQL *sock;
	int connected;
	dlz_mutex_t mutex;
} mysql_instance_t;

typedef struct mysql_transaction mysql_transaction_t;
struct mysql_transaction {
	char *zone;
	char *zone_id;
	mysql_instance_t *dbi;
	mysql_transaction_t *next;
};

typedef struct mysql_data {
	int debug;

	/*
	 * Database connection details
	 */
	char *db_name;
	char *db_host;
	char *db_user;
	char *db_pass;

	/*
	 * Database structures
	 */
	mysql_instance_t db[MAX_DBI];

	/*
	 * Transactions
	 */
	mysql_transaction_t *transactions;

	/*
	 * Mutex for transactions
	 */
	dlz_mutex_t tx_mutex;

	/* Helper functions from the dlz_dlopen driver */
	log_t *log;
	dns_sdlz_putrr_t *putrr;
	dns_sdlz_putnamedrr_t *putnamedrr;
	dns_dlz_writeablezone_t *writeable_zone;
} mysql_data_t;

typedef struct mysql_arg mysql_arg_t;
typedef DLZ_LIST(mysql_arg_t) mysql_arglist_t;
struct mysql_arg {
	char *arg;
	DLZ_LINK(mysql_arg_t) link;
};

static const char *modname = "dlz_mysqldyn";

/*
 * Local functions
 */
static bool
db_connect(mysql_data_t *state, mysql_instance_t *dbi) {
	MYSQL *conn;
	/*
	 * Make sure this thread has been through 'init'
	 */
	mysql_thread_init();

	if (dbi->connected)
		return (true);

	if (state->log != NULL)
		state->log(ISC_LOG_INFO, "%s: init connection %d ",
			   modname, dbi->id);

	conn = mysql_real_connect(dbi->sock, state->db_host,
				  state->db_user, state->db_pass,
				  state->db_name, 0, NULL,
				  CLIENT_REMEMBER_OPTIONS);
	if (conn == NULL) {
		if (state->log != NULL)
			state->log(ISC_LOG_ERROR,
				   "%s: database connection failed: %s",
				   modname, mysql_error(dbi->sock));

		dlz_mutex_unlock(&dbi->mutex);
		return (false);
	}

	dbi->connected = 1;
	return (true);
}

static mysql_instance_t *
get_dbi(mysql_data_t *state) {
	int i;

	/*
	 * Find an available dbi
	 */
	for (i = 0; i < MAX_DBI; i++) {
		if (dlz_mutex_trylock(&state->db[i].mutex) == 0)
			break;
	}

	if (i == MAX_DBI) {
		if (state->debug && state->log != NULL)
			state->log(ISC_LOG_ERROR,
				   "%s: No available connections", modname);
		return (NULL);
	}
	return (&state->db[i]);
}

/*
 * Allocate memory and store an escaped, sanitized version
 * of string 'original'
 */
static char *
sanitize(mysql_instance_t *dbi, const char *original) {
	char *s;

	if (original == NULL)
		return (NULL);

	s = (char *) malloc((strlen(original) * 2) + 1);
	if (s != NULL) {
		memset(s, 0, (strlen(original) * 2) + 1);

		mysql_real_escape_string(dbi->sock, s, original,
					 strlen(original));
	}

	return (s);
}

/*
 * Append the string pointed to by 's' to the argument list 'arglist',
 * and add the string length to the running total pointed to by 'len'.
 */
static isc_result_t
additem(mysql_arglist_t *arglist, char **s, size_t *len) {
	mysql_arg_t *item;

	item = malloc(sizeof(*item));
	if (item == NULL)
		return (ISC_R_NOMEMORY);

	DLZ_LINK_INIT(item, link);
	item->arg = *s;
	*len += strlen(*s);
	DLZ_LIST_APPEND(*arglist, item, link);
	*s = NULL;

	return (ISC_R_SUCCESS);
}

/*
 * Construct a query string using a variable number of arguments, and
 * save it into newly allocated memory.
 *
 * NOTE: 'command' resembles a printf-style format string, but ONLY
 * supports the "%s" directive with no modifiers of any kind.
 *
 * If 'dbi' is NULL, we attempt to get a temporary database connection;
 * otherwise we use the existing one.
 */
static char *
build_query(mysql_data_t *state, mysql_instance_t *dbi,
	    const char *command, ...)
{
	isc_result_t result;
	bool localdbi = false;
	mysql_arglist_t arglist;
	mysql_arg_t *item;
	char *p, *q, *tmp = NULL, *querystr = NULL;
	char *query = NULL;
	size_t len = 0;
	va_list ap1;

	/* Get a DB instance if needed */
	if (dbi == NULL) {
		dbi = get_dbi(state);
		if (dbi == NULL)
			return (NULL);
		localdbi = true;
	}

	/* Make sure this instance is connected */
	if (!db_connect(state, dbi))
		goto fail;

	va_start(ap1, command);
	DLZ_LIST_INIT(arglist);
	q = querystr = strdup(command);
	if (querystr == NULL)
		goto fail;

	for (;;) {
		if (*q == '\0')
			break;

		p = strstr(q, "%s");
		if (p != NULL) {
			*p = '\0';
			tmp = strdup(q);
			if (tmp == NULL)
				goto fail;

			result = additem(&arglist, &tmp, &len);
			if (result != ISC_R_SUCCESS)
				goto fail;

			tmp = sanitize(dbi, va_arg(ap1, const char *));
			if (tmp == NULL)
				goto fail;

			result = additem(&arglist, &tmp, &len);
			if (result != ISC_R_SUCCESS)
				goto fail;

			q = p + 2;
		} else {
			tmp = strdup(q);
			if (tmp == NULL)
				goto fail;

			result = additem(&arglist, &tmp, &len);
			if (result != ISC_R_SUCCESS)
				goto fail;

			break;
		}
	}

	if (len == 0)
		goto fail;

	query = malloc(len + 1);
	if (query == NULL)
		goto fail;

	*query = '\0';
	for (item = DLZ_LIST_HEAD(arglist);
	     item != NULL;
	     item = DLZ_LIST_NEXT(item, link))
		if (item->arg != NULL)
			strcat(query, item->arg);

 fail:
	va_end(ap1);

	for (item = DLZ_LIST_HEAD(arglist);
	     item != NULL;
	     item = DLZ_LIST_NEXT(item, link))
	{
		if (item->arg != NULL)
			free(item->arg);
		free(item);
	}

	if (tmp != NULL)
		free(tmp);
	if (querystr != NULL)
		free (querystr);

	if (dbi != NULL && localdbi)
		dlz_mutex_unlock(&dbi->mutex);

	return (query);
}

/* Does this name end in a dot? */
static bool
isrelative(const char *s) {
	if (s == NULL || s[strlen(s) - 1] == '.')
		return (false);
	return (true);
}

/* Return a dot if 's' doesn't already end with one */
static inline const char *
dot(const char *s) {
	return (isrelative(s) ? "." : "");
}

/*
 * Generate a full hostname from a (presumably relative) name 'name'
 * and a zone name 'zone'; store the result in 'dest' (which must have
 * enough space).
 */
static void
fqhn(const char *name, const char *zone, char *dest) {
	if (dest == NULL)
		return;

	if (strlen(name) == 0 || strcmp(name, "@") == 0)
		sprintf(dest, "%s%s", zone, dot(zone));
	else {
		if (isrelative(name))
			sprintf(dest, "%s.%s%s", name, zone, dot(zone));
		else
			strcpy(dest, name);
	}
}

/*
 * Names are stored in relative form in ZoneData; this function
 * removes labels matching 'zone' from the end of 'name'.
 */
static char *
relname(const char *name, const char *zone) {
	size_t nlen, zlen;
	const char *p;
	char *new;

	new = (char *) malloc(strlen(name) + 1);
	if (new == NULL)
		return (NULL);

	nlen = strlen(name);
	zlen = strlen(zone);

	if (nlen < zlen) {
		strcpy(new, name);
		return (new);
	} else if (nlen == zlen || strcasecmp(name, zone) == 0) {
		strcpy(new, "@");
		return (new);
	}

	p = name + nlen - zlen;
	if (strcasecmp(p, zone) != 0 &&
	    (zone[zlen - 1] != '.' ||
	     strncasecmp(p, zone, zlen - 1) != 0))
	{
		strcpy(new, name);
		return (new);
	}

	strncpy(new, name, nlen - zlen);
	new[nlen - zlen - 1] = '\0';
	return (new);
}

static isc_result_t
validate_txn(mysql_data_t *state, mysql_transaction_t *txn) {
	isc_result_t result = ISC_R_FAILURE;
	mysql_transaction_t *txp;

	dlz_mutex_lock(&state->tx_mutex);
	for (txp = state->transactions; txp != NULL; txp = txp->next) {
		if (txn == txp) {
			result = ISC_R_SUCCESS;
			break;
		}
	}
	dlz_mutex_unlock(&state->tx_mutex);

	if (result != ISC_R_SUCCESS && state->log != NULL)
		state->log(ISC_LOG_ERROR, "%s: invalid txn %x", modname, txn);

	return (result);
}

static isc_result_t
db_execute(mysql_data_t *state, mysql_instance_t *dbi, const char *query) {
	int ret;

	/* Make sure this instance is connected.  */
	if (!db_connect(state, dbi))
		return (ISC_R_FAILURE);

	ret = mysql_real_query(dbi->sock, query, strlen(query));
	if (ret != 0) {
		if (state->debug && state->log != NULL)
			state->log(ISC_LOG_ERROR,
				   "%s: query '%s' failed: %s",
				   modname, query, mysql_error(dbi->sock));
		return (ISC_R_FAILURE);
	}

	if (state->debug && state->log != NULL)
		state->log(ISC_LOG_INFO, "%s: execute(%d) %s",
			   modname, dbi->id, query);

	return (ISC_R_SUCCESS);
}

static MYSQL_RES *
db_query(mysql_data_t *state, mysql_instance_t *dbi, const char *query) {
	isc_result_t result;
	bool localdbi = false;
	MYSQL_RES *res = NULL;

	if (query == NULL)
		return (NULL);

	/* Get a DB instance if needed */
	if (dbi == NULL) {
		dbi = get_dbi(state);
		if (dbi == NULL)
			return (NULL);
		localdbi = true;
	}

	/* Make sure this instance is connected */
	if (!db_connect(state, dbi))
		goto fail;

	result = db_execute(state, dbi, query);
	if (result != ISC_R_SUCCESS)
		goto fail;

	res = mysql_store_result(dbi->sock);
	if (res == NULL) {
		if (state->log != NULL)
			state->log(ISC_LOG_ERROR,
				   "%s: unable to store result: %s",
				   modname, mysql_error(dbi->sock));
		goto fail;
	}

	if (state->debug && state->log != NULL)
		state->log(ISC_LOG_INFO,
			   "%s: query(%d) returned %d rows",
			   modname, dbi->id, mysql_num_rows(res));

 fail:
	if (dbi != NULL && localdbi)
		dlz_mutex_unlock(&dbi->mutex);
	return (res);
}

/*
 * Generate a DNS NOTIFY packet:
 * 12 bytes header
 * Question (1)
 * 	strlen(zone) +2
 *  2 bytes qtype
 *  2 bytes qclass
 *
 * -> 18 bytes + strlen(zone)
 *
 * N.B. Need to be mindful of byte ordering; using htons to map 16bit
 * values to the 'on the wire' packet values.
 */
static unsigned char *
make_notify(const char *zone, int *packetlen) {
	int i, j;
	unsigned char *packet = (unsigned char *) malloc(strlen(zone) + 18);

	if (packet == NULL)
		return (NULL);

	*packetlen = strlen(zone) + 18;
	memset(packet, 0, *packetlen);

	/* Random query ID */
	i = rand();
	packet[0] = htons(i) & 0xff;
	packet[1] = htons(i) >> 8;

	/* Flags (OpCode '4' in bits 14-11), Auth Answer set in bit 10 */
	i = 0x2400;
	packet[2] = htons(i) & 0xff;
	packet[3] = htons(i) >> 8;

	/* QD Count */
	i = 0x1;
	packet[4] = htons(i) & 0xff;
	packet[5] = htons(i) >> 8;

	/* Question */
	packet[12] = '.';
	memcpy(&packet[13], zone, strlen(zone));
	packet[13 + strlen(zone)] = 0;

	/* Make the question into labels */
	j = 12;
	while (packet[j]) {
		for (i = j + 1; packet[i] != '\0' && packet[i] != '.'; i++);
		packet[j] = i - j - 1;
		j = i;
	}

	/* Question type */
	i = 6;
	packet[j + 1] = htons(i) & 0xff;
	packet[j + 2] = htons(i) >> 8;

	/* Queston class */
	i = 1;
	packet[j + 3] = htons(i) & 0xff;
	packet[j + 4] = htons(i) >> 8;

	return (packet);
}

static void
send_notify(struct sockaddr_in *addr, const unsigned char *p, const int plen) {
	int s;

	addr->sin_family = AF_INET;
	addr->sin_port = htons(53);

	if ((s = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
		return;

	sendto(s, p, plen, 0, (struct sockaddr *)addr, sizeof(*addr));
	close(s);
	return;
}

/*
 * Generate and send a DNS NOTIFY packet
 */
static void
notify(mysql_data_t *state, const char *zone, int sn) {
	MYSQL_RES *res;
	MYSQL_ROW row;
	char *query;
	unsigned char *packet;
	int packetlen;
	struct ifaddrs *ifap, *ifa;
	char zaddr[INET_ADDRSTRLEN];
	void *addrp = NULL;

	/* Get the name servers from the NS rrset */
	query = build_query(state, NULL, Q_GETNS, zone);
	res = db_query(state, NULL, query);
	free (query);
	if (res == NULL)
		return;

	/* Create a DNS NOTIFY packet */
	packet = make_notify(zone, &packetlen);
	if (packet == NULL) {
		mysql_free_result(res);
		return;
	}

	/* Get a list of our own addresses */
	if (getifaddrs(&ifap) < 0)
		ifap = NULL;

	/* Tell each nameserver of the update */
	while ((row = mysql_fetch_row(res)) != NULL) {
		bool local = false;
		struct hostent *h;
		struct sockaddr_in addr, *sin;

		/*
		 * Put nameserver rdata through gethostbyname as it
		 * might be an IP address or a hostname. (XXX: switch
		 * this to inet_pton/getaddrinfo.)
		 */
		h = gethostbyname(row[0]);
		if (h == NULL)
			continue;

		memcpy(&addr.sin_addr, h->h_addr, h->h_length);
		addrp = &addr.sin_addr;

		/* Get the address for the nameserver into a string */
		inet_ntop(AF_INET, addrp, zaddr, INET_ADDRSTRLEN);
		for (ifa = ifap; ifa != NULL; ifa = ifa->ifa_next) {
			char ifaddr[INET_ADDRSTRLEN];

			if (ifa->ifa_addr->sa_family != AF_INET)
				continue;

			/* Get local address into a string */
			sin = (struct sockaddr_in *) ifa->ifa_addr;
			addrp = &sin->sin_addr;
			inet_ntop(AF_INET, addrp, ifaddr, INET_ADDRSTRLEN);

			/* See if nameserver address matches this one */
			if (strcmp(ifaddr, zaddr) == 0)
				local = true;
		}

		if (!local) {
			if (state->log != NULL)
				state->log(ISC_LOG_INFO,
					   "%s: notify %s zone %s serial %d",
					   modname, row[0], zone, sn);
			send_notify(&addr, packet, packetlen);
		}
	}

	mysql_free_result(res);
	free(packet);
	if (ifap != NULL)
		freeifaddrs(ifap);
}

/*
 * Constructs a mysql_record_t structure from 'rdatastr', to be
 * used in the dlz_{add,sub,del}rdataset functions below.
 */
static mysql_record_t *
makerecord(mysql_data_t *state, const char *name, const char *rdatastr) {
	mysql_record_t *new_record;
	char *real_name, *dclass, *type, *data, *ttlstr, *buf;
	dns_ttl_t ttlvalue;

	new_record = (mysql_record_t *)
		malloc(sizeof(mysql_record_t));

	if (new_record == NULL) {
		if (state->log != NULL)
			state->log(ISC_LOG_ERROR,
				   "%s: makerecord - unable to malloc",
				   modname);
		return (NULL);
	}

	buf = strdup(rdatastr);
	if (buf == NULL) {
		if (state->log != NULL)
			state->log(ISC_LOG_ERROR,
				   "%s: makerecord - unable to malloc",
				   modname);
		free(new_record);
		return (NULL);
	}

	/*
	 * The format is:
	 * FULLNAME\tTTL\tDCLASS\tTYPE\tDATA
	 *
	 * The DATA field is space separated, and is in the data format
	 * for the type used by dig
	 */
	real_name = strtok_r(buf, "\t", &saveptr);
	if (real_name == NULL)
		goto error;

	ttlstr = strtok_r(NULL, "\t", &saveptr);
	if (ttlstr == NULL || sscanf(ttlstr, "%d", &ttlvalue) != 1)
		goto error;

	dclass = strtok_r(NULL, "\t", &saveptr);
	if (dclass == NULL)
		goto error;

	type = strtok_r(NULL, "\t", &saveptr);
	if (type == NULL)
		goto error;

	data = strtok_r(NULL, "\t", &saveptr);
	if (data == NULL)
		goto error;

	strcpy(new_record->name, name);
	strcpy(new_record->type, type);
	strcpy(new_record->data, data);
	sprintf(new_record->ttl, "%d", ttlvalue);

	free(buf);
	return (new_record);

 error:
	free(buf);
	free(new_record);
	return (NULL);
}

/*
 * Remember a helper function from the bind9 dlz_dlopen driver
 */
static void
b9_add_helper(mysql_data_t *state, const char *helper_name, void *ptr) {
	if (strcmp(helper_name, "log") == 0)
		state->log = (log_t *)ptr;
	if (strcmp(helper_name, "putrr") == 0)
		state->putrr = (dns_sdlz_putrr_t *)ptr;
	if (strcmp(helper_name, "putnamedrr") == 0)
		state->putnamedrr = (dns_sdlz_putnamedrr_t *)ptr;
	if (strcmp(helper_name, "writeable_zone") == 0)
		state->writeable_zone = (dns_dlz_writeablezone_t *)ptr;
}

/*
 * DLZ API functions
 */

/*
 * Return the version of the API
 */
int
dlz_version(unsigned int *flags) {
	UNUSED(flags);
	*flags |= DNS_SDLZFLAG_THREADSAFE;
	return (DLZ_DLOPEN_VERSION);
}

/*
 * Called to initialize the driver
 */
isc_result_t
dlz_create(const char *dlzname, unsigned int argc, char *argv[],
	   void **dbdata, ...)
{
	mysql_data_t *state;
	const char *helper_name;
	va_list ap;
	int n;

	UNUSED(dlzname);

	state = calloc(1, sizeof(mysql_data_t));
	if (state == NULL)
		return (ISC_R_NOMEMORY);

	dlz_mutex_init(&state->tx_mutex, NULL);
	state->transactions = NULL;

	/* Fill in the helper functions */
	va_start(ap, dbdata);
	while ((helper_name = va_arg(ap, const char *)) != NULL)
		b9_add_helper(state, helper_name, va_arg(ap, void *));
	va_end(ap);

	if (state->log != NULL)
		state->log(ISC_LOG_INFO, "loading %s module", modname);

	if ((argc < 2) || (argc > 6)) {
		if (state->log != NULL)
			state->log(ISC_LOG_ERROR,
				   "%s: missing args <dbname> "
				   "[<dbhost> [<user> <pass>]]", modname);
		dlz_destroy(state);
		return (ISC_R_FAILURE);
	}

	state->db_name = strdup(argv[1]);
	if (argc > 2) {
		state->db_host = strdup(argv[2]);
		if (argc > 4) {
			state->db_user = strdup(argv[3]);
			state->db_pass = strdup(argv[4]);
		} else {
			state->db_user = strdup("bind");
			state->db_pass = strdup("");
		}
	} else {
		state->db_host = strdup("localhost");
		state->db_user = strdup("bind");
		state->db_pass = strdup("");
	}

	if (state->log != NULL)
		state->log(ISC_LOG_INFO,
			   "%s: DB=%s, Host=%s, User=%s",
			   modname, state->db_name,
			   state->db_host, state->db_user);

	/*
	 * Assign the 'state' to dbdata so we get it in our callbacks
	 */


	dlz_mutex_lock(&state->tx_mutex);

	/*
	 * Populate DB instances
	 */
	if (mysql_thread_safe()) {
		for (n = 0; n < MAX_DBI; n++) {
			my_bool opt = 1;
			dlz_mutex_init(&state->db[n].mutex, NULL);
			dlz_mutex_lock(&state->db[n].mutex);
			state->db[n].id = n;
			state->db[n].connected = 0;
			state->db[n].sock = mysql_init(NULL);
			mysql_options(state->db[n].sock,
				      MYSQL_READ_DEFAULT_GROUP,
				      modname);
			mysql_options(state->db[n].sock,
				      MYSQL_OPT_RECONNECT, &opt);
			dlz_mutex_unlock(&state->db[n].mutex);
		}

		*dbdata = state;
		dlz_mutex_unlock(&state->tx_mutex);
		return (ISC_R_SUCCESS);
	}

	free(state->db_name);
	free(state->db_host);
	free(state->db_user);
	free(state->db_pass);
	dlz_mutex_destroy(&state->tx_mutex);
	free(state);
	return (ISC_R_FAILURE);
}

/*
 * Shut down the backend
 */
void
dlz_destroy(void *dbdata) {
	mysql_data_t *state = (mysql_data_t *)dbdata;
	int i;

	if (state->debug && state->log != NULL)
		state->log(ISC_LOG_INFO, "%s: shutting down", modname);

	for (i = 0; i < MAX_DBI; i++) {
		if (state->db[i].sock) {
			mysql_close(state->db[i].sock);
			state->db[i].sock=NULL;
		}
	}
	free(state->db_name);
	free(state->db_host);
	free(state->db_user);
	free(state->db_pass);
	dlz_mutex_destroy(&state->tx_mutex);
	free(state);
}

/*
 * See if we handle a given zone
 */
isc_result_t
dlz_findzonedb(void *dbdata, const char *name,
	       dns_clientinfomethods_t *methods,
	       dns_clientinfo_t *clientinfo)
{
	isc_result_t result = ISC_R_SUCCESS;
	mysql_data_t *state = (mysql_data_t *)dbdata;
	MYSQL_RES *res;
	char *query;

	/* Query the Zones table to see if this zone is present */
	query = build_query(state, NULL, Q_FINDZONE, name);

	if (query == NULL)
		return (ISC_R_NOMEMORY);

	res = db_query(state, NULL, query);
	if (res == NULL)
		return (ISC_R_FAILURE);

	if (mysql_num_rows(res) == 0)
		result = ISC_R_NOTFOUND;

	mysql_free_result(res);
	return (result);
}

/*
 * Perform a database lookup
 */
isc_result_t
dlz_lookup(const char *zone, const char *name, void *dbdata,
	   dns_sdlzlookup_t *lookup, dns_clientinfomethods_t *methods,
	   dns_clientinfo_t *clientinfo)
{
	isc_result_t result;
	mysql_data_t *state = (mysql_data_t *)dbdata;
	bool found = false;
	char *real_name;
	MYSQL_RES *res;
	MYSQL_ROW row;
	char *query;
	mysql_transaction_t *txn = NULL;
	mysql_instance_t *dbi = NULL;

	if (state->putrr == NULL) {
		if (state->log != NULL)
			state->log(ISC_LOG_ERROR,
				   "%s: dlz_lookup - no putrr", modname);
		return (ISC_R_NOTIMPLEMENTED);
	}

	/* Are we okay to try to find the txn version?  */
	if (clientinfo != NULL &&
	    clientinfo->version >= DNS_CLIENTINFO_VERSION) {
		txn = (mysql_transaction_t *) clientinfo->dbversion;
		if (txn != NULL && validate_txn(state, txn) == ISC_R_SUCCESS)
			dbi = txn->dbi;
		if (dbi != NULL) {
			state->log(ISC_LOG_DEBUG(1),
				   "%s: lookup in live transaction %p, DBI %p",
				   modname, txn, dbi);
		}
	}

	if (strcmp(name, "@") == 0) {
		real_name = (char *) malloc(strlen(zone) + 1);
		if (real_name == NULL)
			return (ISC_R_NOMEMORY);
		strcpy(real_name, zone);
	} else {
		real_name = (char *) malloc(strlen(name) + 1);
		if (real_name == NULL)
			return (ISC_R_NOMEMORY);
		strcpy(real_name, name);
	}

	if (strcmp(real_name, zone) == 0) {
		/*
		 * Get the Zones table data for use in the SOA:
		 * zone admin serial refresh retry expire min
		 */
		query = build_query(state, dbi, Q_GETSOA, zone);
		if (query == NULL) {
			free(real_name);
			return (ISC_R_NOMEMORY);
		}

		res = db_query(state, dbi, query);
		free (query);

		if (res == NULL) {
			free(real_name);
			return (ISC_R_NOTFOUND);
		}

		while ((row = mysql_fetch_row(res)) != NULL) {
			char host[1024], admin[1024], data[1024];
			int ttl;

			sscanf(row[7], "%d", &ttl);
			fqhn(row[0], zone, host);
			fqhn(row[1], zone, admin);

			/* zone admin serial refresh retry expire min */
			sprintf(data, "%s%s %s%s %s %s %s %s %s",
				host, dot(host), admin, dot(admin),
				row[2], row[3], row[4], row[5], row[6]);

			result = state->putrr(lookup, "soa", ttl, data);
			if (result != ISC_R_SUCCESS) {
				free(real_name);
				mysql_free_result(res);
				return (result);
			}
		}

		mysql_free_result(res);

		/*
		 *  Now we'll get the rest of the apex data
		 */
		query = build_query(state, dbi, Q_GETAPEX, zone, real_name);
	} else
		query = build_query(state, dbi, Q_GETNODE, zone, real_name);

	res = db_query(state, dbi, query);
	free(query);

	if (res == NULL) {
		free(real_name);
		return (ISC_R_NOTFOUND);
	}

	while ((row = mysql_fetch_row(res)) != NULL) {
		int ttl;
		sscanf(row[2], "%d", &ttl);
		result = state->putrr(lookup, row[0], ttl, row[1]);
		if (result != ISC_R_SUCCESS) {
			free(real_name);
			mysql_free_result(res);
			return (result);
		}

		found = true;
	}

	if (state->debug && state->log != NULL)
		state->log(ISC_LOG_INFO,
			   "%s: dlz_lookup %s/%s/%s - (%d rows)",
			   modname, name, real_name, zone,
			   mysql_num_rows(res));

	mysql_free_result(res);
	free(real_name);

	if (!found)
		return (ISC_R_NOTFOUND);

	return (ISC_R_SUCCESS);
}

/*
 * See if a zone transfer is allowed
 */
isc_result_t
dlz_allowzonexfr(void *dbdata, const char *name, const char *client) {
	mysql_data_t *state = (mysql_data_t *)dbdata;

	if (state->debug && state->log != NULL)
		state->log(ISC_LOG_INFO,
			   "dlz_allowzonexfr: %s %s", name, client);

	/* Just say yes for all our zones */
	return (dlz_findzonedb(dbdata, name, NULL, NULL));
}

/*
 * Perform a zone transfer
 */
isc_result_t
dlz_allnodes(const char *zone, void *dbdata, dns_sdlzallnodes_t *allnodes) {
	isc_result_t result = ISC_R_SUCCESS;
	mysql_data_t *state = (mysql_data_t *)dbdata;
	MYSQL_RES *res;
	MYSQL_ROW row;
	char *query;

	UNUSED(zone);

	if (state->debug && (state->log != NULL))
		state->log(ISC_LOG_INFO, "dlz_allnodes: %s", zone);

	if (state->putnamedrr == NULL)
		return (ISC_R_NOTIMPLEMENTED);

	/*
	 * Get all the ZoneData for this zone
	 */
	query = build_query(state, NULL, Q_GETALL, zone);
	if (query == NULL)
		return (ISC_R_NOMEMORY);

	res = db_query(state, NULL, query);
	free(query);
	if (res == NULL)
		return (ISC_R_NOTFOUND);

	while ((row = mysql_fetch_row(res)) != NULL) {
		char hostname[1024];
		int ttl;

		sscanf(row[3], "%d", &ttl);
		fqhn(row[0], zone, hostname);
		result = state->putnamedrr(allnodes, hostname,
					   row[1], ttl, row[2]);
		if (result != ISC_R_SUCCESS)
			break;
	}

	mysql_free_result(res);
	return (result);
}

/*
 * Start a transaction
 */
isc_result_t
dlz_newversion(const char *zone, void *dbdata, void **versionp) {
	isc_result_t result = ISC_R_FAILURE;
	mysql_data_t *state = (mysql_data_t *) dbdata;
	MYSQL_RES *res;
	MYSQL_ROW row;
	char *query;
	char zone_id[16];
	mysql_transaction_t *txn = NULL, *newtx = NULL;

	/*
	 * Check Zone is writable
	 */
	query = build_query(state, NULL, Q_WRITEABLE, zone);
	if (query == NULL)
		return (ISC_R_NOMEMORY);

	res = db_query(state, NULL, query);
	free(query);
	if (res == NULL)
		return (ISC_R_FAILURE);

	if ((row = mysql_fetch_row(res)) == NULL) {
		mysql_free_result(res);
		return (ISC_R_FAILURE);
	}

	strcpy(zone_id, row[0]);
	mysql_free_result(res);

	/*
	 * See if we already have a transaction for this zone
	 */
	dlz_mutex_lock(&state->tx_mutex);
	for (txn = state->transactions; txn != NULL; txn = txn->next) {
		if (strcmp(txn->zone, zone) == 0) {
			if (state->log != NULL)
				state->log(ISC_LOG_ERROR,
				   "%s: transaction already "
				   "started for zone %s", modname, zone);
			dlz_mutex_unlock(&state->tx_mutex);
			return (ISC_R_FAILURE);
		}
	}

	/*
	 * Create new transaction
	 */
	newtx = (mysql_transaction_t *)
		malloc(sizeof(mysql_transaction_t));
	newtx->zone = strdup(zone);
	newtx->zone_id = strdup(zone_id);
	newtx->dbi = get_dbi(state);
	newtx->next = NULL;

	if (newtx->dbi == NULL) {
		result = ISC_R_FAILURE;
		goto cleanup;
	}

	result = db_execute(state, newtx->dbi, "START TRANSACTION");
	if (result != ISC_R_SUCCESS) {
		dlz_mutex_unlock(&newtx->dbi->mutex);
		goto cleanup;
	}

	/*
	 * Add this tx to front of list
	 */
	newtx->next = state->transactions;
	state->transactions = newtx;

	if (state->debug && (state->log != NULL))
		state->log(ISC_LOG_INFO, "%s: New tx %x", modname, newtx);

 cleanup:
	dlz_mutex_unlock(&state->tx_mutex);
	if (result == ISC_R_SUCCESS) {
		*versionp = (void *) newtx;
	} else {
		dlz_mutex_unlock(&state->tx_mutex);
		free(newtx->zone);
		free(newtx->zone_id);
		free(newtx);
	}

	return (result);
}

/*
 * End a transaction
 */
void
dlz_closeversion(const char *zone, bool commit,
		 void *dbdata, void **versionp)
{
	isc_result_t result;
	mysql_data_t *state = (mysql_data_t *)dbdata;
	mysql_transaction_t *txn = (mysql_transaction_t *)*versionp;
	mysql_transaction_t *txp;
	char *query;
	MYSQL_RES *res;
	MYSQL_ROW row;

	/*
	 * Find the transaction
	 */
	dlz_mutex_lock(&state->tx_mutex);
	if (state->transactions == txn) {
		/* Tx is first in list; remove it. */
		state->transactions = txn->next;
	} else {
		txp = state->transactions;
		while (txp != NULL) {
			if (txp->next != NULL) {
				if (txp->next == txn) {
					txp->next = txn->next;
					break;
				}
			}
			if (txp == txn) {
				txp = txn->next;
				break;
			}
			txp = txp->next;
		}
	}

	/*
	 * Tidy up
	 */
	dlz_mutex_unlock(&state->tx_mutex);
	*versionp = NULL;

	if (commit) {
		int oldsn = 0, newsn = 0;

		/*
		 * Find out the serial number of the zone out with the
		 * transaction so we can see if it has incremented or not
		 */
		query = build_query(state, txn->dbi, Q_GETSERIAL, zone);
		if (query == NULL && state->log != NULL) {
			state->log(ISC_LOG_ERROR,
				   "%s: unable to commit transaction %x "
				   "on zone %s: no memory",
				   modname, txn, zone);
			return;
		}

		res = db_query(state, txn->dbi, query);
		if (res != NULL) {
			while ((row = mysql_fetch_row(res)) != NULL)
				sscanf(row[0], "%d", &oldsn);
			mysql_free_result(res);
		}

		/*
		 * Commit the transaction to the database
		 */
		result = db_execute(state, txn->dbi, "COMMIT");
		if (result != ISC_R_SUCCESS && state->log != NULL) {
			state->log(ISC_LOG_INFO,
				   "%s: (%x) commit transaction on zone %s",
				   modname, txn, zone);
			return;
		}

		if (state->debug && state->log != NULL)
			state->log(ISC_LOG_INFO,
				   "%s: (%x) committing transaction "
				   "on zone %s",
				   modname, txn, zone);

		/*
		 * Now get the serial number again
		 */
		query = build_query(state, txn->dbi, Q_GETSERIAL, zone);
		res = db_query(state, txn->dbi, query);
		free(query);

		if (res != NULL) {
			while ((row = mysql_fetch_row(res)) != NULL)
				sscanf(row[0], "%d", &newsn);
			mysql_free_result(res);
		}

		/*
		 * Look to see if serial numbers have changed
		 */
		if (newsn > oldsn)
			notify(state, zone, newsn);
	} else {
		result = db_execute(state, txn->dbi, "ROLLBACK");
		if (state->debug && (state->log != NULL))
			state->log(ISC_LOG_INFO,
				   "%s: (%x) roll back transaction on zone %s",
				   modname, txn, zone);
	}

	/*
	 * Unlock the mutex for this txn
	 */
	dlz_mutex_unlock(&txn->dbi->mutex);

	/*
	 * Free up other structures
	 */
	free(txn->zone);
	free(txn->zone_id);
	free(txn);
}

/*
 * Configure a writeable zone
 */
#if DLZ_DLOPEN_VERSION < 3
isc_result_t
dlz_configure(dns_view_t *view, void *dbdata)
#else /* DLZ_DLOPEN_VERSION >= 3 */
isc_result_t
dlz_configure(dns_view_t *view, dns_dlzdb_t *dlzdb, void *dbdata)
#endif /* DLZ_DLOPEN_VERSION */
{
	mysql_data_t *state = (mysql_data_t *)dbdata;
	isc_result_t result;
	MYSQL_RES *res;
	MYSQL_ROW row;
	int count;

	/*
	 * Seed PRNG (used by Notify code)
	 */
	srand(getpid());

	if (state->debug && state->log != NULL)
		state->log(ISC_LOG_INFO, "%s: dlz_confgure", modname);

	if (state->writeable_zone == NULL) {
		if (state->log != NULL)
			state->log(ISC_LOG_ERROR,
				   "%s: no writeable_zone method available",
				   modname);
		return (ISC_R_FAILURE);
	}

	/*
	 * Get a list of Zones (ignore writeable column at this point)
	 */
	res = db_query(state, NULL, Q_GETZONES);
	if (res == NULL)
		return (ISC_R_FAILURE);

	count = 0;
	while ((row = mysql_fetch_row(res)) != NULL) {
		int sn;
		sscanf(row[1], "%d", &sn);
		notify(state, row[0], sn);
		result = state->writeable_zone(view,
#if DLZ_DLOPEN_VERSION >= 3
					       dlzdb,
#endif
					       row[0]);
		if (result != ISC_R_SUCCESS) {
			if (state->log != NULL)
				state->log(ISC_LOG_ERROR,
					   "%s: failed to configure zone %s",
					   modname, row[0]);
			mysql_free_result(res);
			return (result);
		}
		count++;
	}
	mysql_free_result(res);

	if (state->debug && state->log != NULL)
		state->log(ISC_LOG_INFO,
			   "%s: configured %d zones", modname, count);
	return (ISC_R_SUCCESS);
}

/*
 * Authorize a zone update
 */
bool
dlz_ssumatch(const char *signer, const char *name, const char *tcpaddr,
	     const char *type, const char *key, uint32_t keydatalen,
	     unsigned char *keydata, void *dbdata)
{
	mysql_data_t *state = (mysql_data_t *)dbdata;

	UNUSED(tcpaddr);
	UNUSED(type);
	UNUSED(keydatalen);
	UNUSED(keydata);
	UNUSED(key);

	if (state->debug && state->log != NULL)
		state->log(ISC_LOG_INFO,
			   "%s: allowing update of %s by key %s",
			   modname, name, signer);
	return (true);
}

isc_result_t
dlz_addrdataset(const char *name, const char *rdatastr,
		void *dbdata, void *version)
{
	mysql_data_t *state = (mysql_data_t *)dbdata;
	mysql_transaction_t *txn = (mysql_transaction_t *)version;
	char *new_name, *query;
	mysql_record_t *record;
	isc_result_t result;

	if (txn == NULL)
		return (ISC_R_FAILURE);

	new_name = relname(name, txn->zone);
	if (new_name == NULL)
		return (ISC_R_NOMEMORY);

	if (state->debug && (state->log != NULL))
		state->log(ISC_LOG_INFO, "%s: add (%x) %s (as %s) %s",
			   modname, version, name, new_name, rdatastr);

	record = makerecord(state, new_name, rdatastr);
	free(new_name);
	if (record == NULL)
		return (ISC_R_FAILURE);

	/* Write out data to database */
	if (strcasecmp(record->type, "SOA") != 0) {
		query = build_query(state, txn->dbi, I_DATA,
				    txn->zone_id, record->name,
				    record->type, record->data,
				    record->ttl);
		if (query == NULL) {
			result = ISC_R_FAILURE;
			goto cleanup;
		}
		result = db_execute(state, txn->dbi, query);
		free(query);
	} else {
		/*
		 * This is an SOA record, so we update: it must exist,
		 * or we wouldn't have gotten this far.
		 * SOA: zone admin serial refresh retry expire min
		 */
		char sn[32];
		sscanf(record->data, "%*s %*s %31s %*s %*s %*s %*s", sn);
		query = build_query(state, txn->dbi, U_SERIAL, sn,
				    txn->zone_id);
		if (query == NULL) {
			result = ISC_R_FAILURE;
			goto cleanup;
		}
		result = db_execute(state, txn->dbi, query);
		free(query);
	}

 cleanup:
	free(record);
	return (result);
}

isc_result_t
dlz_subrdataset(const char *name, const char *rdatastr,
		void *dbdata, void *version)
{
	mysql_data_t *state = (mysql_data_t *)dbdata;
	mysql_transaction_t *txn = (mysql_transaction_t *)version;
	char *new_name, *query;
	mysql_record_t *record;
	isc_result_t result;

	if (txn == NULL)
		return (ISC_R_FAILURE);

	new_name = relname(name, txn->zone);
	if (new_name == NULL)
		return (ISC_R_NOMEMORY);

	if (state->debug && (state->log != NULL))
		state->log(ISC_LOG_INFO, "%s: sub (%x) %s %s",
			   modname, version, name, rdatastr);

	record = makerecord(state, new_name, rdatastr);
	free(new_name);
	if (record == NULL)
		return (ISC_R_FAILURE);
	/*
	 * If 'type' isn't 'SOA', delete the records
	 */
	if (strcasecmp(record->type, "SOA") == 0)
		result = ISC_R_SUCCESS;
	else {
		query = build_query(state, txn->dbi, D_RECORD,
				    txn->zone_id, record->name,
				    record->type, record->data,
				    record->ttl);
		if (query == NULL) {
			result = ISC_R_NOMEMORY;
			goto cleanup;
		}

		result = db_execute(state, txn->dbi, query);
		free(query);
	}

 cleanup:
	free(record);
	return (result);
}

isc_result_t
dlz_delrdataset(const char *name, const char *type,
		void *dbdata, void *version)
{
	mysql_data_t *state = (mysql_data_t *)dbdata;
	mysql_transaction_t *txn = (mysql_transaction_t *)version;
	char *new_name, *query;
	isc_result_t result;

	if (txn == NULL)
		return (ISC_R_FAILURE);

	new_name = relname(name, txn->zone);
	if (new_name == NULL)
		return (ISC_R_NOMEMORY);

	if (state->debug && (state->log != NULL))
		state->log(ISC_LOG_INFO, "%s: del (%x) %s %s",
			   modname, version, name, type);

	query = build_query(state, txn->dbi, D_RRSET,
			    txn->zone_id, new_name, type);
	if (query == NULL) {
		result = ISC_R_NOMEMORY;
		goto cleanup;
	}

	result = db_execute(state, txn->dbi, query);
	free(query);

 cleanup:
	free(new_name);
	return (result);
}
