diff -r 1c27769b8435 -r 022ee48c7409 mod_auth_form-2.05/src/mod_auth_form.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_auth_form-2.05/src/mod_auth_form.c Fri May 22 15:42:33 2009 +0100 @@ -0,0 +1,1164 @@ +/* Copyright 1999-2005 The Apache Software Foundation or its licensors, as + * applicable. + * + * 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. + */ + +/* + * http_auth_form: authentication + * + * maintained by Aaron Arthurs + * + * Logs are located in 'NEWS' and detailed in 'ChangeLog'. + */ + +/* + * $Id: mod_auth_form.c,v 2.04 Sat Jun 24 14:48:34 CDT 2006 ueli Exp $ + */ +#define MAF_VERSION "mod_auth_form 2.04" +#define MAF_DESC "Form-based authentication using session management" + +#include +#include +#include +#include +#include +#include // for ap_hook_(check_user_id | auth_checker) +#include // for apr_pstrdup prototype +#include +#include + +#define PCALLOC apr_pcalloc +#define PSPRINTF apr_psprintf +#define PSTRCAT apr_pstrcat +#define PSTRDUP apr_pstrdup +#define SNPRINTF apr_snprintf +#define VSNPRINTF apr_vsnprintf + +#ifndef TRUE +#define TRUE 1 +#endif +#ifndef FALSE +#define FALSE 0 +#endif +#define MAX_VAR_LEN 20 // used by 'parse_condition_vars' + + +// +// Query result structure +// +typedef struct { + unsigned char ***records; // rows and columns of character arrays + unsigned int num_records; // how many rows are in this result + unsigned int num_fields; // how many columns are in each row +} q_result; + +// +// Structures to hold the mod_auth_form's configuration directives +// +// Per-Directory Configuration +// +typedef struct { + unsigned char *dbHost; // host name of db server + unsigned int dbPort; // port number of db server + unsigned char *dbSocket; // socket file of db server (takes precedence) +#ifdef MAF_MYSQL_SSL + int dbSsl; // enable SSL? + unsigned char *dbSslKey; // path to client key + unsigned char *dbSslCert; // path to client certificate + unsigned char *dbSslCa; // path to file listing trusted certificates + unsigned char *dbSslCaPath; // path to directory containing trusted PEM certificates + unsigned char *dbSslCipherList; // cipher list in the format of 'openssl ciphers' +#endif // #ifdef MAF_MYSQL_SSL + unsigned char *dbUsername; // username to connect to db server + unsigned char *dbPassword; // password to connect to db server + unsigned char *dbName; // DB name + unsigned char *dbTableSID; // session table + unsigned char *dbTableGID; // group table + unsigned char *dbTableTracking; // tracking table (optional) + unsigned char *dbFieldUID; // field in group, session, and tracking tables + // with username + unsigned char *dbFieldGID; // field in group table with group names + unsigned char *dbFieldTimeout; // field in session table with session + // timeout date + unsigned char *dbFieldExpiration; // field in session table with sessionr + // expiration date + unsigned char *dbFieldIPAddress; // field in tracking table with client's + // IP address + unsigned char *dbFieldDownloadDate; // field in tracking table with date + // of download + unsigned char *dbFieldDownloadPath; // field in tracking table with path + // of download + unsigned char *dbFieldDownloadSize; // field in tracking table with size + // (in bytes) of download + unsigned char *dbTableSIDCondition; // condition to add to the where-clause + // in the session table + unsigned char *dbTableGIDCondition; // condition to add to the where-clause + // in the group table + unsigned char *dbTableTrackingCondition; // condition to add to the where-clause + // in the tracking table + int sessionTimeout; // session inactivity timeout in minutes + int sessionAutoRefresh; // how often in seconds to refresh a + // current page + int sessionCookies; // read session keys from cookies instead + // of the URL query string? + int sessionDelete; // remove all expired sessions per request + int trackingLifetime; // life-span (in days) of each tracking record + unsigned char *pageLogin; // URL (absolute or relative) to the login page + unsigned char *pageExpired; // URL (absolute or relative) to the + //'session expired' page + unsigned char *pageNotAllowed; // URL (absolute or relative) to the + // 'user not allowed' page + unsigned char *pageAutoRefresh; // URL (absolute or relative) for the + // Refresh header + unsigned char *lastPageKey; // Query-string key containing the last + // unauthorized URL + int authoritative; // are we authoritative? +} auth_form_dir_config; + +// Module-specific functions +static void set_cgi_env_directory(request_rec *r, const unsigned char *uid); +static void *create_auth_form_dir_config(apr_pool_t *p, char *d); +static void register_hooks(apr_pool_t *p); +static int form_authenticator(request_rec *r); +static int form_session_checker(request_rec *r); +static unsigned char *form_check_session(request_rec *r, auth_form_dir_config *config, MYSQL *db_handle, + int *expired); +static int form_check_required(request_rec *r, auth_form_dir_config *config, MYSQL *db_handle, + const unsigned char *uid); +static unsigned char *get_gids(request_rec *r, auth_form_dir_config *config, MYSQL *db_handle, + const unsigned char *uid); +static void track_request(request_rec *r, auth_form_dir_config *config, MYSQL *db_handle, + const unsigned char *uid); + +// Database functions +static int open_db(request_rec *r, auth_form_dir_config *config, MYSQL **db_handle); +static void close_db(MYSQL **db_handle); +static q_result *send_query(request_rec *r, MYSQL *db_handle, unsigned char *query_format, ...); + +// Utility functions +static int redirect(request_rec *r, auth_form_dir_config *config, const unsigned char *page, + int log_level, const unsigned char *reason_format, ...); +static unsigned char *parse_condition_vars(request_rec *r, const unsigned char *condition, int cookies); +static unsigned char *get_value(request_rec *r, const unsigned char *key_values, + const unsigned char *key, unsigned char terminator, const unsigned char *padding); +static unsigned char *construct_full_uri(request_rec *r); +static unsigned char *url_encode(request_rec *r, const unsigned char *uri); +static unsigned char *url_decode(request_rec *r, const unsigned char *uri_enc); + +static command_rec auth_form_cmds[] = { + AP_INIT_TAKE1("AuthFormMySQLHost", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbHost), + OR_AUTHCFG, "mysql server host name"), + + AP_INIT_TAKE1("AuthFormMySQLPort", ap_set_int_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbPort), + OR_AUTHCFG, "mysql server port number"), + + AP_INIT_TAKE1("AuthFormMySQLSocket", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbSocket), + OR_AUTHCFG, "mysql server socket file"), + +#ifdef MAF_MYSQL_SSL + AP_INIT_FLAG("AuthFormMySQLSSL", ap_set_flag_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbSsl), + OR_AUTHCFG, "enable SSL for mysql connection"), + + AP_INIT_TAKE1("AuthFormMySQLSSLKey", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbSslKey), + OR_AUTHCFG, "mysql client certificate key file"), + + AP_INIT_TAKE1("AuthFormMySQLSSLCert", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbSslCert), + OR_AUTHCFG, "mysql client certificate file"), + + AP_INIT_TAKE1("AuthFormMySQLSSLCA", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbSslCa), + OR_AUTHCFG, "path to file listing trusted certificates"), + + AP_INIT_TAKE1("AuthFormMySQLSSLCAPath", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbSslCaPath), + OR_AUTHCFG, "path to directory containing PEM-formatted, trusted certificates"), + + AP_INIT_TAKE1("AuthFormMySQLSSLCipherList", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbSslCipherList), + OR_AUTHCFG, "list of SSL ciphers to allow (in 'openssl ciphers' format)"), +#endif // #ifdef MAF_MYSQL_SSL + + AP_INIT_TAKE1("AuthFormMySQLUsername", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbUsername), + OR_AUTHCFG, "mysql server user name"), + + AP_INIT_TAKE1("AuthFormMySQLPassword", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbPassword), + OR_AUTHCFG, "mysql server user password"), + + AP_INIT_TAKE1("AuthFormMySQLDB", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbName), + OR_AUTHCFG, "mysql database name"), + + AP_INIT_TAKE1("AuthFormMySQLTableSID", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbTableSID), + OR_AUTHCFG, "mysql session table"), + + AP_INIT_TAKE1("AuthFormMySQLTableSIDCondition", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbTableSIDCondition), + OR_AUTHCFG, "condition used in session validation"), + + AP_INIT_TAKE1("AuthFormMySQLTableGID", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbTableGID), + OR_AUTHCFG, "mysql group table"), + + AP_INIT_TAKE1("AuthFormMySQLTableGIDCondition", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbTableGIDCondition), + OR_AUTHCFG, "condition to add to where-clause in group table queries"), + + AP_INIT_TAKE1("AuthFormMySQLTableTracking", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbTableTracking), + OR_AUTHCFG, "mysql tracking table"), + + AP_INIT_TAKE1("AuthFormMySQLTableTrackingCondition", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbTableTrackingCondition), + OR_AUTHCFG, "condition to add to where-clause in tracking table queries"), + + AP_INIT_TAKE1("AuthFormMySQLFieldUID", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbFieldUID), + OR_AUTHCFG, "mysql username field within group, session, and " + "tracking tables"), + + AP_INIT_TAKE1("AuthFormMySQLFieldGID", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbFieldGID), + OR_AUTHCFG, "mysql group field within group table"), + + AP_INIT_TAKE1("AuthFormMySQLFieldTimeout", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbFieldTimeout), + OR_AUTHCFG, "mysql session timeout date field within session table"), + + AP_INIT_TAKE1("AuthFormMySQLFieldExpiration", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbFieldExpiration), + OR_AUTHCFG, "mysql session expiration date field within session table"), + + AP_INIT_TAKE1("AuthFormMySQLFieldIPAddress", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbFieldIPAddress), + OR_AUTHCFG, "mysql client IP address field within tracking table"), + + AP_INIT_TAKE1("AuthFormMySQLFieldDownloadDate", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbFieldDownloadDate), + OR_AUTHCFG, "mysql download date field within tracking table"), + + AP_INIT_TAKE1("AuthFormMySQLFieldDownloadPath", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbFieldDownloadPath), + OR_AUTHCFG, "mysql download path field within tracking table"), + + AP_INIT_TAKE1("AuthFormMySQLFieldDownloadSize", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, dbFieldDownloadSize), + OR_AUTHCFG, "mysql download size (in bytes) field within tracking table"), + + AP_INIT_TAKE1("AuthFormSessionTimeout", ap_set_int_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, sessionTimeout), + OR_AUTHCFG, "session inactivity timeout in minutes"), + + AP_INIT_TAKE1("AuthFormSessionAutoRefresh", ap_set_int_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, sessionAutoRefresh), + OR_AUTHCFG, "how often in seconds to refresh a current page"), + + AP_INIT_FLAG("AuthFormSessionCookies", ap_set_flag_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, sessionCookies), + OR_AUTHCFG, "If On, read from cookies for sessions, else read from " + "the URL query string"), + + AP_INIT_FLAG("AuthFormSessionDelete", ap_set_flag_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, sessionDelete), + OR_AUTHCFG, "If On, remove expired sessions."), + + AP_INIT_TAKE1("AuthFormTrackingLifetime", ap_set_int_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, trackingLifetime), + OR_AUTHCFG, "life-span (in days) of each tracking record in the " + "tracking table"), + + AP_INIT_TAKE1("AuthFormPageLogin", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, pageLogin), + OR_AUTHCFG, "(Absolute | Relative) URL location of the login page"), + + AP_INIT_TAKE1("AuthFormPageExpired", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, pageExpired), + OR_AUTHCFG, "(Absolute | Relative) URL location of the 'session " + "expired' page"), + + AP_INIT_TAKE1("AuthFormPageNotAllowed", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, pageNotAllowed), + OR_AUTHCFG, "(Absolute | Relative) URL location of the 'user not " + "allowed' page"), + + AP_INIT_TAKE1("AuthFormPageAutoRefresh", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, pageAutoRefresh), + OR_AUTHCFG, "(Absolute | Relative) URL location for the Refresh " + "HTTP header (if applicable)"), + + AP_INIT_TAKE1("AuthFormLastPageKey", ap_set_string_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, lastPageKey), + OR_AUTHCFG, "Query-string key containing the last unauthorized URL"), + + AP_INIT_FLAG("AuthFormAuthoritative", ap_set_flag_slot, + (void *) APR_OFFSETOF(auth_form_dir_config, authoritative), + OR_AUTHCFG, "Whether or not this module handles authorization"), + + { NULL } +}; // auth_form_cmds + +module AP_MODULE_DECLARE_DATA auth_form_module = +{ + STANDARD20_MODULE_STUFF, + create_auth_form_dir_config, // dir config creater + NULL, // dir merger --- default is to override + NULL, // server config creater + NULL, // merge server config + auth_form_cmds, // command apr_table_t + register_hooks // register hooks +}; + + +// FUNCTION IMPLEMENTATIONS // + +// Module-specific Functions // + +// +// Set mod_auth_form's environment variables for CGI scripts +// for the current request. +// +static void +set_cgi_env_directory(request_rec *r, const unsigned char *uid) { + apr_table_set(r->subprocess_env, "AP_MAF_VERSION", MAF_VERSION); + apr_table_set(r->subprocess_env, "AP_MAF_DESCRIPTION", MAF_DESC); + apr_table_set(r->subprocess_env, "AP_MAF_ENABLED", "true"); + apr_table_set(r->subprocess_env, "AP_MAF_UID", uid); +} + +// +// CALLBACK: +// Create the per-directory configuration and its defaults. +// +static void * +create_auth_form_dir_config(apr_pool_t *p, char *d) { + auth_form_dir_config *config = (auth_form_dir_config *) + PCALLOC(p, sizeof(auth_form_dir_config)); + if (!config) return NULL; // failure to get memory is a bad thing + + // + // default values + // + config->dbHost = NULL; // connect to localhost + config->dbPort = 3306; + config->dbSocket = NULL; // no socket +#ifdef MAF_MYSQL_SSL + config->dbSsl = 0; // no SSL + config->dbSslCipherList = "!ADH:RC4+RSA:HIGH:MEDIUM:LOW:EXP:+SSLv2:+EXP"; +#endif // #ifdef MAF_MYSQL_SSL + config->dbTableSID = "sessions"; + config->dbTableSIDCondition = "sid=$sid AND uid=$uid"; + config->dbFieldGID = "gid"; + config->dbFieldUID = "uid"; + config->dbFieldTimeout = "timeout_date"; + config->dbFieldIPAddress = "client_ip_address"; + config->dbFieldDownloadDate = "download_date"; + config->dbFieldDownloadPath = "download_path"; + config->dbFieldDownloadSize = "download_size"; + config->sessionTimeout = 0; // no session inactivity timeout + config->sessionAutoRefresh = -1; // refresh whenever session expires + config->sessionCookies = 0; // read from the URL query string + config->sessionDelete = 0; // leave expired sessions in table + config->trackingLifetime = 30; // keep tracking records as old as 30 days + config->authoritative = 1; // we should be the authoritative source + return (void *)config; +} // create_auth_form_dir_config + +// +// CALLBACK: +// Tell Apache which function does what. +// +static void +register_hooks(apr_pool_t *p) { + ap_hook_check_user_id(form_authenticator, NULL, NULL, + APR_HOOK_REALLY_FIRST); + ap_hook_auth_checker(form_session_checker, NULL, NULL, + APR_HOOK_REALLY_FIRST); +} // register_hooks + +// +// CALLBACK: +// This function does nothing more than return OK. The reason +// behind this is that Apache's authorization +// is based on 'Basic Authentication'. +// +// Usually, the 'check_user_id' hook will send the 'WWW-Authenticate' +// challenge, causing the login box to pop-up on the client's screen. +// However, we want a form-based login script (e.g. a PHP script) to do the +// verification for us; all this module does is verify, control, and log access +// to restricted areas (from the 'auth_checker' hook). +// +static int +form_authenticator(request_rec *r) { + auth_form_dir_config *config = (auth_form_dir_config *)ap_get_module_config(r->per_dir_config, + &auth_form_module); + // + // Of course, if we're not authoritative, we should + // pass authorization to other modules. + // + if(config->authoritative && config->pageLogin) + return OK; + else + return DECLINED; +} // form_authenticator + +// +// CALLBACK: +// Check the user's group membership and session. +// +static int +form_session_checker(request_rec *r) { + MYSQL *db_handle = NULL; + unsigned char *uid = NULL; + int expired = FALSE, status; + auth_form_dir_config *config = (auth_form_dir_config *)ap_get_module_config(r->per_dir_config, + &auth_form_module); + + // + // Check if we are authoritative before doing anything else. + // + if(!(config->authoritative && config->pageLogin)) + return DECLINED; + + // + // See if we can open a connection to the database if one is not + // already opened. + // If not, send a FORBIDDEN message to the client. + // + if(!open_db(r, config, &db_handle)) + return HTTP_FORBIDDEN; + + r->unparsed_uri = construct_full_uri(r); + + uid = form_check_session(r, config, db_handle, &expired); + if(!expired && uid != NULL) + status = form_check_required(r, config, db_handle, uid); + else if(expired) + status = redirect(r, config, config->pageExpired, APLOG_INFO, "session expired"); + else // uid == NULL + status = redirect(r, config, config->pageLogin, APLOG_INFO, "invalid session"); + + close_db(&db_handle); + + if(status == OK) + set_cgi_env_directory(r, uid); + + return status; +} // form_session_checker + +// +// Check the session keys against the database using a specified condition. +// +static unsigned char * +form_check_session(request_rec *r, auth_form_dir_config *config, MYSQL *db_handle, + int *expired) { + unsigned char *uid = NULL, *query, + *condition = parse_condition_vars(r, config->dbTableSIDCondition, + config->sessionCookies); + int autorefresh = config->sessionAutoRefresh, + autor_expire_enabled = (autorefresh == -1 && config->dbFieldExpiration), + s_timeout_enabled = (config->sessionTimeout > 0); + q_result *rows = NULL; + + *expired = FALSE; + query = PSTRCAT(r->pool, + "SELECT ", config->dbFieldUID, + (autor_expire_enabled)? // grab the difference + ",TIME_TO_SEC(TIMEDIFF(":"", + (autor_expire_enabled)? + (char *)config->dbFieldExpiration:"", + (autor_expire_enabled)? + ", NOW())) exp_diff":"", + " FROM ", config->dbTableSID, " WHERE (", condition, ")", + (s_timeout_enabled)? // using session inactivity timeout + " AND NOW()<":"", + (s_timeout_enabled)? + (char *)config->dbFieldTimeout:"", + (config->dbFieldExpiration)? // using session expiration + " AND NOW()<":"", + (config->dbFieldExpiration)? + (char *)config->dbFieldExpiration:"", + NULL); + + rows = send_query(r, db_handle, query); + + if(rows) { // session condition satisfied and un-expired + int autoref_sav = autorefresh, + use_page_expired = (!config->pageAutoRefresh && autoref_sav == -1), + add_last_page_key = ((config->pageAutoRefresh || autoref_sav == -1) && config->lastPageKey); + uid = rows->records[0][0]; + if(autorefresh == -1) { + autorefresh = 0; + if(config->dbFieldExpiration) + autorefresh = strtol(rows->records[0][1], NULL, 10); + if(config->sessionTimeout > 0 && + (autorefresh > 60*config->sessionTimeout || + !config->dbFieldExpiration)) + autorefresh = 60*config->sessionTimeout; + } + if(autorefresh > 0) { + apr_table_set(r->headers_out, "Refresh", + PSTRCAT(r->pool, + PSPRINTF(r->pool, "%d", autorefresh+1), + (config->pageAutoRefresh)? + ";url=":"", + (config->pageAutoRefresh)? + (char *)config->pageAutoRefresh:"", + (use_page_expired)? + ";url=":"", + (use_page_expired)? + (char *)config->pageExpired:"", + (add_last_page_key)? + "?":"", + (add_last_page_key)? + (char *)config->lastPageKey:"", + (add_last_page_key)? + "=":"", + (add_last_page_key)? + (char *)url_encode(r, r->unparsed_uri):"", + NULL)); + } + if(config->sessionTimeout > 0) + send_query(r, db_handle, "UPDATE %s SET %s=DATE_ADD(NOW(), INTERVAL %d MINUTE) " + "WHERE %s", config->dbTableSID, config->dbFieldTimeout, + config->sessionTimeout, condition); + + if(config->dbTableTracking) + track_request(r, config, db_handle, uid); + } + else if(config->sessionTimeout > 0 || config->dbFieldExpiration) { + // either the session has expired and/or the condition is not met + rows = send_query(r, db_handle, "SELECT %s FROM %s WHERE (%s)", config->dbFieldUID, + config->dbTableSID, condition); + if(rows) + *expired = TRUE; + } + + if(config->sessionDelete) // remove all expired sessions + send_query(r, db_handle, "DELETE FROM %s WHERE %s", + config->dbTableSID, + PSTRCAT(r->pool, + (s_timeout_enabled)? + (char *)config->dbFieldTimeout:"", + (s_timeout_enabled)? + "sessionTimeout > 0 && config->dbFieldExpiration)? + " OR ":"", + (config->dbFieldExpiration)? + (char *)config->dbFieldExpiration:"", + (config->dbFieldExpiration)? + "elts : NULL; + unsigned char *require = (reqs && reqs->requirement) ? reqs->requirement : NULL, + *last_word = NULL; + + // + // See if we need to check for user/group membership. + // (Check for both for an existing 'Require' line, and + // that the 'Require' line doesn't contain 'valid-user'). + // + if(!require || (strncmp(require, "valid-user", 10) == 0)) + return OK; + + // + // Do we require certain users or certain groups? + // + last_word = strrchr(require, ' '); + last_word++; + if(strncmp(require, "group", 5) == 0) { // it's groups + unsigned char *gids = get_gids(r, config, db_handle, uid); + if(gids) { // user has group membership(s) + unsigned char *cur_gid = strtok(gids, " \t,"); + require += 5; + while(cur_gid) { // match each GID of the user + unsigned char sp_gid_sp[MAX_STRING_LEN]; + SNPRINTF(sp_gid_sp, sizeof(sp_gid_sp)-1, " %s ", cur_gid); + if(strstr(require, sp_gid_sp) || + (strcmp(last_word, cur_gid) == 0)) // found a group match! + return OK; + cur_gid = strtok(NULL, " \t,"); + } + // + // No group matched + // + return redirect(r, config, config->pageNotAllowed, APLOG_INFO, + "user '%s' has a non-matching group membership", uid); + } + else // no group membership found + return redirect(r, config, config->pageNotAllowed, APLOG_INFO, + "user '%s' does not have group membership", uid); + } + else if(strncmp(require, "user", 4) == 0) { // it's users + // + // Generate the phrase " user " where 'user' is the + // name of the user. Use " user " as the matching substring. + // Also, match "user" with the last word in the list. + // (The same is applied above for group matching). + // + unsigned char sp_uid_sp[MAX_STRING_LEN]; + SNPRINTF(sp_uid_sp, sizeof(sp_uid_sp)-1, " %s ", uid); + require += 4; + if(strstr(require, sp_uid_sp) || + (strcmp(last_word, uid) == 0)) // found a user match! + return OK; + else + return redirect(r, config, config->pageNotAllowed, APLOG_INFO, + "user '%s' not listed under Require", uid); + } + + // + // If we're here, we have a bad Require line + // + return redirect(r, config, config->pageNotAllowed, APLOG_ERR, + "invalid Require directive"); +} // form_check_required + +// +// Get list of groups from database (space, tab, and/or comma delimited). +// If successful, the list of groups is returned; +// otherwise NULL is returned. +// +static unsigned char * +get_gids(request_rec *r, auth_form_dir_config *config, MYSQL *db_handle, + const unsigned char *uid) { + q_result *rows = NULL; + unsigned char gids[MAX_STRING_LEN]; + + if(!config->dbTableGID) + return NULL; + + // + // Get the user's GIDs + // + if(config->dbTableGIDCondition) + rows = send_query(r, db_handle, "SELECT %s FROM %s WHERE %s='%s' AND (%s)", + config->dbFieldGID, config->dbTableGID, + config->dbFieldUID, uid, + parse_condition_vars(r, config->dbTableGIDCondition, + config->sessionCookies)); + else + rows = send_query(r, db_handle, "SELECT %s FROM %s WHERE %s='%s'", + config->dbFieldGID, config->dbTableGID, + config->dbFieldUID, uid); + + if(rows) { // user has group membership(s) + unsigned int i, ulen; + SNPRINTF(gids, sizeof(gids)-1, "%s", rows->records[0][0]); + ulen = strlen(gids); + for(i = 1; i < rows->num_records; i++) { + SNPRINTF(&gids[ulen], sizeof(gids)-ulen-1, ",%s", rows->records[i][0]); + ulen += strlen(rows->records[i][0]) + 1; + } + } +#ifdef MAF_DEBUG + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "'%s' belongs to groups " + "'%s'", uid, gids); +#endif + + return (unsigned char *)PSTRDUP(r->pool, gids); +} // get_gids + +// +// Track the request into the database. +// +static void +track_request(request_rec *r, auth_form_dir_config *config, MYSQL *db_handle, + const unsigned char *uid) { + q_result *rows = NULL; + unsigned char *trackingCondition_parsed = NULL; + + if(config->dbTableTrackingCondition) + trackingCondition_parsed = + parse_condition_vars(r, config->dbTableTrackingCondition, + config->sessionCookies); + + // + // First, remove all the user's expired tracking records + // (Based on download date). + // + if(config->trackingLifetime > 0) { + if(trackingCondition_parsed) + send_query(r, db_handle, "DELETE FROM %s WHERE %s='%s' AND DATE_ADD(%s, " + "INTERVAL %d DAY)dbTableTracking, + config->dbFieldUID, uid, config->dbFieldDownloadDate, + config->trackingLifetime, trackingCondition_parsed); + else + send_query(r, db_handle, "DELETE FROM %s WHERE %s='%s' AND DATE_ADD(%s, " + "INTERVAL %d DAY)dbTableTracking, + config->dbFieldUID, uid, config->dbFieldDownloadDate, + config->trackingLifetime); + } + // + // Check for an existing tracking record and update it. + // (Both UID and download path must match). + // + if(trackingCondition_parsed) + rows = send_query(r, db_handle, "SELECT %s FROM %s WHERE %s='%s' AND %s='%s' AND (%s)", + config->dbFieldUID, config->dbTableTracking, + config->dbFieldUID, uid, config->dbFieldDownloadPath, + r->unparsed_uri, trackingCondition_parsed); + else + rows = send_query(r, db_handle, "SELECT %s FROM %s WHERE %s='%s' AND %s='%s'", + config->dbFieldUID, config->dbTableTracking, config->dbFieldUID, + uid, config->dbFieldDownloadPath, r->unparsed_uri); + if(rows) { // existing tracking record (update) + if(trackingCondition_parsed) + send_query(r, db_handle, "UPDATE %s SET %s='%s',%s=NOW(),%s='%ld' WHERE %s='%s' " + "AND %s='%s' AND (%s)", config->dbTableTracking, + config->dbFieldIPAddress, r->connection->remote_ip, + config->dbFieldDownloadDate, config->dbFieldDownloadSize, + (long int)r->finfo.size, config->dbFieldUID, uid, + config->dbFieldDownloadPath, r->unparsed_uri, + trackingCondition_parsed); + else + send_query(r, db_handle, "UPDATE %s SET %s='%s',%s=NOW(),%s='%ld' WHERE %s='%s' " + "AND %s='%s'", config->dbTableTracking, config->dbFieldIPAddress, + r->connection->remote_ip, config->dbFieldDownloadDate, + config->dbFieldDownloadSize, (long int)r->finfo.size, + config->dbFieldUID, uid, config->dbFieldDownloadPath, + r->unparsed_uri); + } + else // create a new tracking record + send_query(r, db_handle, "INSERT INTO %s (%s, %s, %s, %s, %s) VALUES ('%s', '%s', " + "NOW(), '%s', '%ld')", config->dbTableTracking, config->dbFieldUID, + config->dbFieldIPAddress, config->dbFieldDownloadDate, + config->dbFieldDownloadPath, config->dbFieldDownloadSize, uid, + r->connection->remote_ip, r->unparsed_uri, (long int)r->finfo.size); +} // track_request + + +// Database Functions // + +// +// Open connection to DB server and select database. Return TRUE if +// successful, FALSE if not able to connect or select database. If FALSE +// is returned, the reason for failure is logged to error_log file. +// Also, if a connection is made, but the database cannot be selected, +// the opened connection will be closed. +// +// Upon successful completion, 'db_handle' is set. +// +static int +open_db(request_rec *r, auth_form_dir_config *config, MYSQL **db_handle) { + if(*db_handle && mysql_ping(*db_handle) != 0) // already open + return TRUE; + + *db_handle = (MYSQL *)PCALLOC(r->pool, sizeof(MYSQL)); + mysql_init(*db_handle); +#ifdef MAF_MYSQL_SSL + if(config->dbSsl) { + mysql_ssl_set(*db_handle, config->dbSslKey, config->dbSslCert, + config->dbSslCa, config->dbSslCaPath, config->dbSslCipherList); + } +#endif // #ifdef MAF_MYSQL_SSL + if(config->dbSocket != NULL) { + if(!mysql_real_connect(*db_handle, NULL, config->dbUsername, + config->dbPassword, NULL, 0, config->dbSocket, 0)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "MySQL ERROR: %s: %s", mysql_error(*db_handle), + r->unparsed_uri); + return FALSE; + } + } + else { + if(!mysql_real_connect(*db_handle, config->dbHost, config->dbUsername, + config->dbPassword, NULL, config->dbPort, NULL, 0)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "MySQL ERROR: %s: %s", mysql_error(*db_handle), + r->unparsed_uri); + return FALSE; + } + } + + // + // Try to select the database. If not successful, + // close the 'mysql_handle'. + // + if(mysql_select_db(*db_handle, config->dbName) != 0) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "MySQL ERROR %s: %s", mysql_error(*db_handle), + r->unparsed_uri); + close_db(db_handle); + return FALSE; + } + return TRUE; + } + +// +// Close the database handle if it's not already closed. +// +static void +close_db(MYSQL **db_handle) { + if(*db_handle) mysql_close(*db_handle); + *db_handle = NULL; +} // close_db + +// +// Send a database query and, if present, +// return the resulting rows (NULL otherwise). +// +static q_result * +send_query(request_rec *r, MYSQL *db_handle, unsigned char *query_format, ...) { + q_result *rows = NULL; + MYSQL_RES *result; + unsigned char query[MAX_STRING_LEN]; + va_list arg_list; + + va_start(arg_list, query_format); + VSNPRINTF(query, sizeof(query)-1, query_format, arg_list); + va_end(arg_list); + +#ifdef MAF_DEBUG + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Sending query " + "'%s'", query); +#endif + if(mysql_real_query(db_handle, query, strlen(query)) != 0) { + ap_log_rerror(APLOG_MARK, APLOG_CRIT, 0, r, + "MySQL ERROR: %s: %s: %s", query, mysql_error(db_handle), + r->unparsed_uri); + return NULL; + } + + if((result = mysql_store_result(db_handle)) != NULL) { + unsigned int num_rows = (unsigned int)mysql_num_rows(result), + num_fields = mysql_num_fields(result); + register unsigned int i; + + if(num_rows >= 1) { + rows = (q_result *)PCALLOC(r->pool, sizeof(q_result)); + rows->num_records = num_rows; + rows->num_fields = num_fields; + rows->records = (unsigned char ***)PCALLOC(r->pool, sizeof(unsigned int **) * rows->num_records); + for(i = 0; i < rows->num_records; i++) { + register unsigned int j; + unsigned char **cur_row = (unsigned char **)mysql_fetch_row(result); + rows->records[i] = (unsigned char **)PCALLOC(r->pool, sizeof(unsigned int *) * rows->num_fields); + for(j = 0; j < rows->num_fields; j++) + rows->records[i][j] = (unsigned char *)PSTRDUP(r->pool, cur_row[j]); + } + } + mysql_free_result(result); + } + + return rows; +} // send_query + + +// Utility Functions // + +// +// Redirect to a specified page +// +static int +redirect(request_rec *r, auth_form_dir_config *config, const unsigned char *page, int log_level, + const unsigned char *reason_format, ...) { + va_list arg_list; + unsigned char reason[MAX_STRING_LEN]; + va_start(arg_list, reason_format); + VSNPRINTF(reason,sizeof(reason)-1,reason_format,arg_list); + va_end(arg_list); + if(page) { + unsigned char *new_url, *fragment; + fragment = strchr(page, '#'); + if(config->lastPageKey) { + unsigned char *qora; + if(strchr(page, '?')) + qora = "&"; + else + qora = "?"; + new_url = PSTRCAT(r->pool, page, + qora, config->lastPageKey, "=", + url_encode(r, r->unparsed_uri), + (fragment)? + (char *)fragment:"", + NULL); + } + else + new_url = PSTRDUP(r->pool, page); + if(!fragment) + new_url = PSTRCAT(r->pool, new_url, "##", NULL); +#ifdef MAF_DEBUG + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Redirect from " + "%s to %s", r->unparsed_uri, new_url); +#endif + apr_table_set(r->headers_out, "Location", new_url); + ap_log_rerror(APLOG_MARK, log_level, 0, r, + "AuthForm: %s - %s: redirect to %s", + r->the_request, reason, page); + return HTTP_MOVED_TEMPORARILY; + } + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "AuthForm: %s - %s: redirecting from %s: Target" + "page not specified", + r->the_request, reason, page); + return HTTP_FORBIDDEN; +} // redirect + +// +// Parse out the variables (formatted as $var) in the condition string +// (i.e. replace the variables with the values sent by the client). +// +// Returns the parsed boolean expression. +// If unsuccessful, NULL is returned. +// +static unsigned char * +parse_condition_vars(request_rec *r, const unsigned char *condition, int cookies) { + const unsigned char *key_values = NULL; + unsigned char parsed_str[MAX_STRING_LEN], var[MAX_VAR_LEN], terminator, ender, + padding[2]; + int len = strlen(condition), chr, pchr = 0, vchr = 0, + mode = 0, escaping = FALSE; + + if(cookies) { + key_values = apr_table_get(r->headers_in, "Cookie"); + terminator = ';'; + padding[0] = ' '; + } + else { + key_values = r->args; + terminator = '&'; + padding[0] = '\0'; + } + padding[1] = '\0'; + + for(chr = 0; chr < len; chr++) { + switch(mode) { + case 0: // Normal mode + if(condition[chr] != '$') { + parsed_str[pchr] = condition[chr]; pchr++; + if(condition[chr] == '\'' || condition[chr] == '\"') { + mode = 1; // switch to String mode + ender = condition[chr]; + } + } + else { + vchr = 0; + mode = 2; // switch to Variable mode + } + break; + + case 1: // String mode + parsed_str[pchr] = condition[chr]; pchr++; + if(condition[chr] == ender && !escaping) + mode = 0; // switch to Normal mode + if(condition[chr] == '\\' && !escaping) + escaping = TRUE; + else + escaping = FALSE; + break; + + case 2: // Variable mode + if(condition[chr] != ' ' && chr != (len - 1)) { + var[vchr] = condition[chr]; vchr++; + } + else { + unsigned char *value = NULL; + unsigned int val_len = 0, i; + if(condition[chr] != ' ') { + var[vchr] = condition[chr]; vchr++; + } + var[vchr] = '\0'; + + if(key_values != NULL) + value = get_value(r, key_values, var, terminator, padding); + if(value == NULL) + value = ""; + else + val_len = strlen(value); + + // + // Replace the variable with its single-quoted value + // + SNPRINTF(&parsed_str[pchr], 2, "'"); pchr++; + for(i=0; ipool, parsed_str); +} // parse_condition_vars + +// +// Extract a value from a key given +// a string, a terminating character, and the name +// of the key +// +// If successful, return the key's value; +// otherwise, NULL is returned. +// +static unsigned char * +get_value(request_rec *r, const unsigned char *key_values, const unsigned char *key, + unsigned char terminator, const unsigned char *padding) { + if(key_values && key) { + unsigned int pad_len = strlen(padding); + unsigned char key_equal[MAX_STRING_LEN]; + unsigned const char *key_begin = NULL, *value_begin = NULL; + SNPRINTF(key_equal, sizeof(key_equal)-1, "%c%s%s=", terminator, padding, key); + + key_begin = strstr(key_values, key_equal); + + if(key_begin == NULL && + strncmp(&key_equal[pad_len+1], &key_values[0], + strlen(&key_equal[pad_len+1])) == 0) { + key_begin = key_values; + value_begin = key_begin + strlen(key_equal) - pad_len - 1; + } + else + value_begin = key_begin + strlen(key_equal); + if(key_begin != NULL) { + unsigned char *value = (unsigned char *)PSTRDUP(r->pool, value_begin), + *value_end = strchr(value, terminator); + if(value_end) + value_end[0] = '\0'; + return url_decode(r, value); + } + } + ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, + "AuthForm: %s - Could not find value for client key" + " '%s'", r->the_request, key); + + return NULL; +} // get_value + +// +// Build a full URI for the current request. +// +static unsigned char * +construct_full_uri(request_rec *r) { + unsigned int port = ap_get_server_port(r); + const unsigned char *xforwarded = apr_table_get(r->headers_in, "X-Forwarded-Server"); + + return PSTRCAT(r->pool, +#ifdef ap_http_scheme + ap_http_scheme(r), +#else + ap_http_method(r), +#endif + "://", (xforwarded == NULL)?ap_get_server_name(r):(char *)xforwarded, + (!ap_is_default_port(port, r))? + PSPRINTF(r->pool, ":%u", port):"", + r->parsed_uri.path, + (r->parsed_uri.query)? + "?":"", + (r->parsed_uri.query)? + r->parsed_uri.query:"", + (r->parsed_uri.fragment)? + "#":"", + (r->parsed_uri.fragment)? + r->parsed_uri.fragment:"", + NULL); +} // construct_full_uri + +// +// URL-Encoder +// +static unsigned char * +url_encode(request_rec *r, const unsigned char *uri) { + unsigned char uri_enc[MAX_STRING_LEN]; + unsigned int cchar = 0, cchare = 0; + while(uri[cchar] != '\0' && cchar < MAX_STRING_LEN) { + if(uri[cchar] <= 32 || + (uri[cchar] >= 34 && uri[cchar] <= 38) || + uri[cchar] == 43 || + uri[cchar] == 44 || + uri[cchar] == 47 || + (uri[cchar] >= 58 && uri[cchar] <= 64) || + (uri[cchar] >= 91 && uri[cchar] <= 94) || + uri[cchar] == 96 || + (uri[cchar] >= 123 && uri[cchar] <= 126) || + (uri[cchar] >= 128 && uri[cchar] <= 225)) { + SNPRINTF(&uri_enc[cchare], sizeof(uri_enc)-cchare-1, "%%%02.2X", + uri[cchar]); + cchare += 3; + } + else { + uri_enc[cchare] = uri[cchar]; + uri_enc[cchare+1] = '\0'; + cchare++; + } + cchar++; + } + return (unsigned char *)PSTRDUP(r->pool, uri_enc); +} // url_encode + +// +// URL-Decoder +// +static unsigned char * +url_decode(request_rec *r, const unsigned char *uri_enc) { + unsigned char uri[MAX_STRING_LEN]; + unsigned int cchar = 0, cchard = 0; + while(uri_enc[cchar] != '\0' && cchar < MAX_STRING_LEN) { + if(uri_enc[cchar] == '%') { + unsigned char hex_str[3] = {uri_enc[cchar+1], uri_enc[cchar+2], '\0'}; + uri[cchard] = (unsigned char)strtol(hex_str, NULL, 16); + uri[cchard+1] = '\0'; + cchard++; + cchar += 3; + } + else { + uri[cchard] = uri_enc[cchar]; + uri[cchard+1] = '\0'; + cchard++; + cchar++; + } + } + return (unsigned char *)PSTRDUP(r->pool, uri); +} // url_decode