From: Willy Tarreau Date: Tue, 11 Jun 2013 14:06:12 +0000 (+0200) Subject: MEDIUM: http: add a new "http-response" ruleset X-Git-Tag: v1.5-dev19~19 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=e365c0b92b87a326b827b6619ee1cbc5d12cc844;p=thirdparty%2Fhaproxy.git 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 1f3bbb9ea4..a0f88273de 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -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 ] | redirect | See also : "stats http-request", section 3.4 about userlists and section 7 about ACL usage. +http-response { allow | deny | add-header | + set-header } [ { if | unless } ] + 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 + and whose value is defined by 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 [
] Add the server name to a request. Use the header string given by
diff --git a/include/proto/proto_http.h b/include/proto/proto_http.h index e789fc19eb..24e358191d 100644 --- a/include/proto/proto_http.h +++ b/include/proto/proto_http.h @@ -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, diff --git a/include/types/proto_http.h b/include/types/proto_http.h index 12e446f730..6190e6c58e 100644 --- a/include/types/proto_http.h +++ b/include/types/proto_http.h @@ -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). */ diff --git a/include/types/proxy.h b/include/types/proxy.h index 273fb8bc53..e6bc75525e 100644 --- a/include/types/proxy.h +++ b/include/types/proxy.h @@ -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) */ diff --git a/src/cfgparse.c b/src/cfgparse.c index 276766bff9..d74dfe068a 100644 --- a/src/cfgparse.c +++ b/src/cfgparse.c @@ -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)) diff --git a/src/proto_http.c b/src/proto_http.c index da9b97cb90..47f413eed4 100644 --- a/src/proto_http.c +++ b/src/proto_http.c @@ -3232,6 +3232,71 @@ http_req_get_intercept_rule(struct proxy *px, struct list *rules, struct session } +/* Executes the http-response rules for session , proxy and + * transaction . 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 . 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 filled with the error message. */ diff --git a/src/proxy.c b/src/proxy.c index 93177740c8..e6720c9f19 100644 --- a/src/proxy.c +++ b/src/proxy.c @@ -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);