Index: modules/cache/cache_util.c =================================================================== --- modules/cache/cache_util.c (revision 1026618) +++ modules/cache/cache_util.c (working copy) @@ -14,10 +14,9 @@ * limitations under the License. */ -#define CORE_PRIVATE - #include "mod_cache.h" +#include "cache_util.h" #include /* -------------------------------------------------------------- */ @@ -29,49 +28,87 @@ /* Determine if "url" matches the hostname, scheme and port and path * in "filter". All but the path comparisons are case-insensitive. */ -static int uri_meets_conditions(apr_uri_t filter, int pathlen, apr_uri_t url) +static int uri_meets_conditions(const apr_uri_t *filter, const int pathlen, + const apr_uri_t *url) { - /* Compare the hostnames */ - if(filter.hostname) { - if (!url.hostname) { + + /* Scheme, hostname port and local part. The filter URI and the + * URI we test may have the following shapes: + * / + * [:://[:][/]] + * That is, if there is no scheme then there must be only the path, + * and we check only the path; if there is a scheme, we check the + * scheme for equality, and then if present we match the hostname, + * and then if present match the port, and finally the path if any. + * + * Note that this means that "/" only matches local paths, + * and to match proxied paths one *must* specify the scheme. + */ + + /* Is the filter is just for a local path or a proxy URI? */ + if (!filter->scheme) { + if (url->scheme || url->hostname) { return 0; } - else if (strcasecmp(filter.hostname, url.hostname)) { + } + else { + /* The URI scheme must be present and identical except for case. */ + if (!url->scheme || strcasecmp(filter->scheme, url->scheme)) { return 0; } - } - /* Compare the schemes */ - if(filter.scheme) { - if (!url.scheme) { - return 0; + /* If the filter hostname is null or empty it matches any hostname, + * if it begins with a "*" it matches the _end_ of the URI hostname + * excluding the "*", if it begins with a "." it matches the _end_ + * of the URI * hostname including the ".", otherwise it must match + * the URI hostname exactly. */ + + if (filter->hostname && filter->hostname[0]) { + if (filter->hostname[0] == '.') { + const size_t fhostlen = strlen(filter->hostname); + const size_t uhostlen = url->hostname ? strlen(url->hostname) : 0; + + if (fhostlen > uhostlen || strcasecmp(filter->hostname, + url->hostname + uhostlen - fhostlen)) { + return 0; + } + } + else if (filter->hostname[0] == '*') { + const size_t fhostlen = strlen(filter->hostname + 1); + const size_t uhostlen = url->hostname ? strlen(url->hostname) : 0; + + if (fhostlen > uhostlen || strcasecmp(filter->hostname + 1, + url->hostname + uhostlen - fhostlen)) { + return 0; + } + } + else if (!url->hostname || strcasecmp(filter->hostname, url->hostname)) { + return 0; + } } - else if (strcasecmp(filter.scheme, url.scheme)) { - return 0; - } - } - /* Compare the ports */ - if(filter.port_str) { - if (url.port_str && filter.port != url.port) { - return 0; + /* If the filter port is empty it matches any URL port. + * If the filter or URL port are missing, or the URL port is + * empty, they default to the port for their scheme. */ + + if (!(filter->port_str && !filter->port_str[0])) { + /* NOTE: ap_port_of_scheme will return 0 if given NULL input */ + const unsigned fport = filter->port_str ? filter->port + : apr_uri_port_of_scheme(filter->scheme); + const unsigned uport = (url->port_str && url->port_str[0]) + ? url->port : apr_uri_port_of_scheme(url->scheme); + + if (fport != uport) { + return 0; + } } - /* NOTE: ap_port_of_scheme will return 0 if given NULL input */ - else if (filter.port != apr_uri_port_of_scheme(url.scheme)) { - return 0; - } } - else if(url.port_str && filter.scheme) { - if (apr_uri_port_of_scheme(filter.scheme) == url.port) { - return 0; - } - } /* For HTTP caching purposes, an empty (NULL) path is equivalent to * a single "/" path. RFCs 3986/2396 */ - if (!url.path) { - if (*filter.path == '/' && pathlen == 1) { + if (!url->path) { + if (*filter->path == '/' && pathlen == 1) { return 1; } else { @@ -82,12 +119,12 @@ /* Url has met all of the filter conditions so far, determine * if the paths match. */ - return !strncmp(filter.path, url.path, pathlen); + return !strncmp(filter->path, url->path, pathlen); } -CACHE_DECLARE(cache_provider_list *)ap_cache_get_providers(request_rec *r, - cache_server_conf *conf, - apr_uri_t uri) +cache_provider_list *cache_get_providers(request_rec *r, + cache_server_conf *conf, + apr_uri_t uri) { cache_provider_list *providers = NULL; int i; @@ -96,7 +133,7 @@ for (i = 0; i < conf->cacheenable->nelts; i++) { struct cache_enable *ent = (struct cache_enable *)conf->cacheenable->elts; - if (uri_meets_conditions(ent[i].url, ent[i].pathlen, uri)) { + if (uri_meets_conditions(&ent[i].url, ent[i].pathlen, &uri)) { /* Fetch from global config and add to the list. */ cache_provider *provider; provider = ap_lookup_provider(CACHE_PROVIDER_GROUP, ent[i].type, @@ -133,7 +170,7 @@ for (i = 0; i < conf->cachedisable->nelts; i++) { struct cache_disable *ent = (struct cache_disable *)conf->cachedisable->elts; - if (uri_meets_conditions(ent[i].url, ent[i].pathlen, uri)) { + if (uri_meets_conditions(&ent[i].url, ent[i].pathlen, &uri)) { /* Stop searching now. */ return NULL; } @@ -163,6 +200,10 @@ resident_time = now - info->response_time; current_age = corrected_initial_age + resident_time; + if (current_age < 0) { + current_age = 0; + } + return apr_time_sec(current_age); } @@ -187,8 +228,9 @@ * no point is it possible for this lock to permanently deny access to * the backend. */ -CACHE_DECLARE(apr_status_t) ap_cache_try_lock(cache_server_conf *conf, - request_rec *r, char *key) { +apr_status_t cache_try_lock(cache_server_conf *conf, cache_request_rec *cache, + request_rec *r) +{ apr_status_t status; const char *lockname; const char *path; @@ -212,12 +254,12 @@ } /* create the key if it doesn't exist */ - if (!key) { - cache_generate_key(r, r->pool, &key); + if (!cache->key) { + cache_generate_key(r, r->pool, &cache->key); } /* create a hashed filename from the key, and save it for later */ - lockname = ap_cache_generate_name(r->pool, 0, 0, key); + lockname = ap_cache_generate_name(r->pool, 0, 0, cache->key); /* lock files represent discrete just-went-stale URLs "in flight", so * we support a simple two level directory structure, more is overkill. @@ -232,9 +274,9 @@ path = apr_pstrcat(r->pool, conf->lockpath, dir, NULL); if (APR_SUCCESS != (status = apr_dir_make_recursive(path, APR_UREAD|APR_UWRITE|APR_UEXECUTE, r->pool))) { - ap_log_error(APLOG_MARK, APLOG_ERR, status, r->server, - "Could not create a cache lock directory: %s", - path); + ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r, + "Could not create a cache lock directory: %s", + path); return status; } lockname = apr_pstrcat(r->pool, path, "/", lockname, NULL); @@ -244,16 +286,16 @@ status = apr_stat(&finfo, lockname, APR_FINFO_MTIME | APR_FINFO_NLINK, r->pool); if (!(APR_STATUS_IS_ENOENT(status)) && APR_SUCCESS != status) { - ap_log_error(APLOG_MARK, APLOG_ERR, APR_EEXIST, r->server, - "Could not stat a cache lock file: %s", - lockname); + ap_log_rerror(APLOG_MARK, APLOG_ERR, APR_EEXIST, r, + "Could not stat a cache lock file: %s", + lockname); return status; } if ((status == APR_SUCCESS) && (((now - finfo.mtime) > conf->lockmaxage) || (now < finfo.mtime))) { - ap_log_error(APLOG_MARK, APLOG_INFO, status, r->server, - "Cache lock file for '%s' too old, removing: %s", - r->uri, lockname); + ap_log_rerror(APLOG_MARK, APLOG_INFO, status, r, + "Cache lock file for '%s' too old, removing: %s", + r->uri, lockname); apr_file_remove(lockname, r->pool); } @@ -279,8 +321,9 @@ * If an optional bucket brigade is passed, the lock will only be * removed if the bucket brigade contains an EOS bucket. */ -CACHE_DECLARE(apr_status_t) ap_cache_remove_lock(cache_server_conf *conf, - request_rec *r, char *key, apr_bucket_brigade *bb) { +apr_status_t cache_remove_lock(cache_server_conf *conf, + cache_request_rec *cache, request_rec *r, apr_bucket_brigade *bb) +{ void *dummy; const char *lockname; @@ -317,12 +360,12 @@ char dir[5]; /* create the key if it doesn't exist */ - if (!key) { - cache_generate_key(r, r->pool, &key); + if (!cache->key) { + cache_generate_key(r, r->pool, &cache->key); } /* create a hashed filename from the key, and save it for later */ - lockname = ap_cache_generate_name(r->pool, 0, 0, key); + lockname = ap_cache_generate_name(r->pool, 0, 0, cache->key); /* lock files represent discrete just-went-stale URLs "in flight", so * we support a simple two level directory structure, more is overkill. @@ -338,17 +381,74 @@ return apr_file_remove(lockname, r->pool); } -CACHE_DECLARE(int) ap_cache_check_freshness(cache_handle_t *h, - request_rec *r) +CACHE_DECLARE(int) ap_cache_check_allowed(cache_request_rec *cache, request_rec *r) { + const char *cc_req; + const char *pragma; + cache_server_conf *conf = + (cache_server_conf *)ap_get_module_config(r->server->module_config, + &cache_module); + + /* + * At this point, we may have data cached, but the request may have + * specified that cached data may not be used in a response. + * + * This is covered under RFC2616 section 14.9.4 (Cache Revalidation and + * Reload Controls). + * + * - RFC2616 14.9.4 End to end reload, Cache-Control: no-cache, or Pragma: + * no-cache. The server MUST NOT use a cached copy when responding to such + * a request. + * + * - RFC2616 14.9.2 What May be Stored by Caches. If Cache-Control: + * no-store arrives, do not serve from the cache. + */ + + /* This value comes from the client's initial request. */ + cc_req = apr_table_get(r->headers_in, "Cache-Control"); + pragma = apr_table_get(r->headers_in, "Pragma"); + + ap_cache_control(r, &cache->control_in, cc_req, pragma, r->headers_in); + + if (cache->control_in.no_cache) { + + if (!conf->ignorecachecontrol) { + return 0; + } + else { + ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, + "Incoming request is asking for an uncached version of " + "%s, but we have been configured to ignore it and serve " + "cached content anyway", r->unparsed_uri); + } + } + + if (cache->control_in.no_store) { + + if (!conf->ignorecachecontrol) { + /* We're not allowed to serve a cached copy */ + return 0; + } + else { + ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, + "Incoming request is asking for a no-store version of " + "%s, but we have been configured to ignore it and serve " + "cached content anyway", r->unparsed_uri); + } + } + + return 1; +} + + +int cache_check_freshness(cache_handle_t *h, cache_request_rec *cache, + request_rec *r) { apr_status_t status; apr_int64_t age, maxage_req, maxage_cresp, maxage, smaxage, maxstale; apr_int64_t minfresh; - const char *cc_cresp, *cc_req; + const char *cc_req; const char *pragma; const char *agestr = NULL; - const char *expstr = NULL; - char *val; apr_time_t age_c = 0; cache_info *info = &(h->cache_obj->info); const char *warn_head; @@ -360,10 +460,11 @@ * We now want to check if our cached data is still fresh. This depends * on a few things, in this order: * - * - RFC2616 14.9.4 End to end reload, Cache-Control: no-cache. no-cache in - * either the request or the cached response means that we must - * revalidate the request unconditionally, overriding any expiration - * mechanism. It's equivalent to max-age=0,must-revalidate. + * - RFC2616 14.9.4 End to end reload, Cache-Control: no-cache. no-cache + * in either the request or the cached response means that we must + * perform the request unconditionally, and ignore cached content. We + * should never reach here, but if we do, mark the content as stale, + * as this is the best we can do. * * - RFC2616 14.32 Pragma: no-cache This is treated the same as * Cache-Control: no-cache. @@ -393,28 +494,30 @@ cc_req = apr_table_get(r->headers_in, "Cache-Control"); pragma = apr_table_get(r->headers_in, "Pragma"); - if (ap_cache_liststr(NULL, pragma, "no-cache", NULL) - || ap_cache_liststr(NULL, cc_req, "no-cache", NULL)) { + ap_cache_control(r, &cache->control_in, cc_req, pragma, r->headers_in); + if (cache->control_in.no_cache) { + if (!conf->ignorecachecontrol) { /* Treat as stale, causing revalidation */ return 0; } - ap_log_error(APLOG_MARK, APLOG_INFO, 0, r->server, - "Incoming request is asking for a uncached version of " - "%s, but we know better and are ignoring it", - r->unparsed_uri); + ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, + "Incoming request is asking for a uncached version of " + "%s, but we have been configured to ignore it and " + "serve a cached response anyway", + r->unparsed_uri); } /* These come from the cached entity. */ - cc_cresp = apr_table_get(h->resp_hdrs, "Cache-Control"); - expstr = apr_table_get(h->resp_hdrs, "Expires"); - - if (ap_cache_liststr(NULL, cc_cresp, "no-cache", NULL)) { + if (h->cache_obj->info.control.no_cache + || h->cache_obj->info.control.no_cache_header + || h->cache_obj->info.control.private_header) { /* - * The cached entity contained Cache-Control: no-cache, so treat as - * stale causing revalidation + * The cached entity contained Cache-Control: no-cache, or a + * no-cache with a header present, or a private with a header + * present, so treat as stale causing revalidation. */ return 0; } @@ -427,32 +530,16 @@ age = ap_cache_current_age(info, age_c, r->request_time); /* extract s-maxage */ - if (cc_cresp && ap_cache_liststr(r->pool, cc_cresp, "s-maxage", &val) - && val != NULL) { - smaxage = apr_atoi64(val); - } - else { - smaxage = -1; - } + smaxage = h->cache_obj->info.control.s_maxage_value; /* extract max-age from request */ - if (!conf->ignorecachecontrol - && cc_req && ap_cache_liststr(r->pool, cc_req, "max-age", &val) - && val != NULL) { - maxage_req = apr_atoi64(val); + maxage_req = -1; + if (!conf->ignorecachecontrol) { + maxage_req = cache->control_in.max_age_value; } - else { - maxage_req = -1; - } /* extract max-age from response */ - if (cc_cresp && ap_cache_liststr(r->pool, cc_cresp, "max-age", &val) - && val != NULL) { - maxage_cresp = apr_atoi64(val); - } - else { - maxage_cresp = -1; - } + maxage_cresp = h->cache_obj->info.control.max_age_value; /* * if both maxage request and response, the smaller one takes priority @@ -468,9 +555,9 @@ } /* extract max-stale */ - if (cc_req && ap_cache_liststr(r->pool, cc_req, "max-stale", &val)) { - if(val != NULL) { - maxstale = apr_atoi64(val); + if (cache->control_in.max_stale) { + if(cache->control_in.max_stale_value != -1) { + maxstale = cache->control_in.max_stale_value; } else { /* @@ -489,22 +576,16 @@ } /* extract min-fresh */ - if (!conf->ignorecachecontrol - && cc_req && ap_cache_liststr(r->pool, cc_req, "min-fresh", &val) - && val != NULL) { - minfresh = apr_atoi64(val); + if (!conf->ignorecachecontrol && cache->control_in.min_fresh) { + minfresh = cache->control_in.min_fresh_value; } else { minfresh = 0; } /* override maxstale if must-revalidate or proxy-revalidate */ - if (maxstale && ((cc_cresp && - ap_cache_liststr(NULL, cc_cresp, - "must-revalidate", NULL)) || - (cc_cresp && - ap_cache_liststr(NULL, cc_cresp, - "proxy-revalidate", NULL)))) { + if (maxstale && (h->cache_obj->info.control.must_revalidate + || h->cache_obj->info.control.proxy_revalidate)) { maxstale = 0; } @@ -534,13 +615,14 @@ "110 Response is stale"); } } + /* * If none of Expires, Cache-Control: max-age, or Cache-Control: * s-maxage appears in the response, and the response header age * calculated is more than 24 hours add the warning 113 */ - if ((maxage_cresp == -1) && (smaxage == -1) && - (expstr == NULL) && (age > 86400)) { + if ((maxage_cresp == -1) && (smaxage == -1) && (apr_table_get( + h->resp_hdrs, "Expires") == NULL) && (age > 86400)) { /* Make sure we don't stomp on a previous warning, and don't dup * a 113 marning that is already present. Also, make sure to add @@ -585,21 +667,21 @@ * A lock that exceeds a maximum age will be deleted, and another * request gets to make a new lock and try again. */ - status = ap_cache_try_lock(conf, r, (char *)h->cache_obj->key); + status = cache_try_lock(conf, cache, r); if (APR_SUCCESS == status) { /* we obtained a lock, follow the stale path */ - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, - "Cache lock obtained for stale cached URL, " - "revalidating entry: %s", - r->unparsed_uri); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "Cache lock obtained for stale cached URL, " + "revalidating entry: %s", + r->unparsed_uri); return 0; } else if (APR_EEXIST == status) { /* lock already exists, return stale data anyway, with a warning */ - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, - "Cache already locked for stale cached URL, " - "pretend it is fresh: %s", - r->unparsed_uri); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "Cache already locked for stale cached URL, " + "pretend it is fresh: %s", + r->unparsed_uri); /* make sure we don't stomp on a previous warning */ warn_head = apr_table_get(h->resp_hdrs, "Warning"); @@ -613,10 +695,10 @@ } else { /* some other error occurred, just treat the object as stale */ - ap_log_error(APLOG_MARK, APLOG_DEBUG, status, r->server, - "Attempt to obtain a cache lock for stale " - "cached URL failed, revalidating entry anyway: %s", - r->unparsed_uri); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, status, r, + "Attempt to obtain a cache lock for stale " + "cached URL failed, revalidating entry anyway: %s", + r->unparsed_uri); return 0; } @@ -818,23 +900,32 @@ return apr_pstrdup(p, hashfile); } -/* Create a new table consisting of those elements from an input +/* + * Create a new table consisting of those elements from an * headers table that are allowed to be stored in a cache. */ -CACHE_DECLARE(apr_table_t *)ap_cache_cacheable_hdrs_out(apr_pool_t *pool, +CACHE_DECLARE(apr_table_t *)ap_cache_cacheable_headers(apr_pool_t *pool, apr_table_t *t, server_rec *s) { cache_server_conf *conf; char **header; int i; + apr_table_t *headers_out; + /* Short circuit the common case that there are not + * (yet) any headers populated. + */ + if (t == NULL) { + return apr_table_make(pool, 10); + }; + /* Make a copy of the headers, and remove from * the copy any hop-by-hop headers, as defined in Section * 13.5.1 of RFC 2616 */ - apr_table_t *headers_out; headers_out = apr_table_copy(pool, t); + apr_table_unset(headers_out, "Connection"); apr_table_unset(headers_out, "Keep-Alive"); apr_table_unset(headers_out, "Proxy-Authenticate"); @@ -846,6 +937,7 @@ conf = (cache_server_conf *)ap_get_module_config(s->module_config, &cache_module); + /* Remove the user defined headers set with CacheIgnoreHeaders. * This may break RFC 2616 compliance on behalf of the administrator. */ @@ -855,3 +947,209 @@ } return headers_out; } + +/* + * Create a new table consisting of those elements from an input + * headers table that are allowed to be stored in a cache. + */ +CACHE_DECLARE(apr_table_t *)ap_cache_cacheable_headers_in(request_rec *r) +{ + return ap_cache_cacheable_headers(r->pool, r->headers_in, r->server); +} + +/* + * Create a new table consisting of those elements from an output + * headers table that are allowed to be stored in a cache; + * ensure there is a content type and capture any errors. + */ +CACHE_DECLARE(apr_table_t *)ap_cache_cacheable_headers_out(request_rec *r) +{ + apr_table_t *headers_out; + + headers_out = apr_table_overlay(r->pool, r->headers_out, + r->err_headers_out); + + apr_table_clear(r->err_headers_out); + + headers_out = ap_cache_cacheable_headers(r->pool, headers_out, + r->server); + + if (!apr_table_get(headers_out, "Content-Type") + && r->content_type) { + apr_table_setn(headers_out, "Content-Type", + ap_make_content_type(r, r->content_type)); + } + + if (!apr_table_get(headers_out, "Content-Encoding") + && r->content_encoding) { + apr_table_setn(headers_out, "Content-Encoding", + r->content_encoding); + } + + return headers_out; +} + +/** + * Parse the Cache-Control and Pragma headers in one go, marking + * which tokens appear within the header. Populate the structure + * passed in. + */ +int ap_cache_control(request_rec *r, cache_control_t *cc, + const char *cc_header, const char *pragma_header, apr_table_t *headers) +{ + char *last; + + if (cc->parsed) { + return cc->cache_control || cc->pragma; + } + + cc->parsed = 1; + cc->max_age_value = -1; + cc->max_stale_value = -1; + cc->min_fresh_value = -1; + cc->s_maxage_value = -1; + + if (pragma_header) { + char *header = apr_pstrdup(r->pool, pragma_header); + const char *token = apr_strtok(header, ", ", &last); + while (token) { + /* handle most common quickest case... */ + if (!strcmp(token, "no-cache")) { + cc->no_cache = 1; + } + /* ...then try slowest case */ + else if (!strcasecmp(token, "no-cache")) { + cc->no_cache = 1; + } + token = apr_strtok(NULL, ", ", &last); + } + cc->pragma = 1; + } + + if (cc_header) { + char *header = apr_pstrdup(r->pool, cc_header); + const char *token = apr_strtok(header, ", ", &last); + while (token) { + switch (token[0]) { + case 'n': + case 'N': { + /* handle most common quickest cases... */ + if (!strcmp(token, "no-cache")) { + cc->no_cache = 1; + } + else if (!strcmp(token, "no-store")) { + cc->no_store = 1; + } + /* ...then try slowest cases */ + else if (!strncasecmp(token, "no-cache", 8)) { + if (token[8] == '=') { + if (apr_table_get(headers, token + 9)) { + cc->no_cache_header = 1; + } + } + else if (!token[8]) { + cc->no_cache = 1; + } + break; + } + else if (!strcasecmp(token, "no-store")) { + cc->no_store = 1; + } + else if (!strcasecmp(token, "no-transform")) { + cc->no_transform = 1; + } + break; + } + case 'm': + case 'M': { + /* handle most common quickest cases... */ + if (!strcmp(token, "max-age=0")) { + cc->max_age = 1; + cc->max_age_value = 0; + } + else if (!strcmp(token, "must-revalidate")) { + cc->must_revalidate = 1; + } + /* ...then try slowest cases */ + else if (!strncasecmp(token, "max-age", 7)) { + if (token[7] == '=') { + cc->max_age = 1; + cc->max_age_value = apr_atoi64(token + 8); + } + break; + } + else if (!strncasecmp(token, "max-stale", 9)) { + if (token[9] == '=') { + cc->max_stale = 1; + cc->max_stale_value = apr_atoi64(token + 10); + } + else if (!token[10]) { + cc->max_stale = 1; + cc->max_stale_value = -1; + } + break; + } + else if (!strncasecmp(token, "min-fresh", 9)) { + if (token[9] == '=') { + cc->min_fresh = 1; + cc->min_fresh_value = apr_atoi64(token + 10); + } + break; + } + else if (!strcasecmp(token, "must-revalidate")) { + cc->must_revalidate = 1; + } + break; + } + case 'o': + case 'O': { + if (!strcasecmp(token, "only-if-cached")) { + cc->only_if_cached = 1; + } + break; + } + case 'p': + case 'P': { + /* handle most common quickest cases... */ + if (!strcmp(token, "private")) { + cc->private = 1; + } + /* ...then try slowest cases */ + else if (!strcasecmp(token, "public")) { + cc->public = 1; + } + else if (!strncasecmp(token, "private", 7)) { + if (token[7] == '=') { + if (apr_table_get(headers, token + 8)) { + cc->private_header = 1; + } + } + else if (!token[7]) { + cc->private = 1; + } + break; + } + else if (!strcasecmp(token, "proxy-revalidate")) { + cc->proxy_revalidate = 1; + } + break; + } + case 's': + case 'S': { + if (!strncasecmp(token, "s-maxage", 8)) { + if (token[8] == '=') { + cc->s_maxage = 1; + cc->s_maxage_value = apr_atoi64(token + 9); + } + break; + } + break; + } + } + token = apr_strtok(NULL, ", ", &last); + } + cc->cache_control = 1; + } + + return (cc_header != NULL || pragma_header != NULL); +} Index: modules/cache/mod_cache.c =================================================================== --- modules/cache/mod_cache.c (revision 1026618) +++ modules/cache/mod_cache.c (working copy) @@ -14,10 +14,11 @@ * limitations under the License. */ -#define CORE_PRIVATE - #include "mod_cache.h" +#include "cache_storage.h" +#include "cache_util.h" + module AP_MODULE_DECLARE_DATA cache_module; APR_OPTIONAL_FN_TYPE(ap_cache_generate_key) *cache_generate_key; @@ -27,6 +28,7 @@ /* Handles for cache filters, resolved at startup to eliminate * a name-to-function mapping on each request */ +static ap_filter_rec_t *cache_filter_handle; static ap_filter_rec_t *cache_save_filter_handle; static ap_filter_rec_t *cache_save_subreq_filter_handle; static ap_filter_rec_t *cache_out_filter_handle; @@ -46,18 +48,32 @@ * add CACHE_SAVE filter * If No: * oh well. + * + * By default, the cache handler runs in the quick handler, bypassing + * virtually all server processing and offering the cache its optimal + * performance. In this mode, the cache bolts onto the front of the + * server, and behaves as a discrete RFC2616 caching proxy + * implementation. + * + * Under certain circumstances, an admin might want to run the cache as + * a normal handler instead of a quick handler, allowing the cache to + * run after the authorisation hooks, or by allowing fine control over + * the placement of the cache in the filter chain. This option comes at + * a performance penalty, and should only be used to achieve specific + * caching goals where the admin understands what they are doing. */ -static int cache_url_handler(request_rec *r, int lookup) +static int cache_quick_handler(request_rec *r, int lookup) { apr_status_t rv; const char *auth; cache_provider_list *providers; cache_request_rec *cache; - cache_server_conf *conf; apr_bucket_brigade *out; + apr_bucket *e; ap_filter_t *next; ap_filter_rec_t *cache_out_handle; + cache_server_conf *conf; /* Delay initialization until we know we are handling a GET */ if (r->method_number != M_GET) { @@ -67,20 +83,22 @@ conf = (cache_server_conf *) ap_get_module_config(r->server->module_config, &cache_module); + /* only run if the quick handler is enabled */ + if (!conf->quick) { + return DECLINED; + } + /* * Which cache module (if any) should handle this request? */ - if (!(providers = ap_cache_get_providers(r, conf, r->parsed_uri))) { + if (!(providers = cache_get_providers(r, conf, r->parsed_uri))) { return DECLINED; } /* make space for the per request config */ - cache = (cache_request_rec *) ap_get_module_config(r->request_config, - &cache_module); - if (!cache) { - cache = apr_pcalloc(r->pool, sizeof(cache_request_rec)); - ap_set_module_config(r->request_config, &cache_module, cache); - } + cache = apr_pcalloc(r->pool, sizeof(cache_request_rec)); + cache->size = -1; + cache->out = apr_brigade_create(r->pool, r->connection->bucket_alloc); /* save away the possible providers */ cache->providers = providers; @@ -109,7 +127,7 @@ * add cache_out filter * return OK */ - rv = cache_select(r); + rv = cache_select(cache, r); if (rv != OK) { if (rv == DECLINED) { if (!lookup) { @@ -121,7 +139,7 @@ * backend without any attempt to cache. this stops * duplicated simultaneous attempts to cache an entity. */ - rv = ap_cache_try_lock(conf, r, NULL); + rv = cache_try_lock(conf, cache, r); if (APR_SUCCESS == rv) { /* @@ -130,29 +148,32 @@ * or not. */ if (r->main) { - ap_log_error(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, - r->server, - "Adding CACHE_SAVE_SUBREQ filter for %s", + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, + r, "Adding CACHE_SAVE_SUBREQ filter for %s", r->uri); - ap_add_output_filter_handle(cache_save_subreq_filter_handle, - NULL, r, r->connection); + cache->save_filter = ap_add_output_filter_handle( + cache_save_subreq_filter_handle, cache, r, + r->connection); } else { - ap_log_error(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, - r->server, "Adding CACHE_SAVE filter for %s", + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, + r, "Adding CACHE_SAVE filter for %s", r->uri); - ap_add_output_filter_handle(cache_save_filter_handle, - NULL, r, r->connection); + cache->save_filter = ap_add_output_filter_handle( + cache_save_filter_handle, cache, r, + r->connection); } - ap_log_error(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r->server, + apr_pool_userdata_setn(cache, CACHE_CTX_KEY, NULL, r->pool); + + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r, "Adding CACHE_REMOVE_URL filter for %s", r->uri); /* Add cache_remove_url filter to this request to remove a * stale cache entry if needed. Also put the current cache * request rec in the filter context, as the request that - * is available later during running the filter maybe + * is available later during running the filter may be * different due to an internal redirect. */ cache->remove_url_filter = @@ -160,44 +181,40 @@ cache, r, r->connection); } else { - ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, - r->server, "Cache locked for url, not caching " - "response: %s", r->uri); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rv, + r, "Cache locked for url, not caching " + "response: %s", r->uri); } } else { if (cache->stale_headers) { - ap_log_error(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, - r->server, "Restoring request headers for %s", - r->uri); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, + r, "Restoring request headers for %s", + r->uri); r->headers_in = cache->stale_headers; } - - /* Delete our per-request configuration. */ - ap_set_module_config(r->request_config, &cache_module, NULL); } } else { /* error */ - ap_log_error(APLOG_MARK, APLOG_ERR, rv, r->server, - "cache: error returned while checking for cached " - "file by '%s' cache", cache->provider_name); + return rv; } return DECLINED; } + /* we've got a cache hit! tell everyone who cares */ + cache_run_cache_status(cache->handle, r, r->headers_out, AP_CACHE_HIT, + "cache hit"); + /* if we are a lookup, we are exiting soon one way or another; Restore * the headers. */ if (lookup) { if (cache->stale_headers) { - ap_log_error(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r->server, - "Restoring request headers."); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r, + "Restoring request headers."); r->headers_in = cache->stale_headers; } - - /* Delete our per-request configuration. */ - ap_set_module_config(r->request_config, &cache_module, NULL); } rv = ap_meets_conditions(r); @@ -236,7 +253,7 @@ else { cache_out_handle = cache_out_filter_handle; } - ap_add_output_filter_handle(cache_out_handle, NULL, r, r->connection); + ap_add_output_filter_handle(cache_out_handle, cache, r, r->connection); /* * Remove all filters that are before the cache_out filter. This ensures @@ -258,13 +275,16 @@ /* kick off the filter stack */ out = apr_brigade_create(r->pool, r->connection->bucket_alloc); + e = apr_bucket_eos_create(out->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(out, e); rv = ap_pass_brigade(r->output_filters, out); if (rv != APR_SUCCESS) { if (rv != AP_FILTER_ERROR) { - ap_log_error(APLOG_MARK, APLOG_ERR, rv, r->server, - "cache: error returned while trying to return %s " - "cached data", - cache->provider_name); + /* no way to know what type of error occurred */ + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rv, r, + "cache_quick_handler(%s): ap_pass_brigade returned %i", + cache->provider_name, rv); + return HTTP_INTERNAL_SERVER_ERROR; } return rv; } @@ -272,49 +292,405 @@ return OK; } +/** + * If the two filter handles are present within the filter chain, replace + * the last instance of the first filter with the last instance of the + * second filter, and return true. If the second filter is not present at + * all, the first filter is removed, and false is returned. If neither + * filter is present, false is returned and this function does nothing. + */ +static int cache_replace_filter(ap_filter_t *next, ap_filter_rec_t *from, + ap_filter_rec_t *to) { + ap_filter_t *ffrom = NULL, *fto = NULL; + while (next) { + if (next->frec == from && !next->ctx) { + ffrom = next; + } + if (next->frec == to && !next->ctx) { + fto = next; + } + next = next->next; + } + if (ffrom && fto) { + ffrom->frec = fto->frec; + ffrom->ctx = fto->ctx; + ap_remove_output_filter(fto); + return 1; + } + if (ffrom) { + ap_remove_output_filter(ffrom); + } + return 0; +} + +/** + * Find the given filter, and return it if found, or NULL otherwise. + */ +static ap_filter_t *cache_get_filter(ap_filter_t *next, ap_filter_rec_t *rec) { + while (next) { + if (next->frec == rec && next->ctx) { + break; + } + next = next->next; + } + return next; +} + +/** + * The cache handler is functionally similar to the cache_quick_hander, + * however a number of steps that are required by the quick handler are + * not required here, as the normal httpd processing has already handled + * these steps. + */ +static int cache_handler(request_rec *r) +{ + apr_status_t rv; + cache_provider_list *providers; + cache_request_rec *cache; + apr_bucket_brigade *out; + apr_bucket *e; + ap_filter_t *next; + ap_filter_rec_t *cache_out_handle; + ap_filter_rec_t *cache_save_handle; + cache_server_conf *conf; + + /* Delay initialization until we know we are handling a GET */ + if (r->method_number != M_GET) { + return DECLINED; + } + + conf = (cache_server_conf *) ap_get_module_config(r->server->module_config, + &cache_module); + + /* only run if the quick handler is disabled */ + if (conf->quick) { + return DECLINED; + } + + /* + * Which cache module (if any) should handle this request? + */ + if (!(providers = cache_get_providers(r, conf, r->parsed_uri))) { + return DECLINED; + } + + /* make space for the per request config */ + cache = apr_pcalloc(r->pool, sizeof(cache_request_rec)); + cache->size = -1; + cache->out = apr_brigade_create(r->pool, r->connection->bucket_alloc); + + /* save away the possible providers */ + cache->providers = providers; + + /* + * Try to serve this request from the cache. + * + * If no existing cache file (DECLINED) + * add cache_save filter + * If cached file (OK) + * clear filter stack + * add cache_out filter + * return OK + */ + rv = cache_select(cache, r); + if (rv != OK) { + if (rv == DECLINED) { + + /* try to obtain a cache lock at this point. if we succeed, + * we are the first to try and cache this url. if we fail, + * it means someone else is already trying to cache this + * url, and we should just let the request through to the + * backend without any attempt to cache. this stops + * duplicated simultaneous attempts to cache an entity. + */ + rv = cache_try_lock(conf, cache, r); + if (APR_SUCCESS == rv) { + + /* + * Add cache_save filter to cache this request. Choose + * the correct filter by checking if we are a subrequest + * or not. + */ + if (r->main) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, + r, "Adding CACHE_SAVE_SUBREQ filter for %s", + r->uri); + cache_save_handle = cache_save_subreq_filter_handle; + } + else { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, + r, "Adding CACHE_SAVE filter for %s", + r->uri); + cache_save_handle = cache_save_filter_handle; + } + ap_add_output_filter_handle(cache_save_handle, cache, r, + r->connection); + + /* + * Did the user indicate the precise location of the + * CACHE_SAVE filter by inserting the CACHE filter as a + * marker? + * + * If so, we get cunning and replace CACHE with the + * CACHE_SAVE filter. This has the effect of inserting + * the CACHE_SAVE filter at the precise location where + * the admin wants to cache the content. All filters that + * lie before and after the original location of the CACHE + * filter will remain in place. + */ + if (cache_replace_filter(r->output_filters, + cache_filter_handle, cache_save_handle)) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, + r, "Replacing CACHE with CACHE_SAVE " + "filter for %s", r->uri); + } + + /* save away the save filter stack */ + cache->save_filter = cache_get_filter(r->output_filters, + cache_save_filter_handle); + + apr_pool_userdata_setn(cache, CACHE_CTX_KEY, NULL, r->pool); + + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r, + "Adding CACHE_REMOVE_URL filter for %s", + r->uri); + + /* Add cache_remove_url filter to this request to remove a + * stale cache entry if needed. Also put the current cache + * request rec in the filter context, as the request that + * is available later during running the filter may be + * different due to an internal redirect. + */ + cache->remove_url_filter = + ap_add_output_filter_handle(cache_remove_url_filter_handle, + cache, r, r->connection); + + } + else { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rv, + r, "Cache locked for url, not caching " + "response: %s", r->uri); + } + } + else { + /* error */ + return rv; + } + return DECLINED; + } + + /* we've got a cache hit! tell everyone who cares */ + cache_run_cache_status(cache->handle, r, r->headers_out, AP_CACHE_HIT, + "cache hit"); + + rv = ap_meets_conditions(r); + if (rv != OK) { + return rv; + } + + /* Serve up the content */ + + /* + * Add cache_out filter to serve this request. Choose + * the correct filter by checking if we are a subrequest + * or not. + */ + if (r->main) { + cache_out_handle = cache_out_subreq_filter_handle; + } + else { + cache_out_handle = cache_out_filter_handle; + } + ap_add_output_filter_handle(cache_out_handle, cache, r, r->connection); + + /* + * Did the user indicate the precise location of the CACHE_OUT filter by + * inserting the CACHE filter as a marker? + * + * If so, we get cunning and replace CACHE with the CACHE_OUT filters. + * This has the effect of inserting the CACHE_OUT filter at the precise + * location where the admin wants to cache the content. All filters that + * lie *after* the original location of the CACHE filter will remain in + * place. + */ + if (cache_replace_filter(r->output_filters, cache_filter_handle, cache_out_handle)) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, + r, "Replacing CACHE with CACHE_OUT filter for %s", + r->uri); + } + + /* + * Remove all filters that are before the cache_out filter. This ensures + * that we kick off the filter stack with our cache_out filter being the + * first in the chain. This make sense because we want to restore things + * in the same manner as we saved them. + * There may be filters before our cache_out filter, because + * + * 1. We call ap_set_content_type during cache_select. This causes + * Content-Type specific filters to be added. + * 2. We call the insert_filter hook. This causes filters e.g. like + * the ones set with SetOutputFilter to be added. + */ + next = r->output_filters; + while (next && (next->frec != cache_out_handle)) { + ap_remove_output_filter(next); + next = next->next; + } + + /* kick off the filter stack */ + out = apr_brigade_create(r->pool, r->connection->bucket_alloc); + e = apr_bucket_eos_create(out->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(out, e); + rv = ap_pass_brigade(r->output_filters, out); + if (rv != APR_SUCCESS) { + if (rv != AP_FILTER_ERROR) { + /* no way to know what type of error occurred */ + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rv, r, + "cache_handler(%s): ap_pass_brigade returned %i", + cache->provider_name, rv); + return HTTP_INTERNAL_SERVER_ERROR; + } + return rv; + } + + return OK; +} + /* * CACHE_OUT filter * ---------------- * * Deliver cached content (headers and body) up the stack. */ -static int cache_out_filter(ap_filter_t *f, apr_bucket_brigade *bb) +static int cache_out_filter(ap_filter_t *f, apr_bucket_brigade *in) { request_rec *r = f->r; - cache_request_rec *cache; + apr_bucket *e; + cache_request_rec *cache = (cache_request_rec *)f->ctx; - cache = (cache_request_rec *) ap_get_module_config(r->request_config, - &cache_module); - if (!cache) { /* user likely configured CACHE_OUT manually; they should use mod_cache * configuration to do that */ - ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, - "CACHE_OUT enabled unexpectedly"); + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "CACHE/CACHE_OUT filter enabled while caching is disabled, ignoring"); ap_remove_output_filter(f); - return ap_pass_brigade(f->next, bb); + return ap_pass_brigade(f->next, in); } - ap_log_error(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r->server, - "cache: running CACHE_OUT filter"); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r, + "cache: running CACHE_OUT filter"); - /* restore status of cached response */ - /* XXX: This exposes a bug in mem_cache, since it does not - * restore the status into it's handle. */ - r->status = cache->handle->cache_obj->info.status; + /* clean out any previous response up to EOS, if any */ + for (e = APR_BRIGADE_FIRST(in); + e != APR_BRIGADE_SENTINEL(in); + e = APR_BUCKET_NEXT(e)) + { + if (APR_BUCKET_IS_EOS(e)) { + apr_bucket_brigade *bb = apr_brigade_create(r->pool, + r->connection->bucket_alloc); - /* recall_headers() was called in cache_select() */ - cache->provider->recall_body(cache->handle, r->pool, bb); + /* restore status of cached response */ + r->status = cache->handle->cache_obj->info.status; - /* This filter is done once it has served up its content */ - ap_remove_output_filter(f); + /* recall_headers() was called in cache_select() */ + cache->provider->recall_body(cache->handle, r->pool, bb); + APR_BRIGADE_PREPEND(in, bb); - ap_log_error(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r->server, - "cache: serving %s", r->uri); - return ap_pass_brigade(f->next, bb); + /* This filter is done once it has served up its content */ + ap_remove_output_filter(f); + + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r, + "cache: serving %s", r->uri); + return ap_pass_brigade(f->next, in); + + } + apr_bucket_delete(e); + } + + return APR_SUCCESS; } +/* + * Having jumped through all the hoops and decided to cache the + * response, call store_body() for each brigade, handling the + * case where the provider can't swallow the full brigade. In this + * case, we write the brigade we were passed out downstream, and + * loop around to try and cache some more until the in brigade is + * completely empty. As soon as the out brigade contains eos, call + * commit_entity() to finalise the cached element. + */ +static int cache_save_store(ap_filter_t *f, apr_bucket_brigade *in, + cache_server_conf *conf, cache_request_rec *cache) +{ + int rv = APR_SUCCESS; + apr_bucket *e; + /* pass the brigade in into the cache provider, which is then + * expected to move cached buckets to the out brigade, for us + * to pass up the filter stack. repeat until in is empty, or + * we fail. + */ + while (APR_SUCCESS == rv && !APR_BRIGADE_EMPTY(in)) { + + rv = cache->provider->store_body(cache->handle, f->r, in, cache->out); + if (rv != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rv, f->r, + "cache: Cache provider's store_body failed!"); + ap_remove_output_filter(f); + + /* give someone else the chance to cache the file */ + cache_remove_lock(conf, cache, f->r, NULL); + + /* give up trying to cache, just step out the way */ + APR_BRIGADE_PREPEND(in, cache->out); + return ap_pass_brigade(f->next, in); + + } + + /* does the out brigade contain eos? if so, we're done, commit! */ + for (e = APR_BRIGADE_FIRST(cache->out); + e != APR_BRIGADE_SENTINEL(cache->out); + e = APR_BUCKET_NEXT(e)) + { + if (APR_BUCKET_IS_EOS(e)) { + rv = cache->provider->commit_entity(cache->handle, f->r); + break; + } + } + + /* conditionally remove the lock as soon as we see the eos bucket */ + cache_remove_lock(conf, cache, f->r, cache->out); + + if (APR_BRIGADE_EMPTY(cache->out)) { + if (APR_BRIGADE_EMPTY(in)) { + /* cache provider wants more data before passing the brigade + * upstream, oblige the provider by leaving to fetch more. + */ + break; + } + else { + /* oops, no data out, but not all data read in either, be + * safe and stand down to prevent a spin. + */ + ap_log_rerror(APLOG_MARK, APLOG_WARNING, rv, f->r, + "cache: Cache provider's store_body returned an " + "empty brigade, but didn't consume all of the" + "input brigade, standing down to prevent a spin"); + ap_remove_output_filter(f); + + /* give someone else the chance to cache the file */ + cache_remove_lock(conf, cache, f->r, NULL); + + return ap_pass_brigade(f->next, in); + } + } + + rv = ap_pass_brigade(f->next, cache->out); + } + + return rv; +} + /* * CACHE_SAVE filter * --------------- @@ -339,29 +715,32 @@ { int rv = !OK; request_rec *r = f->r; - cache_request_rec *cache; + cache_request_rec *cache = (cache_request_rec *)f->ctx; cache_server_conf *conf; - const char *cc_out, *cl; + cache_dir_conf *dconf; + cache_control_t control; + const char *cc_out, *cl, *pragma; const char *exps, *lastmods, *dates, *etag; apr_time_t exp, date, lastmod, now; - apr_off_t size; + apr_off_t size = -1; cache_info *info = NULL; char *reason; apr_pool_t *p; apr_bucket *e; + apr_table_t *headers; conf = (cache_server_conf *) ap_get_module_config(r->server->module_config, &cache_module); /* Setup cache_request_rec */ - cache = (cache_request_rec *) ap_get_module_config(r->request_config, - &cache_module); if (!cache) { /* user likely configured CACHE_SAVE manually; they should really use * mod_cache configuration to do that */ - cache = apr_pcalloc(r->pool, sizeof(cache_request_rec)); - ap_set_module_config(r->request_config, &cache_module, cache); + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "CACHE/CACHE_SAVE filter enabled while caching is disabled, ignoring"); + ap_remove_output_filter(f); + return ap_pass_brigade(f->next, in); } reason = NULL; @@ -379,32 +758,11 @@ return APR_SUCCESS; } - /* have we already run the cachability check and set up the + /* have we already run the cacheability check and set up the * cached file handle? */ if (cache->in_checked) { - /* pass the brigades into the cache, then pass them - * up the filter stack - */ - rv = cache->provider->store_body(cache->handle, r, in); - if (rv != APR_SUCCESS) { - ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, r->server, - "cache: Cache provider's store_body failed!"); - ap_remove_output_filter(f); - - /* give someone else the chance to cache the file */ - ap_cache_remove_lock(conf, r, cache->handle ? - (char *)cache->handle->cache_obj->key : NULL, NULL); - } - else { - - /* proactively remove the lock as soon as we see the eos bucket */ - ap_cache_remove_lock(conf, r, cache->handle ? - (char *)cache->handle->cache_obj->key : NULL, in); - - } - - return ap_pass_brigade(f->next, in); + return cache_save_store(f, in, conf, cache); } /* @@ -415,6 +773,62 @@ * all. This section is* run before the above section. */ + dconf = ap_get_module_config(r->per_dir_config, &cache_module); + + /* RFC2616 13.8 Errors or Incomplete Response Cache Behavior: + * If a cache receives a 5xx response while attempting to revalidate an + * entry, it MAY either forward this response to the requesting client, + * or act as if the server failed to respond. In the latter case, it MAY + * return a previously received response unless the cached entry + * includes the "must-revalidate" cache-control directive (see section + * 14.9). + * + * This covers the case where an error was generated behind us, for example + * by a backend server via mod_proxy. + */ + if (dconf->stale_on_error && r->status >= HTTP_INTERNAL_SERVER_ERROR) { + + ap_remove_output_filter(cache->remove_url_filter); + + if (cache->stale_handle + && !cache->stale_handle->cache_obj->info.control.must_revalidate + && !cache->stale_handle->cache_obj->info.control.proxy_revalidate) { + const char *warn_head; + + /* morph the current save filter into the out filter, and serve from + * cache. + */ + cache->handle = cache->stale_handle; + if (r->main) { + f->frec = cache_out_subreq_filter_handle; + } + else { + f->frec = cache_out_filter_handle; + } + + r->headers_out = cache->stale_handle->resp_hdrs; + + /* add a revalidation warning */ + warn_head = apr_table_get(r->err_headers_out, "Warning"); + if ((warn_head == NULL) || ((warn_head != NULL) + && (ap_strstr_c(warn_head, "111") == NULL))) { + apr_table_mergen(r->err_headers_out, "Warning", + "111 Revalidation failed"); + } + + cache_run_cache_status(cache->handle, r, r->headers_out, AP_CACHE_HIT, + apr_psprintf(r->pool, + "cache hit: %d status; stale content returned", + r->status)); + + /* give someone else the chance to cache the file */ + cache_remove_lock(conf, cache, f->r, NULL); + + /* pass brigade to our morphed out filter */ + return ap_pass_brigade(f, in); + } + } + /* read expiry date; if a bad date, then leave it so the client can * read it */ @@ -452,10 +866,27 @@ etag = apr_table_get(r->headers_out, "Etag"); } cc_out = apr_table_get(r->err_headers_out, "Cache-Control"); - if (cc_out == NULL) { + pragma = apr_table_get(r->err_headers_out, "Pragma"); + headers = r->err_headers_out; + if (!cc_out && !pragma) { cc_out = apr_table_get(r->headers_out, "Cache-Control"); + pragma = apr_table_get(r->headers_out, "Pragma"); + headers = r->headers_out; } + /* Have we received a 304 response without any headers at all? Fall back to + * the original headers in the original cached request. + */ + if (r->status == HTTP_NOT_MODIFIED && cache->stale_handle && !cc_out + && !pragma) { + cc_out = apr_table_get(cache->stale_handle->resp_hdrs, "Cache-Control"); + pragma = apr_table_get(cache->stale_handle->resp_hdrs, "Pragma"); + } + + /* Parse the cache control header */ + memset(&control, 0, sizeof(cache_control_t)); + ap_cache_control(r, &control, cc_out, pragma, headers); + /* * what responses should we not cache? * @@ -465,11 +896,13 @@ * They are tested here one by one to be clear and unambiguous. */ if (r->status != HTTP_OK && r->status != HTTP_NON_AUTHORITATIVE + && r->status != HTTP_PARTIAL_CONTENT && r->status != HTTP_MULTIPLE_CHOICES && r->status != HTTP_MOVED_PERMANENTLY && r->status != HTTP_NOT_MODIFIED) { /* RFC2616 13.4 we are allowed to cache 200, 203, 206, 300, 301 or 410 - * We don't cache 206, because we don't (yet) cache partial responses. + * We allow the caching of 206, but a cache implementation might choose + * to decline to cache a 206 if it doesn't know how to. * We include 304 Not Modified here too as this is the origin server * telling us to serve the cached copy. */ @@ -500,14 +933,14 @@ /* if a broken Expires header is present, don't cache it */ reason = apr_pstrcat(p, "Broken expires header: ", exps, NULL); } - else if (exp != APR_DATE_BAD && exp < r->request_time) + else if (!dconf->store_expired && exp != APR_DATE_BAD + && exp < r->request_time) { /* if a Expires header is in the past, don't cache it */ - reason = "Expires header already expired, not cacheable"; + reason = "Expires header already expired; not cacheable"; } - else if (!conf->ignorequerystring && r->parsed_uri.query && exps == NULL && - !ap_cache_liststr(NULL, cc_out, "max-age", NULL) && - !ap_cache_liststr(NULL, cc_out, "s-maxage", NULL)) { + else if (!conf->ignorequerystring && r->parsed_uri.query && exps == NULL + && !control.max_age && !control.s_maxage) { /* if a query string is present but no explicit expiration time, * don't cache it (RFC 2616/13.9 & 13.2.1) */ @@ -520,10 +953,9 @@ */ reason = "HTTP Status 304 Not Modified"; } - else if (r->status == HTTP_OK && lastmods == NULL && etag == NULL - && (exps == NULL) && (conf->no_last_mod_ignore ==0) && - !ap_cache_liststr(NULL, cc_out, "max-age", NULL) && - !ap_cache_liststr(NULL, cc_out, "s-maxage", NULL)) { + else if (r->status == HTTP_OK && lastmods == NULL && etag == NULL && (exps + == NULL) && (dconf->no_last_mod_ignore == 0) && !control.max_age + && !control.s_maxage) { /* 200 OK response from HTTP/1.0 and up without Last-Modified, * Etag, Expires, Cache-Control:max-age, or Cache-Control:s-maxage * headers. @@ -531,38 +963,25 @@ /* Note: mod-include clears last_modified/expires/etags - this * is why we have an optional function for a key-gen ;-) */ - reason = "No Last-Modified, Etag, Expires, Cache-Control:max-age or Cache-Control:s-maxage headers"; + reason = "No Last-Modified; Etag; Expires; Cache-Control:max-age or Cache-Control:s-maxage headers"; } - else if (r->header_only && !cache->stale_handle) { - /* Forbid HEAD requests unless we have it cached already */ - reason = "HTTP HEAD request"; - } - else if (!conf->store_nostore && - ap_cache_liststr(NULL, cc_out, "no-store", NULL)) { + else if (!dconf->store_nostore && control.no_store) { /* RFC2616 14.9.2 Cache-Control: no-store response * indicating do not cache, or stop now if you are * trying to cache it. */ - /* FIXME: The Cache-Control: no-store could have come in on a 304, - * FIXME: while the original request wasn't conditional. IOW, we - * FIXME: made the the request conditional earlier to revalidate - * FIXME: our cached response. - */ reason = "Cache-Control: no-store present"; } - else if (!conf->store_private && - ap_cache_liststr(NULL, cc_out, "private", NULL)) { + else if (!dconf->store_private && control.private) { /* RFC2616 14.9.1 Cache-Control: private response * this object is marked for this user's eyes only. Behave * as a tunnel. */ - /* FIXME: See above (no-store) */ reason = "Cache-Control: private present"; } - else if (apr_table_get(r->headers_in, "Authorization") != NULL - && !(ap_cache_liststr(NULL, cc_out, "s-maxage", NULL) - || ap_cache_liststr(NULL, cc_out, "must-revalidate", NULL) - || ap_cache_liststr(NULL, cc_out, "public", NULL))) { + else if (apr_table_get(r->headers_in, "Authorization") + && !(control.s_maxage || control.must_revalidate + || control.proxy_revalidate || control.public)) { /* RFC2616 14.8 Authorisation: * if authorisation is included in the request, we don't cache, * but we can cache if the following exceptions are true: @@ -585,17 +1004,79 @@ reason = "r->no_cache present"; } + /* Hold the phone. Some servers might allow us to cache a 2xx, but + * then make their 304 responses non cacheable. This leaves us in a + * sticky position. If the 304 is in answer to our own conditional + * request, we cannot send this 304 back to the client because the + * client isn't expecting it. Instead, our only option is to respect + * the answer to the question we asked (has it changed, answer was + * no) and return the cached item to the client, and then respect + * the uncacheable nature of this 304 by allowing the remove_url + * filter to kick in and remove the cached entity. + */ + if (reason && r->status == HTTP_NOT_MODIFIED && + cache->stale_handle) { + apr_bucket_brigade *bb; + apr_bucket *bkt; + int status; + + cache->handle = cache->stale_handle; + info = &cache->handle->cache_obj->info; + + /* Load in the saved status and clear the status line. */ + r->status = info->status; + r->status_line = NULL; + + bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); + + r->headers_in = cache->stale_headers; + status = ap_meets_conditions(r); + if (status != OK) { + r->status = status; + + bkt = apr_bucket_flush_create(bb->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, bkt); + } + else { + cache->provider->recall_body(cache->handle, r->pool, bb); + + bkt = apr_bucket_eos_create(bb->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, bkt); + } + + cache->block_response = 1; + + /* we've got a cache conditional hit! tell anyone who cares */ + cache_run_cache_status( + cache->handle, + r, + r->headers_out, + AP_CACHE_REVALIDATE, + apr_psprintf( + r->pool, + "conditional cache hit: 304 was uncacheable though (%s); entity removed", + reason)); + + /* let someone else attempt to cache */ + cache_remove_lock(conf, cache, r, NULL); + + return ap_pass_brigade(f->next, bb); + } + if (reason) { - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, - "cache: %s not cached. Reason: %s", r->unparsed_uri, - reason); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "cache: %s not cached. Reason: %s", r->unparsed_uri, + reason); + /* we've got a cache miss! tell anyone who cares */ + cache_run_cache_status(cache->handle, r, r->headers_out, AP_CACHE_MISS, + reason); + /* remove this filter from the chain */ ap_remove_output_filter(f); /* remove the lock file unconditionally */ - ap_cache_remove_lock(conf, r, cache->handle ? - (char *)cache->handle->cache_obj->key : NULL, NULL); + cache_remove_lock(conf, cache, r, NULL); /* ship the data up the stack */ return ap_pass_brigade(f->next, in); @@ -622,7 +1103,6 @@ * buckets and use their length to calculate the size */ int all_buckets_here=0; - int unresolved_length = 0; size=0; for (e = APR_BRIGADE_FIRST(in); e != APR_BRIGADE_SENTINEL(in); @@ -633,7 +1113,6 @@ break; } if (APR_BUCKET_IS_FLUSH(e)) { - unresolved_length = 1; continue; } if (e->length == (apr_size_t)-1) { @@ -646,6 +1125,9 @@ } } + /* remember content length to check response size against later */ + cache->size = size; + /* It's safe to cache the response. * * There are two possiblities at this point: @@ -660,10 +1142,6 @@ */ /* Did we have a stale cache entry that really is stale? - * - * Note that for HEAD requests, we won't get the body, so for a stale - * HEAD request, we don't remove the entity - instead we let the - * CACHE_REMOVE_URL filter remove the stale item from the cache. */ if (cache->stale_handle) { if (r->status == HTTP_NOT_MODIFIED) { @@ -672,7 +1150,7 @@ info = &cache->handle->cache_obj->info; rv = OK; } - else if (!r->header_only) { + else { /* Oh, well. Toss it. */ cache->provider->remove_entity(cache->stale_handle); /* Treat the request as if it wasn't conditional. */ @@ -686,30 +1164,33 @@ } } - /* no cache handle, create a new entity only for non-HEAD requests */ - if (!cache->handle && !r->header_only) { - rv = cache_create_entity(r, size); + /* no cache handle, create a new entity */ + if (!cache->handle) { + rv = cache_create_entity(cache, r, size, in); info = apr_pcalloc(r->pool, sizeof(cache_info)); /* We only set info->status upon the initial creation. */ info->status = r->status; } if (rv != OK) { + /* we've got a cache miss! tell anyone who cares */ + cache_run_cache_status(cache->handle, r, r->headers_out, AP_CACHE_MISS, + "cache miss: create_entity failed"); + /* Caching layer declined the opportunity to cache the response */ ap_remove_output_filter(f); - ap_cache_remove_lock(conf, r, cache->handle ? - (char *)cache->handle->cache_obj->key : NULL, NULL); + cache_remove_lock(conf, cache, r, NULL); return ap_pass_brigade(f->next, in); } - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, - "cache: Caching url: %s", r->unparsed_uri); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "cache: Caching url: %s", r->unparsed_uri); /* We are actually caching this response. So it does not * make sense to remove this entity any more. */ - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, - "cache: Removing CACHE_REMOVE_URL filter."); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "cache: Removing CACHE_REMOVE_URL filter."); ap_remove_output_filter(cache->remove_url_filter); /* @@ -722,6 +1203,9 @@ * too. */ + /* store away the previously parsed cache control headers */ + memcpy(&info->control, &control, sizeof(cache_control_t)); + /* Read the date. Generate one if one is not supplied */ dates = apr_table_get(r->err_headers_out, "Date"); if (dates == NULL) { @@ -752,14 +1236,13 @@ /* if it's in the future, then replace by date */ lastmod = date; lastmods = dates; - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, - r->server, - "cache: Last modified is in the future, " - "replacing with now"); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, + r, "cache: Last modified is in the future, " + "replacing with now"); } /* if no expiry date then - * if Cache-Control: max-age present + * if Cache-Control: max-age * expiry date = date + max-age * else if lastmod * expiry date = date + min((date - lastmod) * factor, maxexpire) @@ -767,23 +1250,24 @@ * expire date = date + defaultexpire */ if (exp == APR_DATE_BAD) { - char *max_age_val; - if (ap_cache_liststr(r->pool, cc_out, "max-age", &max_age_val) && - max_age_val != NULL) { + if (control.max_age) { apr_int64_t x; errno = 0; - x = apr_atoi64(max_age_val); + x = control.max_age_value; if (errno) { - x = conf->defex; + x = dconf->defex; } else { x = x * MSEC_ONE_SEC; } - if (x > conf->maxex) { - x = conf->maxex; + if (x < dconf->minex) { + x = dconf->minex; } + if (x > dconf->maxex) { + x = dconf->maxex; + } exp = date + x; } else if ((lastmod != APR_DATE_BAD) && (lastmod < date)) { @@ -791,15 +1275,18 @@ * an expiration time of now. This causes some problems with * freshness calculations, so we choose the else path... */ - apr_time_t x = (apr_time_t) ((date - lastmod) * conf->factor); + apr_time_t x = (apr_time_t) ((date - lastmod) * dconf->factor); - if (x > conf->maxex) { - x = conf->maxex; + if (x < dconf->minex) { + x = dconf->minex; } + if (x > dconf->maxex) { + x = dconf->maxex; + } exp = date + x; } else { - exp = date + conf->defex; + exp = date + dconf->defex; } } info->expire = exp; @@ -819,14 +1306,10 @@ * err_headers_out and we also need to strip any hop-by-hop * headers that might have snuck in. */ - r->headers_out = apr_table_overlay(r->pool, r->headers_out, - r->err_headers_out); - r->headers_out = ap_cache_cacheable_hdrs_out(r->pool, r->headers_out, - r->server); - apr_table_clear(r->err_headers_out); + r->headers_out = ap_cache_cacheable_headers_out(r); /* Merge in our cached headers. However, keep any updated values. */ - ap_cache_accept_headers(cache->handle, r, 1); + cache_accept_headers(cache->handle, r, 1); } /* Write away header information to cache. It is possible that we are @@ -849,6 +1332,13 @@ apr_bucket *bkt; int status; + /* We're just saving response headers, so we are done. Commit + * the response at this point, unless there was a previous error. + */ + if (rv == APR_SUCCESS) { + rv = cache->provider->commit_entity(cache->handle, r); + } + bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); /* Restore the original request headers and see if we need to @@ -865,6 +1355,9 @@ } else { cache->provider->recall_body(cache->handle, r->pool, bb); + + bkt = apr_bucket_eos_create(bb->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, bkt); } cache->block_response = 1; @@ -875,58 +1368,63 @@ * the body it is safe to try and remove the url from the cache. */ if (rv != APR_SUCCESS) { - ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, r->server, - "cache: updating headers with store_headers failed. " - "Removing cached url."); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rv, r, + "cache: updating headers with store_headers failed. " + "Removing cached url."); - rv = cache->provider->remove_url(cache->stale_handle, r->pool); + rv = cache->provider->remove_url(cache->stale_handle, r); if (rv != OK) { /* Probably a mod_disk_cache cache area has been (re)mounted * read-only, or that there is a permissions problem. */ - ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, r->server, - "cache: attempt to remove url from cache unsuccessful."); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rv, r, + "cache: attempt to remove url from cache unsuccessful."); } + /* we've got a cache conditional hit! tell anyone who cares */ + cache_run_cache_status(cache->handle, r, r->headers_out, + AP_CACHE_REVALIDATE, + "conditional cache hit: entity refresh failed"); + } + else { + /* we've got a cache conditional hit! tell anyone who cares */ + cache_run_cache_status(cache->handle, r, r->headers_out, + AP_CACHE_REVALIDATE, + "conditional cache hit: entity refreshed"); + + } + /* let someone else attempt to cache */ - ap_cache_remove_lock(conf, r, cache->handle ? - (char *)cache->handle->cache_obj->key : NULL, NULL); + cache_remove_lock(conf, cache, r, NULL); return ap_pass_brigade(f->next, bb); } if (rv != APR_SUCCESS) { - ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, r->server, - "cache: store_headers failed"); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rv, r, + "cache: store_headers failed"); - ap_remove_output_filter(f); - ap_cache_remove_lock(conf, r, cache->handle ? - (char *)cache->handle->cache_obj->key : NULL, NULL); - return ap_pass_brigade(f->next, in); - } + /* we've got a cache miss! tell anyone who cares */ + cache_run_cache_status(cache->handle, r, r->headers_out, AP_CACHE_MISS, + "cache miss: store_headers failed"); - rv = cache->provider->store_body(cache->handle, r, in); - if (rv != APR_SUCCESS) { - ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, r->server, - "cache: store_body failed"); ap_remove_output_filter(f); - ap_cache_remove_lock(conf, r, cache->handle ? - (char *)cache->handle->cache_obj->key : NULL, NULL); + cache_remove_lock(conf, cache, r, NULL); return ap_pass_brigade(f->next, in); } - /* proactively remove the lock as soon as we see the eos bucket */ - ap_cache_remove_lock(conf, r, cache->handle ? - (char *)cache->handle->cache_obj->key : NULL, in); + /* we've got a cache miss! tell anyone who cares */ + cache_run_cache_status(cache->handle, r, r->headers_out, AP_CACHE_MISS, + "cache miss: attempting entity save"); - return ap_pass_brigade(f->next, in); + return cache_save_store(f, in, conf, cache); } /* * CACHE_REMOVE_URL filter - * --------------- + * ----------------------- * * This filter gets added in the quick handler every time the CACHE_SAVE filter * gets inserted. Its purpose is to remove a confirmed stale cache entry from @@ -959,23 +1457,279 @@ * 1. Remove ourselves * 2. Do nothing and bail out */ - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, - "cache: CACHE_REMOVE_URL enabled unexpectedly"); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "cache: CACHE_REMOVE_URL enabled unexpectedly"); ap_remove_output_filter(f); return ap_pass_brigade(f->next, in); } /* Now remove this cache entry from the cache */ - cache_remove_url(cache, r->pool); + cache_remove_url(cache, r); /* remove ourselves */ ap_remove_output_filter(f); return ap_pass_brigade(f->next, in); } +/* + * CACHE filter + * ------------ + * + * This filter can be optionally inserted into the filter chain by the admin as + * a marker representing the precise location within the filter chain where + * caching is to be performed. + * + * When the filter chain is set up in the non-quick version of the URL handler, + * the CACHE filter is replaced by the CACHE_OUT or CACHE_SAVE filter, + * effectively inserting the caching filters at the point indicated by the + * admin. The CACHE filter is then removed. + * + * This allows caching to be performed before the content is passed to the + * INCLUDES filter, or to a filter that might perform transformations unique + * to the specific request and that would otherwise be non-cacheable. + */ +static int cache_filter(ap_filter_t *f, apr_bucket_brigade *in) +{ + /* we are just a marker, so let's just remove ourselves */ + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, f->r, + "cache: CACHE filter was added twice, or was added in quick " + "handler mode and will be ignored."); + ap_remove_output_filter(f); + return ap_pass_brigade(f->next, in); +} + +/** + * If configured, add the status of the caching attempt to the subprocess + * environment, and if configured, to headers in the response. + * + * The status is saved below the broad category of the status (hit, miss, + * revalidate), as well as a single cache-status key. This can be used for + * conditional logging. + * + * The status is optionally saved to an X-Cache header, and the detail of + * why a particular cache entry was cached (or not cached) is optionally + * saved to an X-Cache-Detail header. This extra detail is useful for + * service developers who may need to know whether their Cache-Control headers + * are working correctly. + */ +static int cache_status(cache_handle_t *h, request_rec *r, + apr_table_t *headers, ap_cache_status_e status, const char *reason) +{ + cache_server_conf + *conf = + (cache_server_conf *) ap_get_module_config(r->server->module_config, + &cache_module); + + cache_dir_conf *dconf = ap_get_module_config(r->per_dir_config, &cache_module); + int x_cache = 0, x_cache_detail = 0; + + switch (status) { + case AP_CACHE_HIT: { + apr_table_setn(r->subprocess_env, AP_CACHE_HIT_ENV, reason); + break; + } + case AP_CACHE_REVALIDATE: { + apr_table_setn(r->subprocess_env, AP_CACHE_REVALIDATE_ENV, reason); + break; + } + case AP_CACHE_MISS: { + apr_table_setn(r->subprocess_env, AP_CACHE_MISS_ENV, reason); + break; + } + } + + apr_table_setn(r->subprocess_env, AP_CACHE_STATUS_ENV, reason); + + if (dconf && dconf->x_cache_set) { + x_cache = dconf->x_cache; + } + else { + x_cache = conf->x_cache; + } + if (x_cache) { + apr_table_setn(headers, "X-Cache", + apr_psprintf(r->pool, "%s from %s", + status == AP_CACHE_HIT ? "HIT" : status + == AP_CACHE_REVALIDATE ? "REVALIDATE" : "MISS", + r->server->server_hostname)); + } + + if (dconf && dconf->x_cache_detail_set) { + x_cache_detail = dconf->x_cache_detail; + } + else { + x_cache_detail = conf->x_cache_detail; + } + if (x_cache_detail) { + apr_table_setn(headers, "X-Cache-Detail", apr_psprintf(r->pool, + "\"%s\" from %s", reason, r->server->server_hostname)); + } + + return OK; +} + +/** + * If an error has occurred, but we have a stale cached entry, restore the + * filter stack from the save filter onwards. The canned error message will + * be discarded in the process, and replaced with the cached response. + */ +static void cache_insert_error_filter(request_rec *r) +{ + void *dummy; + cache_dir_conf *dconf; + + /* ignore everything except for 5xx errors */ + if (r->status < HTTP_INTERNAL_SERVER_ERROR) { + return; + } + + dconf = ap_get_module_config(r->per_dir_config, &cache_module); + + if (!dconf->stale_on_error) { + return; + } + + /* RFC2616 13.8 Errors or Incomplete Response Cache Behavior: + * If a cache receives a 5xx response while attempting to revalidate an + * entry, it MAY either forward this response to the requesting client, + * or act as if the server failed to respond. In the latter case, it MAY + * return a previously received response unless the cached entry + * includes the "must-revalidate" cache-control directive (see section + * 14.9). + * + * This covers the case where the error was generated by our server via + * ap_die(). + */ + apr_pool_userdata_get(&dummy, CACHE_CTX_KEY, r->pool); + if (dummy) { + cache_request_rec *cache = (cache_request_rec *) dummy; + + ap_remove_output_filter(cache->remove_url_filter); + + if (cache->stale_handle && cache->save_filter + && !cache->stale_handle->cache_obj->info.control.must_revalidate + && !cache->stale_handle->cache_obj->info.control.proxy_revalidate) { + const char *warn_head; + cache_server_conf + *conf = + (cache_server_conf *) ap_get_module_config(r->server->module_config, + &cache_module); + + /* morph the current save filter into the out filter, and serve from + * cache. + */ + cache->handle = cache->stale_handle; + if (r->main) { + cache->save_filter->frec = cache_out_subreq_filter_handle; + } + else { + cache->save_filter->frec = cache_out_filter_handle; + } + + r->output_filters = cache->save_filter; + + r->err_headers_out = cache->stale_handle->resp_hdrs; + + /* add a revalidation warning */ + warn_head = apr_table_get(r->err_headers_out, "Warning"); + if ((warn_head == NULL) || ((warn_head != NULL) + && (ap_strstr_c(warn_head, "111") == NULL))) { + apr_table_mergen(r->err_headers_out, "Warning", + "111 Revalidation failed"); + } + + cache_run_cache_status( + cache->handle, + r, + r->err_headers_out, + AP_CACHE_HIT, + apr_psprintf( + r->pool, + "cache hit: %d status; stale content returned", + r->status)); + + /* give someone else the chance to cache the file */ + cache_remove_lock(conf, cache, r, NULL); + + } + } + + return; +} + /* -------------------------------------------------------------- */ /* Setup configurable data */ +static void *create_dir_config(apr_pool_t *p, char *dummy) +{ + cache_dir_conf *dconf = apr_pcalloc(p, sizeof(cache_dir_conf)); + + dconf->no_last_mod_ignore = 0; + dconf->store_expired = 0; + dconf->store_private = 0; + dconf->store_nostore = 0; + + /* maximum time to cache a document */ + dconf->maxex = DEFAULT_CACHE_MAXEXPIRE; + dconf->minex = DEFAULT_CACHE_MINEXPIRE; + /* default time to cache a document */ + dconf->defex = DEFAULT_CACHE_EXPIRE; + + /* factor used to estimate Expires date from LastModified date */ + dconf->factor = DEFAULT_CACHE_LMFACTOR; + + dconf->x_cache = DEFAULT_X_CACHE; + dconf->x_cache_detail = DEFAULT_X_CACHE_DETAIL; + + dconf->stale_on_error = DEFAULT_CACHE_STALE_ON_ERROR; + + return dconf; +} + +static void *merge_dir_config(apr_pool_t *p, void *basev, void *addv) { + cache_dir_conf *new = (cache_dir_conf *) apr_pcalloc(p, sizeof(cache_dir_conf)); + cache_dir_conf *add = (cache_dir_conf *) addv; + cache_dir_conf *base = (cache_dir_conf *) basev; + + new->no_last_mod_ignore = (add->no_last_mod_ignore_set == 0) ? base->no_last_mod_ignore : add->no_last_mod_ignore; + new->no_last_mod_ignore_set = add->no_last_mod_ignore_set || base->no_last_mod_ignore_set; + + new->store_expired = (add->store_expired_set == 0) ? base->store_expired : add->store_expired; + new->store_expired_set = add->store_expired_set || base->store_expired_set; + new->store_private = (add->store_private_set == 0) ? base->store_private : add->store_private; + new->store_private_set = add->store_private_set || base->store_private_set; + new->store_nostore = (add->store_nostore_set == 0) ? base->store_nostore : add->store_nostore; + new->store_nostore_set = add->store_nostore_set || base->store_nostore_set; + + /* maximum time to cache a document */ + new->maxex = (add->maxex_set == 0) ? base->maxex : add->maxex; + new->maxex_set = add->maxex_set || base->maxex_set; + new->minex = (add->minex_set == 0) ? base->minex : add->minex; + new->minex_set = add->minex_set || base->minex_set; + + /* default time to cache a document */ + new->defex = (add->defex_set == 0) ? base->defex : add->defex; + new->defex_set = add->defex_set || base->defex_set; + + /* factor used to estimate Expires date from LastModified date */ + new->factor = (add->factor_set == 0) ? base->factor : add->factor; + new->factor_set = add->factor_set || base->factor_set; + + new->x_cache = (add->x_cache_set == 0) ? base->x_cache : add->x_cache; + new->x_cache_set = add->x_cache_set || base->x_cache_set; + new->x_cache_detail = (add->x_cache_detail_set == 0) ? base->x_cache_detail + : add->x_cache_detail; + new->x_cache_detail_set = add->x_cache_detail_set + || base->x_cache_detail_set; + + new->stale_on_error = (add->stale_on_error_set == 0) ? base->stale_on_error + : add->stale_on_error; + new->stale_on_error_set = add->stale_on_error_set + || base->stale_on_error_set; + + return new; +} + static void * create_cache_config(apr_pool_t *p, server_rec *s) { const char *tmppath; @@ -985,29 +1739,17 @@ ps->cacheenable = apr_array_make(p, 10, sizeof(struct cache_enable)); /* array of URL prefixes for which caching is disabled */ ps->cachedisable = apr_array_make(p, 10, sizeof(struct cache_disable)); - /* maximum time to cache a document */ - ps->maxex = DEFAULT_CACHE_MAXEXPIRE; - ps->maxex_set = 0; - /* default time to cache a document */ - ps->defex = DEFAULT_CACHE_EXPIRE; - ps->defex_set = 0; - /* factor used to estimate Expires date from LastModified date */ - ps->factor = DEFAULT_CACHE_LMFACTOR; - ps->factor_set = 0; - ps->no_last_mod_ignore_set = 0; - ps->no_last_mod_ignore = 0; ps->ignorecachecontrol = 0; ps->ignorecachecontrol_set = 0; - ps->store_private = 0; - ps->store_private_set = 0; - ps->store_nostore = 0; - ps->store_nostore_set = 0; /* array of headers that should not be stored in cache */ ps->ignore_headers = apr_array_make(p, 10, sizeof(char *)); ps->ignore_headers_set = CACHE_IGNORE_HEADERS_UNSET; /* flag indicating that query-string should be ignored when caching */ ps->ignorequerystring = 0; ps->ignorequerystring_set = 0; + /* by default, run in the quick handler */ + ps->quick = 1; + ps->quick_set = 0; /* array of identifiers that should not be used for key calculation */ ps->ignore_session_id = apr_array_make(p, 10, sizeof(char *)); ps->ignore_session_id_set = CACHE_IGNORE_SESSION_ID_UNSET; @@ -1018,6 +1760,8 @@ ps->lockpath = apr_pstrcat(p, tmppath, DEFAULT_CACHE_LOCKPATH, NULL); } ps->lockmaxage = apr_time_from_sec(DEFAULT_CACHE_MAXAGE); + ps->x_cache = DEFAULT_X_CACHE; + ps->x_cache_detail = DEFAULT_X_CACHE_DETAIL; return ps; } @@ -1035,30 +1779,11 @@ ps->cacheenable = apr_array_append(p, base->cacheenable, overrides->cacheenable); - /* maximum time to cache a document */ - ps->maxex = (overrides->maxex_set == 0) ? base->maxex : overrides->maxex; - /* default time to cache a document */ - ps->defex = (overrides->defex_set == 0) ? base->defex : overrides->defex; - /* factor used to estimate Expires date from LastModified date */ - ps->factor = - (overrides->factor_set == 0) ? base->factor : overrides->factor; - ps->no_last_mod_ignore = - (overrides->no_last_mod_ignore_set == 0) - ? base->no_last_mod_ignore - : overrides->no_last_mod_ignore; ps->ignorecachecontrol = (overrides->ignorecachecontrol_set == 0) ? base->ignorecachecontrol : overrides->ignorecachecontrol; - ps->store_private = - (overrides->store_private_set == 0) - ? base->store_private - : overrides->store_private; - ps->store_nostore = - (overrides->store_nostore_set == 0) - ? base->store_nostore - : overrides->store_nostore; ps->ignore_headers = (overrides->ignore_headers_set == CACHE_IGNORE_HEADERS_UNSET) ? base->ignore_headers @@ -1083,22 +1808,51 @@ (overrides->lockmaxage_set == 0) ? base->lockmaxage : overrides->lockmaxage; + ps->quick = + (overrides->quick_set == 0) + ? base->quick + : overrides->quick; + ps->x_cache = + (overrides->x_cache_set == 0) + ? base->x_cache + : overrides->x_cache; + ps->x_cache_detail = + (overrides->x_cache_detail_set == 0) + ? base->x_cache_detail + : overrides->x_cache_detail; + ps->base_uri = + (overrides->base_uri_set == 0) + ? base->base_uri + : overrides->base_uri; return ps; } -static const char *set_cache_ignore_no_last_mod(cmd_parms *parms, void *dummy, - int flag) + +static const char *set_cache_quick_handler(cmd_parms *parms, void *dummy, + int flag) { cache_server_conf *conf; conf = - (cache_server_conf *)ap_get_module_config(parms->server->module_config, + (cache_server_conf *)ap_get_module_config(parms->server->module_config +, &cache_module); - conf->no_last_mod_ignore = flag; - conf->no_last_mod_ignore_set = 1; + conf->quick = flag; + conf->quick_set = 1; return NULL; } +static const char *set_cache_ignore_no_last_mod(cmd_parms *parms, void *dummy, + int flag) +{ + cache_dir_conf *dconf = (cache_dir_conf *)dummy; + + dconf->no_last_mod_ignore = flag; + dconf->no_last_mod_ignore_set = 1; + return NULL; + +} + static const char *set_cache_ignore_cachecontrol(cmd_parms *parms, void *dummy, int flag) { @@ -1112,29 +1866,33 @@ return NULL; } +static const char *set_cache_store_expired(cmd_parms *parms, void *dummy, + int flag) +{ + cache_dir_conf *dconf = (cache_dir_conf *)dummy; + + dconf->store_expired = flag; + dconf->store_expired_set = 1; + return NULL; +} + static const char *set_cache_store_private(cmd_parms *parms, void *dummy, int flag) { - cache_server_conf *conf; + cache_dir_conf *dconf = (cache_dir_conf *)dummy; - conf = - (cache_server_conf *)ap_get_module_config(parms->server->module_config, - &cache_module); - conf->store_private = flag; - conf->store_private_set = 1; + dconf->store_private = flag; + dconf->store_private_set = 1; return NULL; } static const char *set_cache_store_nostore(cmd_parms *parms, void *dummy, int flag) { - cache_server_conf *conf; + cache_dir_conf *dconf = (cache_dir_conf *)dummy; - conf = - (cache_server_conf *)ap_get_module_config(parms->server->module_config, - &cache_module); - conf->store_nostore = flag; - conf->store_nostore_set = 1; + dconf->store_nostore = flag; + dconf->store_nostore_set = 1; return NULL; } @@ -1147,7 +1905,7 @@ conf = (cache_server_conf *)ap_get_module_config(parms->server->module_config, &cache_module); - if (!strncasecmp(header, "None", 4)) { + if (!strcasecmp(header, "None")) { /* if header None is listed clear array */ conf->ignore_headers->nelts = 0; } @@ -1175,7 +1933,7 @@ conf = (cache_server_conf *)ap_get_module_config(parms->server->module_config, &cache_module); - if (!strncasecmp(identifier, "None", 4)) { + if (!strcasecmp(identifier, "None")) { /* if identifier None is listed clear array */ conf->ignore_session_id->nelts = 0; } @@ -1201,12 +1959,30 @@ cache_server_conf *conf; struct cache_enable *new; + const char *err = ap_check_cmd_context(parms, + NOT_IN_DIRECTORY|NOT_IN_LIMIT|NOT_IN_FILES); + if (err != NULL) { + return err; + } + if (*type == '/') { return apr_psprintf(parms->pool, "provider (%s) starts with a '/'. Are url and provider switched?", type); } + if (!url) { + url = parms->path; + } + if (!url) { + return apr_psprintf(parms->pool, + "CacheEnable provider (%s) is missing an URL.", type); + } + if (parms->path && strncmp(parms->path, url, strlen(parms->path))) { + return "When in a Location, CacheEnable must specify a path or an URL below " + "that location."; + } + conf = (cache_server_conf *)ap_get_module_config(parms->server->module_config, &cache_module); @@ -1230,6 +2006,25 @@ cache_server_conf *conf; struct cache_disable *new; + const char *err = ap_check_cmd_context(parms, + NOT_IN_DIRECTORY|NOT_IN_LIMIT|NOT_IN_FILES); + if (err != NULL) { + return err; + } + + if (parms->path && !strcmp(url, "on")) { + url = parms->path; + } + if (url[0] != '/' && !ap_strchr_c(url, ':')) { + return "CacheDisable must specify a path or an URL, or when in a Location, " + "the word 'on'."; + } + + if (parms->path && strncmp(parms->path, url, strlen(parms->path))) { + return "When in a Location, CacheDisable must specify a path or an URL below " + "that location."; + } + conf = (cache_server_conf *)ap_get_module_config(parms->server->module_config, &cache_module); @@ -1249,43 +2044,44 @@ static const char *set_cache_maxex(cmd_parms *parms, void *dummy, const char *arg) { - cache_server_conf *conf; + cache_dir_conf *dconf = (cache_dir_conf *)dummy; - conf = - (cache_server_conf *)ap_get_module_config(parms->server->module_config, - &cache_module); - conf->maxex = (apr_time_t) (atol(arg) * MSEC_ONE_SEC); - conf->maxex_set = 1; + dconf->maxex = (apr_time_t) (atol(arg) * MSEC_ONE_SEC); + dconf->maxex_set = 1; return NULL; } +static const char *set_cache_minex(cmd_parms *parms, void *dummy, + const char *arg) +{ + cache_dir_conf *dconf = (cache_dir_conf *)dummy; + + dconf->minex = (apr_time_t) (atol(arg) * MSEC_ONE_SEC); + dconf->minex_set = 1; + return NULL; +} + static const char *set_cache_defex(cmd_parms *parms, void *dummy, const char *arg) { - cache_server_conf *conf; + cache_dir_conf *dconf = (cache_dir_conf *)dummy; - conf = - (cache_server_conf *)ap_get_module_config(parms->server->module_config, - &cache_module); - conf->defex = (apr_time_t) (atol(arg) * MSEC_ONE_SEC); - conf->defex_set = 1; + dconf->defex = (apr_time_t) (atol(arg) * MSEC_ONE_SEC); + dconf->defex_set = 1; return NULL; } static const char *set_cache_factor(cmd_parms *parms, void *dummy, const char *arg) { - cache_server_conf *conf; + cache_dir_conf *dconf = (cache_dir_conf *)dummy; double val; - conf = - (cache_server_conf *)ap_get_module_config(parms->server->module_config, - &cache_module); if (sscanf(arg, "%lg", &val) != 1) { return "CacheLastModifiedFactor value must be a float"; } - conf->factor = val; - conf->factor_set = 1; + dconf->factor = val; + dconf->factor_set = 1; return NULL; } @@ -1351,6 +2147,84 @@ return NULL; } +static const char *set_cache_x_cache(cmd_parms *parms, void *dummy, int flag) +{ + + if (parms->path) { + cache_dir_conf *dconf = (cache_dir_conf *)dummy; + + dconf->x_cache = flag; + dconf->x_cache_set = 1; + + } + else { + cache_server_conf *conf = + (cache_server_conf *)ap_get_module_config(parms->server->module_config, + &cache_module); + + conf->x_cache = flag; + conf->x_cache_set = 1; + + } + + return NULL; +} + +static const char *set_cache_x_cache_detail(cmd_parms *parms, void *dummy, int flag) +{ + + if (parms->path) { + cache_dir_conf *dconf = (cache_dir_conf *)dummy; + + dconf->x_cache_detail = flag; + dconf->x_cache_detail_set = 1; + + } + else { + cache_server_conf *conf = + (cache_server_conf *)ap_get_module_config(parms->server->module_config, + &cache_module); + + conf->x_cache_detail = flag; + conf->x_cache_detail_set = 1; + + } + + return NULL; +} + +static const char *set_cache_key_base_url(cmd_parms *parms, void *dummy, + const char *arg) +{ + cache_server_conf *conf; + apr_status_t rv; + + conf = + (cache_server_conf *)ap_get_module_config(parms->server->module_config, + &cache_module); + conf->base_uri = apr_pcalloc(parms->pool, sizeof(apr_uri_t)); + rv = apr_uri_parse(parms->pool, arg, conf->base_uri); + if (rv != APR_SUCCESS) { + return apr_psprintf(parms->pool, "Could not parse '%s' as an URL.", arg); + } + else if (!conf->base_uri->scheme && !conf->base_uri->hostname && + !conf->base_uri->port_str) { + return apr_psprintf(parms->pool, "URL '%s' must contain at least one of a scheme, a hostname or a port.", arg); + } + conf->base_uri_set = 1; + return NULL; +} + +static const char *set_cache_stale_on_error(cmd_parms *parms, void *dummy, + int flag) +{ + cache_dir_conf *dconf = (cache_dir_conf *)dummy; + + dconf->stale_on_error = flag; + dconf->stale_on_error_set = 1; + return NULL; +} + static int cache_post_config(apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *s) { @@ -1376,26 +2250,35 @@ * This is more intuitive that requiring a LoadModule directive. */ - AP_INIT_TAKE2("CacheEnable", add_cache_enable, NULL, RSRC_CONF, - "A cache type and partial URL prefix below which " - "caching is enabled"), - AP_INIT_TAKE1("CacheDisable", add_cache_disable, NULL, RSRC_CONF, + AP_INIT_TAKE12("CacheEnable", add_cache_enable, NULL, RSRC_CONF|ACCESS_CONF, + "A cache type and partial URL prefix below which " + "caching is enabled"), + AP_INIT_TAKE1("CacheDisable", add_cache_disable, NULL, RSRC_CONF|ACCESS_CONF, "A partial URL prefix below which caching is disabled"), - AP_INIT_TAKE1("CacheMaxExpire", set_cache_maxex, NULL, RSRC_CONF, + AP_INIT_TAKE1("CacheMaxExpire", set_cache_maxex, NULL, RSRC_CONF|ACCESS_CONF, "The maximum time in seconds to cache a document"), - AP_INIT_TAKE1("CacheDefaultExpire", set_cache_defex, NULL, RSRC_CONF, + AP_INIT_TAKE1("CacheMinExpire", set_cache_minex, NULL, RSRC_CONF|ACCESS_CONF, + "The minimum time in seconds to cache a document"), + AP_INIT_TAKE1("CacheDefaultExpire", set_cache_defex, NULL, RSRC_CONF|ACCESS_CONF, "The default time in seconds to cache a document"), + AP_INIT_FLAG("CacheQuickHandler", set_cache_quick_handler, NULL, + RSRC_CONF, + "Run the cache in the quick handler, default on"), AP_INIT_FLAG("CacheIgnoreNoLastMod", set_cache_ignore_no_last_mod, NULL, - RSRC_CONF, + RSRC_CONF|ACCESS_CONF, "Ignore Responses where there is no Last Modified Header"), AP_INIT_FLAG("CacheIgnoreCacheControl", set_cache_ignore_cachecontrol, NULL, RSRC_CONF, "Ignore requests from the client for uncached content"), + AP_INIT_FLAG("CacheStoreExpired", set_cache_store_expired, + NULL, RSRC_CONF|ACCESS_CONF, + "Ignore expiration dates when populating cache, resulting in " + "an If-Modified-Since request to the backend on retrieval"), AP_INIT_FLAG("CacheStorePrivate", set_cache_store_private, - NULL, RSRC_CONF, + NULL, RSRC_CONF|ACCESS_CONF, "Ignore 'Cache-Control: private' and store private content"), AP_INIT_FLAG("CacheStoreNoStore", set_cache_store_nostore, - NULL, RSRC_CONF, + NULL, RSRC_CONF|ACCESS_CONF, "Ignore 'Cache-Control: no-store' and store sensitive content"), AP_INIT_ITERATE("CacheIgnoreHeaders", add_ignore_header, NULL, RSRC_CONF, "A space separated list of headers that should not be " @@ -1407,7 +2290,7 @@ NULL, RSRC_CONF, "A space separated list of session " "identifiers that should be ignored for creating the key " "of the cached entity."), - AP_INIT_TAKE1("CacheLastModifiedFactor", set_cache_factor, NULL, RSRC_CONF, + AP_INIT_TAKE1("CacheLastModifiedFactor", set_cache_factor, NULL, RSRC_CONF|ACCESS_CONF, "The factor used to estimate Expires date from " "LastModified date"), AP_INIT_FLAG("CacheLock", set_cache_lock, @@ -1419,14 +2302,30 @@ "temp directory."), AP_INIT_TAKE1("CacheLockMaxAge", set_cache_lock_maxage, NULL, RSRC_CONF, "Maximum age of any thundering herd lock."), + AP_INIT_FLAG("CacheHeader", set_cache_x_cache, NULL, RSRC_CONF | ACCESS_CONF, + "Add a X-Cache header to responses. Default is off."), + AP_INIT_FLAG("CacheDetailHeader", set_cache_x_cache_detail, NULL, + RSRC_CONF | ACCESS_CONF, + "Add a X-Cache-Detail header to responses. Default is off."), + AP_INIT_TAKE1("CacheKeyBaseURL", set_cache_key_base_url, NULL, RSRC_CONF, + "Override the base URL of reverse proxied cache keys."), + AP_INIT_FLAG("CacheStaleOnError", set_cache_stale_on_error, + NULL, RSRC_CONF|ACCESS_CONF, + "Serve stale content on 5xx errors if present. Defaults to on."), {NULL} }; static void register_hooks(apr_pool_t *p) { /* cache initializer */ + /* cache quick handler */ + ap_hook_quick_handler(cache_quick_handler, NULL, NULL, APR_HOOK_FIRST); /* cache handler */ - ap_hook_quick_handler(cache_url_handler, NULL, NULL, APR_HOOK_FIRST); + ap_hook_handler(cache_handler, NULL, NULL, APR_HOOK_REALLY_FIRST); + /* cache status */ + cache_hook_cache_status(cache_status, NULL, NULL, APR_HOOK_MIDDLE); + /* cache error handler */ + ap_hook_insert_error_filter(cache_insert_error_filter, NULL, NULL, APR_HOOK_MIDDLE); /* cache filters * XXX The cache filters need to run right after the handlers and before * any other filters. Consider creating AP_FTYPE_CACHE for this purpose. @@ -1440,6 +2339,18 @@ * to be run by subrequest */ /* + * CACHE is placed into the filter chain at an admin specified location, + * and when the cache_handler is run, the CACHE filter is swapped with + * the CACHE_OUT filter, or CACHE_SAVE filter as appropriate. This has + * the effect of offering optional fine control of where the cache is + * inserted into the filter chain. + */ + cache_filter_handle = + ap_register_output_filter("CACHE", + cache_filter, + NULL, + AP_FTYPE_RESOURCE); + /* * CACHE_SAVE must go into the filter chain after a possible DEFLATE * filter to ensure that the compressed content is stored. * Incrementing filter type by 1 ensures his happens. @@ -1495,10 +2406,20 @@ module AP_MODULE_DECLARE_DATA cache_module = { STANDARD20_MODULE_STUFF, - NULL, /* create per-directory config structure */ - NULL, /* merge per-directory config structures */ + create_dir_config, /* create per-directory config structure */ + merge_dir_config, /* merge per-directory config structures */ create_cache_config, /* create per-server config structure */ merge_cache_config, /* merge per-server config structures */ cache_cmds, /* command apr_table_t */ register_hooks }; + +APR_HOOK_STRUCT( + APR_HOOK_LINK(cache_status) +) + +APR_IMPLEMENT_EXTERNAL_HOOK_RUN_ALL(cache, CACHE, int, cache_status, + (cache_handle_t *h, request_rec *r, + apr_table_t *headers, ap_cache_status_e status, + const char *reason), (h, r, headers, status, reason), + OK, DECLINED) Index: modules/cache/mod_cache.imp =================================================================== --- modules/cache/mod_cache.imp (revision 1026618) +++ modules/cache/mod_cache.imp (working copy) @@ -1,9 +1,12 @@ (MODCACHE) - ap_cache_get_providers, + ap_cache_check_allowed, + ap_cache_current_age, ap_cache_liststr, ap_cache_tokstr, ap_cache_hex2usec, ap_cache_usec2hex, - ap_cache_cacheable_hdrs_out, - ap_cache_generate_name + ap_cache_generate_name, + ap_cache_cacheable_headers, + ap_cache_cacheable_headers_in, + ap_cache_cacheable_headers_out Index: modules/cache/cache_util.h =================================================================== --- modules/cache/cache_util.h (revision 0) +++ modules/cache/cache_util.h (revision 0) @@ -0,0 +1,293 @@ +/* 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. + */ + +/** + * @file cache_util.h + * @brief Cache Storage Functions + * + * @defgroup Cache_util Cache Utility Functions + * @ingroup MOD_CACHE + * @{ + */ + +#ifndef CACHE_UTIL_H +#define CACHE_UTIL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "mod_cache.h" + +#include "apr_hooks.h" +#include "apr.h" +#include "apr_lib.h" +#include "apr_strings.h" +#include "apr_buckets.h" +#include "apr_md5.h" +#include "apr_pools.h" +#include "apr_strings.h" +#include "apr_optional.h" +#define APR_WANT_STRFUNC +#include "apr_want.h" + +#include "httpd.h" +#include "http_config.h" +#include "ap_config.h" +#include "http_core.h" +#include "http_protocol.h" +#include "http_request.h" +#include "http_vhost.h" +#include "http_main.h" +#include "http_log.h" +#include "http_connection.h" +#include "util_filter.h" +#include "apr_uri.h" + +#ifdef HAVE_NETDB_H +#include +#endif + +#ifdef HAVE_SYS_SOCKET_H +#include +#endif + +#ifdef HAVE_NETINET_IN_H +#include +#endif + +#ifdef HAVE_ARPA_INET_H +#include +#endif + +#include "apr_atomic.h" + +#ifndef MAX +#define MAX(a,b) ((a) > (b) ? (a) : (b)) +#endif +#ifndef MIN +#define MIN(a,b) ((a) < (b) ? (a) : (b)) +#endif + +#define MSEC_ONE_DAY ((apr_time_t)(86400*APR_USEC_PER_SEC)) /* one day, in microseconds */ +#define MSEC_ONE_HR ((apr_time_t)(3600*APR_USEC_PER_SEC)) /* one hour, in microseconds */ +#define MSEC_ONE_MIN ((apr_time_t)(60*APR_USEC_PER_SEC)) /* one minute, in microseconds */ +#define MSEC_ONE_SEC ((apr_time_t)(APR_USEC_PER_SEC)) /* one second, in microseconds */ + +#define DEFAULT_CACHE_MAXEXPIRE MSEC_ONE_DAY +#define DEFAULT_CACHE_MINEXPIRE 0 +#define DEFAULT_CACHE_EXPIRE MSEC_ONE_HR +#define DEFAULT_CACHE_LMFACTOR (0.1) +#define DEFAULT_CACHE_MAXAGE 5 +#define DEFAULT_X_CACHE 0 +#define DEFAULT_X_CACHE_DETAIL 0 +#define DEFAULT_CACHE_STALE_ON_ERROR 1 +#define DEFAULT_CACHE_LOCKPATH "/mod_cache-lock" +#define CACHE_LOCKNAME_KEY "mod_cache-lockname" +#define CACHE_LOCKFILE_KEY "mod_cache-lockfile" +#define CACHE_CTX_KEY "mod_cache-ctx" + +/** + * cache_util.c + */ + +struct cache_enable { + apr_uri_t url; + const char *type; + apr_size_t pathlen; +}; + +struct cache_disable { + apr_uri_t url; + apr_size_t pathlen; +}; + +/* static information about the local cache */ +typedef struct { + apr_array_header_t *cacheenable; /* URLs to cache */ + apr_array_header_t *cachedisable; /* URLs not to cache */ + /** store the headers that should not be stored in the cache */ + apr_array_header_t *ignore_headers; + /** store the identifiers that should not be used for key calculation */ + apr_array_header_t *ignore_session_id; + const char *lockpath; + apr_time_t lockmaxage; + apr_uri_t *base_uri; + /** ignore client's requests for uncached responses */ + int ignorecachecontrol:1; + /** ignore query-string when caching */ + int ignorequerystring:1; + /** run within the quick handler */ + int quick:1; + /* thundering herd lock */ + int lock:1; + int x_cache:1; + int x_cache_detail:1; + /* flag if CacheIgnoreHeader has been set */ + #define CACHE_IGNORE_HEADERS_SET 1 + #define CACHE_IGNORE_HEADERS_UNSET 0 + int ignore_headers_set:1; + /* flag if CacheIgnoreURLSessionIdentifiers has been set */ + #define CACHE_IGNORE_SESSION_ID_SET 1 + #define CACHE_IGNORE_SESSION_ID_UNSET 0 + int ignore_session_id_set:1; + int base_uri_set:1; + int ignorecachecontrol_set:1; + int ignorequerystring_set:1; + int quick_set:1; + int lock_set:1; + int lockpath_set:1; + int lockmaxage_set:1; + int x_cache_set:1; + int x_cache_detail_set:1; +} cache_server_conf; + +typedef struct { + /* Minimum time to keep cached files in msecs */ + apr_time_t minex; + /* Maximum time to keep cached files in msecs */ + apr_time_t maxex; + /* default time to keep cached file in msecs */ + apr_time_t defex; + /* factor for estimating expires date */ + double factor; + /* set X-Cache headers */ + int x_cache:1; + int x_cache_detail:1; + /* serve stale on error */ + int stale_on_error:1; + /** ignore the last-modified header when deciding to cache this request */ + int no_last_mod_ignore:1; + /** ignore expiration date from server */ + int store_expired:1; + /** ignore Cache-Control: private header from server */ + int store_private:1; + /** ignore Cache-Control: no-store header from client or server */ + int store_nostore:1; + int minex_set:1; + int maxex_set:1; + int defex_set:1; + int factor_set:1; + int x_cache_set:1; + int x_cache_detail_set:1; + int stale_on_error_set:1; + int no_last_mod_ignore_set:1; + int store_expired_set:1; + int store_private_set:1; + int store_nostore_set:1; +} cache_dir_conf; + +/* A linked-list of authn providers. */ +typedef struct cache_provider_list cache_provider_list; + +struct cache_provider_list { + const char *provider_name; + const cache_provider *provider; + cache_provider_list *next; +}; + +/* per request cache information */ +typedef struct { + cache_provider_list *providers; /* possible cache providers */ + const cache_provider *provider; /* current cache provider */ + const char *provider_name; /* current cache provider name */ + int fresh; /* is the entity fresh? */ + cache_handle_t *handle; /* current cache handle */ + cache_handle_t *stale_handle; /* stale cache handle */ + apr_table_t *stale_headers; /* original request headers. */ + int in_checked; /* CACHE_SAVE must cache the entity */ + int block_response; /* CACHE_SAVE must block response. */ + apr_bucket_brigade *saved_brigade; /* copy of partial response */ + apr_off_t saved_size; /* length of saved_brigade */ + apr_time_t exp; /* expiration */ + apr_time_t lastmod; /* last-modified time */ + cache_info *info; /* current cache info */ + ap_filter_t *save_filter; /* Enable us to restore the filter on error */ + ap_filter_t *remove_url_filter; /* Enable us to remove the filter */ + const char *key; /* The cache key created for this + * request + */ + apr_off_t size; /* the content length from the headers, or -1 */ + apr_bucket_brigade *out; /* brigade to reuse for upstream responses */ + cache_control_t control_in; /* cache control incoming */ +} cache_request_rec; + +/** + * Check the whether the request allows a cached object to be served as per RFC2616 + * section 14.9.4 (Cache Revalidation and Reload Controls) + * @param h cache_handle_t + * @param r request_rec + * @return 0 ==> cache object may not be served, 1 ==> cache object may be served + */ +CACHE_DECLARE(int) ap_cache_check_allowed(cache_request_rec *cache, request_rec *r); + +/** + * Check the freshness of the cache object per RFC2616 section 13.2 (Expiration Model) + * @param h cache_handle_t + * @param r request_rec + * @return 0 ==> cache object is stale, 1 ==> cache object is fresh + */ +int cache_check_freshness(cache_handle_t *h, cache_request_rec *cache, + request_rec *r); + +/** + * Try obtain a cache wide lock on the given cache key. + * + * If we return APR_SUCCESS, we obtained the lock, and we are clear to + * proceed to the backend. If we return APR_EEXISTS, the the lock is + * already locked, someone else has gone to refresh the backend data + * already, so we must return stale data with a warning in the mean + * time. If we return anything else, then something has gone pear + * shaped, and we allow the request through to the backend regardless. + * + * This lock is created from the request pool, meaning that should + * something go wrong and the lock isn't deleted on return of the + * request headers from the backend for whatever reason, at worst the + * lock will be cleaned up when the request is dies or finishes. + * + * If something goes truly bananas and the lock isn't deleted when the + * request dies, the lock will be trashed when its max-age is reached, + * or when a request arrives containing a Cache-Control: no-cache. At + * no point is it possible for this lock to permanently deny access to + * the backend. + */ +apr_status_t cache_try_lock(cache_server_conf *conf, cache_request_rec *cache, + request_rec *r); + +/** + * Remove the cache lock, if present. + * + * First, try to close the file handle, whose delete-on-close should + * kill the file. Otherwise, just delete the file by name. + * + * If no lock name has yet been calculated, do the calculation of the + * lock name first before trying to delete the file. + * + * If an optional bucket brigade is passed, the lock will only be + * removed if the bucket brigade contains an EOS bucket. + */ +apr_status_t cache_remove_lock(cache_server_conf *conf, + cache_request_rec *cache, request_rec *r, apr_bucket_brigade *bb); + +cache_provider_list *cache_get_providers(request_rec *r, + cache_server_conf *conf, apr_uri_t uri); + +#ifdef __cplusplus +} +#endif + +#endif /* !CACHE_UTIL_H */ +/** @} */ Index: modules/cache/mod_cache.h =================================================================== --- modules/cache/mod_cache.h (revision 1026618) +++ modules/cache/mod_cache.h (working copy) @@ -26,71 +26,11 @@ #ifndef MOD_CACHE_H #define MOD_CACHE_H -#define CORE_PRIVATE - -#include "apr_hooks.h" -#include "apr.h" -#include "apr_lib.h" -#include "apr_strings.h" -#include "apr_buckets.h" -#include "apr_md5.h" -#include "apr_pools.h" -#include "apr_strings.h" -#include "apr_optional.h" -#define APR_WANT_STRFUNC -#include "apr_want.h" - #include "httpd.h" -#include "http_config.h" -#include "ap_config.h" -#include "http_core.h" -#include "http_protocol.h" -#include "http_request.h" -#include "http_vhost.h" -#include "http_main.h" -#include "http_log.h" -#include "http_connection.h" -#include "util_filter.h" #include "apr_date.h" -#include "apr_uri.h" +#include "apr_optional.h" +#include "apr_hooks.h" -#ifdef HAVE_NETDB_H -#include -#endif - -#ifdef HAVE_SYS_SOCKET_H -#include -#endif - -#ifdef HAVE_NETINET_IN_H -#include -#endif - -#ifdef HAVE_ARPA_INET_H -#include -#endif - -#include "apr_atomic.h" - -#ifndef MAX -#define MAX(a,b) ((a) > (b) ? (a) : (b)) -#endif -#ifndef MIN -#define MIN(a,b) ((a) < (b) ? (a) : (b)) -#endif - -#define MSEC_ONE_DAY ((apr_time_t)(86400*APR_USEC_PER_SEC)) /* one day, in microseconds */ -#define MSEC_ONE_HR ((apr_time_t)(3600*APR_USEC_PER_SEC)) /* one hour, in microseconds */ -#define MSEC_ONE_MIN ((apr_time_t)(60*APR_USEC_PER_SEC)) /* one minute, in microseconds */ -#define MSEC_ONE_SEC ((apr_time_t)(APR_USEC_PER_SEC)) /* one second, in microseconds */ -#define DEFAULT_CACHE_MAXEXPIRE MSEC_ONE_DAY -#define DEFAULT_CACHE_EXPIRE MSEC_ONE_HR -#define DEFAULT_CACHE_LMFACTOR (0.1) -#define DEFAULT_CACHE_MAXAGE 5 -#define DEFAULT_CACHE_LOCKPATH "/mod_cache-lock" -#define CACHE_LOCKNAME_KEY "mod_cache-lockname" -#define CACHE_LOCKFILE_KEY "mod_cache-lockfile" - /* Create a set of CACHE_DECLARE(type), CACHE_DECLARE_NONSTD(type) and * CACHE_DECLARE_DATA with appropriate export and import tags for the platform */ @@ -112,84 +52,56 @@ #define CACHE_DECLARE_DATA __declspec(dllimport) #endif -struct cache_enable { - apr_uri_t url; - const char *type; - apr_size_t pathlen; +/* a cache control header breakdown */ +typedef struct cache_control cache_control_t; +struct cache_control { + unsigned int parsed:1; + unsigned int cache_control:1; + unsigned int pragma:1; + unsigned int no_cache:1; + unsigned int no_cache_header:1; /* no cache by header match */ + unsigned int no_store:1; + unsigned int max_age:1; + unsigned int max_stale:1; + unsigned int min_fresh:1; + unsigned int no_transform:1; + unsigned int only_if_cached:1; + unsigned int public:1; + unsigned int private:1; + unsigned int private_header:1; /* private by header match */ + unsigned int must_revalidate:1; + unsigned int proxy_revalidate:1; + unsigned int s_maxage:1; + apr_int64_t max_age_value; /* if positive, then set */ + apr_int64_t max_stale_value; /* if positive, then set */ + apr_int64_t min_fresh_value; /* if positive, then set */ + apr_int64_t s_maxage_value; /* if positive, then set */ }; -struct cache_disable { - apr_uri_t url; - apr_size_t pathlen; -}; - -/* static information about the local cache */ -typedef struct { - apr_array_header_t *cacheenable; /* URLs to cache */ - apr_array_header_t *cachedisable; /* URLs not to cache */ - /* Maximum time to keep cached files in msecs */ - apr_time_t maxex; - int maxex_set; - /* default time to keep cached file in msecs */ - apr_time_t defex; - int defex_set; - /* factor for estimating expires date */ - double factor; - int factor_set; - /** ignore the last-modified header when deciding to cache this request */ - int no_last_mod_ignore_set; - int no_last_mod_ignore; - /** ignore client's requests for uncached responses */ - int ignorecachecontrol; - int ignorecachecontrol_set; - /** ignore Cache-Control: private header from server */ - int store_private; - int store_private_set; - /** ignore Cache-Control: no-store header from client or server */ - int store_nostore; - int store_nostore_set; - /** store the headers that should not be stored in the cache */ - apr_array_header_t *ignore_headers; - /* flag if CacheIgnoreHeader has been set */ - #define CACHE_IGNORE_HEADERS_SET 1 - #define CACHE_IGNORE_HEADERS_UNSET 0 - int ignore_headers_set; - /** ignore query-string when caching */ - int ignorequerystring; - int ignorequerystring_set; - /** store the identifiers that should not be used for key calculation */ - apr_array_header_t *ignore_session_id; - /* flag if CacheIgnoreURLSessionIdentifiers has been set */ - #define CACHE_IGNORE_SESSION_ID_SET 1 - #define CACHE_IGNORE_SESSION_ID_UNSET 0 - int ignore_session_id_set; - /* thundering herd lock */ - int lock; - int lock_set; - const char *lockpath; - int lockpath_set; - apr_time_t lockmaxage; - int lockmaxage_set; -} cache_server_conf; - /* cache info information */ typedef struct cache_info cache_info; struct cache_info { - int status; + /** + * the original time corresponding to the 'Date:' header of the request + * served + */ apr_time_t date; + /** a time when the cached entity is due to expire */ apr_time_t expire; + /** r->request_time from the same request */ apr_time_t request_time; + /** apr_time_now() at the time the entity was acutally cached */ apr_time_t response_time; + /** + * HTTP status code of the cached entity. Though not necessarily the + * status code finally issued to the request. + */ + int status; + /* cached cache-control */ + cache_control_t control; }; /* cache handle information */ - -/* XXX TODO On the next structure change/MMN bump, - * count must become an apr_off_t, representing - * the potential size of disk cached objects. - * Then dig for - * "XXX Bad Temporary Cast - see cache_object_t notes" - */ typedef struct cache_object cache_object_t; struct cache_object { const char *key; @@ -197,10 +109,6 @@ cache_info info; /* Opaque portion (specific to the implementation) of the cache object */ void *vobj; - /* FIXME: These are only required for mod_mem_cache. */ - apr_size_t count; /* Number of body bytes written to the cache so far */ - int complete; - apr_uint32_t refcount; /* refcount and bit flag to cleanup object */ }; typedef struct cache_handle cache_handle_t; @@ -215,174 +123,92 @@ typedef struct { int (*remove_entity) (cache_handle_t *h); apr_status_t (*store_headers)(cache_handle_t *h, request_rec *r, cache_info *i); - apr_status_t (*store_body)(cache_handle_t *h, request_rec *r, apr_bucket_brigade *b); + apr_status_t (*store_body)(cache_handle_t *h, request_rec *r, apr_bucket_brigade *in, + apr_bucket_brigade *out); apr_status_t (*recall_headers) (cache_handle_t *h, request_rec *r); apr_status_t (*recall_body) (cache_handle_t *h, apr_pool_t *p, apr_bucket_brigade *bb); int (*create_entity) (cache_handle_t *h, request_rec *r, - const char *urlkey, apr_off_t len); + const char *urlkey, apr_off_t len, apr_bucket_brigade *bb); int (*open_entity) (cache_handle_t *h, request_rec *r, const char *urlkey); - int (*remove_url) (cache_handle_t *h, apr_pool_t *p); + int (*remove_url) (cache_handle_t *h, request_rec *r); + apr_status_t (*commit_entity)(cache_handle_t *h, request_rec *r); } cache_provider; -/* A linked-list of authn providers. */ -typedef struct cache_provider_list cache_provider_list; +typedef enum { + AP_CACHE_HIT, + AP_CACHE_REVALIDATE, + AP_CACHE_MISS +} ap_cache_status_e; -struct cache_provider_list { - const char *provider_name; - const cache_provider *provider; - cache_provider_list *next; -}; +#define AP_CACHE_HIT_ENV "cache-hit" +#define AP_CACHE_REVALIDATE_ENV "cache-revalidate" +#define AP_CACHE_MISS_ENV "cache-miss" +#define AP_CACHE_STATUS_ENV "cache-status" -/* per request cache information */ -typedef struct { - cache_provider_list *providers; /* possible cache providers */ - const cache_provider *provider; /* current cache provider */ - const char *provider_name; /* current cache provider name */ - int fresh; /* is the entitey fresh? */ - cache_handle_t *handle; /* current cache handle */ - cache_handle_t *stale_handle; /* stale cache handle */ - apr_table_t *stale_headers; /* original request headers. */ - int in_checked; /* CACHE_SAVE must cache the entity */ - int block_response; /* CACHE_SAVE must block response. */ - apr_bucket_brigade *saved_brigade; /* copy of partial response */ - apr_off_t saved_size; /* length of saved_brigade */ - apr_time_t exp; /* expiration */ - apr_time_t lastmod; /* last-modified time */ - cache_info *info; /* current cache info */ - ap_filter_t *remove_url_filter; /* Enable us to remove the filter */ - char *key; /* The cache key created for this - * request - */ -} cache_request_rec; - /* cache_util.c */ /* do a HTTP/1.1 age calculation */ CACHE_DECLARE(apr_time_t) ap_cache_current_age(cache_info *info, const apr_time_t age_value, apr_time_t now); -/** - * Check the freshness of the cache object per RFC2616 section 13.2 (Expiration Model) - * @param h cache_handle_t - * @param r request_rec - * @return 0 ==> cache object is stale, 1 ==> cache object is fresh - */ -CACHE_DECLARE(int) ap_cache_check_freshness(cache_handle_t *h, request_rec *r); - -/** - * Try obtain a cache wide lock on the given cache key. - * - * If we return APR_SUCCESS, we obtained the lock, and we are clear to - * proceed to the backend. If we return APR_EEXISTS, the the lock is - * already locked, someone else has gone to refresh the backend data - * already, so we must return stale data with a warning in the mean - * time. If we return anything else, then something has gone pear - * shaped, and we allow the request through to the backend regardless. - * - * This lock is created from the request pool, meaning that should - * something go wrong and the lock isn't deleted on return of the - * request headers from the backend for whatever reason, at worst the - * lock will be cleaned up when the request is dies or finishes. - * - * If something goes truly bananas and the lock isn't deleted when the - * request dies, the lock will be trashed when its max-age is reached, - * or when a request arrives containing a Cache-Control: no-cache. At - * no point is it possible for this lock to permanently deny access to - * the backend. - */ -CACHE_DECLARE(apr_status_t) ap_cache_try_lock(cache_server_conf *conf, - request_rec *r, char *key); - -/** - * Remove the cache lock, if present. - * - * First, try to close the file handle, whose delete-on-close should - * kill the file. Otherwise, just delete the file by name. - * - * If no lock name has yet been calculated, do the calculation of the - * lock name first before trying to delete the file. - * - * If an optional bucket brigade is passed, the lock will only be - * removed if the bucket brigade contains an EOS bucket. - */ -CACHE_DECLARE(apr_status_t) ap_cache_remove_lock(cache_server_conf *conf, - request_rec *r, char *key, apr_bucket_brigade *bb); - -/** - * Merge in cached headers into the response - * @param h cache_handle_t - * @param r request_rec - * @param preserve_orig If 1, the values in r->headers_out are preserved. - * Otherwise, they are overwritten by the cached value. - */ -CACHE_DECLARE(void) ap_cache_accept_headers(cache_handle_t *h, request_rec *r, - int preserve_orig); - CACHE_DECLARE(apr_time_t) ap_cache_hex2usec(const char *x); CACHE_DECLARE(void) ap_cache_usec2hex(apr_time_t j, char *y); -CACHE_DECLARE(char *) ap_cache_generate_name(apr_pool_t *p, int dirlevels, - int dirlength, +CACHE_DECLARE(char *) ap_cache_generate_name(apr_pool_t *p, int dirlevels, + int dirlength, const char *name); -CACHE_DECLARE(cache_provider_list *)ap_cache_get_providers(request_rec *r, cache_server_conf *conf, apr_uri_t uri); CACHE_DECLARE(int) ap_cache_liststr(apr_pool_t *p, const char *list, const char *key, char **val); CACHE_DECLARE(const char *)ap_cache_tokstr(apr_pool_t *p, const char *list, const char **str); -/* Create a new table consisting of those elements from a request_rec's - * headers_out that are allowed to be stored in a cache +/* Create a new table consisting of those elements from an + * headers table that are allowed to be stored in a cache. */ -CACHE_DECLARE(apr_table_t *)ap_cache_cacheable_hdrs_out(apr_pool_t *pool, +CACHE_DECLARE(apr_table_t *)ap_cache_cacheable_headers(apr_pool_t *pool, apr_table_t *t, server_rec *s); -/** - * cache_storage.c +/* Create a new table consisting of those elements from an input + * headers table that are allowed to be stored in a cache. */ -int cache_remove_url(cache_request_rec *cache, apr_pool_t *p); -int cache_create_entity(request_rec *r, apr_off_t size); -int cache_select(request_rec *r); -apr_status_t cache_generate_key_default( request_rec *r, apr_pool_t*p, char**key ); +CACHE_DECLARE(apr_table_t *)ap_cache_cacheable_headers_in(request_rec *r); + +/* Create a new table consisting of those elements from an output + * headers table that are allowed to be stored in a cache; + * ensure there is a content type and capture any errors. + */ +CACHE_DECLARE(apr_table_t *)ap_cache_cacheable_headers_out(request_rec *r); + /** - * create a key for the cache based on the request record - * this is the 'default' version, which can be overridden by a default function + * Parse the Cache-Control and Pragma headers in one go, marking + * which tokens appear within the header. Populate the structure + * passed in. */ -const char* cache_create_key( request_rec*r ); +int ap_cache_control(request_rec *r, cache_control_t *cc, const char *cc_header, + const char *pragma_header, apr_table_t *headers); -/* -apr_status_t cache_store_entity_headers(cache_handle_t *h, request_rec *r, cache_info *info); -apr_status_t cache_store_entity_body(cache_handle_t *h, request_rec *r, apr_bucket_brigade *bb); -apr_status_t cache_recall_entity_headers(cache_handle_t *h, request_rec *r); -apr_status_t cache_recall_entity_body(cache_handle_t *h, apr_pool_t *p, apr_bucket_brigade *bb); -*/ - /* hooks */ -/* Create a set of CACHE_DECLARE(type), CACHE_DECLARE_NONSTD(type) and - * CACHE_DECLARE_DATA with appropriate export and import tags for the platform +/** + * Cache status hook. + * This hook is called as soon as the cache has made a decision as to whether + * an entity should be served from cache (hit), should be served from cache + * after a successful validation (revalidate), or served from the backend + * and potentially cached (miss). + * + * A basic implementation of this hook exists in mod_cache which writes this + * information to the subprocess environment, and optionally to request + * headers. Further implementations may add hooks as appropriate to perform + * more advanced processing, or to store statistics about the cache behaviour. */ -#if !defined(WIN32) -#define CACHE_DECLARE(type) type -#define CACHE_DECLARE_NONSTD(type) type -#define CACHE_DECLARE_DATA -#elif defined(CACHE_DECLARE_STATIC) -#define CACHE_DECLARE(type) type __stdcall -#define CACHE_DECLARE_NONSTD(type) type -#define CACHE_DECLARE_DATA -#elif defined(CACHE_DECLARE_EXPORT) -#define CACHE_DECLARE(type) __declspec(dllexport) type __stdcall -#define CACHE_DECLARE_NONSTD(type) __declspec(dllexport) type -#define CACHE_DECLARE_DATA __declspec(dllexport) -#else -#define CACHE_DECLARE(type) __declspec(dllimport) type __stdcall -#define CACHE_DECLARE_NONSTD(type) __declspec(dllimport) type -#define CACHE_DECLARE_DATA __declspec(dllimport) -#endif +APR_DECLARE_EXTERNAL_HOOK(cache, CACHE, int, cache_status, (cache_handle_t *h, + request_rec *r, apr_table_t *headers, ap_cache_status_e status, + const char *reason)) APR_DECLARE_OPTIONAL_FN(apr_status_t, ap_cache_generate_key, - (request_rec *r, apr_pool_t*p, char**key )); + (request_rec *r, apr_pool_t*p, const char **key)); #endif /*MOD_CACHE_H*/ Index: modules/cache/cache_hash.h =================================================================== --- modules/cache/cache_hash.h (revision 1026618) +++ modules/cache/cache_hash.h (working copy) @@ -98,11 +98,9 @@ /** * Start iterating over the entries in a hash table. * @param ht The hash table - * @example - */ -/** - *
- * 
+ *
+ * Here is an example of using this:
+ * @code
  *     int sum_values(cache_hash_t *ht)
  *     {
  *         cache_hash_index_t *hi;
@@ -114,12 +112,12 @@
  * 	   }
  * 	   return sum;
  *     }
- * 
+ * @endcode
+ *
  * There is no restriction on adding or deleting hash entries during an
  * iteration (although the results may be unpredictable unless all you do
  * is delete the current entry) and multiple iterations can be in
  * progress at the same time.
- * 
*/ cache_hash_index_t* cache_hash_first(cache_hash_t *ht); Index: modules/cache/mod_disk_cache.c =================================================================== --- modules/cache/mod_disk_cache.c (revision 1026618) +++ modules/cache/mod_disk_cache.c (working copy) @@ -14,10 +14,16 @@ * limitations under the License. */ +#define CORE_PRIVATE 1 + +#include "apr_lib.h" #include "apr_file_io.h" #include "apr_strings.h" #include "mod_cache.h" #include "mod_disk_cache.h" +#include "http_config.h" +#include "http_log.h" +#include "http_core.h" #include "ap_provider.h" #include "util_filter.h" #include "util_script.h" @@ -56,7 +62,8 @@ /* Forward declarations */ static int remove_entity(cache_handle_t *h); static apr_status_t store_headers(cache_handle_t *h, request_rec *r, cache_info *i); -static apr_status_t store_body(cache_handle_t *h, request_rec *r, apr_bucket_brigade *b); +static apr_status_t store_body(cache_handle_t *h, request_rec *r, apr_bucket_brigade *in, + apr_bucket_brigade *out); static apr_status_t recall_headers(cache_handle_t *h, request_rec *r); static apr_status_t recall_body(cache_handle_t *h, apr_pool_t *p, apr_bucket_brigade *bb); static apr_status_t read_array(request_rec *r, apr_array_header_t* arr, @@ -102,7 +109,7 @@ } } -static void mkdir_structure(disk_cache_conf *conf, const char *file, apr_pool_t *pool) +static apr_status_t mkdir_structure(disk_cache_conf *conf, const char *file, apr_pool_t *pool) { apr_status_t rv; char *p; @@ -116,11 +123,12 @@ rv = apr_dir_make(file, APR_UREAD|APR_UWRITE|APR_UEXECUTE, pool); if (rv != APR_SUCCESS && !APR_STATUS_IS_EEXIST(rv)) { - /* XXX */ + return rv; } *p = '/'; ++p; } + return APR_SUCCESS; } /* htcacheclean may remove directories underneath us. @@ -141,7 +149,9 @@ /* 1000 micro-seconds aka 0.001 seconds. */ apr_sleep(1000); - mkdir_structure(conf, dest, pool); + rv = mkdir_structure(conf, dest, pool); + if (rv != APR_SUCCESS) + continue; rv = apr_file_rename(src, dest, pool); } @@ -150,50 +160,58 @@ return rv; } -static apr_status_t file_cache_el_final(disk_cache_object_t *dobj, +static apr_status_t file_cache_el_final(disk_cache_conf *conf, disk_cache_file_t *file, request_rec *r) { - /* move the data over */ - if (dobj->tfd) { - apr_status_t rv; + apr_status_t rv = APR_SUCCESS; - apr_file_close(dobj->tfd); + /* This assumes that the tempfiles are on the same file system + * as the cache_root. If not, then we need a file copy/move + * rather than a rename. + */ - /* This assumes that the tempfile is on the same file system - * as the cache_root. If not, then we need a file copy/move - * rather than a rename. - */ - rv = apr_file_rename(dobj->tempfile, dobj->datafile, r->pool); + /* move the file over */ + if (file->tempfd) { + + rv = safe_file_rename(conf, file->tempfile, file->file, file->pool); if (rv != APR_SUCCESS) { - ap_log_error(APLOG_MARK, APLOG_WARNING, rv, r->server, - "disk_cache: rename tempfile to datafile failed:" - " %s -> %s", dobj->tempfile, dobj->datafile); - apr_file_remove(dobj->tempfile, r->pool); + ap_log_rerror(APLOG_MARK, APLOG_WARNING, rv, r, + "disk_cache: rename tempfile to file failed:" + " %s -> %s", file->tempfile, file->file); + apr_file_remove(file->tempfile, file->pool); } - dobj->tfd = NULL; + file->tempfd = NULL; } + return rv; +} + +static apr_status_t file_cache_temp_cleanup(void *dummy) { + disk_cache_file_t *file = (disk_cache_file_t *)dummy; + + /* clean up the temporary file */ + if (file->tempfd) { + apr_file_remove(file->tempfile, file->pool); + file->tempfd = NULL; + } + file->tempfile = NULL; + file->pool = NULL; + return APR_SUCCESS; } -static apr_status_t file_cache_errorcleanup(disk_cache_object_t *dobj, request_rec *r) +static apr_status_t file_cache_create(disk_cache_conf *conf, disk_cache_file_t *file, + apr_pool_t *pool) { - /* Remove the header file and the body file. */ - apr_file_remove(dobj->hdrsfile, r->pool); - apr_file_remove(dobj->datafile, r->pool); + file->pool = pool; + file->tempfile = apr_pstrcat(pool, conf->cache_root, AP_TEMPFILE, NULL); - /* If we opened the temporary data file, close and remove it. */ - if (dobj->tfd) { - apr_file_close(dobj->tfd); - apr_file_remove(dobj->tempfile, r->pool); - dobj->tfd = NULL; - } + apr_pool_cleanup_register(pool, file, file_cache_temp_cleanup, apr_pool_cleanup_null); return APR_SUCCESS; } - /* These two functions get and put state information into the data * file for an ap_cache_el, this state information will be read * and written transparent to clients of this module @@ -203,34 +221,33 @@ { apr_status_t rv; char *urlbuff; - disk_cache_info_t disk_info; apr_size_t len; /* read the data from the cache file */ len = sizeof(disk_cache_info_t); - rv = apr_file_read_full(fd, &disk_info, len, &len); + rv = apr_file_read_full(fd, &dobj->disk_info, len, &len); if (rv != APR_SUCCESS) { return rv; } /* Store it away so we can get it later. */ - dobj->disk_info = disk_info; + info->status = dobj->disk_info.status; + info->date = dobj->disk_info.date; + info->expire = dobj->disk_info.expire; + info->request_time = dobj->disk_info.request_time; + info->response_time = dobj->disk_info.response_time; - info->status = disk_info.status; - info->date = disk_info.date; - info->expire = disk_info.expire; - info->request_time = disk_info.request_time; - info->response_time = disk_info.response_time; + memcpy(&info->control, &dobj->disk_info.control, sizeof(cache_control_t)); /* Note that we could optimize this by conditionally doing the palloc * depending upon the size. */ - urlbuff = apr_palloc(r->pool, disk_info.name_len + 1); - len = disk_info.name_len; + urlbuff = apr_palloc(r->pool, dobj->disk_info.name_len + 1); + len = dobj->disk_info.name_len; rv = apr_file_read_full(fd, urlbuff, len, &len); if (rv != APR_SUCCESS) { return rv; } - urlbuff[disk_info.name_len] = '\0'; + urlbuff[dobj->disk_info.name_len] = '\0'; /* check that we have the same URL */ /* Would strncmp be correct? */ @@ -319,17 +336,44 @@ /* * Hook and mod_cache callback functions */ -static int create_entity(cache_handle_t *h, request_rec *r, const char *key, apr_off_t len) +static int create_entity(cache_handle_t *h, request_rec *r, const char *key, apr_off_t len, + apr_bucket_brigade *bb) { + disk_cache_dir_conf *dconf = ap_get_module_config(r->per_dir_config, &disk_cache_module); disk_cache_conf *conf = ap_get_module_config(r->server->module_config, &disk_cache_module); cache_object_t *obj; disk_cache_object_t *dobj; + apr_pool_t *pool; if (conf->cache_root == NULL) { return DECLINED; } + /* we don't support caching of range requests (yet) */ + if (r->status == HTTP_PARTIAL_CONTENT) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "disk_cache: URL %s partial content response not cached", + key); + return DECLINED; + } + + /* Note, len is -1 if unknown so don't trust it too hard */ + if (len > dconf->maxfs) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "disk_cache: URL %s failed the size check " + "(%" APR_OFF_T_FMT " > %" APR_OFF_T_FMT ")", + key, len, dconf->maxfs); + return DECLINED; + } + if (len >= 0 && len < dconf->minfs) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "disk_cache: URL %s failed the size check " + "(%" APR_OFF_T_FMT " < %" APR_OFF_T_FMT ")", + key, len, dconf->minfs); + return DECLINED; + } + /* Allocate and initialize cache_object_t and disk_cache_object_t */ h->cache_obj = obj = apr_pcalloc(r->pool, sizeof(*obj)); obj->vobj = dobj = apr_pcalloc(r->pool, sizeof(*dobj)); @@ -341,10 +385,19 @@ /* Save the cache root */ dobj->root = apr_pstrndup(r->pool, conf->cache_root, conf->cache_root_len); dobj->root_len = conf->cache_root_len; - dobj->datafile = data_file(r->pool, conf, dobj, key); - dobj->hdrsfile = header_file(r->pool, conf, dobj, key); - dobj->tempfile = apr_pstrcat(r->pool, conf->cache_root, AP_TEMPFILE, NULL); + apr_pool_create(&pool, r->pool); + + file_cache_create(conf, &dobj->hdrs, pool); + file_cache_create(conf, &dobj->vary, pool); + file_cache_create(conf, &dobj->data, pool); + + dobj->data.file = data_file(r->pool, conf, dobj, key); + dobj->hdrs.file = header_file(r->pool, conf, dobj, key); + dobj->vary.file = header_file(r->pool, conf, dobj, key); + + dobj->disk_info.header_only = r->header_only; + return OK; } @@ -357,15 +410,12 @@ static int error_logged = 0; disk_cache_conf *conf = ap_get_module_config(r->server->module_config, &disk_cache_module); -#ifdef APR_SENDFILE_ENABLED - core_dir_config *coreconf = ap_get_module_config(r->per_dir_config, - &core_module); -#endif apr_finfo_t finfo; cache_object_t *obj; cache_info *info; disk_cache_object_t *dobj; int flags; + apr_pool_t *pool; h->cache_obj = NULL; @@ -373,8 +423,8 @@ if (conf->cache_root == NULL) { if (!error_logged) { error_logged = 1; - ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, - "disk_cache: Cannot cache files to disk without a CacheRoot specified."); + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "disk_cache: Cannot cache files to disk without a CacheRoot specified."); } return DECLINED; } @@ -392,99 +442,139 @@ dobj->root = apr_pstrndup(r->pool, conf->cache_root, conf->cache_root_len); dobj->root_len = conf->cache_root_len; - dobj->hdrsfile = header_file(r->pool, conf, dobj, key); + dobj->vary.file = header_file(r->pool, conf, dobj, key); flags = APR_READ|APR_BINARY|APR_BUFFERED; - rc = apr_file_open(&dobj->hfd, dobj->hdrsfile, flags, 0, r->pool); + rc = apr_file_open(&dobj->vary.fd, dobj->vary.file, flags, 0, r->pool); if (rc != APR_SUCCESS) { return DECLINED; } /* read the format from the cache file */ len = sizeof(format); - apr_file_read_full(dobj->hfd, &format, len, &len); + apr_file_read_full(dobj->vary.fd, &format, len, &len); if (format == VARY_FORMAT_VERSION) { apr_array_header_t* varray; apr_time_t expire; len = sizeof(expire); - apr_file_read_full(dobj->hfd, &expire, len, &len); + apr_file_read_full(dobj->vary.fd, &expire, len, &len); varray = apr_array_make(r->pool, 5, sizeof(char*)); - rc = read_array(r, varray, dobj->hfd); + rc = read_array(r, varray, dobj->vary.fd); if (rc != APR_SUCCESS) { - ap_log_error(APLOG_MARK, APLOG_ERR, rc, r->server, - "disk_cache: Cannot parse vary header file: %s", - dobj->hdrsfile); + ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r, + "disk_cache: Cannot parse vary header file: %s", + dobj->vary.file); + apr_file_close(dobj->vary.fd); return DECLINED; } - apr_file_close(dobj->hfd); + apr_file_close(dobj->vary.fd); nkey = regen_key(r->pool, r->headers_in, varray, key); dobj->hashfile = NULL; - dobj->prefix = dobj->hdrsfile; - dobj->hdrsfile = header_file(r->pool, conf, dobj, nkey); + dobj->prefix = dobj->vary.file; + dobj->hdrs.file = header_file(r->pool, conf, dobj, nkey); flags = APR_READ|APR_BINARY|APR_BUFFERED; - rc = apr_file_open(&dobj->hfd, dobj->hdrsfile, flags, 0, r->pool); + rc = apr_file_open(&dobj->hdrs.fd, dobj->hdrs.file, flags, 0, r->pool); if (rc != APR_SUCCESS) { return DECLINED; } } else if (format != DISK_FORMAT_VERSION) { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, - "cache_disk: File '%s' has a version mismatch. File had version: %d.", - dobj->hdrsfile, format); + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "disk_cache: File '%s' has a version mismatch. File had version: %d.", + dobj->vary.file, format); + apr_file_close(dobj->vary.fd); return DECLINED; } else { apr_off_t offset = 0; + + /* oops, not vary as it turns out */ + dobj->hdrs.fd = dobj->vary.fd; + dobj->vary.fd = NULL; + dobj->hdrs.file = dobj->vary.file; + /* This wasn't a Vary Format file, so we must seek to the * start of the file again, so that later reads work. */ - apr_file_seek(dobj->hfd, APR_SET, &offset); + apr_file_seek(dobj->hdrs.fd, APR_SET, &offset); nkey = key; } obj->key = nkey; dobj->key = nkey; dobj->name = key; - dobj->datafile = data_file(r->pool, conf, dobj, nkey); - dobj->tempfile = apr_pstrcat(r->pool, conf->cache_root, AP_TEMPFILE, NULL); - /* Open the data file */ - flags = APR_READ|APR_BINARY; -#ifdef APR_SENDFILE_ENABLED - /* When we are in the quick handler we don't have the per-directory - * configuration, so this check only takes the globel setting of - * the EnableSendFile directive into account. - */ - flags |= ((coreconf->enable_sendfile == ENABLE_SENDFILE_OFF) - ? 0 : APR_SENDFILE_ENABLED); -#endif - rc = apr_file_open(&dobj->fd, dobj->datafile, flags, 0, r->pool); + apr_pool_create(&pool, r->pool); + + file_cache_create(conf, &dobj->hdrs, pool); + file_cache_create(conf, &dobj->vary, pool); + file_cache_create(conf, &dobj->data, pool); + + dobj->data.file = data_file(r->pool, conf, dobj, nkey); + + /* Read the bytes to setup the cache_info fields */ + rc = file_cache_recall_mydata(dobj->hdrs.fd, info, dobj, r); if (rc != APR_SUCCESS) { - /* XXX: Log message */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r, + "disk_cache: Cannot read header file %s", dobj->hdrs.file); + apr_file_close(dobj->hdrs.fd); return DECLINED; } - rc = apr_file_info_get(&finfo, APR_FINFO_SIZE, dobj->fd); - if (rc == APR_SUCCESS) { - dobj->file_size = finfo.size; - } + apr_file_close(dobj->hdrs.fd); - /* Read the bytes to setup the cache_info fields */ - rc = file_cache_recall_mydata(dobj->hfd, info, dobj, r); - if (rc != APR_SUCCESS) { - /* XXX log message */ + /* Is this a cached HEAD request? */ + if (dobj->disk_info.header_only && !r->header_only) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r, + "disk_cache: HEAD request cached, non-HEAD requested, ignoring: %s", + dobj->hdrs.file); return DECLINED; } - /* Initialize the cache_handle callback functions */ - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, - "disk_cache: Recalled cached URL info header %s", dobj->name); - return OK; + /* Open the data file */ + if (dobj->disk_info.has_body) { + flags = APR_READ | APR_BINARY; + rc = apr_file_open(&dobj->data.fd, dobj->data.file, flags, 0, r->pool); + if (rc != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r, + "disk_cache: Cannot open data file %s", dobj->data.file); + apr_file_close(dobj->hdrs.fd); + return DECLINED; + } + + rc = apr_file_info_get(&finfo, APR_FINFO_SIZE | APR_FINFO_IDENT, + dobj->data.fd); + if (rc == APR_SUCCESS) { + dobj->file_size = finfo.size; + } + + /* Atomic check - does the body file belong to the header file? */ + if (dobj->disk_info.inode == finfo.inode && + dobj->disk_info.device == finfo.device) { + + /* Initialize the cache_handle callback functions */ + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "disk_cache: Recalled cached URL info header %s", dobj->name); + + return OK; + } + + } + else { + return OK; + } + + /* Oh dear, no luck matching header to the body */ + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "disk_cache: Cached URL info header '%s' didn't match body, ignoring this entry", + dobj->name); + + return DECLINED; } static int remove_entity(cache_handle_t *h) @@ -494,7 +584,7 @@ return OK; } -static int remove_url(cache_handle_t *h, apr_pool_t *p) +static int remove_url(cache_handle_t *h, request_rec *r) { apr_status_t rc; disk_cache_object_t *dobj; @@ -506,35 +596,35 @@ } /* Delete headers file */ - if (dobj->hdrsfile) { - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, NULL, - "disk_cache: Deleting %s from cache.", dobj->hdrsfile); + if (dobj->hdrs.file) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "disk_cache: Deleting %s from cache.", dobj->hdrs.file); - rc = apr_file_remove(dobj->hdrsfile, p); + rc = apr_file_remove(dobj->hdrs.file, r->pool); if ((rc != APR_SUCCESS) && !APR_STATUS_IS_ENOENT(rc)) { /* Will only result in an output if httpd is started with -e debug. * For reason see log_error_core for the case s == NULL. */ - ap_log_error(APLOG_MARK, APLOG_DEBUG, rc, NULL, - "disk_cache: Failed to delete headers file %s from cache.", - dobj->hdrsfile); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rc, r, + "disk_cache: Failed to delete headers file %s from cache.", + dobj->hdrs.file); return DECLINED; } } - /* Delete data file */ - if (dobj->datafile) { - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, NULL, - "disk_cache: Deleting %s from cache.", dobj->datafile); + /* Delete data file */ + if (dobj->data.file) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "disk_cache: Deleting %s from cache.", dobj->data.file); - rc = apr_file_remove(dobj->datafile, p); + rc = apr_file_remove(dobj->data.file, r->pool); if ((rc != APR_SUCCESS) && !APR_STATUS_IS_ENOENT(rc)) { /* Will only result in an output if httpd is started with -e debug. * For reason see log_error_core for the case s == NULL. */ - ap_log_error(APLOG_MARK, APLOG_DEBUG, rc, NULL, - "disk_cache: Failed to delete data file %s from cache.", - dobj->datafile); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rc, r, + "disk_cache: Failed to delete data file %s from cache.", + dobj->data.file); return DECLINED; } } @@ -543,11 +633,11 @@ if (dobj->root) { const char *str_to_copy; - str_to_copy = dobj->hdrsfile ? dobj->hdrsfile : dobj->datafile; + str_to_copy = dobj->hdrs.file ? dobj->hdrs.file : dobj->data.file; if (str_to_copy) { char *dir, *slash, *q; - dir = apr_pstrdup(p, str_to_copy); + dir = apr_pstrdup(r->pool, str_to_copy); /* remove filename */ slash = strrchr(dir, '/'); @@ -564,11 +654,11 @@ * we won't either delete or go above our cache root. */ for (q = dir + dobj->root_len; *q ; ) { - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, NULL, - "disk_cache: Deleting directory %s from cache", - dir); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "disk_cache: Deleting directory %s from cache", + dir); - rc = apr_dir_remove(dir, p); + rc = apr_dir_remove(dir, r->pool); if (rc != APR_SUCCESS && !APR_STATUS_IS_ENOENT(rc)) { break; } @@ -703,9 +793,9 @@ ++maybeASCII; } if (maybeASCII > maybeEBCDIC) { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, - "CGI Interface Error: Script headers apparently ASCII: (CGI = %s)", - r->filename); + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "CGI Interface Error: Script headers apparently ASCII: (CGI = %s)", + r->filename); inbytes_left = outbytes_left = cp - w; apr_xlate_conv_buffer(ap_hdrs_from_ascii, w, &inbytes_left, w, &outbytes_left); @@ -741,8 +831,9 @@ disk_cache_object_t *dobj = (disk_cache_object_t *) h->cache_obj->vobj; /* This case should not happen... */ - if (!dobj->hfd) { - /* XXX log message */ + if (!dobj->hdrs.fd) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "disk_cache: recalling headers; but no header fd for %s", dobj->name); return APR_NOTFOUND; } @@ -750,26 +841,23 @@ h->resp_hdrs = apr_table_make(r->pool, 20); /* Call routine to read the header lines/status line */ - read_table(h, r, h->resp_hdrs, dobj->hfd); - read_table(h, r, h->req_hdrs, dobj->hfd); + read_table(h, r, h->resp_hdrs, dobj->hdrs.fd); + read_table(h, r, h->req_hdrs, dobj->hdrs.fd); - apr_file_close(dobj->hfd); + apr_file_close(dobj->hdrs.fd); - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, - "disk_cache: Recalled headers for URL %s", dobj->name); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "disk_cache: Recalled headers for URL %s", dobj->name); return APR_SUCCESS; } static apr_status_t recall_body(cache_handle_t *h, apr_pool_t *p, apr_bucket_brigade *bb) { - apr_bucket *e; disk_cache_object_t *dobj = (disk_cache_object_t*) h->cache_obj->vobj; - e = apr_bucket_file_create(dobj->fd, 0, (apr_size_t) dobj->file_size, p, - bb->bucket_alloc); - APR_BRIGADE_INSERT_HEAD(bb, e); - e = apr_bucket_eos_create(bb->bucket_alloc); - APR_BRIGADE_INSERT_TAIL(bb, e); + if (dobj->data.fd) { + apr_brigade_insert_file(bb, dobj->data.fd, 0, dobj->file_size, p); + } return APR_SUCCESS; } @@ -810,6 +898,23 @@ static apr_status_t store_headers(cache_handle_t *h, request_rec *r, cache_info *info) { + disk_cache_object_t *dobj = (disk_cache_object_t*) h->cache_obj->vobj; + + memcpy(&h->cache_obj->info, info, sizeof(cache_info)); + + if (r->headers_out) { + dobj->headers_out = ap_cache_cacheable_headers_out(r); + } + + if (r->headers_in) { + dobj->headers_in = ap_cache_cacheable_headers_in(r); + } + + return APR_SUCCESS; +} + +static apr_status_t write_headers(cache_handle_t *h, request_rec *r) +{ disk_cache_conf *conf = ap_get_module_config(r->server->module_config, &disk_cache_module); apr_status_t rv; @@ -819,13 +924,12 @@ disk_cache_info_t disk_info; struct iovec iov[2]; - /* This is flaky... we need to manage the cache_info differently */ - h->cache_obj->info = *info; + memset(&disk_info, 0, sizeof(disk_cache_info_t)); - if (r->headers_out) { + if (dobj->headers_out) { const char *tmp; - tmp = apr_table_get(r->headers_out, "Vary"); + tmp = apr_table_get(dobj->headers_out, "Vary"); if (tmp) { apr_array_header_t* varray; @@ -836,253 +940,377 @@ * vary format hints in the appropriate directory. */ if (dobj->prefix) { - dobj->hdrsfile = dobj->prefix; + dobj->hdrs.file = dobj->prefix; dobj->prefix = NULL; } - mkdir_structure(conf, dobj->hdrsfile, r->pool); + rv = mkdir_structure(conf, dobj->hdrs.file, r->pool); - rv = apr_file_mktemp(&dobj->tfd, dobj->tempfile, + rv = apr_file_mktemp(&dobj->vary.tempfd, dobj->vary.tempfile, APR_CREATE | APR_WRITE | APR_BINARY | APR_EXCL, - r->pool); + dobj->vary.pool); if (rv != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, rv, r, + "disk_cache: could not create temp file %s", + dobj->vary.tempfile); return rv; } amt = sizeof(format); - apr_file_write(dobj->tfd, &format, &amt); + apr_file_write(dobj->vary.tempfd, &format, &amt); - amt = sizeof(info->expire); - apr_file_write(dobj->tfd, &info->expire, &amt); + amt = sizeof(h->cache_obj->info.expire); + apr_file_write(dobj->vary.tempfd, &h->cache_obj->info.expire, &amt); varray = apr_array_make(r->pool, 6, sizeof(char*)); tokens_to_array(r->pool, tmp, varray); - store_array(dobj->tfd, varray); + store_array(dobj->vary.tempfd, varray); - apr_file_close(dobj->tfd); + apr_file_close(dobj->vary.tempfd); - dobj->tfd = NULL; - - rv = safe_file_rename(conf, dobj->tempfile, dobj->hdrsfile, - r->pool); - if (rv != APR_SUCCESS) { - ap_log_error(APLOG_MARK, APLOG_WARNING, rv, r->server, - "disk_cache: rename tempfile to varyfile failed: %s -> %s", - dobj->tempfile, dobj->hdrsfile); - apr_file_remove(dobj->tempfile, r->pool); - return rv; - } - - dobj->tempfile = apr_pstrcat(r->pool, conf->cache_root, AP_TEMPFILE, NULL); - tmp = regen_key(r->pool, r->headers_in, varray, dobj->name); - dobj->prefix = dobj->hdrsfile; + tmp = regen_key(r->pool, dobj->headers_in, varray, dobj->name); + dobj->prefix = dobj->hdrs.file; dobj->hashfile = NULL; - dobj->datafile = data_file(r->pool, conf, dobj, tmp); - dobj->hdrsfile = header_file(r->pool, conf, dobj, tmp); + dobj->data.file = data_file(r->pool, conf, dobj, tmp); + dobj->hdrs.file = header_file(r->pool, conf, dobj, tmp); } } - rv = apr_file_mktemp(&dobj->hfd, dobj->tempfile, + rv = apr_file_mktemp(&dobj->hdrs.tempfd, dobj->hdrs.tempfile, APR_CREATE | APR_WRITE | APR_BINARY | - APR_BUFFERED | APR_EXCL, r->pool); + APR_BUFFERED | APR_EXCL, dobj->hdrs.pool); if (rv != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, rv, r, + "disk_cache: could not create temp file %s", + dobj->hdrs.tempfile); return rv; } disk_info.format = DISK_FORMAT_VERSION; - disk_info.date = info->date; - disk_info.expire = info->expire; + disk_info.date = h->cache_obj->info.date; + disk_info.expire = h->cache_obj->info.expire; disk_info.entity_version = dobj->disk_info.entity_version++; - disk_info.request_time = info->request_time; - disk_info.response_time = info->response_time; - disk_info.status = info->status; + disk_info.request_time = h->cache_obj->info.request_time; + disk_info.response_time = h->cache_obj->info.response_time; + disk_info.status = h->cache_obj->info.status; + disk_info.inode = dobj->disk_info.inode; + disk_info.device = dobj->disk_info.device; + disk_info.has_body = dobj->disk_info.has_body; + disk_info.header_only = dobj->disk_info.header_only; disk_info.name_len = strlen(dobj->name); + memcpy(&disk_info.control, &h->cache_obj->info.control, sizeof(cache_control_t)); + iov[0].iov_base = (void*)&disk_info; iov[0].iov_len = sizeof(disk_cache_info_t); iov[1].iov_base = (void*)dobj->name; iov[1].iov_len = disk_info.name_len; - rv = apr_file_writev(dobj->hfd, (const struct iovec *) &iov, 2, &amt); + rv = apr_file_writev(dobj->hdrs.tempfd, (const struct iovec *) &iov, 2, &amt); if (rv != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, rv, r, + "disk_cache: could not write info to header file %s", + dobj->hdrs.tempfile); + apr_file_close(dobj->hdrs.tempfd); return rv; } - if (r->headers_out) { - apr_table_t *headers_out; - - headers_out = apr_table_overlay(r->pool, r->headers_out, - r->err_headers_out); - headers_out = ap_cache_cacheable_hdrs_out(r->pool, headers_out, - r->server); - - if (!apr_table_get(headers_out, "Content-Type") - && r->content_type) { - apr_table_setn(headers_out, "Content-Type", - ap_make_content_type(r, r->content_type)); - } - - if (!apr_table_get(headers_out, "Content-Encoding") - && r->content_encoding) { - apr_table_setn(headers_out, "Content-Encoding", - r->content_encoding); - } - - rv = store_table(dobj->hfd, headers_out); + if (dobj->headers_out) { + rv = store_table(dobj->hdrs.tempfd, dobj->headers_out); if (rv != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, rv, r, + "disk_cache: could not write out-headers to header file %s", + dobj->hdrs.tempfile); + apr_file_close(dobj->hdrs.tempfd); return rv; } } /* Parse the vary header and dump those fields from the headers_in. */ /* FIXME: Make call to the same thing cache_select calls to crack Vary. */ - if (r->headers_in) { - apr_table_t *headers_in; - - headers_in = ap_cache_cacheable_hdrs_out(r->pool, r->headers_in, - r->server); - rv = store_table(dobj->hfd, headers_in); + if (dobj->headers_in) { + rv = store_table(dobj->hdrs.tempfd, dobj->headers_in); if (rv != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, rv, r, + "disk_cache: could not write in-headers to header file %s", + dobj->hdrs.tempfile); + apr_file_close(dobj->hdrs.tempfd); return rv; } } - apr_file_close(dobj->hfd); /* flush and close */ + apr_file_close(dobj->hdrs.tempfd); /* flush and close */ - /* Remove old file with the same name. If remove fails, then - * perhaps we need to create the directory tree where we are - * about to write the new headers file. - */ - rv = apr_file_remove(dobj->hdrsfile, r->pool); - if (rv != APR_SUCCESS) { - mkdir_structure(conf, dobj->hdrsfile, r->pool); - } - - rv = safe_file_rename(conf, dobj->tempfile, dobj->hdrsfile, r->pool); - if (rv != APR_SUCCESS) { - ap_log_error(APLOG_MARK, APLOG_WARNING, rv, r->server, - "disk_cache: rename tempfile to hdrsfile failed: %s -> %s", - dobj->tempfile, dobj->hdrsfile); - apr_file_remove(dobj->tempfile, r->pool); - return rv; - } - - dobj->tempfile = apr_pstrcat(r->pool, conf->cache_root, AP_TEMPFILE, NULL); - - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, - "disk_cache: Stored headers for URL %s", dobj->name); return APR_SUCCESS; } static apr_status_t store_body(cache_handle_t *h, request_rec *r, - apr_bucket_brigade *bb) + apr_bucket_brigade *in, apr_bucket_brigade *out) { apr_bucket *e; - apr_status_t rv; + apr_status_t rv = APR_SUCCESS; disk_cache_object_t *dobj = (disk_cache_object_t *) h->cache_obj->vobj; - disk_cache_conf *conf = ap_get_module_config(r->server->module_config, - &disk_cache_module); + disk_cache_dir_conf *dconf = ap_get_module_config(r->per_dir_config, &disk_cache_module); + int seen_eos = 0; - /* We write to a temp file and then atomically rename the file over - * in file_cache_el_final(). - */ - if (!dobj->tfd) { - rv = apr_file_mktemp(&dobj->tfd, dobj->tempfile, - APR_CREATE | APR_WRITE | APR_BINARY | - APR_BUFFERED | APR_EXCL, r->pool); - if (rv != APR_SUCCESS) { - return rv; - } - dobj->file_size = 0; + if (!dobj->bb) { + dobj->bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); } + if (!dobj->offset) { + dobj->offset = dconf->readsize; + } + if (!dobj->timeout && dconf->readtime) { + dobj->timeout = apr_time_now() + dconf->readtime; + } - for (e = APR_BRIGADE_FIRST(bb); - e != APR_BRIGADE_SENTINEL(bb); - e = APR_BUCKET_NEXT(e)) - { + if (dobj->offset) { + apr_brigade_partition(in, dobj->offset, &e); + } + + while (APR_SUCCESS == rv && !APR_BRIGADE_EMPTY(in)) { const char *str; apr_size_t length, written; + + e = APR_BRIGADE_FIRST(in); + + /* are we done completely? if so, pass any trailing buckets right through */ + if (dobj->done) { + 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; + dobj->done = 1; + APR_BUCKET_REMOVE(e); + APR_BRIGADE_CONCAT(out, dobj->bb); + 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_CONCAT(out, dobj->bb); + 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(dobj->bb, 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(dobj->bb, e); if (rv != APR_SUCCESS) { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, - "cache_disk: Error when reading bucket for URL %s", - h->cache_obj->key); + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "disk_cache: Error when reading bucket for URL %s", + h->cache_obj->key); /* Remove the intermediate cache file and return non-APR_SUCCESS */ - file_cache_errorcleanup(dobj, r); + apr_pool_destroy(dobj->data.pool); + APR_BRIGADE_CONCAT(out, dobj->bb); return rv; } - rv = apr_file_write_full(dobj->tfd, str, length, &written); + + /* don't write empty buckets to the cache */ + if (!length) { + continue; + } + + /* Attempt to create the data file at the last possible moment, if + * the body is empty, we don't write a file at all, and save an inode. + */ + if (!dobj->data.tempfd) { + apr_finfo_t finfo; + rv = apr_file_mktemp(&dobj->data.tempfd, dobj->data.tempfile, + APR_CREATE | APR_WRITE | APR_BINARY | + APR_BUFFERED | APR_EXCL, dobj->data.pool); + if (rv != APR_SUCCESS) { + apr_pool_destroy(dobj->data.pool); + APR_BRIGADE_CONCAT(out, dobj->bb); + return rv; + } + dobj->file_size = 0; + rv = apr_file_info_get(&finfo, APR_FINFO_IDENT, + dobj->data.tempfd); + if (rv != APR_SUCCESS) { + apr_pool_destroy(dobj->data.pool); + APR_BRIGADE_CONCAT(out, dobj->bb); + return rv; + } + dobj->disk_info.device = finfo.device; + dobj->disk_info.inode = finfo.inode; + dobj->disk_info.has_body = 1; + } + + /* write to the cache, leave if we fail */ + rv = apr_file_write_full(dobj->data.tempfd, str, length, &written); if (rv != APR_SUCCESS) { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, - "cache_disk: Error when writing cache file for URL %s", - h->cache_obj->key); + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "disk_cache: Error when writing cache file for URL %s", + h->cache_obj->key); /* Remove the intermediate cache file and return non-APR_SUCCESS */ - file_cache_errorcleanup(dobj, r); + apr_pool_destroy(dobj->data.pool); + APR_BRIGADE_CONCAT(out, dobj->bb); return rv; } dobj->file_size += written; - if (dobj->file_size > conf->maxfs) { - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, - "cache_disk: URL %s failed the size check " - "(%" APR_OFF_T_FMT " > %" APR_OFF_T_FMT ")", - h->cache_obj->key, dobj->file_size, conf->maxfs); + if (dobj->file_size > dconf->maxfs) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "disk_cache: URL %s failed the size check " + "(%" APR_OFF_T_FMT ">%" APR_OFF_T_FMT ")", + h->cache_obj->key, dobj->file_size, dconf->maxfs); /* Remove the intermediate cache file and return non-APR_SUCCESS */ - file_cache_errorcleanup(dobj, r); + apr_pool_destroy(dobj->data.pool); + APR_BRIGADE_CONCAT(out, dobj->bb); return APR_EGENERAL; } + + /* 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. + */ + dobj->offset -= length; + if (dobj->offset <= 0) { + dobj->offset = 0; + APR_BRIGADE_CONCAT(out, dobj->bb); + break; + } + if ((dconf->readtime && apr_time_now() > dobj->timeout)) { + dobj->timeout = 0; + APR_BRIGADE_CONCAT(out, dobj->bb); + break; + } + } /* Was this the final bucket? If yes, close the temp file and perform * sanity checks. */ - if (APR_BUCKET_IS_EOS(APR_BRIGADE_LAST(bb))) { + if (seen_eos) { const char *cl_header = apr_table_get(r->headers_out, "Content-Length"); + if (dobj->data.tempfd) { + apr_file_close(dobj->data.tempfd); + } + if (r->connection->aborted || r->no_cache) { - ap_log_error(APLOG_MARK, APLOG_INFO, 0, r->server, - "disk_cache: Discarding body for URL %s " - "because connection has been aborted.", - h->cache_obj->key); + ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, + "disk_cache: Discarding body for URL %s " + "because connection has been aborted.", + h->cache_obj->key); /* Remove the intermediate cache file and return non-APR_SUCCESS */ - file_cache_errorcleanup(dobj, r); + apr_pool_destroy(dobj->data.pool); return APR_EGENERAL; } - if (dobj->file_size < conf->minfs) { - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, - "cache_disk: URL %s failed the size check " - "(%" APR_OFF_T_FMT " < %" APR_OFF_T_FMT ")", - h->cache_obj->key, dobj->file_size, conf->minfs); + if (dobj->file_size < dconf->minfs) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "disk_cache: URL %s failed the size check " + "(%" APR_OFF_T_FMT "<%" APR_OFF_T_FMT ")", + h->cache_obj->key, dobj->file_size, dconf->minfs); /* Remove the intermediate cache file and return non-APR_SUCCESS */ - file_cache_errorcleanup(dobj, r); + apr_pool_destroy(dobj->data.pool); return APR_EGENERAL; } if (cl_header) { apr_int64_t cl = apr_atoi64(cl_header); if ((errno == 0) && (dobj->file_size != cl)) { - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, - "disk_cache: URL %s didn't receive complete response, not caching", - h->cache_obj->key); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "disk_cache: URL %s didn't receive complete response, not caching", + h->cache_obj->key); /* Remove the intermediate cache file and return non-APR_SUCCESS */ - file_cache_errorcleanup(dobj, r); + apr_pool_destroy(dobj->data.pool); return APR_EGENERAL; } } - /* All checks were fine. Move tempfile to final destination */ - /* Link to the perm file, and close the descriptor */ - file_cache_el_final(dobj, r); - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, - "disk_cache: Body for URL %s cached.", dobj->name); + /* 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) +{ + disk_cache_conf *conf = ap_get_module_config(r->server->module_config, + &disk_cache_module); + disk_cache_object_t *dobj = (disk_cache_object_t *) h->cache_obj->vobj; + apr_status_t rv; + + /* write the headers to disk at the last possible moment */ + rv = write_headers(h, r); + + /* move header and data tempfiles to the final destination */ + if (APR_SUCCESS == rv) { + rv = file_cache_el_final(conf, &dobj->hdrs, r); + } + if (APR_SUCCESS == rv) { + rv = file_cache_el_final(conf, &dobj->vary, r); + } + if (APR_SUCCESS == rv) { + rv = file_cache_el_final(conf, &dobj->data, r); + } + + /* remove the cached items completely on any failure */ + if (APR_SUCCESS != rv) { + remove_url(h, r); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "disk_cache: commit_entity: URL '%s' not cached due to earlier disk error.", + dobj->name); + } + else { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "disk_cache: commit_entity: Headers and body for URL %s cached.", + dobj->name); + } + + apr_pool_destroy(dobj->data.pool); + + return APR_SUCCESS; +} + +static void *create_dir_config(apr_pool_t *p, char *dummy) +{ + disk_cache_dir_conf *dconf = apr_pcalloc(p, sizeof(disk_cache_dir_conf)); + + dconf->maxfs = DEFAULT_MAX_FILE_SIZE; + dconf->minfs = DEFAULT_MIN_FILE_SIZE; + dconf->readsize = DEFAULT_READSIZE; + dconf->readtime = DEFAULT_READTIME; + + return dconf; +} + +static void *merge_dir_config(apr_pool_t *p, void *basev, void *addv) { + disk_cache_dir_conf *new = (disk_cache_dir_conf *) apr_pcalloc(p, sizeof(disk_cache_dir_conf)); + disk_cache_dir_conf *add = (disk_cache_dir_conf *) addv; + disk_cache_dir_conf *base = (disk_cache_dir_conf *) basev; + + new->maxfs = (add->maxfs_set == 0) ? base->maxfs : add->maxfs; + new->maxfs_set = add->maxfs_set || base->maxfs_set; + new->minfs = (add->minfs_set == 0) ? base->minfs : add->minfs; + new->minfs_set = add->minfs_set || base->minfs_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) { disk_cache_conf *conf = apr_pcalloc(p, sizeof(disk_cache_conf)); @@ -1090,8 +1318,6 @@ /* XXX: Set default values */ conf->dirlevels = DEFAULT_DIRLEVELS; conf->dirlength = DEFAULT_DIRLENGTH; - conf->maxfs = DEFAULT_MAX_FILE_SIZE; - conf->minfs = DEFAULT_MIN_FILE_SIZE; conf->cache_root = NULL; conf->cache_root_len = 0; @@ -1151,11 +1377,10 @@ static const char *set_cache_minfs(cmd_parms *parms, void *in_struct_ptr, const char *arg) { - disk_cache_conf *conf = ap_get_module_config(parms->server->module_config, - &disk_cache_module); + disk_cache_dir_conf *dconf = (disk_cache_dir_conf *)in_struct_ptr; - if (apr_strtoff(&conf->minfs, arg, NULL, 0) != APR_SUCCESS || - conf->minfs < 0) + if (apr_strtoff(&dconf->minfs, arg, NULL, 0) != APR_SUCCESS || + dconf->minfs < 0) { return "CacheMinFileSize argument must be a non-negative integer representing the min size of a file to cache in bytes."; } @@ -1165,16 +1390,46 @@ static const char *set_cache_maxfs(cmd_parms *parms, void *in_struct_ptr, const char *arg) { - disk_cache_conf *conf = ap_get_module_config(parms->server->module_config, - &disk_cache_module); - if (apr_strtoff(&conf->maxfs, arg, NULL, 0) != APR_SUCCESS || - conf->maxfs < 0) + disk_cache_dir_conf *dconf = (disk_cache_dir_conf *)in_struct_ptr; + + if (apr_strtoff(&dconf->maxfs, arg, NULL, 0) != APR_SUCCESS || + dconf->maxfs < 0) { return "CacheMaxFileSize argument must be a non-negative integer representing the max size of a file to cache in bytes."; } return NULL; } +static const char +*set_cache_readsize(cmd_parms *parms, void *in_struct_ptr, const char *arg) +{ + disk_cache_dir_conf *dconf = (disk_cache_dir_conf *)in_struct_ptr; + + if (apr_strtoff(&dconf->readsize, arg, NULL, 0) != APR_SUCCESS || + dconf->readsize < 0) + { + return "CacheReadSize 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) +{ + disk_cache_dir_conf *dconf = (disk_cache_dir_conf *)in_struct_ptr; + apr_off_t milliseconds; + + if (apr_strtoff(&milliseconds, arg, NULL, 0) != APR_SUCCESS || + milliseconds < 0) + { + return "CacheReadTime 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 const command_rec disk_cache_cmds[] = { AP_INIT_TAKE1("CacheRoot", set_cache_root, NULL, RSRC_CONF, @@ -1183,10 +1438,14 @@ "The number of levels of subdirectories in the cache"), AP_INIT_TAKE1("CacheDirLength", set_cache_dirlength, NULL, RSRC_CONF, "The number of characters in subdirectory names"), - AP_INIT_TAKE1("CacheMinFileSize", set_cache_minfs, NULL, RSRC_CONF, + AP_INIT_TAKE1("CacheMinFileSize", set_cache_minfs, NULL, RSRC_CONF | ACCESS_CONF, "The minimum file size to cache a document"), - AP_INIT_TAKE1("CacheMaxFileSize", set_cache_maxfs, NULL, RSRC_CONF, + AP_INIT_TAKE1("CacheMaxFileSize", set_cache_maxfs, NULL, RSRC_CONF | ACCESS_CONF, "The maximum file size to cache a document"), + AP_INIT_TAKE1("CacheReadSize", 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("CacheReadTime", set_cache_readtime, NULL, RSRC_CONF | ACCESS_CONF, + "The maximum time taken to attempt to read and cache in go"), {NULL} }; @@ -1200,6 +1459,7 @@ &create_entity, &open_entity, &remove_url, + &commit_entity }; static void disk_cache_register_hook(apr_pool_t *p) @@ -1211,8 +1471,8 @@ module AP_MODULE_DECLARE_DATA disk_cache_module = { STANDARD20_MODULE_STUFF, - NULL, /* create per-directory config structure */ - NULL, /* merge per-directory config structures */ + create_dir_config, /* create per-directory config structure */ + merge_dir_config, /* merge per-directory config structures */ create_config, /* create per-server config structure */ NULL, /* merge per-server config structures */ disk_cache_cmds, /* command apr_table_t */ Index: modules/cache/mod_disk_cache.h =================================================================== --- modules/cache/mod_disk_cache.h (revision 1026618) +++ modules/cache/mod_disk_cache.h (working copy) @@ -17,12 +17,15 @@ #ifndef MOD_DISK_CACHE_H #define MOD_DISK_CACHE_H +#include "mod_cache.h" +#include "apr_file_io.h" + /* * include for mod_disk_cache: Disk Based HTTP 1.1 Cache. */ -#define VARY_FORMAT_VERSION 3 -#define DISK_FORMAT_VERSION 4 +#define VARY_FORMAT_VERSION 5 +#define DISK_FORMAT_VERSION 6 #define CACHE_HEADER_SUFFIX ".header" #define CACHE_DATA_SUFFIX ".data" @@ -49,27 +52,46 @@ apr_time_t expire; apr_time_t request_time; apr_time_t response_time; + /* The ident of the body file, so we can test the body matches the header */ + apr_ino_t inode; + apr_dev_t device; + /* Does this cached request have a body? */ + int has_body:1; + int header_only:1; + /* The parsed cache control header */ + cache_control_t control; } disk_cache_info_t; +typedef struct { + apr_pool_t *pool; + const char *file; + apr_file_t *fd; + char *tempfile; + apr_file_t *tempfd; +} disk_cache_file_t; + /* * disk_cache_object_t * Pointed to by cache_object_t::vobj */ typedef struct disk_cache_object { - const char *root; /* the location of the cache directory */ + const char *root; /* the location of the cache directory */ apr_size_t root_len; - char *tempfile; /* temp file tohold the content */ const char *prefix; - const char *datafile; /* name of file where the data will go */ - const char *hdrsfile; /* name of file where the hdrs will go */ - const char *hashfile; /* Computed hash key for this URI */ - const char *name; /* Requested URI without vary bits - suitable for mortals. */ - const char *key; /* On-disk prefix; URI with Vary bits (if present) */ - apr_file_t *fd; /* data file */ - apr_file_t *hfd; /* headers file */ - apr_file_t *tfd; /* temporary file for data */ - apr_off_t file_size; /* File size of the cached data file */ + disk_cache_file_t data; /* data file structure */ + disk_cache_file_t hdrs; /* headers file structure */ + disk_cache_file_t vary; /* vary file structure */ + const char *hashfile; /* Computed hash key for this URI */ + 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 */ disk_cache_info_t disk_info; /* Header information. */ + apr_bucket_brigade *bb; /* Set aside brigade */ + apr_table_t *headers_in; /* Input headers to save */ + apr_table_t *headers_out; /* Output headers to save */ + apr_off_t offset; /* Max size to set aside */ + apr_time_t timeout; /* Max time to set aside */ + int done:1; /* Is the attempt to cache complete? */ } disk_cache_object_t; @@ -78,18 +100,29 @@ */ /* TODO: Make defaults OS specific */ #define CACHEFILE_LEN 20 /* must be less than HASH_LEN/2 */ -#define DEFAULT_DIRLEVELS 3 +#define DEFAULT_DIRLEVELS 2 #define DEFAULT_DIRLENGTH 2 #define DEFAULT_MIN_FILE_SIZE 1 #define DEFAULT_MAX_FILE_SIZE 1000000 +#define DEFAULT_READSIZE 0 +#define DEFAULT_READTIME 0 typedef struct { const char* cache_root; apr_size_t cache_root_len; int dirlevels; /* Number of levels of subdirectories */ int dirlength; /* Length of subdirectory names */ +} disk_cache_conf; + +typedef struct { apr_off_t minfs; /* minimum file size for cached files */ apr_off_t maxfs; /* maximum file size for cached files */ -} disk_cache_conf; + 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 */ + int minfs_set:1; + int maxfs_set:1; + int readsize_set:1; + int readtime_set:1; +} disk_cache_dir_conf; #endif /*MOD_DISK_CACHE_H*/ Index: modules/cache/cache_cache.c =================================================================== --- modules/cache/cache_cache.c (revision 1026618) +++ modules/cache/cache_cache.c (working copy) @@ -28,6 +28,8 @@ #include #endif +APLOG_USE_MODULE(cache); + struct cache_cache_t { int max_entries; apr_size_t max_size; @@ -121,7 +123,6 @@ CACHE_HASH_KEY_STRING, NULL); - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, NULL, "Cache Purge of %s",c->key_entry(ejected)); c->current_size -= c->size_entry(ejected); c->free_entry(ejected); c->total_purges++; Index: modules/cache/cache_storage.c =================================================================== --- modules/cache/cache_storage.c (revision 1026618) +++ modules/cache/cache_storage.c (working copy) @@ -14,10 +14,11 @@ * limitations under the License. */ -#define CORE_PRIVATE - #include "mod_cache.h" +#include "cache_storage.h" +#include "cache_util.h" + extern APR_OPTIONAL_FN_TYPE(ap_cache_generate_key) *cache_generate_key; extern module AP_MODULE_DECLARE_DATA cache_module; @@ -28,7 +29,7 @@ * delete all URL entities from the cache * */ -int cache_remove_url(cache_request_rec *cache, apr_pool_t *p) +int cache_remove_url(cache_request_rec *cache, request_rec *r) { cache_provider_list *list; cache_handle_t *h; @@ -43,12 +44,12 @@ if (!h) { return OK; } - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, NULL, + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "cache: Removing url %s from the cache", h->cache_obj->key); /* for each specified cache type, delete the URL */ while(list) { - list->provider->remove_url(h, p); + list->provider->remove_url(h, r); list = list->next; } return OK; @@ -66,24 +67,32 @@ * decide whether or not it wants to cache this particular entity. * If the size is unknown, a size of -1 should be set. */ -int cache_create_entity(request_rec *r, apr_off_t size) +int cache_create_entity(cache_request_rec *cache, request_rec *r, + apr_off_t size, apr_bucket_brigade *in) { cache_provider_list *list; cache_handle_t *h = apr_pcalloc(r->pool, sizeof(cache_handle_t)); - char *key; apr_status_t rv; - cache_request_rec *cache = (cache_request_rec *) - ap_get_module_config(r->request_config, &cache_module); - rv = cache_generate_key(r, r->pool, &key); - if (rv != APR_SUCCESS) { - return rv; + if (!cache) { + /* This should never happen */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, APR_EGENERAL, r, + "cache: No cache request information available for key" + " generation"); + return APR_EGENERAL; } + if (!cache->key) { + rv = cache_generate_key(r, r->pool, &cache->key); + if (rv != APR_SUCCESS) { + return rv; + } + } + list = cache->providers; /* for each specified cache type, delete the URL */ while (list) { - switch (rv = list->provider->create_entity(h, r, key, size)) { + switch (rv = list->provider->create_entity(h, r, cache->key, size, in)) { case OK: { cache->handle = h; cache->provider = list->provider; @@ -108,8 +117,12 @@ return 1; } -CACHE_DECLARE(void) ap_cache_accept_headers(cache_handle_t *h, request_rec *r, - int preserve_orig) +/** + * Take headers from the cache, and overlap them over the existing response + * headers. + */ +void cache_accept_headers(cache_handle_t *h, request_rec *r, + int preserve_orig) { apr_table_t *cookie_table, *hdr_copy; const char *v; @@ -180,33 +193,46 @@ * This function returns OK if successful, DECLINED if no * cached entity fits the bill. */ -int cache_select(request_rec *r) +int cache_select(cache_request_rec *cache, request_rec *r) { cache_provider_list *list; apr_status_t rv; cache_handle_t *h; - char *key; - cache_request_rec *cache = (cache_request_rec *) - ap_get_module_config(r->request_config, &cache_module); - rv = cache_generate_key(r, r->pool, &key); - if (rv != APR_SUCCESS) { - return rv; + if (!cache) { + /* This should never happen */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, APR_EGENERAL, r, + "cache: No cache request information available for key" + " generation"); + return DECLINED; } + + if (!cache->key) { + rv = cache_generate_key(r, r->pool, &cache->key); + if (rv != APR_SUCCESS) { + return DECLINED; + } + } + + if (!ap_cache_check_allowed(cache, r)) { + return DECLINED; + } + /* go through the cache types till we get a match */ h = apr_palloc(r->pool, sizeof(cache_handle_t)); list = cache->providers; while (list) { - switch ((rv = list->provider->open_entity(h, r, key))) { + switch ((rv = list->provider->open_entity(h, r, cache->key))) { case OK: { char *vary = NULL; - int fresh; + int fresh, mismatch = 0; if (list->provider->recall_headers(h, r) != APR_SUCCESS) { - /* TODO: Handle this error */ - return DECLINED; + /* try again with next cache type */ + list = list->next; + continue; } /* @@ -254,29 +280,45 @@ } else { /* headers do not match, so Vary failed */ - ap_log_error(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, - r->server, - "cache_select_url(): Vary header mismatch."); - return DECLINED; + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, + r, "cache_select_url(): Vary header mismatch."); + mismatch = 1; } } + /* no vary match, try next provider */ + if (mismatch) { + /* try again with next cache type */ + list = list->next; + continue; + } + cache->provider = list->provider; cache->provider_name = list->provider_name; /* Is our cached response fresh enough? */ - fresh = ap_cache_check_freshness(h, r); + fresh = cache_check_freshness(h, cache, r); if (!fresh) { const char *etag, *lastmod; - ap_log_error(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r->server, - "Cached response for %s isn't fresh. Adding/replacing " - "conditional request headers.", r->uri); + /* Cache-Control: only-if-cached and revalidation required, try + * the next provider + */ + if (cache->control_in.only_if_cached) { + /* try again with next cache type */ + list = list->next; + continue; + } - /* Make response into a conditional */ + /* set aside the stale entry for accessing later */ cache->stale_headers = apr_table_copy(r->pool, - r->headers_in); + r->headers_in); + cache->stale_handle = h; + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r, + "Cached response for %s isn't fresh. Adding/replacing " + "conditional request headers.", r->uri); + /* We can only revalidate with our own conditionals: remove the * conditions from the original request. */ @@ -286,13 +328,6 @@ apr_table_unset(r->headers_in, "If-Range"); apr_table_unset(r->headers_in, "If-Unmodified-Since"); - /* - * Do not do Range requests with our own conditionals: If - * we get 304 the Range does not matter and otherwise the - * entity changed and we want to have the complete entity - */ - apr_table_unset(r->headers_in, "Range"); - etag = apr_table_get(h->resp_hdrs, "ETag"); lastmod = apr_table_get(h->resp_hdrs, "Last-Modified"); @@ -307,30 +342,24 @@ if (lastmod) { apr_table_set(r->headers_in, "If-Modified-Since", - lastmod); + lastmod); } - cache->stale_handle = h; - } - else { - int irv; /* - * The copy isn't fresh enough, but we cannot revalidate. - * So it is the same case as if there had not been a cached - * entry at all. Thus delete the entry from cache. + * Do not do Range requests with our own conditionals: If + * we get 304 the Range does not matter and otherwise the + * entity changed and we want to have the complete entity */ - irv = cache->provider->remove_url(h, r->pool); - if (irv != OK) { - ap_log_error(APLOG_MARK, APLOG_DEBUG, irv, r->server, - "cache: attempt to remove url from cache unsuccessful."); - } + apr_table_unset(r->headers_in, "Range"); + } + /* ready to revalidate, pretend we were never here */ return DECLINED; } /* Okay, this response looks okay. Merge in our stuff and go. */ - ap_cache_accept_headers(h, r, 0); + cache_accept_headers(h, r, 0); cache->handle = h; return OK; @@ -346,35 +375,31 @@ } } } + + /* if Cache-Control: only-if-cached, and not cached, return 504 */ + if (cache->control_in.only_if_cached) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r, + "cache: 'only-if-cached' requested and no cached entity, " + "returning 504 Gateway Timeout for: %s", r->uri); + return HTTP_GATEWAY_TIME_OUT; + } + return DECLINED; } apr_status_t cache_generate_key_default(request_rec *r, apr_pool_t* p, - char**key) + const char **key) { cache_server_conf *conf; - cache_request_rec *cache; char *port_str, *hn, *lcs; const char *hostname, *scheme; int i; char *path, *querystring; - cache = (cache_request_rec *) ap_get_module_config(r->request_config, - &cache_module); - if (!cache) { - /* This should never happen */ - ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, - "cache: No cache request information available for key" - " generation"); - *key = ""; - return APR_EGENERAL; - } - if (cache->key) { + if (*key) { /* * We have been here before during the processing of this request. - * So return the key we already have. */ - *key = apr_pstrdup(p, cache->key); return APR_SUCCESS; } @@ -404,11 +429,16 @@ * in the reverse proxy case. */ if (!r->proxyreq || (r->proxyreq == PROXYREQ_REVERSE)) { - /* Use _default_ as the hostname if none present, as in mod_vhost */ - hostname = ap_get_server_name(r); - if (!hostname) { - hostname = "_default_"; + if (conf->base_uri && conf->base_uri->hostname) { + hostname = conf->base_uri->hostname; } + else { + /* Use _default_ as the hostname if none present, as in mod_vhost */ + hostname = ap_get_server_name(r); + if (!hostname) { + hostname = "_default_"; + } + } } else if(r->parsed_uri.hostname) { /* Copy the parsed uri hostname */ @@ -439,7 +469,12 @@ scheme = lcs; } else { - scheme = ap_http_scheme(r); + if (conf->base_uri && conf->base_uri->scheme) { + scheme = conf->base_uri->scheme; + } + else { + scheme = ap_http_scheme(r); + } } /* @@ -450,7 +485,7 @@ * scheme - if available. Otherwise use the port-number of the current * server. */ - if(r->proxyreq && (r->proxyreq != PROXYREQ_REVERSE)) { + if (r->proxyreq && (r->proxyreq != PROXYREQ_REVERSE)) { if (r->parsed_uri.port_str) { port_str = apr_pcalloc(p, strlen(r->parsed_uri.port_str) + 2); port_str[0] = ':'; @@ -471,8 +506,16 @@ } } else { - /* Use the server port */ - port_str = apr_psprintf(p, ":%u", ap_get_server_port(r)); + if (conf->base_uri && conf->base_uri->port_str) { + port_str = conf->base_uri->port_str; + } + else if (conf->base_uri && conf->base_uri->hostname) { + port_str = ""; + } + else { + /* Use the server port */ + port_str = apr_psprintf(p, ":%u", ap_get_server_port(r)); + } } /* @@ -500,28 +543,60 @@ && (*(param + len + 1) == '=') && !strchr(param + len + 2, '/')) { path = apr_pstrndup(p, path, param - path); - break; + continue; } /* * Check if the identifier is in the querystring and cut it out. */ - if (querystring - && (param = strstr(querystring, *identifier)) - && (*(param + len) == '=') - ) { - char *amp; - - if (querystring != param) { - querystring = apr_pstrndup(p, querystring, - param - querystring); + if (querystring) { + /* + * First check if the identifier is at the beginning of the + * querystring and followed by a '=' + */ + if (!strncmp(querystring, *identifier, len) + && (*(querystring + len) == '=')) { + param = querystring; } else { - querystring = ""; + char *complete; + + /* + * In order to avoid subkey matching (PR 48401) prepend + * identifier with a '&' and append a '=' + */ + complete = apr_pstrcat(p, "&", *identifier, "=", NULL); + param = strstr(querystring, complete); + /* If we found something we are sitting on the '&' */ + if (param) { + param++; + } } - if ((amp = strchr(param + len + 1, '&'))) { - querystring = apr_pstrcat(p, querystring, amp + 1, NULL); + if (param) { + char *amp; + + if (querystring != param) { + querystring = apr_pstrndup(p, querystring, + param - querystring); + } + else { + querystring = ""; + } + + if ((amp = strchr(param + len + 1, '&'))) { + querystring = apr_pstrcat(p, querystring, amp + 1, NULL); + } + else { + /* + * If querystring is not "", then we have the case + * that the identifier parameter we removed was the + * last one in the original querystring. Hence we have + * a trailing '&' which needs to be removed. + */ + if (*querystring) { + querystring[strlen(querystring) - 1] = '\0'; + } + } } - break; } } } @@ -544,10 +619,9 @@ * resource in the cache under a key where it is never found by the quick * handler during following requests. */ - cache->key = apr_pstrdup(r->pool, *key); - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, NULL, - "cache: Key for entity %s?%s is %s", r->uri, - r->parsed_uri.query, *key); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r, + "cache: Key for entity %s?%s is %s", r->uri, + r->parsed_uri.query, *key); return APR_SUCCESS; } Index: modules/cache/cache_pqueue.h =================================================================== --- modules/cache/cache_pqueue.h (revision 1026618) +++ modules/cache/cache_pqueue.h (working copy) @@ -70,7 +70,7 @@ * @param get the callback function to get the current element's position * @param set the callback function to set the current element's position * - * @Return the handle or NULL for insufficent memory + * @return the handle or NULL for insufficent memory */ cache_pqueue_t *cache_pq_init(apr_ssize_t n, cache_pqueue_get_priority pri, @@ -108,15 +108,14 @@ /** * pop the highest-ranking item from the queue. - * @param p the queue - * @param d where to copy the entry to + * @param q the queue * @return NULL on error, otherwise the entry */ void *cache_pq_pop(cache_pqueue_t *q); /** * remove an item from the queue. - * @param p the queue + * @param q the queue * @param d the entry * @return APR_SUCCESS on success */ @@ -125,7 +124,6 @@ /** * access highest-ranking item without removing it. * @param q the queue - * @param d the entry * @return NULL on error, otherwise the entry */ void *cache_pq_peek(cache_pqueue_t *q); @@ -136,7 +134,7 @@ * DEBUG function only * @param q the queue * @param out the output handle - * @param the callback function to print the entry + * @param print the callback function to print the entry */ void cache_pq_print(cache_pqueue_t *q, FILE *out, @@ -148,7 +146,7 @@ * debug function only * @param q the queue * @param out the output handle - * @param the callback function to print the entry + * @param print the callback function to print the entry */ void cache_pq_dump(cache_pqueue_t *q, FILE *out, Index: modules/cache/cache_storage.h =================================================================== --- modules/cache/cache_storage.h (revision 0) +++ modules/cache/cache_storage.h (revision 0) @@ -0,0 +1,61 @@ +/* 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. + */ + +/** + * @file cache_storage.h + * @brief Cache Storage Functions + * + * @defgroup Cache_storage Cache Storage Functions + * @ingroup MOD_CACHE + * @{ + */ + +#ifndef CACHE_STORAGE_H +#define CACHE_STORAGE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "mod_cache.h" +#include "cache_util.h" + +/** + * cache_storage.c + */ +int cache_remove_url(cache_request_rec *cache, request_rec *r); +int cache_create_entity(cache_request_rec *cache, request_rec *r, + apr_off_t size, apr_bucket_brigade *in); +int cache_select(cache_request_rec *cache, request_rec *r); +apr_status_t cache_generate_key_default(request_rec *r, apr_pool_t* p, + const char **key); + +/** + * Merge in cached headers into the response + * @param h cache_handle_t + * @param r request_rec + * @param preserve_orig If 1, the values in r->headers_out are preserved. + * Otherwise, they are overwritten by the cached value. + */ +void cache_accept_headers(cache_handle_t *h, request_rec *r, + int preserve_orig); + +#ifdef __cplusplus +} +#endif + +#endif /* !CACHE_STORAGE_H */ +/** @} */