From b46aad6df449445a9fc4aa7b32bd40005438e3f7 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 13 Apr 2024 14:18:05 +0200 Subject: Adding upstream version 2.9.5. Signed-off-by: Daniel Baumann --- src/vars.c | 1454 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1454 insertions(+) create mode 100644 src/vars.c (limited to 'src/vars.c') diff --git a/src/vars.c b/src/vars.c new file mode 100644 index 0000000..7ec753e --- /dev/null +++ b/src/vars.c @@ -0,0 +1,1454 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +/* This contains a pool of struct vars */ +DECLARE_STATIC_POOL(var_pool, "vars", sizeof(struct var)); + +/* list of variables for the process scope. */ +struct vars proc_vars THREAD_ALIGNED(64); + +/* This array of int contains the system limits per context. */ +static unsigned int var_global_limit = 0; +static unsigned int var_proc_limit = 0; +static unsigned int var_sess_limit = 0; +static unsigned int var_txn_limit = 0; +static unsigned int var_reqres_limit = 0; +static unsigned int var_check_limit = 0; +static uint64_t var_name_hash_seed = 0; + +/* Structure and array matching set-var conditions to their respective flag + * value. + */ +struct var_set_condition { + const char *cond_str; + uint flag; +}; + +static struct var_set_condition conditions_array[] = { + { "ifexists", VF_COND_IFEXISTS }, + { "ifnotexists", VF_COND_IFNOTEXISTS }, + { "ifempty", VF_COND_IFEMPTY }, + { "ifnotempty", VF_COND_IFNOTEMPTY }, + { "ifset", VF_COND_IFSET }, + { "ifnotset", VF_COND_IFNOTSET }, + { "ifgt", VF_COND_IFGT }, + { "iflt", VF_COND_IFLT }, + { NULL, 0 } +}; + +/* returns the struct vars pointer for a session, stream and scope, or NULL if + * it does not exist. + */ +static inline struct vars *get_vars(struct session *sess, struct stream *strm, enum vars_scope scope) +{ + switch (scope) { + case SCOPE_PROC: + return &proc_vars; + case SCOPE_SESS: + return sess ? &sess->vars : NULL; + case SCOPE_CHECK: { + struct check *check = sess ? objt_check(sess->origin) : NULL; + + return check ? &check->vars : NULL; + } + case SCOPE_TXN: + return strm ? &strm->vars_txn : NULL; + case SCOPE_REQ: + case SCOPE_RES: + default: + return strm ? &strm->vars_reqres : NULL; + } +} + +/* This function adds or remove memory size from the accounting. The inner + * pointers may be null when setting the outer ones only. + */ +void var_accounting_diff(struct vars *vars, struct session *sess, struct stream *strm, int size) +{ + switch (vars->scope) { + case SCOPE_REQ: + case SCOPE_RES: + if (var_reqres_limit && strm) + _HA_ATOMIC_ADD(&strm->vars_reqres.size, size); + __fallthrough; + case SCOPE_TXN: + if (var_txn_limit && strm) + _HA_ATOMIC_ADD(&strm->vars_txn.size, size); + goto scope_sess; + case SCOPE_CHECK: + if (var_check_limit) { + struct check *check = objt_check(sess->origin); + + if (check) + _HA_ATOMIC_ADD(&check->vars.size, size); + } +scope_sess: + __fallthrough; + case SCOPE_SESS: + if (var_sess_limit) + _HA_ATOMIC_ADD(&sess->vars.size, size); + __fallthrough; + case SCOPE_PROC: + if (var_proc_limit || var_global_limit) + _HA_ATOMIC_ADD(&proc_vars.size, size); + } +} + +/* This function returns 1 if the is available in the var + * pool , otherwise returns 0. If the space is available, + * the size is reserved. The inner pointers may be null when setting + * the outer ones only. The accounting uses either or + * depending on the scope. may be NULL when no stream is known + * and only the session exists (eg: tcp-request connection). + */ +static int var_accounting_add(struct vars *vars, struct session *sess, struct stream *strm, int size) +{ + switch (vars->scope) { + case SCOPE_REQ: + case SCOPE_RES: + if (var_reqres_limit && strm && strm->vars_reqres.size + size > var_reqres_limit) + return 0; + __fallthrough; + case SCOPE_TXN: + if (var_txn_limit && strm && strm->vars_txn.size + size > var_txn_limit) + return 0; + goto scope_sess; + case SCOPE_CHECK: { + struct check *check = objt_check(sess->origin); + + if (var_check_limit && check && check->vars.size + size > var_check_limit) + return 0; + } +scope_sess: + __fallthrough; + case SCOPE_SESS: + if (var_sess_limit && sess->vars.size + size > var_sess_limit) + return 0; + __fallthrough; + case SCOPE_PROC: + /* note: scope proc collects all others and is currently identical to the + * global limit. + */ + if (var_proc_limit && proc_vars.size + size > var_proc_limit) + return 0; + if (var_global_limit && proc_vars.size + size > var_global_limit) + return 0; + } + var_accounting_diff(vars, sess, strm, size); + return 1; +} + +/* This function removes a variable from the list and frees the memory it was + * using. If the variable is marked "VF_PERMANENT", the sample_data is only + * reset to SMP_T_ANY unless is non nul. Returns the freed size. + */ +unsigned int var_clear(struct var *var, int force) +{ + unsigned int size = 0; + + if (var->data.type == SMP_T_STR || var->data.type == SMP_T_BIN) { + ha_free(&var->data.u.str.area); + size += var->data.u.str.data; + } + else if (var->data.type == SMP_T_METH && var->data.u.meth.meth == HTTP_METH_OTHER) { + ha_free(&var->data.u.meth.str.area); + size += var->data.u.meth.str.data; + } + /* wipe the sample */ + var->data.type = SMP_T_ANY; + + if (!(var->flags & VF_PERMANENT) || force) { + LIST_DELETE(&var->l); + pool_free(var_pool, var); + size += sizeof(struct var); + } + return size; +} + +/* This function free all the memory used by all the variables + * in the list. + */ +void vars_prune(struct vars *vars, struct session *sess, struct stream *strm) +{ + struct var *var, *tmp; + unsigned int size = 0; + + vars_wrlock(vars); + list_for_each_entry_safe(var, tmp, &vars->head, l) { + size += var_clear(var, 1); + } + vars_wrunlock(vars); + var_accounting_diff(vars, sess, strm, -size); +} + +/* This function frees all the memory used by all the session variables in the + * list starting at . + */ +void vars_prune_per_sess(struct vars *vars) +{ + struct var *var, *tmp; + unsigned int size = 0; + + vars_wrlock(vars); + list_for_each_entry_safe(var, tmp, &vars->head, l) { + size += var_clear(var, 1); + } + vars_wrunlock(vars); + + if (var_sess_limit) + _HA_ATOMIC_SUB(&vars->size, size); + if (var_proc_limit || var_global_limit) + _HA_ATOMIC_SUB(&proc_vars.size, size); +} + +/* This function initializes a variables list head */ +void vars_init_head(struct vars *vars, enum vars_scope scope) +{ + LIST_INIT(&vars->head); + vars->scope = scope; + vars->size = 0; + HA_RWLOCK_INIT(&vars->rwlock); +} + +/* This function returns a hash value and a scope for a variable name of a + * specified length. It makes sure that the scope is valid. It returns non-zero + * on success, 0 on failure. Neither hash nor scope may be NULL. + */ +static int vars_hash_name(const char *name, int len, enum vars_scope *scope, + uint64_t *hash, char **err) +{ + const char *tmp; + + /* Check length. */ + if (len == 0) { + memprintf(err, "Empty variable name cannot be accepted"); + return 0; + } + + /* Check scope. */ + if (len > 5 && strncmp(name, "proc.", 5) == 0) { + name += 5; + len -= 5; + *scope = SCOPE_PROC; + } + else if (len > 5 && strncmp(name, "sess.", 5) == 0) { + name += 5; + len -= 5; + *scope = SCOPE_SESS; + } + else if (len > 4 && strncmp(name, "txn.", 4) == 0) { + name += 4; + len -= 4; + *scope = SCOPE_TXN; + } + else if (len > 4 && strncmp(name, "req.", 4) == 0) { + name += 4; + len -= 4; + *scope = SCOPE_REQ; + } + else if (len > 4 && strncmp(name, "res.", 4) == 0) { + name += 4; + len -= 4; + *scope = SCOPE_RES; + } + else if (len > 6 && strncmp(name, "check.", 6) == 0) { + name += 6; + len -= 6; + *scope = SCOPE_CHECK; + } + else { + memprintf(err, "invalid variable name '%.*s'. A variable name must be start by its scope. " + "The scope can be 'proc', 'sess', 'txn', 'req', 'res' or 'check'", len, name); + return 0; + } + + /* Check variable name syntax. */ + for (tmp = name; tmp < name + len; tmp++) { + if (!isalnum((unsigned char)*tmp) && *tmp != '_' && *tmp != '.') { + memprintf(err, "invalid syntax at char '%s'", tmp); + return 0; + } + } + + *hash = XXH3(name, len, var_name_hash_seed); + return 1; +} + +/* This function returns the variable from the given list that matches + * or returns NULL if not found. It's only a linked list since it + * is not expected to have many variables per scope (a few tens at best). + * The caller is responsible for ensuring that is properly locked. + */ +static struct var *var_get(struct vars *vars, uint64_t name_hash) +{ + struct var *var; + + list_for_each_entry(var, &vars->head, l) + if (var->name_hash == name_hash) + return var; + return NULL; +} + +/* Returns 0 if fails, else returns 1. */ +static int smp_fetch_var(const struct arg *args, struct sample *smp, const char *kw, void *private) +{ + const struct var_desc *var_desc = &args[0].data.var; + const struct buffer *def = NULL; + + if (args[1].type == ARGT_STR) + def = &args[1].data.str; + + return vars_get_by_desc(var_desc, smp, def); +} + +/* + * 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 + * content. + */ +static inline void var_clear_buffer(struct sample *smp, struct vars *vars, struct var *var, int var_type) +{ + if (var_type == SMP_T_STR || var_type == SMP_T_BIN) { + ha_free(&var->data.u.str.area); + var_accounting_diff(vars, smp->sess, smp->strm, + -var->data.u.str.data); + } + else if (var_type == SMP_T_METH && var->data.u.meth.meth == HTTP_METH_OTHER) { + ha_free(&var->data.u.meth.str.area); + var_accounting_diff(vars, smp->sess, smp->strm, + -var->data.u.meth.str.data); + } +} + +/* This function tries to create a variable whose name hash is in + * scope and store sample as its value. + * + * The stream and session are extracted from , whose stream may be NULL + * when scope is SCOPE_SESS. In case there wouldn't be enough memory to store + * the sample while the variable was already created, it would be changed to + * a bool (which is memory-less). + * + * Flags is a bitfield that may contain one of the following flags: + * - VF_CREATEONLY: do nothing if the variable already exists (success). + * - VF_PERMANENT: this flag will be passed to the variable upon creation + * + * - VF_COND_IFEXISTS: only set variable if it already exists + * - VF_COND_IFNOTEXISTS: only set variable if it did not exist yet + * - VF_COND_IFEMPTY: only set variable if sample is empty + * - VF_COND_IFNOTEMPTY: only set variable if sample is not empty + * - VF_COND_IFSET: only set variable if its type is not SMP_TYPE_ANY + * - VF_COND_IFNOTSET: only set variable if its type is ANY + * - VF_COND_IFGT: only set variable if its value is greater than the sample's + * - VF_COND_IFLT: only set variable if its value is less than the sample's + * + * It returns 0 on failure, non-zero on success. + */ +static int var_set(uint64_t name_hash, enum vars_scope scope, struct sample *smp, uint flags) +{ + struct vars *vars; + struct var *var; + int ret = 0; + int previous_type = SMP_T_ANY; + + vars = get_vars(smp->sess, smp->strm, scope); + if (!vars || vars->scope != scope) + return 0; + + vars_wrlock(vars); + + /* Look for existing variable name. */ + var = var_get(vars, name_hash); + + if (var) { + if (flags & VF_CREATEONLY) { + ret = 1; + goto unlock; + } + + if (flags & VF_COND_IFNOTEXISTS) + goto unlock; + } else { + if (flags & VF_COND_IFEXISTS) + goto unlock; + + /* Check memory available. */ + if (!var_accounting_add(vars, smp->sess, smp->strm, sizeof(struct var))) + goto unlock; + + /* Create new entry. */ + var = pool_alloc(var_pool); + if (!var) + goto unlock; + LIST_APPEND(&vars->head, &var->l); + var->name_hash = name_hash; + var->flags = flags & VF_PERMANENT; + var->data.type = SMP_T_ANY; + } + + /* A variable of type SMP_T_ANY is considered as unset (either created + * and never set or unset-var was called on it). + */ + if ((flags & VF_COND_IFSET && var->data.type == SMP_T_ANY) || + (flags & VF_COND_IFNOTSET && var->data.type != SMP_T_ANY)) + goto unlock; + + /* Set type. */ + previous_type = var->data.type; + var->data.type = smp->data.type; + + if (flags & VF_COND_IFEMPTY) { + switch(smp->data.type) { + case SMP_T_ANY: + case SMP_T_STR: + case SMP_T_BIN: + /* The actual test on the contents of the sample will be + * performed later. + */ + break; + default: + /* The sample cannot be empty since it has a scalar type. */ + var->data.type = previous_type; + goto unlock; + } + } + + /* Copy data. If the data needs memory, the function can fail. */ + switch (var->data.type) { + case SMP_T_BOOL: + var_clear_buffer(smp, vars, var, previous_type); + var->data.u.sint = smp->data.u.sint; + break; + case SMP_T_SINT: + if (previous_type == var->data.type) { + if (((flags & VF_COND_IFGT) && !(var->data.u.sint > smp->data.u.sint)) || + ((flags & VF_COND_IFLT) && !(var->data.u.sint < smp->data.u.sint))) + goto unlock; + } + var_clear_buffer(smp, vars, var, previous_type); + var->data.u.sint = smp->data.u.sint; + break; + case SMP_T_IPV4: + var_clear_buffer(smp, vars, var, previous_type); + var->data.u.ipv4 = smp->data.u.ipv4; + break; + case SMP_T_IPV6: + var_clear_buffer(smp, vars, var, previous_type); + var->data.u.ipv6 = smp->data.u.ipv6; + break; + case SMP_T_STR: + case SMP_T_BIN: + if ((flags & VF_COND_IFNOTEMPTY && !smp->data.u.str.data) || + (flags & VF_COND_IFEMPTY && smp->data.u.str.data)) { + var->data.type = previous_type; + goto unlock; + } + var_clear_buffer(smp, vars, var, previous_type); + if (!var_accounting_add(vars, smp->sess, smp->strm, smp->data.u.str.data)) { + var->data.type = SMP_T_BOOL; /* This type doesn't use additional memory. */ + goto unlock; + } + + var->data.u.str.area = malloc(smp->data.u.str.data); + if (!var->data.u.str.area) { + var_accounting_diff(vars, smp->sess, smp->strm, + -smp->data.u.str.data); + var->data.type = SMP_T_BOOL; /* This type doesn't use additional memory. */ + goto unlock; + } + var->data.u.str.data = smp->data.u.str.data; + memcpy(var->data.u.str.area, smp->data.u.str.area, + var->data.u.str.data); + break; + case SMP_T_METH: + var_clear_buffer(smp, vars, var, previous_type); + var->data.u.meth.meth = smp->data.u.meth.meth; + if (smp->data.u.meth.meth != HTTP_METH_OTHER) + break; + + if (!var_accounting_add(vars, smp->sess, smp->strm, smp->data.u.meth.str.data)) { + var->data.type = SMP_T_BOOL; /* This type doesn't use additional memory. */ + goto unlock; + } + + var->data.u.meth.str.area = malloc(smp->data.u.meth.str.data); + if (!var->data.u.meth.str.area) { + var_accounting_diff(vars, smp->sess, smp->strm, + -smp->data.u.meth.str.data); + var->data.type = SMP_T_BOOL; /* This type doesn't use additional memory. */ + goto unlock; + } + var->data.u.meth.str.data = smp->data.u.meth.str.data; + var->data.u.meth.str.size = smp->data.u.meth.str.data; + memcpy(var->data.u.meth.str.area, smp->data.u.meth.str.area, + var->data.u.meth.str.data); + break; + } + + /* OK, now done */ + ret = 1; + unlock: + vars_wrunlock(vars); + return ret; +} + +/* Deletes a variable matching name hash and scope for the + * session and stream found in . Note that stream may be null for + * SCOPE_SESS. Returns 0 if the scope was not found otherwise 1. + */ +static int var_unset(uint64_t name_hash, enum vars_scope scope, struct sample *smp) +{ + struct vars *vars; + struct var *var; + unsigned int size = 0; + + vars = get_vars(smp->sess, smp->strm, scope); + if (!vars || vars->scope != scope) + return 0; + + /* Look for existing variable name. */ + vars_wrlock(vars); + var = var_get(vars, name_hash); + if (var) { + size = var_clear(var, 0); + var_accounting_diff(vars, smp->sess, smp->strm, -size); + } + vars_wrunlock(vars); + return 1; +} + + +/* + * Convert a string set-var condition into its numerical value. + * The corresponding bit is set in the parameter if the + * is known. + * Returns 1 in case of success. + */ +static int vars_parse_cond_param(const struct buffer *cond, uint *cond_bitmap, char **err) +{ + struct var_set_condition *cond_elt = &conditions_array[0]; + + /* The conditions array is NULL terminated. */ + while (cond_elt->cond_str) { + if (chunk_strcmp(cond, cond_elt->cond_str) == 0) { + *cond_bitmap |= cond_elt->flag; + break; + } + ++cond_elt; + } + + if (cond_elt->cond_str == NULL && err) + memprintf(err, "unknown condition \"%.*s\"", (int)cond->data, cond->area); + + return cond_elt->cond_str != NULL; +} + +/* Returns 0 if fails, else returns 1. */ +static int smp_conv_store(const struct arg *args, struct sample *smp, void *private) +{ + uint conditions = 0; + int cond_idx = 1; + + while (args[cond_idx].type == ARGT_STR) { + if (vars_parse_cond_param(&args[cond_idx++].data.str, &conditions, NULL) == 0) + break; + } + + return var_set(args[0].data.var.name_hash, args[0].data.var.scope, smp, conditions); +} + +/* Returns 0 if fails, else returns 1. */ +static int smp_conv_clear(const struct arg *args, struct sample *smp, void *private) +{ + return var_unset(args[0].data.var.name_hash, args[0].data.var.scope, smp); +} + +/* This functions check an argument entry and fill it with a variable + * type. The argument must be a string. If the variable lookup fails, + * the function returns 0 and fill , otherwise it returns 1. + */ +int vars_check_arg(struct arg *arg, char **err) +{ + enum vars_scope scope; + struct sample empty_smp = { }; + uint64_t hash; + + /* Check arg type. */ + if (arg->type != ARGT_STR) { + memprintf(err, "unexpected argument type"); + return 0; + } + + /* Register new variable name. */ + if (!vars_hash_name(arg->data.str.area, arg->data.str.data, &scope, &hash, err)) + return 0; + + if (scope == SCOPE_PROC && !var_set(hash, scope, &empty_smp, VF_CREATEONLY|VF_PERMANENT)) + return 0; + + /* properly destroy the chunk */ + chunk_destroy(&arg->data.str); + + /* Use the global variable name pointer. */ + arg->type = ARGT_VAR; + arg->data.var.name_hash = hash; + arg->data.var.scope = scope; + return 1; +} + +/* This function stores a sample in a variable unless it is of type "proc" and + * not defined yet. + * Returns zero on failure and non-zero otherwise. The variable not being + * defined is treated as a failure. + */ +int vars_set_by_name_ifexist(const char *name, size_t len, struct sample *smp) +{ + enum vars_scope scope; + uint64_t hash; + + /* Resolve name and scope. */ + if (!vars_hash_name(name, len, &scope, &hash, NULL)) + return 0; + + /* Variable creation is allowed for all scopes apart from the PROC one. */ + return var_set(hash, scope, smp, (scope == SCOPE_PROC) ? VF_COND_IFEXISTS : 0); +} + + +/* This function stores a sample in a variable. + * Returns zero on failure and non-zero otherwise. + */ +int vars_set_by_name(const char *name, size_t len, struct sample *smp) +{ + enum vars_scope scope; + uint64_t hash; + + /* Resolve name and scope. */ + if (!vars_hash_name(name, len, &scope, &hash, NULL)) + return 0; + + return var_set(hash, scope, smp, 0); +} + +/* This function unsets a variable if it was already defined. + * Returns zero on failure and non-zero otherwise. + */ +int vars_unset_by_name_ifexist(const char *name, size_t len, struct sample *smp) +{ + enum vars_scope scope; + uint64_t hash; + + /* Resolve name and scope. */ + if (!vars_hash_name(name, len, &scope, &hash, NULL)) + return 0; + + return var_unset(hash, scope, smp); +} + + +/* This retrieves variable whose hash matches from variables , + * and if found and not empty, duplicates the result into sample . + * smp_dup() is used in order to release the variables lock ASAP (so a pre- + * allocated chunk is obtained via get_trash_shunk()). The variables' lock is + * used for reads. + * + * The function returns 0 if the variable was not found and no default + * value was provided in , otherwise 1 with the sample filled. + * Default values are always returned as strings. + */ +static int var_to_smp(struct vars *vars, uint64_t name_hash, struct sample *smp, const struct buffer *def) +{ + struct var *var; + + /* Get the variable entry. */ + vars_rdlock(vars); + var = var_get(vars, name_hash); + if (!var || !var->data.type) { + if (!def) { + vars_rdunlock(vars); + return 0; + } + + /* not found but we have a default value */ + smp->data.type = SMP_T_STR; + smp->data.u.str = *def; + } + else + smp->data = var->data; + + /* Copy sample. */ + smp_dup(smp); + + vars_rdunlock(vars); + return 1; +} + +/* This function fills a sample with the variable content. + * + * Keep in mind that a sample content is duplicated by using smp_dup() + * and it therefore uses a pre-allocated trash chunk as returned by + * get_trash_chunk(). + * + * If the variable is not valid in this scope, 0 is always returned. + * If the variable is valid but not found, either the default value + * is returned if not NULL, or zero is returned. + * + * Returns 1 if the sample is filled, otherwise it returns 0. + */ +int vars_get_by_name(const char *name, size_t len, struct sample *smp, const struct buffer *def) +{ + struct vars *vars; + enum vars_scope scope; + uint64_t hash; + + /* Resolve name and scope. */ + if (!vars_hash_name(name, len, &scope, &hash, NULL)) + return 0; + + /* Select "vars" pool according with the scope. */ + vars = get_vars(smp->sess, smp->strm, scope); + if (!vars || vars->scope != scope) + return 0; + + return var_to_smp(vars, hash, smp, def); +} + +/* This function fills a sample with the content of the variable described + * by . + * + * Keep in mind that a sample content is duplicated by using smp_dup() + * and it therefore uses a pre-allocated trash chunk as returned by + * get_trash_chunk(). + * + * If the variable is not valid in this scope, 0 is always returned. + * If the variable is valid but not found, either the default value + * is returned if not NULL, or zero is returned. + * + * Returns 1 if the sample is filled, otherwise it returns 0. + */ +int vars_get_by_desc(const struct var_desc *var_desc, struct sample *smp, const struct buffer *def) +{ + struct vars *vars; + + /* Select "vars" pool according with the scope. */ + vars = get_vars(smp->sess, smp->strm, var_desc->scope); + + /* Check if the scope is available a this point of processing. */ + if (!vars || vars->scope != var_desc->scope) + return 0; + + return var_to_smp(vars, var_desc->name_hash, smp, def); +} + +/* Always returns ACT_RET_CONT even if an error occurs. */ +static enum act_return action_store(struct act_rule *rule, struct proxy *px, + struct session *sess, struct stream *s, int flags) +{ + struct buffer *fmtstr = NULL; + struct sample smp; + int dir; + + switch (rule->from) { + case ACT_F_TCP_REQ_CON: dir = SMP_OPT_DIR_REQ; break; + case ACT_F_TCP_REQ_SES: dir = SMP_OPT_DIR_REQ; break; + case ACT_F_TCP_REQ_CNT: dir = SMP_OPT_DIR_REQ; break; + case ACT_F_TCP_RES_CNT: dir = SMP_OPT_DIR_RES; break; + case ACT_F_HTTP_REQ: dir = SMP_OPT_DIR_REQ; break; + case ACT_F_HTTP_RES: dir = SMP_OPT_DIR_RES; break; + case ACT_F_TCP_CHK: dir = SMP_OPT_DIR_REQ; break; + case ACT_F_CFG_PARSER: dir = SMP_OPT_DIR_REQ; break; /* not used anyway */ + case ACT_F_CLI_PARSER: dir = SMP_OPT_DIR_REQ; break; /* not used anyway */ + default: + send_log(px, LOG_ERR, "Vars: internal error while execute action store."); + if (!(global.mode & MODE_QUIET) || (global.mode & MODE_VERBOSE)) + ha_alert("Vars: internal error while execute action store.\n"); + return ACT_RET_CONT; + } + + /* Process the expression. */ + memset(&smp, 0, sizeof(smp)); + + if (!LIST_ISEMPTY(&rule->arg.vars.fmt)) { + /* a format-string is used */ + + fmtstr = alloc_trash_chunk(); + if (!fmtstr) { + send_log(px, LOG_ERR, "Vars: memory allocation failure while processing store rule."); + if (!(global.mode & MODE_QUIET) || (global.mode & MODE_VERBOSE)) + ha_alert("Vars: memory allocation failure while processing store rule.\n"); + return ACT_RET_CONT; + } + + /* execute the log-format expression */ + fmtstr->data = sess_build_logline(sess, s, fmtstr->area, fmtstr->size, &rule->arg.vars.fmt); + + /* convert it to a sample of type string as it's what the vars + * API consumes, and store it. + */ + smp_set_owner(&smp, px, sess, s, 0); + smp.data.type = SMP_T_STR; + smp.data.u.str = *fmtstr; + var_set(rule->arg.vars.name_hash, rule->arg.vars.scope, &smp, rule->arg.vars.conditions); + } + else { + /* an expression is used */ + if (!sample_process(px, sess, s, dir|SMP_OPT_FINAL, + rule->arg.vars.expr, &smp)) + return ACT_RET_CONT; + } + + /* Store the sample, and ignore errors. */ + var_set(rule->arg.vars.name_hash, rule->arg.vars.scope, &smp, rule->arg.vars.conditions); + free_trash_chunk(fmtstr); + return ACT_RET_CONT; +} + +/* Always returns ACT_RET_CONT even if an error occurs. */ +static enum act_return action_clear(struct act_rule *rule, struct proxy *px, + struct session *sess, struct stream *s, int flags) +{ + struct sample smp; + + memset(&smp, 0, sizeof(smp)); + smp_set_owner(&smp, px, sess, s, SMP_OPT_FINAL); + + /* Clear the variable using the sample context, and ignore errors. */ + var_unset(rule->arg.vars.name_hash, rule->arg.vars.scope, &smp); + return ACT_RET_CONT; +} + +static void release_store_rule(struct act_rule *rule) +{ + struct logformat_node *lf, *lfb; + + list_for_each_entry_safe(lf, lfb, &rule->arg.vars.fmt, list) { + LIST_DELETE(&lf->list); + release_sample_expr(lf->expr); + free(lf->arg); + free(lf); + } + + release_sample_expr(rule->arg.vars.expr); +} + +/* This two function checks the variable name and replace the + * configuration string name by the global string name. its + * the same string, but the global pointer can be easy to + * compare. They return non-zero on success, zero on failure. + * + * The first function checks a sample-fetch and the second + * checks a converter. + */ +static int smp_check_var(struct arg *args, char **err) +{ + return vars_check_arg(&args[0], err); +} + +static int conv_check_var(struct arg *args, struct sample_conv *conv, + const char *file, int line, char **err_msg) +{ + int cond_idx = 1; + uint conditions = 0; + int retval = vars_check_arg(&args[0], err_msg); + + while (retval && args[cond_idx].type == ARGT_STR) + retval = vars_parse_cond_param(&args[cond_idx++].data.str, &conditions, err_msg); + + return retval; +} + +/* This function is a common parser for using variables. It understands + * the format: + * + * set-var-fmt([, ...]) + * set-var([, ...]) + * unset-var() + * + * It returns ACT_RET_PRS_ERR if fails and is filled with an error + * message. Otherwise, it returns ACT_RET_PRS_OK and the variable + * is filled with the pointer to the expression to execute. The proxy is + * only used to retrieve the ->conf entries. + */ +static enum act_parse_ret parse_store(const char **args, int *arg, struct proxy *px, + struct act_rule *rule, char **err) +{ + const char *var_name = args[*arg-1]; + int var_len; + const char *kw_name; + int flags = 0, set_var = 0; /* 0=unset-var, 1=set-var, 2=set-var-fmt */ + struct sample empty_smp = { }; + struct ist condition = IST_NULL; + struct ist var = IST_NULL; + struct ist varname_ist = IST_NULL; + + if (strncmp(var_name, "set-var-fmt", 11) == 0) { + var_name += 11; + set_var = 2; + } + else if (strncmp(var_name, "set-var", 7) == 0) { + var_name += 7; + set_var = 1; + } + else if (strncmp(var_name, "unset-var", 9) == 0) { + var_name += 9; + set_var = 0; + } + + if (*var_name != '(') { + memprintf(err, "invalid or incomplete action '%s'. Expects 'set-var()', 'set-var-fmt()' or 'unset-var()'", + args[*arg-1]); + return ACT_RET_PRS_ERR; + } + var_name++; /* jump the '(' */ + var_len = strlen(var_name); + var_len--; /* remove the ')' */ + if (var_name[var_len] != ')') { + memprintf(err, "incomplete argument after action '%s'. Expects 'set-var()', 'set-var-fmt()' or 'unset-var()'", + args[*arg-1]); + return ACT_RET_PRS_ERR; + } + + /* Parse the optional conditions. */ + var = ist2(var_name, var_len); + varname_ist = istsplit(&var, ','); + var_len = istlen(varname_ist); + + condition = istsplit(&var, ','); + + if (istlen(condition) && set_var == 0) { + memprintf(err, "unset-var does not expect parameters after the variable name. Only \"set-var\" and \"set-var-fmt\" manage conditions"); + return ACT_RET_PRS_ERR; + } + + while (istlen(condition)) { + struct buffer cond = {}; + + chunk_initlen(&cond, istptr(condition), 0, istlen(condition)); + if (vars_parse_cond_param(&cond, &rule->arg.vars.conditions, err) == 0) + return ACT_RET_PRS_ERR; + + condition = istsplit(&var, ','); + } + + LIST_INIT(&rule->arg.vars.fmt); + if (!vars_hash_name(var_name, var_len, &rule->arg.vars.scope, &rule->arg.vars.name_hash, err)) + return ACT_RET_PRS_ERR; + + if (rule->arg.vars.scope == SCOPE_PROC && + !var_set(rule->arg.vars.name_hash, rule->arg.vars.scope, &empty_smp, VF_CREATEONLY|VF_PERMANENT)) + return 0; + + /* There is no fetch method when variable is unset. Just set the right + * action and return. */ + if (!set_var) { + rule->action = ACT_CUSTOM; + rule->action_ptr = action_clear; + rule->release_ptr = release_store_rule; + return ACT_RET_PRS_OK; + } + + kw_name = args[*arg-1]; + + switch (rule->from) { + case ACT_F_TCP_REQ_CON: + flags = SMP_VAL_FE_CON_ACC; + px->conf.args.ctx = ARGC_TCO; + break; + case ACT_F_TCP_REQ_SES: + flags = SMP_VAL_FE_SES_ACC; + px->conf.args.ctx = ARGC_TSE; + break; + case ACT_F_TCP_REQ_CNT: + if (px->cap & PR_CAP_FE) + flags |= SMP_VAL_FE_REQ_CNT; + if (px->cap & PR_CAP_BE) + flags |= SMP_VAL_BE_REQ_CNT; + px->conf.args.ctx = ARGC_TRQ; + break; + case ACT_F_TCP_RES_CNT: + if (px->cap & PR_CAP_FE) + flags |= SMP_VAL_FE_RES_CNT; + if (px->cap & PR_CAP_BE) + flags |= SMP_VAL_BE_RES_CNT; + px->conf.args.ctx = ARGC_TRS; + break; + case ACT_F_HTTP_REQ: + if (px->cap & PR_CAP_FE) + flags |= SMP_VAL_FE_HRQ_HDR; + if (px->cap & PR_CAP_BE) + flags |= SMP_VAL_BE_HRQ_HDR; + px->conf.args.ctx = ARGC_HRQ; + break; + case ACT_F_HTTP_RES: + if (px->cap & PR_CAP_FE) + flags |= SMP_VAL_FE_HRS_HDR; + if (px->cap & PR_CAP_BE) + flags |= SMP_VAL_BE_HRS_HDR; + px->conf.args.ctx = ARGC_HRS; + break; + case ACT_F_TCP_CHK: + flags = SMP_VAL_BE_CHK_RUL; + px->conf.args.ctx = ARGC_TCK; + break; + case ACT_F_CFG_PARSER: + flags = SMP_VAL_CFG_PARSER; + px->conf.args.ctx = ARGC_CFG; + break; + case ACT_F_CLI_PARSER: + flags = SMP_VAL_CLI_PARSER; + px->conf.args.ctx = ARGC_CLI; + break; + default: + memprintf(err, + "internal error, unexpected rule->from=%d, please report this bug!", + rule->from); + return ACT_RET_PRS_ERR; + } + + if (set_var == 2) { /* set-var-fmt */ + if (!parse_logformat_string(args[*arg], px, &rule->arg.vars.fmt, 0, flags, err)) + return ACT_RET_PRS_ERR; + + (*arg)++; + + /* for late error reporting */ + free(px->conf.lfs_file); + px->conf.lfs_file = strdup(px->conf.args.file); + px->conf.lfs_line = px->conf.args.line; + } else { + /* set-var */ + rule->arg.vars.expr = sample_parse_expr((char **)args, arg, px->conf.args.file, + px->conf.args.line, err, &px->conf.args, NULL); + if (!rule->arg.vars.expr) + return ACT_RET_PRS_ERR; + + if (!(rule->arg.vars.expr->fetch->val & flags)) { + memprintf(err, + "fetch method '%s' extracts information from '%s', none of which is available here", + kw_name, sample_src_names(rule->arg.vars.expr->fetch->use)); + free(rule->arg.vars.expr); + return ACT_RET_PRS_ERR; + } + } + + rule->action = ACT_CUSTOM; + rule->action_ptr = action_store; + rule->release_ptr = release_store_rule; + return ACT_RET_PRS_OK; +} + + +/* parses a global "set-var" directive. It will create a temporary rule and + * expression that are parsed, processed, and released on the fly so that we + * respect the real set-var syntax. These directives take the following format: + * set-var + * set-var-fmt + * Note that parse_store() expects "set-var(name) " so we have to + * temporarily replace the keyword here. + */ +static int vars_parse_global_set_var(char **args, int section_type, struct proxy *curpx, + const struct proxy *defpx, const char *file, int line, + char **err) +{ + struct proxy px = { + .id = "CFG", + .conf.args = { .file = file, .line = line, }, + }; + struct act_rule rule = { + .arg.vars.scope = SCOPE_PROC, + .from = ACT_F_CFG_PARSER, + .conf = { .file = (char *)file, .line = line, }, + }; + enum obj_type objt = OBJ_TYPE_NONE; + struct session *sess = NULL; + enum act_parse_ret p_ret; + char *old_arg1; + char *tmp_arg1; + int arg = 2; // variable name + int ret = -1; + int use_fmt = 0; + + LIST_INIT(&px.conf.args.list); + + use_fmt = strcmp(args[0], "set-var-fmt") == 0; + + if (!*args[1] || !*args[2]) { + if (use_fmt) + memprintf(err, "'%s' requires a process-wide variable name ('proc.') and a format string.", args[0]); + else + memprintf(err, "'%s' requires a process-wide variable name ('proc.') and a sample expression.", args[0]); + goto end; + } + + tmp_arg1 = NULL; + if (!memprintf(&tmp_arg1, "set-var%s(%s)", use_fmt ? "-fmt" : "", args[1])) + goto end; + + /* parse_store() will always return a message in on error */ + old_arg1 = args[1]; args[1] = tmp_arg1; + p_ret = parse_store((const char **)args, &arg, &px, &rule, err); + free(args[1]); args[1] = old_arg1; + + if (p_ret != ACT_RET_PRS_OK) + goto end; + + if (rule.arg.vars.scope != SCOPE_PROC) { + memprintf(err, "'%s': cannot set variable '%s', only scope 'proc' is permitted in the global section.", args[0], args[1]); + goto end; + } + + if (smp_resolve_args(&px, err) != 0) { + release_sample_expr(rule.arg.vars.expr); + indent_msg(err, 2); + goto end; + } + + if (use_fmt && !(sess = session_new(&px, NULL, &objt))) { + release_sample_expr(rule.arg.vars.expr); + memprintf(err, "'%s': out of memory when trying to set variable '%s' in the global section.", args[0], args[1]); + goto end; + } + + action_store(&rule, &px, sess, NULL, 0); + release_sample_expr(rule.arg.vars.expr); + if (sess) + session_free(sess); + + ret = 0; + end: + return ret; +} + +/* parse CLI's "get var " */ +static int vars_parse_cli_get_var(char **args, char *payload, struct appctx *appctx, void *private) +{ + struct vars *vars; + struct sample smp = { }; + int i; + + if (!cli_has_level(appctx, ACCESS_LVL_OPER)) + return 1; + + if (!*args[2]) + return cli_err(appctx, "Missing process-wide variable identifier.\n"); + + vars = get_vars(NULL, NULL, SCOPE_PROC); + if (!vars || vars->scope != SCOPE_PROC) + return 0; + + if (!vars_get_by_name(args[2], strlen(args[2]), &smp, NULL)) + return cli_err(appctx, "Variable not found.\n"); + + /* the sample returned by vars_get_by_name() is allocated into a trash + * chunk so we have no constraint to manipulate it. + */ + chunk_printf(&trash, "%s: type=%s value=", args[2], smp_to_type[smp.data.type]); + + if (!sample_casts[smp.data.type][SMP_T_STR] || + !sample_casts[smp.data.type][SMP_T_STR](&smp)) { + chunk_appendf(&trash, "(undisplayable)\n"); + } else { + /* Display the displayable chars*. */ + b_putchr(&trash, '<'); + for (i = 0; i < smp.data.u.str.data; i++) { + if (isprint((unsigned char)smp.data.u.str.area[i])) + b_putchr(&trash, smp.data.u.str.area[i]); + else + b_putchr(&trash, '.'); + } + b_putchr(&trash, '>'); + b_putchr(&trash, '\n'); + b_putchr(&trash, 0); + } + return cli_msg(appctx, LOG_INFO, trash.area); +} + +/* parse CLI's "set var ". It accepts: + * - set var + * - set var expr + * - set var fmt + */ +static int vars_parse_cli_set_var(char **args, char *payload, struct appctx *appctx, void *private) +{ + struct proxy px = { + .id = "CLI", + .conf.args = { .file = "CLI", .line = 0, }, + }; + struct act_rule rule = { + .arg.vars.scope = SCOPE_PROC, + .from = ACT_F_CLI_PARSER, + .conf = { .file = "CLI", .line = 0, }, + }; + enum obj_type objt = OBJ_TYPE_NONE; + struct session *sess = NULL; + enum act_parse_ret p_ret; + const char *tmp_args[3]; + int tmp_arg; + char *tmp_act; + char *err = NULL; + int nberr; + int use_fmt = 0; + + LIST_INIT(&px.conf.args.list); + + if (!cli_has_level(appctx, ACCESS_LVL_OPER)) + return 1; + + if (!*args[2]) + return cli_err(appctx, "Missing process-wide variable identifier.\n"); + + if (!*args[3]) + return cli_err(appctx, "Missing either 'expr', 'fmt' or expression.\n"); + + if (*args[4]) { + /* this is the long format */ + if (strcmp(args[3], "fmt") == 0) + use_fmt = 1; + else if (strcmp(args[3], "expr") != 0) { + memprintf(&err, "'%s %s': arg type must be either 'expr' or 'fmt' but got '%s'.", args[0], args[1], args[3]); + goto fail; + } + } + + tmp_act = NULL; + if (!memprintf(&tmp_act, "set-var%s(%s)", use_fmt ? "-fmt" : "", args[2])) { + memprintf(&err, "memory allocation error."); + goto fail; + } + + /* parse_store() will always return a message in on error */ + tmp_args[0] = tmp_act; + tmp_args[1] = (*args[4]) ? args[4] : args[3]; + tmp_args[2] = ""; + tmp_arg = 1; // must point to the first arg after the action + p_ret = parse_store(tmp_args, &tmp_arg, &px, &rule, &err); + free(tmp_act); + + if (p_ret != ACT_RET_PRS_OK) + goto fail; + + if (rule.arg.vars.scope != SCOPE_PROC) { + memprintf(&err, "'%s %s': cannot set variable '%s', only scope 'proc' is permitted here.", args[0], args[1], args[2]); + goto fail; + } + + err = NULL; + nberr = smp_resolve_args(&px, &err); + if (nberr) { + release_sample_expr(rule.arg.vars.expr); + indent_msg(&err, 2); + goto fail; + } + + if (use_fmt && !(sess = session_new(&px, NULL, &objt))) { + release_sample_expr(rule.arg.vars.expr); + memprintf(&err, "memory allocation error."); + goto fail; + } + + action_store(&rule, &px, sess, NULL, 0); + release_sample_expr(rule.arg.vars.expr); + if (sess) + session_free(sess); + + appctx->st0 = CLI_ST_PROMPT; + return 0; + fail: + return cli_dynerr(appctx, err); +} + +static int vars_max_size(char **args, int section_type, struct proxy *curpx, + const struct proxy *defpx, const char *file, int line, + char **err, unsigned int *limit) +{ + char *error; + + *limit = strtol(args[1], &error, 10); + if (*error != 0) { + memprintf(err, "%s: '%s' is an invalid size", args[0], args[1]); + return -1; + } + return 0; +} + +static int vars_max_size_global(char **args, int section_type, struct proxy *curpx, + const struct proxy *defpx, const char *file, int line, + char **err) +{ + return vars_max_size(args, section_type, curpx, defpx, file, line, err, &var_global_limit); +} + +static int vars_max_size_proc(char **args, int section_type, struct proxy *curpx, + const struct proxy *defpx, const char *file, int line, + char **err) +{ + return vars_max_size(args, section_type, curpx, defpx, file, line, err, &var_proc_limit); +} + +static int vars_max_size_sess(char **args, int section_type, struct proxy *curpx, + const struct proxy *defpx, const char *file, int line, + char **err) +{ + return vars_max_size(args, section_type, curpx, defpx, file, line, err, &var_sess_limit); +} + +static int vars_max_size_txn(char **args, int section_type, struct proxy *curpx, + const struct proxy *defpx, const char *file, int line, + char **err) +{ + return vars_max_size(args, section_type, curpx, defpx, file, line, err, &var_txn_limit); +} + +static int vars_max_size_reqres(char **args, int section_type, struct proxy *curpx, + const struct proxy *defpx, const char *file, int line, + char **err) +{ + return vars_max_size(args, section_type, curpx, defpx, file, line, err, &var_reqres_limit); +} + +static int vars_max_size_check(char **args, int section_type, struct proxy *curpx, + const struct proxy *defpx, const char *file, int line, + char **err) +{ + return vars_max_size(args, section_type, curpx, defpx, file, line, err, &var_check_limit); +} + +/* early boot initialization */ +static void vars_init() +{ + var_name_hash_seed = ha_random64(); + /* Initialize process vars */ + vars_init_head(&proc_vars, SCOPE_PROC); +} + +INITCALL0(STG_PREPARE, vars_init); + +static struct sample_fetch_kw_list sample_fetch_keywords = {ILH, { + + { "var", smp_fetch_var, ARG2(1,STR,STR), smp_check_var, SMP_T_ANY, SMP_USE_CONST }, + { /* END */ }, +}}; + +INITCALL1(STG_REGISTER, sample_register_fetches, &sample_fetch_keywords); + +static struct sample_conv_kw_list sample_conv_kws = {ILH, { + { "set-var", smp_conv_store, ARG5(1,STR,STR,STR,STR,STR), conv_check_var, SMP_T_ANY, SMP_T_ANY }, + { "unset-var", smp_conv_clear, ARG1(1,STR), conv_check_var, SMP_T_ANY, SMP_T_ANY }, + { /* END */ }, +}}; + +INITCALL1(STG_REGISTER, sample_register_convs, &sample_conv_kws); + +static struct action_kw_list tcp_req_conn_kws = { { }, { + { "set-var-fmt", parse_store, KWF_MATCH_PREFIX }, + { "set-var", parse_store, KWF_MATCH_PREFIX }, + { "unset-var", parse_store, KWF_MATCH_PREFIX }, + { /* END */ } +}}; + +INITCALL1(STG_REGISTER, tcp_req_conn_keywords_register, &tcp_req_conn_kws); + +static struct action_kw_list tcp_req_sess_kws = { { }, { + { "set-var-fmt", parse_store, KWF_MATCH_PREFIX }, + { "set-var", parse_store, KWF_MATCH_PREFIX }, + { "unset-var", parse_store, KWF_MATCH_PREFIX }, + { /* END */ } +}}; + +INITCALL1(STG_REGISTER, tcp_req_sess_keywords_register, &tcp_req_sess_kws); + +static struct action_kw_list tcp_req_cont_kws = { { }, { + { "set-var-fmt", parse_store, KWF_MATCH_PREFIX }, + { "set-var", parse_store, KWF_MATCH_PREFIX }, + { "unset-var", parse_store, KWF_MATCH_PREFIX }, + { /* END */ } +}}; + +INITCALL1(STG_REGISTER, tcp_req_cont_keywords_register, &tcp_req_cont_kws); + +static struct action_kw_list tcp_res_kws = { { }, { + { "set-var-fmt", parse_store, KWF_MATCH_PREFIX }, + { "set-var", parse_store, KWF_MATCH_PREFIX }, + { "unset-var", parse_store, KWF_MATCH_PREFIX }, + { /* END */ } +}}; + +INITCALL1(STG_REGISTER, tcp_res_cont_keywords_register, &tcp_res_kws); + +static struct action_kw_list tcp_check_kws = {ILH, { + { "set-var-fmt", parse_store, KWF_MATCH_PREFIX }, + { "set-var", parse_store, KWF_MATCH_PREFIX }, + { "unset-var", parse_store, KWF_MATCH_PREFIX }, + { /* END */ } +}}; + +INITCALL1(STG_REGISTER, tcp_check_keywords_register, &tcp_check_kws); + +static struct action_kw_list http_req_kws = { { }, { + { "set-var-fmt", parse_store, KWF_MATCH_PREFIX }, + { "set-var", parse_store, KWF_MATCH_PREFIX }, + { "unset-var", parse_store, KWF_MATCH_PREFIX }, + { /* END */ } +}}; + +INITCALL1(STG_REGISTER, http_req_keywords_register, &http_req_kws); + +static struct action_kw_list http_res_kws = { { }, { + { "set-var-fmt", parse_store, KWF_MATCH_PREFIX }, + { "set-var", parse_store, KWF_MATCH_PREFIX }, + { "unset-var", parse_store, KWF_MATCH_PREFIX }, + { /* END */ } +}}; + +INITCALL1(STG_REGISTER, http_res_keywords_register, &http_res_kws); + +static struct action_kw_list http_after_res_kws = { { }, { + { "set-var-fmt", parse_store, KWF_MATCH_PREFIX }, + { "set-var", parse_store, KWF_MATCH_PREFIX }, + { "unset-var", parse_store, KWF_MATCH_PREFIX }, + { /* END */ } +}}; + +INITCALL1(STG_REGISTER, http_after_res_keywords_register, &http_after_res_kws); + +static struct cfg_kw_list cfg_kws = {{ },{ + { CFG_GLOBAL, "set-var", vars_parse_global_set_var }, + { CFG_GLOBAL, "set-var-fmt", vars_parse_global_set_var }, + { CFG_GLOBAL, "tune.vars.global-max-size", vars_max_size_global }, + { CFG_GLOBAL, "tune.vars.proc-max-size", vars_max_size_proc }, + { CFG_GLOBAL, "tune.vars.sess-max-size", vars_max_size_sess }, + { CFG_GLOBAL, "tune.vars.txn-max-size", vars_max_size_txn }, + { CFG_GLOBAL, "tune.vars.reqres-max-size", vars_max_size_reqres }, + { CFG_GLOBAL, "tune.vars.check-max-size", vars_max_size_check }, + { /* END */ } +}}; + +INITCALL1(STG_REGISTER, cfg_register_keywords, &cfg_kws); + + +/* register cli keywords */ +static struct cli_kw_list cli_kws = {{ },{ + { { "get", "var", NULL }, "get var : retrieve contents of a process-wide variable", vars_parse_cli_get_var, NULL }, + { { "set", "var", NULL }, "set var [fmt|expr] {|}: set variable from an expression or a format", vars_parse_cli_set_var, NULL, NULL, NULL, ACCESS_EXPERIMENTAL }, + { { NULL }, NULL, NULL, NULL } +}}; +INITCALL1(STG_REGISTER, cli_register_kw, &cli_kws); -- cgit v1.2.3