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
68 module AP_MODULE_DECLARE_DATA usertrack_module;
69
70 typedef struct {
71 int always;
72 int expires;
73 } cookie_log_state;
74
75 typedef enum {
76 CT_UNSET,
77 CT_NETSCAPE,
78 CT_COOKIE,
79 CT_COOKIE2
80 } cookie_type_e;
81
82 typedef 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
make_cookie(request_rec * r)96 static 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
set_and_comp_regexp(cookie_dir_rec * dcfg,apr_pool_t * p,const char * cookie_name)158 static 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
spot_cookie(request_rec * r)205 static 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
make_cookie_log_state(apr_pool_t * p,server_rec * s)254 static 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
make_cookie_dir(apr_pool_t * p,char * d)264 static 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
set_cookie_enable(cmd_parms * cmd,void * mconfig,int arg)281 static 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
set_cookie_exp(cmd_parms * parms,void * dummy,const char * arg)289 static 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
set_cookie_name(cmd_parms * cmd,void * mconfig,const char * name)357 static 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 */
set_cookie_domain(cmd_parms * cmd,void * mconfig,const char * name)380 static 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 */
set_cookie_style(cmd_parms * cmd,void * mconfig,const char * name)407 static 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
433 static 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
register_hooks(apr_pool_t * p)447 static void register_hooks(apr_pool_t *p)
448 {
449 ap_hook_fixups(spot_cookie,NULL,NULL,APR_HOOK_REALLY_FIRST);
450 }
451
452 AP_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