[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