/*
 * 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);
	li_g_string_append_len(cfile->tmpfilename, GSTR_LEN(cfile->filename));
	li_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));
	li_g_string_append_len(file, GSTR_LEN(path));
	li_g_string_append_len(file, GSTR_LEN(vr->request.uri.path));
	li_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;
}
