mod_auth_form-2.05/src/mod_auth_form.c
changeset 11 022ee48c7409
--- /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