MEDIUM: http: add actions "replace-header" and "replace-values" in http-req/resp

This patch adds two new actions to http-request and http-response rulesets :
  - replace-header : replace a whole header line, suited for headers
                     which might contain commas
  - replace-value  : replace a single header value, suited for headers
                     defined as lists.

The match consists in a regex, and the replacement string takes a log-format
and supports back-references.
diff --git a/doc/configuration.txt b/doc/configuration.txt
index 51798b4..370fd53 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -2878,6 +2878,8 @@
 http-request { allow | deny | tarpit | auth [realm <realm>] | redirect <rule> |
               add-header <name> <fmt> | set-header <name> <fmt> |
               del-header <name> | set-nice <nice> | set-log-level <level> |
+              replace-header <name> <match-regex> <replace-fmt> |
+              replace-value <name> <match-regex> <replace-fmt> |
               set-tos <tos> | set-mark <mark> |
               add-acl(<file name>) <key fmt> |
               del-acl(<file name>) <key fmt> |
@@ -2945,6 +2947,47 @@
     - "del-header" removes all HTTP header fields whose name is specified in
       <name>.
 
+    - "replace-header" matches the regular expression in all occurrences of
+      header field <name> according to <match-regex>, and replaces them with
+      the <replace-fmt> argument. Format characters are allowed in replace-fmt
+      and work like in <fmt> arguments in "add-header". The match is only
+      case-sensitive. It is important to understand that this action only
+      considers whole header lines, regardless of the number of values they
+      may contain. This usage is suited to headers naturally containing commas
+      in their value, such as If-Modified-Since and so on.
+
+      Example:
+
+        http-request replace-header Cookie foo=([^;]*);(.*) foo=\1;ip=%bi;\2
+
+      applied to:
+
+        Cookie: foo=foobar; expires=Tue, 14-Jun-2016 01:40:45 GMT;
+
+      outputs:
+
+        Cookie: foo=foobar;ip=192.168.1.20; expires=Tue, 14-Jun-2016 01:40:45 GMT;
+
+      assuming the backend IP is 192.168.1.20
+
+    - "replace-value" works like "replace-header" except that it matches the
+      regex against every comma-delimited value of the header field <name>
+      instead of the entire header. This is suited for all headers which are
+      allowed to carry more than one value. An example could be the Accept
+      header.
+
+      Example:
+
+        http-request replace-value X-Forwarded-For ^192\.168\.(.*)$ 172.16.\1
+
+      applied to:
+
+        X-Forwarded-For: 192.168.10.1, 192.168.13.24, 10.0.0.37
+
+      outputs:
+
+        X-Forwarded-For: 172.16.10.1, 172.16.13.24, 10.0.0.37
+
     - "set-nice" sets the "nice" factor of the current request being processed.
       It only has effect against the other requests being processed at the same
       time. The default value is 0, unless altered by the "nice" setting on the
@@ -3069,6 +3112,8 @@
 
 http-response { allow | deny | add-header <name> <fmt> | set-nice <nice> |
                 set-header <name> <fmt> | del-header <name> |
+                replace-header <name> <regex-match> <replace-fmt> |
+                replace-value <name> <regex-match> <replace-fmt> |
                 set-log-level <level> | set-mark <mark> | set-tos <tos> |
                 add-acl(<file name>) <key fmt> |
                 del-acl(<file name>) <key fmt> |
@@ -3113,6 +3158,47 @@
     - "del-header" removes all HTTP header fields whose name is specified in
       <name>.
 
+    - "replace-header" matches the regular expression in all occurrences of
+      header field <name> according to <match-regex>, and replaces them with
+      the <replace-fmt> argument. Format characters are allowed in replace-fmt
+      and work like in <fmt> arguments in "add-header". The match is only
+      case-sensitive. It is important to understand that this action only
+      considers whole header lines, regardless of the number of values they
+      may contain. This usage is suited to headers naturally containing commas
+      in their value, such as Set-Cookie, Expires and so on.
+
+      Example:
+
+        http-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.
+
+    - "replace-value" works like "replace-header" except that it matches the
+      regex against every comma-delimited value of the header field <name>
+      instead of the entire header. This is suited for all headers which are
+      allowed to carry more than one value. An example could be the Accept
+      header.
+
+      Example:
+
+        http-response replace-value Cache-control ^public$ private
+
+      applied to:
+
+        Cache-Control: max-age=3600, public
+
+      outputs:
+
+        Cache-Control: max-age=3600, private
+
     - "set-nice" sets the "nice" factor of the current request being processed.
       It only has effect against the other requests being processed at the same
       time. The default value is 0, unless altered by the "nice" setting on the
diff --git a/include/proto/proto_http.h b/include/proto/proto_http.h
index 6370e2d..e898ca8 100644
--- a/include/proto/proto_http.h
+++ b/include/proto/proto_http.h
@@ -116,6 +116,7 @@
 struct http_req_rule *parse_http_req_cond(const char **args, const char *file, int linenum, struct proxy *proxy);
 struct http_res_rule *parse_http_res_cond(const char **args, const char *file, int linenum, struct proxy *proxy);
 void free_http_req_rules(struct list *r);
+void free_http_res_rules(struct list *r);
 struct chunk *http_error_message(struct session *s, int msgnum);
 struct redirect_rule *http_parse_redirect_rule(const char *file, int linenum, struct proxy *curproxy,
                                                const char **args, char **errmsg, int use_fmt);
diff --git a/include/types/proto_http.h b/include/types/proto_http.h
index f5dd9a3..ff196a0 100644
--- a/include/types/proto_http.h
+++ b/include/types/proto_http.h
@@ -247,6 +247,8 @@
 	HTTP_REQ_ACT_ADD_HDR,
 	HTTP_REQ_ACT_SET_HDR,
 	HTTP_REQ_ACT_DEL_HDR,
+	HTTP_REQ_ACT_REPLACE_HDR,
+	HTTP_REQ_ACT_REPLACE_VAL,
 	HTTP_REQ_ACT_REDIR,
 	HTTP_REQ_ACT_SET_NICE,
 	HTTP_REQ_ACT_SET_LOGL,
@@ -267,6 +269,8 @@
 	HTTP_RES_ACT_ALLOW,
 	HTTP_RES_ACT_DENY,
 	HTTP_RES_ACT_ADD_HDR,
+	HTTP_RES_ACT_REPLACE_HDR,
+	HTTP_RES_ACT_REPLACE_VAL,
 	HTTP_RES_ACT_SET_HDR,
 	HTTP_RES_ACT_DEL_HDR,
 	HTTP_RES_ACT_SET_NICE,
@@ -415,6 +419,7 @@
 			char *name;            /* header name */
 			int name_len;          /* header name's length */
 			struct list fmt;       /* log-format compatible expression */
+			regex_t* re;           /* used by replace-header and replace-value */
 		} hdr_add;                     /* args used by "add-header" and "set-header" */
 		struct redirect_rule *redir;   /* redirect rule or "http-request redirect" */
 		int nice;                      /* nice value for HTTP_REQ_ACT_SET_NICE */
@@ -440,6 +445,7 @@
 			char *name;            /* header name */
 			int name_len;          /* header name's length */
 			struct list fmt;       /* log-format compatible expression */
+			regex_t* re;           /* used by replace-header and replace-value */
 		} hdr_add;                     /* args used by "add-header" and "set-header" */
 		int nice;                      /* nice value for HTTP_RES_ACT_SET_NICE */
 		int loglevel;                  /* log-level value for HTTP_RES_ACT_SET_LOGL */
diff --git a/src/haproxy.c b/src/haproxy.c
index c4442de..cd42b34 100644
--- a/src/haproxy.c
+++ b/src/haproxy.c
@@ -1203,6 +1203,7 @@
 		free(p->fwdfor_hdr_name);
 
 		free_http_req_rules(&p->http_req_rules);
+		free_http_res_rules(&p->http_res_rules);
 		free(p->task);
 
 		pool_destroy2(p->req_cap_pool);
diff --git a/src/proto_http.c b/src/proto_http.c
index 48dbc43..568b91b 100644
--- a/src/proto_http.c
+++ b/src/proto_http.c
@@ -3176,6 +3176,126 @@
 #endif
 }
 
+/* Returns the number of characters written to destination,
+ * -1 on internal error and -2 if no replacement took place.
+ */
+static int http_replace_header(regex_t* re, char* dst, uint dst_size, char* val,
+                               const char* rep_str)
+{
+	if (regexec(re, val, MAX_MATCH, pmatch, 0))
+		return -2;
+
+	return exp_replace(dst, dst_size, val, rep_str, pmatch);
+}
+
+/* Returns the number of characters written to destination,
+ * -1 on internal error and -2 if no replacement took place.
+ */
+static int http_replace_value(regex_t* re, char* dst, uint dst_size, char* val, char delim,
+                              const char* rep_str)
+{
+	char* p = val;
+	char* dst_end = dst + dst_size;
+	char* dst_p = dst;
+
+	for (;;) {
+		char *p_delim;
+		const char* tok_end;
+
+		if ((p_delim = (char*)strchr(p, delim))) {
+			*p_delim = 0;
+			tok_end = p_delim;
+		} else {
+			tok_end = p + strlen(p);
+		}
+
+		if (regexec(re, p, MAX_MATCH, pmatch, 0) == 0) {
+			int replace_n = exp_replace(dst_p, dst_end - dst_p, p, rep_str, pmatch);
+
+			if (replace_n < 0)
+				return -1;
+
+			dst_p += replace_n;
+		} else {
+			uint len = tok_end - p;
+
+			if (dst_p + len >= dst_end)
+				return -1;
+
+			memcpy(dst_p, p, len);
+			dst_p += len;
+		}
+
+		if (dst_p >= dst_end)
+			return -1;
+
+		if (p_delim) {
+			*p_delim = delim;
+			*dst_p++ = delim;
+			p = p_delim + 1;
+		} else {
+			*dst_p = 0;
+			break;
+		}
+	}
+
+	return dst_p - dst;
+}
+
+static int http_transform_header(struct session* s, struct http_msg *msg, const char* name, uint name_len,
+                                 char* buf, struct hdr_idx* idx, struct list *fmt, regex_t* re,
+                                 struct hdr_ctx* ctx, int action)
+{
+	ctx->idx = 0;
+
+	while (http_find_full_header2(name, name_len, buf, idx, ctx)) {
+		struct hdr_idx_elem *hdr = idx->v + ctx->idx;
+		int delta;
+		char* val = (char*)ctx->line + name_len + 2;
+		char* val_end = (char*)ctx->line + hdr->len;
+		char save_val_end = *val_end;
+		char* reg_dst_buf;
+		uint reg_dst_buf_size;
+		int n_replaced;
+
+		*val_end = 0;
+		trash.len = build_logline(s, trash.str, trash.size, fmt);
+
+		if (trash.len >= trash.size - 1)
+			return -1;
+
+		reg_dst_buf = trash.str + trash.len + 1;
+		reg_dst_buf_size = trash.size - trash.len - 1;
+
+		switch (action) {
+		case HTTP_REQ_ACT_REPLACE_VAL:
+		case HTTP_RES_ACT_REPLACE_VAL:
+			n_replaced = http_replace_value(re, reg_dst_buf, reg_dst_buf_size, val, ',', trash.str);
+			break;
+		case HTTP_REQ_ACT_REPLACE_HDR:
+		case HTTP_RES_ACT_REPLACE_HDR:
+			n_replaced = http_replace_header(re, reg_dst_buf, reg_dst_buf_size, val, trash.str);
+			break;
+		default: /* impossible */
+			return -1;
+		}
+
+		*val_end = save_val_end;
+
+		switch (n_replaced) {
+		case -1: return -1;
+		case -2: continue;
+		}
+
+		delta = buffer_replace2(msg->chn->buf, val, val_end, reg_dst_buf, n_replaced);
+
+		hdr->len += delta;
+		http_msg_move_end(msg, delta);
+	}
+
+	return 0;
+}
+
 /* Executes the http-request rules <rules> for session <s>, proxy <px> and
  * transaction <txn>. Returns the verdict of the first rule that prevents
  * further processing of the request (auth, deny, ...), and defaults to
@@ -3265,6 +3385,14 @@
 			s->logs.level = rule->arg.loglevel;
 			break;
 
+		case HTTP_REQ_ACT_REPLACE_HDR:
+		case HTTP_REQ_ACT_REPLACE_VAL:
+			if (http_transform_header(s, &txn->req, rule->arg.hdr_add.name, rule->arg.hdr_add.name_len,
+			                          txn->req.chn->buf->p, &txn->hdr_idx, &rule->arg.hdr_add.fmt,
+			                          rule->arg.hdr_add.re, &ctx, rule->action))
+				return HTTP_RULE_RES_BADREQ;
+			break;
+
 		case HTTP_REQ_ACT_DEL_HDR:
 		case HTTP_REQ_ACT_SET_HDR:
 			ctx.idx = 0;
@@ -3446,6 +3574,14 @@
 			s->logs.level = rule->arg.loglevel;
 			break;
 
+		case HTTP_RES_ACT_REPLACE_HDR:
+		case HTTP_RES_ACT_REPLACE_VAL:
+			if (http_transform_header(s, &txn->rsp, rule->arg.hdr_add.name, rule->arg.hdr_add.name_len,
+			                          txn->rsp.chn->buf->p, &txn->hdr_idx, &rule->arg.hdr_add.fmt,
+			                          rule->arg.hdr_add.re, &ctx, rule->action))
+				return NULL; /* note: we should report an error here */
+			break;
+
 		case HTTP_RES_ACT_DEL_HDR:
 		case HTTP_RES_ACT_SET_HDR:
 			ctx.idx = 0;
@@ -8759,7 +8895,27 @@
 	s->rep->analyse_exp = TICK_ETERNITY;
 }
 
-void free_http_req_rules(struct list *r) {
+static inline void free_regex(regex_t* re)
+{
+	if (re) {
+		regfree(re);
+		free(re);
+	}
+}
+
+void free_http_res_rules(struct list *r)
+{
+	struct http_res_rule *tr, *pr;
+
+	list_for_each_entry_safe(pr, tr, r, list) {
+		LIST_DEL(&pr->list);
+		free_regex(pr->arg.hdr_add.re);
+		free(pr);
+	}
+}
+
+void free_http_req_rules(struct list *r)
+{
 	struct http_req_rule *tr, *pr;
 
 	list_for_each_entry_safe(pr, tr, r, list) {
@@ -8767,6 +8923,7 @@
 		if (pr->action == HTTP_REQ_ACT_AUTH)
 			free(pr->arg.auth.realm);
 
+		free_regex(pr->arg.hdr_add.re);
 		free(pr);
 	}
 }
@@ -8909,6 +9066,41 @@
 		proxy->conf.lfs_file = strdup(proxy->conf.args.file);
 		proxy->conf.lfs_line = proxy->conf.args.line;
 		cur_arg += 2;
+	} else if (strcmp(args[0], "replace-header") == 0 || strcmp(args[0], "replace-val") == 0) {
+		rule->action = *args[8] == 'h' ? HTTP_REQ_ACT_REPLACE_HDR : HTTP_REQ_ACT_REPLACE_VAL;
+		cur_arg = 1;
+
+		if (!*args[cur_arg] || !*args[cur_arg+1] || !*args[cur_arg+2] ||
+		    (*args[cur_arg+3] && strcmp(args[cur_arg+2], "if") != 0 && strcmp(args[cur_arg+2], "unless") != 0)) {
+			Alert("parsing [%s:%d]: 'http-request %s' expects exactly 3 arguments.\n",
+			      file, linenum, args[0]);
+			goto out_err;
+		}
+
+		rule->arg.hdr_add.name = strdup(args[cur_arg]);
+		rule->arg.hdr_add.name_len = strlen(rule->arg.hdr_add.name);
+		LIST_INIT(&rule->arg.hdr_add.fmt);
+
+		if (!(rule->arg.hdr_add.re = calloc(1, sizeof(*rule->arg.hdr_add.re)))) {
+			Alert("parsing [%s:%d]: out of memory.\n", file, linenum);
+			goto out_err;
+		}
+
+		if (regcomp(rule->arg.hdr_add.re, args[cur_arg + 1], REG_EXTENDED)) {
+			Alert("parsing [%s:%d] : '%s' : bad regular expression.\n", file, linenum,
+			      args[cur_arg + 1]);
+			goto out_err;
+		}
+
+		proxy->conf.args.ctx = ARGC_HRQ;
+		parse_logformat_string(args[cur_arg + 2], proxy, &rule->arg.hdr_add.fmt, LOG_OPT_HTTP,
+				       (proxy->cap & PR_CAP_FE) ? SMP_VAL_FE_HRQ_HDR : SMP_VAL_BE_HRQ_HDR,
+				       file, linenum);
+
+		free(proxy->conf.lfs_file);
+		proxy->conf.lfs_file = strdup(proxy->conf.args.file);
+		proxy->conf.lfs_line = proxy->conf.args.line;
+		cur_arg += 3;
 	} else if (strcmp(args[0], "del-header") == 0) {
 		rule->action = HTTP_REQ_ACT_DEL_HDR;
 		cur_arg = 1;
@@ -9075,7 +9267,7 @@
 			goto out_err;
 		}
 	} else {
-		Alert("parsing [%s:%d]: 'http-request' expects 'allow', 'deny', 'auth', 'redirect', 'tarpit', 'add-header', 'set-header', 'set-nice', 'set-tos', 'set-mark', 'set-log-level', 'add-acl', 'del-acl', 'del-map', 'set-map', but got '%s'%s.\n",
+		Alert("parsing [%s:%d]: 'http-request' expects 'allow', 'deny', 'auth', 'redirect', 'tarpit', 'add-header', 'set-header', 'replace-header', 'replace-value', 'set-nice', 'set-tos', 'set-mark', 'set-log-level', 'add-acl', 'del-acl', 'del-map', 'set-map', but got '%s'%s.\n",
 		      file, linenum, args[0], *args[0] ? "" : " (missing argument)");
 		goto out_err;
 	}
@@ -9228,6 +9420,41 @@
 		proxy->conf.lfs_file = strdup(proxy->conf.args.file);
 		proxy->conf.lfs_line = proxy->conf.args.line;
 		cur_arg += 2;
+	} else if (strcmp(args[0], "replace-header") == 0 || strcmp(args[0], "replace-value") == 0) {
+		rule->action = *args[8] == 'h' ? HTTP_RES_ACT_REPLACE_HDR : HTTP_RES_ACT_REPLACE_VAL;
+		cur_arg = 1;
+
+		if (!*args[cur_arg] || !*args[cur_arg+1] || !*args[cur_arg+2] ||
+		    (*args[cur_arg+3] && strcmp(args[cur_arg+2], "if") != 0 && strcmp(args[cur_arg+2], "unless") != 0)) {
+			Alert("parsing [%s:%d]: 'http-request %s' expects exactly 3 arguments.\n",
+			      file, linenum, args[0]);
+			goto out_err;
+		}
+
+		rule->arg.hdr_add.name = strdup(args[cur_arg]);
+		rule->arg.hdr_add.name_len = strlen(rule->arg.hdr_add.name);
+		LIST_INIT(&rule->arg.hdr_add.fmt);
+
+		if (!(rule->arg.hdr_add.re = calloc(1, sizeof(*rule->arg.hdr_add.re)))) {
+			Alert("parsing [%s:%d]: out of memory.\n", file, linenum);
+			goto out_err;
+		}
+
+		if (regcomp(rule->arg.hdr_add.re, args[cur_arg + 1], REG_EXTENDED)) {
+			Alert("parsing [%s:%d] : '%s' : bad regular expression.\n", file, linenum,
+			      args[cur_arg + 1]);
+			goto out_err;
+		}
+
+		proxy->conf.args.ctx = ARGC_HRQ;
+		parse_logformat_string(args[cur_arg + 2], proxy, &rule->arg.hdr_add.fmt, LOG_OPT_HTTP,
+				       (proxy->cap & PR_CAP_BE) ? SMP_VAL_BE_HRS_HDR : SMP_VAL_FE_HRS_HDR,
+				       file, linenum);
+
+		free(proxy->conf.lfs_file);
+		proxy->conf.lfs_file = strdup(proxy->conf.args.file);
+		proxy->conf.lfs_line = proxy->conf.args.line;
+		cur_arg += 3;
 	} else if (strcmp(args[0], "del-header") == 0) {
 		rule->action = HTTP_RES_ACT_DEL_HDR;
 		cur_arg = 1;
@@ -9378,7 +9605,7 @@
 			goto out_err;
 		}
 	} else {
-		Alert("parsing [%s:%d]: 'http-response' expects 'allow', 'deny', 'redirect', 'add-header', 'del-header', 'set-header', 'set-nice', 'set-tos', 'set-mark', 'set-log-level', 'del-acl', 'add-acl', 'del-map', 'set-map', but got '%s'%s.\n",
+		Alert("parsing [%s:%d]: 'http-response' expects 'allow', 'deny', 'redirect', 'add-header', 'del-header', 'set-header', 'replace-header', 'replace-value', 'set-nice', 'set-tos', 'set-mark', 'set-log-level', 'del-acl', 'add-acl', 'del-map', 'set-map', but got '%s'%s.\n",
 		      file, linenum, args[0], *args[0] ? "" : " (missing argument)");
 		goto out_err;
 	}