2
0
Fork 0
lighttpd2/src/modules/mod_cache_disk_etag.c

357 lines
10 KiB
C

/*
* mod_cache_disk_etag - cache generated content on disk if etag header is set
*
* Description:
* cache generated content on disk if etag header is set
*
* Setups:
* none
* Options:
* none
* Actions:
* cache.disk.etag <path> - cache in specified directory
* path: string
* This blocks action progress until the response headers are
* done (i.e. there has to be a content generator before it (like fastcgi/static file)
* You could insert it multiple times of course (e.g. before and after deflate).
*
* Example config:
* cache.disk.etag "/var/lib/lighttpd/cache_etag"
*
* Todo:
* - use stat cache
*
* Author:
* Copyright (c) 2009 Stefan Bühler
*/
#include <lighttpd/base.h>
#include <lighttpd/plugin_core.h>
#include <sys/stat.h>
#include <fcntl.h>
LI_API gboolean mod_cache_disk_etag_init(liModules *mods, liModule *mod);
LI_API gboolean mod_cache_disk_etag_free(liModules *mods, liModule *mod);
typedef struct cache_etag_context cache_etag_context;
struct cache_etag_context {
GString *path;
};
typedef struct cache_etag_file cache_etag_file;
struct cache_etag_file {
GString *filename, *tmpfilename;
int fd;
/* cache hit */
int hit_fd;
goffset hit_length;
};
static cache_etag_file* cache_etag_file_create(GString *filename) {
cache_etag_file *cfile = g_slice_new0(cache_etag_file);
cfile->filename = filename;
cfile->fd = -1;
cfile->hit_fd = -1;
return cfile;
}
static gboolean mkdir_for_file(liVRequest *vr, char *filename) {
char *p = filename;
if (!filename || !filename[0])
return FALSE;
while ((p = strchr(p + 1, '/')) != NULL) {
*p = '\0';
if ((mkdir(filename, 0700) != 0) && (errno != EEXIST)) {
VR_ERROR(vr, "creating cache-directory '%s' failed: %s", filename, g_strerror(errno));
*p = '/';
return FALSE;
}
*p++ = '/';
if (!*p) {
VR_ERROR(vr, "unexpected trailing slash for filename '%s'", filename);
return FALSE;
}
}
return TRUE;
}
static gboolean cache_etag_file_start(liVRequest *vr, cache_etag_file *cfile) {
cfile->tmpfilename = g_string_sized_new(cfile->filename->len + 7);
g_string_append_len(cfile->tmpfilename, GSTR_LEN(cfile->filename));
g_string_append_len(cfile->tmpfilename, CONST_STR_LEN("-XXXXXX"));
if (!mkdir_for_file(vr, cfile->tmpfilename->str)) {
return FALSE;
}
errno = 0; /* posix doesn't define any errors */
if (-1 == (cfile->fd = mkstemp(cfile->tmpfilename->str))) {
VR_ERROR(vr, "Couldn't create cache tempfile '%s': %s", cfile->tmpfilename->str, g_strerror(errno));
return FALSE;
}
#ifdef FD_CLOEXEC
fcntl(cfile->fd, F_SETFD, FD_CLOEXEC);
#endif
return TRUE;
}
static void cache_etag_file_free(cache_etag_file *cfile) {
if (!cfile) return;
if (cfile->fd != -1) {
close(cfile->fd);
unlink(cfile->tmpfilename->str);
}
if (cfile->hit_fd != -1) close(cfile->hit_fd);
if (cfile->filename) g_string_free(cfile->filename, TRUE);
if (cfile->tmpfilename) g_string_free(cfile->tmpfilename, TRUE);
g_slice_free(cache_etag_file, cfile);
}
static void cache_etag_file_finish(liVRequest *vr, cache_etag_file *cfile) {
close(cfile->fd);
cfile->fd = -1;
if (-1 == rename(cfile->tmpfilename->str, cfile->filename->str)) {
VR_ERROR(vr, "Couldn't move temporary cache file '%s': '%s'", cfile->tmpfilename->str, g_strerror(errno));
unlink(cfile->tmpfilename->str);
}
cache_etag_file_free(cfile);
}
/**********************************************************************************/
static void cache_etag_filter_free(liVRequest *vr, liFilter *f) {
cache_etag_file *cfile = (cache_etag_file*) f->param;
UNUSED(vr);
cache_etag_file_free(cfile);
}
static liHandlerResult cache_etag_filter_hit(liVRequest *vr, liFilter *f) {
cache_etag_file *cfile = (cache_etag_file*) f->param;
UNUSED(vr);
if (!cfile) return LI_HANDLER_GO_ON;
f->in->is_closed = TRUE;
li_chunkqueue_append_file_fd(f->out, NULL, 0, cfile->hit_length, cfile->hit_fd);
cfile->hit_fd = -1;
cache_etag_file_free(cfile);
f->param = NULL;
f->out->is_closed = TRUE;
return LI_HANDLER_GO_ON;
}
static liHandlerResult cache_etag_filter_miss(liVRequest *vr, liFilter *f) {
cache_etag_file *cfile = (cache_etag_file*) f->param;
ssize_t res;
gchar *buf;
goffset buflen;
liChunkIter citer = chunkqueue_iter(f->in);
UNUSED(vr);
if (0 == f->in->length) return LI_HANDLER_GO_ON;
if (!cfile) { /* somehow we lost the file */
li_chunkqueue_steal_all(f->out, f->in);
if (f->in->is_closed) f->out->is_closed = TRUE;
return LI_HANDLER_GO_ON;
}
if (LI_HANDLER_GO_ON != li_chunkiter_read(vr, citer, 0, 64*1024, &buf, &buflen)) {
VR_ERROR(vr, "%s", "Couldn't read data from chunkqueue");
cache_etag_file_free(cfile);
f->param = NULL;
li_chunkqueue_steal_all(f->out, f->in);
if (f->in->is_closed) f->out->is_closed = TRUE;
return LI_HANDLER_GO_ON;
}
res = write(cfile->fd, buf, buflen);
if (res < 0) {
switch (errno) {
case EINTR:
case EAGAIN:
break; /* come back later */
default:
VR_ERROR(vr, "Couldn't write to temporary cache file '%s': %s",
cfile->tmpfilename->str, g_strerror(errno));
cache_etag_file_free(cfile);
f->param = NULL;
li_chunkqueue_steal_all(f->out, f->in);
if (f->in->is_closed) f->out->is_closed = TRUE;
return LI_HANDLER_GO_ON;
}
} else {
li_chunkqueue_steal_len(f->out, f->in, res);
if (f->in->length == 0 && f->in->is_closed) {
cache_etag_file_finish(vr, cfile);
f->param = NULL;
f->out->is_closed = TRUE;
return LI_HANDLER_GO_ON;
}
}
return f->in->length ? LI_HANDLER_COMEBACK : LI_HANDLER_GO_ON;
}
static GString* createFileName(liVRequest *vr, GString *path, liHttpHeader *etagheader) {
GString *file = g_string_sized_new(255);
gchar* etag_base64 = g_base64_encode((guchar*) HEADER_VALUE_LEN(etagheader));
g_string_append_len(file, GSTR_LEN(path));
g_string_append_len(file, GSTR_LEN(vr->request.uri.path));
g_string_append_len(file, CONST_STR_LEN("-"));
g_string_append(file, etag_base64);
g_free(etag_base64);
return file;
}
static liHandlerResult cache_etag_cleanup(liVRequest *vr, gpointer param, gpointer context) {
cache_etag_file *cfile = (cache_etag_file*) context;
UNUSED(vr);
UNUSED(param);
cache_etag_file_free(cfile);
return LI_HANDLER_GO_ON;
}
static liHandlerResult cache_etag_handle(liVRequest *vr, gpointer param, gpointer *context) {
cache_etag_context *ctx = (cache_etag_context*) param;
cache_etag_file *cfile = (cache_etag_file*) *context;
GList *etag_entry;
liHttpHeader *etag;
struct stat st;
GString *tmp_str = vr->wrk->tmp_str;
if (!cfile) {
if (vr->request.http_method != LI_HTTP_METHOD_GET) return LI_HANDLER_GO_ON;
VREQUEST_WAIT_FOR_RESPONSE_HEADERS(vr);
if (vr->response.http_status != 200) return LI_HANDLER_GO_ON;
/* Don't cache static files */
if (vr->out->is_closed && 0 == vr->out->mem_usage) return LI_HANDLER_GO_ON;
etag_entry = li_http_header_find_first(vr->response.headers, CONST_STR_LEN("etag"));
if (!etag_entry) return LI_HANDLER_GO_ON; /* no etag -> no caching */
if (li_http_header_find_next(etag_entry, CONST_STR_LEN("etag"))) {
VR_ERROR(vr, "%s", "duplicate etag header in response, will not cache it");
return LI_HANDLER_GO_ON;
}
etag = (liHttpHeader*) etag_entry->data;
cfile = cache_etag_file_create(createFileName(vr, ctx->path, etag));
*context = cfile;
}
/* TODO use async stat cache*/
if (0 == stat(cfile->filename->str, &st)) {
if (!S_ISREG(st.st_mode)) {
VR_ERROR(vr, "Unexpected file type for cache file '%s' (mode %o)", cfile->filename->str, (unsigned int) st.st_mode);
return LI_HANDLER_GO_ON; /* no caching */
}
if (-1 == (cfile->hit_fd = open(cfile->filename->str, O_RDONLY))) {
if (EMFILE == errno) {
li_server_out_of_fds(vr->wrk->srv);
}
VR_ERROR(vr, "Couldn't open cache file '%s': %s", cfile->filename->str, g_strerror(errno));
return LI_HANDLER_GO_ON; /* no caching */
}
#ifdef FD_CLOEXEC
fcntl(cfile->hit_fd, F_SETFD, FD_CLOEXEC);
#endif
if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
VR_DEBUG(vr, "cache hit for '%s'", vr->request.uri.path->str);
}
cfile->hit_length = st.st_size;
g_string_truncate(tmp_str, 0);
li_string_append_int(tmp_str, st.st_size);
li_http_header_overwrite(vr->response.headers, CONST_STR_LEN("Content-Length"), GSTR_LEN(tmp_str));
li_vrequest_add_filter_out(vr, cache_etag_filter_hit, cache_etag_filter_free, cfile);
*context = NULL;
return LI_HANDLER_GO_ON;
}
if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
VR_DEBUG(vr, "cache miss for '%s'", vr->request.uri.path->str);
}
if (!cache_etag_file_start(vr, cfile)) {
cache_etag_file_free(cfile);
return LI_HANDLER_GO_ON; /* no caching */
}
li_vrequest_add_filter_out(vr, cache_etag_filter_miss, cache_etag_filter_free, cfile);
*context = NULL;
return LI_HANDLER_GO_ON;
}
static void cache_etag_free(liServer *srv, gpointer param) {
cache_etag_context *ctx = (cache_etag_context*) param;
UNUSED(srv);
g_string_free(ctx->path, TRUE);
g_slice_free(cache_etag_context, ctx);
}
static liAction* cache_etag_create(liServer *srv, liPlugin* p, liValue *val) {
cache_etag_context *ctx;
UNUSED(p);
if (val->type != LI_VALUE_STRING) {
ERROR(srv, "%s", "cache.disk.etag expects a string as parameter");
return FALSE;
}
ctx = g_slice_new0(cache_etag_context);
ctx->path = li_value_extract_ptr(val);
return li_action_new_function(cache_etag_handle, cache_etag_cleanup, cache_etag_free, ctx);
}
static const liPluginOption options[] = {
{ NULL, 0, NULL, NULL, NULL }
};
static const liPluginAction actions[] = {
{ "cache.disk.etag", cache_etag_create },
{ NULL, NULL }
};
static const liliPluginSetupCB setups[] = {
{ NULL, NULL }
};
static void plugin_init(liServer *srv, liPlugin *p) {
UNUSED(srv);
p->options = options;
p->actions = actions;
p->setups = setups;
}
gboolean mod_cache_disk_etag_init(liModules *mods, liModule *mod) {
MODULE_VERSION_CHECK(mods);
mod->config = li_plugin_register(mods->main, "mod_cache_disk_etag", plugin_init);
return mod->config != NULL;
}
gboolean mod_cache_disk_etag_free(liModules *mods, liModule *mod) {
if (mod->config)
li_plugin_free(mods->main, mod->config);
return TRUE;
}