/*
	mod_mya - MySQL Authentication Module
    Copyright (C) 2002  Michael K Link

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/
/*  $Id: mod_mya.c,v 1.2 2002/10/31 04:20:47 mlink Exp $
*/

#define __EXTENSIONS__

#include "apr.h"
#include "apr_strings.h"
#include "apr_lib.h"
#include "apr_uri.h"

#include "ap_config.h"
#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_log.h"
#include "http_main.h"
#include "http_protocol.h"
#include "http_request.h"
#include "util_script.h"

#include "ap_config_auto.h"

#ifdef HAVE_STDDEF_H
#include <stddef.h>
#endif

#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

#include <mysql.h>

static int mya_init_handler(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *s);
static void *mya_create_dir_config(apr_pool_t *p, char *d);
static void *mya_merge_dir_config(apr_pool_t *p, void *parentv, void *childv);

/* 5  */ static int mya_check_user_id(request_rec *r);
/* 6  */ static int mya_auth_checker(request_rec *r);

static const char* set_switch(cmd_parms *parms, void *mconfig, int flag);
static const char* set_field(cmd_parms *parms, void *mconfig, const char *arg);
static const char* set_port(cmd_parms *parms, void *mconfig, const char *arg);

enum {
	f_on			= 1,
	e_plaintext		= 2,
	e_crypt			= 4,
	e_password		= 8,
	
	e_min			= 2,	// min(encryption types)
	e_mask			= 14,	// sum of encryption types
	
	f_auth			= 16,
	
	f_compress		= 32,
	f_ssl			= 64
};

static const command_rec mya_commands[] = {
	AP_INIT_FLAG(		"mya",						set_switch,		(void*) f_on,		OR_AUTHCFG,	"turn mya on or off." ),
	AP_INIT_FLAG(		"mya_CLIENT_COMPRESS",		set_switch,		(void*) f_compress,	OR_AUTHCFG,		"use mysql CLIENT_COMPRESS feature." ),
	AP_INIT_FLAG(		"mya_CLIENT_SSL",			set_switch,		(void*) f_ssl,	OR_AUTHCFG,		"use mysql CLIENT_SSL feature." ),
	AP_INIT_TAKE1(		"mya_MySQL_Database",		set_field,		(void*) 0,			OR_AUTHCFG,	"database to use for authentication." ),
	AP_INIT_TAKE1(		"mya_MySQL_Table",			set_field,		(void*) 1,			OR_AUTHCFG,	"table to use for authentication." ),
	AP_INIT_TAKE1(		"mya_MySQL_Password_Field",	set_field,		(void*) 2,			OR_AUTHCFG,	"password field in table." ),
	AP_INIT_TAKE1(		"mya_MySQL_Group_Field",	set_field,		(void*) 3,			OR_AUTHCFG,	"group field in table." ),
	AP_INIT_TAKE1(		"mya_MySQL_Username_Field",	set_field,		(void*) 4,			OR_AUTHCFG,	"user field in table." ),
	AP_INIT_TAKE1(		"mya_MySQL_Host",			set_field,		(void*) 5,			OR_AUTHCFG,	"host to connect to." ),
	AP_INIT_TAKE1(		"mya_MySQL_Port",			set_port,		NULL,				OR_AUTHCFG,	"port to connect to." ),
	AP_INIT_TAKE1(		"mya_MySQL_Password",		set_field,		(void*) 6,			OR_AUTHCFG,	"password to use for connection." ),
	AP_INIT_TAKE1(		"mya_MySQL_Username",		set_field,		(void*) 7,			OR_AUTHCFG,	"username to use for connection." ),
	AP_INIT_TAKE1(		"mya_MySQL_Group_Table",	set_field,		(void*) 8,			OR_AUTHCFG,	"table to use for groups." ),
	AP_INIT_FLAG(		"mya_Authoritative",		set_switch,		(void*) f_auth,		OR_AUTHCFG,	"set authoritative on or off." ),
	AP_INIT_TAKE1(		"mya_Encryption",			set_field,		(void*) 9,			OR_AUTHCFG,	"set encryption methods." ),
	{ NULL }
};

static void register_hooks(apr_pool_t *p)
{
	ap_hook_post_config(mya_init_handler, NULL, NULL, APR_HOOK_MIDDLE);
	ap_hook_check_user_id(mya_check_user_id, NULL, NULL, APR_HOOK_MIDDLE);
	ap_hook_auth_checker(mya_auth_checker, NULL, NULL, APR_HOOK_MIDDLE);
}

AP_DECLARE_DATA module mya_module = {
    STANDARD20_MODULE_STUFF,
    mya_create_dir_config,			/* create per-directory config structure */
    mya_merge_dir_config,			/* merge per-directory config structures */
    NULL,							/* create per-server config structure */
    NULL,							/* merge per-server config structures */
    mya_commands,					/* command apr_table_t */
    register_hooks					/* register hooks */
};

typedef struct {
	ptrdiff_t		fl;
	
	char			*mysql_database;
	char			*mysql_table;
	char			*mysql_group_table;
	char			*mysql_username_field;
	char			*mysql_password_field;
	char			*mysql_group_field;
	
	char			*mysql_host;
	unsigned int	mysql_port;
	char			*mysql_username;
	char			*mysql_password;
	
	MYSQL			*mysql;
} mya_config_rec;

static void *mya_create_dir_config(apr_pool_t *p, char *d)
{
	mya_config_rec *myar = (mya_config_rec*) apr_pcalloc(p, sizeof(mya_config_rec));

	myar->fl = f_on|f_auth|e_plaintext;
	
	return myar;
}

static void *mya_merge_dir_config(apr_pool_t *p, void *parentv, void *childv)
{
	mya_config_rec *parent = (mya_config_rec *) parentv;
	mya_config_rec *child = (mya_config_rec *) childv;
	mya_config_rec *conf = (mya_config_rec*) apr_pcalloc(p, sizeof(mya_config_rec));
	
	if (child->fl & f_on) {
		conf->fl						= child->fl;
		
		if (parent->fl & f_compress) {
			conf->fl |= f_compress;
		}
		
		if (parent->fl & f_ssl) {
			conf->fl |= f_ssl;
		}
		
		if (parent->fl & f_auth) {
			conf->fl |= f_auth;
		}
		
		if (parent->fl & e_plaintext) {
			conf->fl |= e_plaintext;
		}
		
		if (parent->fl & e_crypt) {
			conf->fl |= e_crypt;
		}
		
		if (parent->fl & e_password) {
			conf->fl |= e_password;
		}
		
		conf->mysql_database			= (child->mysql_database ? child->mysql_database : parent->mysql_database);
		conf->mysql_table				= (child->mysql_table ? child->mysql_table : parent->mysql_table);
		conf->mysql_group_table			= (child->mysql_group_table ? child->mysql_group_table : parent->mysql_group_table);
		conf->mysql_username_field		= (child->mysql_username_field ? child->mysql_username_field : parent->mysql_username_field);
		conf->mysql_password_field		= (child->mysql_password_field ? child->mysql_password_field : parent->mysql_password_field);
		conf->mysql_group_field	= (child->mysql_group_field ? child->mysql_group_field : parent->mysql_group_field);
		conf->mysql_host				= (child->mysql_host ? child->mysql_host : parent->mysql_host);
		conf->mysql_port				= (child->mysql_port ? child->mysql_port : parent->mysql_port);
		conf->mysql_username			= (child->mysql_username ? child->mysql_username : parent->mysql_username);
		conf->mysql_password			= (child->mysql_password ? child->mysql_password : parent->mysql_password);
				
		conf->mysql						= NULL;
	}
	
	return conf;
}

static const char* set_switch(cmd_parms *parms, void *mconfig, int flag)
{
	ptrdiff_t pos = (ptrdiff_t) parms->info;
	mya_config_rec *myar = (mya_config_rec*) mconfig;
	
	if (flag) {
		myar->fl |= pos;
	}
	else {
		myar->fl &= ~pos;
	}
	
	return NULL;
}

static const char* set_field(cmd_parms *parms, void *mconfig, const char *arg)
{
	ptrdiff_t pos = (ptrdiff_t) parms->info;
	mya_config_rec *myar = (mya_config_rec*) mconfig;
	
	switch (pos) {
	  case 0:	myar->mysql_database = apr_pstrdup(parms->pool, arg);
		break;
	  case 1:	myar->mysql_table = apr_pstrdup(parms->pool, arg);
		break;
	  case 2:	myar->mysql_password_field = apr_pstrdup(parms->pool, arg);
		break;
	  case 3:	myar->mysql_group_field = apr_pstrdup(parms->pool, arg);
		break;
	  case 4:	myar->mysql_username_field = apr_pstrdup(parms->pool, arg);
		break;
	  case 5:	myar->mysql_host = apr_pstrdup(parms->pool, arg);
		break;
	  case 6:	myar->mysql_password = apr_pstrdup(parms->pool, arg);
		break;
	  case 7:	myar->mysql_username = apr_pstrdup(parms->pool, arg);
		break;
	  case 8:	myar->mysql_group_table = apr_pstrdup(parms->pool, arg);
		break;
	  case 9:
		if (!strcasecmp(arg, "PlainText")) {
			myar->fl &= ~e_mask;
			myar->fl |= e_plaintext;
		}
		else if (!strcasecmp(arg, "Crypt")) {
			myar->fl &= ~e_mask;
			myar->fl |= e_crypt;
		}
		else if (!strcasecmp(arg, "Password")) {
			myar->fl &= ~e_password;
			myar->fl |= e_password;
		}
		else if (!strcasecmp(arg, "All")) {
			myar->fl |= e_mask;
		}
		
		break;
	}
	
	return NULL;
}

static const char* set_port(cmd_parms *parms, void *mconfig, const char *arg)
{
	ptrdiff_t pos = (ptrdiff_t) parms->info;
	mya_config_rec *myar = (mya_config_rec*) mconfig;

	myar->mysql_port = (unsigned int) atoi(arg);
	
	return NULL;
}

#ifndef CLIENT_COMPRESS
# define CLIENT_COMPRESS 0
#endif

#ifndef CLIENT_SSL
# define CLIENT_SSL 0
#endif

static int my_connect(request_rec *r, mya_config_rec *myar)
{
	if (!myar->mysql) {
		unsigned int client_flags = 0;
		
		if (!(myar->mysql = mysql_init(NULL))) {
			ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, r->server, "mya: my_connect: unable to allocate MYSQL connection.");
	
			return -1;
		}
		
		if (myar->fl & f_compress) {
			client_flags |= CLIENT_COMPRESS;
		}
		
		if (myar->fl & f_ssl) {
			client_flags |= CLIENT_SSL;
		}
		
		if (!mysql_real_connect(myar->mysql, myar->mysql_host, myar->mysql_username, myar->mysql_password, myar->mysql_database, myar->mysql_port, NULL, client_flags)) {
			ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, r->server, "mya: my_connect: unable to connect to database: %s.", mysql_error(myar->mysql));
			mysql_close(myar->mysql);
			myar->mysql = NULL;
	
			return -1;
		}
	}
	
	return 0;
}

static int user_check(request_rec *r, char *user, const char *pass, mya_config_rec *myar)
{
	MYSQL_RES *res;
	MYSQL_ROW row;
	char *qstr;
	int i;
	
	if (my_connect(r, myar) == -1) {
		return -1;
	}
		
	qstr = apr_psprintf(r->pool, "SELECT %s FROM %s WHERE %s = '%s'", myar->mysql_password_field, myar->mysql_table, myar->mysql_username_field, user);
	
	if (mysql_real_query(myar->mysql, qstr, strlen(qstr))) {
		ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, r->server, "mya: user_check: %s/%s", mysql_error(myar->mysql), user);
		mysql_close(myar->mysql);
		myar->mysql = NULL;
		
		return -1;
	}
	
	if (!(res = mysql_store_result(myar->mysql))) {
		ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, r->server, "mya: user_check: %s/%s", mysql_error(myar->mysql), user);
		mysql_close(myar->mysql);
		myar->mysql = NULL;
		
		return -1;
	}
	
	switch (mysql_num_rows(res)) {
	  case 1: break; /* what we want */
	  case 0:
		mysql_free_result(res);
		
		return 1;
	  default:
		ap_log_error(APLOG_MARK, APLOG_ALERT, 0, r->server, "mya: user_check: %s has more than 1 password row, failing.", user);
		mysql_free_result(res);
		
		return -1;
	}
	
	if (!(row = mysql_fetch_row(res))) {
		ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, r->server, "mya: user_check: %s/%s", mysql_error(myar->mysql), user);
		mysql_free_result(res);
		mysql_close(myar->mysql);
		myar->mysql = NULL;
		
		return -1;
	}
	
	for (i = e_min; i < e_mask; i *= 2) {
		switch (i) {
		  case e_plaintext:
			if (myar->fl & e_plaintext) {
				if (!strcmp(row[0], pass)) {
					mysql_free_result(res);
					return 0;
				}
			}
			
			break;
		  case e_crypt:
			if (myar->fl & e_crypt) {
				if (!strcmp((char*) crypt(pass, row[0]), row[0])) {
					mysql_free_result(res);
					return 0;
				}
			}
			
			break;
			/* patch from Mark Foster <mdf@enic.cc> for PASSWORD() support */
		  case e_password:
			// row[0] is what is in the table
			// pass is what the user supplied
			if (myar->fl & e_password) {
				char scrambled_pass[32];

				make_scrambled_password(scrambled_pass, pass);

				if (!strcmp((char*) scrambled_pass, row[0])) {
					mysql_free_result(res);
					return 0;
				}
			}
		}
	}
	
	mysql_free_result(res);
	
	return 1;
}

static int group_check(request_rec *r, char *user, char *gq, mya_config_rec *myar)
{
	MYSQL_RES *res;
	MYSQL_ROW row;
	char *qstr;
	int return_code;
	
	if (my_connect(r, myar) == -1) {
		return -1;
	}
	
	qstr = apr_psprintf(r->pool, "SELECT COUNT(*) FROM %s WHERE %s = '%s' AND (%s)", myar->mysql_group_table, myar->mysql_username_field, user, gq);
	
	if (mysql_real_query(myar->mysql, qstr, strlen(qstr))) {
		ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, r->server, "mya: group_check: unable to connect to database: %s.", mysql_error(myar->mysql));
		mysql_close(myar->mysql);
		myar->mysql = NULL;
	}
	
	if (!(res = mysql_store_result(myar->mysql))) {
		ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, r->server, "mya: group_check: %s/%s", mysql_error(myar->mysql), user);
		mysql_close(myar->mysql);
		myar->mysql = NULL;
		
		return -1;
	}
	
	if (mysql_num_rows(res) == 0) {
		mysql_free_result(res);
		
		return 0;
	}
	
	if (!(row = mysql_fetch_row(res)) || !row[0]) {
		ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, r->server, "mya: group_check: %s/%s", mysql_error(myar->mysql), user);
		mysql_free_result(res);
		mysql_close(myar->mysql);
		myar->mysql = NULL;
		
		return -1;
	}

	return_code = atoi(row[0]);
	mysql_free_result(res);
	
	return return_code;
}

static int mya_init_handler(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *s)
{
	ap_add_version_component(pconf, "mod_mya/3.0.1");
	
	return OK;
}

static int mya_check_user_id(request_rec *r)
{
	mya_config_rec *myar = (mya_config_rec*) ap_get_module_config(r->per_dir_config, &mya_module);
	const char *pass;
	int res;
	
	ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "mya: checking user id", r->user);
	
	if (myar->mysql_database == NULL) {
		ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "mya: database was (null) for user %s, return DECLINED", r->user);
		return DECLINED;
	}

	if ((res = ap_get_basic_auth_pw(r, &pass))) {
		ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "mya: ap_get_basic_auth_pw() failed for user %s", r->user);
		return res;
	}
	
	switch (user_check(r, r->user, pass, myar)) {
	  case 1:
		if (!(myar->fl & f_auth)) {
			return DECLINED;
		}
		
		ap_note_basic_auth_failure(r);
		return HTTP_UNAUTHORIZED;
	  case 0:
		return OK;
	}
	
	return HTTP_INTERNAL_SERVER_ERROR;
}

static int mya_auth_checker(request_rec *r)
{
	mya_config_rec *myar = (mya_config_rec*) ap_get_module_config(r->per_dir_config, &mya_module);
	char *user = r->user;
	int m = r->method_number;
	int method_restricted = 0;
	register int x;
	const char *t, *w;
	const apr_array_header_t *reqs_arr = ap_requires(r);
	require_line *reqs;
	
	ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "mya: authorization checking");

	if (!reqs_arr || !myar->mysql_database) {
		ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "mya: reqs_arr: %p myar->mysql_database: %p, return OK", reqs_arr, myar->mysql_database);
		return OK;
	}
	
	reqs = (require_line*) reqs_arr->elts;

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

		method_restricted = 1;

		t = reqs[x].requirement;
		w = ap_getword_white(r->pool, &t);
		
		if (!strcmp(w, "valid-user")) {
			return OK;
		}
		
		if (!strcmp(w, "user")) {
			while (t[0]) {
				w = ap_getword_conf(r->pool, &t);
			
				if (!strcmp(user, w)) {
					return OK;
				}
			}
		}
		else if (!strcmp(w, "group")) {
			char *q = NULL, *gf = myar->mysql_group_field;

			while (t[0]) {
				w = ap_getword_conf(r->pool, &t);

				if (!q) {
					q = apr_psprintf(r->pool, "%s = '%s'", gf, w);
				}
				else {
					q = apr_psprintf(r->pool, "%s OR %s = '%s'", q, gf, w);
				}
			}
			
			if (group_check(r, user, q, myar) > 0) {
				ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "mya: found group, return OK");
				return OK;
			}
		}
		else if (myar->fl & f_auth) {
		    /* if we aren't authoritative, any require directive could be
		     * valid even if we don't grok it.  However, if we are 
		     * authoritative, we can warn the user they did something wrong.
		     * That something could be a missing "AuthAuthoritative off", but
		     * more likely is a typo in the require directive.
		     */
			ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r, "access to %s failed, reason: unknown require directive: \"%s\"", r->uri, reqs[x].requirement);
		}
	}

    if (!method_restricted) {
		return OK;
	}

	if (!(myar->fl & f_auth)) {
		return DECLINED;
	}

	ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, "access to %s failed, reason: user %s not allowed access", r->uri, user);
	ap_note_basic_auth_failure(r);
	
	return HTTP_UNAUTHORIZED;
}
