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
diff --git a/doc/configuration.txt b/doc/configuration.txt
index 1f3bbb9..a0f8827 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -1138,6 +1138,7 @@
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 @@
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>
diff --git a/include/proto/proto_http.h b/include/proto/proto_http.h
index e789fc1..24e3581 100644
--- a/include/proto/proto_http.h
+++ b/include/proto/proto_http.h
@@ -111,6 +111,7 @@
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,
diff --git a/include/types/proto_http.h b/include/types/proto_http.h
index 12e446f..6190e6c 100644
--- a/include/types/proto_http.h
+++ b/include/types/proto_http.h
@@ -236,6 +236,7 @@
HTTP_AUTH_DIGEST,
};
+/* actions for "http-request" */
enum {
HTTP_REQ_ACT_UNKNOWN = 0,
HTTP_REQ_ACT_ALLOW,
@@ -248,6 +249,16 @@
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 @@
} 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).
*/
diff --git a/include/types/proxy.h b/include/types/proxy.h
index 273fb8b..e6bc755 100644
--- a/include/types/proxy.h
+++ b/include/types/proxy.h
@@ -211,7 +211,8 @@
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) */
diff --git a/src/cfgparse.c b/src/cfgparse.c
index 276766b..d74dfe0 100644
--- a/src/cfgparse.c
+++ b/src/cfgparse.c
@@ -2635,6 +2635,7 @@
!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 @@
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))
diff --git a/src/proto_http.c b/src/proto_http.c
index da9b97c..47f413e 100644
--- a/src/proto_http.c
+++ b/src/proto_http.c
@@ -3232,6 +3232,71 @@
}
+/* 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 @@
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 @@
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 @@
}
}
+/* 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 @@
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.
*/
diff --git a/src/proxy.c b/src/proxy.c
index 9317774..e6720c9 100644
--- a/src/proxy.c
+++ b/src/proxy.c
@@ -433,6 +433,7 @@
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);