[MEDIUM] add support for conditional HTTP redirection

A new "redirect" keyword adds the ability to send an HTTP 301/302/303
redirection to either an absolute location or to a prefix followed by
the original URI. The redirection is conditionned by ACL rules, so it
becomes very easy to move parts of a site to another site using this.

This work was almost entirely done at Exceliance by Emeric Brun.

A test-case has been added in the tests/ directory.
diff --git a/doc/configuration.txt b/doc/configuration.txt
index 220990f..65404f5 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -568,6 +568,7 @@
 option tcplog               X          X         X         X
 [no] option tcpsplice       X          X         X         X
 [no] option transparent     X          X         X         -
+redirect                    -          X         X         X
 redisp                      X          -         X         X  (deprecated)
 redispatch                  X          -         X         X  (deprecated)
 reqadd                      -          X         X         X
@@ -2312,6 +2313,32 @@
             "transparent" option of the "bind" keyword.
 
 
+redirect {location | prefix} <to> [code <code>] {if | unless} <condition>
+  Return an HTTP redirection if/unless a condition is matched
+  May be used in sections :   defaults | frontend | listen | backend
+                                 no    |    yes   |   yes  |   yes
+
+  If/unless the condition is matched, the HTTP request will lead to a redirect
+  response. There are currently two types of redirections : "location" and
+  "prefix". With "location", the exact value in <to> is placed into the HTTP
+  "Location" header. With "prefix", the "Location" header is built from the
+  concatenation of <to> and the URI. It is particularly suited for global site
+  redirections.
+
+  The code is optional. It indicates in <code> which type of HTTP redirection
+  is desired. Only codes 301, 302 and 303 are supported. 302 is used if no code
+  is specified.
+
+  Example: move the login URL only to HTTPS.
+        acl clear      dst_port  80
+        acl secure     dst_port  8080
+        acl login_page url_beg   /login
+        redirect prefix   https://mysite.com  if login_page !secure
+        redirect location http://mysite.com/  if !login_page secure
+
+  See section 2.3 about ACL usage.
+
+
 redisp (deprecated)
 redispatch (deprecated)
   Enable or disable session redistribution in case of connection failure
diff --git a/include/types/proto_http.h b/include/types/proto_http.h
index 885fbc6..5b0914f 100644
--- a/include/types/proto_http.h
+++ b/include/types/proto_http.h
@@ -162,6 +162,15 @@
 	DATA_ST_PX_FIN,
 };
 
+
+
+/* Redirect types (location, prefix, extended ) */
+enum {
+	REDIRECT_TYPE_NONE = 0,         /* no redirection */
+	REDIRECT_TYPE_LOCATION,         /* location redirect */
+	REDIRECT_TYPE_PREFIX,           /* prefix redirect */
+};
+
 /* Known HTTP methods */
 typedef enum {
 	HTTP_METH_NONE = 0,
diff --git a/include/types/proxy.h b/include/types/proxy.h
index 9129861..10a69b5 100644
--- a/include/types/proxy.h
+++ b/include/types/proxy.h
@@ -129,6 +129,7 @@
 	} defbe;
 	struct list acl;                        /* ACL declared on this proxy */
 	struct list block_cond;                 /* early blocking conditions (chained) */
+	struct list redirect_rules;             /* content redirecting rules (chained) */
 	struct list switching_rules;            /* content switching rules (chained) */
 	struct server *srv;			/* known servers */
 	int srv_act, srv_bck;			/* # of servers eligible for LB (UP|!checked) AND (enabled+weight!=0) */
@@ -252,6 +253,15 @@
 	} be;
 };
 
+struct redirect_rule {
+	struct list list;                       /* list linked to from the proxy */
+	struct acl_cond *cond;                  /* acl condition to meet */
+	int type;
+	int rdr_len;
+	char *rdr_str;
+	int code;
+};
+
 extern struct proxy *proxy;
 extern int next_pxid;
 
diff --git a/src/cfgparse.c b/src/cfgparse.c
index e4308bf..8804a67 100644
--- a/src/cfgparse.c
+++ b/src/cfgparse.c
@@ -587,6 +587,7 @@
 		LIST_INIT(&curproxy->pendconns);
 		LIST_INIT(&curproxy->acl);
 		LIST_INIT(&curproxy->block_cond);
+		LIST_INIT(&curproxy->redirect_rules);
 		LIST_INIT(&curproxy->mon_fail_cond);
 		LIST_INIT(&curproxy->switching_rules);
 
@@ -1119,6 +1120,98 @@
 		}
 		LIST_ADDQ(&curproxy->block_cond, &cond->list);
 	}
+	else if (!strcmp(args[0], "redirect")) {
+		int pol = ACL_COND_NONE;
+		struct acl_cond *cond;
+		struct redirect_rule *rule;
+		int cur_arg;
+		int type = REDIRECT_TYPE_NONE;
+		int code = 302;
+		char *destination = NULL;
+
+		cur_arg = 1;
+		while (*(args[cur_arg])) {
+			if (!strcmp(args[cur_arg], "location")) {
+				if (!*args[cur_arg + 1]) {
+					Alert("parsing [%s:%d] : '%s': missing argument for '%s'.\n",
+					      file, linenum, args[0], args[cur_arg]);
+					return -1;
+				}
+
+				type = REDIRECT_TYPE_LOCATION;
+				cur_arg++;
+				destination = args[cur_arg];
+			}
+			else if (!strcmp(args[cur_arg], "prefix")) {
+				if (!*args[cur_arg + 1]) {
+					Alert("parsing [%s:%d] : '%s': missing argument for '%s'.\n",
+					      file, linenum, args[0], args[cur_arg]);
+					return -1;
+				}
+
+				type = REDIRECT_TYPE_PREFIX;
+				cur_arg++;
+				destination = args[cur_arg];
+			}
+			else if (!strcmp(args[cur_arg],"code")) {
+				if (!*args[cur_arg + 1]) {
+					Alert("parsing [%s:%d] : '%s': missing HTTP code.\n",
+					      file, linenum, args[0]);
+					return -1;
+				}
+				cur_arg++;
+				code = atol(args[cur_arg]);
+				if (code < 301 || code > 303) {
+					Alert("parsing [%s:%d] : '%s': unsupported HTTP code '%d'.\n",
+					      file, linenum, args[0], code);
+					return -1;
+				}
+			}
+			else if (!strcmp(args[cur_arg], "if")) {
+				pol = ACL_COND_IF;
+				cur_arg++;
+				break;
+			}
+			else if (!strcmp(args[cur_arg], "unless")) {
+				pol = ACL_COND_UNLESS;
+				cur_arg++;
+				break;
+			}
+			else {
+				Alert("parsing [%s:%d] : '%s' expects 'code', 'prefix' or 'location' (was '%s').\n",
+				      file, linenum, args[0], args[cur_arg]);
+				return -1;
+			}
+			cur_arg++;
+		}
+
+		if (type == REDIRECT_TYPE_NONE) {
+			Alert("parsing [%s:%d] : '%s' expects a redirection type ('prefix' or 'location').\n",
+			      file, linenum, args[0]);
+			return -1;
+		}
+
+		if (pol == ACL_COND_NONE) {
+			Alert("parsing [%s:%d] : '%s' requires either 'if' or 'unless' followed by a condition.\n",
+			      file, linenum, args[0]);
+			return -1;
+		}
+
+		if ((cond = parse_acl_cond((const char **)args + cur_arg, &curproxy->acl, pol)) == NULL) {
+			Alert("parsing [%s:%d] : '%s': error detected while parsing condition.\n",
+			      file, linenum, args[0]);
+			return -1;
+		}
+
+		rule = (struct redirect_rule *)calloc(1, sizeof(*rule));
+		rule->cond = cond;
+		rule->rdr_str = strdup(destination);
+		rule->rdr_len = strlen(destination);
+		rule->type = type;
+		rule->code = code;
+		LIST_INIT(&rule->list);
+		LIST_ADDQ(&curproxy->redirect_rules, &rule->list);
+	}
 	else if (!strcmp(args[0], "use_backend")) {  /* early blocking based on ACLs */
 		int pol = ACL_COND_NONE;
 		struct acl_cond *cond;
diff --git a/src/haproxy.c b/src/haproxy.c
index d52524f..a4e02e0 100644
--- a/src/haproxy.c
+++ b/src/haproxy.c
@@ -648,6 +648,7 @@
 	struct hdr_exp *exp, *expb;
 	struct acl *acl, *aclb;
 	struct switching_rule *rule, *ruleb;
+	struct redirect_rule *rdr, *rdrb;
 	struct uri_auth *uap, *ua = NULL;
 	struct user_auth *user;
 	int i;
@@ -758,6 +759,14 @@
 			free(rule);
 		}
 
+		list_for_each_entry_safe(rdr, rdrb, &p->redirect_rules, list) {
+			LIST_DEL(&rdr->list);
+			prune_acl_cond(rdr->cond);
+			free(rdr->cond);
+			free(rdr->rdr_str);
+			free(rdr);
+		}
+
 		if (p->appsession_name)
 			free(p->appsession_name);
 
diff --git a/src/proto_http.c b/src/proto_http.c
index 066db30..6b7ced2 100644
--- a/src/proto_http.c
+++ b/src/proto_http.c
@@ -88,6 +88,12 @@
 	.len = sizeof(HTTP_200)-1
 };
 
+const char *HTTP_301 =
+	"HTTP/1.0 301 Moved Permantenly\r\n"
+	"Cache-Control: no-cache\r\n"
+	"Connection: close\r\n"
+	"Location: "; /* not terminated since it will be concatenated with the URL */
+
 const char *HTTP_302 =
 	"HTTP/1.0 302 Found\r\n"
 	"Cache-Control: no-cache\r\n"
@@ -1800,9 +1806,90 @@
 
 		do {
 			struct acl_cond *cond;
+			struct redirect_rule *rule;
 			struct proxy *rule_set = t->be;
 			cur_proxy = t->be;
 
+			/* first check whether we have some ACLs set to redirect this request */
+			list_for_each_entry(rule, &cur_proxy->redirect_rules, list) {
+				int ret = acl_exec_cond(rule->cond, cur_proxy, t, txn, ACL_DIR_REQ);
+				if (rule->cond->pol == ACL_COND_UNLESS)
+					ret = !ret;
+
+				if (ret) {
+					struct chunk rdr = { trash, 0 };
+					const char *msg_fmt;
+
+					/* build redirect message */
+					switch(rule->code) {
+						case 303:
+							rdr.len = strlen(HTTP_303);
+							msg_fmt = HTTP_303;
+							break;
+						case 301:
+							rdr.len = strlen(HTTP_301);
+							msg_fmt = HTTP_301;
+							break;
+						case 302:
+						default:
+							rdr.len = strlen(HTTP_302);
+							msg_fmt = HTTP_302;
+							break;
+					}
+
+					if (unlikely(rdr.len > sizeof(trash)))
+						goto return_bad_req;
+					memcpy(rdr.str, msg_fmt, rdr.len);
+
+					switch(rule->type) {
+						case REDIRECT_TYPE_PREFIX: {
+							const char *path;
+							int pathlen;
+
+							path = http_get_path(txn);
+							/* build message using path */
+							if (path) {
+								pathlen = txn->req.sl.rq.u_l + (txn->req.sol+txn->req.sl.rq.u) - path;
+							} else {
+								path = "/";
+								pathlen = 1;
+							}
+
+							if (rdr.len + rule->rdr_len + pathlen > sizeof(trash) - 4)
+								goto return_bad_req;
+
+							/* add prefix */
+							memcpy(rdr.str + rdr.len, rule->rdr_str, rule->rdr_len);
+							rdr.len += rule->rdr_len;
+
+							/* add path */
+							memcpy(rdr.str + rdr.len, path, pathlen);
+							rdr.len += pathlen;
+							break;
+						}
+						case REDIRECT_TYPE_LOCATION:
+						default:
+							if (rdr.len + rule->rdr_len > sizeof(trash) - 4)
+								goto return_bad_req;
+
+							/* add location */
+							memcpy(rdr.str + rdr.len, rule->rdr_str, rule->rdr_len);
+							rdr.len += rule->rdr_len;
+							break;
+					}
+
+					/* add end of headers */
+					memcpy(rdr.str + rdr.len, "\r\n\r\n", 4);
+					rdr.len += 4;
+
+					txn->status = rule->code;
+					/* let's log the request time */
+					t->logs.t_request = tv_ms_elapsed(&t->logs.tv_accept, &now);
+					client_retnclose(t, &rdr);
+					goto return_prx_cond;
+				}
+			}
+
 			/* first check whether we have some ACLs set to block this request */
 			list_for_each_entry(cond, &cur_proxy->block_cond, list) {
 				int ret = acl_exec_cond(cond, cur_proxy, t, txn, ACL_DIR_REQ);
diff --git a/tests/test-redirect.cfg b/tests/test-redirect.cfg
new file mode 100644
index 0000000..efafa4f
--- /dev/null
+++ b/tests/test-redirect.cfg
@@ -0,0 +1,41 @@
+# This is a test configuration.
+# It is used to check the redirect keyword.
+
+global
+	maxconn    400
+        stats timeout 3s
+
+listen  sample1
+        mode       http
+        retries    1
+        option     redispatch
+        timeout    client  1m
+        timeout    connect 5s
+        timeout    server  1m
+        maxconn    400
+        bind       :8000
+
+	acl        url_test1 url_reg test1
+	acl        url_test2 url_reg test2
+	redirect   location /abs/test code 301 if url_test1
+	redirect   prefix   /pfx/test code 302 if url_test2
+	redirect   prefix   /pfx/test code 303 if url_test2
+
+	### unconditional redirection
+	#redirect   location https://example.com/ if TRUE
+
+	### parser must detect invalid syntaxes below
+	#redirect
+	#redirect   blah
+	#redirect   location 
+	#redirect   location /abs/test
+	#redirect   location /abs/test code
+	#redirect   location /abs/test code 300
+	#redirect   location /abs/test code 301
+	#redirect   location /abs/test code 304
+
+        balance    roundrobin
+        server     act1 127.0.0.1:80 weight 10
+        option     httpclose
+	stats      uri /stats
+	stats      refresh 5000ms