/* mod_auth_mysql v2.20, by and maintained by Zeev Suraski <bourbon@netvision.net.il>
 *
 * A couple of fixes by Marschall Peter <Peter.Marschall@gedos.de>
 * and Brent Metz <bmetz@thor.tjhsst.edu>
 *
 * Please read the README and USAGE for further information.
 */

#define AUTH_MYSQL_VERSION "2.20"

#include "config.h"

#include "httpd.h"
#include "http_config.h"

#if HAVE_AP_COMPAT_H
#include "ap_compat.h"
#elif HAVE_OLD_COMPAT_H
#include "compat.h"
#endif

#include "http_core.h"
#include "http_log.h"
#include "http_protocol.h"
#include "mysql.h"
#ifdef HAVE_CRYPT_H
#include <crypt.h>
#endif

static MYSQL auth_sql_server, *mysql_auth = NULL;
static char *auth_db_host = NULL, *auth_db_name = NULL, *auth_db_user = NULL, *auth_db_pwd = NULL;

#define MYSQL_ERROR(mysql) ((mysql)?(mysql_error(mysql)):"mysql server has gone away")


/* Support for general-purpose encryption schemes */

#define PLAINTEXT_ENCRYPTION_FLAG	1<<0
#define CRYPT_DES_ENCRYPTION_FLAG	1<<1
#define MYSQL_ENCRYPTION_FLAG		1<<2

static int check_no_encryption(const char *passwd, char *enc_passwd)
{
	return (!strcmp(passwd, enc_passwd));
}


#if STD_DES_CRYPT
static int check_crypt_des_encryption(const char *passwd, char *enc_passwd)
{
	return (!strcmp(crypt(passwd, enc_passwd), enc_passwd));
}
#endif


static int check_mysql_encryption(const char *passwd, char *enc_passwd)
{
	char scrambled_passwd[32];
	
	make_scrambled_password(scrambled_passwd, passwd);
	return (!strcmp(scrambled_passwd, enc_passwd));
}

typedef struct {
	char *name;
	int (*check_function)(const char *passwd, char *enc_passwd);
	int flag;
} encryption_type_entry;


encryption_type_entry supported_encryption_types[] = {
	{ "Plaintext",		check_no_encryption,			PLAINTEXT_ENCRYPTION_FLAG },
#if STD_DES_CRYPT
	{ "Crypt_DES",		check_crypt_des_encryption,		CRYPT_DES_ENCRYPTION_FLAG },
#endif
	{ "MySQL",			check_mysql_encryption,			MYSQL_ENCRYPTION_FLAG },
	/* add additional encryption types below */
	{ NULL,			NULL,						0 }
};

static int get_encryption_flag(char *name)
{
	register encryption_type_entry *ete=supported_encryption_types;
	
	while (ete->name) {
		if (!strcmp(ete->name, name)) {
			return ete->flag;
		}
		ete++;
	}
	return 0;
}

/* end of support for general-purpose encryption schemes */

typedef struct {
	char *db_user;
	char *db_pwd;
	char *db_name;

	char *user_table;
	char *group_table;

	char *user_field;
	char *password_field;
	char *group_field;
	
	int encryption_types;
	unsigned char encryption_types_initialized;

	unsigned char allow_empty_passwords;
	unsigned char assume_authoritative;
	unsigned char enable_mysql_auth;
	unsigned char non_persistent;
} mysql_auth_config_rec;

module auth_mysql_module;


void mysql_auth_init_handler(server_rec *s, pool *p)
{
#if MODULE_MAGIC_NUMBER >= 19980527
    ap_add_version_component("AuthMySQL/" AUTH_MYSQL_VERSION);
#endif
}

void *create_mysql_auth_dir_config(pool *p, char *d)
{
	mysql_auth_config_rec *sec = (mysql_auth_config_rec *) pcalloc(p, sizeof(mysql_auth_config_rec));

	sec->db_name = sec->db_user = sec->db_pwd = NULL;
	sec->user_table = sec->group_table = NULL;
	sec->user_field = sec->password_field = sec->group_field = NULL;

	sec->assume_authoritative = 1;
	sec->allow_empty_passwords = 1;
	sec->enable_mysql_auth = 1;

	sec->encryption_types = CRYPT_DES_ENCRYPTION_FLAG;
	sec->encryption_types_initialized = 0;
	
	sec->non_persistent = 0;
	
	return sec;
}


static const char *my_set_passwd_flag(cmd_parms *cmd, mysql_auth_config_rec *sec, int arg)
{
	sec->allow_empty_passwords = (unsigned char) arg;
	return NULL;
}


static const char *my_set_mysql_auth_flag(cmd_parms *cmd, mysql_auth_config_rec *sec, int arg)
{
	sec->enable_mysql_auth = (unsigned char) arg;
	return NULL;
}


static const char *my_set_authoritative_flag(cmd_parms *cmd, mysql_auth_config_rec *sec, int arg)
{
	sec->assume_authoritative = (unsigned char) arg;
	return NULL;
}


static const char *my_set_crypted_password_flag(cmd_parms *cmd, mysql_auth_config_rec *sec, int arg)
{
	if (arg) {
		sec->encryption_types = CRYPT_DES_ENCRYPTION_FLAG;
	} else {
		sec->encryption_types &= ~CRYPT_DES_ENCRYPTION_FLAG;
		if (!sec->encryption_types) {
			sec->encryption_types = PLAINTEXT_ENCRYPTION_FLAG;
		}
	}
	sec->encryption_types_initialized = 0;
	return NULL;
}


static const char *my_set_scrambled_password_flag(cmd_parms *cmd, mysql_auth_config_rec *sec, int arg)
{
	if (arg) {
		sec->encryption_types = MYSQL_ENCRYPTION_FLAG;
	} else {
		sec->encryption_types &= ~MYSQL_ENCRYPTION_FLAG;
		if (!sec->encryption_types) {
			sec->encryption_types = PLAINTEXT_ENCRYPTION_FLAG;
		}
	}
	sec->encryption_types_initialized = 0;
	return NULL;
}


static const char *my_set_string_slot(cmd_parms *cmd, char *struct_ptr, char *arg)
{
	int offset = (int) cmd->info;

	*(char **) (struct_ptr + offset) = pstrdup(cmd->pool, arg);
	return NULL;
}


static const char *set_auth_mysql_info(cmd_parms * parms, void *dummy, char *host, char *user, char *pwd)
{
	if (*host != '.') {
		auth_db_host = host;
	}
	if (*user != '.') {
		auth_db_user = user;
	}
	if (*pwd != '.') {
		auth_db_pwd = pwd;
	}
	return NULL;
}


static const char *set_auth_mysql_db(cmd_parms * parms, void *dummy, char *db)
{
	auth_db_name = db;
	return NULL;
}

static const char *my_set_encryption_types(cmd_parms *cmd, mysql_auth_config_rec *sec, char *arg)
{
	int new_encryption_flag = get_encryption_flag(arg);

	if (!new_encryption_flag) {
		log_error("Unsupported encryption type", cmd->server);
		return NULL;
	}

	if (!sec->encryption_types_initialized) {
		sec->encryption_types = 0;
		sec->encryption_types_initialized = 1;
	}
	
	sec->encryption_types |= new_encryption_flag;
	
	return NULL;
}


static const char *my_set_non_persistent(cmd_parms *cmd, mysql_auth_config_rec *sec, int arg)
{
	sec->non_persistent = (unsigned char) arg;
	return NULL;
}

command_rec mysql_auth_cmds[] = {
	{ "Auth_MySQL_Info",			set_auth_mysql_info,	NULL,	RSRC_CONF,	TAKE3,	"host, user and password of the MySQL database" },
	{ "Auth_MySQL_General_DB",		set_auth_mysql_db,		NULL,	RSRC_CONF,	TAKE1,	"default database for MySQL authentication" },
	{ "Auth_MySQL_Username",		my_set_string_slot,		(void *) XtOffsetOf(mysql_auth_config_rec, db_user),	OR_AUTHCFG, TAKE1,	"database user" },
	{ "Auth_MySQL_Password",		my_set_string_slot,		(void *) XtOffsetOf(mysql_auth_config_rec, db_pwd),		OR_AUTHCFG, TAKE1,	"database password" },
	{ "Auth_MySQL_DB",				my_set_string_slot,		(void *) XtOffsetOf(mysql_auth_config_rec, db_name),	OR_AUTHCFG, TAKE1,	"database name" },
	{ "Auth_MySQL_Password_Table",	my_set_string_slot,		(void *) XtOffsetOf(mysql_auth_config_rec, user_table),	OR_AUTHCFG, TAKE1,	"Name of the MySQL table containing the password/user-name combination" },
	{ "Auth_MySQL_Group_Table",		my_set_string_slot,		(void *) XtOffsetOf(mysql_auth_config_rec, group_table),OR_AUTHCFG, TAKE1,	"Name of the MySQL table containing the group-name/user-name combination; can be the same as the password-table." },
	{ "Auth_MySQL_Password_Field",	my_set_string_slot,		(void *) XtOffsetOf(mysql_auth_config_rec, password_field),	OR_AUTHCFG, TAKE1,	"The name of the field in the MySQL password table" },
	{ "Auth_MySQL_Username_Field",	my_set_string_slot,		(void *) XtOffsetOf(mysql_auth_config_rec, user_field),	OR_AUTHCFG, TAKE1,	"The name of the user-name field in the MySQL password (and possibly group) table(s)." },
	{ "Auth_MySQL_Group_Field",		my_set_string_slot,		(void *) XtOffsetOf(mysql_auth_config_rec, group_field),OR_AUTHCFG, TAKE1,	"The name of the group field in the MySQL group table; must be set if you want to use groups." },
	{ "Auth_MySQL_Empty_Passwords",	my_set_passwd_flag,		NULL,	OR_AUTHCFG,	FLAG,	"Enable (on) or disable (off) empty password strings; in which case any user password is accepted." },
	{ "Auth_MySQL_Authoritative",		my_set_authoritative_flag,		NULL,	OR_AUTHCFG,	FLAG,	"When 'on' the MySQL database is taken to be authoritative and access control is not passed along to other db or access modules." },
	{ "Auth_MySQL_Encrypted_Passwords",	my_set_crypted_password_flag,	NULL,	OR_AUTHCFG,	FLAG,	"When 'on' the password in the password table are taken to be crypt()ed using your machines crypt() function." },
	{ "Auth_MySQL_Scrambled_Passwords",	my_set_scrambled_password_flag,	NULL,	OR_AUTHCFG,	FLAG,	"When 'on' the password in the password table are taken to be scramble()d using mySQL's password() function." },
	{ "Auth_MySQL",						my_set_mysql_auth_flag,			NULL,	OR_AUTHCFG, FLAG,	"Enable (on) or disable (off) MySQL authentication." },
    { "Auth_MySQL_Encryption_Types",	my_set_encryption_types,		NULL,	OR_AUTHCFG, ITERATE,"Encryption types to use" },
    { "Auth_MySQL_Non_Persistent",		my_set_non_persistent,			NULL,	OR_AUTHCFG,	FLAG,	"Use non-persistent MySQL links" },
	{ NULL }
};


static char *mysql_escape(char *str, pool *p)
{
	int need_to_escape = 0;
	register char *source;

	if (!str) {
		return NULL;
	}
	source = str;
	/* first find out if we need to escape */
	while (*source) {
		if (*source == '\'' || *source == '\\' || *source == '\"') {
			need_to_escape = 1;
			break;
		}
		source++;
	}

	if (need_to_escape) {
		int length = strlen(str);
		char *tmp_str;
		register char *target;

		source = str;

		tmp_str = target = (char *) palloc(p, length * 2 + 1);	/* worst case situation, which wouldn't be a pretty sight :) */
		if (!target) {
			return str;
		}
		while (*source) {
			switch (*source) {
				case '\'':
				case '\"':
				case '\\':
					*target++ = '\\';
					/* break missing intentionally */
				default:
					*target++ = *source++;
					break;
			}
		}
		*target = 0;
		return tmp_str;
	} else {
		return str;
	}
}


static void auth_mysql_cleanup(void *mysql)
{
	if (mysql) {
		mysql_close((MYSQL *) mysql);
	}
}


static void auth_mysql_cleanup_child(void *mysql)
{ 
}


static void note_cleanups_for_mysql_auth(pool *p, MYSQL *mysql)
{ 
	register_cleanup(p, (void *) mysql, auth_mysql_cleanup, auth_mysql_cleanup_child);
}


static void auth_mysql_result_cleanup(void *result)
{
	mysql_free_result((MYSQL_RES *) result);
}


static void note_cleanups_for_mysql_auth_result(pool *p, MYSQL_RES * result)
{
	register_cleanup(p, (void *) result, auth_mysql_result_cleanup, auth_mysql_result_cleanup);
}


static void open_auth_dblink(request_rec *r, mysql_auth_config_rec *sec)
{
	char *name = auth_db_name, *user = auth_db_user, *pwd = auth_db_pwd;

	if (mysql_auth != NULL) {	/* link already opened */
		return;
	}
	if (!user) {
		user = sec->db_user;
	}
	if (!pwd) {
		pwd = sec->db_pwd;
	}
	if (!name) {
		name = sec->db_name;
	}
	if (name != NULL) {			/* open an SQL link */
		/* link to the MySQL database and register its cleanup!@$ */
		mysql_auth = mysql_connect(&auth_sql_server, auth_db_host, user, pwd);
		if (sec->non_persistent && mysql_auth) {
			note_cleanups_for_mysql_auth(r->pool, mysql_auth);
		}
	}
}


static int safe_mysql_query(request_rec *r, char *query, mysql_auth_config_rec *sec)
{
	int error = 1;
	char *str;
	int was_connected = 0;
	void (*sigpipe_handler)();

	sigpipe_handler = signal(SIGPIPE, SIG_IGN);

	if (mysql_auth) {
		mysql_select_db(mysql_auth, (sec->db_name ? sec->db_name : auth_db_name));
	}
	if (!mysql_auth || ((error = mysql_query(mysql_auth, query)) && !strcasecmp(mysql_error(mysql_auth), "mysql server has gone away"))) {
		/* we need to restart the server link */
		if (mysql_auth) {
			log_error("MySQL auth:  connection lost, attempting reconnect", r->server);
			was_connected = 1;
		}
		mysql_auth = NULL;
		open_auth_dblink(r, sec);
		if (mysql_auth == NULL) {	/* unable to link */
			signal(SIGPIPE, sigpipe_handler);
			str = pstrcat(r->pool, "MySQL auth:  connect failed:  ", MYSQL_ERROR(&auth_sql_server), NULL);
			log_error(str, r->server);
			return error;
		}
		if (was_connected) {
			log_error("MySQL auth:  connect successful.", r->server);
		}
		error = mysql_select_db(mysql_auth, (sec->db_name ? sec->db_name : auth_db_name)) || mysql_query(mysql_auth, query);
	}
	signal(SIGPIPE, sigpipe_handler);

	if (error) {
		str = pstrcat(r->pool, "MySQL query failed:  ", query, NULL);
		log_error(str, r->server);
		str = pstrcat(r->pool, "MySQL failure reason:  ", MYSQL_ERROR(mysql_auth), NULL);
		log_error(str, r->server);
	}
	return error;
}

static MYSQL_RES *safe_mysql_store_result(pool *p)
{
	MYSQL_RES *result = mysql_store_result(mysql_auth);

	if (result) {
		block_alarms();
		note_cleanups_for_mysql_auth_result(p, result);
		unblock_alarms();
	}
	return result;
}


/* returns 1 on successful match, 0 unsuccessful match, -1 on error */
static int mysql_check_user_password(request_rec *r, char *user, const char *password, mysql_auth_config_rec *sec)
{
	char *auth_table = "mysql_auth", *auth_user_field = "username", *auth_password_field = "passwd";
	char *query;
	char *esc_user = mysql_escape(user, r->pool);
	MYSQL_RES *result;
	MYSQL_ROW sql_row;
	encryption_type_entry *ete;

	if (sec->user_table) {
		auth_table = sec->user_table;
	}
	if (sec->user_field) {
		auth_user_field = sec->user_field;
	}
	if (sec->password_field) {
		auth_password_field = sec->password_field;
	}
	query = (char *) pstrcat(r->pool, "select ", auth_password_field, " from ", auth_table,
				  " where ", auth_user_field, "='", esc_user, "'", NULL);
	if (!query) {
		return -1;
	}
	if (safe_mysql_query(r, query, sec)) {
		return -1;
	}
	result = safe_mysql_store_result(r->pool);
	if (!result) {
		return -1;
	}
	switch (mysql_num_rows(result)) {
		case 0:
			return 0;
			break;
		case 1:
			sql_row = mysql_fetch_row(result);
			/* ensure we have a row, and non NULL value */
			if (!sql_row || !sql_row[0]) {
				return -1;
			}
			
			/* empty password support */
			if (sec->allow_empty_passwords && !strlen(sql_row[0])) {
				return 1;
			}
			
			for (ete=supported_encryption_types; ete->name; ete++) {
				if (sec->encryption_types & ete->flag) {
					if (ete->check_function(password, sql_row[0])) {
						return 1;
					}
				}
			}
			return 0;
			

			break;
		default:
			return -1;
			break;
	}
	return -1;
}


/* returns 0 if user is not a part of the group, positive integer if he is, -1 on error */
static int mysql_check_group(request_rec *r, char *user, char *groups_query, mysql_auth_config_rec *sec)
{
	char *auth_table = "mysql_auth", *auth_user_field = "username";
	char *query;
	char *esc_user = mysql_escape(user, r->pool);
	MYSQL_RES *result;
	MYSQL_ROW row;

	if (!groups_query) {
		return 0;
	}
	
	if (sec->group_table) {
		auth_table = sec->group_table;
	}
	if (sec->user_field) {
		auth_user_field = sec->user_field;
	}

   query = pstrcat(r->pool,"select count(*) from ",auth_table,
   " where ",auth_user_field,"='",esc_user,"'"
   " and (",groups_query,")",NULL);

	if (!query) {
		return -1;
	}
	if (safe_mysql_query(r, query, sec)) {
		return -1;
	}
	result = safe_mysql_store_result(r->pool);
	if (!result || (row=mysql_fetch_row(result))==NULL || !row[0]) {
		return -1;
	}
	return atoi(row[0]);
}


int mysql_authenticate_basic_user(request_rec *r)
{
	mysql_auth_config_rec *sec = (mysql_auth_config_rec *) get_module_config(r->per_dir_config,
													 &auth_mysql_module);
	conn_rec *c = r->connection;
	const char *sent_pw;
	int res;

	/* obtain sent password */
	if ((res = get_basic_auth_pw(r, &sent_pw))) {
		return res;
	}

	/* use MySQL auth only if we have a database */
	if (!sec->enable_mysql_auth || (!auth_db_name && !sec->db_name)) {
		return DECLINED;
	}

	switch (mysql_check_user_password(r, c->user, sent_pw, sec)) {
		case 0:
			note_basic_auth_failure(r);
			return AUTH_REQUIRED;
			break;
		case 1:
			return OK;
			break;
		case -1:
		default:
			return SERVER_ERROR;
			break;
	}
}

/* Checking ID

 * I'm not even pretending I really tried to understand this .htaccess requires decoding
 * mess.  I copied it more or less as-is from mod_auth.c, and changed the group-related
 * stuff to be authenticated against the MySQL database, instead of the flatfile
 */
int mysql_check_auth(request_rec *r)
{
	mysql_auth_config_rec *sec = (mysql_auth_config_rec *) get_module_config(r->per_dir_config, &auth_mysql_module);
	char *user = r->connection->user;
	int m = r->method_number;
	int method_restricted = 0;
	register int x;
	const char *t, *w;
	const array_header *reqs_arr = requires(r);
	require_line *reqs;

	/* use MySQL auth only if we have a database */
	if (!sec->enable_mysql_auth || (!auth_db_name && !sec->db_name)) {
		return DECLINED;
	}

	if (!reqs_arr) {
		if (sec->assume_authoritative) {
			return AUTH_REQUIRED;
		} else {
			return DECLINED;
		}
	}
	reqs = (require_line *) reqs_arr->elts;

	for (x = 0; x < reqs_arr->nelts; x++) {
		if (!(reqs[x].method_mask & (1 << m))) {
			continue;
		}
		method_restricted = 1;

		t = reqs[x].requirement;
		w = getword(r->pool, &t, ' ');
		if (!strcmp(w, "valid-user")) {
			return OK;
		}
		if (!strcmp(w, "user")) {
			while (t[0]) {
				w = getword_conf(r->pool, &t);
				if (!strcmp(user, w)) {
					return OK;
				}
			}
		} else if (!strcmp(w, "group")) {
			char *groups_query=NULL,*auth_group_field="groups";
			
			if (sec->group_field) {
				auth_group_field = sec->group_field;
			}
			while (t[0]) {
				w = getword_conf(r->pool, &t);
				if (!groups_query) {
					groups_query = pstrcat(r->pool,auth_group_field,"='",mysql_escape((char *)w,r->pool),"'",NULL);
				} else {
					groups_query = pstrcat(r->pool,groups_query," or ",auth_group_field,"='",mysql_escape((char *)w,r->pool),"'",NULL);
				}
			}
			switch (mysql_check_group(r, user, groups_query, sec)) {
				case 0:
					break;	/* no match */
				case -1:
					break;	/* error */
				default:
					return OK;	/* authenticated */
					break;
			}
		}
	}

	if (!method_restricted) {
		return OK;
	}
	if (!(sec->assume_authoritative)) {
		return DECLINED;
	}
	note_basic_auth_failure(r);
	return AUTH_REQUIRED;
}


module auth_mysql_module =
{
	STANDARD_MODULE_STUFF,
	mysql_auth_init_handler,		/* initializer */
	create_mysql_auth_dir_config,	/* dir config creater */
	NULL,						/* dir merger --- default is to override */
	NULL,						/* server config */
	NULL,						/* merge server config */
	mysql_auth_cmds,			/* command table */
	NULL,						/* handlers */
	NULL,						/* filename translation */
	mysql_authenticate_basic_user,	/* check_user_id */
	mysql_check_auth,			/* check auth */
	NULL,						/* check access */
	NULL,						/* type_checker */
	NULL,						/* pre-run fixups */
	NULL						/* logger */
#if MODULE_MAGIC_NUMBER >= 19970103
	,NULL                       /* header parser */
#endif
#if MODULE_MAGIC_NUMBER >= 19970719
	,NULL                       /* child_init */
#endif
#if MODULE_MAGIC_NUMBER >= 19970728
	,NULL                       /* child_exit */
#endif
#if MODULE_MAGIC_NUMBER >= 19970902
	,NULL                       /* post read-request */
#endif
};
