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