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#include "mod_session.h"
18#include "apr_lib.h"
19#include "apr_strings.h"
20#include "util_filter.h"
21#include "http_log.h"
22#include "http_request.h"
23#include "http_protocol.h"
24
25#define SESSION_EXPIRY "expiry"
26#define HTTP_SESSION "HTTP_SESSION"
27
28APR_HOOK_STRUCT(
29                APR_HOOK_LINK(session_load)
30                APR_HOOK_LINK(session_save)
31                APR_HOOK_LINK(session_encode)
32                APR_HOOK_LINK(session_decode)
33)
34APR_IMPLEMENT_EXTERNAL_HOOK_RUN_FIRST(ap, SESSION, int, session_load,
35                      (request_rec * r, session_rec ** z), (r, z), DECLINED)
36APR_IMPLEMENT_EXTERNAL_HOOK_RUN_FIRST(ap, SESSION, int, session_save,
37                       (request_rec * r, session_rec * z), (r, z), DECLINED)
38APR_IMPLEMENT_EXTERNAL_HOOK_RUN_ALL(ap, SESSION, int, session_encode,
39                   (request_rec * r, session_rec * z), (r, z), OK, DECLINED)
40APR_IMPLEMENT_EXTERNAL_HOOK_RUN_ALL(ap, SESSION, int, session_decode,
41                   (request_rec * r, session_rec * z), (r, z), OK, DECLINED)
42
43static int session_identity_encode(request_rec * r, session_rec * z);
44static int session_identity_decode(request_rec * r, session_rec * z);
45static int session_fixups(request_rec * r);
46
47/**
48 * Should the session be included within this URL.
49 *
50 * This function tests whether a session is valid for this URL. It uses the
51 * include and exclude arrays to determine whether they should be included.
52 */
53static int session_included(request_rec * r, session_dir_conf * conf)
54{
55
56    const char **includes = (const char **) conf->includes->elts;
57    const char **excludes = (const char **) conf->excludes->elts;
58    int included = 1;                /* defaults to included */
59    int i;
60
61    if (conf->includes->nelts) {
62        included = 0;
63        for (i = 0; !included && i < conf->includes->nelts; i++) {
64            const char *include = includes[i];
65            if (strncmp(r->uri, include, strlen(include)) == 0) {
66                included = 1;
67            }
68        }
69    }
70
71    if (conf->excludes->nelts) {
72        for (i = 0; included && i < conf->excludes->nelts; i++) {
73            const char *exclude = excludes[i];
74            if (strncmp(r->uri, exclude, strlen(exclude)) == 0) {
75                included = 0;
76            }
77        }
78    }
79
80    return included;
81}
82
83/**
84 * Load the session.
85 *
86 * If the session doesn't exist, a blank one will be created.
87 *
88 * @param r The request
89 * @param z A pointer to where the session will be written.
90 */
91static apr_status_t ap_session_load(request_rec * r, session_rec ** z)
92{
93
94    session_dir_conf *dconf = ap_get_module_config(r->per_dir_config,
95                                                   &session_module);
96    apr_time_t now;
97    session_rec *zz = NULL;
98    int rv = 0;
99
100    /* is the session enabled? */
101    if (!dconf || !dconf->enabled) {
102        return APR_SUCCESS;
103    }
104
105    /* should the session be loaded at all? */
106    if (!session_included(r, dconf)) {
107        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01814)
108                      "excluded by configuration for: %s", r->uri);
109        return APR_SUCCESS;
110    }
111
112    /* load the session from the session hook */
113    rv = ap_run_session_load(r, &zz);
114    if (DECLINED == rv) {
115        ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(01815)
116                      "session is enabled but no session modules have been configured, "
117                      "session not loaded: %s", r->uri);
118        return APR_EGENERAL;
119    }
120    else if (OK != rv) {
121        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01816)
122                      "error while loading the session, "
123                      "session not loaded: %s", r->uri);
124        return rv;
125    }
126
127    /* found a session that hasn't expired? */
128    now = apr_time_now();
129    if (zz) {
130        if (zz->expiry && zz->expiry < now) {
131            zz = NULL;
132        }
133        else {
134            /* having a session we cannot decode is just as good as having
135               none at all */
136            rv = ap_run_session_decode(r, zz);
137            if (OK != rv) {
138                ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01817)
139                              "error while decoding the session, "
140                              "session not loaded: %s", r->uri);
141                zz = NULL;
142            }
143        }
144    }
145
146    /* no luck, create a blank session */
147    if (!zz) {
148        zz = (session_rec *) apr_pcalloc(r->pool, sizeof(session_rec));
149        zz->pool = r->pool;
150        zz->entries = apr_table_make(zz->pool, 10);
151    }
152
153    /* make sure the expiry and maxage are set, if present */
154    if (dconf->maxage) {
155        if (!zz->expiry) {
156            zz->expiry = now + dconf->maxage * APR_USEC_PER_SEC;
157        }
158        zz->maxage = dconf->maxage;
159    }
160
161    *z = zz;
162
163    return APR_SUCCESS;
164
165}
166
167/**
168 * Save the session.
169 *
170 * In most implementations the session is only saved if the dirty flag is
171 * true. This prevents the session being saved unnecessarily.
172 *
173 * @param r The request
174 * @param z A pointer to where the session will be written.
175 */
176static apr_status_t ap_session_save(request_rec * r, session_rec * z)
177{
178    if (z) {
179        apr_time_t now = apr_time_now();
180        int rv = 0;
181
182        session_dir_conf *dconf = ap_get_module_config(r->per_dir_config,
183                                                       &session_module);
184
185        /* sanity checks, should we try save at all? */
186        if (z->written) {
187            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01818)
188                          "attempt made to save the session twice, "
189                          "session not saved: %s", r->uri);
190            return APR_EGENERAL;
191        }
192        if (z->expiry && z->expiry < now) {
193            ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(01819)
194                          "attempt made to save a session when the session had already expired, "
195                          "session not saved: %s", r->uri);
196            return APR_EGENERAL;
197        }
198
199        /* reset the expiry back to maxage, if the expiry is present */
200        if (dconf->maxage) {
201            z->expiry = now + dconf->maxage * APR_USEC_PER_SEC;
202            z->maxage = dconf->maxage;
203        }
204
205        /* reset the expiry before saving if present */
206        if (z->dirty && z->maxage) {
207            z->expiry = now + z->maxage * APR_USEC_PER_SEC;
208        }
209
210        /* encode the session */
211        rv = ap_run_session_encode(r, z);
212        if (OK != rv) {
213            ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01820)
214                          "error while encoding the session, "
215                          "session not saved: %s", r->uri);
216            return rv;
217        }
218
219        /* try the save */
220        rv = ap_run_session_save(r, z);
221        if (DECLINED == rv) {
222            ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(01821)
223                          "session is enabled but no session modules have been configured, "
224                          "session not saved: %s", r->uri);
225            return APR_EGENERAL;
226        }
227        else if (OK != rv) {
228            ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01822)
229                          "error while saving the session, "
230                          "session not saved: %s", r->uri);
231            return rv;
232        }
233        else {
234            z->written = 1;
235        }
236    }
237
238    return APR_SUCCESS;
239
240}
241
242/**
243 * Get a particular value from the session.
244 * @param r The current request.
245 * @param z The current session. If this value is NULL, the session will be
246 * looked up in the request, created if necessary, and saved to the request
247 * notes.
248 * @param key The key to get.
249 * @param value The buffer to write the value to.
250 */
251static apr_status_t ap_session_get(request_rec * r, session_rec * z,
252        const char *key, const char **value)
253{
254    if (!z) {
255        apr_status_t rv;
256        rv = ap_session_load(r, &z);
257        if (APR_SUCCESS != rv) {
258            return rv;
259        }
260    }
261    if (z && z->entries) {
262        *value = apr_table_get(z->entries, key);
263    }
264
265    return OK;
266}
267
268/**
269 * Set a particular value to the session.
270 *
271 * Using this method ensures that the dirty flag is set correctly, so that
272 * the session can be saved efficiently.
273 * @param r The current request.
274 * @param z The current session. If this value is NULL, the session will be
275 * looked up in the request, created if necessary, and saved to the request
276 * notes.
277 * @param key The key to set. The existing key value will be replaced.
278 * @param value The value to set.
279 */
280static apr_status_t ap_session_set(request_rec * r, session_rec * z,
281        const char *key, const char *value)
282{
283    if (!z) {
284        apr_status_t rv;
285        rv = ap_session_load(r, &z);
286        if (APR_SUCCESS != rv) {
287            return rv;
288        }
289    }
290    if (z) {
291        if (value) {
292            apr_table_set(z->entries, key, value);
293        }
294        else {
295            apr_table_unset(z->entries, key);
296        }
297        z->dirty = 1;
298    }
299    return APR_SUCCESS;
300}
301
302static int identity_count(int *count, const char *key, const char *val)
303{
304    *count += strlen(key) * 3 + strlen(val) * 3 + 1;
305    return 1;
306}
307
308static int identity_concat(char *buffer, const char *key, const char *val)
309{
310    char *slider = buffer;
311    int length = strlen(slider);
312    slider += length;
313    if (length) {
314        *slider = '&';
315        slider++;
316    }
317    ap_escape_urlencoded_buffer(slider, key);
318    slider += strlen(slider);
319    *slider = '=';
320    slider++;
321    ap_escape_urlencoded_buffer(slider, val);
322    return 1;
323}
324
325/**
326 * Default identity encoding for the session.
327 *
328 * By default, the name value pairs in the session are URLEncoded, separated
329 * by equals, and then in turn separated by ampersand, in the format of an
330 * html form.
331 *
332 * This was chosen to make it easy for external code to unpack a session,
333 * should there be a need to do so.
334 *
335 * @param r The request pointer.
336 * @param z A pointer to where the session will be written.
337 */
338static apr_status_t session_identity_encode(request_rec * r, session_rec * z)
339{
340
341    char *buffer = NULL;
342    int length = 0;
343    if (z->expiry) {
344        char *expiry = apr_psprintf(z->pool, "%" APR_INT64_T_FMT, z->expiry);
345        apr_table_setn(z->entries, SESSION_EXPIRY, expiry);
346    }
347    apr_table_do((int (*) (void *, const char *, const char *))
348                 identity_count, &length, z->entries, NULL);
349    buffer = apr_pcalloc(r->pool, length + 1);
350    apr_table_do((int (*) (void *, const char *, const char *))
351                 identity_concat, buffer, z->entries, NULL);
352    z->encoded = buffer;
353    return OK;
354
355}
356
357/**
358 * Default identity decoding for the session.
359 *
360 * By default, the name value pairs in the session are URLEncoded, separated
361 * by equals, and then in turn separated by ampersand, in the format of an
362 * html form.
363 *
364 * This was chosen to make it easy for external code to unpack a session,
365 * should there be a need to do so.
366 *
367 * This function reverses that process, and populates the session table.
368 *
369 * Name / value pairs that are not encoded properly are ignored.
370 *
371 * @param r The request pointer.
372 * @param z A pointer to where the session will be written.
373 */
374static apr_status_t session_identity_decode(request_rec * r, session_rec * z)
375{
376
377    char *last = NULL;
378    char *encoded, *pair;
379    const char *sep = "&";
380
381    /* sanity check - anything to decode? */
382    if (!z->encoded) {
383        return OK;
384    }
385
386    /* decode what we have */
387    encoded = apr_pstrdup(r->pool, z->encoded);
388    pair = apr_strtok(encoded, sep, &last);
389    while (pair && pair[0]) {
390        char *plast = NULL;
391        const char *psep = "=";
392        char *key = apr_strtok(pair, psep, &plast);
393        char *val = apr_strtok(NULL, psep, &plast);
394        if (key && *key) {
395            if (!val || !*val) {
396                apr_table_unset(z->entries, key);
397            }
398            else if (!ap_unescape_urlencoded(key) && !ap_unescape_urlencoded(val)) {
399                if (!strcmp(SESSION_EXPIRY, key)) {
400                    z->expiry = (apr_time_t) apr_atoi64(val);
401                }
402                else {
403                    apr_table_set(z->entries, key, val);
404                }
405            }
406        }
407        pair = apr_strtok(NULL, sep, &last);
408    }
409    z->encoded = NULL;
410    return OK;
411
412}
413
414/**
415 * Ensure any changes to the session are committed.
416 *
417 * This is done in an output filter so that our options for where to
418 * store the session can include storing the session within a cookie:
419 * As an HTTP header, the cookie must be set before the output is
420 * written, but after the handler is run.
421 *
422 * NOTE: It is possible for internal redirects to cause more than one
423 * request to be present, and each request might have a session
424 * defined. We need to go through each session in turn, and save each
425 * one.
426 *
427 * The same session might appear in more than one request. The first
428 * attempt to save the session will be called
429 */
430static apr_status_t session_output_filter(ap_filter_t * f,
431        apr_bucket_brigade * in)
432{
433
434    /* save all the sessions in all the requests */
435    request_rec *r = f->r->main;
436    if (!r) {
437        r = f->r;
438    }
439    while (r) {
440        session_rec *z = NULL;
441        session_dir_conf *conf = ap_get_module_config(r->per_dir_config,
442                                                      &session_module);
443
444        /* load the session, or create one if necessary */
445        /* when unset or on error, z will be NULL */
446        ap_session_load(r, &z);
447        if (!z || z->written) {
448            r = r->next;
449            continue;
450        }
451
452        /* if a header was specified, insert the new values from the header */
453        if (conf->header_set) {
454            const char *override = apr_table_get(r->err_headers_out, conf->header);
455            if (!override) {
456                override = apr_table_get(r->headers_out, conf->header);
457            }
458            if (override) {
459                apr_table_unset(r->err_headers_out, conf->header);
460                apr_table_unset(r->headers_out, conf->header);
461                z->encoded = override;
462                z->dirty = 1;
463                session_identity_decode(r, z);
464            }
465        }
466
467        /* save away the session, and we're done */
468        /* when unset or on error, we've complained to the log */
469        ap_session_save(r, z);
470
471        r = r->next;
472    }
473
474    /* remove ourselves from the filter chain */
475    ap_remove_output_filter(f);
476
477    /* send the data up the stack */
478    return ap_pass_brigade(f->next, in);
479
480}
481
482/**
483 * Insert the output filter.
484 */
485static void session_insert_output_filter(request_rec * r)
486{
487    ap_add_output_filter("MOD_SESSION_OUT", NULL, r, r->connection);
488}
489
490/**
491 * Fixups hook.
492 *
493 * Load the session within a fixup - this ensures that the session is
494 * properly loaded prior to the handler being called.
495 *
496 * The fixup is also responsible for injecting the session into the CGI
497 * environment, should the admin have configured it so.
498 *
499 * @param r The request
500 */
501static int session_fixups(request_rec * r)
502{
503    session_dir_conf *conf = ap_get_module_config(r->per_dir_config,
504                                                  &session_module);
505
506    session_rec *z = NULL;
507
508    /* if an error occurs or no session has been configured, we ignore
509     * the broken session and allow it to be recreated from scratch on save
510     * if necessary.
511     */
512    ap_session_load(r, &z);
513
514    if (z && conf->env) {
515        session_identity_encode(r, z);
516        if (z->encoded) {
517            apr_table_set(r->subprocess_env, HTTP_SESSION, z->encoded);
518            z->encoded = NULL;
519        }
520    }
521
522    return OK;
523
524}
525
526
527static void *create_session_dir_config(apr_pool_t * p, char *dummy)
528{
529    session_dir_conf *new =
530    (session_dir_conf *) apr_pcalloc(p, sizeof(session_dir_conf));
531
532    new->includes = apr_array_make(p, 10, sizeof(const char **));
533    new->excludes = apr_array_make(p, 10, sizeof(const char **));
534
535    return (void *) new;
536}
537
538static void *merge_session_dir_config(apr_pool_t * p, void *basev, void *addv)
539{
540    session_dir_conf *new = (session_dir_conf *) apr_pcalloc(p, sizeof(session_dir_conf));
541    session_dir_conf *add = (session_dir_conf *) addv;
542    session_dir_conf *base = (session_dir_conf *) basev;
543
544    new->enabled = (add->enabled_set == 0) ? base->enabled : add->enabled;
545    new->enabled_set = add->enabled_set || base->enabled_set;
546    new->maxage = (add->maxage_set == 0) ? base->maxage : add->maxage;
547    new->maxage_set = add->maxage_set || base->maxage_set;
548    new->header = (add->header_set == 0) ? base->header : add->header;
549    new->header_set = add->header_set || base->header_set;
550    new->env = (add->env_set == 0) ? base->env : add->env;
551    new->env_set = add->env_set || base->env_set;
552    new->includes = apr_array_append(p, base->includes, add->includes);
553    new->excludes = apr_array_append(p, base->excludes, add->excludes);
554
555    return new;
556}
557
558
559static const char *
560     set_session_enable(cmd_parms * parms, void *dconf, int flag)
561{
562    session_dir_conf *conf = dconf;
563
564    conf->enabled = flag;
565    conf->enabled_set = 1;
566
567    return NULL;
568}
569
570static const char *
571     set_session_maxage(cmd_parms * parms, void *dconf, const char *arg)
572{
573    session_dir_conf *conf = dconf;
574
575    conf->maxage = atol(arg);
576    conf->maxage_set = 1;
577
578    return NULL;
579}
580
581static const char *
582     set_session_header(cmd_parms * parms, void *dconf, const char *arg)
583{
584    session_dir_conf *conf = dconf;
585
586    conf->header = arg;
587    conf->header_set = 1;
588
589    return NULL;
590}
591
592static const char *
593     set_session_env(cmd_parms * parms, void *dconf, int flag)
594{
595    session_dir_conf *conf = dconf;
596
597    conf->env = flag;
598    conf->env_set = 1;
599
600    return NULL;
601}
602
603static const char *add_session_include(cmd_parms * cmd, void *dconf, const char *f)
604{
605    session_dir_conf *conf = dconf;
606
607    const char **new = apr_array_push(conf->includes);
608    *new = f;
609
610    return NULL;
611}
612
613static const char *add_session_exclude(cmd_parms * cmd, void *dconf, const char *f)
614{
615    session_dir_conf *conf = dconf;
616
617    const char **new = apr_array_push(conf->excludes);
618    *new = f;
619
620    return NULL;
621}
622
623
624static const command_rec session_cmds[] =
625{
626    AP_INIT_FLAG("Session", set_session_enable, NULL, RSRC_CONF|OR_AUTHCFG,
627                 "on if a session should be maintained for these URLs"),
628    AP_INIT_TAKE1("SessionMaxAge", set_session_maxage, NULL, RSRC_CONF|OR_AUTHCFG,
629                  "length of time for which a session should be valid. Zero to disable"),
630    AP_INIT_TAKE1("SessionHeader", set_session_header, NULL, RSRC_CONF|OR_AUTHCFG,
631                  "output header, if present, whose contents will be injected into the session."),
632    AP_INIT_FLAG("SessionEnv", set_session_env, NULL, RSRC_CONF|OR_AUTHCFG,
633                 "on if a session should be written to the CGI environment. Defaults to off"),
634    AP_INIT_TAKE1("SessionInclude", add_session_include, NULL, RSRC_CONF|OR_AUTHCFG,
635                  "URL prefixes to include in the session. Defaults to all URLs"),
636    AP_INIT_TAKE1("SessionExclude", add_session_exclude, NULL, RSRC_CONF|OR_AUTHCFG,
637                  "URL prefixes to exclude from the session. Defaults to no URLs"),
638    {NULL}
639};
640
641static void register_hooks(apr_pool_t * p)
642{
643    ap_register_output_filter("MOD_SESSION_OUT", session_output_filter,
644                              NULL, AP_FTYPE_CONTENT_SET);
645    ap_hook_insert_filter(session_insert_output_filter, NULL, NULL,
646                          APR_HOOK_MIDDLE);
647    ap_hook_insert_error_filter(session_insert_output_filter,
648                                NULL, NULL, APR_HOOK_MIDDLE);
649    ap_hook_fixups(session_fixups, NULL, NULL, APR_HOOK_MIDDLE);
650    ap_hook_session_encode(session_identity_encode, NULL, NULL,
651                           APR_HOOK_REALLY_FIRST);
652    ap_hook_session_decode(session_identity_decode, NULL, NULL,
653                           APR_HOOK_REALLY_LAST);
654    APR_REGISTER_OPTIONAL_FN(ap_session_get);
655    APR_REGISTER_OPTIONAL_FN(ap_session_set);
656    APR_REGISTER_OPTIONAL_FN(ap_session_load);
657    APR_REGISTER_OPTIONAL_FN(ap_session_save);
658}
659
660AP_DECLARE_MODULE(session) =
661{
662    STANDARD20_MODULE_STUFF,
663    create_session_dir_config,   /* dir config creater */
664    merge_session_dir_config,    /* dir merger --- default is to override */
665    NULL,                        /* server config */
666    NULL,                        /* merge server config */
667    session_cmds,                /* command apr_table_t */
668    register_hooks               /* register hooks */
669};
670