mod_cache_socache.c revision d2fcf1d01ba9f36b78eb7b66fa5af8237b79ac70
/* 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
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "apr_lib.h"
#include "apr_file_io.h"
#include "apr_strings.h"
#include "apr_buckets.h"
#include "httpd.h"
#include "http_config.h"
#include "http_log.h"
#include "http_core.h"
#include "http_protocol.h"
#include "ap_provider.h"
#include "ap_socache.h"
#include "util_filter.h"
#include "util_script.h"
#include "util_charset.h"
#include "util_mutex.h"
#include "mod_cache.h"
#include "mod_status.h"
#include "cache_socache_common.h"
/*
* mod_cache_socache: Shared Object Cache Based HTTP 1.1 Cache.
*
* Flow to Find the entry:
* Fetch URI key (may contain Format #1 or Format #2)
* If format #1 (Contains a list of Vary Headers):
* Use each header name (from .header) with our request values (headers_in) to
* re-read in key (must be format #2)
*
* Format #1:
* apr_uint32_t format;
* apr_time_t expire;
* apr_array_t vary_headers (delimited by CRLF)
*
* Format #2:
* cache_socache_info_t (first sizeof(apr_uint32_t) bytes is the format)
* entity name (sobj->name) [length is in cache_socache_info_t->name_len]
* r->headers_out (delimited by CRLF)
* CRLF
* r->headers_in (delimited by CRLF)
* CRLF
*/
/*
* cache_socache_object_t
* Pointed to by cache_object_t::vobj
*/
typedef struct cache_socache_object_t
{
unsigned char *buffer; /* the cache buffer */
const char *name; /* Requested URI without vary bits - suitable for mortals. */
const char *key; /* On-disk prefix; URI with Vary bits (if present) */
/*
* mod_cache_socache configuration
*/
#define DEFAULT_MAXTIME 86400
#define DEFAULT_MINTIME 600
#define DEFAULT_READSIZE 0
#define DEFAULT_READTIME 0
typedef struct cache_socache_provider_conf
{
const char *args;
typedef struct cache_socache_conf
{
typedef struct cache_socache_dir_conf
{
unsigned int max_set :1;
unsigned int maxtime_set :1;
unsigned int mintime_set :1;
unsigned int readsize_set :1;
unsigned int readtime_set :1;
/* Shared object cache and mutex */
static const char * const cache_socache_id = "cache-socache";
/*
* Local static functions
*/
{
while (*slider < buffer_len) {
(*slider)++;
return APR_SUCCESS;
}
(*slider)++;
(*slider)++;
}
}
(*slider)++;
return APR_SUCCESS;
}
else {
(*slider)++;
}
}
return APR_EOF;
}
{
int i, len;
const char **elts;
return APR_EOF;
}
}
if (buffer) {
}
return APR_SUCCESS;
}
{
;
while (*slider < buffer_len) {
if (!colon) {
}
(*slider)++;
}
(*slider)++;
(*slider)++;
}
return APR_SUCCESS;
}
"Premature end of cache headers.");
return APR_EGENERAL;
}
colon++;
}
(*slider)++;
(*slider)++;
}
colon = 0;
}
(*slider)++;
return APR_SUCCESS;
}
else {
(*slider)++;
}
}
return APR_EOF;
}
{
int i, len;
return APR_EOF;
}
}
}
return APR_EOF;
}
if (buffer) {
}
return APR_SUCCESS;
}
{
int i, k;
int nvec;
const char *header;
const char **elts;
/* TODO:
* - Handle multiple-value headers better. (sort them?)
* - Handle Case in-sensitive Values better.
* This isn't the end of the world, since it just lowers the cache
* hit rate, but it would be nice to fix.
*
* The majority are case insenstive if they are values (encoding etc).
* Most of rfc2616 is case insensitive on header contents.
*
* So the better solution may be to identify headers which should be
* treated case-sensitive?
* HTTP URI's (3.2.3) [host and scheme are insensitive]
* HTTP method (5.1.1)
* HTTP-date values (3.3.1)
* 3.7 Media Types [exerpt]
* The type, subtype, and parameter attribute names are case-
* insensitive. Parameter values might or might not be case-sensitive,
* depending on the semantics of the parameter name.
* 4.20 Except [exerpt]
* Comparison of expectation values is case-insensitive for unquoted
* tokens (including the 100-continue token), and is case-sensitive for
* quoted-string expectation-extensions.
*/
if (!header) {
header = "";
}
k++;
k++;
}
k++;
}
{
}
{
char *token;
}
/* Sort it so that "Vary: A, B" and "Vary: B, A" are stored the same. */
}
/*
* Hook and mod_cache callback functions
*/
{
return DECLINED;
}
/* we don't support caching of range requests (yet) */
/* TODO: but we could */
if (r->status == HTTP_PARTIAL_CONTENT) {
"URL %s partial content response not cached",
key);
return DECLINED;
}
/*
* We have a chicken and egg problem. We don't know until we
* attempt to store_headers just how big the response will be
* and whether it will fit in the cache limits set. But we
* need to make a decision now as to whether we plan to try.
* If we make the wrong decision, we could prevent another
* cache implementation, such as cache_disk, from getting the
* opportunity to cache, and that would be unfortunate.
*
* In a series of tests, from cheapest to most expensive,
* decide whether or not to ignore this attempt to cache,
* with a small margin just to be sure.
*/
if (len < 0) {
"URL '%s' had no explicit size, ignoring", key);
return DECLINED;
}
"URL '%s' body larger than limit, ignoring "
return DECLINED;
}
/* estimate the total cached size, given current headers */
&total)) {
"URL '%s' estimated headers size larger than limit, ignoring "
return DECLINED;
}
"URL '%s' body and headers larger than limit, ignoring "
return DECLINED;
}
/* Allocate and initialize cache_object_t and cache_socache_object_t */
return OK;
}
{
unsigned int buffer_len;
const char *nkey;
return DECLINED;
}
/* Create and init the cache object */
/* Create a temporary pool for the buffer, and destroy it if something
* goes wrong so we don't have large buffers of unused memory hanging
* about for the lifetime of the response.
*/
/* attempt to retrieve the cached entry */
if (socache_mutex) {
if (status != APR_SUCCESS) {
return DECLINED;
}
}
if (socache_mutex) {
if (status != APR_SUCCESS) {
return DECLINED;
}
}
if (rc != APR_SUCCESS) {
"Key not found in cache: %s", key);
return DECLINED;
}
"Key found in cache but too big, ignoring: %s", key);
return DECLINED;
}
/* read the format from the cache file */
if (format == CACHE_SOCACHE_VARY_FORMAT_VERSION) {
if (rc != APR_SUCCESS) {
"Cannot parse vary entry for key: %s", key);
return DECLINED;
}
/* attempt to retrieve the cached entry */
if (socache_mutex) {
if (status != APR_SUCCESS) {
return DECLINED;
}
}
&buffer_len, r->pool);
if (socache_mutex) {
if (status != APR_SUCCESS) {
return DECLINED;
}
}
if (rc != APR_SUCCESS) {
"Key not found in cache: %s", key);
return DECLINED;
}
"Key found in cache but too big, ignoring: %s", key);
goto fail;
}
}
else if (format != CACHE_SOCACHE_DISK_FORMAT_VERSION) {
"Key '%s' found in cache has version %d, expected %d, ignoring",
goto fail;
}
else {
}
if (buffer_len >= sizeof(cache_socache_info_t)) {
}
else {
"Cache entry for key '%s' too short, removing", nkey);
goto fail;
}
slider = sizeof(cache_socache_info_t);
/* Store it away so we can get it later. */
"Cache entry for key '%s' URL mismatch, ignoring", nkey);
return DECLINED;
}
}
else {
"Cache entry for key '%s' too short, removing", nkey);
goto fail;
}
/* Is this a cached HEAD request? */
"HEAD request cached, non-HEAD requested, ignoring: %s",
return DECLINED;
}
&slider)) {
"Cache entry for key '%s' response headers unreadable, removing", nkey);
goto fail;
}
&slider)) {
"Cache entry for key '%s' request headers unreadable, removing", nkey);
goto fail;
}
/* Retrieve the body if we have one */
/*
* Optimisation: if the body is small, we want to make a
* copy of the body and free the temporary pool, as we
* don't want large blocks of unused memory hanging around
* to the end of the response. In contrast, if the body is
* large, we would rather leave the body where it is in the
* temporary pool, and save ourselves the copy.
*/
apr_bucket *e;
/* large - use the brigade as is, we're done */
}
else {
/* small - make a copy of the data... */
/* ...and get rid of the large memory buffer */
}
/* make the configuration stick */
return OK;
fail:
if (socache_mutex) {
if (status != APR_SUCCESS) {
return DECLINED;
}
}
if (socache_mutex) {
if (status != APR_SUCCESS) {
}
}
return DECLINED;
}
static int remove_entity(cache_handle_t *h)
{
/* Null out the cache object pointer so next time we start from scratch */
return OK;
}
{
if (!sobj) {
return DECLINED;
}
/* Remove the key from the cache */
if (socache_mutex) {
if (status != APR_SUCCESS) {
return DECLINED;
}
}
if (socache_mutex) {
if (status != APR_SUCCESS) {
return DECLINED;
}
}
return OK;
}
{
/* we recalled the headers during open_entity, so do nothing */
return APR_SUCCESS;
}
{
apr_bucket *e;
}
return APR_SUCCESS;
}
{
if (r->headers_out) {
}
if (r->headers_in) {
}
if (sobj->headers_out) {
const char *vary;
if (vary) {
"buffer too small for Vary array, caching aborted: %s",
return rv;
}
if (socache_mutex) {
if (status != APR_SUCCESS) {
return status;
}
}
if (socache_mutex) {
if (status != APR_SUCCESS) {
}
}
if (rv != APR_SUCCESS) {
return rv;
}
}
}
}
else {
}
slider = sizeof(cache_socache_info_t);
"cache buffer too small for name: %s",
return APR_EGENERAL;
}
if (sobj->headers_out) {
return APR_EGENERAL;
}
}
/* Parse the vary header and dump those fields from the headers_in. */
/* TODO: Make call to the same thing cache_select calls to crack Vary. */
if (sobj->headers_in) {
"in-headers didn't fit in buffer %s",
return APR_EGENERAL;
}
}
return APR_SUCCESS;
}
{
apr_bucket *e;
int seen_eos = 0;
}
}
}
else {
r->connection->bucket_alloc);
}
}
}
const char *str;
e = APR_BRIGADE_FIRST(in);
/* are we done completely? if so, pass any trailing buckets right through */
continue;
}
/* have we seen eos yet? */
if (APR_BUCKET_IS_EOS(e)) {
seen_eos = 1;
break;
}
/* honour flush buckets, we'll get called again */
if (APR_BUCKET_IS_FLUSH(e)) {
break;
}
/* metadata buckets are preserved as is */
if (APR_BUCKET_IS_METADATA(e)) {
continue;
}
/* read the bucket, write to the cache */
if (rv != APR_SUCCESS) {
"Error when reading bucket for URL %s",
/* Remove the intermediate cache file and return non-APR_SUCCESS */
return rv;
}
/* don't write empty buckets to the cache */
if (!length) {
continue;
}
"URL %s failed the buffer size check "
return APR_EGENERAL;
}
rv = apr_bucket_copy(e, &e);
if (rv != APR_SUCCESS) {
"Error when copying bucket for URL %s",
return rv;
}
/* 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.
*/
break;
}
break;
}
}
/* Was this the final bucket? If yes, perform sanity checks.
*/
if (seen_eos) {
"Discarding body for URL %s "
"because connection has been aborted.",
return APR_EGENERAL;
}
if (cl_header) {
"URL %s didn't receive complete response, not caching",
return APR_EGENERAL;
}
}
/* All checks were fine, we're good to go when the commit comes */
}
return APR_SUCCESS;
}
{
/* flatten the body into the buffer */
if (APR_SUCCESS != rv) {
"could not flatten brigade, not caching: %s",
goto fail;
}
"body too big for the cache buffer, not caching: %s",
goto fail;
}
if (socache_mutex) {
if (status != APR_SUCCESS) {
return rv;
}
}
if (socache_mutex) {
if (status != APR_SUCCESS) {
return DECLINED;
}
}
if (rv != APR_SUCCESS) {
goto fail;
}
"commit_entity: Headers and body for URL %s cached for maximum of %d seconds.",
return APR_SUCCESS;
fail:
/* For safety, remove any existing entry on failure, just in case it could not
* be revalidated successfully.
*/
if (socache_mutex) {
if (status != APR_SUCCESS) {
return rv;
}
}
if (socache_mutex) {
if (status != APR_SUCCESS) {
}
}
return rv;
}
{
/* mark the entity as invalidated */
return commit_entity(h, r);
}
{
apr_pcalloc(p, sizeof(cache_socache_dir_conf));
return dconf;
}
{
*new =
return new;
}
{
return conf;
}
{
/* socache server config only has one field */
return ps;
}
/*
* mod_cache_socache configuration directives handlers.
*/
const char *arg)
{
/* Argument is of form 'name:args' or just 'name'. */
if (sep) {
sep++;
}
else {
}
"Unknown socache provider '%s'. Maybe you need "
"to load the appropriate socache module "
}
return err;
}
const char *arg)
{
< 1024) {
return "CacheSocacheMaxSize argument must be a integer representing the max size of a cached entry (headers and body), at least 1024";
}
return NULL;
}
const char *arg)
{
return "CacheSocacheMaxTime argument must be the maximum amount of time in seconds to cache an entry.";
}
return NULL;
}
const char *arg)
{
return "CacheSocacheMinTime argument must be the minimum amount of time in seconds to cache an entry.";
}
return NULL;
}
const char *arg)
{
return "CacheSocacheReadSize argument must be a non-negative integer representing the max amount of data to cache in go.";
}
return NULL;
}
const char *arg)
{
|| milliseconds < 0) {
return "CacheSocacheReadTime argument must be a non-negative integer representing the max amount of time taken to cache in go.";
}
return NULL;
}
{
if (socache_mutex) {
}
return APR_SUCCESS;
}
{
server_rec *s = data;
}
return APR_SUCCESS;
}
{
return DECLINED;
}
if (!(flags & AP_STATUS_SHORT)) {
ap_rputs("<hr>\n"
"<table cellspacing=0 cellpadding=0>\n"
"<tr><td bgcolor=\"#000000\">\n"
"<b><font color=\"#ffffff\" face=\"Arial,Helvetica\">"
"mod_cache_socache Status:</font></b>\n"
"</td></tr>\n"
"<tr><td bgcolor=\"#ffffff\">\n", r);
}
else {
ap_rputs("ModCacheSocacheStatus\n", r);
}
if (socache_mutex) {
if (status != APR_SUCCESS) {
"could not acquire lock for cache status");
}
}
if (status != APR_SUCCESS) {
if (!(flags & AP_STATUS_SHORT)) {
ap_rputs("No cache status data available\n", r);
}
else {
ap_rputs("NotAvailable\n", r);
}
} else {
r, flags);
}
if (status != APR_SUCCESS) {
"could not release lock for cache status");
}
}
if (!(flags & AP_STATUS_SHORT)) {
ap_rputs("</td></tr>\n</table>\n", r);
}
return OK;
}
static void socache_status_register(apr_pool_t *p)
{
}
{
APR_LOCK_DEFAULT, 0);
if (rv != APR_SUCCESS) {
"failed to register %s mutex", cache_socache_id);
return 500; /* An HTTP status would be a misnomer! */
}
/* Register to handle mod_status status page generation */
return OK;
}
{
server_rec *s;
const char *errmsg;
static struct ap_socache_hints socache_hints =
{ 64, 2048, 60000000 };
for (s = base_server; s; s = s->next) {
continue;
}
if (rv != APR_SUCCESS) {
"failed to create %s mutex", cache_socache_id);
return 500; /* An HTTP status would be a misnomer! */
}
}
pconf);
if (errmsg) {
return 500; /* An HTTP status would be a misnomer! */
}
&socache_hints, s, pconf);
if (rv != APR_SUCCESS) {
"failed to initialise %s cache", cache_socache_id);
return 500; /* An HTTP status would be a misnomer! */
}
}
return OK;
}
{
const char *lock;
if (!socache_mutex) {
return; /* don't waste the overhead of creating mutex & cache */
}
if (rv != APR_SUCCESS) {
"failed to initialise mutex in child_init");
}
}
static const command_rec cache_socache_cmds[] =
{
"The shared object cache to store cache files"),
"The maximum cache expiry age to cache a document in seconds"),
"The minimum cache expiry age to cache a document in seconds"),
"The maximum cache entry size (headers and body) to cache a document"),
"The maximum quantity of data to attempt to read and cache in one go"),
"The maximum time taken to attempt to read and cache in go"),
{ NULL }
};
static const cache_provider cache_socache_provider =
{
};
static void cache_socache_register_hook(apr_pool_t *p)
{
/* cache initializer */
}
create_dir_config, /* create per-directory config structure */
merge_dir_config, /* merge per-directory config structures */
create_config, /* create per-server config structure */
merge_config, /* merge per-server config structures */
cache_socache_cmds, /* command apr_table_t */
cache_socache_register_hook /* register hooks */
};