/*
 * HTTP rules parsing and registration
 *
 * Copyright 2000-2018 Willy Tarreau <w@1wt.eu>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version
 * 2 of the License, or (at your option) any later version.
 *
 */

#include <sys/types.h>

#include <ctype.h>
#include <string.h>
#include <time.h>

#include <haproxy/acl.h>
#include <haproxy/action.h>
#include <haproxy/api.h>
#include <haproxy/arg.h>
#include <haproxy/capture-t.h>
#include <haproxy/cfgparse.h>
#include <haproxy/chunk.h>
#include <haproxy/global.h>
#include <haproxy/http.h>
#include <haproxy/http_ana-t.h>
#include <haproxy/http_rules.h>
#include <haproxy/log.h>
#include <haproxy/pool.h>
#include <haproxy/proxy.h>
#include <haproxy/sample.h>
#include <haproxy/tools.h>
#include <haproxy/version.h>


/* List head of all known action keywords for "http-request" */
struct action_kw_list http_req_keywords = {
       .list = LIST_HEAD_INIT(http_req_keywords.list)
};

/* List head of all known action keywords for "http-response" */
struct action_kw_list http_res_keywords = {
       .list = LIST_HEAD_INIT(http_res_keywords.list)
};

/* List head of all known action keywords for "http-after-response" */
struct action_kw_list http_after_res_keywords = {
       .list = LIST_HEAD_INIT(http_after_res_keywords.list)
};

void http_req_keywords_register(struct action_kw_list *kw_list)
{
	act_add_list(&http_req_keywords.list, kw_list);
}

void http_res_keywords_register(struct action_kw_list *kw_list)
{
	act_add_list(&http_res_keywords.list, kw_list);
}

void http_after_res_keywords_register(struct action_kw_list *kw_list)
{
	act_add_list(&http_after_res_keywords.list, kw_list);
}

/*
 * Return the struct http_req_action_kw associated to a keyword.
 */
struct action_kw *action_http_req_custom(const char *kw)
{
	return action_lookup(&http_req_keywords.list, kw);
}

/*
 * Return the struct http_res_action_kw associated to a keyword.
 */
struct action_kw *action_http_res_custom(const char *kw)
{
	return action_lookup(&http_res_keywords.list, kw);
}

/*
 * Return the struct http_after_res_action_kw associated to a keyword.
 */
struct action_kw *action_http_after_res_custom(const char *kw)
{
	return action_lookup(&http_after_res_keywords.list, kw);
}

/* parse an "http-request" rule */
struct act_rule *parse_http_req_cond(const char **args, const char *file, int linenum, struct proxy *proxy)
{
	struct act_rule *rule;
	const struct action_kw *custom = NULL;
	int cur_arg;

	rule = new_act_rule(ACT_F_HTTP_REQ, file, linenum);
	if (!rule) {
		ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
		goto out;
	}

	if (((custom = action_http_req_custom(args[0])) != NULL)) {
		char *errmsg = NULL;

		cur_arg = 1;
		/* try in the module list */
		rule->kw = custom;

		if (custom->flags & KWF_EXPERIMENTAL) {
			if (!experimental_directives_allowed) {
				ha_alert("parsing [%s:%d] : '%s' action is experimental, must be allowed via a global 'expose-experimental-directives'\n",
				         file, linenum, custom->kw);
				goto out_err;
			}
			mark_tainted(TAINTED_CONFIG_EXP_KW_DECLARED);
		}

		if (custom->parse(args, &cur_arg, proxy, rule, &errmsg) == ACT_RET_PRS_ERR) {
			ha_alert("parsing [%s:%d] : error detected in %s '%s' while parsing 'http-request %s' rule : %s.\n",
				 file, linenum, proxy_type_str(proxy), proxy->id, args[0], errmsg);
			free(errmsg);
			goto out_err;
		}
		else if (errmsg) {
			ha_warning("parsing [%s:%d] : %s.\n", file, linenum, errmsg);
			free(errmsg);
		}
	}
	else {
		const char *best = action_suggest(args[0], &http_req_keywords.list, NULL);

		action_build_list(&http_req_keywords.list, &trash);
		ha_alert("parsing [%s:%d]: 'http-request' expects %s, but got '%s'%s.%s%s%s\n",
		         file, linenum, trash.area,
		         args[0], *args[0] ? "" : " (missing argument)",
		         best ? " Did you mean '" : "",
		         best ? best : "",
		         best ? "' maybe ?" : "");
		goto out_err;
	}

	if (strcmp(args[cur_arg], "if") == 0 || strcmp(args[cur_arg], "unless") == 0) {
		struct acl_cond *cond;
		char *errmsg = NULL;

		if ((cond = build_acl_cond(file, linenum, &proxy->acl, proxy, args+cur_arg, &errmsg)) == NULL) {
			ha_alert("parsing [%s:%d] : error detected while parsing an 'http-request %s' condition : %s.\n",
				 file, linenum, args[0], errmsg);
			free(errmsg);
			goto out_err;
		}
		rule->cond = cond;
	}
	else if (*args[cur_arg]) {
		ha_alert("parsing [%s:%d]: 'http-request %s' expects"
			 " either 'if' or 'unless' followed by a condition but found '%s'.\n",
			 file, linenum, args[0], args[cur_arg]);
		goto out_err;
	}

	return rule;
 out_err:
	free_act_rule(rule);
 out:
	return NULL;
}

/* parse an "http-respose" rule */
struct act_rule *parse_http_res_cond(const char **args, const char *file, int linenum, struct proxy *proxy)
{
	struct act_rule *rule;
	const struct action_kw *custom = NULL;
	int cur_arg;

	rule = new_act_rule(ACT_F_HTTP_RES, file, linenum);
	if (!rule) {
		ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
		goto out;
	}

	if (((custom = action_http_res_custom(args[0])) != NULL)) {
		char *errmsg = NULL;

		cur_arg = 1;
		/* try in the module list */
		rule->kw = custom;

		if (custom->flags & KWF_EXPERIMENTAL) {
			if (!experimental_directives_allowed) {
				ha_alert("parsing [%s:%d] : '%s' action is experimental, must be allowed via a global 'expose-experimental-directives'\n",
				         file, linenum, custom->kw);
				goto out_err;
			}
			mark_tainted(TAINTED_CONFIG_EXP_KW_DECLARED);
		}

		if (custom->parse(args, &cur_arg, proxy, rule, &errmsg) == ACT_RET_PRS_ERR) {
			ha_alert("parsing [%s:%d] : error detected in %s '%s' while parsing 'http-response %s' rule : %s.\n",
				 file, linenum, proxy_type_str(proxy), proxy->id, args[0], errmsg);
			free(errmsg);
			goto out_err;
		}
		else if (errmsg) {
			ha_warning("parsing [%s:%d] : %s.\n", file, linenum, errmsg);
			free(errmsg);
		}
	}
	else {
		const char *best = action_suggest(args[0], &http_res_keywords.list, NULL);

		action_build_list(&http_res_keywords.list, &trash);
		ha_alert("parsing [%s:%d]: 'http-response' expects %s, but got '%s'%s.%s%s%s\n",
		         file, linenum, trash.area,
		         args[0], *args[0] ? "" : " (missing argument)",
		         best ? " Did you mean '" : "",
		         best ? best : "",
		         best ? "' maybe ?" : "");
		goto out_err;
	}

	if (strcmp(args[cur_arg], "if") == 0 || strcmp(args[cur_arg], "unless") == 0) {
		struct acl_cond *cond;
		char *errmsg = NULL;

		if ((cond = build_acl_cond(file, linenum, &proxy->acl, proxy, args+cur_arg, &errmsg)) == NULL) {
			ha_alert("parsing [%s:%d] : error detected while parsing an 'http-response %s' condition : %s.\n",
				 file, linenum, args[0], errmsg);
			free(errmsg);
			goto out_err;
		}
		rule->cond = cond;
	}
	else if (*args[cur_arg]) {
		ha_alert("parsing [%s:%d]: 'http-response %s' expects"
			 " either 'if' or 'unless' followed by a condition but found '%s'.\n",
			 file, linenum, args[0], args[cur_arg]);
		goto out_err;
	}

	return rule;
 out_err:
	free_act_rule(rule);
 out:
	return NULL;
}


/* parse an "http-after-response" rule */
struct act_rule *parse_http_after_res_cond(const char **args, const char *file, int linenum, struct proxy *proxy)
{
	struct act_rule *rule;
	const struct action_kw *custom = NULL;
	int cur_arg;

	rule = new_act_rule(ACT_F_HTTP_RES, file, linenum);
	if (!rule) {
		ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
		goto out;
	}

	if (((custom = action_http_after_res_custom(args[0])) != NULL)) {
		char *errmsg = NULL;

		cur_arg = 1;
		/* try in the module list */
		rule->kw = custom;
		if (custom->parse(args, &cur_arg, proxy, rule, &errmsg) == ACT_RET_PRS_ERR) {
			ha_alert("parsing [%s:%d] : error detected in %s '%s' while parsing 'http-after-response %s' rule : %s.\n",
				 file, linenum, proxy_type_str(proxy), proxy->id, args[0], errmsg);
			free(errmsg);
			goto out_err;
		}
		else if (errmsg) {
			ha_warning("parsing [%s:%d] : %s.\n", file, linenum, errmsg);
			free(errmsg);
		}
	}
	else {
		const char *best = action_suggest(args[0], &http_after_res_keywords.list, NULL);

		action_build_list(&http_after_res_keywords.list, &trash);
		ha_alert("parsing [%s:%d]: 'http-after-response' expects %s, but got '%s'%s.%s%s%s\n",
		         file, linenum, trash.area,
		         args[0], *args[0] ? "" : " (missing argument)",
		         best ? " Did you mean '" : "",
		         best ? best : "",
		         best ? "' maybe ?" : "");
		goto out_err;
	}

	if (strcmp(args[cur_arg], "if") == 0 || strcmp(args[cur_arg], "unless") == 0) {
		struct acl_cond *cond;
		char *errmsg = NULL;

		if ((cond = build_acl_cond(file, linenum, &proxy->acl, proxy, args+cur_arg, &errmsg)) == NULL) {
			ha_alert("parsing [%s:%d] : error detected while parsing an 'http-after-response %s' condition : %s.\n",
				 file, linenum, args[0], errmsg);
			free(errmsg);
			goto out_err;
		}
		rule->cond = cond;
	}
	else if (*args[cur_arg]) {
		ha_alert("parsing [%s:%d]: 'http-after-response %s' expects"
			 " either 'if' or 'unless' followed by a condition but found '%s'.\n",
			 file, linenum, args[0], args[cur_arg]);
		goto out_err;
	}

	return rule;
 out_err:
	free_act_rule(rule);
 out:
	return NULL;
}

/* completely free redirect rule */
void http_free_redirect_rule(struct redirect_rule *rdr)
{
	free_acl_cond(rdr->cond);
	free(rdr->rdr_str);
	if ((rdr->flags & REDIRECT_FLAG_COOKIE_FMT))
		lf_expr_deinit(&rdr->cookie.fmt);
	else
		istfree(&rdr->cookie.str);
	lf_expr_deinit(&rdr->rdr_fmt);
	free(rdr);
}

/* Parses a redirect rule. Returns the redirect rule on success or NULL on error,
 * with <err> filled with the error message. If <use_fmt> is not null, builds a
 * dynamic log-format rule instead of a static string. Parameter <dir> indicates
 * the direction of the rule, and equals 0 for request, non-zero for responses.
 */
struct redirect_rule *http_parse_redirect_rule(const char *file, int linenum, struct proxy *curproxy,
                                               const char **args, char **errmsg, int use_fmt, int dir)
{
	struct redirect_rule *rule = NULL;
	int cur_arg;
	int type = REDIRECT_TYPE_NONE;
	int code = 302;
	const char *destination = NULL;
	const char *cookie = NULL;
	int cookie_set = 0;
	size_t cookie_len = 0;
	unsigned int flags = (!dir ? REDIRECT_FLAG_FROM_REQ : REDIRECT_FLAG_NONE);
	struct acl_cond *cond = NULL;

	cur_arg = 0;
	while (*(args[cur_arg])) {
		if (strcmp(args[cur_arg], "location") == 0) {
			if (!*args[cur_arg + 1])
				goto missing_arg;

			type = REDIRECT_TYPE_LOCATION;
			cur_arg++;
			destination = args[cur_arg];
		}
		else if (strcmp(args[cur_arg], "prefix") == 0) {
			if (!*args[cur_arg + 1])
				goto missing_arg;
			type = REDIRECT_TYPE_PREFIX;
			cur_arg++;
			destination = args[cur_arg];
		}
		else if (strcmp(args[cur_arg], "scheme") == 0) {
			if (!*args[cur_arg + 1])
				goto missing_arg;

			type = REDIRECT_TYPE_SCHEME;
			cur_arg++;
			destination = args[cur_arg];
		}
		else if (strcmp(args[cur_arg], "set-cookie") == 0) {
			if (!*args[cur_arg + 1])
				goto missing_arg;

			cur_arg++;
			cookie = args[cur_arg];
			cookie_set = 1;
		}
		else if (strcmp(args[cur_arg], "set-cookie-fmt") == 0) {
			if (!*args[cur_arg + 1])
				goto missing_arg;

			cur_arg++;
			cookie = args[cur_arg];
			cookie_set = 2;
		}
		else if (strcmp(args[cur_arg], "clear-cookie") == 0) {
			if (!*args[cur_arg + 1])
				goto missing_arg;

			cur_arg++;
			cookie = args[cur_arg];
			cookie_set = 0;
		}
		else if (strcmp(args[cur_arg], "code") == 0) {
			if (!*args[cur_arg + 1])
				goto missing_arg;

			cur_arg++;
			code = atol(args[cur_arg]);
			if (code < 301 || code > 308 || (code > 303 && code < 307)) {
				memprintf(errmsg,
				          "'%s': unsupported HTTP code '%s' (must be one of 301, 302, 303, 307 or 308)",
				          args[cur_arg - 1], args[cur_arg]);
				goto err;
			}
		}
		else if (strcmp(args[cur_arg], "drop-query") == 0) {
			flags |= REDIRECT_FLAG_DROP_QS;
		}
		else if (strcmp(args[cur_arg], "keep-query") == 0) {
			flags |= REDIRECT_FLAG_KEEP_QS;
		}
		else if (strcmp(args[cur_arg], "append-slash") == 0) {
			flags |= REDIRECT_FLAG_APPEND_SLASH;
		}
		else if (strcmp(args[cur_arg], "ignore-empty") == 0) {
			flags |= REDIRECT_FLAG_IGNORE_EMPTY;
		}
		else if (strcmp(args[cur_arg], "if") == 0 ||
			 strcmp(args[cur_arg], "unless") == 0) {
			cond = build_acl_cond(file, linenum, &curproxy->acl, curproxy, (const char **)args + cur_arg, errmsg);
			if (!cond) {
				memprintf(errmsg, "error in condition: %s", *errmsg);
				goto err;
			}
			break;
		}
		else {
			memprintf(errmsg,
			          "expects 'code', 'prefix', 'location', 'scheme', 'set-cookie', 'set-cookie-fmt',"
				  " 'clear-cookie', 'drop-query', 'keep-query', 'ignore-empty' or 'append-slash' (was '%s')",
			          args[cur_arg]);
			goto err;
		}
		cur_arg++;
	}

	if (type == REDIRECT_TYPE_NONE) {
		memprintf(errmsg, "redirection type expected ('prefix', 'location', or 'scheme')");
		goto err;
	}

	if (dir && type != REDIRECT_TYPE_LOCATION) {
		memprintf(errmsg, "response only supports redirect type 'location'");
		goto err;
	}

	rule = calloc(1, sizeof(*rule));
	if (!rule)
		goto out_of_memory;
	rule->cond = cond;
	lf_expr_init(&rule->rdr_fmt);

	if (!use_fmt) {
		/* old-style static redirect rule */
		rule->rdr_str = strdup(destination);
		if (!rule->rdr_str)
			goto out_of_memory;
		rule->rdr_len = strlen(destination);
	}
	else {
		/* log-format based redirect rule */
		int cap = 0;

		/* Parse destination. Note that in the REDIRECT_TYPE_PREFIX case,
		 * if prefix == "/", we don't want to add anything, otherwise it
		 * makes it hard for the user to configure a self-redirection.
		 */
		curproxy->conf.args.ctx = ARGC_RDR;
		if (curproxy->cap & PR_CAP_FE)
			cap |= (dir ? SMP_VAL_FE_HRS_HDR : SMP_VAL_FE_HRQ_HDR);
		if (curproxy->cap & PR_CAP_BE)
			cap |= (dir ? SMP_VAL_BE_HRS_HDR : SMP_VAL_BE_HRQ_HDR);
		if (!(type == REDIRECT_TYPE_PREFIX && destination[0] == '/' && destination[1] == '\0')) {
			if (!parse_logformat_string(destination, curproxy, &rule->rdr_fmt, LOG_OPT_HTTP, cap, errmsg)) {
				goto err;
			}
		}
	}

	if (cookie) {
		/* depending on cookie_set, either we want to set the cookie, or to clear it.
		 * a clear consists in appending "; path=/; Max-Age=0;" at the end.
		 */
		cookie_len = strlen(cookie);
		if (cookie_set == 1) { // set-cookie
			rule->cookie.str = istalloc(cookie_len+9);
			if (!isttest(rule->cookie.str))
				goto out_of_memory;
			istcpy(&rule->cookie.str, ist2(cookie, cookie_len), cookie_len);
			istcat(&rule->cookie.str, ist2("; path=/;", 9), cookie_len+10);
		}
		else if (cookie_set == 2) { // set-cookie-fmt
			int cap = 0;

			lf_expr_init(&rule->cookie.fmt);
			curproxy->conf.args.ctx = ARGC_RDR;
			if (curproxy->cap & PR_CAP_FE)
				cap |= (dir ? SMP_VAL_FE_HRS_HDR : SMP_VAL_FE_HRQ_HDR);
			if (curproxy->cap & PR_CAP_BE)
				cap |= (dir ? SMP_VAL_BE_HRS_HDR : SMP_VAL_BE_HRQ_HDR);

			chunk_memcpy(&trash, cookie, cookie_len);
			chunk_strcat(&trash, "; path=/;");
			if (!parse_logformat_string(trash.area, curproxy, &rule->cookie.fmt, LOG_OPT_HTTP, cap, errmsg)) {
				goto err;
			}

			flags |= REDIRECT_FLAG_COOKIE_FMT;
		}
		else { // clear-cookie
			rule->cookie.str = istalloc(cookie_len+20);
			if (!isttest(rule->cookie.str))
				goto out_of_memory;
			istcpy(&rule->cookie.str, ist2(cookie, cookie_len), cookie_len);
			istcat(&rule->cookie.str, ist2("; path=/; Max-Age=0;", 20), cookie_len+21);
		}
	}
	rule->type = type;
	rule->code = code;
	rule->flags = flags;
	LIST_INIT(&rule->list);
	return rule;

 missing_arg:
	memprintf(errmsg, "missing argument for '%s'", args[cur_arg]);
	goto err;
 out_of_memory:
	memprintf(errmsg, "parsing [%s:%d]: out of memory.", file, linenum);
 err:
	if (rule)
		http_free_redirect_rule(rule);
	else if (cond) {
		/* rule not yet allocated, but cond already is */
		free_acl_cond(cond);
	}

	return NULL;
}

/*
 * Local variables:
 *  c-indent-level: 8
 *  c-basic-offset: 8
 * End:
 */
