/*  Copyright 2004 Thimo Eichstaedt
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 *  Based on code from
 *      Dirk.vanGulik@jrc.it  - original module
 *      mjd@pobox.com         - bug fixes, conversion from mSQL to MySQL
 *
 *
 *  Version history:
 *  Version 0.2 - 28.07.2004  - first release - Thimo Eichstaedt <apache-mod@digithi.de>
 *  Version 0.3 - 03.08.2004  - code fixup - Thimo Eichstaedt <apache-mod@digithi.de>
 *
 */
#include "apr_strings.h"
#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_request.h"
#include "http_log.h"

#include <time.h>               /* for time */
#include <string.h>             /* for strncmp */
#include <mysql.h>              /* for MySQL */

#define MAX_USERNAME_LEN 128
#define MAX_COOKIEVAL_LEN 128

#define WHERE_ALLOWED OR_AUTHCFG
#define ERRTAG "Mysql_Cookie_Auth2 "

module AP_MODULE_DECLARE_DATA auth_cookie_mysql2_module;

/* Public functions */
static int authenticate_user (request_rec *);
// static int check_user_access (request_rec *);
static void *create_auth_dir_config(apr_pool_t *, char *);

typedef struct s_mysql_cookie_auth2_config_struct {
    int  activated;
    char *cookiename;
    char *dbhost;
    char *dbuser;
    char *dbpassword;
    char *dbname;
    char *dbtable;
    char *dbusername_field;
    char *dbsessname_field;
    char *dbsessval_field;
    char *dbexpiry_field;
    char *dbremoteip_field;
    char *failureurl;
} mysql_cookie_auth2_config_rec;

/* Private functions */
int check_against_mysql(mysql_cookie_auth2_config_rec *, request_rec *, char *, char *, char *);
int check_valid_cookie(request_rec *, mysql_cookie_auth2_config_rec *);
static int do_redirect(request_rec *);


static mysql_cookie_auth2_config_rec default_config_rec = {
    0,		/* are we activated ? */
    NULL,	/* Cookie Name */
    NULL,	/* Database host */
    NULL,	/* Database user */
    NULL,	/* Database password */
    NULL,	/* Database name */
    NULL,	/* Database table */
    NULL,	/* Database username field */
    NULL,	/* Database sessname field */
    NULL,	/* Database sessval field */
    NULL,	/* Database expiry information field */
    NULL,	/* Database remote ip information field */
    NULL,	/* goto URL if failure */
};    

static void *create_auth_dir_config(apr_pool_t *p, char *d) {
    mysql_cookie_auth2_config_rec *conf = apr_pcalloc(p, sizeof(*conf));
    
    if (conf) {
	// Set default values
	*conf=default_config_rec;
    }
    return conf;
}

static int
do_redirect(request_rec *r) {
    mysql_cookie_auth2_config_rec *conf = ap_get_module_config(r->per_dir_config, &auth_cookie_mysql2_module);
    char *redirect = apr_psprintf(r->pool,
	                          "%s?r=%s",
			          conf->failureurl,
				  r->uri);
											    
    if (redirect) {
    
/*	apr_table_setn(r->headers_out, "Location", redirect);
	return HTTP_MOVED_TEMPORARILY; */
	
	r->method = "GET";
	r->method_number = M_GET;
	ap_internal_redirect(conf->failureurl,r);
	return HTTP_OK;
    } else {
	return HTTP_INTERNAL_SERVER_ERROR;
    }
}


int check_against_mysql(mysql_cookie_auth2_config_rec *conf, request_rec *r, char *cookiename, char *cookieval, char *username) {
    MYSQL dbh;
    MYSQL_RES *res;
    MYSQL_ROW row;
    my_ulonglong n_rows;
    unsigned ulen=0;
    apr_pool_t *p = r->pool;
    time_t tc;
    char *esc_cookiename;
    char *query, *queryopt;
    
    mysql_init(&dbh);
    
    if (! mysql_real_connect(&dbh,
			     conf->dbhost,
			     conf->dbuser, conf->dbpassword,
			     conf->dbname,
			     0, NULL, 0)) {
			     
	ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, r,
		      ERRTAG "couldn't connect to database: %s",
		      mysql_error(&dbh));
		      
	return -1;
    }
    
    // escape cookiename
    ulen=strlen(cookiename);
    if (! (esc_cookiename = apr_palloc(p, ulen*2 + 1)))
	return -1;
	
    mysql_real_escape_string(&dbh, esc_cookiename, cookiename, ulen);
    
    
    queryopt = apr_psprintf(p, "");
    
    /* Check for cookie Expiry ? */
    if (conf->dbexpiry_field) {
	tc=time(NULL);
	queryopt = apr_psprintf(p, "%s AND %s > %lu", queryopt, conf->dbexpiry_field,tc);
    }
    
    /* Check for right remote ip ? */
    if (conf->dbremoteip_field) {
	queryopt = apr_psprintf(p, "%s AND %s='%s'", queryopt, conf->dbremoteip_field, r->connection->remote_ip);
    }

    
    /* Generate query */
    query = apr_psprintf(p, "SELECT %s,%s FROM %s WHERE %s='%s'%s",
			 conf->dbusername_field,
			 conf->dbsessval_field,
			 conf->dbtable,
			 conf->dbsessname_field,
			 esc_cookiename,
			 queryopt);

    if (mysql_query(&dbh, query) != 0) {
    
	ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, r,
		      ERRTAG "error in MySQL query \"%s\": %s",
		      query,
		      mysql_error(&dbh));
    
	mysql_close(&dbh);
	return -1;
    }
    
    if (! (res = mysql_store_result(&dbh))) {
	ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, r,
		      ERRTAG "couldn't store query result: %s",
		      mysql_error(&dbh));
	mysql_close(&dbh);
	return -1;
    }
    
    if (mysql_num_rows(res) != 1) {
	mysql_free_result(res);
	mysql_close(&dbh);
	return 0;
    }
	
    if (! (row = mysql_fetch_row(res))) {
	ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, r,
		      ERRTAG "couldn't fetch row: %s",
		      mysql_error(&dbh));
    
	mysql_free_result(res);
	mysql_close(&dbh);
	return -1;
    }
    
    if (strlen (row[0] + 1) > MAX_USERNAME_LEN) {
	ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, r,
		      ERRTAG "fetched username from DB, but is longer than max length %d",
		      MAX_USERNAME_LEN);
	mysql_free_result(res);
	mysql_close(&dbh);
	return -1;
    }

    if (strlen (row[1] + 1) > MAX_COOKIEVAL_LEN) {
	ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, r,
		      ERRTAG "fetched value from DB, but is longer than max length %d",
		      MAX_COOKIEVAL_LEN);
	mysql_free_result(res);
	mysql_close(&dbh);
	return -1;
    }
    
    if (strcmp(row[1],cookieval) != 0) {
    
	// Cookie Value changed.. ?
	return 0;
    }
    strcpy(username, row[0]);

    mysql_free_result(res);
    mysql_close(&dbh);
    return 1;
}

int check_valid_cookie(request_rec *r, mysql_cookie_auth2_config_rec *conf) {
    const char *cookieptr;
    char *cookies, *value;
    char username[MAX_USERNAME_LEN];
    int ret;

    // grab pointer to cookie informations
    if (! (cookieptr = apr_table_get(r->headers_in, "Cookie"))) {
	// no cookies at all found
	if (conf->failureurl) {
	    return do_redirect(r);
	} else {
	    return HTTP_UNAUTHORIZED;
	}
    }

    // init username array
    username[0] = '\0';
    
    // make a copy of cookies, so we can make what we want
    if (! (cookies = apr_palloc(r->pool, strlen(cookieptr) + 2))) {
	// error
	return HTTP_INTERNAL_SERVER_ERROR;
    }

    strcpy (cookies, cookieptr);
    cookies[0 + strlen(cookieptr)] = ';';
    cookies[1 + strlen(cookieptr)] = '\0';

    if (conf->cookiename) {
	// a cookiename is given, we are searching only for THIS cookie in the DB

	// find cookie we are searching for
	if (cookies = strstr(cookies, conf->cookiename)) {
	    // found cookie
	    if (value = strchr(cookies, '=')) {
		// found beginning of cookieval
		value++;		
		if (value = strtok(value, " ;\n\r\t\f")) {
		    ret=check_against_mysql(conf, r, conf->cookiename, value, username);
		}
	    }
	}
    } else {
    
	// no cookiename is given, lets try all cookies the client gave us
	for (cookies = strtok(cookies, " ;\n\r\t\f"); (cookies); cookies = strtok(NULL, " ;\n\r\t\f")) {
	    /* cookies now should be like "aaa=bbb", check this and (if not) search for valid cookie */
	    while (cookies && ! (value = strchr (cookies, (int) '='))) {
		// cookie not valid, try the next
		cookies = strtok(NULL, " ;\n\r\t\f");
	    }
	    if (! (cookies))
		break;
		
	    *value = '\0';
	    value++;

	    if (ret=check_against_mysql(conf, r, cookies, value, username) == 1)
		break;
	}
    }

    // handle return codes
    switch (ret) {
        case 1:
    	    // valid cookie found
	    r->user = strdup(username);
	    r->ap_auth_type="Cookie";
	    return OK;
	    break;
	case 0:
	    // no valid information in database found
	    if (conf->failureurl) {
	        return do_redirect(r);
	    } else {
	        return HTTP_UNAUTHORIZED;
	    }
	    break;
	case -1:
	    // error
	    return DECLINED;
	    break;
    }
}
	    
    
static int check_user_access (request_rec *r) {

    mysql_cookie_auth2_config_rec *conf = ap_get_module_config(r->per_dir_config,
					         &auth_cookie_mysql2_module);
						 
    /* No "requires" directive */
/*    if (! reqs_arr) {
	return OK;
    } else return DECLINED; */
}

static int authenticate_user (request_rec *r) {
    mysql_cookie_auth2_config_rec *conf = ap_get_module_config(r->per_dir_config, &auth_cookie_mysql2_module);
    
    if (! conf->activated) {
	// not active
	return DECLINED;
    }

    if ( !conf->dbhost || !conf->dbuser || !conf->dbpassword || !conf->dbname || !conf->dbtable ) {
	ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, r,
		      ERRTAG "please check database connect information, some are missing");
	return DECLINED;
    }
    
    if ( !conf->dbusername_field || !conf->dbsessname_field || !conf->dbsessval_field) {
	ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, r,
		      ERRTAG "please check database field names, some are missing");
	return DECLINED;
    }
    
    ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, r, ERRTAG "module go");
    
    return check_valid_cookie(r,conf);
    
}    


/* Module data */
static const command_rec auth_commands[] = {

  AP_INIT_FLAG("CookieAuth", ap_set_flag_slot,
    (void *)APR_OFFSETOF(mysql_cookie_auth2_config_rec, activated),
    WHERE_ALLOWED, "Set to on to activate Cookie Auth"),

  AP_INIT_TAKE1("CookieAuth_CookieName", ap_set_string_slot,
    (void *)APR_OFFSETOF(mysql_cookie_auth2_config_rec, cookiename),
    WHERE_ALLOWED, "Name of the cookie"),

  AP_INIT_TAKE1("CookieAuth_DBHost", ap_set_string_slot,
    (void *)APR_OFFSETOF(mysql_cookie_auth2_config_rec, dbhost),
    WHERE_ALLOWED, "Host on which DB server resides"),

  AP_INIT_TAKE1("CookieAuth_DBUser", ap_set_string_slot,
    (void *)APR_OFFSETOF(mysql_cookie_auth2_config_rec, dbuser),
    WHERE_ALLOWED, "Username for DB access"),

  AP_INIT_TAKE1("CookieAuth_DBPassword", ap_set_string_slot,
    (void *)APR_OFFSETOF(mysql_cookie_auth2_config_rec, dbpassword),
    WHERE_ALLOWED, "Password for DB access"),
    
  AP_INIT_TAKE1("CookieAuth_DBName", ap_set_string_slot,
    (void *)APR_OFFSETOF(mysql_cookie_auth2_config_rec, dbname),
    WHERE_ALLOWED, "Database name"),

  AP_INIT_TAKE1("CookieAuth_DBTable", ap_set_string_slot,
    (void *)APR_OFFSETOF(mysql_cookie_auth2_config_rec, dbtable),
    WHERE_ALLOWED, "Table name in database"),

  AP_INIT_TAKE1("CookieAuth_UsernameField", ap_set_string_slot,
    (void *)APR_OFFSETOF(mysql_cookie_auth2_config_rec, dbusername_field),
    WHERE_ALLOWED, "Field name in database where username is hold"),

  AP_INIT_TAKE1("CookieAuth_SessnameField", ap_set_string_slot,
    (void *)APR_OFFSETOF(mysql_cookie_auth2_config_rec, dbsessname_field),
    WHERE_ALLOWED, "Field name in database where session name is hold"),

  AP_INIT_TAKE1("CookieAuth_SessvalField", ap_set_string_slot,
    (void *)APR_OFFSETOF(mysql_cookie_auth2_config_rec, dbsessval_field),
    WHERE_ALLOWED, "Field name in database where session key is hold"),

  AP_INIT_TAKE1("CookieAuth_ExpiryField", ap_set_string_slot,
    (void *)APR_OFFSETOF(mysql_cookie_auth2_config_rec, dbexpiry_field),
    WHERE_ALLOWED, "Field name in database where expiry information is hold"),

  AP_INIT_TAKE1("CookieAuth_RemoteIPField", ap_set_string_slot,
    (void *)APR_OFFSETOF(mysql_cookie_auth2_config_rec, dbremoteip_field),
    WHERE_ALLOWED, "Field name in database where remote ip information is hold"),

  AP_INIT_TAKE1("CookieAuth_FailureURL", ap_set_string_slot,
    (void *)APR_OFFSETOF(mysql_cookie_auth2_config_rec, failureurl),
    WHERE_ALLOWED, "URL where to go to when auth not successful")
};    

static void register_hooks(apr_pool_t *p) {

    /* Hook in and great, we are the first :) */
    ap_hook_check_user_id(authenticate_user,NULL,NULL,APR_HOOK_FIRST);
    // ap_hook_auth_checker(check_user_access,NULL,NULL,APR_HOOK_FIRST);
}
	

module AP_MODULE_DECLARE_DATA auth_cookie_mysql2_module = {
    STANDARD20_MODULE_STUFF,
    create_auth_dir_config,     /* per-directory config creater */
    NULL,                       /* dir merger --- default is to override */
    NULL,                       /* server config creator */
    NULL,                       /* server config merger */
    auth_commands,              /* command table */
    register_hooks              /* set up other request processing hooks */
};

