MEDIUM: http: add a new "http-response" ruleset

Some actions were clearly missing to process response headers. This
patch adds a new "http-response" ruleset which provides the following
actions :
  - allow : stop evaluating http-response rules
  - deny : stop and reject the response with a 502
  - add-header : add a header in log-format mode
  - set-header : set a header in log-format mode
This commit is contained in:
Willy Tarreau 2013-06-11 16:06:12 +02:00
parent 2b57cb8f30
commit e365c0b92b
7 changed files with 246 additions and 1 deletions

View File

@ -1138,6 +1138,7 @@ http-check disable-on-404 X - X X
http-check expect - - X X
http-check send-state X - X X
http-request - X X X
http-response - X X X
id - X X X
ignore-persist - X X X
log (*) X X X X
@ -2760,6 +2761,52 @@ http-request { allow | deny | tarpit | auth [realm <realm>] | redirect <rule> |
See also : "stats http-request", section 3.4 about userlists and section 7
about ACL usage.
http-response { allow | deny | add-header <name> <fmt> |
set-header <name> <fmt> } [ { if | unless } <condition> ]
Access control for Layer 7 responses
May be used in sections: defaults | frontend | listen | backend
no | yes | yes | yes
The http-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.
The first keyword is the rule's action. Currently supported actions include :
- "allow" : this stops the evaluation of the rules and lets the response
pass the check. No further "http-response" rules are evaluated for the
current section.
- "deny" : this stops the evaluation of the rules and immediately rejects
the response and emits an HTTP 502 error. No further "http-response"
rules are evaluated.
- "add-header" 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.
- "set-header" 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.
There is no limit to the number of http-response statements per instance.
It is important to know that http-reqsponse rules are processed very early in
the HTTP processing, before "reqdel" or "reqrep" rules. That way, headers
added by "add-header"/"set-header" are visible by almost all further ACL
rules.
See also : "http-request", section 3.4 about userlists and section 7 about
ACL usage.
http-send-name-header [<header>]
Add the server name to a request. Use the header string given by <header>

View File

@ -111,6 +111,7 @@ void http_end_txn(struct session *s);
void http_reset_txn(struct session *s);
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);
struct chunk *http_error_message(struct session *s, int msgnum);
struct redirect_rule *http_parse_redirect_rule(const char *file, int line, struct proxy *curproxy,

View File

@ -236,6 +236,7 @@ enum {
HTTP_AUTH_DIGEST,
};
/* actions for "http-request" */
enum {
HTTP_REQ_ACT_UNKNOWN = 0,
HTTP_REQ_ACT_ALLOW,
@ -248,6 +249,16 @@ enum {
HTTP_REQ_ACT_MAX /* must always be last */
};
/* actions for "http-response" */
enum {
HTTP_RES_ACT_UNKNOWN = 0,
HTTP_RES_ACT_ALLOW,
HTTP_RES_ACT_DENY,
HTTP_RES_ACT_ADD_HDR,
HTTP_RES_ACT_SET_HDR,
HTTP_RES_ACT_MAX /* must always be last */
};
/*
* All implemented return codes
*/
@ -360,6 +371,19 @@ struct http_req_rule {
} arg; /* arguments used by some actions */
};
struct http_res_rule {
struct list list;
struct acl_cond *cond; /* acl condition to meet */
unsigned int action; /* HTTP_RES_* */
union {
struct {
char *name; /* header name */
int name_len; /* header name's length */
struct list fmt; /* log-format compatible expression */
} hdr_add; /* args used by "add-header" and "set-header" */
} arg; /* arguments used by some actions */
};
/* This is an HTTP transaction. It contains both a request message and a
* response message (which can be empty).
*/

View File

@ -211,7 +211,8 @@ struct proxy {
char *name; /* default backend name during config parse */
} defbe;
struct list acl; /* ACL declared on this proxy */
struct list http_req_rules; /* HTTP request rules: allow/deny/http-auth */
struct list http_req_rules; /* HTTP request rules: allow/deny/... */
struct list http_res_rules; /* HTTP response rules: allow/deny/... */
struct list block_cond; /* early blocking conditions (chained) */
struct list redirect_rules; /* content redirecting rules (chained) */
struct list switching_rules; /* content switching rules (chained) */

View File

@ -2635,6 +2635,7 @@ int cfg_parse_listen(const char *file, int linenum, char **args, int kwm)
!LIST_PREV(&curproxy->http_req_rules, struct http_req_rule *, list)->cond &&
(LIST_PREV(&curproxy->http_req_rules, struct http_req_rule *, list)->action == HTTP_REQ_ACT_ALLOW ||
LIST_PREV(&curproxy->http_req_rules, struct http_req_rule *, list)->action == HTTP_REQ_ACT_DENY ||
LIST_PREV(&curproxy->http_req_rules, struct http_req_rule *, list)->action == HTTP_REQ_ACT_REDIR ||
LIST_PREV(&curproxy->http_req_rules, struct http_req_rule *, list)->action == HTTP_REQ_ACT_AUTH)) {
Warning("parsing [%s:%d]: previous '%s' action is final and has no condition attached, further entries are NOOP.\n",
file, linenum, args[0]);
@ -2654,6 +2655,37 @@ int cfg_parse_listen(const char *file, int linenum, char **args, int kwm)
LIST_ADDQ(&curproxy->http_req_rules, &rule->list);
}
else if (!strcmp(args[0], "http-response")) { /* response access control */
struct http_res_rule *rule;
if (curproxy == &defproxy) {
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_res_rules) &&
!LIST_PREV(&curproxy->http_res_rules, struct http_res_rule *, list)->cond &&
(LIST_PREV(&curproxy->http_res_rules, struct http_res_rule *, list)->action == HTTP_RES_ACT_ALLOW ||
LIST_PREV(&curproxy->http_res_rules, struct http_res_rule *, list)->action == HTTP_RES_ACT_DENY)) {
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_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_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))

View File

@ -3232,6 +3232,71 @@ http_req_get_intercept_rule(struct proxy *px, struct list *rules, struct session
}
/* Executes the http-response rules <rules> for session <s>, proxy <px> and
* transaction <txn>. Returns the first rule that prevents further processing
* of the response (deny, ...) or NULL if it executed all rules or stopped
* on an allow. It may set the TX_SVDENY on txn->flags if it encounters a deny
* rule.
*/
static struct http_res_rule *
http_res_get_intercept_rule(struct proxy *px, struct list *rules, struct session *s, struct http_txn *txn)
{
struct http_res_rule *rule;
struct hdr_ctx ctx;
list_for_each_entry(rule, rules, list) {
if (rule->action >= HTTP_RES_ACT_MAX)
continue;
/* check optional condition */
if (rule->cond) {
int ret;
ret = acl_exec_cond(rule->cond, px, s, txn, SMP_OPT_DIR_RES|SMP_OPT_FINAL);
ret = acl_pass(ret);
if (rule->cond->pol == ACL_COND_UNLESS)
ret = !ret;
if (!ret) /* condition not matched */
continue;
}
switch (rule->action) {
case HTTP_RES_ACT_ALLOW:
return NULL; /* "allow" rules are OK */
case HTTP_RES_ACT_DENY:
txn->flags |= TX_SVDENY;
return rule;
case HTTP_RES_ACT_SET_HDR:
ctx.idx = 0;
/* remove all occurrences of the header */
while (http_find_header2(rule->arg.hdr_add.name, rule->arg.hdr_add.name_len,
txn->rsp.chn->buf->p, &txn->hdr_idx, &ctx)) {
http_remove_header2(&txn->rsp, &txn->hdr_idx, &ctx);
}
/* now fall through to header addition */
case HTTP_RES_ACT_ADD_HDR:
chunk_printf(&trash, "%s: ", rule->arg.hdr_add.name);
memcpy(trash.str, rule->arg.hdr_add.name, rule->arg.hdr_add.name_len);
trash.len = rule->arg.hdr_add.name_len;
trash.str[trash.len++] = ':';
trash.str[trash.len++] = ' ';
trash.len += build_logline(s, trash.str + trash.len, trash.size - trash.len, &rule->arg.hdr_add.fmt);
http_header_add_tail2(&txn->rsp, &txn->hdr_idx, trash.str, trash.len);
break;
}
}
/* we reached the end of the rules, nothing to report */
return NULL;
}
/* Perform an HTTP redirect based on the information in <rule>. The function
* returns non-zero on success, or zero in case of a, irrecoverable error such
* as too large a request to build a valid response.
@ -5494,6 +5559,7 @@ int http_process_res_common(struct session *t, struct channel *rep, int an_bit,
struct http_msg *msg = &txn->rsp;
struct proxy *cur_proxy;
struct cond_wordlist *wl;
struct http_res_rule *http_res_last_rule = NULL;
DPRINTF(stderr,"[%u] %s: session=%p b=%p, exp(r,w)=%u,%u bf=%08x bh=%d analysers=%02x\n",
now_ms, __FUNCTION__,
@ -5591,6 +5657,10 @@ int http_process_res_common(struct session *t, struct channel *rep, int an_bit,
while (1) {
struct proxy *rule_set = cur_proxy;
/* evaluate http-response rules */
if (!http_res_last_rule)
http_res_last_rule = http_res_get_intercept_rule(cur_proxy, &cur_proxy->http_res_rules, t, txn);
/* try headers filters */
if (rule_set->rsp_exp != NULL) {
if (apply_filters_to_response(t, rep, rule_set) < 0) {
@ -8283,6 +8353,7 @@ void free_http_req_rules(struct list *r) {
}
}
/* parse an "http-request" rule */
struct http_req_rule *parse_http_req_cond(const char **args, const char *file, int linenum, struct proxy *proxy)
{
struct http_req_rule *rule;
@ -8384,6 +8455,74 @@ struct http_req_rule *parse_http_req_cond(const char **args, const char *file, i
return NULL;
}
/* parse an "http-respose" rule */
struct http_res_rule *parse_http_res_cond(const char **args, const char *file, int linenum, struct proxy *proxy)
{
struct http_res_rule *rule;
int cur_arg;
rule = calloc(1, sizeof(*rule));
if (!rule) {
Alert("parsing [%s:%d]: out of memory.\n", file, linenum);
goto out_err;
}
if (!strcmp(args[0], "allow")) {
rule->action = HTTP_RES_ACT_ALLOW;
cur_arg = 1;
} else if (!strcmp(args[0], "deny")) {
rule->action = HTTP_RES_ACT_DENY;
cur_arg = 1;
} else if (strcmp(args[0], "add-header") == 0 || strcmp(args[0], "set-header") == 0) {
rule->action = *args[0] == 'a' ? HTTP_RES_ACT_ADD_HDR : HTTP_RES_ACT_SET_HDR;
cur_arg = 1;
if (!*args[cur_arg] || !*args[cur_arg+1] ||
(*args[cur_arg+2] && strcmp(args[cur_arg+2], "if") != 0 && strcmp(args[cur_arg+2], "unless") != 0)) {
Alert("parsing [%s:%d]: 'http-response %s' expects exactly 2 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);
proxy->conf.args.ctx = ARGC_HDR;
parse_logformat_string(args[cur_arg + 1], proxy, &rule->arg.hdr_add.fmt, 0,
(proxy->cap & PR_CAP_BE) ? SMP_VAL_BE_HRS_HDR : SMP_VAL_FE_HRS_HDR);
cur_arg += 2;
} else {
Alert("parsing [%s:%d]: 'http-response' expects 'allow', 'deny', 'redirect', 'add-header', 'set-header', but got '%s'%s.\n",
file, linenum, 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, args+cur_arg, &errmsg)) == NULL) {
Alert("parsing [%s:%d] : error detected while parsing an 'http-response %s' condition : %s.\n",
file, linenum, args[0], errmsg);
free(errmsg);
goto out_err;
}
rule->cond = cond;
}
else if (*args[cur_arg]) {
Alert("parsing [%s:%d]: 'http-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.
*/

View File

@ -433,6 +433,7 @@ void init_new_proxy(struct proxy *p)
LIST_INIT(&p->pendconns);
LIST_INIT(&p->acl);
LIST_INIT(&p->http_req_rules);
LIST_INIT(&p->http_res_rules);
LIST_INIT(&p->block_cond);
LIST_INIT(&p->redirect_rules);
LIST_INIT(&p->mon_fail_cond);