You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

367 lines
9.9 KiB

/*
* mod_cache_disk_etag - cache generated content on disk if etag header is set
*
* 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;
}
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);
cfile->hit_fd = -1;
}
if (cfile->filename) {
g_string_free(cfile->filename, TRUE);
cfile->filename = NULL;
}
if (cfile->tmpfilename) {
g_string_free(cfile->tmpfilename, TRUE);
cfile->tmpfilename = NULL;
}
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);
f->param = NULL;
cache_etag_file_free(cfile);
}
static liHandlerResult cache_etag_filter_hit(liVRequest *vr, liFilter *f) {
UNUSED(vr);
if (NULL != f->in) {
li_chunkqueue_skip_all(f->in);
li_stream_disconnect(&f->stream);
return LI_HANDLER_GO_ON;
}
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;
off_t buflen;
liChunkIter citer;
GError *err = NULL;
if (NULL == f->in) {
cache_etag_filter_free(vr, f);
/* didn't handle f->in->is_closed? abort forwarding */
if (!f->out->is_closed) li_stream_reset(&f->stream);
return LI_HANDLER_GO_ON;
}
if (NULL == cfile) goto forward;
if (f->in->length > 0) {
citer = li_chunkqueue_iter(f->in);
if (LI_HANDLER_GO_ON != li_chunkiter_read(citer, 0, 64*1024, &buf, &buflen, &err)) {
if (NULL != err) {
if (NULL != vr) VR_ERROR(vr, "Couldn't read data from chunkqueue: %s", err->message);
g_error_free(err);
} else {
if (NULL != vr) VR_ERROR(vr, "%s", "Couldn't read data from chunkqueue");
}
cache_etag_filter_free(vr, f);
goto forward;
}
res = write(cfile->fd, buf, buflen);
if (res < 0) {
switch (errno) {
case EINTR:
case EAGAIN:
return LI_HANDLER_COMEBACK;
default:
if (NULL != vr) VR_ERROR(vr, "Couldn't write to temporary cache file '%s': %s",
cfile->tmpfilename->str, g_strerror(errno));
cache_etag_filter_free(vr, f);
goto forward;
}
} else {
if (!f->out->is_closed) {
li_chunkqueue_steal_len(f->out, f->in, res);
} else {
li_chunkqueue_skip(f->in, res);
}
}
}
if (0 == f->in->length && f->in->is_closed) {
f->out->is_closed = TRUE;
f->param = NULL;
cache_etag_file_finish(vr, cfile);
return LI_HANDLER_GO_ON;
}
return LI_HANDLER_GO_ON;
forward:
if (f->out->is_closed) {
li_chunkqueue_skip_all(f->in);
li_stream_disconnect(&f->stream);
} else {
li_chunkqueue_steal_all(f->out, f->in);
if (f->in->is_closed) f->out->is_closed = f->in->is_closed;
}
return 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*) LI_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;
liHandlerResult res;
int err, fd;
if (!cfile) {
if (vr->request.http_method != LI_HTTP_METHOD_GET) return LI_HANDLER_GO_ON;
LI_VREQUEST_WAIT_FOR_RESPONSE_HEADERS(vr);
if (vr->response.http_status != 200) return LI_HANDLER_GO_ON;
/* Don't cache static files if filter list is empty */
if (NULL == vr->filters_out_first && vr->backend_source->out->is_closed && 0 == vr->backend_source->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;
}
res = li_stat_cache_get(vr, cfile->filename, &st, &err, &fd);
if (res == LI_HANDLER_WAIT_FOR_EVENT)
return res;
if (res == LI_HANDLER_GO_ON) {
liFilter *f;
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);
close(fd);
return LI_HANDLER_GO_ON; /* no caching */
}
cfile->hit_fd = fd;
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));
f = li_vrequest_add_filter_out(vr, cache_etag_filter_hit, NULL, NULL, NULL);
if (NULL != f) {
li_chunkqueue_append_file_fd(f->out, NULL, 0, cfile->hit_length, cfile->hit_fd);
f->out->is_closed = TRUE;
cfile->hit_fd = -1;
}
cache_etag_file_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);
*context = NULL;
return LI_HANDLER_GO_ON; /* no caching */
}
li_vrequest_add_filter_out(vr, cache_etag_filter_miss, cache_etag_filter_free, NULL, 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, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
cache_etag_context *ctx;
UNUSED(wrk); UNUSED(p); UNUSED(userdata);
val = li_value_get_single_argument(val);
if (LI_VALUE_STRING != li_value_type(val)) {
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_string(val);
return li_action_new_function(cache_etag_handle, cache_etag_cleanup, cache_etag_free, ctx);
}
static const liPluginOption options[] = {
{ NULL, 0, 0, NULL }
};
static const liPluginAction actions[] = {
{ "cache.disk.etag", cache_etag_create, NULL },
{ NULL, NULL, NULL }
};
static const liPluginSetup setups[] = {
{ NULL, NULL, NULL }
};
static void plugin_init(liServer *srv, liPlugin *p, gpointer userdata) {
UNUSED(srv); UNUSED(userdata);
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, NULL);
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;
}