diff --git a/src/vars.c b/src/vars.c index 74ceb264f..5b28b8534 100644 --- a/src/vars.c +++ b/src/vars.c @@ -58,6 +58,16 @@ static struct var_set_condition conditions_array[] = { { NULL, 0 } }; +/* Variable scope names with their prefixes for output */ +static const char *var_scope_names[] = { + [SCOPE_SESS] = "sess.", + [SCOPE_TXN] = "txn.", + [SCOPE_REQ] = "req.", + [SCOPE_RES] = "res.", + [SCOPE_PROC] = "proc.", + [SCOPE_CHECK] = "check.", +}; + /* returns the struct vars pointer for a session, stream and scope, or NULL if * it does not exist. */ @@ -352,6 +362,171 @@ static int smp_fetch_var(const struct arg *args, struct sample *smp, const char return vars_get_by_desc(var_desc, smp, def); } +/* Dumps all variables in the specified scope, optionally filtered by prefix. + * Output format: var1=value1, var2=value2, ... + * String values are quoted and escaped, binary values are hex-encoded (x...). + * Returns 1 on success, 0 on failure (buffer too small). + * Note: When using prefix filtering, all variables are still visited, so this + * should not be used with configs involving thousands of variables. + */ +static int smp_fetch_dump_all_vars(const struct arg *args, struct sample *smp, const char *kw, void *private) +{ + struct buffer *output; + struct vars *vars; + struct var *var; + struct var_desc desc; + const char *prefix = NULL; + size_t prefix_len = 0; + const char *delim = ", "; + size_t delim_len = 2; + int first = 1; + int i; + + /* Get output buffer */ + output = get_trash_chunk(); + chunk_reset(output); + + /* Parse arguments */ + if (args[0].type == ARGT_SINT) { + desc.scope = args[0].data.sint; + } else { + /* Auto-detect scope from context*/ + if (smp->strm) + desc.scope = SCOPE_TXN; + else if (smp->sess) + desc.scope = SCOPE_SESS; + else + desc.scope = SCOPE_PROC; + } + + /* Optional prefix filter */ + if (args[1].type == ARGT_STR) { + prefix = args[1].data.str.area; + prefix_len = args[1].data.str.data; + } + + /* Optional delimiter */ + if (args[2].type == ARGT_STR) { + delim = args[2].data.str.area; + delim_len = args[2].data.str.data; + } + + desc.flags = 0; + desc.name_hash = 0; + + vars = get_vars(smp->sess, smp->strm, &desc); + if (!vars || vars->scope != desc.scope) + return 0; + + vars_rdlock(vars); + + /* Iterate through all variable roots */ + for (i = 0; i < VAR_NAME_ROOTS; i++) { + var = cebu64_item_first(&vars->name_root[i], name_node, name_hash, struct var); + + while (var) { + const char *scope_prefix; + + /* Check prefix filter */ + if (prefix) { + if (!var->name || strncmp(var->name, prefix, prefix_len) != 0) { + var = cebu64_item_next(&vars->name_root[i], name_node, name_hash, var); + continue; + } + } + + /* Add delimiter */ + if (!first) { + if (output->data + delim_len >= output->size) + goto fail_unlock; + chunk_memcat(output, delim, delim_len); + } + first = 0; + + /* Add variable name with scope prefix */ + scope_prefix = var_scope_names[desc.scope]; + if (var->name) { + if (chunk_appendf(output, "%s%s=", scope_prefix, var->name) < 0) + goto fail_unlock; + } else { + if (chunk_appendf(output, "var_%016llx=", (unsigned long long)var->name_hash) < 0) + goto fail_unlock; + } + + /* Convert value based on type */ + if (var->data.type == SMP_T_STR) { + /* String: quote and escape */ + if (chunk_escape_string(output, var->data.u.str.area, var->data.u.str.data) < 0) + goto fail_unlock; + + } else if (var-> data.type == SMP_T_BIN) { + /* Binary: hex encode */ + if (dump_binary(output, var->data.u.str.area, var->data.u.str.data) != var->data.u.str.data) + goto fail_unlock; + } else if (var->data.type == SMP_T_SINT) { + /* Integer */ + if (chunk_appendf(output, "%lld", (long long)var->data.u.sint) < 0) + goto fail_unlock; + + } else if (var->data.type == SMP_T_BOOL) { + /* Boolean */ + const char *bool_str = var->data.u.sint ? "true" : "false"; + if (chunk_appendf(output, "%s", bool_str) < 0) + goto fail_unlock; + + } else if (var->data.type == SMP_T_IPV4 || var->data.type == SMP_T_IPV6) { + /* Address */ + char addr_str[INET6_ADDRSTRLEN]; + const char *res; + + if (var->data.type == SMP_T_IPV4) + res = inet_ntop(AF_INET, &var->data.u.ipv4, addr_str, sizeof(addr_str)); + else + res = inet_ntop(AF_INET6, &var->data.u.ipv6, addr_str, sizeof(addr_str)); + + if (!res) { + if (chunk_appendf(output, "(addr)") < 0) + goto fail_unlock; + } else { + if (chunk_appendf(output, "%s", addr_str) < 0) + goto fail_unlock; + } + } else if (var->data.type == SMP_T_METH) { + /* HTTP Method */ + if (var->data.u.meth.meth == HTTP_METH_OTHER) { + if (chunk_escape_string(output, var->data.u.meth.str.area, var->data.u.meth.str.data) < 0) + goto fail_unlock; + } else { + const char *method_str = http_known_methods[var->data.u.meth.meth].ptr; + if (chunk_appendf(output, "\"%s\"", method_str) < 0) + goto fail_unlock; + } + } else { + /* Other types: show type number */ + if (chunk_appendf(output, "(type:%d)", var->data.type) < 0) + goto fail_unlock; + } + + var = cebu64_item_next(&vars->name_root[i], name_node, name_hash, var); + } + } + + vars_rdunlock(vars); + + /* Set output sample */ + smp->data.type = SMP_T_STR; + smp->data.u.str.area = output->area; + smp->data.u.str.data = output->data; + smp->flags &= ~SMP_F_CONST; + + return 1; + +fail_unlock: + vars_rdunlock(vars); + output->data = 0; + return 0; +} + /* * Clear the contents of a variable so that it can be reset directly. * This function is used just before a variable is filled out of a sample's @@ -916,6 +1091,60 @@ static int smp_check_var(struct arg *args, char **err) return vars_check_arg(&args[0], err); } +/* This function checks all arguments for dump_all_vars() + * Args: [scope], [prefix], [delimiter] + * Both arguments are optional + */ +static int smp_check_dump_all_vars(struct arg *args, char **err) +{ + /* First argument (scope) is optional */ + + if (args[0].type == ARGT_STR) { + const char *scope = args[0].data.str.area; + int scope_id = -1; + int i; + char buf[16]; + + if (args[0].data.str.data < sizeof(buf) - 1) { + snprintf(buf, sizeof(buf), "%s.", scope); + + for (i = 0; i <= SCOPE_CHECK; i++) { + if (strcmp(buf, var_scope_names[i]) == 0) { + scope_id = i; + break; + } + } + } + + if (scope_id == -1) { + memprintf(err, "invalid scope '%s', must be one of: sess, txn, req, res, proc", scope); + return 0; + } + + chunk_destroy(&args[0].data.str); + args[0].type = ARGT_SINT; + args[0].data.sint = scope_id; + } + else if (args[0].type != ARGT_STOP) { + memprintf(err, "first argument must be a string (scope) or omitted"); + return 0; + } + + /* Second argument (prefix) is optional */ + if (args[1].type != ARGT_STR && args[1].type != ARGT_STOP) { + memprintf(err, "second argument must be a string (prefix) or omitted"); + return 0; + } + + /* Third argument (delimiter) is optional */ + if (args[2].type != ARGT_STR && args[2].type != ARGT_STOP) { + memprintf(err, "third argument must be a string (delimiter) or omitted"); + return 0; + } + + return 1; +} + static int conv_check_var(struct arg *args, struct sample_conv *conv, const char *file, int line, char **err_msg) { @@ -1410,6 +1639,7 @@ INITCALL0(STG_PREPARE, vars_init); static struct sample_fetch_kw_list sample_fetch_keywords = {ILH, { + { "dump_all_vars", smp_fetch_dump_all_vars, ARG3(0,STR,STR,STR), smp_check_dump_all_vars, SMP_T_STR, SMP_USE_CONST }, { "var", smp_fetch_var, ARG2(1,STR,STR), smp_check_var, SMP_T_ANY, SMP_USE_CONST }, { /* END */ }, }};