| /* Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #include "mod_session.h" |
| #include "apr_lib.h" |
| #include "apr_strings.h" |
| #include "util_filter.h" |
| #include "http_log.h" |
| #include "http_request.h" |
| #include "http_protocol.h" |
| |
| #define SESSION_EXPIRY "expiry" |
| #define HTTP_SESSION "HTTP_SESSION" |
| |
| APR_HOOK_STRUCT( |
| APR_HOOK_LINK(session_load) |
| APR_HOOK_LINK(session_save) |
| APR_HOOK_LINK(session_encode) |
| APR_HOOK_LINK(session_decode) |
| ) |
| APR_IMPLEMENT_EXTERNAL_HOOK_RUN_FIRST(ap, SESSION, int, session_load, |
| (request_rec * r, session_rec ** z), (r, z), DECLINED) |
| APR_IMPLEMENT_EXTERNAL_HOOK_RUN_FIRST(ap, SESSION, int, session_save, |
| (request_rec * r, session_rec * z), (r, z), DECLINED) |
| APR_IMPLEMENT_EXTERNAL_HOOK_RUN_ALL(ap, SESSION, int, session_encode, |
| (request_rec * r, session_rec * z), (r, z), OK, DECLINED) |
| APR_IMPLEMENT_EXTERNAL_HOOK_RUN_ALL(ap, SESSION, int, session_decode, |
| (request_rec * r, session_rec * z), (r, z), OK, DECLINED) |
| |
| static int session_identity_encode(request_rec * r, session_rec * z); |
| static int session_identity_decode(request_rec * r, session_rec * z); |
| static int session_fixups(request_rec * r); |
| |
| /** |
| * Should the session be included within this URL. |
| * |
| * This function tests whether a session is valid for this URL. It uses the |
| * include and exclude arrays to determine whether they should be included. |
| */ |
| static int session_included(request_rec * r, session_dir_conf * conf) |
| { |
| |
| const char **includes = (const char **) conf->includes->elts; |
| const char **excludes = (const char **) conf->excludes->elts; |
| int included = 1; /* defaults to included */ |
| int i; |
| |
| if (conf->includes->nelts) { |
| included = 0; |
| for (i = 0; !included && i < conf->includes->nelts; i++) { |
| const char *include = includes[i]; |
| if (strncmp(r->uri, include, strlen(include)) == 0) { |
| included = 1; |
| } |
| } |
| } |
| |
| if (conf->excludes->nelts) { |
| for (i = 0; included && i < conf->excludes->nelts; i++) { |
| const char *exclude = excludes[i]; |
| if (strncmp(r->uri, exclude, strlen(exclude)) == 0) { |
| included = 0; |
| } |
| } |
| } |
| |
| return included; |
| } |
| |
| /** |
| * Load the session. |
| * |
| * If the session doesn't exist, a blank one will be created. |
| * |
| * @param r The request |
| * @param z A pointer to where the session will be written. |
| */ |
| static apr_status_t ap_session_load(request_rec * r, session_rec ** z) |
| { |
| |
| session_dir_conf *dconf = ap_get_module_config(r->per_dir_config, |
| &session_module); |
| apr_time_t now; |
| session_rec *zz = NULL; |
| int rv = 0; |
| |
| /* is the session enabled? */ |
| if (!dconf || !dconf->enabled) { |
| return APR_SUCCESS; |
| } |
| |
| /* should the session be loaded at all? */ |
| if (!session_included(r, dconf)) { |
| ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01814) |
| "excluded by configuration for: %s", r->uri); |
| return APR_SUCCESS; |
| } |
| |
| /* load the session from the session hook */ |
| rv = ap_run_session_load(r, &zz); |
| if (DECLINED == rv) { |
| ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(01815) |
| "session is enabled but no session modules have been configured, " |
| "session not loaded: %s", r->uri); |
| return APR_EGENERAL; |
| } |
| else if (OK != rv) { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01816) |
| "error while loading the session, " |
| "session not loaded: %s", r->uri); |
| return rv; |
| } |
| |
| /* found a session that hasn't expired? */ |
| now = apr_time_now(); |
| if (zz) { |
| if (zz->expiry && zz->expiry < now) { |
| zz = NULL; |
| } |
| else { |
| /* having a session we cannot decode is just as good as having |
| none at all */ |
| rv = ap_run_session_decode(r, zz); |
| if (OK != rv) { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01817) |
| "error while decoding the session, " |
| "session not loaded: %s", r->uri); |
| zz = NULL; |
| } |
| } |
| } |
| |
| /* no luck, create a blank session */ |
| if (!zz) { |
| zz = (session_rec *) apr_pcalloc(r->pool, sizeof(session_rec)); |
| zz->pool = r->pool; |
| zz->entries = apr_table_make(zz->pool, 10); |
| } |
| |
| /* make sure the expiry and maxage are set, if present */ |
| if (dconf->maxage) { |
| if (!zz->expiry) { |
| zz->expiry = now + dconf->maxage * APR_USEC_PER_SEC; |
| } |
| zz->maxage = dconf->maxage; |
| } |
| |
| *z = zz; |
| |
| return APR_SUCCESS; |
| |
| } |
| |
| /** |
| * Save the session. |
| * |
| * In most implementations the session is only saved if the dirty flag is |
| * true. This prevents the session being saved unnecessarily. |
| * |
| * @param r The request |
| * @param z A pointer to where the session will be written. |
| */ |
| static apr_status_t ap_session_save(request_rec * r, session_rec * z) |
| { |
| if (z) { |
| apr_time_t now = apr_time_now(); |
| apr_time_t initialExpiry = z->expiry; |
| int rv = 0; |
| |
| session_dir_conf *dconf = ap_get_module_config(r->per_dir_config, |
| &session_module); |
| |
| /* sanity checks, should we try save at all? */ |
| if (z->written) { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01818) |
| "attempt made to save the session twice, " |
| "session not saved: %s", r->uri); |
| return APR_EGENERAL; |
| } |
| if (z->expiry && z->expiry < now) { |
| ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(01819) |
| "attempt made to save a session when the session had already expired, " |
| "session not saved: %s", r->uri); |
| return APR_EGENERAL; |
| } |
| |
| /* reset the expiry back to maxage, if the expiry is present */ |
| if (dconf->maxage) { |
| z->expiry = now + dconf->maxage * APR_USEC_PER_SEC; |
| z->maxage = dconf->maxage; |
| } |
| |
| /* reset the expiry before saving if present */ |
| if (z->dirty && z->maxage) { |
| z->expiry = now + z->maxage * APR_USEC_PER_SEC; |
| } |
| |
| /* don't save if the only change is the expiry by a small amount */ |
| if (!z->dirty && dconf->expiry_update_time |
| && (z->expiry - initialExpiry < dconf->expiry_update_time)) { |
| return APR_SUCCESS; |
| } |
| |
| /* also don't save sessions that didn't change at all */ |
| if (!z->dirty && !z->maxage) { |
| return APR_SUCCESS; |
| } |
| |
| /* encode the session */ |
| rv = ap_run_session_encode(r, z); |
| if (OK != rv) { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01820) |
| "error while encoding the session, " |
| "session not saved: %s", r->uri); |
| return rv; |
| } |
| |
| /* try the save */ |
| rv = ap_run_session_save(r, z); |
| if (DECLINED == rv) { |
| ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(01821) |
| "session is enabled but no session modules have been configured, " |
| "session not saved: %s", r->uri); |
| return APR_EGENERAL; |
| } |
| else if (OK != rv) { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01822) |
| "error while saving the session, " |
| "session not saved: %s", r->uri); |
| return rv; |
| } |
| else { |
| z->written = 1; |
| } |
| } |
| |
| return APR_SUCCESS; |
| |
| } |
| |
| /** |
| * Get a particular value from the session. |
| * @param r The current request. |
| * @param z The current session. If this value is NULL, the session will be |
| * looked up in the request, created if necessary, and saved to the request |
| * notes. |
| * @param key The key to get. |
| * @param value The buffer to write the value to. |
| */ |
| static apr_status_t ap_session_get(request_rec * r, session_rec * z, |
| const char *key, const char **value) |
| { |
| if (!z) { |
| apr_status_t rv; |
| rv = ap_session_load(r, &z); |
| if (APR_SUCCESS != rv) { |
| return rv; |
| } |
| } |
| if (z && z->entries) { |
| *value = apr_table_get(z->entries, key); |
| } |
| |
| return OK; |
| } |
| |
| /** |
| * Set a particular value to the session. |
| * |
| * Using this method ensures that the dirty flag is set correctly, so that |
| * the session can be saved efficiently. |
| * @param r The current request. |
| * @param z The current session. If this value is NULL, the session will be |
| * looked up in the request, created if necessary, and saved to the request |
| * notes. |
| * @param key The key to set. The existing key value will be replaced. |
| * @param value The value to set. |
| */ |
| static apr_status_t ap_session_set(request_rec * r, session_rec * z, |
| const char *key, const char *value) |
| { |
| if (!z) { |
| apr_status_t rv; |
| rv = ap_session_load(r, &z); |
| if (APR_SUCCESS != rv) { |
| return rv; |
| } |
| } |
| if (z) { |
| if (value) { |
| apr_table_set(z->entries, key, value); |
| } |
| else { |
| apr_table_unset(z->entries, key); |
| } |
| z->dirty = 1; |
| } |
| return APR_SUCCESS; |
| } |
| |
| static int identity_count(int *count, const char *key, const char *val) |
| { |
| *count += strlen(key) * 3 + strlen(val) * 3 + 1; |
| return 1; |
| } |
| |
| static int identity_concat(char *buffer, const char *key, const char *val) |
| { |
| char *slider = buffer; |
| int length = strlen(slider); |
| slider += length; |
| if (length) { |
| *slider = '&'; |
| slider++; |
| } |
| ap_escape_urlencoded_buffer(slider, key); |
| slider += strlen(slider); |
| *slider = '='; |
| slider++; |
| ap_escape_urlencoded_buffer(slider, val); |
| return 1; |
| } |
| |
| /** |
| * Default identity encoding for the session. |
| * |
| * By default, the name value pairs in the session are URLEncoded, separated |
| * by equals, and then in turn separated by ampersand, in the format of an |
| * html form. |
| * |
| * This was chosen to make it easy for external code to unpack a session, |
| * should there be a need to do so. |
| * |
| * @param r The request pointer. |
| * @param z A pointer to where the session will be written. |
| */ |
| static apr_status_t session_identity_encode(request_rec * r, session_rec * z) |
| { |
| |
| char *buffer = NULL; |
| int length = 0; |
| if (z->expiry) { |
| char *expiry = apr_psprintf(z->pool, "%" APR_INT64_T_FMT, z->expiry); |
| apr_table_setn(z->entries, SESSION_EXPIRY, expiry); |
| } |
| apr_table_do((int (*) (void *, const char *, const char *)) |
| identity_count, &length, z->entries, NULL); |
| buffer = apr_pcalloc(r->pool, length + 1); |
| apr_table_do((int (*) (void *, const char *, const char *)) |
| identity_concat, buffer, z->entries, NULL); |
| z->encoded = buffer; |
| return OK; |
| |
| } |
| |
| /** |
| * Default identity decoding for the session. |
| * |
| * By default, the name value pairs in the session are URLEncoded, separated |
| * by equals, and then in turn separated by ampersand, in the format of an |
| * html form. |
| * |
| * This was chosen to make it easy for external code to unpack a session, |
| * should there be a need to do so. |
| * |
| * This function reverses that process, and populates the session table. |
| * |
| * Name / value pairs that are not encoded properly are ignored. |
| * |
| * @param r The request pointer. |
| * @param z A pointer to where the session will be written. |
| */ |
| static apr_status_t session_identity_decode(request_rec * r, session_rec * z) |
| { |
| |
| char *last = NULL; |
| char *encoded, *pair; |
| const char *sep = "&"; |
| |
| /* sanity check - anything to decode? */ |
| if (!z->encoded) { |
| return OK; |
| } |
| |
| /* decode what we have */ |
| encoded = apr_pstrdup(r->pool, z->encoded); |
| pair = apr_strtok(encoded, sep, &last); |
| while (pair && pair[0]) { |
| char *plast = NULL; |
| const char *psep = "="; |
| char *key = apr_strtok(pair, psep, &plast); |
| char *val = apr_strtok(NULL, psep, &plast); |
| if (key && *key) { |
| if (!val || !*val) { |
| apr_table_unset(z->entries, key); |
| } |
| else if (!ap_unescape_urlencoded(key) && !ap_unescape_urlencoded(val)) { |
| if (!strcmp(SESSION_EXPIRY, key)) { |
| z->expiry = (apr_time_t) apr_atoi64(val); |
| } |
| else { |
| apr_table_set(z->entries, key, val); |
| } |
| } |
| } |
| pair = apr_strtok(NULL, sep, &last); |
| } |
| z->encoded = NULL; |
| return OK; |
| |
| } |
| |
| /** |
| * Ensure any changes to the session are committed. |
| * |
| * This is done in an output filter so that our options for where to |
| * store the session can include storing the session within a cookie: |
| * As an HTTP header, the cookie must be set before the output is |
| * written, but after the handler is run. |
| * |
| * NOTE: It is possible for internal redirects to cause more than one |
| * request to be present, and each request might have a session |
| * defined. We need to go through each session in turn, and save each |
| * one. |
| * |
| * The same session might appear in more than one request. The first |
| * attempt to save the session will be called |
| */ |
| static apr_status_t session_output_filter(ap_filter_t * f, |
| apr_bucket_brigade * in) |
| { |
| |
| /* save all the sessions in all the requests */ |
| request_rec *r = f->r->main; |
| if (!r) { |
| r = f->r; |
| } |
| while (r) { |
| session_rec *z = NULL; |
| session_dir_conf *conf = ap_get_module_config(r->per_dir_config, |
| &session_module); |
| |
| /* load the session, or create one if necessary */ |
| /* when unset or on error, z will be NULL */ |
| ap_session_load(r, &z); |
| if (!z || z->written) { |
| r = r->next; |
| continue; |
| } |
| |
| /* if a header was specified, insert the new values from the header */ |
| if (conf->header_set) { |
| const char *override = apr_table_get(r->err_headers_out, conf->header); |
| if (!override) { |
| override = apr_table_get(r->headers_out, conf->header); |
| } |
| if (override) { |
| apr_table_unset(r->err_headers_out, conf->header); |
| apr_table_unset(r->headers_out, conf->header); |
| z->encoded = override; |
| z->dirty = 1; |
| session_identity_decode(r, z); |
| } |
| } |
| |
| /* save away the session, and we're done */ |
| /* when unset or on error, we've complained to the log */ |
| ap_session_save(r, z); |
| |
| r = r->next; |
| } |
| |
| /* remove ourselves from the filter chain */ |
| ap_remove_output_filter(f); |
| |
| /* send the data up the stack */ |
| return ap_pass_brigade(f->next, in); |
| |
| } |
| |
| /** |
| * Insert the output filter. |
| */ |
| static void session_insert_output_filter(request_rec * r) |
| { |
| ap_add_output_filter("MOD_SESSION_OUT", NULL, r, r->connection); |
| } |
| |
| /** |
| * Fixups hook. |
| * |
| * Load the session within a fixup - this ensures that the session is |
| * properly loaded prior to the handler being called. |
| * |
| * The fixup is also responsible for injecting the session into the CGI |
| * environment, should the admin have configured it so. |
| * |
| * @param r The request |
| */ |
| static int session_fixups(request_rec * r) |
| { |
| session_dir_conf *conf = ap_get_module_config(r->per_dir_config, |
| &session_module); |
| |
| session_rec *z = NULL; |
| |
| /* if an error occurs or no session has been configured, we ignore |
| * the broken session and allow it to be recreated from scratch on save |
| * if necessary. |
| */ |
| ap_session_load(r, &z); |
| |
| if (z && conf->env) { |
| session_identity_encode(r, z); |
| if (z->encoded) { |
| apr_table_set(r->subprocess_env, HTTP_SESSION, z->encoded); |
| z->encoded = NULL; |
| } |
| } |
| |
| return OK; |
| |
| } |
| |
| |
| static void *create_session_dir_config(apr_pool_t * p, char *dummy) |
| { |
| session_dir_conf *new = |
| (session_dir_conf *) apr_pcalloc(p, sizeof(session_dir_conf)); |
| |
| new->includes = apr_array_make(p, 10, sizeof(const char **)); |
| new->excludes = apr_array_make(p, 10, sizeof(const char **)); |
| |
| return (void *) new; |
| } |
| |
| static void *merge_session_dir_config(apr_pool_t * p, void *basev, void *addv) |
| { |
| session_dir_conf *new = (session_dir_conf *) apr_pcalloc(p, sizeof(session_dir_conf)); |
| session_dir_conf *add = (session_dir_conf *) addv; |
| session_dir_conf *base = (session_dir_conf *) basev; |
| |
| new->enabled = (add->enabled_set == 0) ? base->enabled : add->enabled; |
| new->enabled_set = add->enabled_set || base->enabled_set; |
| new->maxage = (add->maxage_set == 0) ? base->maxage : add->maxage; |
| new->maxage_set = add->maxage_set || base->maxage_set; |
| new->header = (add->header_set == 0) ? base->header : add->header; |
| new->header_set = add->header_set || base->header_set; |
| new->env = (add->env_set == 0) ? base->env : add->env; |
| new->env_set = add->env_set || base->env_set; |
| new->includes = apr_array_append(p, base->includes, add->includes); |
| new->excludes = apr_array_append(p, base->excludes, add->excludes); |
| new->expiry_update_time = (add->expiry_update_set == 0) |
| ? base->expiry_update_time |
| : add->expiry_update_time; |
| new->expiry_update_set = add->expiry_update_set || base->expiry_update_set; |
| |
| return new; |
| } |
| |
| |
| static const char * |
| set_session_enable(cmd_parms * parms, void *dconf, int flag) |
| { |
| session_dir_conf *conf = dconf; |
| |
| conf->enabled = flag; |
| conf->enabled_set = 1; |
| |
| return NULL; |
| } |
| |
| static const char * |
| set_session_maxage(cmd_parms * parms, void *dconf, const char *arg) |
| { |
| session_dir_conf *conf = dconf; |
| |
| conf->maxage = atol(arg); |
| conf->maxage_set = 1; |
| |
| return NULL; |
| } |
| |
| static const char * |
| set_session_header(cmd_parms * parms, void *dconf, const char *arg) |
| { |
| session_dir_conf *conf = dconf; |
| |
| conf->header = arg; |
| conf->header_set = 1; |
| |
| return NULL; |
| } |
| |
| static const char * |
| set_session_env(cmd_parms * parms, void *dconf, int flag) |
| { |
| session_dir_conf *conf = dconf; |
| |
| conf->env = flag; |
| conf->env_set = 1; |
| |
| return NULL; |
| } |
| |
| static const char *add_session_include(cmd_parms * cmd, void *dconf, const char *f) |
| { |
| session_dir_conf *conf = dconf; |
| |
| const char **new = apr_array_push(conf->includes); |
| *new = f; |
| |
| return NULL; |
| } |
| |
| static const char *add_session_exclude(cmd_parms * cmd, void *dconf, const char *f) |
| { |
| session_dir_conf *conf = dconf; |
| |
| const char **new = apr_array_push(conf->excludes); |
| *new = f; |
| |
| return NULL; |
| } |
| |
| static const char * |
| set_session_expiry_update(cmd_parms * parms, void *dconf, const char *arg) |
| { |
| session_dir_conf *conf = dconf; |
| |
| conf->expiry_update_time = atoi(arg); |
| if (conf->expiry_update_time < 0) { |
| return "SessionExpiryUpdateInterval must be positive or nul"; |
| } |
| conf->expiry_update_time = apr_time_from_sec(conf->expiry_update_time); |
| conf->expiry_update_set = 1; |
| |
| return NULL; |
| } |
| |
| |
| static const command_rec session_cmds[] = |
| { |
| AP_INIT_FLAG("Session", set_session_enable, NULL, RSRC_CONF|OR_AUTHCFG, |
| "on if a session should be maintained for these URLs"), |
| AP_INIT_TAKE1("SessionMaxAge", set_session_maxage, NULL, RSRC_CONF|OR_AUTHCFG, |
| "length of time for which a session should be valid. Zero to disable"), |
| AP_INIT_TAKE1("SessionHeader", set_session_header, NULL, RSRC_CONF|OR_AUTHCFG, |
| "output header, if present, whose contents will be injected into the session."), |
| AP_INIT_FLAG("SessionEnv", set_session_env, NULL, RSRC_CONF|OR_AUTHCFG, |
| "on if a session should be written to the CGI environment. Defaults to off"), |
| AP_INIT_TAKE1("SessionInclude", add_session_include, NULL, RSRC_CONF|OR_AUTHCFG, |
| "URL prefixes to include in the session. Defaults to all URLs"), |
| AP_INIT_TAKE1("SessionExclude", add_session_exclude, NULL, RSRC_CONF|OR_AUTHCFG, |
| "URL prefixes to exclude from the session. Defaults to no URLs"), |
| AP_INIT_TAKE1("SessionExpiryUpdateInterval", set_session_expiry_update, NULL, RSRC_CONF|OR_AUTHCFG, |
| "time interval for which a session's expiry time may change " |
| "without having to be rewritten. Zero to disable"), |
| {NULL} |
| }; |
| |
| static void register_hooks(apr_pool_t * p) |
| { |
| ap_register_output_filter("MOD_SESSION_OUT", session_output_filter, |
| NULL, AP_FTYPE_CONTENT_SET); |
| ap_hook_insert_filter(session_insert_output_filter, NULL, NULL, |
| APR_HOOK_MIDDLE); |
| ap_hook_insert_error_filter(session_insert_output_filter, |
| NULL, NULL, APR_HOOK_MIDDLE); |
| ap_hook_fixups(session_fixups, NULL, NULL, APR_HOOK_MIDDLE); |
| ap_hook_session_encode(session_identity_encode, NULL, NULL, |
| APR_HOOK_REALLY_FIRST); |
| ap_hook_session_decode(session_identity_decode, NULL, NULL, |
| APR_HOOK_REALLY_LAST); |
| APR_REGISTER_OPTIONAL_FN(ap_session_get); |
| APR_REGISTER_OPTIONAL_FN(ap_session_set); |
| APR_REGISTER_OPTIONAL_FN(ap_session_load); |
| APR_REGISTER_OPTIONAL_FN(ap_session_save); |
| } |
| |
| AP_DECLARE_MODULE(session) = |
| { |
| STANDARD20_MODULE_STUFF, |
| create_session_dir_config, /* dir config creater */ |
| merge_session_dir_config, /* dir merger --- default is to override */ |
| NULL, /* server config */ |
| NULL, /* merge server config */ |
| session_cmds, /* command apr_table_t */ |
| register_hooks /* register hooks */ |
| }; |