/*
 * Copyright (C) 2002 Stichting NLnet, Netherlands, stichting@nlnet.nl.
 * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
 *
 * SPDX-License-Identifier: MPL-2.0
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0.  If a copy of the MPL was not distributed with this
 * file, you can obtain one at https://mozilla.org/MPL/2.0/.
 *
 * See the COPYRIGHT file distributed with this work for additional
 * information regarding copyright ownership.
 */

/*
 * Copyright (C) 1999-2001, 2016  Internet Systems Consortium, Inc. ("ISC")
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#ifdef DLZ_ODBC
#include <sql.h>
#include <sqlext.h>
#include <sqltypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <isc/mem.h>
#include <isc/platform.h>
#include <isc/print.h>
#include <isc/result.h>
#include <isc/string.h>
#include <isc/util.h>

#include <dns/log.h>
#include <dns/result.h>
#include <dns/sdlz.h>

#include <dlz/dlz_odbc_driver.h>
#include <dlz/sdlz_helper.h>
#include <named/globals.h>

static dns_sdlzimplementation_t *dlz_odbc = NULL;

#define dbc_search_limit 30
#define ALLNODES	 1
#define ALLOWXFR	 2
#define AUTHORITY	 3
#define FINDZONE	 4
#define LOOKUP		 5

#define sqlOK(a) ((a == SQL_SUCCESS || a == SQL_SUCCESS_WITH_INFO) ? -1 : 0)

/*
 * Private Structures
 */

/*
 * structure to hold ODBC connection & statement
 */

typedef struct {
	SQLHDBC dbc;
	SQLHSTMT stmnt;
} odbc_db_t;

/*
 * Structure to hold everything needed by this "instance" of the odbc driver
 * remember, the driver code is only loaded once, but may have many separate
 * instances
 */

typedef struct {
	db_list_t *db;	 /* handle to a list of DB */
	SQLHENV sql_env; /* handle to SQL environment */
	SQLCHAR *dsn;
	SQLCHAR *user;
	SQLCHAR *pass;
} odbc_instance_t;

/* forward reference */

static size_t
odbc_makesafe(char *to, const char *from, size_t length);

/*
 * Private methods
 */

static SQLSMALLINT
safeLen(void *a) {
	if (a == NULL) {
		return (0);
	}
	return (strlen((char *)a));
}

/*% properly cleans up an odbc_instance_t */

static void
destroy_odbc_instance(odbc_instance_t *odbc_inst) {
	dbinstance_t *ndbi = NULL;
	dbinstance_t *dbi = NULL;

	/* get the first DBI in the list */
	ndbi = ISC_LIST_HEAD(*odbc_inst->db);

	/* loop through the list */
	while (ndbi != NULL) {
		dbi = ndbi;
		/* get the next DBI in the list */
		ndbi = ISC_LIST_NEXT(dbi, link);

		/* if we have a connection / statement object in memory */
		if (dbi->dbconn != NULL) {
			/* free statement handle */
			if (((odbc_db_t *)(dbi->dbconn))->stmnt != NULL) {
				SQLFreeHandle(
					SQL_HANDLE_STMT,
					((odbc_db_t *)(dbi->dbconn))->stmnt);
				((odbc_db_t *)(dbi->dbconn))->stmnt = NULL;
			}

			/* disconnect from database & free connection handle */
			if (((odbc_db_t *)(dbi->dbconn))->dbc != NULL) {
				SQLDisconnect(((odbc_db_t *)dbi->dbconn)->dbc);
				SQLFreeHandle(
					SQL_HANDLE_DBC,
					((odbc_db_t *)(dbi->dbconn))->dbc);
				((odbc_db_t *)(dbi->dbconn))->dbc = NULL;
			}

			/* free memory that held connection & statement. */
			isc_mem_free(named_g_mctx, dbi->dbconn);
		}
		/* release all memory that comprised a DBI */
		destroy_sqldbinstance(dbi);
	}
	/* release memory for the list structure */
	isc_mem_put(named_g_mctx, odbc_inst->db, sizeof(db_list_t));

	/* free sql environment */
	if (odbc_inst->sql_env != NULL) {
		SQLFreeHandle(SQL_HANDLE_ENV, odbc_inst->sql_env);
	}

	/* free ODBC instance strings */
	if (odbc_inst->dsn != NULL) {
		isc_mem_free(named_g_mctx, odbc_inst->dsn);
	}
	if (odbc_inst->pass != NULL) {
		isc_mem_free(named_g_mctx, odbc_inst->pass);
	}
	if (odbc_inst->user != NULL) {
		isc_mem_free(named_g_mctx, odbc_inst->user);
	}

	/* free memory for odbc_inst */
	if (odbc_inst != NULL) {
		isc_mem_put(named_g_mctx, odbc_inst, sizeof(odbc_instance_t));
	}
}

/*% Connects to database, and creates ODBC statements */

static isc_result_t
odbc_connect(odbc_instance_t *dbi, odbc_db_t **dbc) {
	odbc_db_t *ndb = *dbc;
	SQLRETURN sqlRes;
	isc_result_t result = ISC_R_SUCCESS;

	if (ndb != NULL) {
		/*
		 * if db != null, we have to do some cleanup
		 * if statement handle != null free it
		 */
		if (ndb->stmnt != NULL) {
			SQLFreeHandle(SQL_HANDLE_STMT, ndb->stmnt);
			ndb->stmnt = NULL;
		}

		/* if connection handle != null free it */
		if (ndb->dbc != NULL) {
			SQLFreeHandle(SQL_HANDLE_DBC, ndb->dbc);
			ndb->dbc = NULL;
		}
	} else {
		ndb = isc_mem_allocate(named_g_mctx, sizeof(odbc_db_t));
		memset(ndb, 0, sizeof(odbc_db_t));
	}

	sqlRes = SQLAllocHandle(SQL_HANDLE_DBC, dbi->sql_env, &(ndb->dbc));
	if (!sqlOK(sqlRes)) {
		isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
			      DNS_LOGMODULE_DLZ, ISC_LOG_ERROR,
			      "Odbc driver unable to allocate memory");
		result = ISC_R_NOMEMORY;
		goto cleanup;
	}

	sqlRes = SQLConnect(ndb->dbc, dbi->dsn, safeLen(dbi->dsn), dbi->user,
			    safeLen(dbi->user), dbi->pass, safeLen(dbi->pass));
	if (!sqlOK(sqlRes)) {
		isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
			      DNS_LOGMODULE_DLZ, ISC_LOG_ERROR,
			      "Odbc driver unable to connect");
		result = ISC_R_FAILURE;
		goto cleanup;
	}

	sqlRes = SQLAllocHandle(SQL_HANDLE_STMT, ndb->dbc, &(ndb->stmnt));
	if (!sqlOK(sqlRes)) {
		isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
			      DNS_LOGMODULE_DLZ, ISC_LOG_ERROR,
			      "Odbc driver unable to allocate memory");
		result = ISC_R_NOMEMORY;
		goto cleanup;
	}

	*dbc = ndb;

	return (ISC_R_SUCCESS);

cleanup:

	if (ndb != NULL) {
		/* if statement handle != null free it */
		if (ndb->stmnt != NULL) {
			SQLFreeHandle(SQL_HANDLE_STMT, ndb->stmnt);
			ndb->stmnt = NULL;
		}

		/* if connection handle != null free it */
		if (ndb->dbc != NULL) {
			SQLDisconnect(ndb->dbc);
			SQLFreeHandle(SQL_HANDLE_DBC, ndb->dbc);
			ndb->dbc = NULL;
		}
		/* free memory holding ndb */
		isc_mem_free(named_g_mctx, ndb);
	}

	return (result);
}

/*%
 * Loops through the list of DB instances, attempting to lock
 * on the mutex.  If successful, the DBI is reserved for use
 * and the thread can perform queries against the database.
 * If the lock fails, the next one in the list is tried.
 * looping continues until a lock is obtained, or until
 * the list has been searched dbc_search_limit times.
 * This function is only used when the driver is compiled for
 * multithreaded operation.
 */

static dbinstance_t *
odbc_find_avail_conn(db_list_t *dblist) {
	dbinstance_t *dbi = NULL;
	dbinstance_t *head;
	int count = 0;

	/* get top of list */
	head = dbi = ISC_LIST_HEAD(*dblist);

	/* loop through list */
	while (count < dbc_search_limit) {
		/* try to lock on the mutex */
		if (isc_mutex_trylock(&dbi->instance_lock) == ISC_R_SUCCESS) {
			return (dbi); /* success, return the DBI for use. */
		}
		/* not successful, keep trying */
		dbi = ISC_LIST_NEXT(dbi, link);

		/* check to see if we have gone to the top of the list. */
		if (dbi == NULL) {
			count++;
			dbi = head;
		}
	}
	isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_DLZ,
		      ISC_LOG_INFO,
		      "Odbc driver unable to find available "
		      "connection after searching %d times",
		      count);
	return (NULL);
}

/*% Allocates memory for a new string, and then constructs the new
 * string by "escaping" the input string.  The new string is
 * safe to be used in queries.  This is necessary because we cannot
 * be sure of what types of strings are passed to us, and we don't
 * want special characters in the string causing problems.
 */

static char *
odbc_escape_string(const char *instr) {
	char *outstr;
	unsigned int len;

	if (instr == NULL) {
		return (NULL);
	}

	len = strlen(instr);

	outstr = isc_mem_allocate(named_g_mctx, (2 * len * sizeof(char)) + 1);

	odbc_makesafe(outstr, instr, len);

	return (outstr);
}

/* ---------------
 * Escaping arbitrary strings to get valid SQL strings/identifiers.
 *
 * Replaces "\\" with "\\\\" and "'" with "''".
 * length is the length of the buffer pointed to by
 * from.  The buffer at to must be at least 2*length + 1 characters
 * long.  A terminating NUL character is written.
 *
 * NOTICE!!!
 * This function was borrowed directly from PostgreSQL's libpq.
 *
 * The copyright statements from the original file containing this
 * function are included below:
 * Portions Copyright (c) 1996-2001, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 * ---------------
 */

static size_t
odbc_makesafe(char *to, const char *from, size_t length) {
	const char *source = from;
	char *target = to;
	unsigned int remaining = length;

	while (remaining > 0) {
		switch (*source) {
		case '\\':
			*target = '\\';
			target++;
			*target = '\\';
			/* target and remaining are updated below. */
			break;

		case '\'':
			*target = '\'';
			target++;
			*target = '\'';
			/* target and remaining are updated below. */
			break;

		default:
			*target = *source;
			/* target and remaining are updated below. */
		}
		source++;
		target++;
		remaining--;
	}

	/* Write the terminating NUL character. */
	*target = '\0';

	return (target - to);
}

/*%
 * This function is the real core of the driver.   Zone, record
 * and client strings are passed in (or NULL is passed if the
 * string is not available).  The type of query we want to run
 * is indicated by the query flag, and the dbdata object is passed
 * passed in to.  dbdata really holds either:
 *		1) a list of database instances (in multithreaded mode) OR
 *		2) a single database instance (in single threaded mode)
 * The function will construct the query and obtain an available
 * database instance (DBI).  It will then run the query and hopefully
 * obtain a result set.  The data base instance that is used is returned
 * to the caller so they can get the data from the result set from it.
 * If successful, it will be the responsibility of the caller to close
 * the cursor, and unlock the mutex of the DBI when they are done with it.
 * If not successful, this function will perform all the cleanup.
 */

static isc_result_t
odbc_get_resultset(const char *zone, const char *record, const char *client,
		   unsigned int query, void *dbdata, dbinstance_t **r_dbi) {
	isc_result_t result;
	dbinstance_t *dbi = NULL;
	char *querystring = NULL;
	unsigned int j = 0;
	SQLRETURN sqlRes;

	REQUIRE(*r_dbi == NULL);

	/* get db instance / connection */

	/* find an available DBI from the list */
	dbi = odbc_find_avail_conn(((odbc_instance_t *)dbdata)->db);

	/* if DBI is null, can't do anything else */
	if (dbi == NULL) {
		return (ISC_R_FAILURE);
	}

	/* what type of query are we going to run? */
	switch (query) {
	case ALLNODES:
		/*
		 * if the query was not passed in from the config file
		 * then we can't run it.  return not_implemented, so
		 * it's like the code for that operation was never
		 * built into the driver.... AHHH flexibility!!!
		 */
		if (dbi->allnodes_q == NULL) {
			result = ISC_R_NOTIMPLEMENTED;
			goto cleanup;
		}
		break;
	case ALLOWXFR:
		/* same as comments as ALLNODES */
		if (dbi->allowxfr_q == NULL) {
			result = ISC_R_NOTIMPLEMENTED;
			goto cleanup;
		}
		break;
	case AUTHORITY:
		/* same as comments as ALLNODES */
		if (dbi->authority_q == NULL) {
			result = ISC_R_NOTIMPLEMENTED;
			goto cleanup;
		}
		break;
	case FINDZONE:
		/* this is required.  It's the whole point of DLZ! */
		if (dbi->findzone_q == NULL) {
			isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
				      DNS_LOGMODULE_DLZ, ISC_LOG_DEBUG(2),
				      "No query specified for findzone.  "
				      "Findzone requires a query");
			result = ISC_R_FAILURE;
			goto cleanup;
		}
		break;
	case LOOKUP:
		/* this is required.  It's also a major point of DLZ! */
		if (dbi->lookup_q == NULL) {
			isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
				      DNS_LOGMODULE_DLZ, ISC_LOG_DEBUG(2),
				      "No query specified for lookup.  "
				      "Lookup requires a query");
			result = ISC_R_FAILURE;
			goto cleanup;
		}
		break;
	default:
		/*
		 * this should never happen.  If it does, the code is
		 * screwed up!
		 */
		UNEXPECTED_ERROR(__FILE__, __LINE__,
				 "Incorrect query flag passed to "
				 "odbc_get_resultset");
		result = ISC_R_UNEXPECTED;
		goto cleanup;
	}

	/*
	 * was a zone string passed?  If so, make it safe for use in
	 * queries.
	 */
	if (zone != NULL) {
		dbi->zone = odbc_escape_string(zone);
		if (dbi->zone == NULL) {
			result = ISC_R_NOMEMORY;
			goto cleanup;
		}
	} else { /* no string passed, set the string pointer to NULL */
		dbi->zone = NULL;
	}

	/*
	 * was a record string passed?  If so, make it safe for use in
	 * queries.
	 */
	if (record != NULL) {
		dbi->record = odbc_escape_string(record);
		if (dbi->record == NULL) {
			result = ISC_R_NOMEMORY;
			goto cleanup;
		}
	} else { /* no string passed, set the string pointer to NULL */
		dbi->record = NULL;
	}

	/*
	 * was a client string passed?  If so, make it safe for use in
	 * queries.
	 */
	if (client != NULL) {
		dbi->client = odbc_escape_string(client);
		if (dbi->client == NULL) {
			result = ISC_R_NOMEMORY;
			goto cleanup;
		}
	} else { /* no string passed, set the string pointer to NULL */
		dbi->client = NULL;
	}

	/*
	 * what type of query are we going to run?
	 * this time we build the actual query to run.
	 */
	switch (query) {
	case ALLNODES:
		querystring = build_querystring(named_g_mctx, dbi->allnodes_q);
		break;
	case ALLOWXFR:
		querystring = build_querystring(named_g_mctx, dbi->allowxfr_q);
		break;
	case AUTHORITY:
		querystring = build_querystring(named_g_mctx, dbi->authority_q);
		break;
	case FINDZONE:
		querystring = build_querystring(named_g_mctx, dbi->findzone_q);
		break;
	case LOOKUP:
		querystring = build_querystring(named_g_mctx, dbi->lookup_q);
		break;
	default:
		/*
		 * this should never happen.  If it does, the code is
		 * screwed up!
		 */
		UNEXPECTED_ERROR(__FILE__, __LINE__,
				 "Incorrect query flag passed to "
				 "odbc_get_resultset");
		result = ISC_R_UNEXPECTED;
		goto cleanup;
	}

	/* if the querystring is null, Bummer, outta RAM.  UPGRADE TIME!!!   */
	if (querystring == NULL) {
		result = ISC_R_NOMEMORY;
		goto cleanup;
	}

	/* output the full query string during debug so we can see */
	/* what lame error the query has. */
	isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_DLZ,
		      ISC_LOG_DEBUG(1), "\nQuery String: %s\n", querystring);

	/* attempt query up to 3 times. */
	for (j = 0; j < 3; j++) {
		/* try to get result set */
		sqlRes = SQLExecDirect(((odbc_db_t *)dbi->dbconn)->stmnt,
				       (SQLCHAR *)querystring,
				       (SQLINTEGER)strlen(querystring));

		/* if error, reset DB connection */
		if (!sqlOK(sqlRes)) {
			/* close cursor */
			SQLCloseCursor(((odbc_db_t *)dbi->dbconn)->stmnt);
			/* attempt to reconnect */
			result = odbc_connect((odbc_instance_t *)dbdata,
					      (odbc_db_t **)&(dbi->dbconn));
			/* check if we reconnected */
			if (result != ISC_R_SUCCESS) {
				break;
			}
			/* in case this is the last time through the loop */
			result = ISC_R_FAILURE;
		} else {
			result = ISC_R_SUCCESS;
			/* return dbi */
			*r_dbi = dbi;
			/* result set ok, break loop */
			break;
		}
	} /* end for loop */

cleanup: /* it's always good to cleanup after yourself */

	/* free dbi->zone string */
	if (dbi->zone != NULL) {
		isc_mem_free(named_g_mctx, dbi->zone);
	}

	/* free dbi->record string */
	if (dbi->record != NULL) {
		isc_mem_free(named_g_mctx, dbi->record);
	}

	/* free dbi->client string */
	if (dbi->client != NULL) {
		isc_mem_free(named_g_mctx, dbi->client);
	}

	/* if we are done using this dbi, release the lock */
	if (result != ISC_R_SUCCESS) {
		isc_mutex_unlock(&dbi->instance_lock);
	}

	/* release query string */
	if (querystring != NULL) {
		isc_mem_free(named_g_mctx, querystring);
	}

	/* return result */
	return (result);
}

/*%
 * Gets a single field from the ODBC statement.  The memory for the
 * returned data is dynamically allocated.  If this method is successful
 * it is the responsibility of the caller to free the memory using
 * isc_mem_free(named_g_mctx, *ptr);
 */

static isc_result_t
odbc_getField(SQLHSTMT *stmnt, SQLSMALLINT field, char **data) {
	SQLLEN size;

	REQUIRE(data != NULL && *data == NULL);

	if (sqlOK(SQLColAttribute(stmnt, field, SQL_DESC_DISPLAY_SIZE, NULL, 0,
				  NULL, &size)) &&
	    size > 0)
	{
		*data = isc_mem_allocate(named_g_mctx, size + 1);
		if (data != NULL) {
			if (sqlOK(SQLGetData(stmnt, field, SQL_C_CHAR, *data,
					     size + 1, &size)))
			{
				return (ISC_R_SUCCESS);
			}
			isc_mem_free(named_g_mctx, *data);
		}
	}
	return (ISC_R_FAILURE);
}

/*%
 * Gets multiple fields from the ODBC statement.  The memory for the
 * returned data is dynamically allocated.  If this method is successful
 * it is the responsibility of the caller to free the memory using
 * isc_mem_free(named_g_mctx, *ptr);
 */

static isc_result_t
odbc_getManyFields(SQLHSTMT *stmnt, SQLSMALLINT startField,
		   SQLSMALLINT endField, char **retData) {
	isc_result_t result;
	SQLLEN size;
	int totSize = 0;
	SQLSMALLINT i;
	int j = 0;
	char *data;

	REQUIRE(retData != NULL && *retData == NULL);
	REQUIRE(startField > 0 && startField <= endField);

	/* determine how large the data is */
	for (i = startField; i <= endField; i++) {
		if (sqlOK(SQLColAttribute(stmnt, i, SQL_DESC_DISPLAY_SIZE, NULL,
					  0, NULL, &size)) &&
		    size > 0)
		{
			{
				/* always allow for a " " (space) character */
				totSize += (size + 1);
				/* after the data item */
			}
		}
	}

	if (totSize < 1) {
		return (ISC_R_FAILURE);
	}

	/* allow for a "\n" at the end of the string/ */
	data = isc_mem_allocate(named_g_mctx, ++totSize);

	result = ISC_R_FAILURE;

	/* get the data and concat all fields into a large string */
	for (i = startField; i <= endField; i++) {
		if (sqlOK(SQLGetData(stmnt, i, SQL_C_CHAR, &(data[j]),
				     totSize - j, &size)))
		{
			if (size > 0) {
				j += size;
				data[j++] = ' ';
				data[j] = '\0';
				result = ISC_R_SUCCESS;
			}
		} else {
			isc_mem_free(named_g_mctx, data);
			return (ISC_R_FAILURE);
		}
	}

	if (result != ISC_R_SUCCESS) {
		isc_mem_free(named_g_mctx, data);
		return (result);
	}

	*retData = data;
	return (ISC_R_SUCCESS);
}

/*%
 * The processing of result sets for lookup and authority are
 * exactly the same.  So that functionality has been moved
 * into this function to minimize code.
 */

static isc_result_t
odbc_process_rs(dns_sdlzlookup_t *lookup, dbinstance_t *dbi) {
	isc_result_t result;
	SQLSMALLINT fields;
	SQLHSTMT *stmnt;
	char *ttl_s;
	char *type;
	char *data;
	char *endp;
	int ttl;

	REQUIRE(dbi != NULL);

	stmnt = ((odbc_db_t *)(dbi->dbconn))->stmnt;

	/* get number of columns */
	if (!sqlOK(SQLNumResultCols(stmnt, &fields))) {
		isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
			      DNS_LOGMODULE_DLZ, ISC_LOG_ERROR,
			      "Odbc driver unable to process result set");
		result = ISC_R_FAILURE;
		goto process_rs_cleanup;
	}

	/* get things ready for processing */
	result = ISC_R_FAILURE;

	while (sqlOK(SQLFetch(stmnt))) {
		/* set to null for next pass through */
		data = type = ttl_s = NULL;

		switch (fields) {
		case 1:
			/*
			 * one column in rs, it's the data field.  use
			 * default type of A record, and default TTL
			 * of 86400.  attempt to get data, & tell bind
			 * about it.
			 */
			if ((result = odbc_getField(stmnt, 1, &data)) ==
			    ISC_R_SUCCESS)
			{
				result = dns_sdlz_putrr(lookup, "a", 86400,
							data);
			}
			break;
		case 2:
			/*
			 * two columns, data field, and data type.
			 * use default TTL of 86400.  attempt to get
			 * DNS type & data, then tell bind about it.
			 */
			if ((result = odbc_getField(stmnt, 1, &type)) ==
				    ISC_R_SUCCESS &&
			    (result = odbc_getField(stmnt, 2, &data)) ==
				    ISC_R_SUCCESS)
			{
				result = dns_sdlz_putrr(lookup, type, 86400,
							data);
			}
			break;
		default:
			/*
			 * 3 fields or more, concatenate the last ones
			 * together.  attempt to get DNS ttl, type,
			 * data then tell Bind about them.
			 */
			if ((result = odbc_getField(stmnt, 1, &ttl_s)) ==
				    ISC_R_SUCCESS &&
			    (result = odbc_getField(stmnt, 2, &type)) ==
				    ISC_R_SUCCESS &&
			    (result = odbc_getManyFields(
				     stmnt, 3, fields, &data)) == ISC_R_SUCCESS)
			{
				/* try to convert ttl string to int */
				ttl = strtol(ttl_s, &endp, 10);
				/* failure converting ttl. */
				if (*endp != '\0' || ttl < 0) {
					isc_log_write(dns_lctx,
						      DNS_LOGCATEGORY_DATABASE,
						      DNS_LOGMODULE_DLZ,
						      ISC_LOG_ERROR,
						      "Odbc driver ttl must "
						      "be a positive number");
					result = ISC_R_FAILURE;
				} else {
					/*
					 * successful converting TTL,
					 * tell Bind everything
					 */
					result = dns_sdlz_putrr(lookup, type,
								ttl, data);
				}
			} /* closes bid if () */
		}	  /* closes switch(fields) */

		/* clean up mem */
		if (ttl_s != NULL) {
			isc_mem_free(named_g_mctx, ttl_s);
		}
		if (type != NULL) {
			isc_mem_free(named_g_mctx, type);
		}
		if (data != NULL) {
			isc_mem_free(named_g_mctx, data);
		}

		/* I sure hope we were successful */
		if (result != ISC_R_SUCCESS) {
			isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
				      DNS_LOGMODULE_DLZ, ISC_LOG_ERROR,
				      "dns_sdlz_putrr returned error. "
				      "Error code was: %s",
				      isc_result_totext(result));
			result = ISC_R_FAILURE;
			goto process_rs_cleanup;
		}
	} /* closes while loop */

process_rs_cleanup:

	/* close cursor */
	SQLCloseCursor(((odbc_db_t *)(dbi->dbconn))->stmnt);

	/* free lock on dbi so someone else can use it. */
	isc_mutex_unlock(&dbi->instance_lock);

	return (result);
}

/*
 * SDLZ interface methods
 */

/*% determine if the zone is supported by (in) the database */

static isc_result_t
odbc_findzone(void *driverarg, void *dbdata, const char *name,
	      dns_clientinfomethods_t *methods, dns_clientinfo_t *clientinfo) {
	isc_result_t result;
	dbinstance_t *dbi = NULL;

	UNUSED(driverarg);
	UNUSED(methods);
	UNUSED(clientinfo);

	/* run the query and get the result set from the database. */
	/* if result != ISC_R_SUCCESS cursor and mutex already cleaned up. */
	/* so we don't have to do it here. */
	result = odbc_get_resultset(name, NULL, NULL, FINDZONE, dbdata, &dbi);

	/* Check that we got a result set with data */
	if (result == ISC_R_SUCCESS &&
	    !sqlOK(SQLFetch(((odbc_db_t *)(dbi->dbconn))->stmnt)))
	{
		result = ISC_R_NOTFOUND;
	}

	if (dbi != NULL) {
		/* get rid of result set, we are done with it. */
		SQLCloseCursor(((odbc_db_t *)(dbi->dbconn))->stmnt);

		/* free lock on dbi so someone else can use it. */
		isc_mutex_unlock(&dbi->instance_lock);
	}

	return (result);
}

/*% Determine if the client is allowed to perform a zone transfer */
static isc_result_t
odbc_allowzonexfr(void *driverarg, void *dbdata, const char *name,
		  const char *client) {
	isc_result_t result;
	dbinstance_t *dbi = NULL;

	UNUSED(driverarg);

	/* first check if the zone is supported by the database. */
	result = odbc_findzone(driverarg, dbdata, name, NULL, NULL);
	if (result != ISC_R_SUCCESS) {
		return (ISC_R_NOTFOUND);
	}

	/*
	 * if we get to this point we know the zone is supported by
	 * the database.  the only questions now are is the zone
	 * transfer is allowed for this client and did the config file
	 * have an allow zone xfr query
	 *
	 * Run our query, and get a result set from the database.  if
	 * result != ISC_R_SUCCESS cursor and mutex already cleaned
	 * up, so we don't have to do it here.
	 */
	result = odbc_get_resultset(name, NULL, client, ALLOWXFR, dbdata, &dbi);

	/* if we get "not implemented", send it along. */
	if (result == ISC_R_NOTIMPLEMENTED) {
		return (result);
	}

	/* Check that we got a result set with data */
	if (result == ISC_R_SUCCESS &&
	    !sqlOK(SQLFetch(((odbc_db_t *)(dbi->dbconn))->stmnt)))
	{
		result = ISC_R_NOPERM;
	}

	if (dbi != NULL) {
		/* get rid of result set, we are done with it. */
		SQLCloseCursor(((odbc_db_t *)(dbi->dbconn))->stmnt);

		/* free lock on dbi so someone else can use it. */
		isc_mutex_unlock(&dbi->instance_lock);
	}

	return (result);
}

/*%
 * If the client is allowed to perform a zone transfer, the next order of
 * business is to get all the nodes in the zone, so bind can respond to the
 * query.
 */

static isc_result_t
odbc_allnodes(const char *zone, void *driverarg, void *dbdata,
	      dns_sdlzallnodes_t *allnodes) {
	isc_result_t result;
	dbinstance_t *dbi = NULL;
	SQLHSTMT *stmnt;
	SQLSMALLINT fields;
	char *data;
	char *type;
	char *ttl_s;
	int ttl;
	char *host;
	char *endp;

	UNUSED(driverarg);

	/* run the query and get the result set from the database. */
	result = odbc_get_resultset(zone, NULL, NULL, ALLNODES, dbdata, &dbi);

	/* if we get "not implemented", send it along */
	if (result == ISC_R_NOTIMPLEMENTED) {
		return (result);
	}

	/* if we didn't get a result set, log an err msg. */
	if (result != ISC_R_SUCCESS) {
		isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
			      DNS_LOGMODULE_DLZ, ISC_LOG_ERROR,
			      "Odbc driver unable to return "
			      "result set for all nodes query");
		return (ISC_R_FAILURE);
	}

	stmnt = ((odbc_db_t *)(dbi->dbconn))->stmnt;

	/* get number of columns */
	if (!sqlOK(SQLNumResultCols(stmnt, &fields))) {
		isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
			      DNS_LOGMODULE_DLZ, ISC_LOG_ERROR,
			      "Odbc driver unable to process result set");
		result = ISC_R_FAILURE;
		goto allnodes_cleanup;
	}

	if (fields < 4) { /* gotta have at least 4 columns */
		isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
			      DNS_LOGMODULE_DLZ, ISC_LOG_ERROR,
			      "Odbc driver too few fields returned by "
			      "all nodes query");
		result = ISC_R_FAILURE;
		goto allnodes_cleanup;
	}

	/* get things ready for processing */
	result = ISC_R_FAILURE;

	while (sqlOK(SQLFetch(stmnt))) {
		/* set to null for next pass through */
		data = host = type = ttl_s = NULL;

		/*
		 * attempt to get DNS ttl, type, host, data then tell
		 * Bind about them
		 */
		if ((result = odbc_getField(stmnt, 1, &ttl_s)) ==
			    ISC_R_SUCCESS &&
		    (result = odbc_getField(stmnt, 2, &type)) ==
			    ISC_R_SUCCESS &&
		    (result = odbc_getField(stmnt, 3, &host)) ==
			    ISC_R_SUCCESS &&
		    (result = odbc_getManyFields(stmnt, 4, fields, &data)) ==
			    ISC_R_SUCCESS)
		{
			/* convert ttl string to int */
			ttl = strtol(ttl_s, &endp, 10);
			/* failure converting ttl. */
			if (*endp != '\0' || ttl < 0) {
				isc_log_write(dns_lctx,
					      DNS_LOGCATEGORY_DATABASE,
					      DNS_LOGMODULE_DLZ, ISC_LOG_ERROR,
					      "Odbc driver ttl must be "
					      "a positive number");
				result = ISC_R_FAILURE;
			} else {
				/* successful converting TTL, tell Bind  */
				result = dns_sdlz_putnamedrr(allnodes, host,
							     type, ttl, data);
			}
		} /* closes big if () */

		/* clean up mem */
		if (ttl_s != NULL) {
			isc_mem_free(named_g_mctx, ttl_s);
		}
		if (type != NULL) {
			isc_mem_free(named_g_mctx, type);
		}
		if (host != NULL) {
			isc_mem_free(named_g_mctx, host);
		}
		if (data != NULL) {
			isc_mem_free(named_g_mctx, data);
		}

		/* if we weren't successful, log err msg */
		if (result != ISC_R_SUCCESS) {
			isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
				      DNS_LOGMODULE_DLZ, ISC_LOG_ERROR,
				      "dns_sdlz_putnamedrr returned error. "
				      "Error code was: %s",
				      isc_result_totext(result));
			result = ISC_R_FAILURE;
			goto allnodes_cleanup;
		}
	} /* closes while loop */

allnodes_cleanup:

	/* close cursor */
	SQLCloseCursor(((odbc_db_t *)(dbi->dbconn))->stmnt);

	/* free lock on dbi so someone else can use it. */
	isc_mutex_unlock(&dbi->instance_lock);

	return (result);
}

/*%
 * if the lookup function does not return SOA or NS records for the zone,
 * use this function to get that information for Bind.
 */

static isc_result_t
odbc_authority(const char *zone, void *driverarg, void *dbdata,
	       dns_sdlzlookup_t *lookup) {
	isc_result_t result;
	dbinstance_t *dbi = NULL;

	UNUSED(driverarg);

	/* run the query and get the result set from the database. */
	result = odbc_get_resultset(zone, NULL, NULL, AUTHORITY, dbdata, &dbi);
	/* if we get "not implemented", send it along */
	if (result == ISC_R_NOTIMPLEMENTED) {
		return (result);
	}
	/* if we didn't get a result set, log an err msg. */
	if (result != ISC_R_SUCCESS) {
		isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
			      DNS_LOGMODULE_DLZ, ISC_LOG_ERROR,
			      "Odbc driver unable to return "
			      "result set for authority query");
		return (ISC_R_FAILURE);
	}
	/* lookup and authority result sets are processed in the same manner */
	/* odbc_process_rs does the job for both functions. */
	return (odbc_process_rs(lookup, dbi));
}

/*% if zone is supported, lookup up a (or multiple) record(s) in it */

static isc_result_t
odbc_lookup(const char *zone, const char *name, void *driverarg, void *dbdata,
	    dns_sdlzlookup_t *lookup, dns_clientinfomethods_t *methods,
	    dns_clientinfo_t *clientinfo) {
	isc_result_t result;
	dbinstance_t *dbi = NULL;

	UNUSED(driverarg);
	UNUSED(methods);
	UNUSED(clientinfo);

	/* run the query and get the result set from the database. */
	result = odbc_get_resultset(zone, name, NULL, LOOKUP, dbdata, &dbi);
	/* if we didn't get a result set, log an err msg. */
	if (result != ISC_R_SUCCESS) {
		isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
			      DNS_LOGMODULE_DLZ, ISC_LOG_ERROR,
			      "Odbc driver unable to return "
			      "result set for lookup query");
		return (ISC_R_FAILURE);
	}
	/* lookup and authority result sets are processed in the same manner */
	/* odbc_process_rs does the job for both functions. */
	return (odbc_process_rs(lookup, dbi));
}

/*%
 * create an instance of the driver.  Remember, only 1 copy of the driver's
 * code is ever loaded, the driver has to remember which context it's
 * operating in.  This is done via use of the dbdata argument which is
 * passed into all query functions.
 */
static isc_result_t
odbc_create(const char *dlzname, unsigned int argc, char *argv[],
	    void *driverarg, void **dbdata) {
	isc_result_t result;
	odbc_instance_t *odbc_inst = NULL;
	dbinstance_t *db = NULL;
	SQLRETURN sqlRes;
	int dbcount;
	int i;
	char *endp;

	UNUSED(dlzname);
	UNUSED(driverarg);

	/* if debugging, let user know we are multithreaded. */
	isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_DLZ,
		      ISC_LOG_DEBUG(1), "Odbc driver running multithreaded");

	/* verify we have at least 5 arg's passed to the driver */
	if (argc < 5) {
		isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
			      DNS_LOGMODULE_DLZ, ISC_LOG_ERROR,
			      "Odbc driver requires at least "
			      "4 command line args.");
		return (ISC_R_FAILURE);
	}

	/* no more than 8 arg's should be passed to the driver */
	if (argc > 8) {
		isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
			      DNS_LOGMODULE_DLZ, ISC_LOG_ERROR,
			      "Odbc driver cannot accept more than "
			      "7 command line args.");
		return (ISC_R_FAILURE);
	}

	/* multithreaded build can have multiple DB connections */
	/* check how many db connections we should create */
	dbcount = strtol(argv[1], &endp, 10);
	if (*endp != '\0' || dbcount < 0) {
		isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
			      DNS_LOGMODULE_DLZ, ISC_LOG_ERROR,
			      "Odbc driver database connection count "
			      "must be positive.");
		return (ISC_R_FAILURE);
	}

	/* allocate memory for odbc instance */
	odbc_inst = isc_mem_get(named_g_mctx, sizeof(odbc_instance_t));
	memset(odbc_inst, 0, sizeof(odbc_instance_t));

	/* parse connection string and get parameters. */

	/* get odbc database dsn - required */
	odbc_inst->dsn = (SQLCHAR *)getParameterValue(argv[2], "dsn=");
	if (odbc_inst->dsn == NULL) {
		isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
			      DNS_LOGMODULE_DLZ, ISC_LOG_ERROR,
			      "odbc driver requires a dns parameter.");
		result = ISC_R_FAILURE;
		goto cleanup;
	}
	/* get odbc database username */
	/* if no username was passed, set odbc_inst.user = NULL; */
	odbc_inst->user = (SQLCHAR *)getParameterValue(argv[2], "user=");

	/* get odbc database password */
	/* if no password was passed, set odbc_inst.pass = NULL; */
	odbc_inst->pass = (SQLCHAR *)getParameterValue(argv[2], "pass=");

	/* create odbc environment & set environment to ODBC V3 */
	if (odbc_inst->sql_env == NULL) {
		/* create environment handle */
		sqlRes = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE,
					&(odbc_inst->sql_env));
		if (!sqlOK(sqlRes)) {
			isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
				      DNS_LOGMODULE_DLZ, ISC_LOG_INFO,
				      "Odbc driver unable to allocate memory");
			result = ISC_R_NOMEMORY;
			goto cleanup;
		}
		/*set ODBC version = 3 */
		sqlRes = SQLSetEnvAttr(odbc_inst->sql_env,
				       SQL_ATTR_ODBC_VERSION,
				       (void *)SQL_OV_ODBC3, 0);
		if (!sqlOK(sqlRes)) {
			isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
				      DNS_LOGMODULE_DLZ, ISC_LOG_INFO,
				      "Unable to configure ODBC environment");
			result = ISC_R_NOMEMORY;
			goto cleanup;
		}
	}

	/* allocate memory for database connection list */
	odbc_inst->db = isc_mem_get(named_g_mctx, sizeof(db_list_t));

	/* initialize DB connection list */
	ISC_LIST_INIT(*odbc_inst->db);

	/* create the appropriate number of database instances (DBI) */
	/* append each new DBI to the end of the list */
	for (i = 0; i < dbcount; i++) {
		/* how many queries were passed in from config file? */
		switch (argc) {
		case 5:
			result = build_sqldbinstance(named_g_mctx, NULL, NULL,
						     NULL, argv[3], argv[4],
						     NULL, &db);
			break;
		case 6:
			result = build_sqldbinstance(named_g_mctx, NULL, NULL,
						     argv[5], argv[3], argv[4],
						     NULL, &db);
			break;
		case 7:
			result = build_sqldbinstance(named_g_mctx, argv[6],
						     NULL, argv[5], argv[3],
						     argv[4], NULL, &db);
			break;
		case 8:
			result = build_sqldbinstance(named_g_mctx, argv[6],
						     argv[7], argv[5], argv[3],
						     argv[4], NULL, &db);
			break;
		default:
			/* not really needed, should shut up compiler. */
			result = ISC_R_FAILURE;
		}

		/* unsuccessful?, log err msg and cleanup. */
		if (result != ISC_R_SUCCESS) {
			isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
				      DNS_LOGMODULE_DLZ, ISC_LOG_ERROR,
				      "Odbc driver could not create "
				      "database instance object.");
			goto cleanup;
		}

		/* when multithreaded, build a list of DBI's */
		ISC_LINK_INIT(db, link);
		ISC_LIST_APPEND(*odbc_inst->db, db, link);

		result = odbc_connect(odbc_inst, (odbc_db_t **)&(db->dbconn));

		if (result != ISC_R_SUCCESS) {
			/*
			 * if multi threaded, let user know which
			 * connection failed.  user could be
			 * attempting to create 10 db connections and
			 * for some reason the db backend only allows
			 * 9.
			 */
			isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
				      DNS_LOGMODULE_DLZ, ISC_LOG_ERROR,
				      "Odbc driver failed to create database "
				      "connection number %u after 3 attempts",
				      i + 1);
			goto cleanup;
		}

		/* set DB = null for next loop through. */
		db = NULL;
	} /* end for loop */

	/* set dbdata to the odbc_instance we created. */
	*dbdata = odbc_inst;

	/* hey, we got through all of that ok, return success. */
	return (ISC_R_SUCCESS);

cleanup:

	destroy_odbc_instance(odbc_inst);

	return (result);
}

/*%
 * destroy an instance of the driver.  Remember, only 1 copy of the driver's
 * code is ever loaded, the driver has to remember which context it's
 * operating in.  This is done via use of the dbdata argument.
 * so we really only need to clean it up since we are not using driverarg.
 */

static void
odbc_destroy(void *driverarg, void *dbdata) {
	UNUSED(driverarg);

	destroy_odbc_instance((odbc_instance_t *)dbdata);
}

/* pointers to all our runtime methods. */
/* this is used during driver registration */
/* i.e. in dlz_odbc_init below. */
static dns_sdlzmethods_t dlz_odbc_methods = {
	odbc_create,
	odbc_destroy,
	odbc_findzone,
	odbc_lookup,
	odbc_authority,
	odbc_allnodes,
	odbc_allowzonexfr,
	NULL,
	NULL,
	NULL,
	NULL,
	NULL,
	NULL,
	NULL,
};

/*%
 * Wrapper around dns_sdlzregister().
 */
isc_result_t
dlz_odbc_init(void) {
	isc_result_t result;

	/*
	 * Write debugging message to log
	 */
	isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_DLZ,
		      ISC_LOG_DEBUG(2), "Registering DLZ odbc driver.");

	/*
	 * Driver is always threadsafe.  When multithreaded all
	 * functions use multithreaded code.  When not multithreaded,
	 * all functions can only be entered once, but only 1 thread
	 * of operation is available in Bind.  So everything is still
	 * threadsafe.
	 */
	result = dns_sdlzregister("odbc", &dlz_odbc_methods, NULL,
				  DNS_SDLZFLAG_RELATIVEOWNER |
					  DNS_SDLZFLAG_RELATIVERDATA |
					  DNS_SDLZFLAG_THREADSAFE,
				  named_g_mctx, &dlz_odbc);
	/* if we can't register the driver, there are big problems. */
	if (result != ISC_R_SUCCESS) {
		UNEXPECTED_ERROR(__FILE__, __LINE__,
				 "dns_sdlzregister() failed: %s",
				 isc_result_totext(result));
		result = ISC_R_UNEXPECTED;
	}

	return (result);
}

/*%
 * Wrapper around dns_sdlzunregister().
 */
void
dlz_odbc_clear(void) {
	/*
	 * Write debugging message to log
	 */
	isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_DLZ,
		      ISC_LOG_DEBUG(2), "Unregistering DLZ odbc driver.");

	/* unregister the driver. */
	if (dlz_odbc != NULL) {
		dns_sdlzunregister(&dlz_odbc);
	}
}

#endif /* ifdef DLZ_ODBC */
