
#include <lighttpd/base.h>
#include <lighttpd/pattern.h>

#include <lighttpd/plugin_core.h>

#include <lighttpd/version.h>

#include <lighttpd/http_range_parser.h>

#include <sys/stat.h>
#include <fcntl.h>

static liAction* core_list(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	liAction *a;
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);

	if (NULL == val) {
		ERROR(srv, "%s", "need parameter");
		return NULL;
	}

	if (LI_VALUE_ACTION == li_value_type(val)) {
		a = val->data.val_action.action;
		li_action_acquire(a);
		return a;
	}

	if (LI_VALUE_LIST != li_value_type(val)) {
		ERROR(srv, "expected list, got %s", li_value_type_string(val));
		return NULL;
	}

	a = li_action_new_list();
	LI_VALUE_FOREACH(oa, val)
		if (LI_VALUE_ACTION != li_value_type(oa)) {
			ERROR(srv, "expected action at entry %u of list, got %s", _oa_i, li_value_type_string(oa));
			li_action_release(srv, a);
			return NULL;
		}
		LI_FORCE_ASSERT(srv == oa->data.val_action.srv);
		li_action_acquire(oa->data.val_action.action);
		g_array_append_val(a->data.list, oa->data.val_action.action);
	LI_VALUE_END_FOREACH()
	return a;
}

static liAction* core_when(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	liValue *val_cond, *val_act, *val_act_else;
	liAction *a, *act = NULL, *act_else = NULL;
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	if (NULL == val) {
		ERROR(srv, "%s", "need parameter");
		return NULL;
	}
	if (LI_VALUE_LIST != li_value_type(val)) {
		ERROR(srv, "expected list, got %s", li_value_type_string(val));
		return NULL;
	}
	if (li_value_list_has_len(val, 2)) {
		val_act_else = NULL;
		act_else = NULL;
	} else if (li_value_list_has_len(val, 3)) {
		val_act_else = li_value_list_at(val, 2);
	} else {
		ERROR(srv, "expected list with length 2 or 3, has length %u", li_value_list_len(val));
		return NULL;
	}
	val_cond = li_value_list_at(val, 0);
	val_act = li_value_list_at(val, 1);

	if (LI_VALUE_CONDITION != li_value_type(val_cond)) {
		ERROR(srv, "expected condition as first parameter, got %s", NULL == val_cond ? "NULL" : li_value_type_string(val_cond));
		return NULL;
	}
	if (LI_VALUE_NONE == li_value_type(val_act)) {
		act = NULL;
	} else if (LI_VALUE_ACTION == li_value_type(val_act)) {
		act = val_act->data.val_action.action;
	} else {
		ERROR(srv, "expected action as second parameter, got %s", li_value_type_string(val_act));
		return NULL;
	}
	if (LI_VALUE_NONE == li_value_type(val_act_else)) {
		act_else = NULL;
	} else if (LI_VALUE_ACTION == li_value_type(val_act_else)) {
		act_else = val_act_else->data.val_action.action;
	} else {
		ERROR(srv, "expected action as third parameter, got %s", li_value_type_string(val_act_else));
		return NULL;
	}
	li_condition_acquire(val_cond->data.val_cond.cond);
	if (NULL != act) li_action_acquire(act);
	if (NULL != act_else) li_action_acquire(act_else);
	a = li_action_new_condition(val_cond->data.val_cond.cond, act, act_else);
	return a;
}

typedef struct docroot_split docroot_split;
struct docroot_split {
	GString *hostname;
	gchar** splits;
	guint split_len;
};

static void core_docroot_nth_cb(GString *pattern_result, guint to, guint from, gpointer data) {
	/* $n means n-th part of hostname from end divided by dots */
	/* range is interpreted reversed !!! */
	gboolean first = TRUE;
	guint i;
	docroot_split *ctx = data;

	if (0 == ctx->hostname->len) return;

	/* ranges including 0 will only get the complete hostname */
	if (0 == from || 0 == to) {
		li_g_string_append_len(pattern_result, GSTR_LEN(ctx->hostname));
		return;
	}

	if (NULL == ctx->splits) {
		ctx->splits = g_strsplit_set(ctx->hostname->str, ".", 31);
		ctx->split_len = g_strv_length(ctx->splits);
	}

	if (0 == ctx->split_len) return;

	from = MIN(from, ctx->split_len);
	to = MIN(to, ctx->split_len);

	if (from <= to) {
		for (i = from; i <= to; i++) {
			if (first) {
				first = FALSE;
			} else {
				li_g_string_append_len(pattern_result, CONST_STR_LEN("."));
			}
			g_string_append(pattern_result, ctx->splits[ctx->split_len - i]);
		}
	} else {
		for (i = from; i >= to; i--) { /* to > 0, so no underflow in i possible */
			if (first) {
				first = FALSE;
			} else {
				li_g_string_append_len(pattern_result, CONST_STR_LEN("."));
			}
			g_string_append(pattern_result, ctx->splits[ctx->split_len - i]);
		}
	}
}

static liHandlerResult core_handle_docroot(liVRequest *vr, gpointer param, gpointer *context) {
	guint i;
	GMatchInfo *match_info = NULL;
	GArray *arr = param;
	docroot_split dsplit = { vr->request.uri.host, NULL, 0 };

	g_string_truncate(vr->physical.doc_root, 0);

	if (vr->action_stack.regex_stack->len) {
		GArray *rs = vr->action_stack.regex_stack;
		match_info = g_array_index(rs, liActionRegexStackElement, rs->len - 1).match_info;
	}

	/* resume from last stat check */
	if (*context) {
		i = GPOINTER_TO_INT(*context);
	} else {
		i = 0;
	}
	*context = NULL;

	/* loop over all the patterns until we find an existing path */
	for (; i < arr->len; i++) {
		struct stat st;
		gint err;


		g_string_truncate(vr->physical.doc_root, 0);
		li_pattern_eval(vr, vr->physical.doc_root, g_array_index(arr, liPattern*, i), core_docroot_nth_cb, &dsplit, li_pattern_regex_cb, match_info);

		/* if there's only one entry and we're not debug logging, don't stat */
		if (i == arr->len - 1 && !CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) break;

		/* check if path exists */
		switch (li_stat_cache_get(vr, vr->physical.doc_root, &st, &err, NULL)) {
		case LI_HANDLER_GO_ON: break;
		case LI_HANDLER_WAIT_FOR_EVENT:
			*context = GINT_TO_POINTER(i);
			if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
				VR_DEBUG(vr, "docroot: waiting for async: \"%s\"", vr->physical.doc_root->str);
			}
			g_strfreev(dsplit.splits);
			return LI_HANDLER_WAIT_FOR_EVENT;
		default:
			/* not found, try next pattern */
			if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
				VR_DEBUG(vr, "docroot: not found: \"%s\", trying next", vr->physical.doc_root->str);
			}
			continue;
		}

		break;
	}

	g_strfreev(dsplit.splits);

	/* build physical path: docroot + uri.path */
	g_string_truncate(vr->physical.path, 0);
	li_g_string_append_len(vr->physical.path, GSTR_LEN(vr->physical.doc_root));
	if (vr->request.uri.path->len == 0 || vr->request.uri.path->str[0] != '/')
		li_path_append_slash(vr->physical.path);
	li_g_string_append_len(vr->physical.path, GSTR_LEN(vr->request.uri.path));

	if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
		VR_DEBUG(vr, "docroot: \"%s\"", vr->physical.doc_root->str);
		VR_DEBUG(vr, "physical path: \"%s\"", vr->physical.path->str);
	}

	return LI_HANDLER_GO_ON;
}

static void core_docroot_free(liServer *srv, gpointer param) {
	guint i;
	GArray *arr = param;

	UNUSED(srv);

	for (i = 0; i < arr->len; i++) {
		li_pattern_free(g_array_index(arr, liPattern*, i));
	}

	g_array_free(arr, TRUE);
}

static liAction* core_docroot(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	GArray *arr;
	liPattern *pattern;
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);

	if (LI_VALUE_STRING != li_value_type(val) && LI_VALUE_LIST != li_value_type(val)) {
		ERROR(srv, "%s", "docroot action expects a string or list of strings as parameter");
		return NULL;
	}

	arr = g_array_new(FALSE, TRUE, sizeof(liPattern*));

	if (LI_VALUE_STRING == li_value_type(val)) {
		pattern = li_pattern_new(srv, val->data.string->str);
		if (NULL == pattern) return FALSE;
		g_array_append_val(arr, pattern);
	} else {
		LI_VALUE_FOREACH(v, val)
			if (LI_VALUE_STRING != li_value_type(v)) {
				ERROR(srv, "%s", "docroot action expects a string or list of strings as parameter");
				core_docroot_free(srv, arr);
				return NULL;
			}

			pattern = li_pattern_new(srv, v->data.string->str);
			if (NULL == pattern) {
				ERROR(srv, "%s", "docroot: failed to parse pattern");
				core_docroot_free(srv, arr);
				return FALSE;
			}
			g_array_append_val(arr, pattern);
		LI_VALUE_END_FOREACH()
	}

	return li_action_new_function(core_handle_docroot, NULL, core_docroot_free, arr);
}

typedef struct {
	GString *prefix;
	liPattern *path;
} core_alias_config;

static liHandlerResult core_handle_alias(liVRequest *vr, gpointer _param, gpointer *context) {
	GArray *param = _param;
	guint i;
	docroot_split dsplit = { vr->request.uri.host, NULL, 0 };
	GMatchInfo *match_info = NULL;
	UNUSED(context);

	if (vr->action_stack.regex_stack->len) {
		GArray *rs = vr->action_stack.regex_stack;
		match_info = g_array_index(rs, liActionRegexStackElement, rs->len - 1).match_info;
	}

	for (i = 0; i < param->len; i++) {
		core_alias_config ac = g_array_index(param, core_alias_config, i);
		gsize preflen = ac.prefix->len;

		if (preflen > 0 && ac.prefix->str[preflen-1] == '/') {
			preflen--;
		}

		if (li_string_prefix(vr->request.uri.path, ac.prefix->str, preflen)) {
			/* check if url has the form "prefix" or "prefix/.*" */
			if (vr->request.uri.path->str[preflen] != '\0' && vr->request.uri.path->str[preflen] != '/') continue;

			g_string_truncate(vr->physical.doc_root, 0);
			li_pattern_eval(vr, vr->physical.doc_root, ac.path, core_docroot_nth_cb, &dsplit, li_pattern_regex_cb, match_info);

			/* prefix matched */
			if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
				VR_DEBUG(vr, "alias path: %s", vr->physical.doc_root->str);
			}

			/* build physical path: docroot + uri.path */
			g_string_truncate(vr->physical.path, 0);
			li_g_string_append_len(vr->physical.path, GSTR_LEN(vr->physical.doc_root));
			/* we checked uri.path to be either "prefix" or "prefix/.*", so uri.path[prefixlen..] is either empty
			 * or starts with a "/" */
			li_g_string_append_len(vr->physical.path, vr->request.uri.path->str + preflen, vr->request.uri.path->len - preflen);

			if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
				VR_DEBUG(vr, "alias physical path: %s", vr->physical.path->str);
			}

			return LI_HANDLER_GO_ON;
		}
	}

	return LI_HANDLER_GO_ON;
}

static void core_alias_free(liServer *srv, gpointer _param) {
	GArray *param = _param;
	guint i;
	UNUSED(srv);

	if (NULL == param) return;

	for (i = 0; i < param->len; i++) {
		core_alias_config ac = g_array_index(param, core_alias_config, i);
		g_string_free(ac.prefix, TRUE);
		li_pattern_free(ac.path);
	}
	g_array_free(param, TRUE);
}

static liAction* core_alias(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	GArray *a = NULL;
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);

	if (LI_VALUE_LIST != li_value_type(val)) {
		ERROR(srv, "%s", "unexpected or no parameter for alias action");
		return NULL;
	}
	if (li_value_list_has_len(val, 2)) {
		liValue *vPrefix = li_value_list_at(val, 0);
		liValue *vPath = li_value_list_at(val, 1);
		if (NULL == vPrefix || NULL == vPath) goto fail;

		if (LI_VALUE_STRING == li_value_type(vPrefix) && LI_VALUE_STRING == li_value_type(vPath)) {
			core_alias_config ac;

			ac.path = li_pattern_new(srv, vPath->data.string->str);
			if (NULL == ac.path) goto fail;
			ac.prefix = li_value_extract_string(vPrefix);

			a = g_array_sized_new(FALSE, TRUE, sizeof(core_alias_config), 1);
			g_array_append_val(a, ac);
		}
	}

	if (NULL == a) {
		a = g_array_sized_new(FALSE, TRUE, sizeof(core_alias_config), li_value_list_len(val));
		LI_VALUE_FOREACH(entry, val)
			liValue *vPrefix, *vPath;
			core_alias_config ac;

			if (!li_value_list_has_len(entry, 2)) goto fail;
			vPrefix = li_value_list_at(entry, 0);
			vPath = li_value_list_at(entry, 1);

			if (LI_VALUE_STRING != li_value_type(vPrefix) || LI_VALUE_STRING != li_value_type(vPath)) goto fail;

			ac.path = li_pattern_new(srv, vPath->data.string->str);
			if (NULL == ac.path) goto fail;
			ac.prefix = li_value_extract_string(vPrefix);

			g_array_append_val(a, ac);
		LI_VALUE_END_FOREACH()
	}

	return li_action_new_function(core_handle_alias, NULL, core_alias_free, a);

fail:
	ERROR(srv, "%s", "unexpected entry in parameter for alias action");
	core_alias_free(srv, a);
	return NULL;
}

/* *context represents the current entry + 1, 0 is the ISDIR(vr->physical.path) check */
static liHandlerResult core_handle_index(liVRequest *vr, gpointer param, gpointer *context) {
	liHandlerResult res;
	guint i;
	struct stat st;
	gint err;
	GString *file, *tmp_docroot, *tmp_path;
	GPtrArray *files = param;
	gint contextNdx = GPOINTER_TO_INT(*context);
	gboolean is_dir = TRUE, need_redirect = FALSE;

	UNUSED(context);

	if (!vr->physical.doc_root->len) {
		VR_ERROR(vr, "%s", "no docroot specified yet but index action called");
		return LI_HANDLER_ERROR;
	}

	if (0 == contextNdx) {
		if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
			VR_DEBUG(vr, "index: stat phys file: '%s'", vr->physical.path->str);
		}
		res = li_stat_cache_get(vr, vr->physical.path, &st, &err, NULL);
		if (res == LI_HANDLER_WAIT_FOR_EVENT)
			return LI_HANDLER_WAIT_FOR_EVENT;

		if (LI_HANDLER_GO_ON == res && S_ISREG(st.st_mode)) {
			return LI_HANDLER_GO_ON;
		}

		i = 0;
		is_dir = (LI_HANDLER_ERROR != res && S_ISDIR(st.st_mode));
	} else {
		i = (contextNdx >> 1) - 1;
		is_dir = (0 != (contextNdx & 0x1));
	}

	if (is_dir) {
		/* need trailing slash? */
		need_redirect = vr->request.uri.path->len == 0 || vr->request.uri.path->str[vr->request.uri.path->len-1] != '/';
	}

	/* we use two temporary strings here, one to append to docroot and one to append to physical path */
	tmp_docroot = vr->wrk->tmp_str;
	g_string_truncate(tmp_docroot, 0);
	li_g_string_append_len(vr->wrk->tmp_str, GSTR_LEN(vr->physical.doc_root));
	tmp_path = g_string_new_len(GSTR_LEN(vr->physical.path));

	/* loop through the list to find a possible index file */
	for (; i < files->len; i++) {
		file = ((liValue*)g_ptr_array_index(files, i))->data.string;

		if (file->str[0] == '/') {
			/* entries beginning with a slash shall be looked up directly at the docroot */
			g_string_truncate(tmp_docroot, vr->physical.doc_root->len);
			li_g_string_append_len(tmp_docroot, GSTR_LEN(file));
			if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
				VR_DEBUG(vr, "trying index file: '%s' -> '%s'", file->str, tmp_docroot->str);
			}
			res = li_stat_cache_get(vr, tmp_docroot, &st, &err, NULL);
		} else {
			if (!is_dir) continue;

			if (need_redirect) {
				li_vrequest_redirect_directory(vr);
				g_string_free(tmp_path, TRUE);
				return LI_HANDLER_GO_ON;
			}

			g_string_truncate(tmp_path, vr->physical.path->len);
			li_g_string_append_len(tmp_path, GSTR_LEN(file));
			if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
				VR_DEBUG(vr, "trying index file: '%s' -> '%s'", file->str, tmp_path->str);
			}
			res = li_stat_cache_get(vr, tmp_path, &st, &err, NULL);
		}

		if (res == LI_HANDLER_WAIT_FOR_EVENT) {
			*context = GINT_TO_POINTER(2*(i + 1) + (is_dir ? 1 : 0));
			g_string_free(tmp_path, TRUE);
			return LI_HANDLER_WAIT_FOR_EVENT;
		}

		if (res == LI_HANDLER_GO_ON) {
			/* file exists. change physical path */
			if (file->str[0] == '/') {
				g_string_truncate(vr->physical.path, vr->physical.doc_root->len);
				g_string_truncate(vr->request.uri.path, 0);
			}

			li_g_string_append_len(vr->physical.path, GSTR_LEN(file));
			li_g_string_append_len(vr->request.uri.path, GSTR_LEN(file));

			if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
				VR_DEBUG(vr, "using index file: '%s'", file->str);
			}

			g_string_free(tmp_path, TRUE);

			return LI_HANDLER_GO_ON;
		}
	}

	g_string_free(tmp_path, TRUE);

	return LI_HANDLER_GO_ON;
}

static void core_index_free(liServer *srv, gpointer param) {
	UNUSED(srv);
	li_value_list_free((GPtrArray*) param);
}

static liAction* core_index(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	if (LI_VALUE_STRING == li_value_type(val)) {
		li_value_wrap_in_list(val);
	}

	if (LI_VALUE_LIST != li_value_type(val)) {
		ERROR(srv, "%s", "index action expects a list of strings as parameter");
		return NULL;
	}

	if (li_value_list_has_len(val, 1) && LI_VALUE_LIST == li_value_list_type_at(val, 1)) {
		/* only extract if single element is a list */
		val = li_value_get_single_argument(val);
	}

	LI_VALUE_FOREACH(entry, val)
		if (LI_VALUE_STRING != li_value_type(entry)) {
			ERROR(srv, "%s", "index action expects a list of strings as parameter");
			return NULL;
		}
	LI_VALUE_END_FOREACH()

	return li_action_new_function(core_handle_index, NULL, core_index_free, li_value_extract_list(val));
}


static liHandlerResult core_handle_static(liVRequest *vr, gpointer param, gpointer *context) {
	int fd = -1;
	struct stat st;
	int err;
	liHandlerResult res;
	GPtrArray *exclude_arr = CORE_OPTIONPTR(LI_CORE_OPTION_STATIC_FILE_EXCLUDE_EXTENSIONS).list;
	static const gchar boundary[] = "fkj49sn38dcn3";
	gboolean no_fail = GPOINTER_TO_INT(param);

	UNUSED(param);
	UNUSED(context);

	if (li_vrequest_is_handled(vr))
		return LI_HANDLER_GO_ON;

	switch (vr->request.http_method) {
	case LI_HTTP_METHOD_GET:
	case LI_HTTP_METHOD_HEAD:
		break;
	default:
		if (no_fail) return LI_HANDLER_GO_ON;

		if (!li_vrequest_handle_direct(vr)) {
			return LI_HANDLER_ERROR;
		}

		vr->response.http_status = 405;
		li_http_header_overwrite(vr->response.headers, CONST_STR_LEN("Allow"), CONST_STR_LEN("GET, HEAD"));
		return LI_HANDLER_GO_ON;
	}

	if (vr->physical.path->len == 0) return LI_HANDLER_GO_ON;
	if (vr->physical.path->str[vr->physical.path->len-1] == '/') return LI_HANDLER_GO_ON;

	if (exclude_arr) {
		guint i;
		GString *tmp_str = vr->wrk->tmp_str;
		gchar *basep = g_path_get_basename(vr->physical.path->str);
		g_string_assign(tmp_str, basep);
		g_free(basep);

		for (i = 0; i < exclude_arr->len; i++) {
			liValue *v = g_ptr_array_index(exclude_arr, i);
			if (li_string_suffix(tmp_str, GSTR_LEN(v->data.string))) {
				if (no_fail) return LI_HANDLER_GO_ON;

				if (!li_vrequest_handle_direct(vr)) {
					return LI_HANDLER_ERROR;
				}

				vr->response.http_status = 403;
				return LI_HANDLER_GO_ON;
			}
		}
	}

	res = li_stat_cache_get(vr, vr->physical.path, &st, &err, &fd);
	if (res == LI_HANDLER_WAIT_FOR_EVENT)
		return res;

	if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
		VR_DEBUG(vr, "try serving static file: '%s'", vr->physical.path->str);
	}

	if (res == LI_HANDLER_ERROR) {
		/* open or fstat failed */

		if (fd != -1)
			close(fd);

		if (no_fail) return LI_HANDLER_GO_ON;

		if (!li_vrequest_handle_direct(vr)) {
			return LI_HANDLER_ERROR;
		}

		switch (err) {
		case ENOENT:
		case ENOTDIR:
			vr->response.http_status = 404;
			return LI_HANDLER_GO_ON;
		case EACCES:
			vr->response.http_status = 403;
			return LI_HANDLER_GO_ON;
		case ENAMETOOLONG:
			vr->response.http_status = 414;
			return LI_HANDLER_GO_ON;
		default:
			VR_ERROR(vr, "stat() or open() for '%s' failed: %s", vr->physical.path->str, g_strerror(err));
			vr->response.http_status = 500;
			return LI_HANDLER_GO_ON;
		}
	} else if (S_ISDIR(st.st_mode)) {
		if (fd != -1)
			close(fd);
		return LI_HANDLER_GO_ON;
	} else if (!S_ISREG(st.st_mode)) {
		if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
			VR_DEBUG(vr, "not a regular file: '%s'", vr->physical.path->str);
		}

		if (fd != -1)
			close(fd);

		if (no_fail) return LI_HANDLER_GO_ON;

		if (!li_vrequest_handle_direct(vr)) {
			return LI_HANDLER_ERROR;
		}
		vr->response.http_status = 403;
		return LI_HANDLER_GO_ON;
	} else {
		const GString *mime_str;
		gboolean cachable;
		gboolean ranged_response = FALSE;
		liHttpHeader *hh_range;
		liChunkFile *cf;
		static const GString default_mime_str = { CONST_STR_LEN("application/octet-stream"), 0 };

		if (!li_vrequest_handle_direct(vr)) {
			close(fd);
			return LI_HANDLER_ERROR;
		}

		li_etag_set_header(vr, &st, &cachable);
		if (cachable) {
			vr->response.http_status = 304;
			close(fd);
			return LI_HANDLER_GO_ON;
		}

		cf = li_chunkfile_new(NULL, fd, FALSE);

		mime_str = li_mimetype_get(vr, vr->physical.path);
		if (!mime_str) mime_str = &default_mime_str;

		if (CORE_OPTION(LI_CORE_OPTION_STATIC_RANGE_REQUESTS).boolean) {
			li_http_header_overwrite(vr->response.headers, CONST_STR_LEN("Accept-Ranges"), CONST_STR_LEN("bytes"));

			hh_range = li_http_header_lookup(vr->request.headers, CONST_STR_LEN("range"));
			if (hh_range) {
				/* TODO: Check If-Range: header */
				const GString range_str = li_const_gstring(LI_HEADER_VALUE_LEN(hh_range));
				liParseHttpRangeState rs;
				gboolean is_multipart = FALSE, done = FALSE;

				li_parse_http_range_init(&rs, &range_str, st.st_size);
				do {
					switch (li_parse_http_range_next(&rs)) {
					case LI_PARSE_HTTP_RANGE_OK:
						if (!is_multipart && !rs.last_range) {
							is_multipart = TRUE;
						}
						g_string_printf(vr->wrk->tmp_str, "bytes %"G_GINT64_FORMAT"-%"G_GINT64_FORMAT"/%"G_GINT64_FORMAT, rs.range_start, rs.range_end, (goffset) st.st_size);
						if (is_multipart) {
							GString *subheader = g_string_sized_new(1023);
							g_string_append_printf(subheader, "\r\n--%s\r\nContent-Type: %s\r\nContent-Range: %s\r\n\r\n", boundary, mime_str->str, vr->wrk->tmp_str->str);
							li_chunkqueue_append_string(vr->direct_out, subheader);
							li_chunkqueue_append_chunkfile(vr->direct_out, cf, rs.range_start, rs.range_length);
						} else {
							li_http_header_overwrite(vr->response.headers, CONST_STR_LEN("Content-Range"), GSTR_LEN(vr->wrk->tmp_str));
							li_chunkqueue_append_chunkfile(vr->direct_out, cf, rs.range_start, rs.range_length);
						}
						break;
					case LI_PARSE_HTTP_RANGE_DONE:
						ranged_response = TRUE;
						done = TRUE;
						vr->response.http_status = 206;
						if (is_multipart) {
							GString *subheader = g_string_sized_new(1023);
							g_string_append_printf(subheader, "\r\n--%s--\r\n", boundary);
							li_chunkqueue_append_string(vr->direct_out, subheader);

							g_string_printf(vr->wrk->tmp_str, "multipart/byteranges; boundary=%s", boundary);
							li_http_header_overwrite(vr->response.headers, CONST_STR_LEN("Content-Type"), GSTR_LEN(vr->wrk->tmp_str));
						} else {
							li_http_header_overwrite(vr->response.headers, CONST_STR_LEN("Content-Type"), GSTR_LEN(mime_str));
						}
						break;
					case LI_PARSE_HTTP_RANGE_INVALID:
						done = TRUE;
						/* indirect handing: out cq is already "closed" */
						li_chunkqueue_reset(vr->direct_out); vr->direct_out->is_closed = TRUE;
						break;
					case LI_PARSE_HTTP_RANGE_NOT_SATISFIABLE:
						ranged_response = TRUE;
						done = TRUE;
						li_chunkqueue_reset(vr->direct_out);
						g_string_printf(vr->wrk->tmp_str, "bytes */%"G_GINT64_FORMAT, (goffset) st.st_size);
						li_http_header_overwrite(vr->response.headers, CONST_STR_LEN("Content-Range"), GSTR_LEN(vr->wrk->tmp_str));
						vr->response.http_status = 416;
						break;
					}
				} while (!done);
				li_parse_http_range_clear(&rs);
			}
		}

		if (!ranged_response) {
			vr->response.http_status = 200;
			li_http_header_overwrite(vr->response.headers, CONST_STR_LEN("Content-Type"), GSTR_LEN(mime_str));
			li_chunkqueue_append_chunkfile(vr->direct_out, cf, 0, st.st_size);
		}

		li_chunkfile_release(cf);
	}

	return LI_HANDLER_GO_ON;
}

static liAction* core_static(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);
	if (!li_value_is_nothing(val)) {
		ERROR(srv, "%s", "static action doesn't have parameters");
		return NULL;
	}

	return li_action_new_function(core_handle_static, NULL, NULL, GINT_TO_POINTER(0));
}

static liAction* core_static_no_fail(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);
	if (!li_value_is_nothing(val)) {
		ERROR(srv, "%s", "static_no_fail action doesn't have parameters");
		return NULL;
	}

	return li_action_new_function(core_handle_static, NULL, NULL, GINT_TO_POINTER(1));
}


static liHandlerResult core_handle_pathinfo(liVRequest *vr, gpointer param, gpointer *context) {
	struct stat st;
	int err;
	liHandlerResult res;
	gchar *slash;
	UNUSED(param);
	UNUSED(context);

	if (li_vrequest_is_handled(vr)) return LI_HANDLER_GO_ON;

next_round:
	if (vr->physical.path->len <= vr->physical.doc_root->len) return LI_HANDLER_GO_ON;

	if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
		VR_DEBUG(vr, "stat: physical path: %s", vr->physical.path->str);
	}
	res = li_stat_cache_get(vr, vr->physical.path, &st, &err, NULL);
	if (res == LI_HANDLER_GO_ON) {
		if (vr->physical.pathinfo->len > 0) {
			/* remove PATHINFO from url too ("SCRIPT_NAME") */
			if (li_string_suffix(vr->request.uri.path, GSTR_LEN(vr->physical.pathinfo))) {
				g_string_set_size(vr->request.uri.path, vr->request.uri.path->len - vr->physical.pathinfo->len);
			}
		}
	}
	if (res == LI_HANDLER_WAIT_FOR_EVENT || res == LI_HANDLER_GO_ON)
		return res;

	/* stat failed. why? */
	if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
		VR_DEBUG(vr, "stat failed %i: physical path: %s", err, vr->physical.path->str);
	}
	switch (err) {
	case ENOTDIR:
		slash = strrchr(vr->physical.path->str, '/');
		if (!slash) {
			if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
				VR_DEBUG(vr, "no slash: %s", vr->physical.path->str);
			}
			return LI_HANDLER_GO_ON;
		}
		g_string_prepend(vr->physical.pathinfo, slash);
		g_string_set_size(vr->physical.path, slash - vr->physical.path->str);
		if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
			VR_DEBUG(vr, "physical path: %s", vr->physical.path->str);
		}
		goto next_round;
	case ENAMETOOLONG:
	case ENOENT:
	case EACCES:
		return LI_HANDLER_GO_ON;
	default:
		VR_ERROR(vr, "stat() or open() for '%s' failed: %s", vr->physical.path->str, g_strerror(err));
		if (!li_vrequest_handle_direct(vr)) return LI_HANDLER_ERROR;
		vr->response.http_status = 500;
		return LI_HANDLER_GO_ON;
	}

	return LI_HANDLER_GO_ON;
}

static liAction* core_pathinfo(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);
	if (!li_value_is_nothing(val)) {
		ERROR(srv, "%s", "pathinfo action doesn't have parameters");
		return NULL;
	}

	return li_action_new_function(core_handle_pathinfo, NULL, NULL, NULL);
}

static liHandlerResult core_handle_status(liVRequest *vr, gpointer param, gpointer *context) {
	UNUSED(param);
	UNUSED(context);

	vr->response.http_status = GPOINTER_TO_INT(param);

	return LI_HANDLER_GO_ON;
}

static liAction* core_status(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	gpointer ptr;
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);

	if (LI_VALUE_NUMBER != li_value_type(val)) {
		ERROR(srv, "%s", "set_status action expects a number as parameter");
		return NULL;
	}

	ptr = GINT_TO_POINTER((gint) val->data.number);

	return li_action_new_function(core_handle_status, NULL, NULL, ptr);
}


static void core_log_write_free(liServer *srv, gpointer param) {
	UNUSED(srv);

	li_pattern_free(param);
}

static liHandlerResult core_handle_log_write(liVRequest *vr, gpointer param, gpointer *context) {
	liPattern *pattern = param;
	GMatchInfo *match_info = NULL;

	if (vr->action_stack.regex_stack->len) {
		GArray *rs = vr->action_stack.regex_stack;
		match_info = g_array_index(rs, liActionRegexStackElement, rs->len - 1).match_info;
	}

	UNUSED(context);

	/* eval pattern, ignore $n */
	g_string_truncate(vr->wrk->tmp_str, 0);
	li_pattern_eval(vr, vr->wrk->tmp_str, pattern, NULL, NULL, li_pattern_regex_cb, match_info);

	VR_INFO(vr, "%s", vr->wrk->tmp_str->str);

	return LI_HANDLER_GO_ON;
}

static liAction* core_log_write(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	liPattern *pattern;
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);

	if (LI_VALUE_STRING != li_value_type(val)) {
		ERROR(srv, "%s", "log.write expects a string parameter");
		return NULL;
	}

	pattern = li_pattern_new(srv, val->data.string->str);
	if (!pattern) {
		ERROR(srv, "%s", "log.write failed to parse pattern");
		return NULL;
	}

	return li_action_new_function(core_handle_log_write, NULL, core_log_write_free, pattern);
}


typedef struct respond_param respond_param;
struct respond_param {
	guint status_code;
	liPattern *pattern;
};

static void core_respond_free(liServer *srv, gpointer param) {
	respond_param *rp = param;

	UNUSED(srv);

	if (rp->pattern)
		li_pattern_free(rp->pattern);

	g_slice_free(respond_param, rp);
}

static liHandlerResult core_handle_respond(liVRequest *vr, gpointer param, gpointer *context) {
	respond_param *rp = param;
	GMatchInfo *match_info = NULL;

	UNUSED(context);

	if (!li_vrequest_handle_direct(vr))
		return LI_HANDLER_GO_ON;

	if (vr->action_stack.regex_stack->len) {
		GArray *rs = vr->action_stack.regex_stack;
		match_info = g_array_index(rs, liActionRegexStackElement, rs->len - 1).match_info;
	}

	vr->response.http_status = rp->status_code;

	if (!li_http_header_lookup(vr->response.headers, CONST_STR_LEN("content-type")))
		li_http_header_insert(vr->response.headers, CONST_STR_LEN("Content-Type"), CONST_STR_LEN("text/plain"));

	if (rp->pattern) {
		g_string_truncate(vr->wrk->tmp_str, 0);
		li_pattern_eval(vr, vr->wrk->tmp_str, rp->pattern, NULL, NULL, li_pattern_regex_cb, match_info);
		li_chunkqueue_append_mem(vr->direct_out, GSTR_LEN(vr->wrk->tmp_str));
	}

	return LI_HANDLER_GO_ON;
}

static liAction* core_respond(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	respond_param *rp;
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);
	val = li_value_get_single_argument(val);

	rp = g_slice_new(respond_param);

	if (NULL == val) {
		/* respond; */
		rp->status_code = 200;
		rp->pattern = NULL;
	} else if (LI_VALUE_STRING == li_value_type(val)) {
		/* respond "foo"; */
		rp->status_code = 200;
		rp->pattern = li_pattern_new(srv, val->data.string->str);

		if (!rp->pattern) goto fail;
	} else if (LI_VALUE_NUMBER == li_value_type(val)) {
		/* respond 404; */
		rp->status_code = val->data.number;
		rp->pattern = NULL;
	} else if (li_value_list_has_len(val, 2)) {
		liValue *vCode = li_value_list_at(val, 0);
		liValue *vText = li_value_list_at(val, 1);
		if (LI_VALUE_NUMBER != li_value_type(vCode) || LI_VALUE_STRING != li_value_type(vText)) goto fail;

		/* respond 200 => "foo"; */
		rp->status_code = vCode->data.number;
		rp->pattern = li_pattern_new(srv, vText->data.string->str);

		if (!rp->pattern) goto fail;
	} else {
		goto fail;
	}

	return li_action_new_function(core_handle_respond, NULL, core_respond_free, rp);

fail:
	g_slice_free(respond_param, rp);
	ERROR(srv, "%s", "'respond' action takes an optional string as parameter");
	return NULL;
}

typedef struct {
	GString *key;
	liPattern *pattern;
} env_set_add_ctx;

static void core_env_set_add_free(liServer *srv, gpointer param) {
	env_set_add_ctx *ctx = param;

	UNUSED(srv);

	g_string_free(ctx->key, TRUE);
	li_pattern_free(ctx->pattern);
	g_slice_free(env_set_add_ctx, ctx);
}

static liHandlerResult core_handle_env_set(liVRequest *vr, gpointer param, gpointer *context) {
	env_set_add_ctx *ctx = param;
	GMatchInfo *match_info = NULL;

	UNUSED(context);

	if (vr->action_stack.regex_stack->len) {
		GArray *rs = vr->action_stack.regex_stack;
		match_info = g_array_index(rs, liActionRegexStackElement, rs->len - 1).match_info;
	}

	g_string_truncate(vr->wrk->tmp_str, 0);
	li_pattern_eval(vr, vr->wrk->tmp_str, ctx->pattern, NULL, NULL, li_pattern_regex_cb, match_info);
	li_environment_set(&vr->env, GSTR_LEN(ctx->key), GSTR_LEN(vr->wrk->tmp_str));

	return LI_HANDLER_GO_ON;
}

static liAction* core_env_set(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	liPattern *pattern;
	env_set_add_ctx *ctx;
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	/* first argument is a key value list - list of list of list of strings */
	val = li_value_get_single_argument(val);
	val = li_value_get_single_argument(val);

	if (!li_value_list_has_len(val, 2)
		|| LI_VALUE_STRING != li_value_list_type_at(val, 0)
		|| LI_VALUE_STRING != li_value_list_type_at(val, 1)) {
		ERROR(srv, "%s", "'env.set' action requires a pair of strings as parameter");
		return NULL;
	}

	if (NULL == (pattern = li_pattern_new(srv, li_value_list_at(val, 1)->data.string->str)))
		return NULL;

	ctx = g_slice_new0(env_set_add_ctx);
	ctx->key = li_value_extract_string(li_value_list_at(val, 0));
	ctx->pattern = pattern;

	return li_action_new_function(core_handle_env_set, NULL, core_env_set_add_free, ctx);
}


static liHandlerResult core_handle_env_add(liVRequest *vr, gpointer param, gpointer *context) {
	env_set_add_ctx *ctx = param;
	GMatchInfo *match_info = NULL;

	UNUSED(context);

	if (vr->action_stack.regex_stack->len) {
		GArray *rs = vr->action_stack.regex_stack;
		match_info = g_array_index(rs, liActionRegexStackElement, rs->len - 1).match_info;
	}

	g_string_truncate(vr->wrk->tmp_str, 0);
	li_pattern_eval(vr, vr->wrk->tmp_str, ctx->pattern, NULL, NULL, li_pattern_regex_cb, match_info);
	li_environment_insert(&vr->env, GSTR_LEN(ctx->key), GSTR_LEN(vr->wrk->tmp_str));

	return LI_HANDLER_GO_ON;
}

static liAction* core_env_add(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	liPattern *pattern;
	env_set_add_ctx *ctx;
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	/* first argument is a key value list - list of list of list of strings */
	val = li_value_get_single_argument(val);
	val = li_value_get_single_argument(val);

	if (!li_value_list_has_len(val, 2)
		|| LI_VALUE_STRING != li_value_list_type_at(val, 0)
		|| LI_VALUE_STRING != li_value_list_type_at(val, 1)) {
		ERROR(srv, "%s", "'env.add' action requires a pair of strings as parameter");
		return NULL;
	}

	if (NULL == (pattern = li_pattern_new(srv, li_value_list_at(val, 1)->data.string->str)))
		return NULL;

	ctx = g_slice_new0(env_set_add_ctx);
	ctx->key = li_value_extract_string(li_value_list_at(val, 0));
	ctx->pattern = pattern;

	return li_action_new_function(core_handle_env_add, NULL, core_env_set_add_free, ctx);
}


static void core_env_remove_free(liServer *srv, gpointer param) {
	UNUSED(srv);

	g_string_free(param, TRUE);
}

static liHandlerResult core_handle_env_remove(liVRequest *vr, gpointer param, gpointer *context) {
	GString *key = param;
	UNUSED(context);

	li_environment_remove(&vr->env, GSTR_LEN(key));

	return LI_HANDLER_GO_ON;
}

static liAction* core_env_remove(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);

	if (LI_VALUE_STRING != li_value_type(val)) {
		ERROR(srv, "%s", "'env.remove' action requires a string as parameter");
		return NULL;
	}

	return li_action_new_function(core_handle_env_remove, NULL, core_env_remove_free, li_value_extract_string(val));
}


static liHandlerResult core_handle_env_clear(liVRequest *vr, gpointer param, gpointer *context) {
	UNUSED(param); UNUSED(context);

	li_environment_reset(&vr->env);

	return LI_HANDLER_GO_ON;
}

static liAction* core_env_clear(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	if (!li_value_is_nothing(val)) {
		ERROR(srv, "%s", "'env.clear' action doesn't have parameters");
		return NULL;
	}

	return li_action_new_function(core_handle_env_clear, NULL, NULL, NULL);
}


static gboolean core_listen(liServer *srv, liPlugin* p, liValue *val, gpointer userdata) {
	UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);

	if (NULL == val) goto fail;

	if (LI_VALUE_STRING == li_value_type(val)) {
		li_angel_listen(srv, val->data.string, NULL, NULL);
	} else if (LI_VALUE_LIST == li_value_type(val)) {
		LI_VALUE_FOREACH(ip, val);
			if (LI_VALUE_STRING != li_value_type(ip)) goto fail;
			li_angel_listen(srv, ip->data.string, NULL, NULL);
		LI_VALUE_END_FOREACH()
	} else {
		goto fail;
	}

	return TRUE;

fail:
	ERROR(srv, "%s", "listen expects a string or list of strings as parameter");
	return FALSE;
}


static gboolean core_workers(liServer *srv, liPlugin* p, liValue *val, gpointer userdata) {
	gint workers;
	UNUSED(p); UNUSED(userdata);
	val = li_value_get_single_argument(val);

	if (LI_VALUE_NUMBER != li_value_type(val) || (workers = val->data.number) < 1) {
		ERROR(srv, "%s", "workers expects a positive integer as parameter");
		return FALSE;
	}

	if (srv->worker_count != 0) {
		ERROR(srv, "workers already called with '%i'", srv->worker_count);
		return FALSE;
	}
	srv->worker_count = workers;
	return TRUE;
}

static gboolean core_workers_cpu_affinity(liServer *srv, liPlugin* p, liValue *val, gpointer userdata) {
#if defined(LIGHTY_OS_LINUX) || defined(LIGHTY_OS_FREEBSD)
	UNUSED(p); UNUSED(userdata);

	if (LI_VALUE_LIST != li_value_type(val)) {
		ERROR(srv, "%s", "workers.cpu_affinity expects a list of integers or list of list of integers");
		return FALSE;
	}

	LI_VALUE_FOREACH(v, val)
		if (LI_VALUE_LIST == li_value_type(v)) {
			LI_VALUE_FOREACH(v2, v)
				if (LI_VALUE_NUMBER != li_value_type(v2)) {
					ERROR(srv, "%s", "workers.cpu_affinity expects a list of integers or list of list of integers");
					return FALSE;
				}
			LI_VALUE_END_FOREACH()
		} else if (LI_VALUE_NUMBER != li_value_type(v)) {
			ERROR(srv, "%s", "workers.cpu_affinity expects a list of integers or list of list of integers");
			return FALSE;
		}
	LI_VALUE_END_FOREACH()

	srv->workers_cpu_affinity = li_value_copy(val);

	return TRUE;
#else
	UNUSED(p); UNUSED(val); UNUSED(userdata);
	ERROR(srv, "%s", "workers.cpu_affinity is only available on Linux and FreeBSD systems");
	return FALSE;
#endif
}

static gboolean core_module_load(liServer *srv, liPlugin* p, liValue *val, gpointer userdata) {
	GError *err = NULL;
	UNUSED(p); UNUSED(userdata);

	if (!g_module_supported()) {
		ERROR(srv, "%s", "module loading not supported on this platform");
		return FALSE;
	}

	val = li_value_get_single_argument(val);

	if (LI_VALUE_STRING == li_value_type(val)) {
		/* load only one module */
		li_value_wrap_in_list(val);
	} else if (LI_VALUE_LIST == li_value_type(val)) {
		/* load a list of modules */
		LI_VALUE_FOREACH(module, val)
			if (LI_VALUE_STRING != li_value_type(module)) {
				ERROR(srv, "module_load takes either a string or a list of strings as parameter, list with %s entry given",
					li_value_type_string(module));
				return FALSE;
			}
		LI_VALUE_END_FOREACH()
	} else {
		ERROR(srv, "module_load takes either a string or a list of strings as parameter, %s given", li_value_type_string(val));
		return FALSE;
	}

	/* parameter types ok, load modules */
	LI_VALUE_FOREACH(module, val)
		GString *name = module->data.string;
		if (li_module_lookup(srv->modules, name->str)) {
			DEBUG(srv, "module_load: module '%s' already loaded", name->str);
			continue;
		}

		if (!li_module_load(srv->modules, name->str, &err)) {
			ERROR(srv, "could not load module '%s': %s", name->str, err->message);
			g_error_free(err);
			return FALSE;
		}

		DEBUG(srv, "loaded module '%s'", name->str);
	LI_VALUE_END_FOREACH()

	return TRUE;
}

static gboolean core_io_timeout(liServer *srv, liPlugin* p, liValue *val, gpointer userdata) {
	UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);

	if (LI_VALUE_NUMBER != li_value_type(val) || val->data.number < 1) {
		ERROR(srv, "%s", "io.timeout expects a positive number as parameter");
		return FALSE;
	}

	srv->io_timeout = val->data.number;

	return TRUE;
}

static gboolean core_stat_cache_ttl(liServer *srv, liPlugin* p, liValue *val, gpointer userdata) {
	UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);

	if (LI_VALUE_NUMBER != li_value_type(val) || val->data.number < 0) {
		ERROR(srv, "%s", "stat_cache.ttl expects a positive number as parameter");
		return FALSE;
	}

	srv->stat_cache_ttl = (gdouble)val->data.number;

	return TRUE;
}

static gboolean core_tasklet_pool_threads(liServer *srv, liPlugin* p, liValue *val, gpointer userdata) {
	UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);

	if (LI_VALUE_NUMBER != li_value_type(val)) {
		ERROR(srv, "%s", "tasklet_pool.threads expects a number as parameter");
		return FALSE;
	}

	srv->tasklet_pool_threads = val->data.number;
	li_tasklet_pool_set_threads(srv->main_worker->tasklets, srv->tasklet_pool_threads);

	return TRUE;
}

/*
 * OPTIONS
 */

static liLogMap* logmap_from_value(liServer *srv, liValue *val) {
	liLogMap *log_map;
	GString *default_path = NULL;

	if (NULL == val) {
		return li_log_map_new_default();
	}

	if (NULL == (val = li_value_to_key_value_list(val))) {
		ERROR(srv, "%s", "log expects a hashtable/key-value list");
		return NULL;
	}

	log_map = li_log_map_new();

	LI_VALUE_FOREACH(entry, val)
		liValue *entryKey = li_value_list_at(entry, 0);
		liValue *entryValue = li_value_list_at(entry, 1);
		GString *entryKeyStr;

		if (LI_VALUE_STRING != li_value_type(entryValue)) {
			ERROR(srv, "log expects a hashtable/key-value list with string values as parameter, %s value given", li_value_type_string(entryValue));
			li_log_map_release(log_map);
			return NULL;
		}

		/* NONE or STRING */
		entryKeyStr = (LI_VALUE_STRING == li_value_type(entryKey)) ? entryKey->data.string : NULL;

		if (NULL != entryKeyStr && g_str_equal(entryKeyStr->str, "*")) {
			WARNING(srv, "%s", "log: found entry with string key \"*\". please convert the parameter to a key-value list and use the keyword default instead.");
			/* TODO: remove support for "default" */
			entryKeyStr = NULL;
		}

		if (NULL == entryKeyStr) {
			if (NULL != default_path) {
				ERROR(srv, "%s", "already have a default path");
				li_log_map_release(log_map);
				return NULL;
			}
			default_path = entryValue->data.string;
		} else {
			int level = li_log_level_from_string(entryKeyStr);
			if (-1 == level) {
				ERROR(srv, "unknown log level '%s'", entryKeyStr->str);
				li_log_map_release(log_map);
				return NULL;
			}
			if (NULL != log_map->targets[level]) {
				ERROR(srv, "already have a path for log level '%s'", entryKeyStr->str);
				li_log_map_release(log_map);
				return NULL;
			}
			log_map->targets[level] = li_value_extract_string(entryValue);
		}
	LI_VALUE_END_FOREACH()

	if (NULL != default_path) {
		guint i;
		for (i = 0; i < LI_LOG_LEVEL_COUNT; i++) {
			/* do NOT overwrite other paths */
			if (NULL != log_map->targets[i]) continue;

			log_map->targets[i] = g_string_new_len(GSTR_LEN(default_path));
		}
	}

	return log_map;
}

static void core_log_free(liServer *srv, gpointer param) {
	liLogMap *log_map = param;

	UNUSED(srv);

	li_log_map_release(log_map);
}

static liHandlerResult core_handle_log(liVRequest *vr, gpointer param, gpointer *context) {
	liLogMap *log_map = param;

	UNUSED(context);

	li_log_context_set(&vr->log_context, log_map);

	return LI_HANDLER_GO_ON;
}

static liAction* core_log(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	liLogMap *log_map;
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);

	if (NULL == val) {
		return li_action_new_function(core_handle_log, NULL, core_log_free, NULL);
	}

	log_map = logmap_from_value(srv, val);
	if (NULL == log_map) return NULL;

	return li_action_new_function(core_handle_log, NULL, core_log_free, log_map);
}

static gboolean core_setup_log(liServer *srv, liPlugin* p, liValue *val, gpointer userdata) {
	liLogMap *log_map;
	UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);

	log_map = logmap_from_value(srv, val);
	if (NULL == log_map) return FALSE;

	li_log_context_set(&srv->logs.log_context, log_map);
	li_log_map_release(log_map);

	return TRUE;
}

static gboolean core_setup_log_timestamp(liServer *srv, liPlugin* p, liValue *val, gpointer userdata) {
	UNUSED(p);
	UNUSED(userdata);

	val = li_value_get_single_argument(val);

	if (LI_VALUE_STRING != li_value_type(val)) {
		ERROR(srv, "%s", "log.timestamp expects a string as parameter");
		return FALSE;
	}

	if (NULL != srv->logs.timestamp.format) {
		g_string_free(srv->logs.timestamp.format, TRUE);
	}
	srv->logs.timestamp.format = li_value_extract_string(val);
	srv->logs.timestamp.last_ts = 0;

	return TRUE;
}

static gboolean core_option_static_exclude_exts_parse(liServer *srv, liWorker *wrk, liPlugin *p, size_t ndx, liValue *val, gpointer *oval) {
	UNUSED(srv); UNUSED(wrk); UNUSED(p); UNUSED(ndx);

	if (NULL == val) return TRUE;
	LI_FORCE_ASSERT(LI_VALUE_LIST == val->type);

	LI_VALUE_FOREACH(v, val)
		if (LI_VALUE_STRING != li_value_type(v)) {
			ERROR(srv, "static.exclude_extensions option expects a list of strings, entry #%u is of type %s", _v_i, li_value_type_string(v));
			return FALSE;
		}
	LI_VALUE_END_FOREACH()

	/* everything ok */
	*oval = li_value_extract_list(val);

	return TRUE;
}


static gboolean core_option_mime_types_parse(liServer *srv, liWorker *wrk, liPlugin *p, size_t ndx, liValue *val, gpointer *oval) {
	liMimetypeNode *node;

	UNUSED(srv); UNUSED(wrk); UNUSED(p); UNUSED(ndx);

	*oval = node = li_mimetype_node_new();
	node->mimetype = g_string_new_len(CONST_STR_LEN("application/octet-stream"));

	/* default value */
	if (NULL == val) return TRUE;

	/* check if the passed val is of type (("a", "b"), ("x", y")) */
	LI_VALUE_FOREACH(v, val)
		liValue *v1, *v2;

		if (!li_value_list_has_len(v, 2)) {
			ERROR(srv, "mime_types option expects a list of string tuples, entry #%u is not a tuple", _v_i);
			li_mimetype_node_free(node);
			return FALSE;
		}

		v1 = li_value_list_at(v, 0);
		v2 = li_value_list_at(v, 1);
		if (LI_VALUE_STRING != li_value_type(v1) || LI_VALUE_STRING != li_value_type(v2)) {
			ERROR(srv, "mime_types option expects a list of string tuples, entry #%u is a (%s,%s) tuple", _v_i, li_value_type_string(v1), li_value_type_string(v2));
			li_mimetype_node_free(node);
			return FALSE;
		}

		li_mimetype_insert(node, v1->data.string, li_value_extract_string(v2));
	LI_VALUE_END_FOREACH()

	return TRUE;
}

static void core_option_mime_types_free(liServer *srv, liPlugin *p, size_t ndx, gpointer oval) {
	UNUSED(srv);
	UNUSED(p);
	UNUSED(ndx);

	li_mimetype_node_free(oval);
}

static gboolean core_option_etag_use_parse(liServer *srv, liWorker *wrk, liPlugin *p, size_t ndx, liValue *val, liOptionValue *oval) {
	guint flags = 0;
	UNUSED(p); UNUSED(ndx); UNUSED(wrk);

	/* default value */
	if (NULL == val) {
		oval->number = LI_ETAG_USE_INODE | LI_ETAG_USE_MTIME | LI_ETAG_USE_SIZE;
		return TRUE;
	}

	/* Need manual type check, as resulting option type is number */
	if (LI_VALUE_LIST != li_value_type(val)) {
		ERROR(srv, "etag.use option expects a list of strings, parameter is of type %s", li_value_type_string(val));
		return FALSE;
	}
	LI_VALUE_FOREACH(v, val)
		if (LI_VALUE_STRING != li_value_type(v)) {
			ERROR(srv, "etag.use option expects a list of strings, entry #%u is of type %s", _v_i, li_value_type_string(v));
			return FALSE;
		}

		if (0 == strcmp(v->data.string->str, "inode")) {
			flags |= LI_ETAG_USE_INODE;
		} else if (0 == strcmp(v->data.string->str, "mtime")) {
			flags |= LI_ETAG_USE_MTIME;
		} else if (0 == strcmp(v->data.string->str, "size")) {
			flags |= LI_ETAG_USE_SIZE;
		} else {
			ERROR(srv, "unknown etag.use flag: %s", v->data.string->str);
			return FALSE;
		}
	LI_VALUE_END_FOREACH()

	oval->number = (guint64) flags;
	return TRUE;
}

typedef void (*header_cb)(liHttpHeaders *headers, const gchar *key, size_t keylen, const gchar *val, size_t valuelen);

typedef struct header_ctx header_ctx;
struct header_ctx {
	GString *key;
	liPattern *value;
	header_cb cb;
	gboolean use_req_header;
};

static void core_header_free(liServer *srv, gpointer param) {
	header_ctx *ctx = param;

	UNUSED(srv);

	g_string_free(ctx->key, TRUE);
	li_pattern_free(ctx->value);
	g_slice_free(header_ctx, ctx);
}

static liHandlerResult core_handle_header(liVRequest *vr, gpointer param, gpointer *context) {
	header_ctx *ctx = param;
	GMatchInfo *match_info = NULL;

	UNUSED(context);

	if (vr->action_stack.regex_stack->len) {
		GArray *rs = vr->action_stack.regex_stack;
		match_info = g_array_index(rs, liActionRegexStackElement, rs->len - 1).match_info;
	}

	g_string_truncate(vr->wrk->tmp_str, 0);
	li_pattern_eval(vr, vr->wrk->tmp_str, ctx->value, NULL, NULL, li_pattern_regex_cb, match_info);

	ctx->cb(ctx->use_req_header ? vr->request.headers : vr->response.headers, GSTR_LEN(ctx->key), GSTR_LEN(vr->wrk->tmp_str));

	return LI_HANDLER_GO_ON;
}

static liAction* core_generic_header_add(liServer *srv, liValue *val, header_cb header_callback, gboolean use_req_header) {
	liPattern *pat;
	header_ctx *ctx;

	val = li_value_get_single_argument(val);
	val = li_value_get_single_argument(val);

	if (!li_value_list_has_len(val, 2)) {
		ERROR(srv, "'[req_]header.add/append/overwrite' action expects a string tuple as parameter, %s given", li_value_type_string(val));
		return NULL;
	}

	if (LI_VALUE_STRING != li_value_list_type_at(val, 0) || LI_VALUE_STRING != li_value_list_type_at(val, 1)) {
		ERROR(srv, "%s", "'[req_]header.add/append/overwrite' action expects a string tuple as parameter");
		return NULL;
	}

	if (NULL == (pat = li_pattern_new(srv, li_value_list_at(val, 1)->data.string->str))) {
		ERROR(srv, "%s", "'[req_]header.add/append/overwrite': parsing value pattern failed");
		return NULL;
	}

	ctx = g_slice_new(header_ctx);
	ctx->key = li_value_extract_string(li_value_list_at(val, 0));
	ctx->value = pat;
	ctx->cb = header_callback;
	ctx->use_req_header = use_req_header;

	return li_action_new_function(core_handle_header, NULL, core_header_free, ctx);
}

static liAction* core_header_add(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	UNUSED(wrk); UNUSED(p);

	return core_generic_header_add(srv, val, (header_cb)(intptr_t)userdata, FALSE);
}

static liAction* core_req_header_add(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	UNUSED(wrk); UNUSED(p);

	return core_generic_header_add(srv, val, (header_cb)(intptr_t)userdata, TRUE);
}

static void core_header_remove_free(liServer *srv, gpointer param) {
	UNUSED(srv);

	g_string_free(param, TRUE);
}

static liHandlerResult core_handle_header_remove(liVRequest *vr, gpointer param, gpointer *context) {
	GString *str = param;
	UNUSED(context);

	li_http_header_remove(vr->response.headers, GSTR_LEN(str));

	return LI_HANDLER_GO_ON;
}

static liAction* core_header_remove(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);

	if (LI_VALUE_STRING != li_value_type(val)) {
		ERROR(srv, "'header.remove' action expects a string as parameter, %s given", li_value_type_string(val));
		return NULL;
	}

	return li_action_new_function(core_handle_header_remove, NULL, core_header_remove_free, li_value_extract_string(val));
}

static liHandlerResult core_handle_req_header_remove(liVRequest *vr, gpointer param, gpointer *context) {
	GString *str = param;
	UNUSED(context);

	li_http_header_remove(vr->request.headers, GSTR_LEN(str));

	return LI_HANDLER_GO_ON;
}

static liAction* core_req_header_remove(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);

	if (LI_VALUE_STRING != li_value_type(val)) {
		ERROR(srv, "'req_header.remove' action expects a string as parameter, %s given", li_value_type_string(val));
		return NULL;
	}

	return li_action_new_function(core_handle_req_header_remove, NULL, core_header_remove_free, li_value_extract_string(val));
}

/* chunkqueue memory limits */
static liHandlerResult core_handle_buffer_out(liVRequest *vr, gpointer param, gpointer *context) {
	gint limit = GPOINTER_TO_INT(param);
	UNUSED(context);

	li_chunkqueue_use_limit(vr->coninfo->resp->out, limit);

	return LI_HANDLER_GO_ON;
}

static liAction* core_buffer_out(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	gint64 limit;
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);

	if (LI_VALUE_NUMBER != li_value_type(val)) {
		ERROR(srv, "'io.buffer_out' action expects an integer as parameter, %s given", li_value_type_string(val));
		return NULL;
	}

	limit = val->data.number;

	if (limit < 0) {
		limit = 0; /* no limit */
	} else if (limit < (16*1024)) {
		ERROR(srv, "buffer %"G_GINT64_FORMAT" is too low (need at least 16 kb)", limit);
		return NULL;
	} else if (limit > (1 << 30)) {
		ERROR(srv, "buffer %"G_GINT64_FORMAT" is too high (1GB is the maximum)", limit);
		return NULL;
	}

	return li_action_new_function(core_handle_buffer_out, NULL, NULL, GINT_TO_POINTER((gint) limit));
}

static liHandlerResult core_handle_buffer_in(liVRequest *vr, gpointer param, gpointer *context) {
	gint limit = GPOINTER_TO_INT(param);
	UNUSED(context);

	li_chunkqueue_use_limit(vr->coninfo->req->out, limit);

	return LI_HANDLER_GO_ON;
}

static liAction* core_buffer_in(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	gint64 limit;
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);

	if (LI_VALUE_NUMBER != li_value_type(val)) {
		ERROR(srv, "'io.buffer_in' action expects an integer as parameter, %s given", li_value_type_string(val));
		return NULL;
	}

	limit = val->data.number;

	if (limit < 0) {
		limit = 0; /* no limit */
	}
	if (limit > (1 << 30)) {
		ERROR(srv, "buffer %"G_GINT64_FORMAT" is too high (1GB is the maximum)", limit);
		return NULL;
	}

	return li_action_new_function(core_handle_buffer_in, NULL, NULL, GINT_TO_POINTER((gint) limit));
}

typedef struct core_map_data core_map_data;
struct core_map_data {
	liPattern *pattern;
	GHashTable *hash;
	liAction *default_action;
};
static void core_map_free(liServer *srv, gpointer param) {
	core_map_data *md = param;

	UNUSED(srv);

	if (md->default_action)
		li_action_release(srv, md->default_action);

	li_pattern_free(md->pattern);
	g_hash_table_destroy(md->hash);
	g_slice_free(core_map_data, md);
}

static liHandlerResult core_handle_map(liVRequest *vr, gpointer param, gpointer *context) {
	liValue *v;
	core_map_data *md = param;
	GMatchInfo *match_info = NULL;
	UNUSED(context);

	g_string_truncate(vr->wrk->tmp_str, 0);

	if (vr->action_stack.regex_stack->len) {
		GArray *rs = vr->action_stack.regex_stack;
		match_info = g_array_index(rs, liActionRegexStackElement, rs->len - 1).match_info;
	}

	li_pattern_eval(vr, vr->wrk->tmp_str, md->pattern, NULL, NULL, li_pattern_regex_cb, match_info);
	v = g_hash_table_lookup(md->hash, vr->wrk->tmp_str);
	if (NULL != v) {
		li_action_enter(vr, v->data.val_action.action);
	} else if (NULL != md->default_action) {
		li_action_enter(vr, md->default_action);
	}

	return LI_HANDLER_GO_ON;
}

static liAction* core_map(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	core_map_data *md;
	liValue *list, *l, *r;
	liPattern *pattern;
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);
	val = li_value_get_single_argument(val);

	if (!li_value_list_has_len(val, 2)) {
		ERROR(srv, "%s", "'map' action expects a string => (list of key => action pairs) as parameter");
		return NULL;
	}

	l = li_value_list_at(val, 0);
	r = li_value_list_at(val, 1);
	if (LI_VALUE_STRING != li_value_type(l) || LI_VALUE_LIST != li_value_type(r)) {
		ERROR(srv, "%s", "'map' action expects a string => (list of key => action pairs) as parameter");
		return NULL;
	}

	pattern = li_pattern_new(srv, l->data.string->str);
	if (NULL == pattern) {
		ERROR(srv, "'map' action: failed to compile pattern '%s'", l->data.string->str);
		return NULL;
	}

	md = g_slice_new(core_map_data);
	md->pattern = pattern;
	md->default_action = NULL;
	md->hash = li_value_new_hashtable();

	list = r;

	LI_VALUE_FOREACH(v, list)
		if (!li_value_list_has_len(v, 2)) {
			ERROR(srv, "%s", "'map' action expects a string => (list of key => action pairs) as parameter");
			core_map_free(srv, md);
			return NULL;
		}

		l = li_value_list_at(v, 0);
		r = li_value_list_at(v, 1);

		if (LI_VALUE_ACTION != li_value_type(r)) {
			ERROR(srv, "%s", "'map' action expects a string => (list of key => action pairs) as parameter");
			core_map_free(srv, md);
			return NULL;
		}

		/* TODO: error on duplicate keys? right now last key wins. */
		if (LI_VALUE_NONE == li_value_type(l)) {
			if (NULL != md->default_action) {
				li_action_release(srv, md->default_action);
			}
			/* default action */
			md->default_action = li_value_extract_action(r);
		} else if (LI_VALUE_STRING == li_value_type(l)) {
			/* string => action */
			g_hash_table_insert(md->hash, li_value_extract_string(l), li_value_extract(r));
		} else if (LI_VALUE_LIST == li_value_type(l)) {
			/* (string, string, ...) => action */
			LI_VALUE_FOREACH(v2, l)
				if (LI_VALUE_STRING != li_value_type(v2)) {
					ERROR(srv, "%s", "'map' action expects a string => (list of key => action pairs) as parameter");
					core_map_free(srv, md);
					return NULL;
				}

				g_hash_table_insert(md->hash, li_value_extract_string(v2), li_value_extract(r));
			LI_VALUE_END_FOREACH()
		}
	LI_VALUE_END_FOREACH()

	return li_action_new_function(core_handle_map, NULL, core_map_free, md);
}

typedef struct core_map_cidr_data core_map_cidr_data;
struct core_map_cidr_data {
	GPtrArray *actions;
	liRadixTree *ipv4; /* map to index in actions */
	liRadixTree *ipv6; /* map to index in actions */
	gint default_action;
};
static void core_map_cidr_free(liServer *srv, gpointer param) {
	core_map_cidr_data *md = param;

	UNUSED(srv);

	for (gsize i = 0; i < md->actions->len; i++) {
		li_action_release(srv, g_ptr_array_index(md->actions, i));
	}

	g_ptr_array_free(md->actions, TRUE);
	li_radixtree_free(md->ipv4, NULL, NULL);
	li_radixtree_free(md->ipv6, NULL, NULL);

	g_slice_free(core_map_cidr_data, md);
}

static liHandlerResult core_handle_map_cidr(liVRequest *vr, gpointer param, gpointer *context) {
	core_map_cidr_data *md = param;
	gpointer action_ndx_ptr = NULL;
	gint action_ndx = md->default_action;
	liSockAddrPtr addr_up = vr->coninfo->remote_addr.addr_up;
	UNUSED(context);

	if (addr_up.plain->sa_family == AF_INET) {
		action_ndx_ptr = li_radixtree_lookup(md->ipv4, &addr_up.ipv4->sin_addr.s_addr, 32);
#ifdef HAVE_IPV6
	} else if (addr_up.plain->sa_family == AF_INET6) {
		action_ndx_ptr = li_radixtree_lookup(md->ipv6, &addr_up.ipv6->sin6_addr.s6_addr, 128);
#endif
	}

	if (NULL != action_ndx_ptr) {
		action_ndx = GPOINTER_TO_INT(action_ndx_ptr);
	}

	if (action_ndx >= 0) {
		li_action_enter(vr, g_ptr_array_index(md->actions, action_ndx));
	}

	return LI_HANDLER_GO_ON;
}

static liAction* core_map_cidr(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	core_map_cidr_data *md;
	liValue *cidr_list,  *action;
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	md = g_slice_new(core_map_cidr_data);
	md->actions = g_ptr_array_new();
	md->ipv4 = li_radixtree_new();
	md->ipv6 = li_radixtree_new();
	md->default_action = -1;

	/* can't store anything at index 0 */
	g_ptr_array_add(md->actions, NULL);

	val = li_value_get_single_argument(val);

	LI_VALUE_FOREACH(map_entry, val)
		gint action_ndx;

		if (!li_value_list_has_len(map_entry, 2)) {
			ERROR(srv, "%s", "'map_cidr' action expects list of (key => action) pairs as parameter");
			core_map_cidr_free(srv, md);
			return NULL;
		}

		cidr_list = li_value_list_at(map_entry, 0);
		action = li_value_list_at(map_entry, 1);

		if (LI_VALUE_ACTION != li_value_type(action)) {
			ERROR(
				srv,
				"'map_cidr' action expects list of (key => action) pairs as parameter, expected action, got %s",
				li_value_type_string(action)
			);
			core_map_cidr_free(srv, md);
			return NULL;
		}

		action_ndx = md->actions->len;
		g_ptr_array_add(md->actions, li_value_extract_action(action));

		if (LI_VALUE_NONE == li_value_type(cidr_list)) {
			md->default_action = action_ndx;
			continue;
		}

		if (LI_VALUE_LIST != li_value_type(cidr_list)) li_value_wrap_in_list(cidr_list);

		LI_VALUE_FOREACH(cidr_entry, cidr_list)
			guint32 ipv4, netmaskv4;
			guint8 ipv6_addr[16];
			guint ipv6_network;

			if (LI_VALUE_NONE == li_value_type(cidr_entry)) {
				md->default_action = action_ndx;
				continue;
			}

			if (LI_VALUE_STRING != li_value_type(cidr_entry)) {
				ERROR(srv, "%s", "map_cidr: expect strings as keys in 'map_cidr'");
				core_map_cidr_free(srv, md);
				return NULL;
			}

			if (g_str_equal(cidr_entry->data.string->str, "all")) {
				md->default_action = action_ndx;
			} else if (li_parse_ipv4(cidr_entry->data.string->str, &ipv4, &netmaskv4, NULL)) {
				gint prefixlen;
				netmaskv4 = ntohl(netmaskv4);
				prefixlen = 32 - g_bit_nth_lsf(netmaskv4, -1);
				if (prefixlen < 0 || prefixlen > 32) prefixlen = 0;
				li_radixtree_insert(md->ipv4, &ipv4, prefixlen, GINT_TO_POINTER(action_ndx));
			} else if (li_parse_ipv6(cidr_entry->data.string->str, ipv6_addr, &ipv6_network, NULL)) {
				li_radixtree_insert(md->ipv6, ipv6_addr, ipv6_network, GINT_TO_POINTER(action_ndx));
			} else {
				ERROR(srv, "map_cidr: error parsing IP cidr: %s", cidr_entry->data.string->str);
				core_map_cidr_free(srv, md);
				return NULL;
			}
		LI_VALUE_END_FOREACH()
	LI_VALUE_END_FOREACH()

	return li_action_new_function(core_handle_map_cidr, NULL, core_map_cidr_free, md);
}

static liHandlerResult core_handle_proxy_prot_trust(liVRequest *vr, gpointer param, gpointer *context) {
	liConInfo *coninfo = vr->coninfo;
	UNUSED(param);
	UNUSED(context);

	if (coninfo->proxy_prot_used) return LI_HANDLER_GO_ON;

	coninfo->proxy_prot_used = TRUE; /* don't try again */

	if (0 == coninfo->proxy_prot_data.version) {
		if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
			VR_DEBUG(vr, "%s", "No PROXY header present");
		}
		return LI_HANDLER_GO_ON;
	}

	if (0 == coninfo->proxy_prot_data.remote.len || 0 == coninfo->proxy_prot_data.local.len) {
		if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
			VR_DEBUG(vr, "%s", "No addresses in PROXY header");
		}
		return LI_HANDLER_GO_ON;
	}

	li_sockaddr_clear(&coninfo->remote_addr);
	coninfo->remote_addr = li_sockaddr_dup(coninfo->proxy_prot_data.remote);
	li_sockaddr_to_string(coninfo->remote_addr, coninfo->remote_addr_str, FALSE);

	li_sockaddr_clear(&coninfo->local_addr);
	coninfo->local_addr = li_sockaddr_dup(coninfo->proxy_prot_data.local);
	li_sockaddr_to_string(coninfo->local_addr, coninfo->local_addr_str, FALSE);

	return LI_HANDLER_GO_ON;
}

static liAction* core_proxy_prot_trust(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);
	if (NULL != val) {
		ERROR(srv, "%s", "'proxy_protocol.trust' action doesn't take any parameters");
		return NULL;
	}

	return li_action_new_function(core_handle_proxy_prot_trust, NULL, NULL, NULL);
}

static void fetch_files_static_lookup(liFetchDatabase* db, gpointer data, liFetchEntry *entry) {
	GHashTable *stringdb = (GHashTable*) data;
	UNUSED(db);
	entry->data = g_hash_table_lookup(stringdb, entry->key);
	li_fetch_entry_ready(entry);
}
static gboolean fetch_files_static_revalidate(liFetchDatabase* db, gpointer data, liFetchEntry *entry) {
	UNUSED(db); UNUSED(data); UNUSED(entry);
	return TRUE;
}
static void fetch_files_static_refresh(liFetchDatabase* db, gpointer data, liFetchEntry *cur_entry, liFetchEntry *new_entry) {
	UNUSED(db); UNUSED(data); UNUSED(cur_entry);
	li_fetch_entry_refresh_skip(new_entry);
}
static void fetch_files_static_free_db(gpointer data) {
	GHashTable *stringdb = (GHashTable*) data;
	g_hash_table_destroy(stringdb);
}

static const liFetchCallbacks fetch_files_static_callbacks = {
	fetch_files_static_lookup,
	fetch_files_static_revalidate,
	fetch_files_static_refresh,
	/* fetch_files_static_free_entry */ NULL,
	fetch_files_static_free_db
};

static gboolean core_register_fetch_files_static(liServer *srv, liPlugin* p, liValue *val, gpointer userdata) {
	const gchar *name;
	gchar *wildcard, *tmp;
	const gchar *_entry;
	GString *pattern;
	GString *basedir = NULL, *subfile = NULL;
	GString *prefix = NULL, *suffix = NULL;
	GString *filename = NULL;
	GDir *dir = NULL;
	GError *err = NULL;
	gboolean result = FALSE;
	GHashTable *stringdb = NULL;
	liFetchDatabase *db = NULL;
	UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);
	val = li_value_get_single_argument(val);

	if (!li_value_list_has_len(val, 2)
		|| LI_VALUE_STRING != li_value_list_type_at(val, 0)
		|| LI_VALUE_STRING != li_value_list_type_at(val, 1)) {
		ERROR(srv, "%s", "fetch.files_static expects two strings as parameter: \"<name>\" => \"/path/abc_*.d/file\"");
		goto out;
	}

	name = li_value_list_at(val, 0)->data.string->str;
	pattern = li_value_list_at(val, 1)->data.string;

	wildcard = strchr(pattern->str, '*');
	if (NULL == wildcard || NULL != strchr(wildcard + 1, '*')) {
		ERROR(srv, "%s", "fetch.files_static file pattern doesn't contain exactly one wildcard");
		goto out;
	}

	for (tmp = wildcard; tmp >= pattern->str; --tmp) {
		if ('/' == *tmp) break;
	}
	if (tmp >= pattern->str) { /* found '/' before '*' */
		basedir = g_string_new_len(pattern->str, tmp - pattern->str);
		prefix = g_string_new_len(tmp + 1, wildcard - tmp - 1);
	} else {
		prefix = g_string_new_len(pattern->str, wildcard - pattern->str);
	}
	if (NULL != (tmp = strchr(wildcard, '/'))) {
		subfile = g_string_new(tmp);
		suffix = g_string_new_len(wildcard + 1, tmp - wildcard - 1);
	} else {
		suffix = g_string_new(wildcard + 1);
	}

	filename = g_string_sized_new(127);
	dir = g_dir_open(NULL != basedir ? basedir->str : ".", 0, &err);
	if (NULL == dir) {
		ERROR(srv, "fetch.files_static: couldn't open basedir '%s': %s", NULL != basedir ? basedir->str : ".", err->message);
		goto out;
	}

	stringdb = g_hash_table_new_full((GHashFunc) g_string_hash, (GEqualFunc) g_string_equal, li_g_string_free, li_g_string_free);
	while (NULL != (_entry = g_dir_read_name(dir))) {
		GString entry = li_const_gstring(_entry, strlen(_entry));
		if (entry.len <= prefix->len + suffix->len) continue;
		if (li_string_prefix(&entry, GSTR_LEN(prefix)) && li_string_suffix(&entry, GSTR_LEN(suffix))) {
			GString *key;
			GString *file;
			gchar *file_contents;
			gsize file_len;

			g_string_truncate(filename, 0);
			if (NULL != basedir) {
				li_g_string_append_len(filename, GSTR_LEN(basedir));
				li_path_append_slash(filename);
			}
			li_g_string_append_len(filename, GSTR_LEN(&entry));
			if (NULL != subfile) {
				li_path_append_slash(filename);
				li_g_string_append_len(filename, GSTR_LEN(subfile));
			}

			if (!g_file_test(filename->str, G_FILE_TEST_IS_REGULAR)) continue;

			if (!g_file_get_contents(filename->str, &file_contents, &file_len, &err)) {
				ERROR(srv, "fetch.files_static: couldn't read file '%s': %s", filename->str, err->message);
				goto out;
			}

			key = g_string_new_len(entry.str + prefix->len, entry.len - prefix->len - suffix->len);
			file = g_string_new_len(file_contents, file_len);
			g_free(file_contents);

			g_hash_table_insert(stringdb, key, file);
		}
	}

	db = li_fetch_database_new(&fetch_files_static_callbacks, stringdb, g_hash_table_size(stringdb), 0);
	stringdb = NULL; /* now owned by db */

	if (!li_server_register_fetch_database(srv, name, db)) {
		ERROR(srv, "fetch.files_static: duplicate name: can't register another backend for name '%s'", name);
		goto out;
	}

	result = TRUE;

out:
	if (NULL != basedir) g_string_free(basedir, TRUE);
	if (NULL != subfile) g_string_free(subfile, TRUE);
	if (NULL != prefix) g_string_free(prefix, TRUE);
	if (NULL != suffix) g_string_free(suffix, TRUE);
	if (NULL != filename) g_string_free(filename, TRUE);
	if (NULL != dir) g_dir_close(dir);
	if (NULL != err) g_error_free(err);
	if (NULL != stringdb) g_hash_table_destroy(stringdb);
	if (NULL != db) li_fetch_database_release(db);
	return result;
}





static void core_warmup(liServer *srv, liPlugin *p, gint32 id, GString *data) {
	UNUSED(p);
	UNUSED(id);
	UNUSED(data);

	li_server_goto_state(srv, LI_SERVER_WARMUP);
}

static void core_run(liServer *srv, liPlugin *p, gint32 id, GString *data) {
	UNUSED(p);
	UNUSED(id);
	UNUSED(data);

	li_server_goto_state(srv, LI_SERVER_RUNNING);
}

static void core_suspend(liServer *srv, liPlugin *p, gint32 id, GString *data) {
	UNUSED(p);
	UNUSED(id);
	UNUSED(data);

	li_server_goto_state(srv, LI_SERVER_SUSPENDED);
}

static const liPluginOption options[] = {
	{ "debug.log_request_handling", LI_VALUE_BOOLEAN, FALSE, NULL },

	{ "static.range_requests", LI_VALUE_BOOLEAN, TRUE, NULL },

	{ "keepalive.timeout", LI_VALUE_NUMBER, 5, NULL },
	{ "keepalive.requests", LI_VALUE_NUMBER, 0, NULL },

	{ "etag.use", LI_VALUE_NONE, 0, core_option_etag_use_parse }, /* type in config is list, internal type is number for flags */

	{ "stat.async", LI_VALUE_BOOLEAN, TRUE, NULL },

	{ "buffer_request_body", LI_VALUE_BOOLEAN, TRUE, NULL },

	{ "strict.post_content_length", LI_VALUE_BOOLEAN, TRUE, NULL },

	{ "proxy_protocol.tlv_max_length", LI_VALUE_NUMBER, -1, NULL },

	{ "cgi.mangle_env_names", LI_VALUE_BOOLEAN, TRUE, NULL },

	{ NULL, 0, 0, NULL }
};

static const liPluginOptionPtr optionptrs[] = {
	{ "static.exclude_extensions", LI_VALUE_LIST, NULL, core_option_static_exclude_exts_parse, NULL },

	{ "server.name", LI_VALUE_STRING, NULL, NULL, NULL },
	{ "server.tag", LI_VALUE_STRING, PACKAGE_DESC, NULL, NULL },

	{ "mime_types", LI_VALUE_LIST, NULL, core_option_mime_types_parse, core_option_mime_types_free },

	{ NULL, 0, NULL, NULL, NULL }
};

static const liPluginAction actions[] = {
	{ "list", core_list, NULL },
	{ "when", core_when, NULL },

	{ "docroot", core_docroot, NULL },
	{ "alias", core_alias, NULL },
	{ "index", core_index, NULL },
	{ "static", core_static, NULL },
	{ "static_no_fail", core_static_no_fail, NULL },
	{ "pathinfo", core_pathinfo, NULL },

	{ "set_status", core_status, NULL },

	{ "log", core_log, NULL },
	{ "log.write", core_log_write, NULL },

	{ "respond", core_respond, NULL },

	{ "env.set", core_env_set, NULL },
	{ "env.add", core_env_add, NULL },
	{ "env.remove", core_env_remove, NULL },
	{ "env.clear", core_env_clear, NULL },

	{ "header.add", core_header_add, (void*)(intptr_t)li_http_header_insert },
	{ "header.append", core_header_add, (void*)(intptr_t)li_http_header_append },
	{ "header.overwrite", core_header_add, (void*)(intptr_t)li_http_header_overwrite },
	{ "header.remove", core_header_remove, NULL },

	{ "req_header.add", core_req_header_add, (void*)(intptr_t)li_http_header_insert },
	{ "req_header.append", core_req_header_add, (void*)(intptr_t)li_http_header_append },
	{ "req_header.overwrite", core_req_header_add, (void*)(intptr_t)li_http_header_overwrite },
	{ "req_header.remove", core_req_header_remove, NULL },

	{ "io.buffer_out", core_buffer_out, NULL },
	{ "io.buffer_in", core_buffer_in, NULL },

	{ "map", core_map, NULL },
	{ "map_cidr", core_map_cidr, NULL },

	{ "proxy_protocol.trust", core_proxy_prot_trust, NULL },

	{ NULL, NULL, NULL }
};

static const liPluginSetup setups[] = {
	{ "listen", core_listen, NULL },
	{ "workers", core_workers, NULL },
	{ "workers.cpu_affinity", core_workers_cpu_affinity, NULL },
	{ "module_load", core_module_load, NULL },
	{ "io.timeout", core_io_timeout, NULL },
	{ "stat_cache.ttl", core_stat_cache_ttl, NULL },
	{ "tasklet_pool.threads", core_tasklet_pool_threads, NULL },
	{ "log", core_setup_log, NULL },
	{ "log.timestamp", core_setup_log_timestamp, NULL },
	{ "fetch.files_static", core_register_fetch_files_static, NULL },

	{ NULL, NULL, NULL }
};

static const liPluginAngel angelcbs[] = {
	{ "warmup", core_warmup },
	{ "run", core_run },
	{ "suspend", core_suspend },

	{ NULL, NULL }
};
#include <sys/types.h>

#if defined(LIGHTY_OS_LINUX) || defined(LIGHTY_OS_FREEBSD)
/* settings CPU affinity is only supported on linux and FreeBSD */

#if defined(LIGHTY_OS_LINUX)
typedef cpu_set_t li_cpu_set_t;
#elif defined(LIGHTY_OS_FREEBSD)
typedef cpuset_t li_cpu_set_t;
#endif

static gboolean pin_cpu(liServer *srv, liWorker *wrk, li_cpu_set_t *cpu_set, gint64 ndx) {
	size_t cpu_ndx;

	if (ndx < 0 || ((guint64)ndx) > CPU_SETSIZE) {
		ERROR(srv, "invalid cpu affinity index for worker #%u: %"G_GINT64_FORMAT, wrk->ndx+1, ndx);
		return FALSE;
	}

	cpu_ndx = (size_t) ndx;
	CPU_SET(cpu_ndx, cpu_set);
	return TRUE;
}

static void plugin_core_prepare_worker_cpu_affinity(liServer *srv, liWorker *wrk) {
	li_cpu_set_t mask;
	liValue *v = srv->workers_cpu_affinity;

	if (NULL == v) return;

	if (wrk->ndx >= li_value_list_len(v)) {
		WARNING(srv, "worker #%u has no entry in workers.cpu_affinity", wrk->ndx+1);
		return;
	}

	CPU_ZERO(&mask);

	v = li_value_list_at(v, wrk->ndx);
	if (LI_VALUE_NUMBER == li_value_type(v)) {
		if (!pin_cpu(srv, wrk, &mask, v->data.number)) {
			return; /* pin_cpu already printed error */
		}
		DEBUG(srv, "binding worker #%u to cpu %u", wrk->ndx+1, (guint)v->data.number);
	} else {
		g_string_truncate(wrk->tmp_str, 0);

		LI_VALUE_FOREACH(entry, v)
			if (!pin_cpu(srv, wrk, &mask, entry->data.number)) {
				return; /* pin_cpu already printed error */
			}
			g_string_append_printf(wrk->tmp_str, _entry_i ? ",%u":"%u", (guint)entry->data.number);
		LI_VALUE_END_FOREACH()

		DEBUG(srv, "binding worker #%u to cpus %s", wrk->ndx+1, wrk->tmp_str->str);
	}

#if defined(LIGHTY_OS_LINUX)
	if (0 != sched_setaffinity(0, sizeof(mask), &mask)) {
		ERROR(srv, "couldn't set cpu affinity mask for worker #%u: %s", wrk->ndx+1, g_strerror(errno));
	}
#elif defined(LIGHTY_OS_FREEBSD)
	if (0 != cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_TID, -1, sizeof(mask), &mask)) {
		ERROR(srv, "couldn't set cpu affinity mask for worker #%u: %s", wrk->ndx+1, g_strerror(errno));
	}
#endif
}
#else
static void plugin_core_prepare_worker_cpu_affinity(liServer *srv, liWorker *wrk) {
	UNUSED(srv);
	UNUSED(wrk);
}
#endif

static void plugin_core_prepare_worker(liServer *srv, liPlugin *p, liWorker *wrk) {
	UNUSED(p);

	plugin_core_prepare_worker_cpu_affinity(srv, wrk);
}

void li_plugin_core_init(liServer *srv, liPlugin *p, gpointer userdata) {
	UNUSED(srv); UNUSED(userdata);

	p->options = options;
	p->optionptrs = optionptrs;
	p->actions = actions;
	p->setups = setups;
	p->angelcbs = angelcbs;

	p->handle_prepare_worker = plugin_core_prepare_worker;
}
