The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

/* ====================================================================
* The Apache Software License, Version 1.1
*
* Copyright (c) 2000 The Apache Software Foundation. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The end-user documentation included with the redistribution,
* if any, must include the following acknowledgment:
* "This product includes software developed by the
* Apache Software Foundation (http://www.apache.org/)."
* Alternately, this acknowledgment may appear in the software itself,
* if and wherever such third-party acknowledgments normally appear.
*
* 4. The names "Apache" and "Apache Software Foundation" must
* not be used to endorse or promote products derived from this
* software without prior written permission. For written
* permission, please contact apache@apache.org.
*
* 5. Products derived from this software may not be called "Apache",
* nor may "Apache" appear in their name, without prior written
* permission of the Apache Software Foundation.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
*
* Portions of this software are based upon public domain software
* originally written at the National Center for Supercomputing Applications,
* University of Illinois, Urbana-Champaign.
*/
/* Cache and garbage collection routines for Apache proxy */
#include "mod_proxy.h"
#include "http_conf_globals.h"
#include "http_log.h"
#include "http_main.h"
#include "http_core.h"
#include "util_date.h"
#ifdef WIN32
#include <sys/utime.h>
#else
#include <utime.h>
#endif /* WIN32 */
#include "multithread.h"
#include "ap_md5.h"
#ifdef __TANDEM
#include <sys/types.h>
#include <sys/stat.h>
#endif
#ifdef TPF
#include "os.h"
#endif
struct gc_ent {
unsigned long int len;
time_t expire;
char file[HASH_LEN + 1];
};
/* Poor man's 61 bit arithmetic */
typedef struct {
long lower; /* lower 30 bits of result */
long upper; /* upper 31 bits of result */
} long61_t;
/* FIXME: The block size can be different on a `per file system' base.
* This would make automatic detection highly OS specific.
* In the GNU fileutils code for du(1), you can see how complicated it can
* become to detect the block size. And, with BSD-4.x fragments, it
* it even more difficult to get precise results.
* As a compromise (and to improve on the incorrect counting of cache
* size on byte level, omitting directory sizes entirely, which was
* used up to apache-1.3b7) we're rounding to multiples of 512 here.
* Your file system may be using larger blocks (I certainly hope so!)
* but it will hardly use smaller blocks.
* (So this approximation is still closer to reality than the old behavior).
* The best solution would be automatic detection, the next best solution
* IMHO is a sensible default and the possibility to override it.
*/
#define ROUNDUP2BLOCKS(_bytes) (((_bytes)+block_size-1) & ~(block_size-1))
static long block_size = 512; /* this must be a power of 2 */
static long61_t curbytes, cachesize;
static time_t garbage_now, garbage_expire;
static mutex *garbage_mutex = NULL;
int ap_proxy_garbage_init(server_rec *r, pool *p)
{
if (!garbage_mutex)
garbage_mutex = ap_create_mutex(NULL);
return (0);
}
static int sub_garbage_coll(request_rec *r, array_header *files,
const char *cachedir, const char *cachesubdir);
static void help_proxy_garbage_coll(request_rec *r);
static int should_proxy_garbage_coll(request_rec *r);
#if !defined(WIN32) && !defined(MPE) && !defined(OS2) && !defined(NETWARE) && !defined(TPF)
static void detached_proxy_garbage_coll(request_rec *r);
#endif
void ap_proxy_garbage_coll(request_rec *r)
{
static int inside = 0;
(void) ap_acquire_mutex(garbage_mutex);
if (inside == 1) {
(void) ap_release_mutex(garbage_mutex);
return;
}
else
inside = 1;
(void) ap_release_mutex(garbage_mutex);
ap_block_alarms(); /* avoid SIGALRM on big cache cleanup */
if (should_proxy_garbage_coll(r))
#if !defined(WIN32) && !defined(MPE) && !defined(OS2) && !defined(NETWARE) && !defined(TPF)
detached_proxy_garbage_coll(r);
#else
help_proxy_garbage_coll(r);
#endif
ap_unblock_alarms();
(void) ap_acquire_mutex(garbage_mutex);
inside = 0;
(void) ap_release_mutex(garbage_mutex);
}
static void
add_long61 (long61_t *accu, long val)
{
/* Add in lower 30 bits */
accu->lower += (val & 0x3FFFFFFFL);
/* add in upper bits, and carry */
accu->upper += (val >> 30) + ((accu->lower & ~0x3FFFFFFFL) != 0L);
/* Clear carry */
accu->lower &= 0x3FFFFFFFL;
}
static void
sub_long61 (long61_t *accu, long val)
{
int carry = (val & 0x3FFFFFFFL) > accu->lower;
/* Subtract lower 30 bits */
accu->lower = accu->lower - (val & 0x3FFFFFFFL) + ((carry) ? 0x40000000 : 0);
/* add in upper bits, and carry */
accu->upper -= (val >> 30) + carry;
}
/* Compare two long61's:
* return <0 when left < right
* return 0 when left == right
* return >0 when left > right
*/
static long
cmp_long61 (long61_t *left, long61_t *right)
{
return (left->upper == right->upper) ? (left->lower - right->lower)
: (left->upper - right->upper);
}
/* Compare two gc_ent's, sort them by expiration date */
static int gcdiff(const void *ap, const void *bp)
{
const struct gc_ent *a = (const struct gc_ent *) ap;
const struct gc_ent *b = (const struct gc_ent *) bp;
if (a->expire > b->expire)
return 1;
else if (a->expire < b->expire)
return -1;
else
return 0;
}
#if !defined(WIN32) && !defined(MPE) && !defined(OS2) && !defined(NETWARE) && !defined(TPF)
static void detached_proxy_garbage_coll(request_rec *r)
{
pid_t pid;
int status;
pid_t pgrp;
#if 0
ap_log_error(APLOG_MARK, APLOG_DEBUG, r->server,
"proxy: Guess what; we fork() again...");
#endif
switch (pid = fork()) {
case -1:
ap_log_error(APLOG_MARK, APLOG_ERR, r->server,
"proxy: fork() for cache cleanup failed");
return;
case 0: /* Child */
/* close all sorts of things, including the socket fd */
ap_cleanup_for_exec();
/* Fork twice to disassociate from the child */
switch (pid = fork()) {
case -1:
ap_log_error(APLOG_MARK, APLOG_ERR, r->server,
"proxy: fork(2nd) for cache cleanup failed");
exit(1);
case 0: /* Child */
/* The setpgrp() stuff was snarfed from http_main.c */
#ifndef NO_SETSID
if ((pgrp = setsid()) == -1) {
perror("setsid");
fprintf(stderr, "%s: setsid failed\n",
ap_server_argv0);
exit(1);
}
#elif defined(NEXT) || defined(NEWSOS)
if (setpgrp(0, getpid()) == -1 || (pgrp = getpgrp(0)) == -1) {
perror("setpgrp");
fprintf(stderr, "%S: setpgrp or getpgrp failed\n",
ap_server_argv0);
exit(1);
}
#else
if ((pgrp = setpgrp(getpid(), 0)) == -1) {
perror("setpgrp");
fprintf(stderr, "%s: setpgrp failed\n",
ap_server_argv0);
exit(1);
}
#endif
help_proxy_garbage_coll(r);
exit(0);
default: /* Father */
/* After grandson has been forked off, */
/* there's nothing else to do. */
exit(0);
}
default:
/* Wait until grandson has been forked off */
/* (without wait we'd leave a zombie) */
waitpid(pid, &status, 0);
return;
}
}
#endif /* ndef WIN32 */
#define DOT_TIME "/.time" /* marker */
static int should_proxy_garbage_coll(request_rec *r)
{
void *sconf = r->server->module_config;
proxy_server_conf *pconf =
(proxy_server_conf *) ap_get_module_config(sconf, &proxy_module);
const struct cache_conf *conf = &pconf->cache;
const char *cachedir = conf->root;
char *filename;
struct stat buf;
int timefd;
time_t every = conf->gcinterval;
static time_t lastcheck = BAD_DATE; /* static (per-process) data!!! */
if (cachedir == NULL || every == -1)
return 0;
filename = ap_palloc(r->pool, strlen(cachedir) + strlen( DOT_TIME ) +1);
garbage_now = time(NULL);
/* Usually, the modification time of <cachedir>/.time can only increase.
* Thus, even with several child processes having their own copy of
* lastcheck, if time(NULL) still < lastcheck then it's not time
* for GC yet.
*/
if (garbage_now != -1 && lastcheck != BAD_DATE && garbage_now < lastcheck + every)
return 0;
strcpy(filename,cachedir);
strcat(filename,DOT_TIME);
/* At this point we have a bit of an engineering compromise. We could either
* create and/or mark the .time file (prior to the fork which might
* fail on a resource issue) or wait until we are safely forked. The
* advantage of doing it now in this process is that we get some
* usefull live out of the global last check variable. (XXX which
* should go scoreboard IMHO.) Note that the actual counting is
* at a later moment.
*/
if (stat(filename, &buf) == -1) { /* does not exist */
if (errno != ENOENT) {
ap_log_error(APLOG_MARK, APLOG_ERR, r->server,
"proxy: stat(%s)", filename);
return 0;
}
if ((timefd = creat(filename, 0666)) == -1) {
if (errno != EEXIST)
ap_log_error(APLOG_MARK, APLOG_ERR, r->server,
"proxy: creat(%s)", filename);
else
lastcheck = garbage_now; /* someone else got in there */
return 0;
}
close(timefd);
}
else {
lastcheck = buf.st_mtime; /* save the time */
if (garbage_now < lastcheck + every) {
return 0;
}
if (utime(filename, NULL) == -1)
ap_log_error(APLOG_MARK, APLOG_ERR, r->server,
"proxy: utimes(%s)", filename);
}
return 1;
}
static void help_proxy_garbage_coll(request_rec *r)
{
const char *cachedir;
void *sconf = r->server->module_config;
proxy_server_conf *pconf =
(proxy_server_conf *) ap_get_module_config(sconf, &proxy_module);
const struct cache_conf *conf = &pconf->cache;
array_header *files;
struct gc_ent *fent;
char *filename;
int i;
cachedir = conf->root;
filename = ap_palloc(r->pool, strlen(cachedir) + HASH_LEN + 2);
/* configured size is given in kB. Make it bytes, convert to long61_t: */
cachesize.lower = cachesize.upper = 0;
add_long61(&cachesize, conf->space << 10);
ap_block_alarms(); /* avoid SIGALRM on big cache cleanup */
files = ap_make_array(r->pool, 100, sizeof(struct gc_ent));
curbytes.upper = curbytes.lower = 0L;
sub_garbage_coll(r, files, cachedir, "/");
if (cmp_long61(&curbytes, &cachesize) < 0L) {
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server,
"proxy GC: Cache is %ld%% full (nothing deleted)",
(long)(((curbytes.upper<<20)|(curbytes.lower>>10))*100/conf->space));
ap_unblock_alarms();
return;
}
/* sort the files we found by expiration date */
qsort(files->elts, files->nelts, sizeof(struct gc_ent), gcdiff);
for (i = 0; i < files->nelts; i++) {
fent = &((struct gc_ent *) files->elts)[i];
sprintf(filename, "%s%s", cachedir, fent->file);
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "GC Unlinking %s (expiry %ld, garbage_now %ld)", filename, (long)fent->expire, (long)garbage_now);
#if TESTING
fprintf(stderr, "Would unlink %s\n", filename);
#else
if (unlink(filename) == -1) {
if (errno != ENOENT)
ap_log_error(APLOG_MARK, APLOG_ERR, r->server,
"proxy gc: unlink(%s)", filename);
}
else
#endif
{
sub_long61(&curbytes, ROUNDUP2BLOCKS(fent->len));
if (cmp_long61(&curbytes, &cachesize) < 0)
break;
}
}
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server,
"proxy GC: Cache is %ld%% full (%d deleted)",
(long)(((curbytes.upper<<20)|(curbytes.lower>>10))*100/conf->space), i);
ap_unblock_alarms();
}
static int sub_garbage_coll(request_rec *r, array_header *files,
const char *cachebasedir, const char *cachesubdir)
{
char line[17*(3)];
char cachedir[HUGE_STRING_LEN];
struct stat buf;
int fd, i;
DIR *dir;
#if defined(NEXT) || defined(WIN32)
struct DIR_TYPE *ent;
#else
struct dirent *ent;
#endif
struct gc_ent *fent;
int nfiles = 0;
char *filename;
ap_snprintf(cachedir, sizeof(cachedir), "%s%s", cachebasedir, cachesubdir);
filename = ap_palloc(r->pool, strlen(cachedir) + HASH_LEN + 2);
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "GC Examining directory %s", cachedir);
dir = opendir(cachedir);
if (dir == NULL) {
ap_log_error(APLOG_MARK, APLOG_ERR, r->server,
"proxy gc: opendir(%s)", cachedir);
return 0;
}
while ((ent = readdir(dir)) != NULL) {
if (ent->d_name[0] == '.')
continue;
sprintf(filename, "%s%s", cachedir, ent->d_name);
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "GC Examining file %s", filename);
/* is it a temporary file? */
if (strncmp(ent->d_name, "tmp", 3) == 0) {
/* then stat it to see how old it is; delete temporary files > 1 day old */
if (stat(filename, &buf) == -1) {
if (errno != ENOENT)
ap_log_error(APLOG_MARK, APLOG_ERR, r->server,
"proxy gc: stat(%s)", filename);
}
else if (garbage_now != -1 && buf.st_atime < garbage_now - SEC_ONE_DAY &&
buf.st_mtime < garbage_now - SEC_ONE_DAY) {
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "GC unlink %s", filename);
ap_log_error(APLOG_MARK, APLOG_INFO|APLOG_NOERRNO, r->server,
"proxy gc: deleting orphaned cache file %s", filename);
#if TESTING
fprintf(stderr, "Would unlink %s\n", filename);
#else
unlink(filename);
#endif
}
continue;
}
++nfiles;
/* is it another file? */
/* FIXME: Shouldn't any unexpected files be deleted? */
/* if (strlen(ent->d_name) != HASH_LEN) continue; */
/* under OS/2 use dirent's d_attr to identify a diretory */
/* under TPF use stat to identify a directory */
#if defined(OS2) || defined(TPF)
/* is it a directory? */
#ifdef OS2
if (ent->d_attr & A_DIR) {
#elif defined(TPF)
if (stat(filename, &buf) == -1) {
if (errno != ENOENT)
ap_log_error(APLOG_MARK, APLOG_ERR, r->server,
"proxy gc: stat(%s)", filename);
}
if (S_ISDIR(buf.st_mode)) {
#endif
char newcachedir[HUGE_STRING_LEN];
ap_snprintf(newcachedir, sizeof(newcachedir),
"%s%s/", cachesubdir, ent->d_name);
if (!sub_garbage_coll(r, files, cachebasedir, newcachedir)) {
ap_snprintf(newcachedir, sizeof(newcachedir),
"%s%s", cachedir, ent->d_name);
#if TESTING
fprintf(stderr, "Would remove directory %s\n", newcachedir);
#else
rmdir(newcachedir);
#endif
--nfiles;
}
continue;
}
#endif
/* read the file */
#if defined(WIN32)
/* On WIN32 open does not work for directories,
* so we us stat instead of fstat to determine
* if the file is a directory
*/
if (stat(filename, &buf) == -1) {
ap_log_error(APLOG_MARK, APLOG_ERR, r->server,
"proxy gc: stat(%s)", filename);
continue;
}
fd = -1;
#else
fd = open(filename, O_RDONLY | O_BINARY);
if (fd == -1) {
if (errno != ENOENT)
ap_log_error(APLOG_MARK, APLOG_ERR, r->server,
"proxy gc: open(%s)", filename);
continue;
}
if (fstat(fd, &buf) == -1) {
ap_log_error(APLOG_MARK, APLOG_ERR, r->server,
"proxy gc: fstat(%s)", filename);
close(fd);
continue;
}
#endif
/* In OS/2 and TPF this has already been done above */
#if !defined(OS2) && !defined(TPF)
if (S_ISDIR(buf.st_mode)) {
char newcachedir[HUGE_STRING_LEN];
#if !defined(WIN32)
/* Win32 used stat, no file to close */
close(fd);
#endif
ap_snprintf(newcachedir, sizeof(newcachedir),
"%s%s/", cachesubdir, ent->d_name);
if (!sub_garbage_coll(r, files, cachebasedir, newcachedir)) {
ap_snprintf(newcachedir, sizeof(newcachedir),
"%s%s", cachedir, ent->d_name);
#if TESTING
fprintf(stderr, "Would remove directory %s\n", newcachedir);
#else
rmdir(newcachedir);
#endif
--nfiles;
} else {
/* Directory is not empty. Account for its size: */
add_long61(&curbytes, ROUNDUP2BLOCKS(buf.st_size));
}
continue;
}
#endif
#if defined(WIN32)
/* Since we have determined above that the file is not a directory,
* it should be safe to open it now
*/
fd = open(filename, O_RDONLY | O_BINARY);
if (fd == -1) {
if (errno != ENOENT)
ap_log_error(APLOG_MARK, APLOG_ERR, r->server,
"proxy gc: open(%s) = %d", filename, errno);
continue;
}
#endif
i = read(fd, line, 17*(3)-1);
close(fd);
if (i == -1) {
ap_log_error(APLOG_MARK, APLOG_ERR, r->server,
"proxy gc: read(%s)", filename);
continue;
}
line[i] = '\0';
garbage_expire = ap_proxy_hex2sec(line + 17*(2));
if (!ap_checkmask(line, "&&&&&&&&&&&&&&&& &&&&&&&&&&&&&&&& &&&&&&&&&&&&&&&&") ||
garbage_expire == BAD_DATE) {
/* bad file */
if (garbage_now != -1 && buf.st_atime > garbage_now + SEC_ONE_DAY &&
buf.st_mtime > garbage_now + SEC_ONE_DAY) {
ap_log_error(APLOG_MARK, APLOG_WARNING|APLOG_NOERRNO, r->server,
"proxy: deleting bad cache file with future date: %s", filename);
#if TESTING
fprintf(stderr, "Would unlink bad file %s\n", filename);
#else
unlink(filename);
#endif
}
continue;
}
/*
* we need to calculate an 'old' factor, and remove the 'oldest' files
* so that the space requirement is met; sort by the expires date of the
* file.
*
*/
fent = (struct gc_ent *) ap_push_array(files);
fent->len = buf.st_size;
fent->expire = garbage_expire;
strcpy(fent->file, cachesubdir);
strcat(fent->file, ent->d_name);
/* accumulate in blocks, to cope with directories > 4Gb */
add_long61(&curbytes, ROUNDUP2BLOCKS(buf.st_size));
}
closedir(dir);
return nfiles;
}
/*
* Read a cache file;
* returns 1 on success,
* 0 on failure (bad file or wrong URL)
* -1 on UNIX error
*
* We read the cache hex header, then the message response line and
* response headers, and finally we return with the filepointer
* pointing at the start of the message body itself, ready to be
* shipped to the client later on, if appropriate.
*/
static int rdcache(request_rec *r, BUFF *cachefp, cache_req *c)
{
char urlbuff[HUGE_STRING_LEN], *strp;
int len;
/* read the data from the cache file */
/* Format:
*
* The cache needs to keep track of the following information:
* - Date, LastMod, Version, ReqTime, RespTime, ContentLength
* - The original request headers (for Vary)
* - The original response headers (for returning with a cached response)
* - The body of the message
*
* date SP lastmod SP expire SP count SP request-time SP response-time SP content-lengthCRLF
* (dates are stored as hex seconds since 1970)
* Original URLCRLF
* Original Request Headers
* CRLF
* Original Response Headers
* CRLF
* Body
*
*/
/* retrieve cachefile information values */
len = ap_bgets(urlbuff, sizeof urlbuff, cachefp);
if (len == -1)
return -1;
if (len == 0 || urlbuff[len - 1] != '\n')
return 0;
urlbuff[len - 1] = '\0';
if (!ap_checkmask(urlbuff,
"&&&&&&&&&&&&&&&& &&&&&&&&&&&&&&&& &&&&&&&&&&&&&&&& &&&&&&&&&&&&&&&& &&&&&&&&&&&&&&&& &&&&&&&&&&&&&&&& &&&&&&&&&&&&&&&&"))
return 0;
c->date = ap_proxy_hex2sec(urlbuff + 17*(0));
c->lmod = ap_proxy_hex2sec(urlbuff + 17*(1));
c->expire = ap_proxy_hex2sec(urlbuff + 17*(2));
c->version = ap_proxy_hex2sec(urlbuff + 17*(3));
c->req_time = ap_proxy_hex2sec(urlbuff + 17*(4));
c->resp_time = ap_proxy_hex2sec(urlbuff + 17*(5));
c->len = ap_proxy_hex2sec(urlbuff + 17*(6));
/* check that we have the same URL */
len = ap_bgets(urlbuff, sizeof urlbuff, cachefp);
if (len == -1)
return -1;
if (len == 0 || strncmp(urlbuff, "X-URL: ", 7) != 0 ||
urlbuff[len - 1] != '\n')
return 0;
urlbuff[len - 1] = '\0';
if (strcmp(urlbuff + 7, c->url) != 0)
return 0;
/* then the original request headers */
c->req_hdrs = ap_proxy_read_headers(r, urlbuff, sizeof urlbuff, cachefp);
if (c->req_hdrs == NULL)
return -1;
/* then the original response headers */
len = ap_bgets(urlbuff, sizeof urlbuff, cachefp);
if (len == -1)
return -1;
if (len == 0 || urlbuff[len - 1] != '\n')
return 0;
urlbuff[--len] = '\0';
c->resp_line = ap_pstrdup(r->pool, urlbuff);
strp = strchr(urlbuff, ' ');
if (strp == NULL)
return 0;
c->status = atoi(strp);
c->hdrs = ap_proxy_read_headers(r, urlbuff, sizeof urlbuff, cachefp);
if (c->hdrs == NULL)
return -1;
if (c->len != -1) /* add a content-length header */
if (ap_table_get(c->hdrs, "Content-Length") == NULL) {
ap_table_set(c->hdrs, "Content-Length",
ap_psprintf(r->pool, "%lu", (unsigned long)c->len));
}
return 1;
}
/*
* Call this to check the possible conditional status of
* the client request, and return the response from the cache
*
* Conditionals include If-Modified-Since, If-Match, If-Unmodified-Since
* and If-None-Match.
*
* We don't yet understand If-Range, but we will...
*/
int ap_proxy_cache_conditional(request_rec *r, cache_req *c, BUFF *cachefp)
{
const char *etag, *wetag = NULL;
/* get etag */
if ((etag = ap_table_get(c->hdrs, "Etag"))) {
wetag = ap_pstrcat(r->pool, "W/", etag, NULL);
}
/* check for If-Match, If-Unmodified-Since */
while (1) {
/* check If-Match and If-Unmodified-Since exist
*
* If neither of these exist, the request is not conditional, and
* we serve it normally
*/
if (!c->im && BAD_DATE == c->ius) {
break;
}
/* check If-Match
*
* we check if the Etag on the cached file is in the list of Etags
* in the If-Match field. The comparison must be a strong comparison,
* so the Etag cannot be marked as weak. If the comparision fails
* we return 412 Precondition Failed.
*
* if If-Match is specified AND
* If-Match is not a "*" AND
* Etag is missing or weak or not in the list THEN
* return 412 Precondition Failed
*/
if (c->im) {
if (strcmp(c->im, "*") &&
(!etag || (strlen(etag) > 1 && 'W' == etag[0] && '/' == etag[1]) || !ap_proxy_liststr(c->im, etag, NULL))) {
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "If-Match specified, and it didn't - return 412");
}
else {
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "If-Match specified, and it matched");
break;
}
}
/* check If-Unmodified-Since
*
* if If-Unmodified-Since is specified AND
* Last-Modified is specified somewhere AND
* If-Unmodified-Since is in the past compared to Last-Modified THEN
* return 412 Precondition Failed
*/
if (BAD_DATE != c->ius && BAD_DATE != c->lmod) {
if (c->ius < c->lmod) {
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "If-Unmodified-Since specified, but it wasn't - return 412");
}
else {
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "If-Unmodified-Since specified, and it was unmodified");
break;
}
}
/* if cache file is being updated */
if (c->origfp) {
ap_proxy_write_headers(c, c->resp_line, c->hdrs);
ap_proxy_send_fb(c->origfp, r, c, c->len, 1);
ap_pclosef(r->pool, ap_bfileno(c->origfp, B_WR));
ap_proxy_cache_tidy(c);
}
else
ap_pclosef(r->pool, ap_bfileno(cachefp, B_WR));
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "Use your cached copy, conditional precondition failed.");
return HTTP_PRECONDITION_FAILED;
}
/* check for If-None-Match, If-Modified-Since */
while (1) {
/* check for existance of If-None-Match and If-Modified-Since
*
* if neither of these headers have been set, then the request
* is not conditional, and we just send the cached response and
* be done with it.
*/
if (!c->inm && BAD_DATE == c->ims) {
break;
}
/* check If-None-Match
*
* we check if the Etag on the cached file is in the list of Etags
* in the If-None-Match field. The comparison must be a strong comparison,
* so the Etag cannot be marked as weak. If the comparision fails
* we return 412 Precondition Failed.
*
* if If-None-Match is specified:
* if If-None-Match is a "*" THEN 304
* else if Etag is specified AND we get a match THEN 304
* else if Weak Etag is specified AND we get a match THEN 304
* else sent the original object
*/
if (c->inm) {
if (!strcmp(c->inm, "*")) {
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "If-None-Match: * specified, return 304");
}
else if (etag && ap_proxy_liststr(c->inm, etag, NULL)) {
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "If-None-Match: specified and we got a strong match - return 304");
}
else if (wetag && ap_proxy_liststr(c->inm, wetag, NULL)) {
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "If-None-Match specified, and we got a weak match - return 304");
}
else
break;
}
/* check If-Modified-Since
*
* if If-Modified-Since is specified AND
* Last-Modified is specified somewhere:
* if last modification date is earlier than If-Modified-Since THEN 304
* else send the original object
*/
if (BAD_DATE != c->ims && BAD_DATE != c->lmod) {
if (c->ims >= c->lmod) {
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "If-Modified-Since specified and not modified, try return 304");
}
else
break;
}
/* are we updating the cache file? */
if (c->origfp) {
ap_proxy_write_headers(c, c->resp_line, c->hdrs);
ap_proxy_send_fb(c->origfp, r, c, c->len, 1);
ap_pclosef(r->pool, ap_bfileno(c->origfp, B_WR));
ap_proxy_cache_tidy(c);
}
else
ap_pclosef(r->pool, ap_bfileno(cachefp, B_WR));
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "Use local copy, cached file hasn't changed");
return HTTP_NOT_MODIFIED;
}
/* No conditional - just send it cousin! */
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "Local copy modified, send it");
r->status_line = strchr(c->resp_line, ' ') + 1;
r->status = c->status;
/* Prepare and send headers to client */
ap_overlap_tables(r->headers_out, c->hdrs, AP_OVERLAP_TABLES_SET);
ap_table_setn(r->headers_out, "X-Cache", c->xcache);
r->content_type = ap_table_get(r->headers_out, "Content-Type");
ap_send_http_header(r);
/* are we rewriting the cache file? */
if (c->origfp) {
ap_proxy_write_headers(c, c->resp_line, c->hdrs);
ap_proxy_send_fb(c->origfp, r, c, c->len, r->header_only);
ap_pclosef(r->pool, ap_bfileno(c->origfp, B_WR));
ap_proxy_cache_tidy(c);
return OK;
}
/* no, we not */
if (!r->header_only)
ap_proxy_send_fb(cachefp, r, NULL, c->len, 0);
ap_pclosef(r->pool, ap_bfileno(cachefp, B_WR));
return OK;
}
/*
* Call this to test for a resource in the cache
* Returns DECLINED if we need to check the remote host
* or an HTTP status code if successful
*
* Functions:
* if URL is cached then
* if cached file is not expired then
* if last modified after if-modified-since then send body
* else send 304 Not modified
* else if cached file is expired then
* if last modified after if-modified-since then add
* last modified date to request
*/
int ap_proxy_cache_check(request_rec *r, char *url, struct cache_conf *conf,
cache_req **cr)
{
const char *datestr, *pragma_req = NULL, *pragma_cresp = NULL, *cc_req = NULL, *cc_cresp = NULL, *vary = NULL;
cache_req *c;
time_t now;
BUFF *cachefp;
int i;
void *sconf = r->server->module_config;
proxy_server_conf *pconf =
(proxy_server_conf *) ap_get_module_config(sconf, &proxy_module);
const char *agestr = NULL;
char *val;
time_t age_c = 0;
time_t age, maxage_req, maxage_cresp, maxage, smaxage, maxstale, minfresh;
c = ap_pcalloc(r->pool, sizeof(cache_req));
*cr = c;
c->req = r;
c->url = ap_pstrdup(r->pool, url);
c->filename = NULL;
c->tempfile = NULL;
c->fp = NULL;
c->origfp = NULL;
c->version = 0;
c->len = -1;
c->req_hdrs = NULL;
c->hdrs = NULL;
c->xcache = NULL;
/* get the If-Modified-Since date of the request, if it exists */
c->ims = BAD_DATE;
datestr = ap_table_get(r->headers_in, "If-Modified-Since");
if (datestr != NULL) {
/* this may modify the value in the original table */
datestr = ap_proxy_date_canon(r->pool, datestr);
c->ims = ap_parseHTTPdate(datestr);
if (c->ims == BAD_DATE) /* bad or out of range date; remove it */
ap_table_unset(r->headers_in, "If-Modified-Since");
}
/* get the If-Unmodified-Since date of the request, if it exists */
c->ius = BAD_DATE;
datestr = ap_table_get(r->headers_in, "If-Unmodified-Since");
if (datestr != NULL) {
/* this may modify the value in the original table */
datestr = ap_proxy_date_canon(r->pool, datestr);
c->ius = ap_parseHTTPdate(datestr);
if (c->ius == BAD_DATE) /* bad or out of range date; remove it */
ap_table_unset(r->headers_in, "If-Unmodified-Since");
}
/* get the If-Match of the request, if it exists */
c->im = ap_table_get(r->headers_in, "If-Match");
/* get the If-None-Match of the request, if it exists */
c->inm = ap_table_get(r->headers_in, "If-None-Match");
/* find the filename for this cache entry */
if (conf->root != NULL) {
char hashfile[66];
ap_proxy_hash(url, hashfile, pconf->cache.dirlevels, pconf->cache.dirlength);
c->filename = ap_pstrcat(r->pool, conf->root, "/", hashfile, NULL);
}
else {
c->filename = NULL;
c->fp = NULL;
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "No CacheRoot, so no caching. Declining.");
return DECLINED;
}
/* find certain cache controlling headers */
pragma_req = ap_table_get(r->headers_in, "Pragma");
cc_req = ap_table_get(r->headers_in, "Cache-Control");
/* first things first - does the request allow us to return
* cached information at all? If not, just decline the request.
*
* Note that there is a big difference between not being allowed
* to cache a request (no-store) and not being allowed to return
* a cached request without revalidation (max-age=0).
*
* Caching is forbidden under the following circumstances:
*
* - RFC2616 14.9.2 Cache-Control: no-store
* we are not supposed to store this request at all. Behave as a tunnel.
*
*/
if (ap_proxy_liststr(cc_req, "no-store", NULL)) {
/* delete the previously cached file */
if (c->filename)
unlink(c->filename);
c->fp = NULL;
c->filename = NULL;
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "no-store forbids caching. Declining.");
return DECLINED;
}
/* if the cache file exists, open it */
cachefp = NULL;
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "Request for %s, pragma_req=%s, ims=%ld", url,
pragma_req, c->ims);
/* find out about whether the request can access the cache */
if (c->filename != NULL && r->method_number == M_GET &&
strlen(url) < 1024 ) {
cachefp = ap_proxy_open_cachefile(r, c->filename);
}
/* if a cache file exists, try reading body and headers from cache file */
if (cachefp != NULL) {
i = rdcache(r, cachefp, c);
if (i == -1)
ap_log_rerror(APLOG_MARK, APLOG_ERR, r,
"proxy: error reading cache file %s",
c->filename);
else if (i == 0)
ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, r,
"proxy: bad (short?) cache file: %s", c->filename);
if (i != 1) {
ap_pclosef(r->pool, ap_bfileno(cachefp, B_WR));
cachefp = NULL;
}
if (c->hdrs) {
cc_cresp = ap_table_get(c->hdrs, "Cache-Control");
pragma_cresp = ap_table_get(c->hdrs, "Pragma");
vary = ap_table_get(c->hdrs, "Vary");
if ((agestr = ap_table_get(c->hdrs, "Age"))) {
age_c = atoi(agestr);
}
}
}
/* if a cache file does not exist, create empty header array */
/* fixed? in this case, we want to get the headers from the remote server
it will be handled later if we don't do this (I hope ;-)
if (cachefp == NULL)
c->hdrs = ap_make_table(r->pool, 20);
*/
/* FIXME: Shouldn't we check the URL somewhere? */
/* Check Content-Negotiation - Vary
*
* At this point we need to make sure that the object we found in the cache
* is the same object that would be delivered to the client, when the
* effects of content negotiation are taken into effect.
*
* In plain english, we want to make sure that a language-negotiated
* document in one language is not given to a client asking for a
* language negotiated document in a different language by mistake.
*
* RFC2616 13.6 and 14.44 describe the Vary mechanism.
*/
if (c->hdrs && c->req_hdrs) {
char *vary = ap_pstrdup(r->pool, ap_table_get(c->hdrs, "Vary"));
while (vary && *vary) {
char *name = vary;
const char *h1, *h2;
/* isolate header name */
while (*vary && !ap_isspace(*vary) && (*vary != ','))
++vary;
while (*vary && (ap_isspace(*vary) || (*vary == ','))) {
*vary = '\0';
++vary;
}
/* is this header in the request and the header in the cached
* request identical? If not, we give up and do a straight get */
h1 = ap_table_get(r->headers_in, name);
h2 = ap_table_get(c->req_hdrs, name);
if (h1 == h2) {
/* both headers NULL, so a match - do nothing */
}
else if (h1 && h2 && !strcmp(h1, h2)) {
/* both headers exist and are equal - do nothing */
}
else {
/* headers do not match, so Vary failed */
c->fp = cachefp;
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "Vary header mismatch - object must be fetched from scratch. Declining.");
return DECLINED;
}
}
}
/* 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.32 Pragma: no-cache
* This is treated the same as Cache-Control: no-cache.
*
* - RFC2616 14.9.3 Cache-Control: max-stale, must-revalidate, proxy-revalidate
* if the max-stale request header exists, modify the stale calculations
* below so that an object can be at most <max-stale> seconds stale before
* we request a revalidation, _UNLESS_ a must-revalidate or
* proxy-revalidate cached response header exists to stop us doing this.
*
* - RFC2616 14.9.3 Cache-Control: s-maxage
* the origin server specifies the maximum age an object can be before
* it is considered stale. This directive has the effect of proxy|must
* revalidate, which in turn means simple ignore any max-stale setting.
*
* - RFC2616 14.9.4 Cache-Control: max-age
* this header can appear in both requests and responses. If both are
* specified, the smaller of the two takes priority.
*
* - RFC2616 14.21 Expires:
* if this request header exists in the cached entity, and it's value is
* in the past, it has expired.
*
*/
/* calculate age of object */
age = ap_proxy_current_age(c, age_c);
/* extract s-maxage */
if (cc_cresp && ap_proxy_liststr(cc_cresp, "s-maxage", &val))
smaxage = atoi(val);
else
smaxage = -1;
/* extract max-age from request */
if (cc_cresp && ap_proxy_liststr(cc_req, "max-age", &val))
maxage_req = atoi(val);
else
maxage_req = -1;
/* extract max-age from response */
if (cc_cresp && ap_proxy_liststr(cc_cresp, "max-age", &val))
maxage_cresp = atoi(val);
else
maxage_cresp = -1;
/* if both maxage request and response, the smaller one takes priority */
if (-1 == maxage_req)
maxage = maxage_cresp;
else if (-1 == maxage_cresp)
maxage = maxage_req;
else
maxage = MIN(maxage_req, maxage_cresp);
/* extract max-stale */
if (cc_req && ap_proxy_liststr(cc_req, "max-stale", &val))
maxstale = atoi(val);
else
maxstale = 0;
/* extract min-fresh */
if (cc_req && ap_proxy_liststr(cc_req, "min-fresh", &val))
minfresh = atoi(val);
else
minfresh = 0;
/* override maxstale if must-revalidate or proxy-revalidate */
if (maxstale && ( (cc_cresp && ap_proxy_liststr(cc_cresp, "must-revalidate", NULL)) || (cc_cresp && ap_proxy_liststr(cc_cresp, "proxy-revalidate", NULL)) ))
maxstale = 0;
now = time(NULL);
if (cachefp != NULL &&
/* handle no-cache */
!( (cc_req && ap_proxy_liststr(cc_req, "no-cache", NULL)) ||
(pragma_req && ap_proxy_liststr(pragma_req, "no-cache", NULL)) ||
(cc_cresp && ap_proxy_liststr(cc_cresp, "no-cache", NULL)) ||
(pragma_cresp && ap_proxy_liststr(pragma_cresp, "no-cache", NULL)) ) &&
/* handle expiration */
( (-1 < smaxage && age < (smaxage - minfresh)) ||
(-1 < maxage && age < (maxage + maxstale - minfresh)) ||
(c->expire != BAD_DATE && age < (c->expire - c->date + maxstale - minfresh)) )
) {
/* it's fresh darlings... */
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "Unexpired data available");
/* set age header on response */
ap_table_set(c->hdrs, "Age",
ap_psprintf(r->pool, "%lu", (unsigned long)age));
/* add warning if maxstale overrode freshness calculation */
if (!( (-1 < smaxage && age < smaxage) ||
(-1 < maxage && age < maxage) ||
(c->expire != BAD_DATE && (c->expire - c->date) > age) )) {
ap_table_set(c->hdrs, "Warning", "110 Response is stale");
}
/* check conditionals (If-Modified-Since, etc) */
c->xcache = ap_pstrcat(r->pool, "HIT from ", ap_get_server_name(r), NULL);
return ap_proxy_cache_conditional(r, c, cachefp);
}
/* at this point we have determined our cached data needs revalidation
* but first - we check 1 thing:
*
* RFC2616 14.9.4 - if "only-if-cached" specified, send a
* 504 Gateway Timeout - we're not allowed to revalidate the object
*/
if (ap_proxy_liststr(cc_req, "only-if-cached", NULL)) {
if (cachefp)
ap_pclosef(r->pool, ap_bfileno(cachefp, B_WR));
return HTTP_GATEWAY_TIME_OUT;
}
/* If we already have cached data and a last-modified date, and it is
* not a head request, then add an If-Modified-Since.
*
* If we also have an Etag, then the object must have come from
* an HTTP/1.1 server. Add an If-None-Match as well.
*
* See RFC2616 13.3.4
*/
if (cachefp != NULL && !r->header_only) {
const char *etag = ap_table_get(c->hdrs, "Etag");
/* If-Modified-Since */
if (c->lmod != BAD_DATE) {
/* use the later of the one from the request and the last-modified date
* from the cache */
if (c->ims == BAD_DATE || c->ims < c->lmod) {
const char *q;
if ((q = ap_table_get(c->hdrs, "Last-Modified")) != NULL)
ap_table_set(r->headers_in, "If-Modified-Since", (char *) q);
}
}
/* If-None-Match */
if (etag) {
ap_table_set(r->headers_in, "If-None-Match", etag);
}
}
c->fp = cachefp;
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "Local copy not present or expired. Declining.");
return DECLINED;
}
/*
* Having read the response from the client, decide what to do
* If the response is not cachable, then delete any previously cached
* response, and copy data from remote server to client.
* Functions:
* parse dates
* check for an uncachable response
* calculate an expiry date, if one is not provided
* if the remote file has not been modified, then return the document
* from the cache, maybe updating the header line
* otherwise, delete the old cached file and open a new temporary file
*/
int ap_proxy_cache_update(cache_req *c, table *resp_hdrs,
const int is_HTTP1, int nocache)
{
#if defined(ULTRIX_BRAIN_DEATH) || defined(SINIX_D_RESOLVER_BUG)
extern char *mktemp(char *template);
#endif
request_rec *r = c->req;
char *p;
const char *expire, *lmods, *dates, *clen;
time_t expc, date, lmod, now;
char buff[17*7+1];
void *sconf = r->server->module_config;
proxy_server_conf *conf =
(proxy_server_conf *) ap_get_module_config(sconf, &proxy_module);
const char *cc_resp;
table *req_hdrs;
cc_resp = ap_table_get(resp_hdrs, "Cache-Control");
c->tempfile = NULL;
/* we've received the response from the origin server */
/* read expiry date; if a bad date, then leave it so the client can
* read it */
expire = ap_table_get(resp_hdrs, "Expires");
if (expire != NULL)
expc = ap_parseHTTPdate(expire);
else
expc = BAD_DATE;
/* read the last-modified date; if the date is bad, then delete it */
lmods = ap_table_get(resp_hdrs, "Last-Modified");
if (lmods != NULL) {
lmod = ap_parseHTTPdate(lmods);
if (lmod == BAD_DATE) {
/* kill last modified date */
lmods = NULL;
}
}
else
lmod = BAD_DATE;
/*
* what responses should we not cache?
*
* At this point we decide based on the response headers whether it
* is appropriate _NOT_ to cache the data from the server. There are
* a whole lot of conditions that prevent us from caching this data.
* They are tested here one by one to be clear and unambiguous. */
/* 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 include 304 Not Modified here too as this is the origin server
* telling us to serve the cached copy. */
if ((r->status != HTTP_OK && r->status != HTTP_NON_AUTHORITATIVE && r->status != HTTP_MULTIPLE_CHOICES && r->status != HTTP_MOVED_PERMANENTLY && r->status != HTTP_NOT_MODIFIED) ||
/* if a broken Expires header is present, don't cache it */
(expire != NULL && expc == BAD_DATE) ||
/* if the server said 304 Not Modified but we have no cache file - pass
* this untouched to the user agent, it's not for us. */
(r->status == HTTP_NOT_MODIFIED && (c == NULL || c->fp == NULL)) ||
/* 200 OK response from HTTP/1.0 and up without a Last-Modified header */
(r->status == HTTP_OK && lmods == NULL && is_HTTP1) ||
/* HEAD requests */
r->header_only ||
/* RFC2616 14.9.2 Cache-Control: no-store response indicating do not
* cache, or stop now if you are trying to cache it */
ap_proxy_liststr(cc_resp, "no-store", NULL) ||
/* RFC2616 14.9.1 Cache-Control: private
* this object is marked for this user's eyes only. Behave as a tunnel. */
ap_proxy_liststr(cc_resp, "private", NULL) ||
/* 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:
* 1) If Cache-Control: s-maxage is included
* 2) If Cache-Control: must-revalidate is included
* 3) If Cache-Control: public is included
*/
(ap_table_get(r->headers_in, "Authorization") != NULL
&& !(ap_proxy_liststr(cc_resp, "s-maxage", NULL) || ap_proxy_liststr(cc_resp, "must-revalidate", NULL) || ap_proxy_liststr(cc_resp, "public", NULL))
) ||
/* or we've been asked not to cache it above */
nocache) {
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "Response is not cacheable, unlinking %s", c->filename);
/* close the file */
if (c->fp != NULL) {
ap_pclosef(r->pool, ap_bfileno(c->fp, B_WR));
c->fp = NULL;
}
/* delete the previously cached file */
if (c->filename)
unlink(c->filename);
return DECLINED; /* send data to client but not cache */
}
/* It's safe to cache the response.
*
* We now want to update the cache file header information with
* the new date, last modified, expire and content length and write
* it away to our cache file. First, we determine these values from
* the response, using heuristics if appropriate.
*
* In addition, we make HTTP/1.1 age calculations and write them away
* too.
*/
/* Read the date. Generate one if one is not supplied */
dates = ap_table_get(resp_hdrs, "Date");
if (dates != NULL)
date = ap_parseHTTPdate(dates);
else
date = BAD_DATE;
now = time(NULL);
if (date == BAD_DATE) { /* No, or bad date */
/* no date header! */
/* add one; N.B. use the time _now_ rather than when we were checking the cache
*/
date = now;
dates = ap_gm_timestr_822(r->pool, now);
ap_table_set(resp_hdrs, "Date", dates);
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "Added date header");
}
/* set response_time for HTTP/1.1 age calculations */
c->resp_time = now;
/* check last-modified date */
if (lmod != BAD_DATE && lmod > date)
/* if its in the future, then replace by date */
{
lmod = date;
lmods = dates;
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "Last modified is in the future, replacing with now");
}
/* if the response did not contain the header, then use the cached version */
if (lmod == BAD_DATE && c->fp != NULL) {
lmod = c->lmod;
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "Reusing cached last modified");
}
/* we now need to calculate the expire data for the object. */
if (expire == NULL && c->fp != NULL) { /* no expiry data sent in response */
expire = ap_table_get(c->hdrs, "Expires");
if (expire != NULL)
expc = ap_parseHTTPdate(expire);
}
/* so we now have the expiry date */
/* if no expiry date then
* if lastmod
* expiry date = now + min((date - lastmod) * factor, maxexpire)
* else
* expire date = now + defaultexpire
*/
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "Expiry date is %ld", (long)expc);
if (expc == BAD_DATE) {
if (lmod != BAD_DATE) {
double x = (double) (date - lmod) * conf->cache.lmfactor;
double maxex = conf->cache.maxexpire;
if (x > maxex)
x = maxex;
expc = now + (int) x;
}
else
expc = now + conf->cache.defaultexpire;
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "Expiry date calculated %ld", (long)expc);
}
/* get the content-length header */
clen = ap_table_get(resp_hdrs, "Content-Length");
if (clen == NULL)
c->len = -1;
else
c->len = atoi(clen);
/* we have all the header information we need - write it to the cache file */
c->version++;
ap_proxy_sec2hex(date, buff + 17*(0));
buff[17*(1)-1] = ' ';
ap_proxy_sec2hex(lmod, buff + 17*(1));
buff[17*(2)-1] = ' ';
ap_proxy_sec2hex(expc, buff + 17*(2));
buff[17*(3)-1] = ' ';
ap_proxy_sec2hex(c->version, buff + 17*(3));
buff[17*(4)-1] = ' ';
ap_proxy_sec2hex(c->req_time, buff + 17*(4));
buff[17*(5)-1] = ' ';
ap_proxy_sec2hex(c->resp_time, buff + 17*(5));
buff[17*(6)-1] = ' ';
ap_proxy_sec2hex(c->len, buff + 17*(6));
buff[17*(7)-1] = '\n';
buff[17*(7)] = '\0';
/* Was the server response a 304 Not Modified?
*
* If it was, it means that we requested a revalidation, and that
* the result of that revalidation was that the object was fresh.
*
*/
/* if response from server 304 not modified */
if (r->status == HTTP_NOT_MODIFIED) {
/* Have the headers changed?
*
* if not - we fulfil the request and return now.
*/
if (c->hdrs) {
if (!ap_proxy_table_replace(c->hdrs, resp_hdrs)) {
c->xcache = ap_pstrcat(r->pool, "HIT from ", ap_get_server_name(r), " (with revalidation)", NULL);
return ap_proxy_cache_conditional(r, c, c->fp);
}
}
else
c->hdrs = resp_hdrs;
/* if we get here - the headers have changed. Go through the motions
* of creating a new temporary cache file below, we'll then serve
* the request like we would have in ap_proxy_cache_conditional()
* above, and at the same time we will also rewrite the contents
* to the new temporary file.
*/
}
/*
* Ok - lets prepare and open the cached file
*
* If a cached file (in c->fp) is already open, then we want to
* update that cached file. Copy the c->fp to c->origfp and open
* up a new one.
*
* If the cached file (in c->fp) is NULL, we must open a new cached
* file from scratch.
*
* The new cache file will be moved to it's final location in the
* directory tree later, overwriting the old cache file should it exist.
*/
/* if a cache file was already open */
if (c->fp != NULL) {
c->origfp = c->fp;
}
while (1) {
/* create temporary filename */
#ifndef TPF
#define TMPFILESTR "/tmpXXXXXX"
if (conf->cache.root == NULL) {
c = ap_proxy_cache_error(c);
break;
}
c->tempfile = ap_palloc(r->pool, strlen(conf->cache.root) + sizeof(TMPFILESTR));
strcpy(c->tempfile, conf->cache.root);
strcat(c->tempfile, TMPFILESTR);
#undef TMPFILESTR
p = mktemp(c->tempfile);
#else
if (conf->cache.root == NULL) {
c = ap_proxy_cache_error(c);
break;
}
c->tempfile = ap_palloc(r->pool, strlen(conf->cache.root) +1+ L_tmpnam);
strcpy(c->tempfile, conf->cache.root);
strcat(c->tempfile, "/");
p = tmpnam(NULL);
strcat(c->tempfile, p);
#endif
if (p == NULL) {
c = ap_proxy_cache_error(c);
break;
}
ap_log_error(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r->server, "Create temporary file %s", c->tempfile);
/* create the new file */
c->fp = ap_proxy_create_cachefile(r, c->tempfile);
if (NULL == c->fp) {
c = ap_proxy_cache_error(c);
break;
}
/* write away the cache header and the URL */
if (ap_bvputs(c->fp, buff, "X-URL: ", c->url, "\n", NULL) == -1) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, r,
"proxy: error writing cache file(%s)", c->tempfile);
c = ap_proxy_cache_error(c);
break;
}
/* get original request headers */
if (c->req_hdrs)
req_hdrs = ap_copy_table(r->pool, c->req_hdrs);
else
req_hdrs = ap_copy_table(r->pool, r->headers_in);
/* remove hop-by-hop headers */
ap_proxy_clear_connection(r->pool, req_hdrs);
/* save original request headers */
if (c->req_hdrs)
ap_table_do(ap_proxy_send_hdr_line, c, c->req_hdrs, NULL);
else
ap_table_do(ap_proxy_send_hdr_line, c, r->headers_in, NULL);
if (ap_bputs(CRLF, c->fp) == -1) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, c->req,
"proxy: error writing request headers terminating CRLF to %s", c->tempfile);
c = ap_proxy_cache_error(c);
break;
}
break;
}
/* Was the server response a 304 Not Modified?
*
* If so, we have some work to do that we didn't do when we first
* checked above. We need to fulfil the request, and we need to
* copy the body from the old object to the new one.
*/
/* if response from server 304 not modified */
if (r->status == HTTP_NOT_MODIFIED) {
/* fulfil the request */
c->xcache = ap_pstrcat(r->pool, "HIT from ", ap_get_server_name(r), " (with revalidation)", NULL);
return ap_proxy_cache_conditional(r, c, c->fp);
}
return DECLINED;
}
void ap_proxy_cache_tidy(cache_req *c)
{
server_rec *s;
long int bc;
if (!c || !c->fp)
return;
s = c->req->server;
/* don't care how much was sent, but rather how much was written to cache
ap_bgetopt(c->req->connection->client, BO_BYTECT, &bc);
*/
bc = c->written;
if (c->len != -1) {
/* file lengths don't match; don't cache it */
if (bc != c->len) {
ap_pclosef(c->req->pool, ap_bfileno(c->fp, B_WR)); /* no need to flush */
unlink(c->tempfile);
return;
}
}
/* don't care if aborted, cache it if fully retrieved from host!
else if (c->req->connection->aborted) {
ap_pclosef(c->req->pool, c->fp->fd); / no need to flush /
unlink(c->tempfile);
return;
}
*/
else {
/* update content-length of file */
char buff[17];
off_t curpos;
c->len = bc;
ap_bflush(c->fp);
ap_proxy_sec2hex(c->len, buff);
curpos = lseek(ap_bfileno(c->fp, B_WR), 17*6, SEEK_SET);
if (curpos == -1)
ap_log_error(APLOG_MARK, APLOG_ERR, s,
"proxy: error seeking on cache file %s", c->tempfile);
else if (write(ap_bfileno(c->fp, B_WR), buff, sizeof(buff) - 1) == -1)
ap_log_error(APLOG_MARK, APLOG_ERR, s,
"proxy: error updating cache file %s", c->tempfile);
}
if (ap_bflush(c->fp) == -1) {
ap_log_error(APLOG_MARK, APLOG_ERR, s,
"proxy: error writing to cache file %s",
c->tempfile);
ap_pclosef(c->req->pool, ap_bfileno(c->fp, B_WR));
unlink(c->tempfile);
return;
}
if (ap_pclosef(c->req->pool, ap_bfileno(c->fp, B_WR)) == -1) {
ap_log_error(APLOG_MARK, APLOG_ERR, s,
"proxy: error closing cache file %s", c->tempfile);
unlink(c->tempfile);
return;
}
if (unlink(c->filename) == -1 && errno != ENOENT) {
ap_log_error(APLOG_MARK, APLOG_ERR, s,
"proxy: error deleting old cache file %s",
c->tempfile);
}
else {
char *p;
proxy_server_conf *conf =
(proxy_server_conf *) ap_get_module_config(s->module_config, &proxy_module);
for (p = c->filename + strlen(conf->cache.root) + 1;;) {
p = strchr(p, '/');
if (!p)
break;
*p = '\0';
#if defined(WIN32) || defined(NETWARE)
if (mkdir(c->filename) < 0 && errno != EEXIST)
#elif defined(__TANDEM)
if (mkdir(c->filename, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST)
#else
if (mkdir(c->filename, S_IREAD | S_IWRITE | S_IEXEC) < 0 && errno != EEXIST)
#endif /* WIN32 */
ap_log_error(APLOG_MARK, APLOG_ERR, s,
"proxy: error creating cache directory %s",
c->filename);
*p = '/';
++p;
}
#if defined(OS2) || defined(WIN32) || defined(NETWARE) || defined(MPE)
/* Under OS/2 use rename. */
if (rename(c->tempfile, c->filename) == -1)
ap_log_error(APLOG_MARK, APLOG_ERR, s,
"proxy: error renaming cache file %s to %s",
c->tempfile, c->filename);
}
#else
if (link(c->tempfile, c->filename) == -1)
ap_log_error(APLOG_MARK, APLOG_ERR, s,
"proxy: error linking cache file %s to %s",
c->tempfile, c->filename);
}
if (unlink(c->tempfile) == -1)
ap_log_error(APLOG_MARK, APLOG_ERR, s,
"proxy: error deleting temp file %s", c->tempfile);
#endif
}