/* * mod_alterquery.c --> alters the QUERY_STRING part of a GET request * LICENSE The mod_alterquery package falls under the Open-Source Software label because it's distributed under a BSD-style license. The detailed license information follows. ==================================================================== Copyright (c) 2002 Manni Lee Wood. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. All advertising materials mentioning features or use of this software must display the following acknowledgment: "This product includes software developed by Manni Lee Wood , (http://manniwood.net)." 4. The name "mod_alterquery" must not be used to endorse or promote products derived from this software without prior written permission. For written permission, please contact manniwood@planet-save.com. 5. Products derived from this software may not be called "mod_alterquery" nor may "mod_alterquery" appear in their names without prior written permission of Manni Lee Wood. 6. Redistributions of any form whatsoever must retain the following acknowledgment: "This product includes software developed by Manni Lee Wood , (http://manniwood.net)." THIS SOFTWARE IS PROVIDED BY MANNI LEE WOOD ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RALF S. ENGELSCHALL OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ==================================================================== * * Installation: * ASSUMPTIONS * - you have downloaded the Apache source code, compiled it with * the ability to dynamically load modules, and have installed it * somewhere on your system * 1. copy this file to /src/modules/extra * 2. cd to /src/modules/extra * 3. run /bin/apxs -c mod_alterquery.c * 4. run /bin/apxs -i -a -n alterquery mod_alterquery.so * 5. Ensure that mod_alterquery.so is at the bottom of the LoadModule and * AddModule sections of httpd.conf. (Should be as a result of running * apxs with the -i and -a switches.) * 6. Add mod_alterquery directives to httpd.conf according to the Usage * section following. * * Usage: * * ORDER OF DIRECTIVES IN httpd.conf MATTERS! * * First, you need either of these two directives: * * AlterQueryForURI * * AlterQueryForGlobURI * * must be a URI starting from the server (or virutal server) * root. must be an exact match, whereas * can be a simple glob expression using wildcards * and ?, and * string sets like file.{h,c,java} and character ranges like * pic[0-9][a-zA-Z].gif. * * Follow the above directive with one or more of the following: * * 1. Remove a key and its value whenever is found in the QUERY_STRING. * To remove more than one key, httpd.conf will need to contain more than * one AlterQueryRemove statement. * * AlterQueryRemove * * 2. Remove a key and its value whenever the is found in the QUERY_STRING * and its value equals . Use more than one AlterQueryRemove to remove * more than one key/value pair from the QUERY_STRING. * * AlterQueryRemove * * 3. Change a key's value to if is found. Can be put * in httpd.conf more than once. * * AlterQueryChange * * 4. Change the value of a key to if is found and its value * is . Can be put in httpd.conf more than once. * * AlterQueryChange * * 5. Add a with value to the QUERY_STRING. Can be put * in httpd.conf more than once. * * AlterQueryAdd * * NOTE that in all examples above, you can count on keys and values * being matched against their unescaped values, so if you are looking to * match "some value", don't enter the url-encoded "some+value", but use * "some value". */ #include "httpd.h" #include "http_config.h" #include "http_core.h" #include "http_log.h" #include "fnmatch.h" #define MOD_ALTERQUERY_VERSION "1.0" module MODULE_VAR_EXPORT alterquery_module; /* --------------- Configuration Structs --------------------------- */ typedef enum action_enum { REMOVE, CHANGE, ADD } action; /* A record representing a change to a user's form data in the GET * request. The key is the key to be matched. The val is action-dependent; * it can either also be a match paramater, or a replacement parameter. * val2 gets used as a replacement parameter in cases where we needed to * use key and val as match parameters. act describes what action will * be taken on the user's original form data: currently, REMOVE, CHANGE, * or ADD */ typedef struct change_rec_struct { char *key; char *val; char *val2; action act; struct change_rec_struct *next; } change_rec; /* list of change recs and add recs associated with a particular URI */ typedef struct uri_rec_struct { char *uri; /* URI to match before using this struct's change_recs */ char *glob_uri; /* URI with file glob expression to match */ change_rec *ch_rec; /* singly linked, NULL-terminated list of change recs */ change_rec *add_rec; /* singly linked, NULL-terminated list of adds */ struct uri_rec_struct *next; } uri_rec; /* per-server configuration record; standard part of an Apache module */ typedef struct { ap_pool *pool; /* pointer to the pool this gets allocated in */ uri_rec *u_rec; /* singly linked, NULL-terminated list of uri recs */ } alterquery_per_server_config; /* --------------- Utility Functions and Data ---------------------- */ typedef struct rebuild_args_data rebuild_args_data; struct rebuild_args_data { request_rec *r; /* the all-important, ever ubiquitous request record */ uri_rec *u_rec; /* not a list; just one */ char *args; /* just like request_rec.args but with certain keys removed */ }; /* Sadly, ap_unescape_url, presumably because it has to be used on an * entire url and not the parts after the ?, does not escape + signs to * spaces, as is needed for the parts after the ?. This copied * version of x2c (because the apache API does not expose it but * it is a utility function of ap_unescape_url) and re-written * version of ap_unescape_url take care of that. * Now *this* is why open source software rocks so much; * I can see how the implementation does something by looking at the * source, and then I can make a needed * change for functionality unanticipated in the original by * copying parts of the original. Go, Apache! */ static char mod_alterquery_x2c(const char *what) { register char digit; #ifndef CHARSET_EBCDIC digit = ((what[0] >= 'A') ? ((what[0] & 0xdf) - 'A') + 10 : (what[0] - '0')); digit *= 16; digit += (what[1] >= 'A' ? ((what[1] & 0xdf) - 'A') + 10 : (what[1] - '0')); #else /*CHARSET_EBCDIC*/ char xstr[5]; xstr[0]='0'; xstr[1]='x'; xstr[2]=what[0]; xstr[3]=what[1]; xstr[4]='\0'; digit = os_toebcdic[0xFF & strtol(xstr, NULL, 16)]; #endif /*CHARSET_EBCDIC*/ return (digit); } /* Almost, but not quite, a copy of ap_unescape_url; * see note above mod_alterquery_x2c. */ static int mod_alterquery_unescape_url(char *url) { register int x, y, badesc, badpath; badesc = 0; badpath = 0; for (x = 0, y = 0; url[y]; ++x, ++y) { if (url[y] == '+') { url[x] = ' '; continue; } if (url[y] != '%') url[x] = url[y]; else { if (!ap_isxdigit(url[y + 1]) || !ap_isxdigit(url[y + 2])) { badesc = 1; url[x] = '%'; } else { url[x] = mod_alterquery_x2c(&url[y + 1]); y += 2; if (url[x] == '/' || url[x] == '\0') badpath = 1; } } } url[x] = '\0'; if (badesc) return BAD_REQUEST; else if (badpath) return NOT_FOUND; else return OK; } static int rebuild_args(void *data, const char *key, const char *val) { rebuild_args_data *rad = (rebuild_args_data *)data; request_rec *r = rad->r; uri_rec *u_rec = rad->u_rec; char *key_unescaped; char *val_unescaped; change_rec *ch_rec; /* We need to match the escaped key or value. */ key_unescaped = ap_pstrdup(r->pool, key); val_unescaped = ap_pstrdup(r->pool, val); mod_alterquery_unescape_url(key_unescaped); mod_alterquery_unescape_url(val_unescaped); for (ch_rec = u_rec->ch_rec; ch_rec != NULL; ch_rec = ch_rec->next) { /* If the key matches, we must have to do something. */ if ( ! strcmp(ch_rec->key, key_unescaped) ) { /* If this is a CHANGE rec, val is a replacement value if * val2 is NULL, otherwise, val is a match parameter, and * val2 is the replacement value */ if (ch_rec->act == CHANGE && (ch_rec->val2 == NULL || ! strcmp(ch_rec->val, val_unescaped) ) ) { char *tmp_val; ap_log_error(APLOG_MARK, APLOG_DEBUG, r->server, "AlterQueryChange \"%s\" \"%s\" \"%s\" (mod_alterquery action=\"%i\") on original \"%s\" = \"%s\"", ch_rec->key, ch_rec->val, ch_rec->val2, ch_rec->act, key_unescaped, val_unescaped); /* Remember that if val2 was not NULL, it was the * replacement value (because val was an additional * match criteria); whereas NULL val2 leaves val * as the replacement value. */ tmp_val = (ch_rec->val2) ? ch_rec->val2 : ch_rec->val; /* See note 1, below. */ val = ap_os_escape_path(r->pool, tmp_val, 0); } /* If this is a REMOVE rec, a non-NULL val is further * used to determine a match. */ if (ch_rec->act == REMOVE && (ch_rec->val == NULL || ! strcmp(ch_rec->val, val_unescaped) ) ) { ap_log_error(APLOG_MARK, APLOG_DEBUG, r->server, "AlterQueryRemove \"%s\" \"%s\" \"%s\" (mod_alterquery action=\"%i\") on original \"%s\" = \"%s\"", ch_rec->key, ch_rec->val, ch_rec->val2, ch_rec->act, key_unescaped, val_unescaped); /* Return from entire function to make sure that the * rest of this function (which adds key and val back * to args) is not executed. Not allowing the add * to happen is what makes the remove work. */ return 1; } } } /* The default is to add the value, untouched, back to the * new args string we are building. */ if (rad->args == NULL) { rad->args = ap_pstrdup(r->pool, key); rad->args = ap_pstrcat(r->pool, rad->args, "=", val, "&", NULL); } else { rad->args = ap_pstrcat(r->pool, rad->args, key, "=", val, "&", NULL); } /* return TRUE; */ return 1; } static int add_args(rebuild_args_data *rad) { request_rec *r = rad->r; uri_rec *u_rec = rad->u_rec; change_rec *add_rec; for (add_rec = u_rec->add_rec; add_rec != NULL; add_rec = add_rec->next) { char *escaped_val; char *escaped_key; ap_log_error(APLOG_MARK, APLOG_DEBUG, r->server, "AlterQueryAdd \"%s\" \"%s\" \"%s\" (mod_alterquery action=\"%i\")", add_rec->key, add_rec->val, add_rec->val2, add_rec->act); escaped_key = ap_os_escape_path(r->pool, add_rec->key, 0); escaped_val = ap_os_escape_path(r->pool, add_rec->val, 0); if (rad->args == NULL) { rad->args = ap_pstrdup(r->pool, escaped_key); rad->args = ap_pstrcat(r->pool, rad->args, "=", escaped_val, "&", NULL); } else { rad->args = ap_pstrcat(r->pool, rad->args, escaped_key, "=", escaped_val, "&", NULL); } } /* return TRUE; */ return 1; } /* --------------- Handler Functions ---------------------- */ static void *create_per_server_config(pool *p, server_rec *s) { alterquery_per_server_config *p_s_config = (alterquery_per_server_config *) ap_palloc(p, sizeof(alterquery_per_server_config)); p_s_config->pool = p; p_s_config->u_rec = NULL; /* begin with a NULL-terminated empty list */ return (void *) p_s_config; } /* handle_post_read_request (below) hands off control * to this function to process all the args for one uri_rec */ static int process_args(request_rec *r, uri_rec *u_rec) { const char *query_string = r->args; /* QUERY_STRING after ? in request */ const char *pair; /* key and value pair from QUERY_STRING, eg: foo=bar */ table *query_args; /* parsed (hashified) QUERY_STRING */ rebuild_args_data rad; /* extra data to be passed to the app_table_do callback; goes out of scope after this function call, but that should be OK. */ /* Parse the query string into a table called query_args. * This is bullet-proof in the event query_string is NULL. */ query_args = ap_make_table(r->pool, 10); if (query_string != NULL) { while (*query_string && (pair = ap_getword(r->pool, &query_string, '&'))) { const char *key; key = ap_getword(r->pool, &pair, '='); /* pair is now a misnomer, as ap_getword has updated it to point * past the =, which would make it just the value */ ap_table_add(query_args, key, pair); /* app_table_add can add multiple keys; * app_table_merge would actually * comma-append duplicate values under the same key */ } } rad.args = NULL; /* ensure this string starts as NULL before we grow it */ rad.r = r; rad.u_rec = u_rec; /* This is bullet-proof in the event query_args is empty * because query_string is NULL */ ap_table_do(rebuild_args, &rad, query_args, NULL); /* ADDING should be done AFTER the ap_table_do; this lets a user whack * all values from a multi-checkbox, then ADD values of the same * key back later. */ add_args(&rad); /* Because I add a '&' after every change record, including the last one, * I will need to remove the final, superflous '&'. But, remember, * rad.args could still == NULL because we may have the coincidence * where the configured deletes were the only keys in the GET * request to begin with, and there were no configured adds. */ if (rad.args != NULL) { char *last_ampersand; size_t last_amp_index; last_amp_index = strlen(rad.args); last_ampersand = rad.args + last_amp_index - 1; *last_ampersand = '\0'; } /* Even though rad gets cleaned up when this funtion's stack is popped, * the string rad.args points to was allocated from the requests's * memory pool, so this assignment is OK. */ r->args = rad.args; ap_log_error(APLOG_MARK, APLOG_DEBUG, r->server, "New QUERY_STRING: %s", r->args); return DECLINED; } static int handle_post_read_request(request_rec *r) { const char *query_string = r->args; /* QUERY_STRING after ? in request */ uri_rec *u_rec; /* uri record */ alterquery_per_server_config *p_s_config; p_s_config = ap_get_module_config(r->server->module_config, &alterquery_module); /* In very first part of processing the request, * if r->method_number != M_GET, stop processing, because we only * concern ourselves with form data in the get request anyway; then, * if GET, stop processing if r->args is null, because then there's * nothing after the uri to process anyway. */ if (r->method_number != M_GET) { ap_log_error(APLOG_MARK, APLOG_DEBUG, r->server, "Not a GET request. Done.", query_string); return DECLINED; } else { ap_log_error(APLOG_MARK, APLOG_DEBUG, r->server, "Is a GET request; does URI match any of those we need to look for?"); } /* Damn: What if we want to add args to an empty arg list? * We have to search all the uris and remove this performance * enhancement. */ /* if (query_string == NULL) { ap_log_error(APLOG_MARK, APLOG_DEBUG, r->server, "args is empty; skipping processing", query_string); return DECLINED; } else { ap_log_error(APLOG_MARK, APLOG_DEBUG, r->server, "args: %s", query_string); } */ /* search all uris in the per-server config * to see if they match the request uri */ for (u_rec = p_s_config->u_rec; u_rec != NULL; u_rec = u_rec->next) { /* PLEASE NOTE that both strcmp and sp_fnmatch * counterintuitively return 0 on success! */ if (u_rec->uri && ! strcmp(u_rec->uri, r->uri)) { ap_log_error(APLOG_MARK, APLOG_DEBUG, r->server, "URI exact match! Looking for \"%s\" and found \"%s\" in request. Start processing.", (u_rec->uri ? u_rec->uri : u_rec->glob_uri), r->uri); ap_log_error(APLOG_MARK, APLOG_DEBUG, r->server, "Original QUERY_STRING: %s", r->args); return process_args(r, u_rec); } /* PLEASE NOTE that both strcmp and sp_fnmatch * counterintuitively return 0 on success! */ if (u_rec->glob_uri && ! ap_fnmatch(u_rec->glob_uri, r->uri, 0)) { ap_log_error(APLOG_MARK, APLOG_DEBUG, r->server, "URI glob match! Looking for \"%s\" and found \"%s\" in request. Start processing.", (u_rec->uri ? u_rec->uri : u_rec->glob_uri), r->uri); ap_log_error(APLOG_MARK, APLOG_DEBUG, r->server, "Original QUERY_STRING: %s", r->args); return process_args(r, u_rec); } } ap_log_error(APLOG_MARK, APLOG_DEBUG, r->server, "Requested URI not found in list of URIs to look for. Done."); return DECLINED; } /* This actually does all the real work of create_new_regular_uri_record * and create_new_glob_uri_record. */ static const char *create_new_uri_record(cmd_parms *parms, void *mconfig, char *uri, char *glob_uri) { alterquery_per_server_config *p_s_config; uri_rec *new_uri_rec; p_s_config = ap_get_module_config(parms->server->module_config, &alterquery_module); new_uri_rec = (uri_rec *) ap_palloc(p_s_config->pool, sizeof(uri_rec)); /* ap_pstrdup safely handles NULLs */ new_uri_rec->uri = ap_pstrdup(p_s_config->pool, uri); new_uri_rec->glob_uri = ap_pstrdup(p_s_config->pool, glob_uri); new_uri_rec->ch_rec = NULL; /* make sure to terminate empty list! */ new_uri_rec->add_rec = NULL; /* make sure to terminate empty list! */ new_uri_rec->next = p_s_config->u_rec; p_s_config->u_rec = new_uri_rec; return NULL; } /* The next two functions add a new URI record to the * per-server configuration. Subsequent * calls to the other AlterQuery directives will add to this new URI * record until another AlterQueryForURI directive is encountered, creating * a new URI record. */ static const char *create_new_regular_uri_record(cmd_parms *parms, void *mconfig, char *uri) { return create_new_uri_record(parms, mconfig, uri, NULL); } static const char *create_new_glob_uri_record(cmd_parms *parms, void *mconfig, char *glob_uri) { return create_new_uri_record(parms, mconfig, NULL, glob_uri); } /* Add a "key [value] [value]" * record from an AlterQuery* command in httpd.conf * to the module's per-server configuration struct. */ static const char *set_ch_rec(cmd_parms *parms, action act, char *key, char *val, char *val2) { alterquery_per_server_config *p_s_config; /* new change record to be composed of key and val */ change_rec *new_ch_rec; p_s_config = ap_get_module_config(parms->server->module_config, &alterquery_module); /* allocate new change record, then * allocate copy of key and any vals for the new change record * NOTE that optional vals can be NULL; * ap_pstrdup can gracefully return NULL when * given NULL */ new_ch_rec = (change_rec *) ap_palloc(p_s_config->pool, sizeof(change_rec)); new_ch_rec->key = ap_pstrdup(p_s_config->pool, key); new_ch_rec->val = ap_pstrdup(p_s_config->pool, val); new_ch_rec->val2 = ap_pstrdup(p_s_config->pool, val2); new_ch_rec->act = act; /* Add the new change record to the start of the list (becasue * it's easier to do). This should gracefully deal with * p_s_config->ch_rec being NULL (for an empty list) or pointing * to the first element of a list. * NOTE that ADDs go in their own separate list, as they are processed * separately. */ if (act == ADD) { new_ch_rec->next = p_s_config->u_rec->add_rec; p_s_config->u_rec->add_rec = new_ch_rec; } else { /* act == REMOVE || act == CHANGE */ new_ch_rec->next = p_s_config->u_rec->ch_rec; p_s_config->u_rec->ch_rec = new_ch_rec; } ap_log_error(APLOG_MARK, APLOG_DEBUG, parms->server, "AlterQuery* config command: URI: \"%s\", Glob URI: \"%s\", Key: \"%s\", Val: \"%s\", Val2: \"%s\"", p_s_config->u_rec->uri, p_s_config->u_rec->glob_uri, new_ch_rec->key, new_ch_rec->val, new_ch_rec->val2); return NULL; } static const char *set_ch_rec_for_removal(cmd_parms *parms, void *act, char *key, char *val) { return set_ch_rec(parms, REMOVE, key, val, NULL); } static const char *set_ch_rec_for_change(cmd_parms *parms, void *act, char *key, char *val, char *val2) { return set_ch_rec(parms, CHANGE, key, val, val2); } static const char *set_ch_rec_for_addition(cmd_parms *parms, void *act, char *key, char *val) { return set_ch_rec(parms, ADD, key, val, NULL); } static const command_rec alterquery_cmds[] = { {"AlterQueryForURI", create_new_regular_uri_record, NULL, RSRC_CONF, TAKE1, "the uri of the GET request"}, {"AlterQueryForGlobURI", create_new_glob_uri_record, NULL, RSRC_CONF, TAKE1, "the uri using file glob expression of the GET request"}, {"AlterQueryRemove", set_ch_rec_for_removal, NULL, RSRC_CONF, TAKE12, "a key and optional value in the QUERY_STRING"}, {"AlterQueryChange", set_ch_rec_for_change, NULL, RSRC_CONF, TAKE23, "a key and value in the QUERY_STRING"}, {"AlterQueryAdd", set_ch_rec_for_addition, NULL, RSRC_CONF, TAKE2, "a key and value in the QUERY_STRING"}, {NULL} }; module MODULE_VAR_EXPORT alterquery_module = { STANDARD_MODULE_STUFF, NULL, /* module initializer */ NULL, /* per-directory config creator */ NULL, /* dir config merger */ create_per_server_config, /* server config creator */ NULL, /* server config merger */ alterquery_cmds, /* configuration directive table */ NULL, /* [9] content handlers */ NULL, /* [2] URI-to-filename translation */ NULL, /* [5] check/validate user_id */ NULL, /* [6] check user_id is valid *here* */ NULL, /* [4] check access by host address */ NULL, /* [7] MIME type checker/setter */ NULL, /* [8] pre-run fixups */ NULL, /* [10] logger */ NULL, /* [3] header parser */ NULL, /* child process initialisation */ NULL, /* child process exit/cleanup */ handle_post_read_request /* [1] post read-request */ }; /* * INTERESTING QUESTIONS I ENCOUNTERED WHILE WRITING THIS * * Question: does ap_log_error accept, on all platforms, * the null pointers that I'm giving it? * Answer: Yes, because of * #define S_NULL "(null)" * in ap_snprintf.c, which is put in place of NULLs found by ap_vformatter() * called by ap_vsnprintf, which is called from http_log.c by log_error_core() * which is called by ap_log_error(). */ /* * NOTES: * * 1. val should really first de-allocate its duplicate of * what was in v, but the pool API does not provide * that granularity; only the ability to free the * entire pool associated with the request; and that * will happen automatically later anyway. */