diff --git a/doc/configuration.txt b/doc/configuration.txt index 51798b4f9..370fd53e5 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -2878,6 +2878,8 @@ http-check send-state http-request { allow | deny | tarpit | auth [realm ] | redirect | add-header | set-header | del-header | set-nice | set-log-level | + replace-header | + replace-value | set-tos | set-mark | add-acl() | del-acl() | @@ -2945,6 +2947,47 @@ http-request { allow | deny | tarpit | auth [realm ] | redirect | - "del-header" removes all HTTP header fields whose name is specified in . + - "replace-header" matches the regular expression in all occurrences of + header field according to , and replaces them with + the argument. Format characters are allowed in replace-fmt + and work like in 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 + 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 ] | redirect | http-response { allow | deny | add-header | set-nice | set-header | del-header | + replace-header | + replace-value | set-log-level | set-mark | set-tos | add-acl() | del-acl() | @@ -3113,6 +3158,47 @@ http-response { allow | deny | add-header | set-nice | - "del-header" removes all HTTP header fields whose name is specified in . + - "replace-header" matches the regular expression in all occurrences of + header field according to , and replaces them with + the argument. Format characters are allowed in replace-fmt + and work like in 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 + 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 diff --git a/include/proto/proto_http.h b/include/proto/proto_http.h index 6370e2d63..e898ca872 100644 --- a/include/proto/proto_http.h +++ b/include/proto/proto_http.h @@ -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); diff --git a/include/types/proto_http.h b/include/types/proto_http.h index f5dd9a397..ff196a0cd 100644 --- a/include/types/proto_http.h +++ b/include/types/proto_http.h @@ -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 */ diff --git a/src/haproxy.c b/src/haproxy.c index c4442de81..cd42b348a 100644 --- a/src/haproxy.c +++ b/src/haproxy.c @@ -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); diff --git a/src/proto_http.c b/src/proto_http.c index 48dbc43cc..568b91b00 100644 --- a/src/proto_http.c +++ b/src/proto_http.c @@ -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 for session , proxy and * transaction . 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; }