MEDIUM: http_act: Rework *-headers-bin actions

These actions were added recently and it appeared the way binary headers
were retrieved could be simplified.

First, there is no reason to retrieve a base64 encoded string. It is
possible to rely on the binary string directly. "b64dec" converter can be
used to perform a base64 decoding if necessary.

Then, using a log-format string is quite overkill and probably
conterintuitive. Most of time, the headers will be retrieved from a
variable. So a sample expression is easier to use. Thanks to the previous
patch, it is quite easy to achieve.

This patch relies on the commit "MINOR: action: Add a sample expression
field in arguments used by HTTP actions". The documentation was updated
accordingly.
This commit is contained in:
Christopher Faulet 2026-04-01 14:02:30 +02:00
parent 2adcdbacc2
commit 8c00df7448
2 changed files with 119 additions and 184 deletions

View File

@ -15314,29 +15314,28 @@ add-header <name> <fmt>
previous rule.
add-headers-bin <fmt> [ prefix <str> ]
add-headers-bin <expr> [ prefix <str> ]
Usable in: QUIC Ini| TCP RqCon| RqSes| RqCnt| RsCnt| HTTP Req| Res| Aft
- | - | - | - | - | X | X | X
This is a variant of the "add-header" action where the header names and
values are passed as a base64 encoded string of varint encoded header names
and values. See the "req.hdrs_bin" sample fetch about the varint format. This
is useful when you want to set multiple headers at once, without having to
know the header names in advance. Note that these headers have not been
validated by the HTTP parser and could lead to emitting invalid messages and
in worst cases lead to request smuggling attacks. The number of headers
inserted are also of importance, as that is limited by tune.http.maxhdr.
Optional prefix will only set the headers from the encoded string that
start with <str>.
values are passed as a varint encoded binary string. See the "req.hdrs_bin"
sample fetch about the varint format. This is useful when you want to set
multiple headers at once, without having to know the header names in
advance. Note that these headers have not been validated by the HTTP parser
and could lead to emitting invalid messages and in worst cases lead to
request smuggling attacks. The number of headers inserted are also of
importance, as that is limited by tune.http.maxhdr. Optional prefix will
only set the headers from the encoded string that start with <str>.
Example:
# This would reset the Accept/UA/Host headers to their initial values
http-request set-var(txn.oldheaders) req.hdrs_bin,base64
http-request set-var(txn.oldheaders) req.hdrs_bin
http-request del-header Accept
http-request del-header User-Agent
http-request del-header Host
http-request add-headers-bin %[var(txn.oldheaders)]
http-request add-headers-bin var(txn.oldheaders)
allow
@ -15481,17 +15480,17 @@ del-header <name> [ -m <meth> ]
method is used.
del-headers-bin <fmt> [ -m <meth> ]
del-headers-bin <expr> [ -m <meth> ]
Usable in: QUIC Ini| TCP RqCon| RqSes| RqCnt| RsCnt| HTTP Req| Res| Aft
- | - | - | - | - | X | X | X
This removes all HTTP header fields whose names are specified in <fmt>. <fmt>
is a base64 encoded varint encoded string of all header names that should be
deleted. See "add-headers-bin" and "set-headers-bin" for the description of
encoding and examples. <meth> is the matching method, applied on all the
header names. Supported matching methods are "str" (exact match), "beg"
(prefix match), "end" (suffix match) and "sub" (substring match). The "reg"
(regex match) is not supported due to unpredictable performance during
This removes all HTTP header fields whose names are specified in <expr>.
<expr> must return a varint encoded binary string of all header names that
should be deleted. See "add-headers-bin" and "set-headers-bin" for the
description of encoding and examples. <meth> is the matching method, applied
on all the header names. Supported matching methods are "str" (exact match),
"beg" (prefix match), "end" (suffix match) and "sub" (substring match). The
"reg" (regex match) is not supported due to unpredictable performance during
runtime. If not specified, exact matching method is used.
@ -16395,29 +16394,28 @@ set-header <name> <fmt>
http-request set-header X-SSL-Client-NotBefore %{+Q}[ssl_c_notbefore]
http-request set-header X-SSL-Client-NotAfter %{+Q}[ssl_c_notafter]
set-headers-bin <fmt> [ prefix <str> ]
set-headers-bin <expr> [ prefix <str> ]
Usable in: QUIC Ini| TCP RqCon| RqSes| RqCnt| RsCnt| HTTP Req| Res| Aft
- | - | - | - | - | X | X | X
This is a variant of the "set-header" action where the header names and
values are passed as a base64 encoded string of varint encoded header names
and values. See the "req.hdrs_bin" sample fetch about the varint format. This
is useful when you want to set multiple headers at once, without having to
know the header names in advance. Note that these headers have not been
validated by the HTTP parser and could lead to emitting invalid messages and
in worst cases lead to request smuggling attacks. The number of headers
inserted are also of importance, as that is limited by tune.http.maxhdr.
Optional prefix will only set the headers from the encoded string that
start with <str>.
values are passed as a varint encoded binary string. See the "req.hdrs_bin"
sample fetch about the varint format. This is useful when you want to set
multiple headers at once, without having to know the header names in
advance. Note that these headers have not been validated by the HTTP parser
and could lead to emitting invalid messages and in worst cases lead to
request smuggling attacks. The number of headers inserted are also of
importance, as that is limited by tune.http.maxhdr. Optional prefix will
only set the headers from the encoded string that start with <str>.
Example:
# This would reset the Accept/UA/Host headers to their initial values
http-request set-var(txn.oldheaders) req.hdrs_bin,base64
http-request set-var(txn.oldheaders) req.hdrs_bin
http-request del-header Accept
http-request del-header User-Agent
http-request del-header Host
http-request set-headers-bin %[var(txn.oldheaders)]
http-request set-headers-bin var(txn.oldheaders)
set-log-level <level>

View File

@ -1489,63 +1489,36 @@ static enum act_return http_action_set_headers_bin(struct act_rule *rule, struct
{
struct http_msg *msg = ((rule->from == ACT_F_HTTP_REQ) ? &s->txn->req : &s->txn->rsp);
struct htx *htx = htxbuf(&msg->chn->buf);
struct sample *hdrs_bin;
char *p, *end;
enum act_return ret = ACT_RET_CONT;
struct buffer *binstring, *decoded = NULL;
struct http_hdr_ctx ctx;
struct ist n, v;
uint64_t sz = 0;
size_t offset = 0;
int bytes_read;
char *oldarea = NULL;
binstring = alloc_trash_chunk();
if (!binstring)
goto fail_alloc;
decoded = alloc_trash_chunk();
if (!decoded)
goto fail_alloc;
hdrs_bin = sample_fetch_as_type(px, sess, s, SMP_OPT_FINAL, rule->arg.http.expr, SMP_T_BIN);
if (!hdrs_bin)
return ACT_RET_CONT;
oldarea = decoded->area;
binstring->data = build_logline(s, binstring->area, binstring->size, &rule->arg.http.fmt);
bytes_read = base64dec(binstring->area, binstring->data, decoded->area, decoded->size);
if (bytes_read < 0)
goto fail_rewrite;
decoded->data = bytes_read;
while (1) {
int ret;
ret = decode_varint(&decoded->area, decoded->area + decoded->data - offset, &sz);
if (ret == -1)
p = b_orig(&hdrs_bin->data.u.str);
end = b_tail(&hdrs_bin->data.u.str);
while (p < end) {
if (decode_varint(&p, end, &sz) == -1)
goto fail_rewrite;
offset += ret;
if (!sz) {
ret = decode_varint(&decoded->area, decoded->area + decoded->data - offset, &sz);
if (ret == -1)
goto fail_rewrite;
offset += ret;
if (!sz)
goto leave;
else
if (decode_varint(&p, end, &sz) == -1 || sz > 0)
goto fail_rewrite;
goto leave;
}
n = ist2(decoded->area, sz);
offset += sz;
decoded->area += sz;
n = ist2(p, sz);
p += sz;
ret = decode_varint(&decoded->area, decoded->area + decoded->data - offset, &sz);
if (ret == -1)
if (decode_varint(&p, end, &sz) == -1)
goto fail_rewrite;
offset += ret;
v = ist2(decoded->area, sz);
offset += sz;
decoded->area += sz;
v = ist2(p, sz);
p += sz;
if (istlen(rule->arg.http.str) && !istmatch(n, rule->arg.http.str))
continue;
@ -1565,19 +1538,11 @@ static enum act_return http_action_set_headers_bin(struct act_rule *rule, struct
goto fail_rewrite;
}
leave:
free_trash_chunk(binstring);
/* decode_varint moves the area pointer, so return it to the correct position */
if (decoded)
decoded->area = oldarea;
free_trash_chunk(decoded);
return ret;
fail_alloc:
if (!(s->flags & SF_ERR_MASK))
s->flags |= SF_ERR_RESOURCE;
/* invalid encoding */
ret = ACT_RET_ERR;
goto leave;
leave:
return ret;
fail_rewrite:
if (sess->fe_tgcounters)
@ -1678,14 +1643,15 @@ static enum act_parse_ret parse_http_set_header(const char **args, int *orig_arg
static enum act_parse_ret parse_http_set_headers_bin(const char **args, int *orig_arg, struct proxy *px,
struct act_rule *rule, char **err)
{
int cap = 0, cur_arg;
struct sample_expr *expr;
unsigned int where;
int cur_arg;
if (args[*orig_arg-1][0] == 's')
rule->action = 0; // set-header
else
rule->action = 1; // add-header
rule->action_ptr = http_action_set_headers_bin;
rule->release_ptr = release_http_action;
lf_expr_init(&rule->arg.http.fmt);
@ -1695,35 +1661,39 @@ static enum act_parse_ret parse_http_set_headers_bin(const char **args, int *ori
return ACT_RET_PRS_ERR;
}
if (rule->from == ACT_F_HTTP_REQ) {
px->conf.args.ctx = ARGC_HRQ;
if (px->cap & PR_CAP_FE)
cap |= SMP_VAL_FE_HRQ_HDR;
if (px->cap & PR_CAP_BE)
cap |= SMP_VAL_BE_HRQ_HDR;
}
else{
px->conf.args.ctx = ARGC_HRS;
if (px->cap & PR_CAP_FE)
cap |= SMP_VAL_FE_HRS_HDR;
if (px->cap & PR_CAP_BE)
cap |= SMP_VAL_BE_HRS_HDR;
}
if (!parse_logformat_string(args[cur_arg], px, &rule->arg.http.fmt, LOG_OPT_HTTP, cap, err)) {
expr = sample_parse_expr((char **)args, &cur_arg, px->conf.args.file, px->conf.args.line,
err, &px->conf.args, NULL);
if (!expr)
return ACT_RET_PRS_ERR;
where = 0;
if (px->cap & PR_CAP_FE)
where |= (rule->from == ACT_F_HTTP_REQ ? SMP_VAL_FE_HRQ_HDR : SMP_VAL_FE_HRS_HDR);
if (px->cap & PR_CAP_BE)
where |= (rule->from == ACT_F_HTTP_REQ ? SMP_VAL_BE_HRQ_HDR : SMP_VAL_BE_HRS_HDR);
if (!(expr->fetch->val & where)) {
memprintf(err, "fetch method '%s' extracts information from '%s', none of which is available here",
args[cur_arg-1], sample_src_names(expr->fetch->use));
release_sample_expr(expr);
return ACT_RET_PRS_ERR;
}
/* Check if an argument is available */
if (*args[cur_arg+1] && strcmp(args[cur_arg+1], "prefix") == 0 ) {
if(!*args[cur_arg+2]) {
if (strcmp(args[cur_arg], "prefix") == 0 ) {
cur_arg++;
if(!*args[cur_arg]) {
memprintf(err, "expects 1 argument: <headers>; or 3 arguments: <headers> prefix <pfx>");
release_sample_expr(expr);
return ACT_RET_PRS_ERR;
}
cur_arg += 2;
rule->arg.http.str = ist(strdup(args[cur_arg]));
cur_arg++;
}
*orig_arg = cur_arg + 1;
rule->arg.http.expr = expr;
*orig_arg = cur_arg;
return ACT_RET_PRS_OK;
}
@ -1948,54 +1918,26 @@ static enum act_return http_action_del_headers_bin(struct act_rule *rule, struct
struct http_hdr_ctx ctx;
struct http_msg *msg = ((rule->from == ACT_F_HTTP_REQ) ? &s->txn->req : &s->txn->rsp);
struct htx *htx = htxbuf(&msg->chn->buf);
struct sample *hdrs_bin;
char *p, *end;
enum act_return ret = ACT_RET_CONT;
struct buffer *binstring, *decoded = NULL;
struct ist n;
uint64_t sz = 0;
size_t offset = 0;
int bytes_read;
char *oldarea = NULL;
binstring = alloc_trash_chunk();
if (!binstring)
goto fail_alloc;
decoded = alloc_trash_chunk();
if (!decoded)
goto fail_alloc;
hdrs_bin = sample_fetch_as_type(px, sess, s, SMP_OPT_FINAL, rule->arg.http.expr, SMP_T_BIN);
if (!hdrs_bin)
return ACT_RET_CONT;
oldarea = decoded->area;
binstring->data = build_logline(s, binstring->area, binstring->size, &rule->arg.http.fmt);
bytes_read = base64dec(binstring->area, binstring->data, decoded->area, decoded->size);
if (bytes_read < 0) {
goto fail_rewrite;
}
decoded->data = bytes_read;
while (1) {
int ret;
ret = decode_varint(&decoded->area, decoded->area + decoded->data - offset, &sz);
if (ret == -1)
p = b_orig(&hdrs_bin->data.u.str);
end = b_tail(&hdrs_bin->data.u.str);
while (p < end) {
if (decode_varint(&p, end, &sz) == -1)
goto fail_rewrite;
offset += ret;
if (!sz)
goto leave;
if (!sz) {
ret = decode_varint(&decoded->area, decoded->area + decoded->data - offset, &sz);
if (ret == -1)
goto fail_rewrite;
offset += ret;
if (!sz)
goto leave;
else
goto fail_rewrite;
}
n = ist2(decoded->area, sz);
offset += sz;
decoded->area += sz;
n = ist2(p, sz);
p += sz;
if (is_immutable_header(n))
continue;
@ -2024,19 +1966,11 @@ static enum act_return http_action_del_headers_bin(struct act_rule *rule, struct
}
}
leave:
free_trash_chunk(binstring);
/* decode_varint moves the area pointer, so return it to the correct position */
if (decoded)
decoded->area = oldarea;
free_trash_chunk(decoded);
return ret;
fail_alloc:
if (!(s->flags & SF_ERR_MASK))
s->flags |= SF_ERR_RESOURCE;
/* invalid encoding */
ret = ACT_RET_ERR;
goto leave;
leave:
return ret;
fail_rewrite:
if (sess->fe_tgcounters)
@ -2064,7 +1998,9 @@ static enum act_return http_action_del_headers_bin(struct act_rule *rule, struct
static enum act_parse_ret parse_http_del_headers_bin(const char **args, int *orig_arg, struct proxy *px,
struct act_rule *rule, char **err)
{
int cap = 0, cur_arg;
struct sample_expr *expr;
unsigned int where;
int cur_arg;
int pat_idx;
/* set exact matching (-m str) as default */
@ -2079,40 +2015,37 @@ static enum act_parse_ret parse_http_del_headers_bin(const char **args, int *ori
return ACT_RET_PRS_ERR;
}
if (rule->from == ACT_F_HTTP_REQ) {
px->conf.args.ctx = ARGC_HRQ;
if (px->cap & PR_CAP_FE)
cap |= SMP_VAL_FE_HRQ_HDR;
if (px->cap & PR_CAP_BE)
cap |= SMP_VAL_BE_HRQ_HDR;
}
else{
px->conf.args.ctx = ARGC_HRS;
if (px->cap & PR_CAP_FE)
cap |= SMP_VAL_FE_HRS_HDR;
if (px->cap & PR_CAP_BE)
cap |= SMP_VAL_BE_HRS_HDR;
}
expr = sample_parse_expr((char **)args, &cur_arg, px->conf.args.file, px->conf.args.line,
err, &px->conf.args, NULL);
if (!expr)
return ACT_RET_PRS_ERR;
if (!parse_logformat_string(args[cur_arg], px, &rule->arg.http.fmt, LOG_OPT_HTTP, cap, err)) {
istfree(&rule->arg.http.str);
where = 0;
if (px->cap & PR_CAP_FE)
where |= (rule->from == ACT_F_HTTP_REQ ? SMP_VAL_FE_HRQ_HDR : SMP_VAL_FE_HRS_HDR);
if (px->cap & PR_CAP_BE)
where |= (rule->from == ACT_F_HTTP_REQ ? SMP_VAL_BE_HRQ_HDR : SMP_VAL_BE_HRS_HDR);
if (!(expr->fetch->val & where)) {
memprintf(err, "fetch method '%s' extracts information from '%s', none of which is available here",
args[cur_arg-1], sample_src_names(expr->fetch->use));
release_sample_expr(expr);
return ACT_RET_PRS_ERR;
}
px->conf.args.ctx = (rule->from == ACT_F_HTTP_REQ ? ARGC_HRQ : ARGC_HRS);
if (strcmp(args[cur_arg+1], "-m") == 0) {
if (strcmp(args[cur_arg], "-m") == 0) {
cur_arg++;
if (!*args[cur_arg+1]) {
if (!*args[cur_arg]) {
memprintf(err, "-m flag expects exactly 1 argument");
release_sample_expr(expr);
return ACT_RET_PRS_ERR;
}
cur_arg++;
pat_idx = pat_find_match_name(args[cur_arg]);
switch (pat_idx) {
case PAT_MATCH_REG:
memprintf(err, "-m reg with is unsupported with del-header-bin due to performance reasons");
release_sample_expr(expr);
return ACT_RET_PRS_ERR;
case PAT_MATCH_STR:
case PAT_MATCH_BEG:
@ -2122,11 +2055,15 @@ static enum act_parse_ret parse_http_del_headers_bin(const char **args, int *ori
break;
default:
memprintf(err, "-m with unsupported matching method '%s'", args[cur_arg]);
release_sample_expr(expr);
return ACT_RET_PRS_ERR;
}
cur_arg++;
}
*orig_arg = cur_arg + 1;
rule->arg.http.expr = expr;
*orig_arg = cur_arg;
return ACT_RET_PRS_OK;
}