]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MEDIUM: http: add a new "http-response" ruleset
authorWilly Tarreau <w@1wt.eu>
Tue, 11 Jun 2013 14:06:12 +0000 (16:06 +0200)
committerWilly Tarreau <w@1wt.eu>
Tue, 11 Jun 2013 14:06:12 +0000 (16:06 +0200)
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

doc/configuration.txt
include/proto/proto_http.h
include/types/proto_http.h
include/types/proxy.h
src/cfgparse.c
src/proto_http.c
src/proxy.c

index 1f3bbb9ea479d288ce2ed1226103ec797fb77e44..a0f88273dea2f5ed29669f664ce876c9934a3923 100644 (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>
 
index e789fc19eb740f593e7490eb745d19232fd0fc77..24e358191d2fea09b39621e441bca21409886c97 100644 (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,
index 12e446f73066e7a72f215e0e2fa1203dac0036bf..6190e6c58e6cf27d8780bcda41aaa8b4d96e49b7 100644 (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).
  */
index 273fb8bc535379af372141531ce9248493494e3f..e6bc75525eb7e30ac43b71eb9ed74813c5f2930d 100644 (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) */
index 276766bff9eb848dd8c75bf33daae12efe6f6ba7..d74dfe068aa571d63269cd76f499f5c3e8a9bb0e 100644 (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))
index da9b97cb90e2fbbe24045deb2015ca5373ddf4b9..47f413eed437733f3e319c60c21c58bd5cb77183 100644 (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.
  */
index 93177740c86a3a5a9e4d365a74a5b4ab0af41392..e6720c9f19813412c4b096b1bfe94bd7df100c83 100644 (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);