]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MEDIUM: http: add actions "replace-header" and "replace-values" in http-req/resp
authorSasha Pachev <sasha@asksasha.com>
Mon, 16 Jun 2014 18:05:59 +0000 (12:05 -0600)
committerWilly Tarreau <w@1wt.eu>
Tue, 17 Jun 2014 16:34:32 +0000 (18:34 +0200)
This patch adds two new actions to http-request and http-response rulesets :
  - replace-header : replace a whole header line, suited for headers
                     which might contain commas
  - replace-value  : replace a single header value, suited for headers
                     defined as lists.

The match consists in a regex, and the replacement string takes a log-format
and supports back-references.

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

index 51798b4f966da996da4ccdf44d6963f258904141..370fd53e531c037c1aa135abaccb0fd42a39c0c4 100644 (file)
@@ -2878,6 +2878,8 @@ http-check send-state
 http-request { allow | deny | tarpit | auth [realm <realm>] | redirect <rule> |
               add-header <name> <fmt> | set-header <name> <fmt> |
               del-header <name> | set-nice <nice> | set-log-level <level> |
+              replace-header <name> <match-regex> <replace-fmt> |
+              replace-value <name> <match-regex> <replace-fmt> |
               set-tos <tos> | set-mark <mark> |
               add-acl(<file name>) <key fmt> |
               del-acl(<file name>) <key fmt> |
@@ -2945,6 +2947,47 @@ http-request { allow | deny | tarpit | auth [realm <realm>] | redirect <rule> |
     - "del-header" removes all HTTP header fields whose name is specified in
       <name>.
 
+    - "replace-header" matches the regular expression in all occurrences of
+      header field <name> according to <match-regex>, and replaces them with
+      the <replace-fmt> argument. Format characters are allowed in replace-fmt
+      and work like in <fmt> arguments in "add-header". The match is only
+      case-sensitive. It is important to understand that this action only
+      considers whole header lines, regardless of the number of values they
+      may contain. This usage is suited to headers naturally containing commas
+      in their value, such as If-Modified-Since and so on.
+
+      Example:
+
+        http-request replace-header Cookie foo=([^;]*);(.*) foo=\1;ip=%bi;\2
+
+      applied to:
+
+        Cookie: foo=foobar; expires=Tue, 14-Jun-2016 01:40:45 GMT;
+
+      outputs:
+
+        Cookie: foo=foobar;ip=192.168.1.20; expires=Tue, 14-Jun-2016 01:40:45 GMT;
+
+      assuming the backend IP is 192.168.1.20
+
+    - "replace-value" works like "replace-header" except that it matches the
+      regex against every comma-delimited value of the header field <name>
+      instead of the entire header. This is suited for all headers which are
+      allowed to carry more than one value. An example could be the Accept
+      header.
+
+      Example:
+
+        http-request replace-value X-Forwarded-For ^192\.168\.(.*)$ 172.16.\1
+
+      applied to:
+
+        X-Forwarded-For: 192.168.10.1, 192.168.13.24, 10.0.0.37
+
+      outputs:
+
+        X-Forwarded-For: 172.16.10.1, 172.16.13.24, 10.0.0.37
+
     - "set-nice" sets the "nice" factor of the current request being processed.
       It only has effect against the other requests being processed at the same
       time. The default value is 0, unless altered by the "nice" setting on the
@@ -3069,6 +3112,8 @@ http-request { allow | deny | tarpit | auth [realm <realm>] | redirect <rule> |
 
 http-response { allow | deny | add-header <name> <fmt> | set-nice <nice> |
                 set-header <name> <fmt> | del-header <name> |
+                replace-header <name> <regex-match> <replace-fmt> |
+                replace-value <name> <regex-match> <replace-fmt> |
                 set-log-level <level> | set-mark <mark> | set-tos <tos> |
                 add-acl(<file name>) <key fmt> |
                 del-acl(<file name>) <key fmt> |
@@ -3113,6 +3158,47 @@ http-response { allow | deny | add-header <name> <fmt> | set-nice <nice> |
     - "del-header" removes all HTTP header fields whose name is specified in
       <name>.
 
+    - "replace-header" matches the regular expression in all occurrences of
+      header field <name> according to <match-regex>, and replaces them with
+      the <replace-fmt> argument. Format characters are allowed in replace-fmt
+      and work like in <fmt> arguments in "add-header". The match is only
+      case-sensitive. It is important to understand that this action only
+      considers whole header lines, regardless of the number of values they
+      may contain. This usage is suited to headers naturally containing commas
+      in their value, such as Set-Cookie, Expires and so on.
+
+      Example:
+
+        http-response replace-header Set-Cookie (C=[^;]*);(.*) \1;ip=%bi;\2
+
+      applied to:
+
+        Set-Cookie: C=1; expires=Tue, 14-Jun-2016 01:40:45 GMT
+
+      outputs:
+
+        Set-Cookie: C=1;ip=192.168.1.20; expires=Tue, 14-Jun-2016 01:40:45 GMT
+
+      assuming the backend IP is 192.168.1.20.
+
+    - "replace-value" works like "replace-header" except that it matches the
+      regex against every comma-delimited value of the header field <name>
+      instead of the entire header. This is suited for all headers which are
+      allowed to carry more than one value. An example could be the Accept
+      header.
+
+      Example:
+
+        http-response replace-value Cache-control ^public$ private
+
+      applied to:
+
+        Cache-Control: max-age=3600, public
+
+      outputs:
+
+        Cache-Control: max-age=3600, private
+
     - "set-nice" sets the "nice" factor of the current request being processed.
       It only has effect against the other requests being processed at the same
       time. The default value is 0, unless altered by the "nice" setting on the
index 6370e2d636a41867937d80b74b2e6826779cd2ea..e898ca8721347e4239c7e0a0f0ee401819806322 100644 (file)
@@ -116,6 +116,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);
+void free_http_res_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 linenum, struct proxy *curproxy,
                                                const char **args, char **errmsg, int use_fmt);
index f5dd9a39721024bfb142848c9929b64cd2d91261..ff196a0cd644fb42e50dca72b22886fb5a92e39a 100644 (file)
@@ -247,6 +247,8 @@ enum {
        HTTP_REQ_ACT_ADD_HDR,
        HTTP_REQ_ACT_SET_HDR,
        HTTP_REQ_ACT_DEL_HDR,
+       HTTP_REQ_ACT_REPLACE_HDR,
+       HTTP_REQ_ACT_REPLACE_VAL,
        HTTP_REQ_ACT_REDIR,
        HTTP_REQ_ACT_SET_NICE,
        HTTP_REQ_ACT_SET_LOGL,
@@ -267,6 +269,8 @@ enum {
        HTTP_RES_ACT_ALLOW,
        HTTP_RES_ACT_DENY,
        HTTP_RES_ACT_ADD_HDR,
+       HTTP_RES_ACT_REPLACE_HDR,
+       HTTP_RES_ACT_REPLACE_VAL,
        HTTP_RES_ACT_SET_HDR,
        HTTP_RES_ACT_DEL_HDR,
        HTTP_RES_ACT_SET_NICE,
@@ -415,6 +419,7 @@ struct http_req_rule {
                        char *name;            /* header name */
                        int name_len;          /* header name's length */
                        struct list fmt;       /* log-format compatible expression */
+                       regex_t* re;           /* used by replace-header and replace-value */
                } hdr_add;                     /* args used by "add-header" and "set-header" */
                struct redirect_rule *redir;   /* redirect rule or "http-request redirect" */
                int nice;                      /* nice value for HTTP_REQ_ACT_SET_NICE */
@@ -440,6 +445,7 @@ struct http_res_rule {
                        char *name;            /* header name */
                        int name_len;          /* header name's length */
                        struct list fmt;       /* log-format compatible expression */
+                       regex_t* re;           /* used by replace-header and replace-value */
                } hdr_add;                     /* args used by "add-header" and "set-header" */
                int nice;                      /* nice value for HTTP_RES_ACT_SET_NICE */
                int loglevel;                  /* log-level value for HTTP_RES_ACT_SET_LOGL */
index c4442de81ee2a5db46dbd44b255e96d470bb54d7..cd42b348af0d387571ad325aac4e9a45358e3016 100644 (file)
@@ -1203,6 +1203,7 @@ void deinit(void)
                free(p->fwdfor_hdr_name);
 
                free_http_req_rules(&p->http_req_rules);
+               free_http_res_rules(&p->http_res_rules);
                free(p->task);
 
                pool_destroy2(p->req_cap_pool);
index 48dbc43cc79b9e44002f35784bc6c9fbc6866a83..568b91b0075dfb4cfcbbaf132e7cd78417839ca4 100644 (file)
@@ -3176,6 +3176,126 @@ static inline void inet_set_tos(int fd, struct sockaddr_storage from, int tos)
 #endif
 }
 
+/* Returns the number of characters written to destination,
+ * -1 on internal error and -2 if no replacement took place.
+ */
+static int http_replace_header(regex_t* re, char* dst, uint dst_size, char* val,
+                               const char* rep_str)
+{
+       if (regexec(re, val, MAX_MATCH, pmatch, 0))
+               return -2;
+
+       return exp_replace(dst, dst_size, val, rep_str, pmatch);
+}
+
+/* Returns the number of characters written to destination,
+ * -1 on internal error and -2 if no replacement took place.
+ */
+static int http_replace_value(regex_t* re, char* dst, uint dst_size, char* val, char delim,
+                              const char* rep_str)
+{
+       char* p = val;
+       char* dst_end = dst + dst_size;
+       char* dst_p = dst;
+
+       for (;;) {
+               char *p_delim;
+               const char* tok_end;
+
+               if ((p_delim = (char*)strchr(p, delim))) {
+                       *p_delim = 0;
+                       tok_end = p_delim;
+               } else {
+                       tok_end = p + strlen(p);
+               }
+
+               if (regexec(re, p, MAX_MATCH, pmatch, 0) == 0) {
+                       int replace_n = exp_replace(dst_p, dst_end - dst_p, p, rep_str, pmatch);
+
+                       if (replace_n < 0)
+                               return -1;
+
+                       dst_p += replace_n;
+               } else {
+                       uint len = tok_end - p;
+
+                       if (dst_p + len >= dst_end)
+                               return -1;
+
+                       memcpy(dst_p, p, len);
+                       dst_p += len;
+               }
+
+               if (dst_p >= dst_end)
+                       return -1;
+
+               if (p_delim) {
+                       *p_delim = delim;
+                       *dst_p++ = delim;
+                       p = p_delim + 1;
+               } else {
+                       *dst_p = 0;
+                       break;
+               }
+       }
+
+       return dst_p - dst;
+}
+
+static int http_transform_header(struct session* s, struct http_msg *msg, const char* name, uint name_len,
+                                 char* buf, struct hdr_idx* idx, struct list *fmt, regex_t* re,
+                                 struct hdr_ctx* ctx, int action)
+{
+       ctx->idx = 0;
+
+       while (http_find_full_header2(name, name_len, buf, idx, ctx)) {
+               struct hdr_idx_elem *hdr = idx->v + ctx->idx;
+               int delta;
+               char* val = (char*)ctx->line + name_len + 2;
+               char* val_end = (char*)ctx->line + hdr->len;
+               char save_val_end = *val_end;
+               char* reg_dst_buf;
+               uint reg_dst_buf_size;
+               int n_replaced;
+
+               *val_end = 0;
+               trash.len = build_logline(s, trash.str, trash.size, fmt);
+
+               if (trash.len >= trash.size - 1)
+                       return -1;
+
+               reg_dst_buf = trash.str + trash.len + 1;
+               reg_dst_buf_size = trash.size - trash.len - 1;
+
+               switch (action) {
+               case HTTP_REQ_ACT_REPLACE_VAL:
+               case HTTP_RES_ACT_REPLACE_VAL:
+                       n_replaced = http_replace_value(re, reg_dst_buf, reg_dst_buf_size, val, ',', trash.str);
+                       break;
+               case HTTP_REQ_ACT_REPLACE_HDR:
+               case HTTP_RES_ACT_REPLACE_HDR:
+                       n_replaced = http_replace_header(re, reg_dst_buf, reg_dst_buf_size, val, trash.str);
+                       break;
+               default: /* impossible */
+                       return -1;
+               }
+
+               *val_end = save_val_end;
+
+               switch (n_replaced) {
+               case -1: return -1;
+               case -2: continue;
+               }
+
+               delta = buffer_replace2(msg->chn->buf, val, val_end, reg_dst_buf, n_replaced);
+
+               hdr->len += delta;
+               http_msg_move_end(msg, delta);
+       }
+
+       return 0;
+}
+
 /* Executes the http-request rules <rules> for session <s>, proxy <px> and
  * transaction <txn>. Returns the verdict of the first rule that prevents
  * further processing of the request (auth, deny, ...), and defaults to
@@ -3265,6 +3385,14 @@ http_req_get_intercept_rule(struct proxy *px, struct list *rules, struct session
                        s->logs.level = rule->arg.loglevel;
                        break;
 
+               case HTTP_REQ_ACT_REPLACE_HDR:
+               case HTTP_REQ_ACT_REPLACE_VAL:
+                       if (http_transform_header(s, &txn->req, rule->arg.hdr_add.name, rule->arg.hdr_add.name_len,
+                                                 txn->req.chn->buf->p, &txn->hdr_idx, &rule->arg.hdr_add.fmt,
+                                                 rule->arg.hdr_add.re, &ctx, rule->action))
+                               return HTTP_RULE_RES_BADREQ;
+                       break;
+
                case HTTP_REQ_ACT_DEL_HDR:
                case HTTP_REQ_ACT_SET_HDR:
                        ctx.idx = 0;
@@ -3446,6 +3574,14 @@ http_res_get_intercept_rule(struct proxy *px, struct list *rules, struct session
                        s->logs.level = rule->arg.loglevel;
                        break;
 
+               case HTTP_RES_ACT_REPLACE_HDR:
+               case HTTP_RES_ACT_REPLACE_VAL:
+                       if (http_transform_header(s, &txn->rsp, rule->arg.hdr_add.name, rule->arg.hdr_add.name_len,
+                                                 txn->rsp.chn->buf->p, &txn->hdr_idx, &rule->arg.hdr_add.fmt,
+                                                 rule->arg.hdr_add.re, &ctx, rule->action))
+                               return NULL; /* note: we should report an error here */
+                       break;
+
                case HTTP_RES_ACT_DEL_HDR:
                case HTTP_RES_ACT_SET_HDR:
                        ctx.idx = 0;
@@ -8759,7 +8895,27 @@ void http_reset_txn(struct session *s)
        s->rep->analyse_exp = TICK_ETERNITY;
 }
 
-void free_http_req_rules(struct list *r) {
+static inline void free_regex(regex_t* re)
+{
+       if (re) {
+               regfree(re);
+               free(re);
+       }
+}
+
+void free_http_res_rules(struct list *r)
+{
+       struct http_res_rule *tr, *pr;
+
+       list_for_each_entry_safe(pr, tr, r, list) {
+               LIST_DEL(&pr->list);
+               free_regex(pr->arg.hdr_add.re);
+               free(pr);
+       }
+}
+
+void free_http_req_rules(struct list *r)
+{
        struct http_req_rule *tr, *pr;
 
        list_for_each_entry_safe(pr, tr, r, list) {
@@ -8767,6 +8923,7 @@ void free_http_req_rules(struct list *r) {
                if (pr->action == HTTP_REQ_ACT_AUTH)
                        free(pr->arg.auth.realm);
 
+               free_regex(pr->arg.hdr_add.re);
                free(pr);
        }
 }
@@ -8909,6 +9066,41 @@ struct http_req_rule *parse_http_req_cond(const char **args, const char *file, i
                proxy->conf.lfs_file = strdup(proxy->conf.args.file);
                proxy->conf.lfs_line = proxy->conf.args.line;
                cur_arg += 2;
+       } else if (strcmp(args[0], "replace-header") == 0 || strcmp(args[0], "replace-val") == 0) {
+               rule->action = *args[8] == 'h' ? HTTP_REQ_ACT_REPLACE_HDR : HTTP_REQ_ACT_REPLACE_VAL;
+               cur_arg = 1;
+
+               if (!*args[cur_arg] || !*args[cur_arg+1] || !*args[cur_arg+2] ||
+                   (*args[cur_arg+3] && strcmp(args[cur_arg+2], "if") != 0 && strcmp(args[cur_arg+2], "unless") != 0)) {
+                       Alert("parsing [%s:%d]: 'http-request %s' expects exactly 3 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);
+
+               if (!(rule->arg.hdr_add.re = calloc(1, sizeof(*rule->arg.hdr_add.re)))) {
+                       Alert("parsing [%s:%d]: out of memory.\n", file, linenum);
+                       goto out_err;
+               }
+
+               if (regcomp(rule->arg.hdr_add.re, args[cur_arg + 1], REG_EXTENDED)) {
+                       Alert("parsing [%s:%d] : '%s' : bad regular expression.\n", file, linenum,
+                             args[cur_arg + 1]);
+                       goto out_err;
+               }
+
+               proxy->conf.args.ctx = ARGC_HRQ;
+               parse_logformat_string(args[cur_arg + 2], proxy, &rule->arg.hdr_add.fmt, LOG_OPT_HTTP,
+                                      (proxy->cap & PR_CAP_FE) ? SMP_VAL_FE_HRQ_HDR : SMP_VAL_BE_HRQ_HDR,
+                                      file, linenum);
+
+               free(proxy->conf.lfs_file);
+               proxy->conf.lfs_file = strdup(proxy->conf.args.file);
+               proxy->conf.lfs_line = proxy->conf.args.line;
+               cur_arg += 3;
        } else if (strcmp(args[0], "del-header") == 0) {
                rule->action = HTTP_REQ_ACT_DEL_HDR;
                cur_arg = 1;
@@ -9075,7 +9267,7 @@ struct http_req_rule *parse_http_req_cond(const char **args, const char *file, i
                        goto out_err;
                }
        } else {
-               Alert("parsing [%s:%d]: 'http-request' expects 'allow', 'deny', 'auth', 'redirect', 'tarpit', 'add-header', 'set-header', 'set-nice', 'set-tos', 'set-mark', 'set-log-level', 'add-acl', 'del-acl', 'del-map', 'set-map', but got '%s'%s.\n",
+               Alert("parsing [%s:%d]: 'http-request' expects 'allow', 'deny', 'auth', 'redirect', 'tarpit', 'add-header', 'set-header', 'replace-header', 'replace-value', 'set-nice', 'set-tos', 'set-mark', 'set-log-level', 'add-acl', 'del-acl', 'del-map', 'set-map', but got '%s'%s.\n",
                      file, linenum, args[0], *args[0] ? "" : " (missing argument)");
                goto out_err;
        }
@@ -9228,6 +9420,41 @@ struct http_res_rule *parse_http_res_cond(const char **args, const char *file, i
                proxy->conf.lfs_file = strdup(proxy->conf.args.file);
                proxy->conf.lfs_line = proxy->conf.args.line;
                cur_arg += 2;
+       } else if (strcmp(args[0], "replace-header") == 0 || strcmp(args[0], "replace-value") == 0) {
+               rule->action = *args[8] == 'h' ? HTTP_RES_ACT_REPLACE_HDR : HTTP_RES_ACT_REPLACE_VAL;
+               cur_arg = 1;
+
+               if (!*args[cur_arg] || !*args[cur_arg+1] || !*args[cur_arg+2] ||
+                   (*args[cur_arg+3] && strcmp(args[cur_arg+2], "if") != 0 && strcmp(args[cur_arg+2], "unless") != 0)) {
+                       Alert("parsing [%s:%d]: 'http-request %s' expects exactly 3 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);
+
+               if (!(rule->arg.hdr_add.re = calloc(1, sizeof(*rule->arg.hdr_add.re)))) {
+                       Alert("parsing [%s:%d]: out of memory.\n", file, linenum);
+                       goto out_err;
+               }
+
+               if (regcomp(rule->arg.hdr_add.re, args[cur_arg + 1], REG_EXTENDED)) {
+                       Alert("parsing [%s:%d] : '%s' : bad regular expression.\n", file, linenum,
+                             args[cur_arg + 1]);
+                       goto out_err;
+               }
+
+               proxy->conf.args.ctx = ARGC_HRQ;
+               parse_logformat_string(args[cur_arg + 2], proxy, &rule->arg.hdr_add.fmt, LOG_OPT_HTTP,
+                                      (proxy->cap & PR_CAP_BE) ? SMP_VAL_BE_HRS_HDR : SMP_VAL_FE_HRS_HDR,
+                                      file, linenum);
+
+               free(proxy->conf.lfs_file);
+               proxy->conf.lfs_file = strdup(proxy->conf.args.file);
+               proxy->conf.lfs_line = proxy->conf.args.line;
+               cur_arg += 3;
        } else if (strcmp(args[0], "del-header") == 0) {
                rule->action = HTTP_RES_ACT_DEL_HDR;
                cur_arg = 1;
@@ -9378,7 +9605,7 @@ struct http_res_rule *parse_http_res_cond(const char **args, const char *file, i
                        goto out_err;
                }
        } else {
-               Alert("parsing [%s:%d]: 'http-response' expects 'allow', 'deny', 'redirect', 'add-header', 'del-header', 'set-header', 'set-nice', 'set-tos', 'set-mark', 'set-log-level', 'del-acl', 'add-acl', 'del-map', 'set-map', but got '%s'%s.\n",
+               Alert("parsing [%s:%d]: 'http-response' expects 'allow', 'deny', 'redirect', 'add-header', 'del-header', 'set-header', 'replace-header', 'replace-value', 'set-nice', 'set-tos', 'set-mark', 'set-log-level', 'del-acl', 'add-acl', 'del-map', 'set-map', but got '%s'%s.\n",
                      file, linenum, args[0], *args[0] ? "" : " (missing argument)");
                goto out_err;
        }