/*======================================================================= * Copyright (c) 2000 MPL Communications Inc. Author: Justin Wells * http://www.carlsononline.com justin@semiotek.com * ====================================================================== Module Name: mod_ticket.c Version: 1.1 Contributor: MPL Communications Inc. (http://www.carlsononline.com) Author: Justin Wells (justin@semiotek.com) Description: Check for a a digitally signed ticket in the URI, and if found make it available in the variable $TICKET Motivation: Allow passing authenticated sessions from one domain to another in a secure fashion by way of a shared secret; track an http session through a site without using cookies in a manner which survives relative URL links. URI Format: http://servername/$ticketname$ticketvalue$md5sum/URI where md5sum == md5sum(strcat(secret,ticketvalue,remote_ip)) Directives : TicketKey NAME SECRET define a ticketname TicketDelim CHAR use CHAR instead of $ TicketSumLength NUMBER minimum length of md5sum TicketCryptIP on|off include remote IP in md5sum? TicketEnabled on|off enable/disable mod_ticket TicketCookie on|off ticket may be in cookie if not URI TicketHeader on|off ticket will be added to headers Environment: $TICKET the value of the ticket $TICKET_NAME the name of the ticket $TICKET_ERROR reason for ignoring ticket $TICKET_SUM the string used to generate md5sum mod_ticket will only set the environment variables $TICKET and $TICKET_NAME if the md5sum in the ticket is valid. If a ticket is rejected an explanation will appear in the variable $TICKET_ERROR. HOW TO INSTALL ============== You must compile and install it into your httpd like this: $APACHE/bin/apxs -i -c -a mod_ticket.c HOW TO USE ========== mod_ticket is quite versatile so you can no doubt find lots of uses for it. Here is what I use it for: -- I install mod_ticket with: TicketKey example example_password TicketSumLength 12 TicketCryptIP on TicketCookie on TicketHeader on -- To give a ticket to a user I set the to a URL base that includes the ticket I am trying to set. I also write the ticket to a cookie called Ticket. These tickets then survive all kinds of things--if the user supports cookies, then they last forever. If the user doesn't support cookies, then the ticket survives through the site so long as the site uses only relative URLs. The mechanism of setting the cookie is rather nice since it avoids a redirect "page flash". I give each site that wants to pass authenticated traffic to me a key and a secret passphrase, along with some example PERL code showing how to construct the md5 strings. Then I can read the tickets coming from those sites. I know which site sent me the traffic not only from the referer (which can be spoofed) but from the $TICKET_NAME that is set in the environment. I turned TicketHeader on in order to make the tickets available to things like Java servlets which cannot easily get at environment variables (ugh!). They appear as "Ticket: name=value" in the request headers. This way of using mod_ticket provides for session tracking and cookie data that works for users who do not support cookies, and which works between sites in different domains. Also, if a user does support cookies, then tracking of their session is improved. DIRECTIVES ========== TicketEnabled turns on and off mod_ticket on a per-server basis. The TicketKey directive is used to specify ticket names. Each ticket name has a secret phrase. When checking a request, the ticket value is appended to the ticket secret and an md5sum is computed. The computed sum must match the md5sum stored in the ticket. The TicketDelim directive allows you to specify a different delimiter other than the default $. Any URI beginning with this delimiter will be inspected to see if it begins with a ticket. The TicketCryptIP directive allows you to turn on and off inclusion of the remote IP in the generated md5sum. Including the IP results in tickets that are valid for only one client. The TicketSumLength directive allows you to specify only a subset of the actual md5sum in the URL. This shortens the URL, and reduces the cryptographic security of the scheme. If you specify a value of 12, for example, then you only need the 12 rightmost digits of the md5sum in the ticket (though you are allowed to use more). Setting TicketSumLength to zero allows tickets with no authentication, though if a sum is specified it must still be valid. If the TicketCookie flag is set on then mod_ticket will look for the ticket in a cookie before checking the URI. ======================================================================= Copyright (c) 2000 MPL Communications Inc. All rights reserved. This software was written for MPL Communications Inc. by Justin Wells and is hereby contributed to the Apache Software Foundation for distribution under the Apache license, as follows. 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 the Apache Group for use in the Apache HTTP server project (http://www.apache.org/)." 4. The names "Apache Server" and "Apache Group" must not be used to endorse or promote products derived from this software without prior written permission. For written permission, please contact apache@apache.org. 5. Products derived from this software may not be called "Apache" nor may "Apache" appear in their names without prior written permission of the Apache Group. 6. Redistributions of any form whatsoever must retain the following acknowledgment: "This product includes software developed by the Apache Group for use in the Apache HTTP server project (http://www.apache.org/)." THIS SOFTWARE IS PROVIDED BY THE APACHE GROUP ``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 THE APACHE GROUP OR ITS 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. ======================================================================= This software consists of voluntary contributions made by many individuals on behalf of the Apache Group and was originally based on public domain software written at the National Center for Supercomputing Applications, University of Illinois, Urbana-Champaign. For more information on the Apache Group and the Apache HTTP server project, please see . * ====================================================================== */ #include "httpd.h" #include "http_config.h" #include "http_core.h" #include "http_log.h" #include "http_main.h" #include "http_protocol.h" #include "util_script.h" #include "util_md5.h" #include /* * Declare ourselves so the configuration routines can find and know us. * We'll fill it in at the end of the module. */ module ticket_module; /*--------------------------------------------------------------------------*/ /* */ /* CONFIGURATION HANDLING--build up the key_node list */ /* */ /*--------------------------------------------------------------------------*/ #define TICKET_DELIM '$' #define SUM_LENGTH 32 #define FALSE 0 #define TRUE 1 #define TICKET_HEADER "Ticket" /* * Configuration for this module consists of a list of ticket_records. Each * record supplies the data for one passphrase, along with a pointer to * the next ticket record. This is a linked list. The first node in the * list is a dummy record. The subsequent nodes are the config data. * New data is added to the start of the list. Data is merged from a parent * config by sharing the tail of a list, and then appending the override * data to the start of the list. * * Our main hook is URI rewriting, which is called before per-directory * configuration steps are performed, therefore we can only use server conf. */ typedef struct key_node { char *name; char *phrase; struct key_node *next; } key_node; typedef struct ticket_conf { key_node *head; char delimiter; int md5length; int cryptip; int enabled; int cookie; int header; } ticket_conf; /** * Internal method--create a new key node */ key_node *new_key_node(pool * p, char *name, char *phrase) { key_node *new = (key_node *) ap_palloc(p, sizeof(key_node));; new->name = name; new->phrase = phrase; new->next = NULL; return new; } /** * Internal method--insert a key node into a key node list */ void insert_key_node(key_node * head, key_node * new) { new->next = head->next; head->next = new; } /** * Create a new config structure, being a list of key_node. This * list begins with a dummy node. This is the server config. */ static void *ticket_create_sconfig(pool * p, server_rec * s) { ticket_conf *conf = ap_palloc(p, sizeof(ticket_conf)); conf->delimiter = TICKET_DELIM; conf->md5length = SUM_LENGTH; conf->head = new_key_node(p, NULL, NULL); conf->cryptip = 1; conf->enabled = 1; conf->cookie = 0; conf->header = 0; return conf; } /** * Merge the config structure for this location with its parents. This * means attaching the parents list to the end of the childs list. */ static void *ticket_merge_sconfig(pool * p, void *parent_conf, void *sub_conf) { ticket_conf *par = (ticket_conf *) parent_conf; ticket_conf *sub = (ticket_conf *) sub_conf; ticket_conf *conf = ap_palloc(p, sizeof(ticket_conf)); key_node *phead = ((ticket_conf *) parent_conf)->head; key_node *shead = ((ticket_conf *) sub_conf)->head; key_node *head = new_key_node(p, NULL, NULL); key_node *n = NULL; conf->delimiter = sub->delimiter; conf->md5length = sub->md5length; conf->cryptip = sub->cryptip; conf->enabled = sub->enabled; conf->cookie = sub->cookie; conf->header = sub->header; /* parent list at end, unmodified, sub list inserted ahead of that */ conf->head->next = phead->next; while (shead->next != NULL) { shead = shead->next; n = new_key_node(p, shead->name, shead->phrase); insert_key_node(conf->head, n); } return conf; } /** * Add a new key to the list of ticket records, at the beginning, after * the initial dummy node. */ static const char *ticket_handle_key(cmd_parms * cmd, void *mconfig, char *keyname, char *keyphrase) { server_rec *s = cmd->server; ticket_conf *conf = (ticket_conf *) ap_get_module_config(s->module_config, &ticket_module); key_node *n = new_key_node(cmd->pool, keyname, keyphrase); if (!keyname || !keyphrase) { return "You must specify both a keyname and a keyphrase"; } insert_key_node(conf->head, n); return NULL; } /** * Set the delimiter character used to separate the tickets. By default * this is the $ character, but you can set this to another character * if you prefer something different. Do not use / or ~ or something * meaningful to another module or in a URL. */ static const char *ticket_set_delim(cmd_parms * cmd, void *mconfig, char *delimiter) { server_rec *s = cmd->server; char delim; ticket_conf *conf = (ticket_conf *) ap_get_module_config(s->module_config, &ticket_module); if (delimiter) { delim = delimiter[0]; switch (delim) { case '\0': case '/': case '\\': case ':': case ' ': case ' ': case '#': return "Illegal character specified as ticket delimiter"; } conf->delimiter = delim; } else { "Ticket delimiter directive incorrectly specified"; } return NULL; } /** * Set the length of the MD5SUM used to generate tickets. A full MD5SUM * will be generated, but you may wish to reduce the length of the * resulting URLs (and thus reduce the security of the scheme) by * using only part of the computed sum. mod_ticket will only use this * many characters from the __end__ of the md5sum. By default it is * a 12 character checksum. It can range from 0 to 32. Setting it * to zero turns off authentication checking. */ static const char *ticket_set_sumlength(cmd_parms * cmd, void *mconfig, char *length) { int len = atoi(length); server_rec *s = cmd->server; ticket_conf *conf = (ticket_conf *) ap_get_module_config(s->module_config, &ticket_module); if ((len >= 0) && (len <= 32)) { conf->md5length = len; } else { return "Ticket MD5 length must be between 0 and 32"; } return NULL; } /** * Set the name of the cookie that we can look for tickets in, if the * ticket is not found in the URL. */ static const char *ticket_set_cookie(cmd_parms * cmd, void *mconfig, int bool) { server_rec *s = cmd->server; ticket_conf *conf = (ticket_conf *) ap_get_module_config(s->module_config, &ticket_module); conf->cookie = bool; return NULL; } /** * Set whether or not the ticket will be passed in the header */ static const char *ticket_set_header(cmd_parms * cmd, void *mconfig, int bool) { server_rec *s = cmd->server; ticket_conf *conf = (ticket_conf *) ap_get_module_config(s->module_config, &ticket_module); conf->header = bool; return NULL; } /** * Set whether or not the IP number is crypted into the md5sum or not. * The default is that it is. */ static const char *ticket_set_cryptip(cmd_parms * cmd, void *mconfig, int bool) { server_rec *s = cmd->server; ticket_conf *conf = (ticket_conf *) ap_get_module_config(s->module_config, &ticket_module); conf->cryptip = bool; return NULL; } /** * Set whether mod_ticket is enabled for this server or not */ static const char *ticket_set_enabled(cmd_parms * cmd, void *mconfig, int bool) { server_rec *s = cmd->server; ticket_conf *conf = (ticket_conf *) ap_get_module_config(s->module_config, &ticket_module); conf->enabled = bool; return NULL; } /*--------------------------------------------------------------------------*/ /* */ /* REQUEST PROCESSING -- Handle requests and extract our ticket from the */ /* URL if it is there */ /* */ /*--------------------------------------------------------------------------*/ /** * Try and extract a ticket out of the URL */ char *extract_uri_ticket(ticket_conf *conf, request_rec *r) { char *name = r->uri; const char *filename; char *newfilename; char *ticket; /** * Do nothing if the URI doesn't match up */ if ((name[0] != '/') || (name[1] != conf->delimiter)) { return NULL; } filename = name + 2; /* * Advance filename so that the / following our ticket, if there was one * is at the head of filename. */ ticket = ap_getword(r->pool, &filename, '/'); if (filename[-1] == '/') { --filename; } /* * Check whether we got a ticket */ if (ticket[0] == '\0') { return NULL; } /** * Fixup the URI and filename so that the ticket was never there */ newfilename = ap_pstrdup(r->pool, filename); r->filename = newfilename; r->uri = ap_pstrdup(r->pool, newfilename); return ticket; } /** * try and extract a ticket out of a cookie */ char *extract_cookie_ticket(ticket_conf *conf, request_rec *r) { const char *cookie; char *ticket; if ((cookie = ap_table_get(r->headers_in, "Cookie"))) { const char *value = strstr(cookie,TICKET_HEADER); if (!value) { return NULL; /* our cookie was not found */ } value += strlen(TICKET_HEADER); /* get to the = sign */ if (*value != '=') { return NULL; } value++; if (*value != conf->delimiter) { return NULL; } value++; ticket = ap_getword(r->pool, &value, ';'); return ticket; } return NULL; } /** * Examine the ticket to see if its md5sum is correct, and if so pass * it along to the rest of the system via the environment variables * and the request header. */ int process_ticket(char *ticket, ticket_conf *conf, request_rec *r) { char *ticket_value, *ticket_name, *ticket_sum; char *sum, *md5string; int len = 0; key_node *keynode = conf->head; if (ticket == NULL) { return FALSE; } /* * Extract data from the ticket */ ticket_name = ticket; ticket_value = strchr(ticket, conf->delimiter); if (!ticket_value) { ap_table_setn(r->subprocess_env, "TICKET_ERROR", "Supplied ticket does not have a value!"); return FALSE; } ticket_value[0] = '\0'; ticket_value++; /* If we need an md5sum (conf length > 0) make sure we got one */ ticket_sum = strchr(ticket_value, conf->delimiter); if (ticket_sum) { ticket_sum[0] = '\0'; ticket_sum++; len = strlen(ticket_sum); if (len < conf->md5length) { ap_table_setn(r->subprocess_env, "TICKET_ERROR", "Supplied md5sum was not long enough"); return FALSE; } if (len > 32) { len = 32; } } else if (conf->md5length) { ap_table_setn(r->subprocess_env, "TICKET_ERROR", "Supplied ticket did not have an md5sum"); return FALSE; } while (keynode->next) { keynode = keynode->next; if (keynode->name && (strcmp(ticket_name, keynode->name) == 0)) { if (len > 0) { md5string = ap_pstrcat(r->pool, keynode->phrase, ticket_value, (conf->cryptip ? r->connection-> remote_ip : NULL), NULL); ap_table_setn(r->subprocess_env, "TICKET_SUM", md5string); sum = ap_md5(r->pool, md5string); sum = sum + (32 - len); if (ticket_sum && strcmp(sum, ticket_sum) != 0) { ap_table_setn(r->subprocess_env, "TICKET_ERROR", "Ticket failed md5sum check"); return FALSE; } } ap_table_setn(r->subprocess_env, "TICKET_NAME", ticket_name); ap_table_setn(r->subprocess_env, "TICKET", ticket_value); if (conf->header) { char *header_value = ap_pstrcat(r->pool, ticket_name, "=", ticket_value, NULL); ap_table_setn(r->headers_in, TICKET_HEADER, header_value); } return TRUE; } } ap_table_setn(r->subprocess_env, "TICKET_ERROR", "Ticket NAME did not match any of the available keys"); return FALSE; } /* * This routine gives our module an opportunity to translate the URI into an * actual filename. If we don't do anything special, the server's default * rules (Alias directives and the like) will continue to be followed. * * The return value is OK, DECLINED, or HTTP_mumble. If we return OK, no * further modules are called for this phase. */ static int ticket_translate_handler(request_rec *r) { void *sconf = r->server->module_config; char *ticket; ticket_conf *conf = (ticket_conf *) ap_get_module_config(sconf, &ticket_module); /* * Do nothing if we're unconfigured or disabled */ if ((conf->enabled == FALSE) || (conf->head == NULL) || (conf->head->next == NULL)) { return DECLINED; } /* * Make sure there is no Ticket header already in the stream */ if (conf->header) { ap_table_unset(r->headers_in, TICKET_HEADER); } /* * Find a ticket first in a cookie, then in a header */ ticket = extract_uri_ticket(conf,r); (conf->cookie && process_ticket( extract_cookie_ticket(conf,r), conf, r)) || process_ticket( ticket, conf, r); return DECLINED; } /*--------------------------------------------------------------------------*/ /* */ /* MODULE DEFINITION -- these tables define the content of the module */ /* */ /*--------------------------------------------------------------------------*/ static const command_rec ticket_cmds[] = { { "TicketKey", /* directive name */ ticket_handle_key, /* config action routine */ NULL, /* argument to include in call */ RSRC_CONF, /* in .htaccess if AllowOverride Options */ TAKE2, /* arguments */ "Define a key for decrypting URL tickets. Two arguments: name password" /* directive description */ }, { "TicketDelim", /* directive name */ ticket_set_delim, /* config action routine */ NULL, /* argument to include in call */ RSRC_CONF, /* in .htaccess if AllowOverride Options */ TAKE1, /* arguments */ "Define the delimiter character (eg: $) used to delimit tickets\n" /* directive description */ }, { "TicketSumLength", /* directive name */ ticket_set_sumlength, /* config action routine */ NULL, /* argument to include in call */ RSRC_CONF, /* in .htaccess if AllowOverride Options */ TAKE1, /* arguments */ "The length of the md5-based checksum used in a ticket (up to 32)\n" /* directive description */ }, { "TicketCryptIP", /* directive name */ ticket_set_cryptip, /* config action routine */ NULL, /* argument to include in call */ RSRC_CONF, /* in .htaccess if AllowOverride Options */ FLAG, /* arguments */ "Does the md5sum also hash the IP number of the request?\n" /* directive description */ }, { "TicketEnabled", /* directive name */ ticket_set_enabled, /* config action routine */ NULL, /* argument to include in call */ RSRC_CONF, /* in .htaccess if AllowOverride Options */ FLAG, /* arguments */ "Is this module enabled for this server?\n" /* directive description */ }, { "TicketCookie", /* directive name */ ticket_set_cookie, /* config action routine */ NULL, /* argument to include in call */ RSRC_CONF, /* in .htaccess if AllowOverride Options */ FLAG, /* arguments */ "Look in cookies with this name if the ticket is not in the URIn" /* directive description */ }, { "TicketHeader", /* directive name */ ticket_set_header, /* config action routine */ NULL, /* argument to include in call */ RSRC_CONF, /* in .htaccess if AllowOverride Options */ FLAG, /* arguments */ "Set whether or not to provide the ticket in a Ticket header", /* directive description */ }, {NULL} }; module ticket_module = { STANDARD_MODULE_STUFF, NULL, /* module initializer */ NULL, /* per-directory config creator */ NULL, /* dir config merger */ ticket_create_sconfig, /* server config creator */ ticket_merge_sconfig, /* server config merger */ ticket_cmds, /* command table */ NULL, /* [7] list of handlers */ ticket_translate_handler, /* [2] filename-to-URI 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, /* [10] logger */ NULL, /* [3] header parser */ NULL, /* process initializer */ NULL, /* process exit/cleanup */ NULL, /* [1] post read_request handling */ };