/* 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 "apr_lib.h" #include "apr_file_io.h" #include "apr_strings.h" #include "apr_buckets.h" #include "httpd.h" #include "http_config.h" #include "http_log.h" #include "http_core.h" #include "ap_provider.h" #include "ap_socache.h" #include "util_filter.h" #include "util_script.h" #include "util_charset.h" #include "util_mutex.h" #include "mod_cache.h" #include "cache_socache_common.h" /* * mod_cache_socache: Shared Object Cache Based HTTP 1.1 Cache. * * Flow to Find the entry: * Incoming client requests URI /foo/bar/baz * Fetch URI key (may contain Format #1 or Format #2) * If format #1 (Contains a list of Vary Headers): * Use each header name (from .header) with our request values (headers_in) to * regenerate key using HeaderName+HeaderValue+.../foo/bar/baz * re-read in key (must be format #2) * * Format #1: * apr_uint32_t format; * apr_time_t expire; * apr_array_t vary_headers (delimited by CRLF) * * Format #2: * cache_socache_info_t (first sizeof(apr_uint32_t) bytes is the format) * entity name (sobj->name) [length is in cache_socache_info_t->name_len] * r->headers_out (delimited by CRLF) * CRLF * r->headers_in (delimited by CRLF) * CRLF */ module AP_MODULE_DECLARE_DATA cache_socache_module; /* * cache_socache_object_t * Pointed to by cache_object_t::vobj */ typedef struct cache_socache_object_t { apr_pool_t *pool; /* pool */ unsigned char *buffer; /* the cache buffer */ apr_size_t buffer_len; /* size of the buffer */ apr_bucket_brigade *body; /* brigade containing the body, if any */ apr_table_t *headers_in; /* Input headers to save */ apr_table_t *headers_out; /* Output headers to save */ cache_socache_info_t socache_info; /* Header information. */ apr_size_t body_offset; /* offset to the start of the body */ unsigned int newbody :1; /* whether a new body is present */ apr_time_t expire; /* when to expire the entry */ const char *name; /* Requested URI without vary bits - suitable for mortals. */ const char *key; /* On-disk prefix; URI with Vary bits (if present) */ apr_off_t file_size; /* File size of the cached data file */ apr_off_t offset; /* Max size to set aside */ apr_time_t timeout; /* Max time to set aside */ unsigned int done :1; /* Is the attempt to cache complete? */ } cache_socache_object_t; /* * mod_cache_socache configuration */ #define DEFAULT_MAX_FILE_SIZE 100*1024 #define DEFAULT_MAXTIME 86400 #define DEFAULT_MINTIME 600 #define DEFAULT_READSIZE 0 #define DEFAULT_READTIME 0 typedef struct cache_socache_provider_conf { const char *args; ap_socache_provider_t *socache_provider; ap_socache_instance_t *socache_instance; } cache_socache_provider_conf; typedef struct cache_socache_conf { cache_socache_provider_conf *provider; } cache_socache_conf; typedef struct cache_socache_dir_conf { apr_off_t max; /* maximum file size for cached files */ apr_time_t maxtime; /* maximum expiry time */ apr_time_t mintime; /* minimum expiry time */ apr_off_t readsize; /* maximum data to attempt to cache in one go */ apr_time_t readtime; /* maximum time taken to cache in one go */ unsigned int max_set :1; unsigned int maxtime_set :1; unsigned int mintime_set :1; unsigned int readsize_set :1; unsigned int readtime_set :1; } cache_socache_dir_conf; /* Shared object cache and mutex */ static const char * const cache_socache_id = "cache-socache"; static apr_global_mutex_t *socache_mutex = NULL; /* * Local static functions */ static apr_status_t read_array(request_rec *r, apr_array_header_t *arr, unsigned char *buffer, apr_size_t buffer_len, apr_size_t *slider) { apr_size_t val = *slider; while (*slider < buffer_len) { if (buffer[*slider] == '\r') { if (val == *slider) { (*slider)++; return APR_SUCCESS; } *((const char **) apr_array_push(arr)) = apr_pstrndup(r->pool, (const char *) buffer + val, *slider - val); (*slider)++; if (buffer[*slider] == '\n') { (*slider)++; } val = *slider; } else if (buffer[*slider] == '\0') { (*slider)++; return APR_SUCCESS; } else { (*slider)++; } } return APR_EOF; } static apr_status_t store_array(apr_array_header_t *arr, unsigned char *buffer, apr_size_t buffer_len, apr_size_t *slider) { int i, len; const char **elts; elts = (const char **) arr->elts; for (i = 0; i < arr->nelts; i++) { apr_size_t e_len = strlen(elts[i]); if (e_len + 3 >= buffer_len - *slider) { return APR_EOF; } len = apr_snprintf(buffer ? (char *) buffer + *slider : NULL, buffer ? buffer_len - *slider : 0, "%s" CRLF, elts[i]); *slider += len; } if (buffer) { memcpy(buffer + *slider, CRLF, sizeof(CRLF) - 1); } *slider += sizeof(CRLF) - 1; return APR_SUCCESS; } static apr_status_t read_table(cache_handle_t *handle, request_rec *r, apr_table_t *table, unsigned char *buffer, apr_size_t buffer_len, apr_size_t *slider) { apr_size_t key = *slider, colon = 0, len = 0; ; while (*slider < buffer_len) { if (buffer[*slider] == ':') { if (!colon) { colon = *slider; } (*slider)++; } else if (buffer[*slider] == '\r') { len = colon; if (key == *slider) { (*slider)++; if (buffer[*slider] == '\n') { (*slider)++; } return APR_SUCCESS; } if (!colon || buffer[colon++] != ':') { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(02344) "Premature end of cache headers."); return APR_EGENERAL; } while (apr_isspace(buffer[colon])) { colon++; } apr_table_addn(table, apr_pstrndup(r->pool, (const char *) buffer + key, len - key), apr_pstrndup(r->pool, (const char *) buffer + colon, *slider - colon)); (*slider)++; if (buffer[*slider] == '\n') { (*slider)++; } key = *slider; colon = 0; } else if (buffer[*slider] == '\0') { (*slider)++; return APR_SUCCESS; } else { (*slider)++; } } return APR_EOF; } static apr_status_t store_table(apr_table_t *table, unsigned char *buffer, apr_size_t buffer_len, apr_size_t *slider) { int i, len; apr_table_entry_t *elts; elts = (apr_table_entry_t *) apr_table_elts(table)->elts; for (i = 0; i < apr_table_elts(table)->nelts; ++i) { if (elts[i].key != NULL) { apr_size_t key_len = strlen(elts[i].key); apr_size_t val_len = strlen(elts[i].val); if (key_len + val_len + 5 >= buffer_len - *slider) { return APR_EOF; } len = apr_snprintf(buffer ? (char *) buffer + *slider : NULL, buffer ? buffer_len - *slider : 0, "%s: %s" CRLF, elts[i].key, elts[i].val); *slider += len; } } if (3 >= buffer_len - *slider) { return APR_EOF; } if (buffer) { memcpy(buffer + *slider, CRLF, sizeof(CRLF) - 1); } *slider += sizeof(CRLF) - 1; return APR_SUCCESS; } static const char* regen_key(apr_pool_t *p, apr_table_t *headers, apr_array_header_t *varray, const char *oldkey) { struct iovec *iov; int i, k; int nvec; const char *header; const char **elts; nvec = (varray->nelts * 2) + 1; iov = apr_palloc(p, sizeof(struct iovec) * nvec); elts = (const char **) varray->elts; /* TODO: * - Handle multiple-value headers better. (sort them?) * - Handle Case in-sensitive Values better. * This isn't the end of the world, since it just lowers the cache * hit rate, but it would be nice to fix. * * The majority are case insenstive if they are values (encoding etc). * Most of rfc2616 is case insensitive on header contents. * * So the better solution may be to identify headers which should be * treated case-sensitive? * HTTP URI's (3.2.3) [host and scheme are insensitive] * HTTP method (5.1.1) * HTTP-date values (3.3.1) * 3.7 Media Types [exerpt] * The type, subtype, and parameter attribute names are case- * insensitive. Parameter values might or might not be case-sensitive, * depending on the semantics of the parameter name. * 4.20 Except [exerpt] * Comparison of expectation values is case-insensitive for unquoted * tokens (including the 100-continue token), and is case-sensitive for * quoted-string expectation-extensions. */ for (i = 0, k = 0; i < varray->nelts; i++) { header = apr_table_get(headers, elts[i]); if (!header) { header = ""; } iov[k].iov_base = (char*) elts[i]; iov[k].iov_len = strlen(elts[i]); k++; iov[k].iov_base = (char*) header; iov[k].iov_len = strlen(header); k++; } iov[k].iov_base = (char*) oldkey; iov[k].iov_len = strlen(oldkey); k++; return apr_pstrcatv(p, iov, k, NULL); } static int array_alphasort(const void *fn1, const void *fn2) { return strcmp(*(char**) fn1, *(char**) fn2); } static void tokens_to_array(apr_pool_t *p, const char *data, apr_array_header_t *arr) { char *token; while ((token = ap_get_list_item(p, &data)) != NULL) { *((const char **) apr_array_push(arr)) = token; } /* Sort it so that "Vary: A, B" and "Vary: B, A" are stored the same. */ qsort((void *) arr->elts, arr->nelts, sizeof(char *), array_alphasort); } /* * Hook and mod_cache callback functions */ static int create_entity(cache_handle_t *h, request_rec *r, const char *key, apr_off_t len, apr_bucket_brigade *bb) { cache_socache_dir_conf *dconf = ap_get_module_config(r->per_dir_config, &cache_socache_module); cache_socache_conf *conf = ap_get_module_config(r->server->module_config, &cache_socache_module); cache_object_t *obj; cache_socache_object_t *sobj; apr_size_t total; if (conf->provider == NULL) { return DECLINED; } /* we don't support caching of range requests (yet) */ /* TODO: but we could */ if (r->status == HTTP_PARTIAL_CONTENT) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02345) "URL %s partial content response not cached", key); return DECLINED; } /* * We have a chicken and egg problem. We don't know until we * attempt to store_headers just how big the response will be * and whether it will fit in the cache limits set. But we * need to make a decision now as to whether we plan to try. * If we make the wrong decision, we could prevent another * cache implementation, such as cache_disk, from getting the * opportunity to cache, and that would be unfortunate. * * In a series of tests, from cheapest to most expensive, * decide whether or not to ignore this attempt to cache, * with a small margin just to be sure. */ if (len < 0) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02346) "URL '%s' had no explicit size, ignoring", key); return DECLINED; } if (len > dconf->max) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02347) "URL '%s' body larger than limit, ignoring " "(%" APR_OFF_T_FMT " > %" APR_OFF_T_FMT ")", key, len, dconf->max); return DECLINED; } /* estimate the total cached size, given current headers */ total = len + sizeof(cache_socache_info_t) + strlen(key); if (APR_SUCCESS != store_table(r->headers_out, NULL, dconf->max, &total) || APR_SUCCESS != store_table(r->headers_in, NULL, dconf->max, &total)) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02348) "URL '%s' estimated headers size larger than limit, ignoring " "(%" APR_SIZE_T_FMT " > %" APR_OFF_T_FMT ")", key, total, dconf->max); return DECLINED; } if (total >= dconf->max) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02349) "URL '%s' body and headers larger than limit, ignoring " "(%" APR_OFF_T_FMT " > %" APR_OFF_T_FMT ")", key, len, dconf->max); return DECLINED; } /* Allocate and initialize cache_object_t and cache_socache_object_t */ h->cache_obj = obj = apr_pcalloc(r->pool, sizeof(*obj)); obj->vobj = sobj = apr_pcalloc(r->pool, sizeof(*sobj)); obj->key = apr_pstrdup(r->pool, key); sobj->key = obj->key; sobj->name = obj->key; return OK; } static int open_entity(cache_handle_t *h, request_rec *r, const char *key) { cache_socache_dir_conf *dconf = ap_get_module_config(r->per_dir_config, &cache_socache_module); cache_socache_conf *conf = ap_get_module_config(r->server->module_config, &cache_socache_module); apr_uint32_t format; apr_size_t slider; unsigned int buffer_len; const char *nkey; apr_status_t rc; cache_object_t *obj; cache_info *info; cache_socache_object_t *sobj; apr_size_t len; nkey = NULL; h->cache_obj = NULL; if (!conf->provider || !conf->provider->socache_instance) { return DECLINED; } /* Create and init the cache object */ obj = apr_pcalloc(r->pool, sizeof(cache_object_t)); sobj = apr_pcalloc(r->pool, sizeof(cache_socache_object_t)); info = &(obj->info); /* Create a temporary pool for the buffer, and destroy it if something * goes wrong so we don't have large buffers of unused memory hanging * about for the lifetime of the response. */ apr_pool_create(&sobj->pool, r->pool); sobj->buffer = apr_palloc(sobj->pool, dconf->max + 1); sobj->buffer_len = dconf->max + 1; /* attempt to retrieve the cached entry */ if (socache_mutex) { apr_status_t status = apr_global_mutex_lock(socache_mutex); if (status != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r, APLOGNO(02350) "could not acquire lock, ignoring: %s", obj->key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return DECLINED; } } buffer_len = sobj->buffer_len; rc = conf->provider->socache_provider->retrieve( conf->provider->socache_instance, r->server, (unsigned char *) key, strlen(key), sobj->buffer, &buffer_len, r->pool); if (socache_mutex) { apr_status_t status = apr_global_mutex_unlock(socache_mutex); if (status != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r, APLOGNO(02351) "could not release lock, ignoring: %s", obj->key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return DECLINED; } } if (rc != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rc, r, APLOGNO(02352) "Key not found in cache: %s", key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return DECLINED; } if (buffer_len >= sobj->buffer_len) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rc, r, APLOGNO(02353) "Key found in cache but too big, ignoring: %s", key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return DECLINED; } /* read the format from the cache file */ memcpy(&format, sobj->buffer, sizeof(format)); slider = sizeof(format); if (format == CACHE_SOCACHE_VARY_FORMAT_VERSION) { apr_array_header_t* varray; apr_time_t expire; memcpy(&expire, sobj->buffer + slider, sizeof(expire)); slider += sizeof(expire); varray = apr_array_make(r->pool, 5, sizeof(char*)); rc = read_array(r, varray, sobj->buffer, buffer_len, &slider); if (rc != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r, APLOGNO(02354) "Cannot parse vary entry for key: %s", key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return DECLINED; } nkey = regen_key(r->pool, r->headers_in, varray, key); /* attempt to retrieve the cached entry */ if (socache_mutex) { apr_status_t status = apr_global_mutex_lock(socache_mutex); if (status != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r, APLOGNO(02355) "could not acquire lock, ignoring: %s", obj->key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return DECLINED; } } buffer_len = sobj->buffer_len; rc = conf->provider->socache_provider->retrieve( conf->provider->socache_instance, r->server, (unsigned char *) nkey, strlen(nkey), sobj->buffer, &buffer_len, r->pool); if (socache_mutex) { apr_status_t status = apr_global_mutex_unlock(socache_mutex); if (status != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r, APLOGNO(02356) "could not release lock, ignoring: %s", obj->key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return DECLINED; } } if (rc != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rc, r, APLOGNO(02357) "Key not found in cache: %s", key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return DECLINED; } if (buffer_len >= sobj->buffer_len) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rc, r, APLOGNO(02358) "Key found in cache but too big, ignoring: %s", key); goto fail; } } else if (format != CACHE_SOCACHE_DISK_FORMAT_VERSION) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(02359) "Key '%s' found in cache has version %d, expected %d, ignoring", key, format, CACHE_SOCACHE_DISK_FORMAT_VERSION); goto fail; } else { nkey = key; } obj->key = nkey; sobj->key = nkey; sobj->name = key; if (buffer_len >= sizeof(cache_socache_info_t)) { memcpy(&sobj->socache_info, sobj->buffer, sizeof(cache_socache_info_t)); } else { ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r, APLOGNO(02360) "Cache entry for key '%s' too short, removing", nkey); goto fail; } slider = sizeof(cache_socache_info_t); /* Store it away so we can get it later. */ info->status = sobj->socache_info.status; info->date = sobj->socache_info.date; info->expire = sobj->socache_info.expire; info->request_time = sobj->socache_info.request_time; info->response_time = sobj->socache_info.response_time; memcpy(&info->control, &sobj->socache_info.control, sizeof(cache_control_t)); if (sobj->socache_info.name_len <= buffer_len - slider) { if (strncmp((const char *) sobj->buffer + slider, sobj->name, sobj->socache_info.name_len)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r, APLOGNO(02361) "Cache entry for key '%s' URL mismatch, ignoring", nkey); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return DECLINED; } slider += sobj->socache_info.name_len; } else { ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r, APLOGNO(02362) "Cache entry for key '%s' too short, removing", nkey); goto fail; } /* Is this a cached HEAD request? */ if (sobj->socache_info.header_only && !r->header_only) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r, APLOGNO(02363) "HEAD request cached, non-HEAD requested, ignoring: %s", sobj->key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return DECLINED; } h->req_hdrs = apr_table_make(r->pool, 20); h->resp_hdrs = apr_table_make(r->pool, 20); /* Call routine to read the header lines/status line */ if (APR_SUCCESS != read_table(h, r, h->resp_hdrs, sobj->buffer, buffer_len, &slider)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r, APLOGNO(02364) "Cache entry for key '%s' response headers unreadable, removing", nkey); goto fail; } if (APR_SUCCESS != read_table(h, r, h->req_hdrs, sobj->buffer, buffer_len, &slider)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r, APLOGNO(02365) "Cache entry for key '%s' request headers unreadable, removing", nkey); goto fail; } /* Retrieve the body if we have one */ sobj->body = apr_brigade_create(r->pool, r->connection->bucket_alloc); len = buffer_len - slider; /* * Optimisation: if the body is small, we want to make a * copy of the body and free the temporary pool, as we * don't want large blocks of unused memory hanging around * to the end of the response. In contrast, if the body is * large, we would rather leave the body where it is in the * temporary pool, and save ourselves the copy. */ if (len * 2 > dconf->max) { apr_bucket *e; /* large - use the brigade as is, we're done */ e = apr_bucket_immortal_create((const char *) sobj->buffer + slider, len, r->connection->bucket_alloc); APR_BRIGADE_INSERT_TAIL(sobj->body, e); } else { /* small - make a copy of the data... */ apr_brigade_write(sobj->body, NULL, NULL, (const char *) sobj->buffer + slider, len); /* ...and get rid of the large memory buffer */ apr_pool_destroy(sobj->pool); sobj->pool = NULL; } /* make the configuration stick */ h->cache_obj = obj; obj->vobj = sobj; return OK; fail: if (socache_mutex) { apr_status_t status = apr_global_mutex_lock(socache_mutex); if (status != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r, APLOGNO(02366) "could not acquire lock, ignoring: %s", obj->key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return DECLINED; } } conf->provider->socache_provider->remove( conf->provider->socache_instance, r->server, (unsigned char *) nkey, strlen(nkey), r->pool); if (socache_mutex) { apr_status_t status = apr_global_mutex_unlock(socache_mutex); if (status != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r, APLOGNO(02367) "could not release lock, ignoring: %s", obj->key); } } apr_pool_destroy(sobj->pool); sobj->pool = NULL; return DECLINED; } static int remove_entity(cache_handle_t *h) { /* Null out the cache object pointer so next time we start from scratch */ h->cache_obj = NULL; return OK; } static int remove_url(cache_handle_t *h, request_rec *r) { cache_socache_conf *conf = ap_get_module_config(r->server->module_config, &cache_socache_module); cache_socache_object_t *sobj; sobj = (cache_socache_object_t *) h->cache_obj->vobj; if (!sobj) { return DECLINED; } /* Remove the key from the cache */ if (socache_mutex) { apr_status_t status = apr_global_mutex_lock(socache_mutex); if (status != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r, APLOGNO(02368) "could not acquire lock, ignoring: %s", sobj->key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return DECLINED; } } conf->provider->socache_provider->remove(conf->provider->socache_instance, r->server, (unsigned char *) sobj->key, strlen(sobj->key), r->pool); if (socache_mutex) { apr_status_t status = apr_global_mutex_unlock(socache_mutex); if (status != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r, APLOGNO(02369) "could not release lock, ignoring: %s", sobj->key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return DECLINED; } } return OK; } static apr_status_t recall_headers(cache_handle_t *h, request_rec *r) { /* we recalled the headers during open_entity, so do nothing */ return APR_SUCCESS; } static apr_status_t recall_body(cache_handle_t *h, apr_pool_t *p, apr_bucket_brigade *bb) { cache_socache_object_t *sobj = (cache_socache_object_t*) h->cache_obj->vobj; apr_bucket *e; e = APR_BRIGADE_FIRST(sobj->body); if (e != APR_BRIGADE_SENTINEL(sobj->body)) { APR_BUCKET_REMOVE(e); APR_BRIGADE_INSERT_TAIL(bb, e); } return APR_SUCCESS; } static apr_status_t store_headers(cache_handle_t *h, request_rec *r, cache_info *info) { cache_socache_dir_conf *dconf = ap_get_module_config(r->per_dir_config, &cache_socache_module); cache_socache_conf *conf = ap_get_module_config(r->server->module_config, &cache_socache_module); apr_size_t slider; apr_status_t rv; cache_object_t *obj = h->cache_obj; cache_socache_object_t *sobj = (cache_socache_object_t*) obj->vobj; cache_socache_info_t *socache_info; memcpy(&h->cache_obj->info, info, sizeof(cache_info)); if (r->headers_out) { sobj->headers_out = ap_cache_cacheable_headers_out(r); } if (r->headers_in) { sobj->headers_in = ap_cache_cacheable_headers_in(r); } sobj->expire = obj->info.expire > r->request_time + dconf->maxtime ? r->request_time + dconf->maxtime : obj->info.expire + dconf->mintime; apr_pool_create(&sobj->pool, r->pool); sobj->buffer = apr_palloc(sobj->pool, dconf->max); sobj->buffer_len = dconf->max; socache_info = (cache_socache_info_t *) sobj->buffer; if (sobj->headers_out) { const char *vary; vary = apr_table_get(sobj->headers_out, "Vary"); if (vary) { apr_array_header_t* varray; apr_uint32_t format = CACHE_SOCACHE_VARY_FORMAT_VERSION; memcpy(sobj->buffer, &format, sizeof(format)); slider = sizeof(format); memcpy(sobj->buffer + slider, &obj->info.expire, sizeof(obj->info.expire)); slider += sizeof(obj->info.expire); varray = apr_array_make(r->pool, 6, sizeof(char*)); tokens_to_array(r->pool, vary, varray); if (APR_SUCCESS != (rv = store_array(varray, sobj->buffer, sobj->buffer_len, &slider))) { ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(02370) "buffer too small for Vary array, caching aborted: %s", obj->key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return rv; } if (socache_mutex) { apr_status_t status = apr_global_mutex_lock(socache_mutex); if (status != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r, APLOGNO(02371) "could not acquire lock, ignoring: %s", obj->key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return status; } } rv = conf->provider->socache_provider->store( conf->provider->socache_instance, r->server, (unsigned char *) obj->key, strlen(obj->key), sobj->expire, (unsigned char *) sobj->buffer, (unsigned int) slider, sobj->pool); if (socache_mutex) { apr_status_t status = apr_global_mutex_unlock(socache_mutex); if (status != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r, APLOGNO(02372) "could not release lock, ignoring: %s", obj->key); } } if (rv != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rv, r, APLOGNO(02373) "Vary not written to cache, ignoring: %s", obj->key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return rv; } obj->key = sobj->key = regen_key(r->pool, sobj->headers_in, varray, sobj->name); } } socache_info->format = CACHE_SOCACHE_DISK_FORMAT_VERSION; socache_info->date = obj->info.date; socache_info->expire = obj->info.expire; socache_info->entity_version = sobj->socache_info.entity_version++; socache_info->request_time = obj->info.request_time; socache_info->response_time = obj->info.response_time; socache_info->status = obj->info.status; if (r->header_only && r->status != HTTP_NOT_MODIFIED) { socache_info->header_only = 1; } else { socache_info->header_only = sobj->socache_info.header_only; } socache_info->name_len = strlen(sobj->name); memcpy(&socache_info->control, &obj->info.control, sizeof(cache_control_t)); slider = sizeof(cache_socache_info_t); if (slider + socache_info->name_len >= sobj->buffer_len) { ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(02374) "cache buffer too small for name: %s", sobj->name); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return APR_EGENERAL; } memcpy(sobj->buffer + slider, sobj->name, socache_info->name_len); slider += socache_info->name_len; if (sobj->headers_out) { if (APR_SUCCESS != store_table(sobj->headers_out, sobj->buffer, sobj->buffer_len, &slider)) { ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(02375) "out-headers didn't fit in buffer: %s", sobj->name); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return APR_EGENERAL; } } /* Parse the vary header and dump those fields from the headers_in. */ /* TODO: Make call to the same thing cache_select calls to crack Vary. */ if (sobj->headers_in) { if (APR_SUCCESS != store_table(sobj->headers_in, sobj->buffer, sobj->buffer_len, &slider)) { ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(02376) "in-headers didn't fit in buffer %s", sobj->key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return APR_EGENERAL; } } sobj->body_offset = slider; return APR_SUCCESS; } static apr_status_t store_body(cache_handle_t *h, request_rec *r, apr_bucket_brigade *in, apr_bucket_brigade *out) { apr_bucket *e; apr_status_t rv = APR_SUCCESS; cache_socache_object_t *sobj = (cache_socache_object_t *) h->cache_obj->vobj; cache_socache_dir_conf *dconf = ap_get_module_config(r->per_dir_config, &cache_socache_module); int seen_eos = 0; if (!sobj->offset) { sobj->offset = dconf->readsize; } if (!sobj->timeout && dconf->readtime) { sobj->timeout = apr_time_now() + dconf->readtime; } if (!sobj->newbody) { if (sobj->body) { apr_brigade_cleanup(sobj->body); } else { sobj->body = apr_brigade_create(r->pool, r->connection->bucket_alloc); } sobj->newbody = 1; } if (sobj->offset) { apr_brigade_partition(in, sobj->offset, &e); } while (APR_SUCCESS == rv && !APR_BRIGADE_EMPTY(in)) { const char *str; apr_size_t length; e = APR_BRIGADE_FIRST(in); /* are we done completely? if so, pass any trailing buckets right through */ if (sobj->done || !sobj->pool) { APR_BUCKET_REMOVE(e); APR_BRIGADE_INSERT_TAIL(out, e); continue; } /* have we seen eos yet? */ if (APR_BUCKET_IS_EOS(e)) { seen_eos = 1; sobj->done = 1; APR_BUCKET_REMOVE(e); APR_BRIGADE_INSERT_TAIL(out, e); break; } /* honour flush buckets, we'll get called again */ if (APR_BUCKET_IS_FLUSH(e)) { APR_BUCKET_REMOVE(e); APR_BRIGADE_INSERT_TAIL(out, e); break; } /* metadata buckets are preserved as is */ if (APR_BUCKET_IS_METADATA(e)) { APR_BUCKET_REMOVE(e); APR_BRIGADE_INSERT_TAIL(out, e); continue; } /* read the bucket, write to the cache */ rv = apr_bucket_read(e, &str, &length, APR_BLOCK_READ); APR_BUCKET_REMOVE(e); APR_BRIGADE_INSERT_TAIL(out, e); if (rv != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(02377) "Error when reading bucket for URL %s", h->cache_obj->key); /* Remove the intermediate cache file and return non-APR_SUCCESS */ apr_pool_destroy(sobj->pool); sobj->pool = NULL; return rv; } /* don't write empty buckets to the cache */ if (!length) { continue; } sobj->file_size += length; if (sobj->file_size >= sobj->buffer_len - sobj->body_offset) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02378) "URL %s failed the buffer size check " "(%" APR_OFF_T_FMT ">=%" APR_SIZE_T_FMT ")", h->cache_obj->key, sobj->file_size, sobj->buffer_len - sobj->body_offset); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return APR_EGENERAL; } rv = apr_bucket_copy(e, &e); if (rv != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(02379) "Error when copying bucket for URL %s", h->cache_obj->key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return rv; } APR_BRIGADE_INSERT_TAIL(sobj->body, e); /* have we reached the limit of how much we're prepared to write in one * go? If so, leave, we'll get called again. This prevents us from trying * to swallow too much data at once, or taking so long to write the data * the client times out. */ sobj->offset -= length; if (sobj->offset <= 0) { sobj->offset = 0; break; } if ((dconf->readtime && apr_time_now() > sobj->timeout)) { sobj->timeout = 0; break; } } /* Was this the final bucket? If yes, perform sanity checks. */ if (seen_eos) { const char *cl_header = apr_table_get(r->headers_out, "Content-Length"); if (r->connection->aborted || r->no_cache) { ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO(02380) "Discarding body for URL %s " "because connection has been aborted.", h->cache_obj->key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return APR_EGENERAL; } if (cl_header) { apr_int64_t cl = apr_atoi64(cl_header); if ((errno == 0) && (sobj->file_size != cl)) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02381) "URL %s didn't receive complete response, not caching", h->cache_obj->key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return APR_EGENERAL; } } /* All checks were fine, we're good to go when the commit comes */ } return APR_SUCCESS; } static apr_status_t commit_entity(cache_handle_t *h, request_rec *r) { cache_socache_conf *conf = ap_get_module_config(r->server->module_config, &cache_socache_module); cache_object_t *obj = h->cache_obj; cache_socache_object_t *sobj = (cache_socache_object_t *) obj->vobj; apr_status_t rv; apr_size_t len; /* flatten the body into the buffer */ len = sobj->buffer_len - sobj->body_offset; rv = apr_brigade_flatten(sobj->body, (char *) sobj->buffer + sobj->body_offset, &len); if (APR_SUCCESS != rv) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(02382) "could not flatten brigade, not caching: %s", sobj->key); goto fail; } if (len >= sobj->buffer_len - sobj->body_offset) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(02383) "body too big for the cache buffer, not caching: %s", h->cache_obj->key); goto fail; } if (socache_mutex) { apr_status_t status = apr_global_mutex_lock(socache_mutex); if (status != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r, APLOGNO(02384) "could not acquire lock, ignoring: %s", obj->key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return rv; } } rv = conf->provider->socache_provider->store( conf->provider->socache_instance, r->server, (unsigned char *) sobj->key, strlen(sobj->key), sobj->expire, sobj->buffer, (unsigned int) sobj->body_offset + len, sobj->pool); if (socache_mutex) { apr_status_t status = apr_global_mutex_unlock(socache_mutex); if (status != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r, APLOGNO(02385) "could not release lock, ignoring: %s", obj->key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return DECLINED; } } if (rv != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_WARNING, rv, r, APLOGNO(02386) "could not write to cache, ignoring: %s", sobj->key); goto fail; } ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02387) "commit_entity: Headers and body for URL %s cached for maximum of %d seconds.", sobj->name, (apr_uint32_t)apr_time_sec(sobj->expire - r->request_time)); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return APR_SUCCESS; fail: /* For safety, remove any existing entry on failure, just in case it could not * be revalidated successfully. */ if (socache_mutex) { apr_status_t status = apr_global_mutex_lock(socache_mutex); if (status != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r, APLOGNO(02388) "could not acquire lock, ignoring: %s", obj->key); apr_pool_destroy(sobj->pool); sobj->pool = NULL; return rv; } } conf->provider->socache_provider->remove(conf->provider->socache_instance, r->server, (unsigned char *) sobj->key, strlen(sobj->key), r->pool); if (socache_mutex) { apr_status_t status = apr_global_mutex_unlock(socache_mutex); if (status != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r, APLOGNO(02389) "could not release lock, ignoring: %s", obj->key); } } apr_pool_destroy(sobj->pool); sobj->pool = NULL; return rv; } static apr_status_t invalidate_entity(cache_handle_t *h, request_rec *r) { /* mark the entity as invalidated */ h->cache_obj->info.control.invalidated = 1; return commit_entity(h, r); } static void *create_dir_config(apr_pool_t *p, char *dummy) { cache_socache_dir_conf *dconf = apr_pcalloc(p, sizeof(cache_socache_dir_conf)); dconf->max = DEFAULT_MAX_FILE_SIZE; dconf->maxtime = apr_time_from_sec(DEFAULT_MAXTIME); dconf->mintime = apr_time_from_sec(DEFAULT_MINTIME); dconf->readsize = DEFAULT_READSIZE; dconf->readtime = DEFAULT_READTIME; return dconf; } static void *merge_dir_config(apr_pool_t *p, void *basev, void *addv) { cache_socache_dir_conf *new = (cache_socache_dir_conf *) apr_pcalloc(p, sizeof(cache_socache_dir_conf)); cache_socache_dir_conf *add = (cache_socache_dir_conf *) addv; cache_socache_dir_conf *base = (cache_socache_dir_conf *) basev; new->max = (add->max_set == 0) ? base->max : add->max; new->max_set = add->max_set || base->max_set; new->maxtime = (add->maxtime_set == 0) ? base->maxtime : add->maxtime; new->maxtime_set = add->maxtime_set || base->maxtime_set; new->mintime = (add->mintime_set == 0) ? base->mintime : add->mintime; new->mintime_set = add->mintime_set || base->mintime_set; new->readsize = (add->readsize_set == 0) ? base->readsize : add->readsize; new->readsize_set = add->readsize_set || base->readsize_set; new->readtime = (add->readtime_set == 0) ? base->readtime : add->readtime; new->readtime_set = add->readtime_set || base->readtime_set; return new; } static void *create_config(apr_pool_t *p, server_rec *s) { cache_socache_conf *conf = apr_pcalloc(p, sizeof(cache_socache_conf)); return conf; } static void *merge_config(apr_pool_t *p, void *basev, void *overridesv) { cache_socache_conf *ps; cache_socache_conf *base = (cache_socache_conf *) basev; cache_socache_conf *overrides = (cache_socache_conf *) overridesv; /* socache server config only has one field */ ps = overrides ? overrides : base; return ps; } /* * mod_cache_socache configuration directives handlers. */ static const char *set_cache_socache(cmd_parms *cmd, void *in_struct_ptr, const char *arg) { cache_socache_conf *conf = ap_get_module_config(cmd->server->module_config, &cache_socache_module); cache_socache_provider_conf *provider = conf->provider = apr_pcalloc(cmd->pool, sizeof(cache_socache_provider_conf)); const char *err = NULL, *sep, *name; /* Argument is of form 'name:args' or just 'name'. */ sep = ap_strchr_c(arg, ':'); if (sep) { name = apr_pstrmemdup(cmd->pool, arg, sep - arg); sep++; provider->args = sep; } else { name = arg; } provider->socache_provider = ap_lookup_provider(AP_SOCACHE_PROVIDER_GROUP, name, AP_SOCACHE_PROVIDER_VERSION); if (provider->socache_provider == NULL) { err = apr_psprintf(cmd->pool, "Unknown socache provider '%s'. Maybe you need " "to load the appropriate socache module " "(mod_socache_%s?)", name, name); } return err; } static const char *set_cache_max(cmd_parms *parms, void *in_struct_ptr, const char *arg) { cache_socache_dir_conf *dconf = (cache_socache_dir_conf *) in_struct_ptr; if (apr_strtoff(&dconf->max, arg, NULL, 10) != APR_SUCCESS || dconf->max < 1024) { return "CacheSocacheMaxSize argument must be a integer representing the max size of a cached entry (headers and body), at least 1024"; } dconf->max_set = 1; return NULL; } static const char *set_cache_maxtime(cmd_parms *parms, void *in_struct_ptr, const char *arg) { cache_socache_dir_conf *dconf = (cache_socache_dir_conf *) in_struct_ptr; apr_off_t seconds; if (apr_strtoff(&seconds, arg, NULL, 10) != APR_SUCCESS || seconds < 0) { return "CacheSocacheMaxTime argument must be the maximum amount of time in seconds to cache an entry."; } dconf->maxtime = apr_time_from_sec(seconds); dconf->maxtime_set = 1; return NULL; } static const char *set_cache_mintime(cmd_parms *parms, void *in_struct_ptr, const char *arg) { cache_socache_dir_conf *dconf = (cache_socache_dir_conf *) in_struct_ptr; apr_off_t seconds; if (apr_strtoff(&seconds, arg, NULL, 10) != APR_SUCCESS || seconds < 0) { return "CacheSocacheMinTime argument must be the minimum amount of time in seconds to cache an entry."; } dconf->mintime = apr_time_from_sec(seconds); dconf->mintime_set = 1; return NULL; } static const char *set_cache_readsize(cmd_parms *parms, void *in_struct_ptr, const char *arg) { cache_socache_dir_conf *dconf = (cache_socache_dir_conf *) in_struct_ptr; if (apr_strtoff(&dconf->readsize, arg, NULL, 10) != APR_SUCCESS || dconf->readsize < 0) { return "CacheSocacheReadSize argument must be a non-negative integer representing the max amount of data to cache in go."; } dconf->readsize_set = 1; return NULL; } static const char *set_cache_readtime(cmd_parms *parms, void *in_struct_ptr, const char *arg) { cache_socache_dir_conf *dconf = (cache_socache_dir_conf *) in_struct_ptr; apr_off_t milliseconds; if (apr_strtoff(&milliseconds, arg, NULL, 10) != APR_SUCCESS || milliseconds < 0) { return "CacheSocacheReadTime argument must be a non-negative integer representing the max amount of time taken to cache in go."; } dconf->readtime = apr_time_from_msec(milliseconds); dconf->readtime_set = 1; return NULL; } static apr_status_t remove_lock(void *data) { if (socache_mutex) { apr_global_mutex_destroy(socache_mutex); socache_mutex = NULL; } return APR_SUCCESS; } static apr_status_t destroy_cache(void *data) { server_rec *s = data; cache_socache_conf *conf = ap_get_module_config(s->module_config, &cache_socache_module); if (conf->provider && conf->provider->socache_instance) { conf->provider->socache_provider->destroy( conf->provider->socache_instance, s); conf->provider->socache_instance = NULL; } return APR_SUCCESS; } static int socache_precfg(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptmp) { apr_status_t rv = ap_mutex_register(pconf, cache_socache_id, NULL, APR_LOCK_DEFAULT, 0); if (rv != APR_SUCCESS) { ap_log_perror(APLOG_MARK, APLOG_CRIT, rv, plog, APLOGNO(02390) "failed to register %s mutex", cache_socache_id); return 500; /* An HTTP status would be a misnomer! */ } return OK; } static int socache_post_config(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptmp, server_rec *base_server) { server_rec *s; apr_status_t rv; const char *errmsg; static struct ap_socache_hints socache_hints = { 64, 32, 60000000 }; for (s = base_server; s; s = s->next) { cache_socache_conf *conf = ap_get_module_config(s->module_config, &cache_socache_module); if (!conf->provider) { continue; } if (!socache_mutex && conf->provider->socache_provider->flags & AP_SOCACHE_FLAG_NOTMPSAFE) { rv = ap_global_mutex_create(&socache_mutex, NULL, cache_socache_id, NULL, s, pconf, 0); if (rv != APR_SUCCESS) { ap_log_perror(APLOG_MARK, APLOG_CRIT, rv, plog, APLOGNO(02391) "failed to create %s mutex", cache_socache_id); return 500; /* An HTTP status would be a misnomer! */ } apr_pool_cleanup_register(pconf, NULL, remove_lock, apr_pool_cleanup_null); } errmsg = conf->provider->socache_provider->create( &conf->provider->socache_instance, conf->provider->args, ptmp, pconf); if (errmsg) { ap_log_perror(APLOG_MARK, APLOG_CRIT, 0, plog, APLOGNO(02392) "%s", errmsg); return 500; /* An HTTP status would be a misnomer! */ } rv = conf->provider->socache_provider->init( conf->provider->socache_instance, cache_socache_id, &socache_hints, s, pconf); if (rv != APR_SUCCESS) { ap_log_perror(APLOG_MARK, APLOG_CRIT, rv, plog, APLOGNO(02393) "failed to initialise %s cache", cache_socache_id); return 500; /* An HTTP status would be a misnomer! */ } apr_pool_cleanup_register(pconf, (void *) s, destroy_cache, apr_pool_cleanup_null); } return OK; } static void socache_child_init(apr_pool_t *p, server_rec *s) { const char *lock; apr_status_t rv; if (!socache_mutex) { return; /* don't waste the overhead of creating mutex & cache */ } lock = apr_global_mutex_lockfile(socache_mutex); rv = apr_global_mutex_child_init(&socache_mutex, lock, p); if (rv != APR_SUCCESS) { ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, APLOGNO(02394) "failed to initialise mutex in child_init"); } } static const command_rec cache_socache_cmds[] = { AP_INIT_TAKE1("CacheSocache", set_cache_socache, NULL, RSRC_CONF, "The shared object cache to store cache files"), AP_INIT_TAKE1("CacheSocacheMaxTime", set_cache_maxtime, NULL, RSRC_CONF | ACCESS_CONF, "The maximum cache expiry age to cache a document in seconds"), AP_INIT_TAKE1("CacheSocacheMinTime", set_cache_mintime, NULL, RSRC_CONF | ACCESS_CONF, "The minimum cache expiry age to cache a document in seconds"), AP_INIT_TAKE1("CacheSocacheMaxSize", set_cache_max, NULL, RSRC_CONF | ACCESS_CONF, "The maximum cache entry size (headers and body) to cache a document"), AP_INIT_TAKE1("CacheSocacheReadSize", set_cache_readsize, NULL, RSRC_CONF | ACCESS_CONF, "The maximum quantity of data to attempt to read and cache in one go"), AP_INIT_TAKE1("CacheSocacheReadTime", set_cache_readtime, NULL, RSRC_CONF | ACCESS_CONF, "The maximum time taken to attempt to read and cache in go"), { NULL } }; static const cache_provider cache_socache_provider = { &remove_entity, &store_headers, &store_body, &recall_headers, &recall_body, &create_entity, &open_entity, &remove_url, &commit_entity, &invalidate_entity }; static void cache_socache_register_hook(apr_pool_t *p) { /* cache initializer */ ap_register_provider(p, CACHE_PROVIDER_GROUP, "socache", "0", &cache_socache_provider); ap_hook_pre_config(socache_precfg, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_post_config(socache_post_config, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_child_init(socache_child_init, NULL, NULL, APR_HOOK_MIDDLE); } AP_DECLARE_MODULE(cache_socache) = { STANDARD20_MODULE_STUFF, create_dir_config, /* create per-directory config structure */ merge_dir_config, /* merge per-directory config structures */ create_config, /* create per-server config structure */ merge_config, /* merge per-server config structures */ cache_socache_cmds, /* command apr_table_t */ cache_socache_register_hook /* register hooks */ };