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#include "http_log.h" 66 67 68module AP_MODULE_DECLARE_DATA usertrack_module; 69 70typedef struct { 71 int always; 72 int expires; 73} cookie_log_state; 74 75typedef enum { 76 CT_UNSET, 77 CT_NETSCAPE, 78 CT_COOKIE, 79 CT_COOKIE2 80} cookie_type_e; 81 82typedef struct { 83 int enabled; 84 cookie_type_e style; 85 const char *cookie_name; 86 const char *cookie_domain; 87 char *regexp_string; /* used to compile regexp; save for debugging */ 88 ap_regex_t *regexp; /* used to find usertrack cookie in cookie header */ 89} cookie_dir_rec; 90 91/* Make Cookie: Now we have to generate something that is going to be 92 * pretty unique. We can base it on the pid, time, hostip */ 93 94#define COOKIE_NAME "Apache" 95 96static void make_cookie(request_rec *r) 97{ 98 cookie_log_state *cls = ap_get_module_config(r->server->module_config, 99 &usertrack_module); 100 char cookiebuf[2 * (sizeof(apr_uint64_t) + sizeof(int)) + 2]; 101 unsigned int random; 102 apr_time_t now = r->request_time ? r->request_time : apr_time_now(); 103 char *new_cookie; 104 cookie_dir_rec *dcfg; 105 106 ap_random_insecure_bytes(&random, sizeof(random)); 107 apr_snprintf(cookiebuf, sizeof(cookiebuf), "%x.%" APR_UINT64_T_HEX_FMT, 108 random, (apr_uint64_t)now); 109 dcfg = ap_get_module_config(r->per_dir_config, &usertrack_module); 110 if (cls->expires) { 111 112 /* Cookie with date; as strftime '%a, %d-%h-%y %H:%M:%S GMT' */ 113 new_cookie = apr_psprintf(r->pool, "%s=%s; path=/", 114 dcfg->cookie_name, cookiebuf); 115 116 if ((dcfg->style == CT_UNSET) || (dcfg->style == CT_NETSCAPE)) { 117 apr_time_exp_t tms; 118 apr_time_exp_gmt(&tms, r->request_time 119 + apr_time_from_sec(cls->expires)); 120 new_cookie = apr_psprintf(r->pool, 121 "%s; expires=%s, " 122 "%.2d-%s-%.2d %.2d:%.2d:%.2d GMT", 123 new_cookie, apr_day_snames[tms.tm_wday], 124 tms.tm_mday, 125 apr_month_snames[tms.tm_mon], 126 tms.tm_year % 100, 127 tms.tm_hour, tms.tm_min, tms.tm_sec); 128 } 129 else { 130 new_cookie = apr_psprintf(r->pool, "%s; max-age=%d", 131 new_cookie, cls->expires); 132 } 133 } 134 else { 135 new_cookie = apr_psprintf(r->pool, "%s=%s; path=/", 136 dcfg->cookie_name, cookiebuf); 137 } 138 if (dcfg->cookie_domain != NULL) { 139 new_cookie = apr_pstrcat(r->pool, new_cookie, "; domain=", 140 dcfg->cookie_domain, 141 (dcfg->style == CT_COOKIE2 142 ? "; version=1" 143 : ""), 144 NULL); 145 } 146 147 apr_table_addn(r->err_headers_out, 148 (dcfg->style == CT_COOKIE2 ? "Set-Cookie2" : "Set-Cookie"), 149 new_cookie); 150 apr_table_setn(r->notes, "cookie", apr_pstrdup(r->pool, cookiebuf)); /* log first time */ 151 return; 152} 153 154/* dcfg->regexp is "^cookie_name=([^;]+)|;[ \t]+cookie_name=([^;]+)", 155 * which has three subexpressions, $0..$2 */ 156#define NUM_SUBS 3 157 158static void set_and_comp_regexp(cookie_dir_rec *dcfg, 159 apr_pool_t *p, 160 const char *cookie_name) 161{ 162 int danger_chars = 0; 163 const char *sp = cookie_name; 164 165 /* The goal is to end up with this regexp, 166 * ^cookie_name=([^;,]+)|[;,][ \t]+cookie_name=([^;,]+) 167 * with cookie_name obviously substituted either 168 * with the real cookie name set by the user in httpd.conf, or with the 169 * default COOKIE_NAME. */ 170 171 /* Anyway, we need to escape the cookie_name before pasting it 172 * into the regex 173 */ 174 while (*sp) { 175 if (!apr_isalnum(*sp)) { 176 ++danger_chars; 177 } 178 ++sp; 179 } 180 181 if (danger_chars) { 182 char *cp; 183 cp = apr_palloc(p, sp - cookie_name + danger_chars + 1); /* 1 == \0 */ 184 sp = cookie_name; 185 cookie_name = cp; 186 while (*sp) { 187 if (!apr_isalnum(*sp)) { 188 *cp++ = '\\'; 189 } 190 *cp++ = *sp++; 191 } 192 *cp = '\0'; 193 } 194 195 dcfg->regexp_string = apr_pstrcat(p, "^", 196 cookie_name, 197 "=([^;,]+)|[;,][ \t]*", 198 cookie_name, 199 "=([^;,]+)", NULL); 200 201 dcfg->regexp = ap_pregcomp(p, dcfg->regexp_string, AP_REG_EXTENDED); 202 ap_assert(dcfg->regexp != NULL); 203} 204 205static int spot_cookie(request_rec *r) 206{ 207 cookie_dir_rec *dcfg = ap_get_module_config(r->per_dir_config, 208 &usertrack_module); 209 const char *cookie_header; 210 ap_regmatch_t regm[NUM_SUBS]; 211 212 /* Do not run in subrequests */ 213 if (!dcfg->enabled || r->main) { 214 return DECLINED; 215 } 216 217 if ((cookie_header = apr_table_get(r->headers_in, "Cookie"))) { 218 if (!ap_regexec(dcfg->regexp, cookie_header, NUM_SUBS, regm, 0)) { 219 char *cookieval = NULL; 220 int err = 0; 221 /* Our regexp, 222 * ^cookie_name=([^;]+)|;[ \t]+cookie_name=([^;]+) 223 * only allows for $1 or $2 to be available. ($0 is always 224 * filled with the entire matched expression, not just 225 * the part in parentheses.) So just check for either one 226 * and assign to cookieval if present. */ 227 if (regm[1].rm_so != -1) { 228 cookieval = ap_pregsub(r->pool, "$1", cookie_header, 229 NUM_SUBS, regm); 230 if (cookieval == NULL) 231 err = 1; 232 } 233 if (regm[2].rm_so != -1) { 234 cookieval = ap_pregsub(r->pool, "$2", cookie_header, 235 NUM_SUBS, regm); 236 if (cookieval == NULL) 237 err = 1; 238 } 239 if (err) { 240 ap_log_rerror(APLOG_MARK, APLOG_CRIT, 0, r, APLOGNO(01499) 241 "Failed to extract cookie value (out of mem?)"); 242 return HTTP_INTERNAL_SERVER_ERROR; 243 } 244 /* Set the cookie in a note, for logging */ 245 apr_table_setn(r->notes, "cookie", cookieval); 246 247 return DECLINED; /* There's already a cookie, no new one */ 248 } 249 } 250 make_cookie(r); 251 return OK; /* We set our cookie */ 252} 253 254static void *make_cookie_log_state(apr_pool_t *p, server_rec *s) 255{ 256 cookie_log_state *cls = 257 (cookie_log_state *) apr_palloc(p, sizeof(cookie_log_state)); 258 259 cls->expires = 0; 260 261 return (void *) cls; 262} 263 264static void *make_cookie_dir(apr_pool_t *p, char *d) 265{ 266 cookie_dir_rec *dcfg; 267 268 dcfg = (cookie_dir_rec *) apr_pcalloc(p, sizeof(cookie_dir_rec)); 269 dcfg->cookie_name = COOKIE_NAME; 270 dcfg->cookie_domain = NULL; 271 dcfg->style = CT_UNSET; 272 dcfg->enabled = 0; 273 274 /* In case the user does not use the CookieName directive, 275 * we need to compile the regexp for the default cookie name. */ 276 set_and_comp_regexp(dcfg, p, COOKIE_NAME); 277 278 return dcfg; 279} 280 281static const char *set_cookie_enable(cmd_parms *cmd, void *mconfig, int arg) 282{ 283 cookie_dir_rec *dcfg = mconfig; 284 285 dcfg->enabled = arg; 286 return NULL; 287} 288 289static const char *set_cookie_exp(cmd_parms *parms, void *dummy, 290 const char *arg) 291{ 292 cookie_log_state *cls; 293 time_t factor, modifier = 0; 294 time_t num = 0; 295 char *word; 296 297 cls = ap_get_module_config(parms->server->module_config, 298 &usertrack_module); 299 /* The simple case first - all numbers (we assume) */ 300 if (apr_isdigit(arg[0]) && apr_isdigit(arg[strlen(arg) - 1])) { 301 cls->expires = atol(arg); 302 return NULL; 303 } 304 305 /* 306 * The harder case - stolen from mod_expires 307 * 308 * CookieExpires "[plus] {<num> <type>}*" 309 */ 310 311 word = ap_getword_conf(parms->pool, &arg); 312 if (!strncasecmp(word, "plus", 1)) { 313 word = ap_getword_conf(parms->pool, &arg); 314 }; 315 316 /* {<num> <type>}* */ 317 while (word[0]) { 318 /* <num> */ 319 if (apr_isdigit(word[0])) 320 num = atoi(word); 321 else 322 return "bad expires code, numeric value expected."; 323 324 /* <type> */ 325 word = ap_getword_conf(parms->pool, &arg); 326 if (!word[0]) 327 return "bad expires code, missing <type>"; 328 329 if (!strncasecmp(word, "years", 1)) 330 factor = 60 * 60 * 24 * 365; 331 else if (!strncasecmp(word, "months", 2)) 332 factor = 60 * 60 * 24 * 30; 333 else if (!strncasecmp(word, "weeks", 1)) 334 factor = 60 * 60 * 24 * 7; 335 else if (!strncasecmp(word, "days", 1)) 336 factor = 60 * 60 * 24; 337 else if (!strncasecmp(word, "hours", 1)) 338 factor = 60 * 60; 339 else if (!strncasecmp(word, "minutes", 2)) 340 factor = 60; 341 else if (!strncasecmp(word, "seconds", 1)) 342 factor = 1; 343 else 344 return "bad expires code, unrecognized type"; 345 346 modifier = modifier + factor * num; 347 348 /* next <num> */ 349 word = ap_getword_conf(parms->pool, &arg); 350 } 351 352 cls->expires = modifier; 353 354 return NULL; 355} 356 357static const char *set_cookie_name(cmd_parms *cmd, void *mconfig, 358 const char *name) 359{ 360 cookie_dir_rec *dcfg = (cookie_dir_rec *) mconfig; 361 362 dcfg->cookie_name = name; 363 364 set_and_comp_regexp(dcfg, cmd->pool, name); 365 366 if (dcfg->regexp == NULL) { 367 return "Regular expression could not be compiled."; 368 } 369 if (dcfg->regexp->re_nsub + 1 != NUM_SUBS) { 370 return apr_pstrcat(cmd->pool, "Invalid cookie name \"", 371 name, "\"", NULL); 372 } 373 374 return NULL; 375} 376 377/* 378 * Set the value for the 'Domain=' attribute. 379 */ 380static const char *set_cookie_domain(cmd_parms *cmd, void *mconfig, 381 const char *name) 382{ 383 cookie_dir_rec *dcfg; 384 385 dcfg = (cookie_dir_rec *) mconfig; 386 387 /* 388 * Apply the restrictions on cookie domain attributes. 389 */ 390 if (!name[0]) { 391 return "CookieDomain values may not be null"; 392 } 393 if (name[0] != '.') { 394 return "CookieDomain values must begin with a dot"; 395 } 396 if (ap_strchr_c(&name[1], '.') == NULL) { 397 return "CookieDomain values must contain at least one embedded dot"; 398 } 399 400 dcfg->cookie_domain = name; 401 return NULL; 402} 403 404/* 405 * Make a note of the cookie style we should use. 406 */ 407static const char *set_cookie_style(cmd_parms *cmd, void *mconfig, 408 const char *name) 409{ 410 cookie_dir_rec *dcfg; 411 412 dcfg = (cookie_dir_rec *) mconfig; 413 414 if (strcasecmp(name, "Netscape") == 0) { 415 dcfg->style = CT_NETSCAPE; 416 } 417 else if ((strcasecmp(name, "Cookie") == 0) 418 || (strcasecmp(name, "RFC2109") == 0)) { 419 dcfg->style = CT_COOKIE; 420 } 421 else if ((strcasecmp(name, "Cookie2") == 0) 422 || (strcasecmp(name, "RFC2965") == 0)) { 423 dcfg->style = CT_COOKIE2; 424 } 425 else { 426 return apr_psprintf(cmd->pool, "Invalid %s keyword: '%s'", 427 cmd->cmd->name, name); 428 } 429 430 return NULL; 431} 432 433static const command_rec cookie_log_cmds[] = { 434 AP_INIT_TAKE1("CookieExpires", set_cookie_exp, NULL, OR_FILEINFO, 435 "an expiry date code"), 436 AP_INIT_TAKE1("CookieDomain", set_cookie_domain, NULL, OR_FILEINFO, 437 "domain to which this cookie applies"), 438 AP_INIT_TAKE1("CookieStyle", set_cookie_style, NULL, OR_FILEINFO, 439 "'Netscape', 'Cookie' (RFC2109), or 'Cookie2' (RFC2965)"), 440 AP_INIT_FLAG("CookieTracking", set_cookie_enable, NULL, OR_FILEINFO, 441 "whether or not to enable cookies"), 442 AP_INIT_TAKE1("CookieName", set_cookie_name, NULL, OR_FILEINFO, 443 "name of the tracking cookie"), 444 {NULL} 445}; 446 447static void register_hooks(apr_pool_t *p) 448{ 449 ap_hook_fixups(spot_cookie,NULL,NULL,APR_HOOK_REALLY_FIRST); 450} 451 452AP_DECLARE_MODULE(usertrack) = { 453 STANDARD20_MODULE_STUFF, 454 make_cookie_dir, /* dir config creater */ 455 NULL, /* dir merger --- default is to override */ 456 make_cookie_log_state, /* server config */ 457 NULL, /* merge server configs */ 458 cookie_log_cmds, /* command apr_table_t */ 459 register_hooks /* register hooks */ 460}; 461