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 "http_log.h"
21#include "util_cookies.h"
22#include "apr_dbd.h"
23#include "mod_dbd.h"
24#include "mpm_common.h"
25
26#define MOD_SESSION_DBD "mod_session_dbd"
27
28module AP_MODULE_DECLARE_DATA session_dbd_module;
29
30/**
31 * Structure to carry the per-dir session config.
32 */
33typedef struct {
34    const char *name;
35    int name_set;
36    const char *name_attrs;
37    const char *name2;
38    int name2_set;
39    const char *name2_attrs;
40    int peruser;
41    int peruser_set;
42    int remove;
43    int remove_set;
44    const char *selectlabel;
45    const char *insertlabel;
46    const char *updatelabel;
47    const char *deletelabel;
48} session_dbd_dir_conf;
49
50/* optional function - look it up once in post_config */
51static ap_dbd_t *(*session_dbd_acquire_fn) (request_rec *) = NULL;
52static void (*session_dbd_prepare_fn) (server_rec *, const char *, const char *) = NULL;
53
54/**
55 * Initialise the database.
56 *
57 * If the mod_dbd module is missing, this method will return APR_EGENERAL.
58 */
59static apr_status_t dbd_init(request_rec *r, const char *query, ap_dbd_t **dbdp,
60                             apr_dbd_prepared_t **statementp)
61{
62    ap_dbd_t *dbd;
63    apr_dbd_prepared_t *statement;
64
65    if (!session_dbd_prepare_fn || !session_dbd_acquire_fn) {
66        session_dbd_prepare_fn = APR_RETRIEVE_OPTIONAL_FN(ap_dbd_prepare);
67        session_dbd_acquire_fn = APR_RETRIEVE_OPTIONAL_FN(ap_dbd_acquire);
68        if (!session_dbd_prepare_fn || !session_dbd_acquire_fn) {
69            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01850)
70                          "You must load mod_dbd to enable AuthDBD functions");
71            return APR_EGENERAL;
72        }
73    }
74
75    dbd = session_dbd_acquire_fn(r);
76    if (!dbd) {
77        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01851)
78                      "failed to acquire database connection");
79        return APR_EGENERAL;
80    }
81
82    statement = apr_hash_get(dbd->prepared, query, APR_HASH_KEY_STRING);
83    if (!statement) {
84        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01852)
85                      "failed to find the prepared statement called '%s'", query);
86        return APR_EGENERAL;
87    }
88
89    *dbdp = dbd;
90    *statementp = statement;
91
92    return APR_SUCCESS;
93}
94
95/**
96 * Load the session by the key specified.
97 */
98static apr_status_t dbd_load(request_rec * r, const char *key, const char **val)
99{
100
101    apr_status_t rv;
102    ap_dbd_t *dbd = NULL;
103    apr_dbd_prepared_t *statement = NULL;
104    apr_dbd_results_t *res = NULL;
105    apr_dbd_row_t *row = NULL;
106    apr_int64_t expiry = (apr_int64_t) apr_time_now();
107
108    session_dbd_dir_conf *conf = ap_get_module_config(r->per_dir_config,
109                                                      &session_dbd_module);
110
111    if (conf->selectlabel == NULL) {
112        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01853)
113                      "no SessionDBDselectlabel has been specified");
114        return APR_EGENERAL;
115    }
116
117    rv = dbd_init(r, conf->selectlabel, &dbd, &statement);
118    if (rv) {
119        return rv;
120    }
121    rv = apr_dbd_pvbselect(dbd->driver, r->pool, dbd->handle, &res, statement,
122                          0, key, &expiry, NULL);
123    if (rv) {
124        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01854)
125                      "query execution error saving session '%s' "
126                      "in database using query '%s': %s", key, conf->selectlabel,
127                      apr_dbd_error(dbd->driver, dbd->handle, rv));
128        return APR_EGENERAL;
129    }
130    for (rv = apr_dbd_get_row(dbd->driver, r->pool, res, &row, -1);
131         rv != -1;
132         rv = apr_dbd_get_row(dbd->driver, r->pool, res, &row, -1)) {
133        if (rv != 0) {
134            ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01855)
135                          "error retrieving results while saving '%s' "
136                          "in database using query '%s': %s", key, conf->selectlabel,
137                           apr_dbd_error(dbd->driver, dbd->handle, rv));
138            return APR_EGENERAL;
139        }
140        if (*val == NULL) {
141            *val = apr_dbd_get_entry(dbd->driver, row, 0);
142        }
143        /* we can't break out here or row won't get cleaned up */
144    }
145
146    return APR_SUCCESS;
147
148}
149
150/**
151 * Load the session by firing off a dbd query.
152 *
153 * If the session is anonymous, the session key will be extracted from
154 * the cookie specified. Failing that, the session key will be extracted
155 * from the GET parameters.
156 *
157 * If the session is keyed by the username, the session will be extracted
158 * by that.
159 *
160 * If no session is found, an empty session will be created.
161 *
162 * On success, this returns OK.
163 */
164static apr_status_t session_dbd_load(request_rec * r, session_rec ** z)
165{
166
167    session_dbd_dir_conf *conf = ap_get_module_config(r->per_dir_config,
168                                                      &session_dbd_module);
169
170    apr_status_t ret = APR_SUCCESS;
171    session_rec *zz = NULL;
172    const char *name = NULL;
173    const char *note = NULL;
174    const char *val = NULL;
175    const char *key = NULL;
176    request_rec *m = r->main ? r->main : r;
177
178    /* is our session in a cookie? */
179    if (conf->name2_set) {
180        name = conf->name2;
181    }
182    else if (conf->name_set) {
183        name = conf->name;
184    }
185    else if (conf->peruser_set && r->user) {
186        name = r->user;
187    }
188    else {
189        return DECLINED;
190    }
191
192    /* first look in the notes */
193    note = apr_pstrcat(r->pool, MOD_SESSION_DBD, name, NULL);
194    zz = (session_rec *)apr_table_get(m->notes, note);
195    if (zz) {
196        *z = zz;
197        return OK;
198    }
199
200    /* load anonymous sessions */
201    if (conf->name_set || conf->name2_set) {
202
203        /* load an RFC2109 or RFC2965 compliant cookie */
204        ap_cookie_read(r, name, &key, conf->remove);
205        if (key) {
206            ret = dbd_load(r, key, &val);
207            if (ret != APR_SUCCESS) {
208                return ret;
209            }
210        }
211
212    }
213
214    /* load named session */
215    else if (conf->peruser) {
216        if (r->user) {
217            ret = dbd_load(r, r->user, &val);
218            if (ret != APR_SUCCESS) {
219                return ret;
220            }
221        }
222    }
223
224    /* otherwise not for us */
225    else {
226        return DECLINED;
227    }
228
229    /* create a new session and return it */
230    zz = (session_rec *) apr_pcalloc(r->pool, sizeof(session_rec));
231    zz->pool = r->pool;
232    zz->entries = apr_table_make(zz->pool, 10);
233    if (key && val) {
234        apr_uuid_t *uuid = apr_pcalloc(zz->pool, sizeof(apr_uuid_t));
235        if (APR_SUCCESS == apr_uuid_parse(uuid, key)) {
236            zz->uuid = uuid;
237        }
238    }
239    zz->encoded = val;
240    *z = zz;
241
242    /* put the session in the notes so we don't have to parse it again */
243    apr_table_setn(m->notes, note, (char *)zz);
244
245    return OK;
246
247}
248
249/**
250 * Save the session by the key specified.
251 */
252static apr_status_t dbd_save(request_rec * r, const char *oldkey,
253        const char *newkey, const char *val, apr_int64_t expiry)
254{
255
256    apr_status_t rv;
257    ap_dbd_t *dbd = NULL;
258    apr_dbd_prepared_t *statement;
259    int rows = 0;
260
261    session_dbd_dir_conf *conf = ap_get_module_config(r->per_dir_config,
262                                                      &session_dbd_module);
263
264    if (conf->updatelabel == NULL) {
265        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01856)
266                      "no SessionDBDupdatelabel has been specified");
267        return APR_EGENERAL;
268    }
269
270    rv = dbd_init(r, conf->updatelabel, &dbd, &statement);
271    if (rv) {
272        return rv;
273    }
274
275    if (oldkey) {
276        rv = apr_dbd_pvbquery(dbd->driver, r->pool, dbd->handle, &rows,
277                statement, val, &expiry, newkey, oldkey, NULL);
278        if (rv) {
279            ap_log_rerror(
280                    APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01857) "query execution error updating session '%s' "
281                    "using database query '%s': %s/%s", oldkey, newkey, conf->updatelabel, apr_dbd_error(dbd->driver, dbd->handle, rv));
282            return APR_EGENERAL;
283        }
284
285        /*
286         * if some rows were updated it means a session existed and was updated,
287         * so we are done.
288         */
289        if (rows != 0) {
290            return APR_SUCCESS;
291        }
292    }
293
294    if (conf->insertlabel == NULL) {
295        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01858)
296                      "no SessionDBDinsertlabel has been specified");
297        return APR_EGENERAL;
298    }
299
300    rv = dbd_init(r, conf->insertlabel, &dbd, &statement);
301    if (rv) {
302        return rv;
303    }
304    rv = apr_dbd_pvbquery(dbd->driver, r->pool, dbd->handle, &rows, statement,
305                          val, &expiry, newkey, NULL);
306    if (rv) {
307        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01859)
308                      "query execution error inserting session '%s' "
309                      "in database with '%s': %s", newkey, conf->insertlabel,
310                      apr_dbd_error(dbd->driver, dbd->handle, rv));
311        return APR_EGENERAL;
312    }
313
314    /*
315     * if some rows were inserted it means a session was inserted, so we are
316     * done.
317     */
318    if (rows != 0) {
319        return APR_SUCCESS;
320    }
321
322    ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01860)
323                  "the session insert query did not cause any rows to be added "
324                  "to the database for session '%s', session not inserted", newkey);
325
326    return APR_EGENERAL;
327
328}
329
330/**
331 * Remove the session by the key specified.
332 */
333static apr_status_t dbd_remove(request_rec * r, const char *key)
334{
335
336    apr_status_t rv;
337    ap_dbd_t *dbd;
338    apr_dbd_prepared_t *statement;
339    int rows = 0;
340
341    session_dbd_dir_conf *conf = ap_get_module_config(r->per_dir_config,
342                                                      &session_dbd_module);
343
344    if (conf->deletelabel == NULL) {
345        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01862)
346                      "no SessionDBDdeletelabel has been specified");
347        return APR_EGENERAL;
348    }
349
350    rv = dbd_init(r, conf->deletelabel, &dbd, &statement);
351    if (rv != APR_SUCCESS) {
352        /* No need to do additional error logging here, it has already
353           been done in dbd_init if needed */
354        return rv;
355    }
356
357    rv = apr_dbd_pvbquery(dbd->driver, r->pool, dbd->handle, &rows, statement,
358                          key, NULL);
359    if (rv != APR_SUCCESS) {
360        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01864)
361                      "query execution error removing session '%s' "
362                      "from database", key);
363        return rv;
364    }
365
366    return APR_SUCCESS;
367
368}
369
370/**
371 * Clean out expired sessions.
372 *
373 * TODO: We need to figure out a way to clean out expired sessions from the database.
374 * The monitor hook doesn't help us that much, as we have no handle into the
375 * server, and so we need to come up with a way to do this safely.
376 */
377static apr_status_t dbd_clean(apr_pool_t *p, server_rec *s)
378{
379
380    return APR_ENOTIMPL;
381
382}
383
384/**
385 * Save the session by firing off a dbd query.
386 *
387 * If the session is anonymous, save the session and write a cookie
388 * containing the uuid.
389 *
390 * If the session is keyed to the username, save the session using
391 * the username as a key.
392 *
393 * On success, this method will return APR_SUCCESS.
394 *
395 * @param r The request pointer.
396 * @param z A pointer to where the session will be written.
397 */
398static apr_status_t session_dbd_save(request_rec * r, session_rec * z)
399{
400
401    apr_status_t ret = APR_SUCCESS;
402    session_dbd_dir_conf *conf = ap_get_module_config(r->per_dir_config,
403                                                      &session_dbd_module);
404
405    /* support anonymous sessions */
406    if (conf->name_set || conf->name2_set) {
407        char *oldkey = NULL, *newkey = NULL;
408
409        /* don't cache pages with a session */
410        apr_table_addn(r->headers_out, "Cache-Control", "no-cache");
411
412        /* if the session is new or changed, make a new session ID */
413        if (z->uuid) {
414            oldkey = apr_pcalloc(r->pool, APR_UUID_FORMATTED_LENGTH + 1);
415            apr_uuid_format(oldkey, z->uuid);
416        }
417        if (z->dirty || !oldkey) {
418            z->uuid = apr_pcalloc(z->pool, sizeof(apr_uuid_t));
419            apr_uuid_get(z->uuid);
420            newkey = apr_pcalloc(r->pool, APR_UUID_FORMATTED_LENGTH + 1);
421            apr_uuid_format(newkey, z->uuid);
422        }
423        else {
424            newkey = oldkey;
425        }
426
427        /* save the session with the uuid as key */
428        if (z->encoded && z->encoded[0]) {
429            ret = dbd_save(r, oldkey, newkey, z->encoded, z->expiry);
430        }
431        else {
432            ret = dbd_remove(r, oldkey);
433        }
434        if (ret != APR_SUCCESS) {
435            return ret;
436        }
437
438        /* create RFC2109 compliant cookie */
439        if (conf->name_set) {
440            ap_cookie_write(r, conf->name, newkey, conf->name_attrs, z->maxage,
441                            r->headers_out, r->err_headers_out, NULL);
442        }
443
444        /* create RFC2965 compliant cookie */
445        if (conf->name2_set) {
446            ap_cookie_write2(r, conf->name2, newkey, conf->name2_attrs, z->maxage,
447                             r->headers_out, r->err_headers_out, NULL);
448        }
449
450        return OK;
451
452    }
453
454    /* save named session */
455    else if (conf->peruser) {
456
457        /* don't cache pages with a session */
458        apr_table_addn(r->headers_out, "Cache-Control", "no-cache");
459
460        if (r->user) {
461            ret = dbd_save(r, r->user, r->user, z->encoded, z->expiry);
462            if (ret != APR_SUCCESS) {
463                return ret;
464            }
465            return OK;
466        }
467        else {
468            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01865)
469               "peruser sessions can only be saved if a user is logged in, "
470                          "session not saved: %s", r->uri);
471        }
472    }
473
474    return DECLINED;
475
476}
477
478/**
479 * This function performs housekeeping on the database, deleting expired
480 * sessions.
481 */
482static int session_dbd_monitor(apr_pool_t *p, server_rec *s)
483{
484    /* TODO handle housekeeping */
485    dbd_clean(p, s);
486    return OK;
487}
488
489
490static void *create_session_dbd_dir_config(apr_pool_t * p, char *dummy)
491{
492    session_dbd_dir_conf *new =
493    (session_dbd_dir_conf *) apr_pcalloc(p, sizeof(session_dbd_dir_conf));
494
495    new->remove = 1;
496
497    new->selectlabel = "selectsession";
498    new->insertlabel = "insertsession";
499    new->updatelabel = "updatesession";
500    new->deletelabel = "deletesession";
501
502    return (void *) new;
503}
504
505static void *merge_session_dbd_dir_config(apr_pool_t * p, void *basev, void *addv)
506{
507    session_dbd_dir_conf *new = (session_dbd_dir_conf *) apr_pcalloc(p, sizeof(session_dbd_dir_conf));
508    session_dbd_dir_conf *add = (session_dbd_dir_conf *) addv;
509    session_dbd_dir_conf *base = (session_dbd_dir_conf *) basev;
510
511    new->name = (add->name_set == 0) ? base->name : add->name;
512    new->name_attrs = (add->name_set == 0) ? base->name_attrs : add->name_attrs;
513    new->name_set = add->name_set || base->name_set;
514    new->name2 = (add->name2_set == 0) ? base->name2 : add->name2;
515    new->name2_attrs = (add->name2_set == 0) ? base->name2_attrs : add->name2_attrs;
516    new->name2_set = add->name2_set || base->name2_set;
517    new->peruser = (add->peruser_set == 0) ? base->peruser : add->peruser;
518    new->peruser_set = add->peruser_set || base->peruser_set;
519    new->remove = (add->remove_set == 0) ? base->remove : add->remove;
520    new->remove_set = add->remove_set || base->remove_set;
521    new->selectlabel = (!add->selectlabel) ? base->selectlabel : add->selectlabel;
522    new->updatelabel = (!add->updatelabel) ? base->updatelabel : add->updatelabel;
523    new->insertlabel = (!add->insertlabel) ? base->insertlabel : add->insertlabel;
524    new->deletelabel = (!add->deletelabel) ? base->deletelabel : add->deletelabel;
525
526    return new;
527}
528
529/**
530 * Sanity check a given string that it exists, is not empty,
531 * and does not contain special characters.
532 */
533static const char *check_string(cmd_parms * cmd, const char *string)
534{
535    if (APR_SUCCESS != ap_cookie_check_string(string)) {
536        return apr_pstrcat(cmd->pool, cmd->directive->directive,
537                           " cannot be empty, or contain '=', ';' or '&'.",
538                           NULL);
539    }
540    return NULL;
541}
542
543static const char *
544     set_dbd_peruser(cmd_parms * parms, void *dconf, int flag)
545{
546    session_dbd_dir_conf *conf = dconf;
547
548    conf->peruser = flag;
549    conf->peruser_set = 1;
550
551    return NULL;
552}
553
554static const char *
555     set_dbd_cookie_remove(cmd_parms * parms, void *dconf, int flag)
556{
557    session_dbd_dir_conf *conf = dconf;
558
559    conf->remove = flag;
560    conf->remove_set = 1;
561
562    return NULL;
563}
564
565static const char *set_cookie_name(cmd_parms * cmd, void *config, const char *args)
566{
567    char *last;
568    char *line = apr_pstrdup(cmd->pool, args);
569    session_dbd_dir_conf *conf = (session_dbd_dir_conf *) config;
570    char *cookie = apr_strtok(line, " \t", &last);
571    conf->name = cookie;
572    conf->name_set = 1;
573    while (apr_isspace(*last)) {
574        last++;
575    }
576    conf->name_attrs = last;
577    return check_string(cmd, cookie);
578}
579
580static const char *set_cookie_name2(cmd_parms * cmd, void *config, const char *args)
581{
582    char *last;
583    char *line = apr_pstrdup(cmd->pool, args);
584    session_dbd_dir_conf *conf = (session_dbd_dir_conf *) config;
585    char *cookie = apr_strtok(line, " \t", &last);
586    conf->name2 = cookie;
587    conf->name2_set = 1;
588    while (apr_isspace(*last)) {
589        last++;
590    }
591    conf->name2_attrs = last;
592    return check_string(cmd, cookie);
593}
594
595static const command_rec session_dbd_cmds[] =
596{
597    AP_INIT_TAKE1("SessionDBDSelectLabel", ap_set_string_slot,
598      (void *) APR_OFFSETOF(session_dbd_dir_conf, selectlabel), RSRC_CONF|OR_AUTHCFG,
599                  "Query label used to select a new session"),
600    AP_INIT_TAKE1("SessionDBDInsertLabel", ap_set_string_slot,
601      (void *) APR_OFFSETOF(session_dbd_dir_conf, insertlabel), RSRC_CONF|OR_AUTHCFG,
602                  "Query label used to insert a new session"),
603    AP_INIT_TAKE1("SessionDBDUpdateLabel", ap_set_string_slot,
604      (void *) APR_OFFSETOF(session_dbd_dir_conf, updatelabel), RSRC_CONF|OR_AUTHCFG,
605                  "Query label used to update an existing session"),
606    AP_INIT_TAKE1("SessionDBDDeleteLabel", ap_set_string_slot,
607      (void *) APR_OFFSETOF(session_dbd_dir_conf, deletelabel), RSRC_CONF|OR_AUTHCFG,
608                  "Query label used to delete an existing session"),
609    AP_INIT_FLAG("SessionDBDPerUser", set_dbd_peruser, NULL, RSRC_CONF|OR_AUTHCFG,
610                 "Save the session per user"),
611    AP_INIT_FLAG("SessionDBDCookieRemove", set_dbd_cookie_remove, NULL, RSRC_CONF|OR_AUTHCFG,
612                 "Remove the session cookie after session load. On by default."),
613    AP_INIT_RAW_ARGS("SessionDBDCookieName", set_cookie_name, NULL, RSRC_CONF|OR_AUTHCFG,
614                 "The name of the RFC2109 cookie carrying the session key"),
615    AP_INIT_RAW_ARGS("SessionDBDCookieName2", set_cookie_name2, NULL, RSRC_CONF|OR_AUTHCFG,
616                 "The name of the RFC2965 cookie carrying the session key"),
617    {NULL}
618};
619
620static void register_hooks(apr_pool_t * p)
621{
622    ap_hook_session_load(session_dbd_load, NULL, NULL, APR_HOOK_MIDDLE);
623    ap_hook_session_save(session_dbd_save, NULL, NULL, APR_HOOK_MIDDLE);
624    ap_hook_monitor(session_dbd_monitor, NULL, NULL, APR_HOOK_MIDDLE);
625}
626
627AP_DECLARE_MODULE(session_dbd) =
628{
629    STANDARD20_MODULE_STUFF,
630    create_session_dbd_dir_config, /* dir config creater */
631    merge_session_dbd_dir_config,  /* dir merger --- default is to
632                                    * override */
633    NULL,                          /* server config */
634    NULL,                          /* merge server config */
635    session_dbd_cmds,              /* command apr_table_t */
636    register_hooks                 /* register hooks */
637};
638