1/* Licensed to the Apache Software Foundation (ASF) under one or more 2 * contributor license agreements. See the NOTICE file distributed with 3 * this work for additional information regarding copyright ownership. 4 * The ASF licenses this file to You under the Apache License, Version 2.0 5 * (the "License"); you may not use this file except in compliance with 6 * the License. You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17/* User Tracking Module (Was mod_cookies.c) 18 * 19 * *** IMPORTANT NOTE: This module is not designed to generate 20 * *** cryptographically secure cookies. This means you should not 21 * *** use cookies generated by this module for authentication purposes 22 * 23 * This Apache module is designed to track users paths through a site. 24 * It uses the client-side state ("Cookie") protocol developed by Netscape. 25 * It is known to work on most browsers. 26 * 27 * Each time a page is requested we look to see if the browser is sending 28 * us a Cookie: header that we previously generated. 29 * 30 * If we don't find one then the user hasn't been to this site since 31 * starting their browser or their browser doesn't support cookies. So 32 * we generate a unique Cookie for the transaction and send it back to 33 * the browser (via a "Set-Cookie" header) 34 * Future requests from the same browser should keep the same Cookie line. 35 * 36 * By matching up all the requests with the same cookie you can 37 * work out exactly what path a user took through your site. To log 38 * the cookie use the " %{Cookie}n " directive in a custom access log; 39 * 40 * Example 1 : If you currently use the standard Log file format (CLF) 41 * and use the command "TransferLog somefilename", add the line 42 * LogFormat "%h %l %u %t \"%r\" %s %b %{Cookie}n" 43 * to your config file. 44 * 45 * Example 2 : If you used to use the old "CookieLog" directive, you 46 * can emulate it by adding the following command to your config file 47 * CustomLog filename "%{Cookie}n \"%r\" %t" 48 * 49 * Mark Cox, mjc@apache.org, 6 July 95 50 * 51 * This file replaces mod_cookies.c 52 */ 53 54#include "apr.h" 55#include "apr_lib.h" 56#include "apr_strings.h" 57 58#define APR_WANT_STRFUNC 59#include "apr_want.h" 60 61#include "httpd.h" 62#include "http_config.h" 63#include "http_core.h" 64#include "http_request.h" 65 66 67module AP_MODULE_DECLARE_DATA usertrack_module; 68 69typedef struct { 70 int always; 71 int expires; 72} cookie_log_state; 73 74typedef enum { 75 CT_UNSET, 76 CT_NETSCAPE, 77 CT_COOKIE, 78 CT_COOKIE2 79} cookie_type_e; 80 81typedef struct { 82 int enabled; 83 cookie_type_e style; 84 char *cookie_name; 85 char *cookie_domain; 86 char *regexp_string; /* used to compile regexp; save for debugging */ 87 ap_regex_t *regexp; /* used to find usertrack cookie in cookie header */ 88} cookie_dir_rec; 89 90/* Make Cookie: Now we have to generate something that is going to be 91 * pretty unique. We can base it on the pid, time, hostip */ 92 93#define COOKIE_NAME "Apache" 94 95static void make_cookie(request_rec *r) 96{ 97 cookie_log_state *cls = ap_get_module_config(r->server->module_config, 98 &usertrack_module); 99 /* 1024 == hardcoded constant */ 100 char cookiebuf[1024]; 101 char *new_cookie; 102 const char *rname = ap_get_remote_host(r->connection, r->per_dir_config, 103 REMOTE_NAME, NULL); 104 cookie_dir_rec *dcfg; 105 106 dcfg = ap_get_module_config(r->per_dir_config, &usertrack_module); 107 108 /* XXX: hmm, this should really tie in with mod_unique_id */ 109 apr_snprintf(cookiebuf, sizeof(cookiebuf), "%s.%" APR_TIME_T_FMT, rname, 110 apr_time_now()); 111 112 if (cls->expires) { 113 114 /* Cookie with date; as strftime '%a, %d-%h-%y %H:%M:%S GMT' */ 115 new_cookie = apr_psprintf(r->pool, "%s=%s; path=/", 116 dcfg->cookie_name, cookiebuf); 117 118 if ((dcfg->style == CT_UNSET) || (dcfg->style == CT_NETSCAPE)) { 119 apr_time_exp_t tms; 120 apr_time_exp_gmt(&tms, r->request_time 121 + apr_time_from_sec(cls->expires)); 122 new_cookie = apr_psprintf(r->pool, 123 "%s; expires=%s, " 124 "%.2d-%s-%.2d %.2d:%.2d:%.2d GMT", 125 new_cookie, apr_day_snames[tms.tm_wday], 126 tms.tm_mday, 127 apr_month_snames[tms.tm_mon], 128 tms.tm_year % 100, 129 tms.tm_hour, tms.tm_min, tms.tm_sec); 130 } 131 else { 132 new_cookie = apr_psprintf(r->pool, "%s; max-age=%d", 133 new_cookie, cls->expires); 134 } 135 } 136 else { 137 new_cookie = apr_psprintf(r->pool, "%s=%s; path=/", 138 dcfg->cookie_name, cookiebuf); 139 } 140 if (dcfg->cookie_domain != NULL) { 141 new_cookie = apr_pstrcat(r->pool, new_cookie, "; domain=", 142 dcfg->cookie_domain, 143 (dcfg->style == CT_COOKIE2 144 ? "; version=1" 145 : ""), 146 NULL); 147 } 148 149 apr_table_addn(r->headers_out, 150 (dcfg->style == CT_COOKIE2 ? "Set-Cookie2" : "Set-Cookie"), 151 new_cookie); 152 apr_table_setn(r->notes, "cookie", apr_pstrdup(r->pool, cookiebuf)); /* log first time */ 153 return; 154} 155 156/* dcfg->regexp is "^cookie_name=([^;]+)|;[ \t]+cookie_name=([^;]+)", 157 * which has three subexpressions, $0..$2 */ 158#define NUM_SUBS 3 159 160static void set_and_comp_regexp(cookie_dir_rec *dcfg, 161 apr_pool_t *p, 162 const char *cookie_name) 163{ 164 int danger_chars = 0; 165 const char *sp = cookie_name; 166 167 /* The goal is to end up with this regexp, 168 * ^cookie_name=([^;,]+)|[;,][ \t]+cookie_name=([^;,]+) 169 * with cookie_name obviously substituted either 170 * with the real cookie name set by the user in httpd.conf, or with the 171 * default COOKIE_NAME. */ 172 173 /* Anyway, we need to escape the cookie_name before pasting it 174 * into the regex 175 */ 176 while (*sp) { 177 if (!apr_isalnum(*sp)) { 178 ++danger_chars; 179 } 180 ++sp; 181 } 182 183 if (danger_chars) { 184 char *cp; 185 cp = apr_palloc(p, sp - cookie_name + danger_chars + 1); /* 1 == \0 */ 186 sp = cookie_name; 187 cookie_name = cp; 188 while (*sp) { 189 if (!apr_isalnum(*sp)) { 190 *cp++ = '\\'; 191 } 192 *cp++ = *sp++; 193 } 194 *cp = '\0'; 195 } 196 197 dcfg->regexp_string = apr_pstrcat(p, "^", 198 cookie_name, 199 "=([^;,]+)|[;,][ \t]*", 200 cookie_name, 201 "=([^;,]+)", NULL); 202 203 dcfg->regexp = ap_pregcomp(p, dcfg->regexp_string, AP_REG_EXTENDED); 204 ap_assert(dcfg->regexp != NULL); 205} 206 207static int spot_cookie(request_rec *r) 208{ 209 cookie_dir_rec *dcfg = ap_get_module_config(r->per_dir_config, 210 &usertrack_module); 211 const char *cookie_header; 212 ap_regmatch_t regm[NUM_SUBS]; 213 214 /* Do not run in subrequests */ 215 if (!dcfg->enabled || r->main) { 216 return DECLINED; 217 } 218 219 if ((cookie_header = apr_table_get(r->headers_in, "Cookie"))) { 220 if (!ap_regexec(dcfg->regexp, cookie_header, NUM_SUBS, regm, 0)) { 221 char *cookieval = NULL; 222 /* Our regexp, 223 * ^cookie_name=([^;]+)|;[ \t]+cookie_name=([^;]+) 224 * only allows for $1 or $2 to be available. ($0 is always 225 * filled with the entire matched expression, not just 226 * the part in parentheses.) So just check for either one 227 * and assign to cookieval if present. */ 228 if (regm[1].rm_so != -1) { 229 cookieval = ap_pregsub(r->pool, "$1", cookie_header, 230 NUM_SUBS, regm); 231 } 232 if (regm[2].rm_so != -1) { 233 cookieval = ap_pregsub(r->pool, "$2", cookie_header, 234 NUM_SUBS, regm); 235 } 236 /* Set the cookie in a note, for logging */ 237 apr_table_setn(r->notes, "cookie", cookieval); 238 239 return DECLINED; /* There's already a cookie, no new one */ 240 } 241 } 242 make_cookie(r); 243 return OK; /* We set our cookie */ 244} 245 246static void *make_cookie_log_state(apr_pool_t *p, server_rec *s) 247{ 248 cookie_log_state *cls = 249 (cookie_log_state *) apr_palloc(p, sizeof(cookie_log_state)); 250 251 cls->expires = 0; 252 253 return (void *) cls; 254} 255 256static void *make_cookie_dir(apr_pool_t *p, char *d) 257{ 258 cookie_dir_rec *dcfg; 259 260 dcfg = (cookie_dir_rec *) apr_pcalloc(p, sizeof(cookie_dir_rec)); 261 dcfg->cookie_name = COOKIE_NAME; 262 dcfg->cookie_domain = NULL; 263 dcfg->style = CT_UNSET; 264 dcfg->enabled = 0; 265 266 /* In case the user does not use the CookieName directive, 267 * we need to compile the regexp for the default cookie name. */ 268 set_and_comp_regexp(dcfg, p, COOKIE_NAME); 269 270 return dcfg; 271} 272 273static const char *set_cookie_enable(cmd_parms *cmd, void *mconfig, int arg) 274{ 275 cookie_dir_rec *dcfg = mconfig; 276 277 dcfg->enabled = arg; 278 return NULL; 279} 280 281static const char *set_cookie_exp(cmd_parms *parms, void *dummy, 282 const char *arg) 283{ 284 cookie_log_state *cls; 285 time_t factor, modifier = 0; 286 time_t num = 0; 287 char *word; 288 289 cls = ap_get_module_config(parms->server->module_config, 290 &usertrack_module); 291 /* The simple case first - all numbers (we assume) */ 292 if (apr_isdigit(arg[0]) && apr_isdigit(arg[strlen(arg) - 1])) { 293 cls->expires = atol(arg); 294 return NULL; 295 } 296 297 /* 298 * The harder case - stolen from mod_expires 299 * 300 * CookieExpires "[plus] {<num> <type>}*" 301 */ 302 303 word = ap_getword_conf(parms->pool, &arg); 304 if (!strncasecmp(word, "plus", 1)) { 305 word = ap_getword_conf(parms->pool, &arg); 306 }; 307 308 /* {<num> <type>}* */ 309 while (word[0]) { 310 /* <num> */ 311 if (apr_isdigit(word[0])) 312 num = atoi(word); 313 else 314 return "bad expires code, numeric value expected."; 315 316 /* <type> */ 317 word = ap_getword_conf(parms->pool, &arg); 318 if (!word[0]) 319 return "bad expires code, missing <type>"; 320 321 factor = 0; 322 if (!strncasecmp(word, "years", 1)) 323 factor = 60 * 60 * 24 * 365; 324 else if (!strncasecmp(word, "months", 2)) 325 factor = 60 * 60 * 24 * 30; 326 else if (!strncasecmp(word, "weeks", 1)) 327 factor = 60 * 60 * 24 * 7; 328 else if (!strncasecmp(word, "days", 1)) 329 factor = 60 * 60 * 24; 330 else if (!strncasecmp(word, "hours", 1)) 331 factor = 60 * 60; 332 else if (!strncasecmp(word, "minutes", 2)) 333 factor = 60; 334 else if (!strncasecmp(word, "seconds", 1)) 335 factor = 1; 336 else 337 return "bad expires code, unrecognized type"; 338 339 modifier = modifier + factor * num; 340 341 /* next <num> */ 342 word = ap_getword_conf(parms->pool, &arg); 343 } 344 345 cls->expires = modifier; 346 347 return NULL; 348} 349 350static const char *set_cookie_name(cmd_parms *cmd, void *mconfig, 351 const char *name) 352{ 353 cookie_dir_rec *dcfg = (cookie_dir_rec *) mconfig; 354 355 dcfg->cookie_name = apr_pstrdup(cmd->pool, name); 356 357 set_and_comp_regexp(dcfg, cmd->pool, name); 358 359 if (dcfg->regexp == NULL) { 360 return "Regular expression could not be compiled."; 361 } 362 if (dcfg->regexp->re_nsub + 1 != NUM_SUBS) { 363 return apr_pstrcat(cmd->pool, "Invalid cookie name \"", 364 name, "\"", NULL); 365 } 366 367 return NULL; 368} 369 370/* 371 * Set the value for the 'Domain=' attribute. 372 */ 373static const char *set_cookie_domain(cmd_parms *cmd, void *mconfig, 374 const char *name) 375{ 376 cookie_dir_rec *dcfg; 377 378 dcfg = (cookie_dir_rec *) mconfig; 379 380 /* 381 * Apply the restrictions on cookie domain attributes. 382 */ 383 if (strlen(name) == 0) { 384 return "CookieDomain values may not be null"; 385 } 386 if (name[0] != '.') { 387 return "CookieDomain values must begin with a dot"; 388 } 389 if (ap_strchr_c(&name[1], '.') == NULL) { 390 return "CookieDomain values must contain at least one embedded dot"; 391 } 392 393 dcfg->cookie_domain = apr_pstrdup(cmd->pool, name); 394 return NULL; 395} 396 397/* 398 * Make a note of the cookie style we should use. 399 */ 400static const char *set_cookie_style(cmd_parms *cmd, void *mconfig, 401 const char *name) 402{ 403 cookie_dir_rec *dcfg; 404 405 dcfg = (cookie_dir_rec *) mconfig; 406 407 if (strcasecmp(name, "Netscape") == 0) { 408 dcfg->style = CT_NETSCAPE; 409 } 410 else if ((strcasecmp(name, "Cookie") == 0) 411 || (strcasecmp(name, "RFC2109") == 0)) { 412 dcfg->style = CT_COOKIE; 413 } 414 else if ((strcasecmp(name, "Cookie2") == 0) 415 || (strcasecmp(name, "RFC2965") == 0)) { 416 dcfg->style = CT_COOKIE2; 417 } 418 else { 419 return apr_psprintf(cmd->pool, "Invalid %s keyword: '%s'", 420 cmd->cmd->name, name); 421 } 422 423 return NULL; 424} 425 426static const command_rec cookie_log_cmds[] = { 427 AP_INIT_TAKE1("CookieExpires", set_cookie_exp, NULL, OR_FILEINFO, 428 "an expiry date code"), 429 AP_INIT_TAKE1("CookieDomain", set_cookie_domain, NULL, OR_FILEINFO, 430 "domain to which this cookie applies"), 431 AP_INIT_TAKE1("CookieStyle", set_cookie_style, NULL, OR_FILEINFO, 432 "'Netscape', 'Cookie' (RFC2109), or 'Cookie2' (RFC2965)"), 433 AP_INIT_FLAG("CookieTracking", set_cookie_enable, NULL, OR_FILEINFO, 434 "whether or not to enable cookies"), 435 AP_INIT_TAKE1("CookieName", set_cookie_name, NULL, OR_FILEINFO, 436 "name of the tracking cookie"), 437 {NULL} 438}; 439 440static void register_hooks(apr_pool_t *p) 441{ 442 ap_hook_fixups(spot_cookie,NULL,NULL,APR_HOOK_FIRST); 443} 444 445module AP_MODULE_DECLARE_DATA usertrack_module = { 446 STANDARD20_MODULE_STUFF, 447 make_cookie_dir, /* dir config creater */ 448 NULL, /* dir merger --- default is to override */ 449 make_cookie_log_state, /* server config */ 450 NULL, /* merge server configs */ 451 cookie_log_cmds, /* command apr_table_t */ 452 register_hooks /* register hooks */ 453}; 454