MEDIUM: http: Add a ruleset evaluated on all responses just before forwarding

This patch introduces the 'http-after-response' rules. These rules are evaluated
at the end of the response analysis, just before the data forwarding, on ALL
HTTP responses, the server ones but also all responses generated by
HAProxy. Thanks to this ruleset, it is now possible for instance to add some
headers to the responses generated by the stats applet. Following actions are
supported :

   * allow
   * add-header
   * del-header
   * replace-header
   * replace-value
   * set-header
   * set-status
   * set-var
   * strict-mode
   * unset-var
diff --git a/doc/configuration.txt b/doc/configuration.txt
index 776fd70..cfbcc5c 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -2605,6 +2605,7 @@
 fullconn                                  X          -         X         X
 grace                                     X          X         X         X
 hash-type                                 X          -         X         X
+http-after-response                       -          X         X         X
 http-check disable-on-404                 X          -         X         X
 http-check expect                         -          -         X         X
 http-check send-state                     X          -         X         X
@@ -4189,6 +4190,154 @@
   See also : "balance", "hash-balance-factor", "server"
 
 
+http-after-response <action> <options...> [ { if | unless } <condition> ]
+  Access control for all Layer 7 responses (server, applet/service and internal
+  ones).
+
+  May be used in sections:   defaults | frontend | listen | backend
+                                no    |    yes   |   yes  |   yes
+
+  The http-after-response statement defines a set of rules which apply to layer
+  7 processing. The rules are evaluated in their declaration order when they
+  are met in a frontend, listen or backend section. Any rule may optionally be
+  followed by an ACL-based condition, in which case it will only be evaluated
+  if the condition is true. Since these rules apply on responses, the backend
+  rules are applied first, followed by the frontend's rules.
+
+  Unlike http-response rules, these ones are applied on all responses, the
+  server ones but also to all responses generated by HAProxy. These rules are
+  evaluated at the end of the responses analysis, before the data forwarding.
+
+  The first keyword is the rule's action. The supported actions are described
+  below.
+
+  There is no limit to the number of http-after-response statements per
+  instance.
+
+  Example:
+    http-after-response set-header Strict-Transport-Security "max-age=31536000"
+    http-after-response set-header Cache-Control "no-store,no-cache,private"
+    http-after-response set-header Pragma "no-cache"
+
+http-after-response add-header <name> <fmt> [ { if | unless } <condition> ]
+
+  This appends an HTTP header field whose name is specified in <name> and whose
+  value is defined by <fmt> which follows the log-format rules (see Custom Log
+  Format in section 8.2.4). This may be used to send a cookie to a client for
+  example, or to pass some internal information.
+  This rule is not final, so it is possible to add other similar rules.
+  Note that header addition is performed immediately, so one rule might reuse
+  the resulting header from a previous rule.
+
+http-after-response allow [ { if | unless } <condition> ]
+
+  This stops the evaluation of the rules and lets the response pass the check.
+  No further "http-after-response" rules are evaluated.
+
+http-after-response del-header <name> [ { if | unless } <condition> ]
+
+  This removes all HTTP header fields whose name is specified in <name>.
+
+http-after-response replace-header <name> <regex-match> <replace-fmt>
+                                   [ { if | unless } <condition> ]
+
+  This works like "http-response replace-header".
+
+  Example:
+    http-after-response replace-header Set-Cookie (C=[^;]*);(.*) \1;ip=%bi;\2
+
+    # applied to:
+    Set-Cookie: C=1; expires=Tue, 14-Jun-2016 01:40:45 GMT
+
+    # outputs:
+    Set-Cookie: C=1;ip=192.168.1.20; expires=Tue, 14-Jun-2016 01:40:45 GMT
+
+    # assuming the backend IP is 192.168.1.20.
+
+http-after-response replace-value <name> <regex-match> <replace-fmt>
+                                 [ { if | unless } <condition> ]
+
+  This works like "http-response replace-value".
+
+  Example:
+    http-after-response replace-value Cache-control ^public$ private
+
+    # applied to:
+    Cache-Control: max-age=3600, public
+
+    # outputs:
+    Cache-Control: max-age=3600, private
+
+http-after-response set-header <name> <fmt> [ { if | unless } <condition> ]
+
+  This does the same as "add-header" except that the header name is first
+  removed if it existed. This is useful when passing security information to
+  the server, where the header must not be manipulated by external users.
+
+http-after-response set-status <status> [reason <str>]
+                               [ { if | unless } <condition> ]
+
+  This replaces the response status code with <status> which must be an integer
+  between 100 and 999. Optionally, a custom reason text can be provided defined
+  by <str>, or the default reason for the specified code will be used as a
+  fallback.
+
+  Example:
+    # return "431 Request Header Fields Too Large"
+    http-response set-status 431
+    # return "503 Slow Down", custom reason
+    http-response set-status 503 reason "Slow Down"
+
+http-after-response set-var(<var-name>) <expr> [ { if | unless } <condition> ]
+
+  This is used to set the contents of a variable. The variable is declared
+  inline.
+
+  Arguments:
+    <var-name>  The name of the variable starts with an indication about its
+                scope. The scopes allowed are:
+                  "proc" : the variable is shared with the whole process
+                  "sess" : the variable is shared with the whole session
+                  "txn"  : the variable is shared with the transaction
+                           (request and response)
+                  "req"  : the variable is shared only during request
+                           processing
+                  "res"  : the variable is shared only during response
+                           processing
+                This prefix is followed by a name. The separator is a '.'.
+                The name may only contain characters 'a-z', 'A-Z', '0-9', '.'
+                and '_'.
+
+    <expr>      Is a standard HAProxy expression formed by a sample-fetch
+                followed by some converters.
+
+  Example:
+    http-after-response set-var(sess.last_redir) res.hdr(location)
+
+http-after-response strict-mode { on | off }
+
+  This enables or disables the strict rewriting mode for following rules. It
+  does not affect rules declared before it and it is only applicable on rules
+  performing a rewrite on the responses. When the strict mode is enabled, any
+  rewrite failure triggers an internal error. Otherwise, such errors are
+  silently ignored. The purpose of the strict rewriting mode is to make some
+  rewrites optionnal while others must be performed to continue the response
+  processing.
+
+  By default, the strict rewriting mode is enabled. Its value is also reset
+  when a ruleset evaluation ends. So, for instance, if you change the mode on
+  the bacnkend, the default mode is restored when HAProxy starts the frontend
+  rules evaluation.
+
+http-after-response unset-var(<var-name>) [ { if | unless } <condition> ]
+
+  This is used to unset a variable. See "http-after-response set-var" for
+  details about <var-name>.
+
+  Example:
+    http-after-response unset-var(sess.last_redir)
+
+
 http-check disable-on-404
   Enable a maintenance mode upon HTTP/404 response to health-checks
   May be used in sections :   defaults | frontend | listen | backend
diff --git a/include/proto/http_ana.h b/include/proto/http_ana.h
index 2b35388..62fd74d 100644
--- a/include/proto/http_ana.h
+++ b/include/proto/http_ana.h
@@ -40,6 +40,7 @@
 int http_request_forward_body(struct stream *s, struct channel *req, int an_bit);
 int http_response_forward_body(struct stream *s, struct channel *res, int an_bit);
 int http_apply_redirect_rule(struct redirect_rule *rule, struct stream *s, struct http_txn *txn);
+int http_eval_after_res_rules(struct stream *s);
 int http_replace_hdrs(struct stream* s, struct htx *htx, struct ist name, const char *str, struct my_regex *re, int full);
 int http_req_replace_stline(int action, const char *replace, int len,
 			    struct proxy *px, struct stream *s);
diff --git a/include/proto/http_rules.h b/include/proto/http_rules.h
index 608ca57..3e57b9d 100644
--- a/include/proto/http_rules.h
+++ b/include/proto/http_rules.h
@@ -29,9 +29,11 @@
 
 extern struct action_kw_list http_req_keywords;
 extern struct action_kw_list http_res_keywords;
+extern struct action_kw_list http_after_res_keywords;
 
 struct act_rule *parse_http_req_cond(const char **args, const char *file, int linenum, struct proxy *proxy);
 struct act_rule *parse_http_res_cond(const char **args, const char *file, int linenum, struct proxy *proxy);
+struct act_rule *parse_http_after_res_cond(const char **args, const char *file, int linenum, struct proxy *proxy);
 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);
 
@@ -45,6 +47,11 @@
 	LIST_ADDQ(&http_res_keywords.list, &kw_list->list);
 }
 
+static inline void http_after_res_keywords_register(struct action_kw_list *kw_list)
+{
+	LIST_ADDQ(&http_after_res_keywords.list, &kw_list->list);
+}
+
 #endif /* _PROTO_HTTP_RULES_H */
 
 /*
diff --git a/include/types/proxy.h b/include/types/proxy.h
index c6b56aa..f3b0e6b 100644
--- a/include/types/proxy.h
+++ b/include/types/proxy.h
@@ -315,6 +315,7 @@
 	struct list acl;                        /* ACL declared on this proxy */
 	struct list http_req_rules;		/* HTTP request rules: allow/deny/... */
 	struct list http_res_rules;		/* HTTP response rules: allow/deny/... */
+	struct list http_after_res_rules;	/* HTTP final response rules: set-header/del-header/... */
 	struct list redirect_rules;             /* content redirecting rules (chained) */
 	struct list switching_rules;            /* content switching rules (chained) */
 	struct list persist_rules;		/* 'force-persist' and 'ignore-persist' rules (chained) */
diff --git a/src/cfgparse-listen.c b/src/cfgparse-listen.c
index 3f16a25..70627c3 100644
--- a/src/cfgparse-listen.c
+++ b/src/cfgparse-listen.c
@@ -1413,6 +1413,36 @@
 
 		LIST_ADDQ(&curproxy->http_res_rules, &rule->list);
 	}
+	else if (!strcmp(args[0], "http-after-response")) {
+		struct act_rule *rule;
+
+		if (curproxy == &defproxy) {
+			ha_alert("parsing [%s:%d]: '%s' not allowed in 'defaults' section.\n", file, linenum, args[0]);
+			err_code |= ERR_ALERT | ERR_FATAL;
+			goto out;
+		}
+
+		if (!LIST_ISEMPTY(&curproxy->http_after_res_rules) &&
+		    !LIST_PREV(&curproxy->http_after_res_rules, struct act_rule *, list)->cond &&
+		    (LIST_PREV(&curproxy->http_after_res_rules, struct act_rule *, list)->flags & ACT_FLAG_FINAL)) {
+			ha_warning("parsing [%s:%d]: previous '%s' action is final and has no condition attached, further entries are NOOP.\n",
+				   file, linenum, args[0]);
+			err_code |= ERR_WARN;
+		}
+
+		rule = parse_http_after_res_cond((const char **)args + 1, file, linenum, curproxy);
+
+		if (!rule) {
+			err_code |= ERR_ALERT | ERR_ABORT;
+			goto out;
+		}
+
+		err_code |= warnif_cond_conflicts(rule->cond,
+	                                          (curproxy->cap & PR_CAP_BE) ? SMP_VAL_BE_HRS_HDR : SMP_VAL_FE_HRS_HDR,
+	                                          file, linenum);
+
+		LIST_ADDQ(&curproxy->http_after_res_rules, &rule->list);
+	}
 	else if (!strcmp(args[0], "http-send-name-header")) { /* send server name in request header */
 		/* set the header name and length into the proxy structure */
 		if (warnifnotcap(curproxy, PR_CAP_BE, file, linenum, args[0], NULL))
diff --git a/src/cfgparse.c b/src/cfgparse.c
index cd90d7e..8ad64bc 100644
--- a/src/cfgparse.c
+++ b/src/cfgparse.c
@@ -2841,6 +2841,16 @@
 			}
 		}
 
+		/* check validity for 'http-after-response' layer 7 rules */
+		list_for_each_entry(arule, &curproxy->http_after_res_rules, list) {
+			err = NULL;
+			if (arule->check_ptr && !arule->check_ptr(arule, curproxy, &err)) {
+				ha_alert("Proxy '%s': %s.\n", curproxy->id, err);
+				free(err);
+				cfgerr++;
+			}
+		}
+
 		if (curproxy->table && curproxy->table->peers.name) {
 			struct peers *curpeers;
 
diff --git a/src/haproxy.c b/src/haproxy.c
index 5bca002..f04ccea 100644
--- a/src/haproxy.c
+++ b/src/haproxy.c
@@ -2440,6 +2440,7 @@
 		deinit_act_rules(&p->tcp_req.l5_rules);
 		deinit_act_rules(&p->http_req_rules);
 		deinit_act_rules(&p->http_res_rules);
+		deinit_act_rules(&p->http_after_res_rules);
 
 		deinit_stick_rules(&p->storersp_rules);
 		deinit_stick_rules(&p->sticking_rules);
diff --git a/src/http_act.c b/src/http_act.c
index 3826ddf..a74aeea 100644
--- a/src/http_act.c
+++ b/src/http_act.c
@@ -1875,6 +1875,22 @@
 
 INITCALL1(STG_REGISTER, http_res_keywords_register, &http_res_actions);
 
+static struct action_kw_list http_after_res_actions = {
+	.kw = {
+		{ "add-header",      parse_http_set_header,     0 },
+		{ "allow",           parse_http_allow,          0 },
+		{ "del-header",      parse_http_del_header,     0 },
+		{ "replace-header",  parse_http_replace_header, 0 },
+		{ "replace-value",   parse_http_replace_header, 0 },
+		{ "set-header",      parse_http_set_header,     0 },
+		{ "set-status",      parse_http_set_status,     0 },
+		{ "strict-mode",     parse_http_strict_mode,    0 },
+		{ NULL, NULL }
+	}
+};
+
+INITCALL1(STG_REGISTER, http_after_res_keywords_register, &http_after_res_actions);
+
 /*
  * Local variables:
  *  c-indent-level: 8
diff --git a/src/http_ana.c b/src/http_ana.c
index 5e0e248..2548a9a 100644
--- a/src/http_ana.c
+++ b/src/http_ana.c
@@ -1986,15 +1986,6 @@
 		cur_proxy = sess->fe;
 	}
 
-	/* After this point, this anayzer can't return yield, so we can
-	 * remove the bit corresponding to this analyzer from the list.
-	 *
-	 * Note that the intermediate returns and goto found previously
-	 * reset the analyzers.
-	 */
-	rep->analysers &= ~an_bit;
-	rep->analyse_exp = TICK_ETERNITY;
-
 	/* OK that's all we can do for 1xx responses */
 	if (unlikely(txn->status < 200 && txn->status != 101))
 		goto end;
@@ -2116,6 +2107,14 @@
 	}
 
   end:
+	/*
+	 * Evaluate after-response rules before forwarding the response. rules
+	 * from the backend are evaluated first, then one from the frontend if
+	 * it differs.
+	 */
+	if (!http_eval_after_res_rules(s))
+		goto return_int_err;
+
 	/* Always enter in the body analyzer */
 	rep->analysers &= ~AN_RES_FLT_XFER_DATA;
 	rep->analysers |= AN_RES_HTTP_XFER_BODY;
@@ -2130,10 +2129,9 @@
 		s->do_log(s);
 		s->logs.bytes_out = 0;
 	}
-	DBG_TRACE_LEAVE(STRM_EV_STRM_ANA|STRM_EV_HTTP_ANA, s, txn);
-	return 1;
 
  done:
+	DBG_TRACE_LEAVE(STRM_EV_STRM_ANA|STRM_EV_HTTP_ANA, s, txn);
 	rep->analysers &= ~an_bit;
 	rep->analyse_exp = TICK_ETERNITY;
 	return 1;
@@ -3120,6 +3118,31 @@
 	return rule_ret;
 }
 
+/* Executes backend and frontend http-after-response rules for the stream <s>,
+ * in that order. it return 1 on success and 0 on error. It is the caller
+ * responsibility to catch error or ignore it. If it catches it, this function
+ * may be called a second time, for the internal error.
+ */
+int http_eval_after_res_rules(struct stream *s)
+{
+	struct session *sess = s->sess;
+	enum rule_result ret = HTTP_RULE_RES_CONT;
+
+	/* prune the request variables if not already done and swap to the response variables. */
+	if (s->vars_reqres.scope != SCOPE_RES) {
+		if (!LIST_ISEMPTY(&s->vars_reqres.head))
+			vars_prune(&s->vars_reqres, s->sess, s);
+		vars_init(&s->vars_reqres, SCOPE_RES);
+	}
+
+	ret = http_res_get_intercept_rule(s->be, &s->be->http_after_res_rules, s);
+	if ((ret == HTTP_RULE_RES_CONT || ret == HTTP_RULE_RES_STOP) && sess->fe != s->be)
+		ret = http_res_get_intercept_rule(sess->fe, &sess->fe->http_after_res_rules, s);
+
+	/* All other codes than CONTINUE, STOP or DONE are forbidden */
+	return (ret == HTTP_RULE_RES_CONT || ret == HTTP_RULE_RES_STOP || ret == HTTP_RULE_RES_DONE);
+}
+
 /*
  * Manage client-side cookie. It can impact performance by about 2% so it is
  * desirable to call it only when needed. This code is quite complex because
@@ -4534,6 +4557,8 @@
 
 	if (final) {
 		htx->flags |= HTX_FL_PROXY_RESP;
+		if (!http_eval_after_res_rules(s))
+			return 0;
 
 		channel_auto_read(req);
 		channel_abort(req);
diff --git a/src/http_htx.c b/src/http_htx.c
index accfa19..587b9a5 100644
--- a/src/http_htx.c
+++ b/src/http_htx.c
@@ -1308,7 +1308,7 @@
 		if (htx_free_data_space(htx) < global.tune.maxrewrite) {
 			ha_warning("config: errorfile '%s' runs over the buffer space"
 				   " reserved to headers rewritting. It may lead to internal errors if "
-				   " http-final-response rules are evaluated on this message.\n",
+				   " http-after-response rules are evaluated on this message.\n",
 				   (char *)node->key);
 			err_code |= ERR_WARN;
 		}
diff --git a/src/http_rules.c b/src/http_rules.c
index f1a187b..2e58ec2 100644
--- a/src/http_rules.c
+++ b/src/http_rules.c
@@ -47,6 +47,11 @@
        .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)
+};
+
 /*
  * Return the struct http_req_action_kw associated to a keyword.
  */
@@ -63,6 +68,14 @@
 	return action_lookup(&http_res_keywords.list, kw);
 }
 
+/*
+ * Return the struct http_after_res_action_kw associated to a keyword.
+ */
+static 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)
 {
@@ -191,6 +204,71 @@
 	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;
+	struct action_kw *custom = NULL;
+	int cur_arg;
+
+	rule = calloc(1, sizeof(*rule));
+	if (!rule) {
+		ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
+		goto out_err;
+	}
+	rule->from = ACT_F_HTTP_RES;
+
+	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 {
+		action_build_list(&http_after_res_keywords.list, &trash);
+		ha_alert("parsing [%s:%d]: 'http-after-response' expects %s%s, but got '%s'%s.\n",
+			 file, linenum, *trash.area ? ", " : "", trash.area,
+			 args[0], *args[0] ? "" : " (missing argument)");
+		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(rule);
+	return NULL;
+}
+
 /* 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
diff --git a/src/proxy.c b/src/proxy.c
index 2d6fe48..8e32564 100644
--- a/src/proxy.c
+++ b/src/proxy.c
@@ -855,6 +855,7 @@
 	LIST_INIT(&p->acl);
 	LIST_INIT(&p->http_req_rules);
 	LIST_INIT(&p->http_res_rules);
+	LIST_INIT(&p->http_after_res_rules);
 	LIST_INIT(&p->redirect_rules);
 	LIST_INIT(&p->mon_fail_cond);
 	LIST_INIT(&p->switching_rules);
diff --git a/src/vars.c b/src/vars.c
index b2a2b7e..ff6baf5 100644
--- a/src/vars.c
+++ b/src/vars.c
@@ -883,6 +883,14 @@
 
 INITCALL1(STG_REGISTER, http_res_keywords_register, &http_res_kws);
 
+static struct action_kw_list http_after_res_kws = { { }, {
+	{ "set-var",   parse_store, 1 },
+	{ "unset-var", parse_store, 1 },
+	{ /* END */ }
+}};
+
+INITCALL1(STG_REGISTER, http_after_res_keywords_register, &http_after_res_kws);
+
 static struct cfg_kw_list cfg_kws = {{ },{
 	{ CFG_GLOBAL, "tune.vars.global-max-size", vars_max_size_global },
 	{ CFG_GLOBAL, "tune.vars.proc-max-size",   vars_max_size_proc   },