diff --git a/doc/configuration.txt b/doc/configuration.txt index d49d359a2..37fb004c5 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -18059,7 +18059,21 @@ bool bytes([,]) Extracts some bytes from an input binary sample. The result is a binary sample starting at an offset (in bytes) of the original sample and - optionally truncated at the given length. + optionally truncated at the given length. and can be numeric + values or variable names. The converter returns an empty sample if either + or is invalid. Invalid means a negative value or a + value >= length of the input sample. Invalid means a negative value + or, in some cases, a value bigger than the length of the input sample. + + Example: + http-request set-var(txn.input) req.hdr(input) # let's say input is "012345" + + http-response set-header bytes_0 "%[var(txn.input),bytes(0)]" # outputs "012345" + http-response set-header bytes_1_3 "%[var(txn.input),bytes(1,3)]" # outputs "123" + + http-response set-var(txn.var_start) int(1) + http-response set-var(txn.var_length) int(3) + http-response set-header bytes_var1_var3 "%[var(txn.input),bytes(txn.var_start,txn.var_length)]" # outputs "123" concat([],[],[]) Concatenates up to 3 fields after the current sample which is then turned to diff --git a/reg-tests/converter/bytes.vtc b/reg-tests/converter/bytes.vtc new file mode 100644 index 000000000..8abe401e5 --- /dev/null +++ b/reg-tests/converter/bytes.vtc @@ -0,0 +1,156 @@ +varnishtest "bytes converter Test" + +feature cmd "$HAPROXY_PROGRAM -cc 'version_atleast(2.9-dev4)'" + +feature ignore_unknown_macro + +# TEST - 1 +server s1 { + rxreq + txresp -hdr "Connection: close" +} -repeat 1 -start + +haproxy h1 -conf { + defaults + mode http + timeout connect "${HAPROXY_TEST_TIMEOUT-5s}" + timeout client "${HAPROXY_TEST_TIMEOUT-5s}" + timeout server "${HAPROXY_TEST_TIMEOUT-5s}" + + frontend fe + bind "fd@${fe}" + + #### requests + http-request set-var(txn.input) req.hdr(input) + + http-response set-header bytes_0 "%[var(txn.input),bytes(0)]" + http-response set-header bytes_1 "%[var(txn.input),bytes(1)]" + http-response set-header bytes_0_3 "%[var(txn.input),bytes(0,3)]" + http-response set-header bytes_1_3 "%[var(txn.input),bytes(1,3)]" + http-response set-header bytes_99 "%[var(txn.input),bytes(99)]" + http-response set-header bytes_5 "%[var(txn.input),bytes(5)]" + http-response set-header bytes_6 "%[var(txn.input),bytes(6)]" + http-response set-header bytes_0_6 "%[var(txn.input),bytes(0,6)]" + http-response set-header bytes_0_7 "%[var(txn.input),bytes(0,7)]" + + http-response set-var(txn.var_start) int(0) + http-response set-header bytes_var0 "%[var(txn.input),bytes(txn.var_start)]" + + http-response set-var(txn.var_start) int(1) + http-response set-var(txn.var_length) int(3) + http-response set-header bytes_var1_var3 "%[var(txn.input),bytes(txn.var_start,txn.var_length)]" + + http-response set-var(txn.var_start) int(99) + http-response set-header bytes_var99 "%[var(txn.input),bytes(txn.var_start)]" + + http-response set-var(txn.var_start) int(0) + http-response set-var(txn.var_length) int(7) + http-response set-header bytes_var0_var7 "%[var(txn.input),bytes(txn.var_start,txn.var_length)]" + + http-response set-var(txn.var_start) int(1) + http-response set-var(txn.var_length) int(3) + http-response set-header bytes_var1_3 "%[var(txn.input),bytes(txn.var_start,3)]" + http-response set-header bytes_1_var3 "%[var(txn.input),bytes(1,txn.var_length)]" + + http-response set-var(txn.var_start) int(-1) + http-response set-var(txn.var_length) int(-1) + http-response set-header bytes_varminus1 "%[var(txn.input),bytes(txn.var_start)]" + http-response set-header bytes_0_varminus1 "%[var(txn.input),bytes(0,txn.var_length)]" + + http-response set-header bytes_varNA "%[var(txn.input),bytes(txn.NA)]" + http-response set-header bytes_1_varNA "%[var(txn.input),bytes(1,txn.NA)]" + + default_backend be + + backend be + server s1 ${s1_addr}:${s1_port} +} -start + +client c1 -connect ${h1_fe_sock} { + txreq -url "/" \ + -hdr "input: 012345" + rxresp + expect resp.status == 200 + expect resp.http.bytes_0 == "012345" + expect resp.http.bytes_1 == "12345" + expect resp.http.bytes_0_3 == "012" + expect resp.http.bytes_1_3 == "123" + expect resp.http.bytes_99 == "" + expect resp.http.bytes_5 == "5" + expect resp.http.bytes_6 == "" + expect resp.http.bytes_0_6 == "012345" + + # since specified length is > input length, response contains the input till the end + expect resp.http.bytes_0_7 == "012345" + + expect resp.http.bytes_var0 == "012345" + expect resp.http.bytes_var1_var3 == "123" + expect resp.http.bytes_var99 == "" + expect resp.http.bytes_var0_var7 == "012345" + expect resp.http.bytes_var1_3 == "123" + expect resp.http.bytes_1_var3 == "123" + expect resp.http.bytes_varminus1 == "" + expect resp.http.bytes_0_varminus1 == "" + expect resp.http.bytes_varNA == "" + expect resp.http.bytes_1_varNA == "" +} -run + +# TEST - 2 +# negative starting index causes startup failure +haproxy h2 -conf { + defaults + mode http + timeout connect "${HAPROXY_TEST_TIMEOUT-5s}" + timeout client "${HAPROXY_TEST_TIMEOUT-5s}" + timeout server "${HAPROXY_TEST_TIMEOUT-5s}" + + frontend fe + bind "fd@${fe}" + + http-response set-header bytes_output "%[var(txn.input),bytes(-1)]" + + default_backend be + + backend be + server s1 ${s1_addr}:${s1_port} +} -start -expectexit 1 + +# TEST - 3 +# negative length causes startup failure +haproxy h3 -conf { + defaults + mode http + timeout connect "${HAPROXY_TEST_TIMEOUT-5s}" + timeout client "${HAPROXY_TEST_TIMEOUT-5s}" + timeout server "${HAPROXY_TEST_TIMEOUT-5s}" + + frontend fe + bind "fd@${fe}" + + http-response set-header bytes_output "%[var(txn.input),bytes(0,-1)]" + + default_backend be + + backend be + server s1 ${s1_addr}:${s1_port} +} -start -expectexit 1 + +# TEST - 4 +# 0 length causes startup failure +haproxy h4 -conf { + defaults + mode http + timeout connect "${HAPROXY_TEST_TIMEOUT-5s}" + timeout client "${HAPROXY_TEST_TIMEOUT-5s}" + timeout server "${HAPROXY_TEST_TIMEOUT-5s}" + + frontend fe + bind "fd@${fe}" + + http-response set-header bytes_output "%[var(txn.input),bytes(0,0)]" + + default_backend be + + backend be + server s1 ${s1_addr}:${s1_port} +} -start -expectexit 1 diff --git a/src/sample.c b/src/sample.c index 07c881dcf..bb7db28e7 100644 --- a/src/sample.c +++ b/src/sample.c @@ -2705,18 +2705,54 @@ static int sample_conv_json(const struct arg *arg_p, struct sample *smp, void *p * Optional second arg is the length to truncate */ static int sample_conv_bytes(const struct arg *arg_p, struct sample *smp, void *private) { - if (smp->data.u.str.data <= arg_p[0].data.sint) { + struct sample smp_arg0, smp_arg1; + long long start_idx, length; + + // determine the start_idx and length of the output + smp_set_owner(&smp_arg0, smp->px, smp->sess, smp->strm, smp->opt); + if (!sample_conv_var2smp_sint(&arg_p[0], &smp_arg0)) { smp->data.u.str.data = 0; - return 1; + return 0; + } + if (smp_arg0.data.u.sint < 0 || (smp_arg0.data.u.sint >= smp->data.u.str.data)) { + // empty output if the arg0 value is negative or >= the input length + smp->data.u.str.data = 0; + return 0; + } + start_idx = smp_arg0.data.u.sint; + + // length comes from arg1 if present, otherwise it's the remaining length + if (arg_p[1].type != ARGT_STOP) { + smp_set_owner(&smp_arg1, smp->px, smp->sess, smp->strm, smp->opt); + if (!sample_conv_var2smp_sint(&arg_p[1], &smp_arg1)) { + smp->data.u.str.data = 0; + return 0; + } + if (smp_arg1.data.u.sint < 0) { + // empty output if the arg1 value is negative + smp->data.u.str.data = 0; + return 0; + } + + if (smp_arg1.data.u.sint > (smp->data.u.str.data - start_idx)) { + // arg1 value is greater than the remaining length + if (smp->opt & SMP_OPT_FINAL) { + // truncate to remaining length + length = smp->data.u.str.data - start_idx; + } else { + smp->data.u.str.data = 0; + return 0; + } + } else { + length = smp_arg1.data.u.sint; + } + } else { + length = smp->data.u.str.data - start_idx; } - if (smp->data.u.str.size) - smp->data.u.str.size -= arg_p[0].data.sint; - smp->data.u.str.data -= arg_p[0].data.sint; - smp->data.u.str.area += arg_p[0].data.sint; - - if ((arg_p[1].type == ARGT_SINT) && (arg_p[1].data.sint < smp->data.u.str.data)) - smp->data.u.str.data = arg_p[1].data.sint; + // update the output using the start_idx and length + smp->data.u.str.area += start_idx; + smp->data.u.str.data = length; return 1; } @@ -4929,6 +4965,34 @@ static int smp_fetch_bytes(const struct arg *args, struct sample *smp, const cha return 1; } +static int sample_conv_bytes_check(struct arg *args, struct sample_conv *conv, + const char *file, int line, char **err) +{ + // arg0 is not optional, must be >= 0 + if (!check_operator(&args[0], conv, file, line, err)) { + return 0; + } + if (args[0].type != ARGT_VAR) { + if (args[0].type != ARGT_SINT || args[0].data.sint < 0) { + memprintf(err, "expects a non-negative integer"); + return 0; + } + } + // arg1 is optional, must be > 0 + if (args[1].type != ARGT_STOP) { + if (!check_operator(&args[1], conv, file, line, err)) { + return 0; + } + if (args[1].type != ARGT_VAR) { + if (args[1].type != ARGT_SINT || args[1].data.sint <= 0) { + memprintf(err, "expects a positive integer"); + return 0; + } + } + } + + return 1; +} static struct sample_fetch_kw_list smp_logs_kws = {ILH, { { "bytes_in", smp_fetch_bytes, 0, NULL, SMP_T_SINT, SMP_USE_INTRN }, @@ -5025,7 +5089,7 @@ static struct sample_conv_kw_list sample_conv_kws = {ILH, { { "xxh32", sample_conv_xxh32, ARG1(0,SINT), NULL, SMP_T_BIN, SMP_T_SINT }, { "xxh64", sample_conv_xxh64, ARG1(0,SINT), NULL, SMP_T_BIN, SMP_T_SINT }, { "json", sample_conv_json, ARG1(1,STR), sample_conv_json_check, SMP_T_STR, SMP_T_STR }, - { "bytes", sample_conv_bytes, ARG2(1,SINT,SINT), NULL, SMP_T_BIN, SMP_T_BIN }, + { "bytes", sample_conv_bytes, ARG2(1,STR,STR), sample_conv_bytes_check, SMP_T_BIN, SMP_T_BIN }, { "field", sample_conv_field, ARG3(2,SINT,STR,SINT), sample_conv_field_check, SMP_T_STR, SMP_T_STR }, { "word", sample_conv_word, ARG3(2,SINT,STR,SINT), sample_conv_field_check, SMP_T_STR, SMP_T_STR }, { "param", sample_conv_param, ARG2(1,STR,STR), sample_conv_param_check, SMP_T_STR, SMP_T_STR },