--- /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 <ajarthu@uark.edu>
+ *
+ * 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 <httpd.h>
+#include <http_config.h>
+#include <http_core.h>
+#include <http_log.h>
+#include <http_protocol.h>
+#include <http_request.h> // for ap_hook_(check_user_id | auth_checker)
+#include <apr_strings.h> // for apr_pstrdup prototype
+#include <apr_reslist.h>
+#include <mysql.h>
+
+#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)?
+ "<NOW()":"",
+ (config->sessionTimeout > 0 && config->dbFieldExpiration)?
+ " OR ":"",
+ (config->dbFieldExpiration)?
+ (char *)config->dbFieldExpiration:"",
+ (config->dbFieldExpiration)?
+ "<NOW()":"",
+ NULL));
+
+ return uid;
+} // form_check_session
+
+//
+// Check if user is member of at least one of the necessary user(s)/group(s).
+// If so, return TRUE; otherwise, return FALSE.
+//
+static int
+form_check_required(request_rec *r, auth_form_dir_config *config, MYSQL *db_handle,
+ const unsigned char *uid) {
+ const apr_array_header_t *reqs_arr = ap_requires(r);
+ require_line *reqs = reqs_arr ? (require_line *)reqs_arr->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)<NOW() AND (%s)", config->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)<NOW()", config->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; i<val_len; i++) { // copy the value while escaping apostrophes
+ if(value[i] == '\\') { // skip escapes (including apostrophes)
+ parsed_str[pchr] = value[i];
+ parsed_str[pchr+1] = value[i+1];
+ pchr += 2;
+ i++;
+ }
+ else if(value[i] == '\'') { // escape the apostrophe
+ parsed_str[pchr] = '\\';
+ parsed_str[pchr+1] = '\'';
+ pchr += 2;
+ }
+ else { // copy the character
+ parsed_str[pchr] = value[i];
+ pchr++;
+ }
+ }
+ if(condition[chr] != ' ') {
+ SNPRINTF(&parsed_str[pchr], 2, "'"); pchr++;
+ }
+ else {
+ SNPRINTF(&parsed_str[pchr], 3, "' "); pchr += 2;
+ }
+
+ mode = 0; // switch to Normal mode
+ }
+ break;
+ }
+ }
+ parsed_str[pchr] = '\0';
+
+ return (unsigned char *)PSTRDUP(r->pool, 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