[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.
This commit is contained in:
parent
8001d6162e
commit
b463dfb2de
@ -568,6 +568,7 @@ option tcpka X X X X
|
||||
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 @@ no option transparent
|
||||
"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
|
||||
|
@ -162,6 +162,15 @@ enum {
|
||||
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,
|
||||
|
@ -129,6 +129,7 @@ struct proxy {
|
||||
} 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 @@ struct switching_rule {
|
||||
} 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;
|
||||
|
||||
|
@ -587,6 +587,7 @@ int cfg_parse_listen(const char *file, int linenum, char **args, int inv)
|
||||
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 @@ int cfg_parse_listen(const char *file, int linenum, char **args, int inv)
|
||||
}
|
||||
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;
|
||||
|
@ -648,6 +648,7 @@ void deinit(void)
|
||||
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 @@ void deinit(void)
|
||||
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);
|
||||
|
||||
|
@ -88,6 +88,12 @@ const struct chunk http_200_chunk = {
|
||||
.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 @@ int process_cli(struct session *t)
|
||||
|
||||
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);
|
||||
|
41
tests/test-redirect.cfg
Normal file
41
tests/test-redirect.cfg
Normal file
@ -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
|
Loading…
x
Reference in New Issue
Block a user