From 97a5de64083bdd33dc61ba6d98e7c067792558c8 Mon Sep 17 00:00:00 2001 From: "Alan T. DeKok" Date: Thu, 3 Jul 2025 13:24:30 -0400 Subject: [PATCH] move keyword compile_foo() to foo.c mostly. compile_edit(), compile_tmpl(), compile_module(), and compile_function() are still in compile.c Many helper functions are exported in interpret_priv.h. Some of those should be cleaned up / renamed / un-exported. The function names could likely be cleaned up. Much of the code uses unlang_foo() for private functions. It should arguably use private function names. The unlang_register() function takes a type as both a function argument, and as a field in unlang_op_t. Fixing that will require changing instruction->type to instruction->op in the interpreter, which should be a next step. The hash function is just fr_hash(). We should arguably come up with a perfect hash for the keywords, which will help simplify the lookups. --- src/lib/unlang/base.c | 32 + src/lib/unlang/call.c | 76 + src/lib/unlang/caller.c | 88 + src/lib/unlang/catch.c | 95 + src/lib/unlang/child_request.c | 10 +- src/lib/unlang/compile.c | 4552 +++++++------------------------- src/lib/unlang/condition.c | 156 +- src/lib/unlang/detach.c | 30 + src/lib/unlang/edit.c | 4 + src/lib/unlang/finally.c | 11 +- src/lib/unlang/foreach.c | 298 ++- src/lib/unlang/function.c | 4 +- src/lib/unlang/group.c | 31 + src/lib/unlang/limit.c | 104 + src/lib/unlang/load_balance.c | 114 +- src/lib/unlang/map.c | 616 +++++ src/lib/unlang/module.c | 9 +- src/lib/unlang/parallel.c | 56 + src/lib/unlang/return.c | 21 + src/lib/unlang/subrequest.c | 294 +++ src/lib/unlang/switch.c | 393 +++ src/lib/unlang/timeout.c | 103 + src/lib/unlang/tmpl.c | 4 + src/lib/unlang/transaction.c | 133 + src/lib/unlang/try.c | 42 + src/lib/unlang/unlang_priv.h | 94 +- src/lib/unlang/xlat.c | 6 +- 27 files changed, 3737 insertions(+), 3639 deletions(-) diff --git a/src/lib/unlang/base.c b/src/lib/unlang/base.c index 91d9ef2669..2f5716041e 100644 --- a/src/lib/unlang/base.c +++ b/src/lib/unlang/base.c @@ -42,6 +42,8 @@ bool unlang_section(CONF_SECTION *cs) return (cf_data_find(cs, unlang_group_t, NULL) != NULL); } +fr_hash_table_t *unlang_op_table = NULL; + /** Register an operation with the interpreter * * The main purpose of this registration API is to avoid intermixing the xlat, @@ -63,15 +65,42 @@ bool unlang_section(CONF_SECTION *cs) void unlang_register(int type, unlang_op_t *op) { fr_assert(type < UNLANG_TYPE_MAX); /* Unlang max isn't a valid type */ + fr_assert(unlang_op_table != NULL); + fr_assert(op->type == (unlang_type_t) type); memcpy(&unlang_ops[type], op, sizeof(unlang_ops[type])); + + /* + * Some instruction types are internal, and are not real keywords. + */ + if ((op->flag & UNLANG_OP_FLAG_INTERNAL) != 0) return; + + MEM(fr_hash_table_insert(unlang_op_table, &unlang_ops[type])); } static TALLOC_CTX *unlang_ctx = NULL; +static uint32_t op_hash(void const *data) +{ + unlang_op_t const *a = data; + + return fr_hash_string(a->name); +} + +static int8_t op_cmp(void const *one, void const *two) +{ + unlang_op_t const *a = one; + unlang_op_t const *b = two; + + ERROR("CMP ::%s:: ::%s::", a->name, b->name); + + return CMP(strcmp(a->name, b->name), 0); +} + static int _unlang_global_free(UNUSED void *uctx) { TALLOC_FREE(unlang_ctx); + unlang_op_table = NULL; return 0; } @@ -81,6 +110,9 @@ static int _unlang_global_init(UNUSED void *uctx) unlang_ctx = talloc_init("unlang"); if (!unlang_ctx) return -1; + unlang_op_table = fr_hash_table_alloc(unlang_ctx, op_hash, op_cmp, NULL); + if (!unlang_op_table) goto fail; + /* * Explicitly initialise the xlat tree, and perform dictionary lookups. */ diff --git a/src/lib/unlang/call.c b/src/lib/unlang/call.c index 53f35896c3..aef7a3a5c7 100644 --- a/src/lib/unlang/call.c +++ b/src/lib/unlang/call.c @@ -246,6 +246,81 @@ CONF_SECTION *unlang_call_current(request_t *request) return NULL; } +static unlang_t *unlang_compile_call(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci) +{ + CONF_SECTION *cs = cf_item_to_section(ci); + virtual_server_t const *vs; + unlang_t *c; + + unlang_group_t *g; + unlang_call_t *gext; + + fr_token_t type; + char const *server; + CONF_SECTION *server_cs; + fr_dict_t const *dict; + fr_dict_attr_t const *attr_packet_type; + + server = cf_section_name2(cs); + if (!server) { + cf_log_err(cs, "You MUST specify a server name for 'call { ... }'"); + print_url: + cf_log_err(ci, DOC_KEYWORD_REF(call)); + return NULL; + } + + type = cf_section_name2_quote(cs); + if (type != T_BARE_WORD) { + cf_log_err(cs, "The arguments to 'call' cannot be a quoted string or a dynamic value"); + goto print_url; + } + + vs = virtual_server_find(server); + if (!vs) { + cf_log_err(cs, "Unknown virtual server '%s'", server); + return NULL; + } + + server_cs = virtual_server_cs(vs); + + /* + * The dictionaries are not compatible, forbid it. + */ + dict = virtual_server_dict_by_name(server); + if (!dict) { + cf_log_err(cs, "Cannot call virtual server '%s', failed retrieving its namespace", + server); + return NULL; + } + if ((dict != fr_dict_internal()) && fr_dict_internal() && + unlang_ctx->rules->attr.dict_def && !fr_dict_compatible(unlang_ctx->rules->attr.dict_def, dict)) { + cf_log_err(cs, "Cannot call server %s with namespace '%s' from namespaces '%s' - they have incompatible protocols", + server, fr_dict_root(dict)->name, fr_dict_root(unlang_ctx->rules->attr.dict_def)->name); + return NULL; + } + + attr_packet_type = fr_dict_attr_by_name(NULL, fr_dict_root(dict), "Packet-Type"); + if (!attr_packet_type) { + cf_log_err(cs, "Cannot call server %s with namespace '%s' - it has no Packet-Type attribute", + server, fr_dict_root(dict)->name); + return NULL; + } + + c = unlang_compile_section(parent, unlang_ctx, cs, UNLANG_TYPE_CALL); + if (!c) return NULL; + + /* + * Set the virtual server name, which tells unlang_call() + * which virtual server to call. + */ + g = unlang_generic_to_group(c); + gext = unlang_group_to_call(g); + gext->server_cs = server_cs; + gext->attr_packet_type = attr_packet_type; + + return c; +} + void unlang_call_init(void) { unlang_register(UNLANG_TYPE_CALL, @@ -254,6 +329,7 @@ void unlang_call_init(void) .flag = UNLANG_OP_FLAG_RCODE_SET | UNLANG_OP_FLAG_DEBUG_BRACES, .type = UNLANG_TYPE_CALL, + .compile = unlang_compile_call, .interpret = unlang_call_frame_init, diff --git a/src/lib/unlang/caller.c b/src/lib/unlang/caller.c index def5c58345..ad21042c88 100644 --- a/src/lib/unlang/caller.c +++ b/src/lib/unlang/caller.c @@ -52,6 +52,93 @@ static unlang_action_t unlang_caller(unlang_result_t *p_result, request_t *reque } +static unlang_t *unlang_compile_caller(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci) +{ + CONF_SECTION *cs = cf_item_to_section(ci); + unlang_t *c; + + unlang_group_t *g; + unlang_caller_t *gext; + + fr_token_t type; + char const *name; + fr_dict_t const *dict; + unlang_compile_ctx_t unlang_ctx2; + tmpl_rules_t parent_rules, t_rules; + + fr_dict_autoload_talloc_t *dict_ref = NULL; + + name = cf_section_name2(cs); + if (!name) { + cf_log_err(cs, "You MUST specify a protocol name for 'caller { ... }'"); + print_url: + cf_log_err(ci, DOC_KEYWORD_REF(caller)); + return NULL; + } + + type = cf_section_name2_quote(cs); + if (type != T_BARE_WORD) { + cf_log_err(cs, "The argument to 'caller' cannot be a quoted string or a dynamic value"); + goto print_url; + } + + dict = fr_dict_by_protocol_name(name); + if (!dict) { + dict_ref = fr_dict_autoload_talloc(NULL, &dict, name); + if (!dict_ref) { + cf_log_perr(cs, "Unknown protocol '%s'", name); + goto print_url; + } + } + + /* + * Create a new parent context with the new dictionary. + */ + memcpy(&parent_rules, unlang_ctx->rules, sizeof(parent_rules)); + memcpy(&t_rules, unlang_ctx->rules, sizeof(t_rules)); + parent_rules.attr.dict_def = dict; + t_rules.parent = &parent_rules; + + /* + * We don't want to modify the context we were passed, so + * we just clone it + */ + memcpy(&unlang_ctx2, unlang_ctx, sizeof(unlang_ctx2)); + unlang_ctx2.rules = &t_rules; + unlang_ctx2.section_name1 = "caller"; + unlang_ctx2.section_name2 = name; + + c = unlang_compile_section(parent, &unlang_ctx2, cs, UNLANG_TYPE_CALLER); + if (!c) { + talloc_free(dict_ref); + return NULL; + } + + /* + * Set the virtual server name, which tells unlang_call() + * which virtual server to call. + */ + g = unlang_generic_to_group(c); + gext = unlang_group_to_caller(g); + gext->dict = dict; + + if (dict_ref) { + /* + * Parent the dictionary reference correctly now that we + * have the section with the dependency. This should + * be fast as dict_ref has no siblings. + */ + talloc_steal(gext, dict_ref); + } + + if (!g->num_children) { + talloc_free(c); + return UNLANG_IGNORE; + } + + return c; +} + void unlang_caller_init(void) { unlang_register(UNLANG_TYPE_CALLER, @@ -60,6 +147,7 @@ void unlang_caller_init(void) .type = UNLANG_TYPE_CALLER, .flag = UNLANG_OP_FLAG_DEBUG_BRACES, + .compile = unlang_compile_caller, .interpret = unlang_caller, .unlang_size = sizeof(unlang_caller_t), diff --git a/src/lib/unlang/catch.c b/src/lib/unlang/catch.c index c607be9f27..5be1865910 100644 --- a/src/lib/unlang/catch.c +++ b/src/lib/unlang/catch.c @@ -99,6 +99,100 @@ unlang_action_t unlang_interpret_skip_to_catch(UNUSED unlang_result_t *p_result, return frame_set_next(frame, unlang); } +static int catch_argv(CONF_SECTION *cs, unlang_catch_t *ca, char const *name) +{ + int rcode; + + rcode = fr_table_value_by_str(mod_rcode_table, name, -1); + if (rcode < 0) { + cf_log_err(cs, "Unknown rcode '%s'.", name); + return -1; + } + + if (ca->catching[rcode]) { + cf_log_err(cs, "Duplicate rcode '%s'.", name); + return -1; + } + + ca->catching[rcode] = true; + + return 0; +} + +static unlang_t *unlang_compile_catch(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci) +{ + CONF_SECTION *cs = cf_item_to_section(ci); + unlang_group_t *g; + unlang_t *c; + unlang_catch_t *ca; + CONF_ITEM *prev; + char const *name; + + prev = cf_item_prev(cf_parent(ci), ci); + while (prev && cf_item_is_data(prev)) prev = cf_item_prev(cf_parent(ci), prev); + + if (!prev || !cf_item_is_section(prev)) { + fail: + cf_log_err(cs, "Found 'catch' section with no previous 'try'"); + cf_log_err(ci, DOC_KEYWORD_REF(catch)); + return NULL; + } + + name = cf_section_name1(cf_item_to_section(prev)); + fr_assert(name != NULL); + + if ((strcmp(name, "try") != 0) && (strcmp(name, "catch") != 0)) { + /* + * The previous thing has to be a section. And it has to + * be either a "try" or a "catch". + */ + goto fail; + } + + g = unlang_group_allocate(parent, cs, UNLANG_TYPE_CATCH); + if (!g) return NULL; + + c = unlang_group_to_generic(g); + + /* + * Want to log what we caught + */ + c->debug_name = c->name = talloc_typed_asprintf(c, "%s %s", cf_section_name1(cs), cf_section_name2(cs)); + + ca = unlang_group_to_catch(g); + if (!cf_section_name2(cs)) { + /* + * No arg2: catch errors + */ + ca->catching[RLM_MODULE_REJECT] = true; + ca->catching[RLM_MODULE_FAIL] = true; + ca->catching[RLM_MODULE_INVALID] = true; + ca->catching[RLM_MODULE_DISALLOW] = true; + + } else { + int i; + + name = cf_section_name2(cs); + + if (catch_argv(cs, ca, name) < 0) { + talloc_free(c); + return NULL; + } + + for (i = 0; (name = cf_section_argv(cs, i)) != NULL; i++) { + if (catch_argv(cs, ca, name) < 0) { + talloc_free(c); + return NULL; + } + } + } + + /* + * @todo - Else parse and limit the things we catch + */ + return unlang_compile_children(g, unlang_ctx, true); +} + void unlang_catch_init(void) { unlang_register(UNLANG_TYPE_CATCH, @@ -107,6 +201,7 @@ void unlang_catch_init(void) .type = UNLANG_TYPE_CATCH, .flag = UNLANG_OP_FLAG_DEBUG_BRACES, + .compile = unlang_compile_catch, .interpret = unlang_catch, .unlang_size = sizeof(unlang_catch_t), diff --git a/src/lib/unlang/child_request.c b/src/lib/unlang/child_request.c index 323bbd2d62..1677ad4f27 100644 --- a/src/lib/unlang/child_request.c +++ b/src/lib/unlang/child_request.c @@ -281,8 +281,8 @@ int unlang_child_request_op_init(void) unlang_register(UNLANG_TYPE_CHILD_REQUEST, &(unlang_op_t){ .name = "child-request", - .interpret = unlang_child_request_done, - .signal = unlang_child_request_signal, + .type = UNLANG_TYPE_CHILD_REQUEST, + /* * Frame can't be cancelled, because children need to * write out status to the parent. If we don't do this, @@ -294,7 +294,11 @@ int unlang_child_request_op_init(void) * to end normally so that non-detachable requests are * guaranteed the parent still exists. */ - .flag = UNLANG_OP_FLAG_NO_FORCE_UNWIND, + .flag = UNLANG_OP_FLAG_NO_FORCE_UNWIND | UNLANG_OP_FLAG_INTERNAL, + + .interpret = unlang_child_request_done, + .signal = unlang_child_request_signal, + .frame_state_size = sizeof(unlang_frame_state_child_request_t), .frame_state_type = "unlang_frame_state_child_request_t" }); diff --git a/src/lib/unlang/compile.c b/src/lib/unlang/compile.c index 537880159a..8a4c94b028 100644 --- a/src/lib/unlang/compile.c +++ b/src/lib/unlang/compile.c @@ -55,8 +55,6 @@ RCSID("$Id$") #include "try_priv.h" #include "mod_action.h" -#define UNLANG_IGNORE ((unlang_t *) -1) - static unsigned int unlang_number = 1; /* @@ -98,64 +96,12 @@ fr_table_num_sorted_t const mod_action_table[] = { }; size_t mod_action_table_len = NUM_ELEMENTS(mod_action_table); -typedef struct { - virtual_server_t const *vs; //!< Virtual server we're compiling in the context of. - ///< This shouldn't change during the compilation of - ///< a single unlang section. - char const *section_name1; - char const *section_name2; - unlang_mod_actions_t actions; - tmpl_rules_t const *rules; -} unlang_compile_t; - -/* - * When we switch to a new unlang ctx, we use the new component - * name and number, but we use the CURRENT actions. - */ -static inline CC_HINT(always_inline) -void compile_copy_context(unlang_compile_t *dst, unlang_compile_t const *src) -{ - int i; - - *dst = *src; - - /* - * Ensure that none of the actions are RETRY. - */ - for (i = 0; i < RLM_MODULE_NUMCODES; i++) { - if (dst->actions.actions[i] == MOD_ACTION_RETRY) dst->actions.actions[i] = MOD_PRIORITY_MIN; - } - memset(&dst->actions.retry, 0, sizeof(dst->actions.retry)); \ -} - -#define UPDATE_CTX2 compile_copy_context(&unlang_ctx2, unlang_ctx) +#define UPDATE_CTX2 unlang_compile_ctx_copy(&unlang_ctx2, unlang_ctx) -static unlang_t *compile_empty(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_SECTION *cs, unlang_type_t type); - static char const unlang_spaces[] = " "; -static inline CC_HINT(always_inline) int unlang_attr_rules_verify(tmpl_attr_rules_t const *rules) -{ - if (!fr_cond_assert_msg(rules->dict_def, "No protocol dictionary set")) return -1; - if (!fr_cond_assert_msg(rules->dict_def != fr_dict_internal(), "rules->attr.dict_def must not be the internal dictionary")) return -1; - if (!fr_cond_assert_msg(!rules->allow_foreign, "rules->attr.allow_foreign must be false")) return -1; - - return 0; -} - -static inline CC_HINT(always_inline) int unlang_rules_verify(tmpl_rules_t const *rules) -{ - if (!fr_cond_assert_msg(!rules->at_runtime, "rules->at_runtime must be false")) return -1; - return unlang_attr_rules_verify(&rules->attr); -} - -#if 0 -#define ATTR_RULES_VERIFY(_rules) if (unlang_attr_rules_verify(_rules) < 0) return NULL; -#endif -#define RULES_VERIFY(_rules) do { if (unlang_rules_verify(_rules) < 0) return NULL; } while (0) - -static bool pass2_fixup_tmpl(UNUSED TALLOC_CTX *ctx, tmpl_t **vpt_p, CONF_ITEM const *ci, fr_dict_t const *dict) +bool pass2_fixup_tmpl(UNUSED TALLOC_CTX *ctx, tmpl_t **vpt_p, CONF_ITEM const *ci, fr_dict_t const *dict) { tmpl_t *vpt = *vpt_p; @@ -183,7 +129,7 @@ static bool pass2_fixup_tmpl(UNUSED TALLOC_CTX *ctx, tmpl_t **vpt_p, CONF_ITEM c * This function resolves most things. Most notable it CAN leave the * RHS unresolved, for use in `map` sections. */ -static bool pass2_fixup_map(map_t *map, tmpl_rules_t const *rules, fr_dict_attr_t const *parent) +bool pass2_fixup_map(map_t *map, tmpl_rules_t const *rules, fr_dict_attr_t const *parent) { RULES_VERIFY(rules); @@ -247,7 +193,7 @@ static bool pass2_fixup_map(map_t *map, tmpl_rules_t const *rules, fr_dict_attr_ /* * Do all kinds of fixups and checks for update sections. */ -static bool pass2_fixup_update(unlang_group_t *g, tmpl_rules_t const *rules) +bool pass2_fixup_update(unlang_group_t *g, tmpl_rules_t const *rules) { unlang_map_t *gext = unlang_group_to_map(g); map_t *map = NULL; @@ -274,7 +220,7 @@ static bool pass2_fixup_update(unlang_group_t *g, tmpl_rules_t const *rules) /* * Compile the RHS of map sections to xlat_exp_t */ -static bool pass2_fixup_map_rhs(unlang_group_t *g, tmpl_rules_t const *rules) +bool pass2_fixup_map_rhs(unlang_group_t *g, tmpl_rules_t const *rules) { unlang_map_t *gext = unlang_group_to_map(g); map_t *map = NULL; @@ -402,52 +348,6 @@ static void unlang_dump(unlang_t *instruction, int depth) } } -/** Validate and fixup a map that's part of an map section. - * - * @param map to validate. - * @param ctx data to pass to fixup function (currently unused). - * @return 0 if valid else -1. - */ -static int unlang_fixup_map(map_t *map, UNUSED void *ctx) -{ - switch (map->lhs->type) { - case TMPL_TYPE_ATTR: - case TMPL_TYPE_XLAT_UNRESOLVED: - case TMPL_TYPE_XLAT: - break; - - default: - cf_log_err(map->ci, "Left side of map must be an attribute " - "or an xlat (that expands to an attribute), not a %s", - tmpl_type_to_str(map->lhs->type)); - return -1; - } - - switch (map->rhs->type) { - case TMPL_TYPE_XLAT_UNRESOLVED: - case TMPL_TYPE_DATA_UNRESOLVED: - case TMPL_TYPE_DATA: - case TMPL_TYPE_XLAT: - case TMPL_TYPE_ATTR: - case TMPL_TYPE_EXEC: - break; - - default: - cf_log_err(map->ci, "Right side of map must be an attribute, literal, xlat or exec, got type %s", - tmpl_type_to_str(map->rhs->type)); - return -1; - } - - if (!fr_assignment_op[map->op] && !fr_comparison_op[map->op]) { - cf_log_err(map->ci, "Invalid operator \"%s\" in map section. " - "Only assignment or filter operators are allowed", - fr_table_str_by_value(fr_tokens_table, map->op, "")); - return -1; - } - - return 0; -} - /** Validate and fixup a map that's part of an update section. * @@ -553,7 +453,7 @@ int unlang_fixup_update(map_t *map, void *ctx) } -static unlang_group_t *group_allocate(unlang_t *parent, CONF_SECTION *cs, unlang_type_t type) +unlang_group_t *unlang_group_allocate(unlang_t *parent, CONF_SECTION *cs, unlang_type_t type) { unlang_group_t *g; unlang_t *c; @@ -584,7 +484,7 @@ static unlang_group_t *group_allocate(unlang_t *parent, CONF_SECTION *cs, unlang return g; } -static void compile_action_defaults(unlang_t *c, unlang_compile_t *unlang_ctx) +void unlang_compile_action_defaults(unlang_t *c, unlang_compile_ctx_t *unlang_ctx) { int i; @@ -630,3741 +530,1175 @@ static void compile_action_defaults(unlang_t *c, unlang_compile_t *unlang_ctx) } } -static int compile_map_name(unlang_group_t *g) -{ - unlang_map_t *gext = unlang_group_to_map(g); +#define T(_x) [T_OP_ ## _x] = true - /* - * map - */ - if (gext->vpt) { - char quote; - size_t quoted_len; - char *quoted_str; - - switch (cf_section_argv_quote(g->cs, 0)) { - case T_DOUBLE_QUOTED_STRING: - quote = '"'; - break; +static const bool edit_list_sub_op[T_TOKEN_LAST] = { + T(NE), + T(GE), + T(GT), + T(LE), + T(LT), + T(CMP_EQ), +}; - case T_SINGLE_QUOTED_STRING: - quote = '\''; - break; +/** Validate and fixup a map that's part of an edit section. + * + * @param map to validate. + * @param ctx data to pass to fixup function (currently unused). + * @return 0 if valid else -1. + * + * @todo - this is only called for CONF_PAIR maps, not for + * CONF_SECTION. So when we parse nested maps, there's no validation + * done of the CONF_SECTION. In order to fix this, we need to have + * map_afrom_cs() call the validation function for the CONF_SECTION + * *before* recursing. + */ +static int unlang_fixup_edit(map_t *map, void *ctx) +{ + CONF_PAIR *cp = cf_item_to_pair(map->ci); + fr_dict_attr_t const *da; + fr_dict_attr_t const *parent = NULL; + map_t *parent_map = ctx; - case T_BACK_QUOTED_STRING: - quote = '`'; - break; + fr_assert(parent_map); +#ifdef STATIC_ANALYZER + if (!parent_map) return -1; +#endif - default: - quote = '\0'; - break; + fr_assert(tmpl_is_attr(parent_map->lhs)); + + if (parent_map && (parent_map->op == T_OP_SUB_EQ)) { + if (!edit_list_sub_op[map->op]) { + cf_log_err(cp, "Invalid operator '%s' for right-hand side list. It must be a comparison operator", fr_tokens[map->op]); + return -1; } - quoted_len = fr_snprint_len(gext->vpt->name, gext->vpt->len, quote); - quoted_str = talloc_array(g, char, quoted_len); - fr_snprint(quoted_str, quoted_len, gext->vpt->name, gext->vpt->len, quote); + } else if (map->op != T_OP_EQ) { + cf_log_err(cp, "Invalid operator '%s' for right-hand side list. It must be '='", fr_tokens[map->op]); + return -1; + } - g->self.name = talloc_typed_asprintf(g, "map %s %s", cf_section_name2(g->cs), quoted_str); - g->self.debug_name = g->self.name; - talloc_free(quoted_str); + /* + * map_afrom_cs() will build its tree recursively, and call us for each child map. + */ + if (map->parent && (map->parent != parent_map)) parent_map = map->parent; - return 0; - } + parent = tmpl_attr_tail_da(parent_map->lhs); - g->self.name = talloc_typed_asprintf(g, "map %s", cf_section_name2(g->cs)); - g->self.debug_name = g->self.name; + switch (map->lhs->type) { + case TMPL_TYPE_ATTR: + da = tmpl_attr_tail_da(map->lhs); + if (!da->flags.internal && parent && (parent->type != FR_TYPE_GROUP) && + (da->parent != parent)) { + /* FIXME - Broken check, doesn't work for key attributes */ + cf_log_err(cp, "Invalid location for %s - it is not a child of %s", + da->name, parent->name); + return 0; + } + break; - return 0; -} + case TMPL_TYPE_XLAT_UNRESOLVED: + case TMPL_TYPE_XLAT: + break; -static unlang_t *compile_map(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci) -{ - CONF_SECTION *cs = cf_item_to_section(ci); - int rcode; + default: + cf_log_err(map->ci, "Left side of map must be an attribute " + "or an xlat (that expands to an attribute), not a %s", + tmpl_type_to_str(map->lhs->type)); + return -1; + } - unlang_group_t *g; - unlang_map_t *gext; + fr_assert(map->rhs); - unlang_t *c; - CONF_SECTION *modules; - char const *tmpl_str; + switch (map->rhs->type) { + case TMPL_TYPE_DATA_UNRESOLVED: + case TMPL_TYPE_XLAT_UNRESOLVED: + case TMPL_TYPE_XLAT: + case TMPL_TYPE_DATA: + case TMPL_TYPE_ATTR: + case TMPL_TYPE_EXEC: + break; - tmpl_t *vpt = NULL; + default: + cf_log_err(map->ci, "Right side of map must be an attribute, literal, xlat or exec, got type %s", + tmpl_type_to_str(map->rhs->type)); + return -1; + } - map_proc_t *proc; - map_proc_inst_t *proc_inst; + return 0; +} - char const *name2 = cf_section_name2(cs); +/** Compile one edit section. + */ +static unlang_t *compile_edit_section(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_SECTION *cs) +{ + unlang_edit_t *edit; + unlang_t *c, *out = UNLANG_IGNORE; + map_t *map; + char const *name; + fr_token_t op; + ssize_t slen; + fr_dict_attr_t const *parent_da; + int num; tmpl_rules_t t_rules; - /* - * The RHS is NOT resolved in the context of the LHS. - */ - t_rules = *(unlang_ctx->rules); - t_rules.attr.disallow_rhs_resolve = true; - RULES_VERIFY(&t_rules); - - modules = cf_section_find(cf_root(cs), "modules", NULL); - if (!modules) { - cf_log_err(cs, "'map' sections require a 'modules' section"); + name = cf_section_name2(cs); + if (name) { + cf_log_err(cs, "Unexpected name2 '%s' for editing list %s ", name, cf_section_name1(cs)); return NULL; } - proc = map_proc_find(name2); - if (!proc) { - cf_log_err(cs, "Failed to find map processor '%s'", name2); + op = cf_section_name2_quote(cs); + if ((op == T_INVALID) || !fr_list_assignment_op[op]) { + cf_log_err(cs, "Invalid operator '%s' for editing list %s.", fr_tokens[op], cf_section_name1(cs)); return NULL; } - t_rules.literals_safe_for = map_proc_literals_safe_for(proc); - - g = group_allocate(parent, cs, UNLANG_TYPE_MAP); - if (!g) return NULL; - gext = unlang_group_to_map(g); + if ((op == T_OP_CMP_TRUE) || (op == T_OP_CMP_FALSE)) { + cf_log_err(cs, "Invalid operator \"%s\".", + fr_table_str_by_value(fr_tokens_table, op, "")); + return NULL; + } /* - * If there's a third string, it's the map src. - * - * Convert it into a template. + * We allow unknown attributes here. */ - tmpl_str = cf_section_argv(cs, 0); /* AFTER name1, name2 */ - if (tmpl_str) { - fr_token_t type; + t_rules = *(unlang_ctx->rules); + t_rules.attr.allow_unknown = true; + RULES_VERIFY(&t_rules); - type = cf_section_argv_quote(cs, 0); + edit = talloc_zero(parent, unlang_edit_t); + if (!edit) return NULL; - /* - * Try to parse the template. - */ - (void) tmpl_afrom_substr(gext, &vpt, - &FR_SBUFF_IN(tmpl_str, talloc_array_length(tmpl_str) - 1), - type, - NULL, - &t_rules); - if (!vpt) { - cf_log_perr(cs, "Failed parsing map"); - error: - talloc_free(g); - return NULL; - } + c = out = unlang_edit_to_generic(edit); + c->parent = parent; + c->next = NULL; + c->name = cf_section_name1(cs); + c->debug_name = c->name; + c->type = UNLANG_TYPE_EDIT; + c->ci = CF_TO_ITEM(cs); - /* - * Limit the allowed template types. - */ - switch (vpt->type) { - case TMPL_TYPE_DATA_UNRESOLVED: - case TMPL_TYPE_ATTR: - case TMPL_TYPE_ATTR_UNRESOLVED: - case TMPL_TYPE_XLAT: - case TMPL_TYPE_XLAT_UNRESOLVED: - case TMPL_TYPE_EXEC: - case TMPL_TYPE_EXEC_UNRESOLVED: - case TMPL_TYPE_DATA: - break; + map_list_init(&edit->maps); - default: - talloc_free(vpt); - cf_log_err(cs, "Invalid third argument for map"); - return NULL; - } - } + unlang_compile_action_defaults(c, unlang_ctx); /* - * This looks at cs->name2 to determine which list to update + * Allocate the map and initialize it. */ - map_list_init(&gext->map); - rcode = map_afrom_cs(gext, &gext->map, cs, unlang_ctx->rules, &t_rules, unlang_fixup_map, NULL, 256); - if (rcode < 0) return NULL; /* message already printed */ - if (map_list_empty(&gext->map)) { - cf_log_err(cs, "'map' sections cannot be empty"); - goto error; - } + MEM(map = talloc_zero(parent, map_t)); + map->op = op; + map->ci = cf_section_to_item(cs); + map_list_init(&map->child); + + name = cf_section_name1(cs); + slen = tmpl_afrom_attr_str(map, NULL, &map->lhs, name, &t_rules); + if (slen <= 0) { + cf_log_err(cs, "Failed parsing list reference %s - %s", name, fr_strerror()); + fail: + talloc_free(edit); + return NULL; + } /* - * Call the map's instantiation function to validate - * the map and perform any caching required. + * Can't assign to [*] or [#] */ - proc_inst = map_proc_instantiate(gext, proc, cs, vpt, &gext->map); - if (!proc_inst) { - cf_log_err(cs, "Failed instantiating map function '%s'", name2); - goto error; + num = tmpl_attr_tail_num(map->lhs); + if ((num == NUM_ALL) || (num == NUM_COUNT)) { + cf_log_err(cs, "Invalid array reference in %s", name); + goto fail; } - c = unlang_group_to_generic(g); - - gext->vpt = vpt; - gext->proc_inst = proc_inst; - - compile_map_name(g); /* - * Cache the module in the unlang_group_t struct. - * - * Ensure that the module has a "map" entry in its module - * header? Or ensure that the map is registered in the - * "bootstrap" phase, so that it's always available here. + * If the DA isn't structural, then it can't have children. */ - if (!pass2_fixup_map_rhs(g, unlang_ctx->rules)) goto error; - - compile_action_defaults(c, unlang_ctx); - - return c; -} - -static int edit_section_alloc(CONF_SECTION *parent, CONF_SECTION **child, char const *name1, fr_token_t op) -{ - CONF_SECTION *cs; - - cs = cf_section_alloc(parent, parent, name1, NULL); - if (!cs) return -1; + parent_da = tmpl_attr_tail_da(map->lhs); + if (fr_type_is_structural(parent_da->type)) { + map_t *child; - cf_section_add_name2_quote(cs, op); - - if (child) *child = cs; - - return 0; -} - -static int edit_pair_alloc(CONF_SECTION *cs, CONF_PAIR *original, char const *attr, fr_token_t op, char const *value, fr_token_t list_op) -{ - CONF_PAIR *cp; - fr_token_t rhs_quote; - - if (original) { - rhs_quote = cf_pair_value_quote(original); - } else { - rhs_quote = T_BARE_WORD; - } - - cp = cf_pair_alloc(cs, attr, value, op, T_BARE_WORD, rhs_quote); - if (!cp) return -1; - - if (!original) return 0; - - cf_filename_set(cp, cf_filename(original)); - cf_lineno_set(cp, cf_lineno(original)); - - if (fr_debug_lvl >= 3) { - if (list_op == T_INVALID) { - cf_log_err(original, "%s %s %s --> %s %s %s", - cf_pair_attr(original), fr_tokens[cf_pair_operator(original)], cf_pair_value(original), - attr, fr_tokens[op], value); - } else { - if (*attr == '&') attr++; - cf_log_err(original, "%s %s %s --> %s %s { %s %s %s }", - cf_pair_attr(original), fr_tokens[cf_pair_operator(original)], cf_pair_value(original), - cf_section_name1(cs), fr_tokens[list_op], attr, fr_tokens[op], value); - } - } else if (fr_debug_lvl >= 2) { - if (list_op == T_INVALID) { - cf_log_err(original, "--> %s %s %s", - attr, fr_tokens[op], value); - } else { - cf_log_err(original, "--> %s %s { %s %s %s }", - cf_section_name1(cs), fr_tokens[list_op], attr, fr_tokens[op], value); + /* + * Don't update namespace for &reply += { ... } + * + * Do update namespace for &reply.foo += { ... } + * + * Don't update if the LHS is an internal group. + */ + if ((tmpl_attr_num_elements(map->lhs) > 1) && (t_rules.attr.list_def != parent_da) && + !((parent_da->type == FR_TYPE_GROUP) && parent_da->flags.internal)) { + t_rules.attr.namespace = parent_da; } - } - - return 0; -} - -/* - * Convert "update" to "edit" using evil spells and sorcery. - */ -static unlang_t *compile_update_to_edit(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_SECTION *cs) -{ - char const *name2 = cf_section_name2(cs); - CONF_ITEM *ci; - CONF_SECTION *group; - unlang_group_t *g; - char list_buffer[32]; - char value_buffer[256]; - char attr_buffer[256]; - char const *list; - - g = unlang_generic_to_group(parent); - - /* - * Wrap it all in a group, no matter what. Because of - * limitations in the cf_pair_alloc() API. - */ - group = cf_section_alloc(g->cs, g->cs, "group", NULL); - if (!group) return NULL; - - (void) cf_item_remove(g->cs, group); /* was added at the end */ - cf_item_insert_after(g->cs, cs, group); - - /* - * Hoist this out of the loop, and make sure it never has a '&' prefix. - */ - if (name2) { - if (*name2 == '&') name2++; - snprintf(list_buffer, sizeof(list_buffer), "%s", name2); - } else { - snprintf(list_buffer, sizeof(list_buffer), "%s", tmpl_list_name(unlang_ctx->rules->attr.list_def, "")); - - } - - /* - * Loop over the entries, rewriting them. - */ - for (ci = cf_item_next(cs, NULL); - ci != NULL; - ci = cf_item_next(cs, ci)) { - CONF_PAIR *cp; - CONF_SECTION *child; - int rcode; - fr_token_t op; - char const *attr, *value, *end; - if (cf_item_is_section(ci)) { - cf_log_err(ci, "Cannot specify subsections for 'update'"); - return NULL; + if (map_afrom_cs_edit(map, &map->child, cs, &t_rules, &t_rules, unlang_fixup_edit, map, 256) < 0) { + goto fail; } - if (!cf_item_is_pair(ci)) continue; - - cp = cf_item_to_pair(ci); - - attr = cf_pair_attr(cp); - value = cf_pair_value(cp); - op = cf_pair_operator(cp); - - fr_assert(attr); - fr_assert(value); - - list = list_buffer; - - if (*attr == '&') attr++; - - end = strchr(attr, '.'); - if (!end) end = attr + strlen(attr); - /* - * Separate out the various possibilities for the "name", which could be a list, an - * attribute name, or a list followed by an attribute name. - * - * Note that even if we have "update request { ....}", the v3 parser allowed the contents - * of the "update" section to still specify parent / lists. Which makes parsing it all - * annoying. - * - * The good news is that all we care about is whether or not there's a parent / list ref. - * We don't care what that ref is. + * As a set of fixups... we can't do array references in -= */ - { - fr_dict_attr_t const *tmpl_list; + if (map->op == T_OP_SUB_EQ) { + for (child = map_list_head(&map->child); child != NULL; child = map_list_next(&map->child, child)) { + if (!tmpl_is_attr(child->lhs)) continue; - /* - * Allow for a "parent" or "outer" reference. There may be multiple - * "parent.parent", so we keep processing them until we get a list reference. - */ - if (fr_table_value_by_substr(tmpl_request_ref_table, attr, end - attr, REQUEST_UNKNOWN) != REQUEST_UNKNOWN) { + if (tmpl_attr_tail_num(child->lhs) != NUM_UNSPEC) { + cf_log_err(child->ci, "Cannot use array references and values when deleting from a list"); + goto fail; + } /* - * Catch one more case where the behavior is different. - * - * &request += &config[*] + * The edit code doesn't do this correctly, so we just forbid it. */ - if ((cf_pair_value_quote(cp) == T_BARE_WORD) && (*value == '&') && - (strchr(value, '.') == NULL) && (strchr(value, '[') != NULL)) { - char const *p = strchr(value, '['); - - cf_log_err(cp, "Cannot do array assignments for lists. Just use '%s %s %.*s'", - list, fr_tokens[op], (int) (p - value), value); - return NULL; - } - - goto attr_is_list; - - /* - * Doesn't have a parent ref, maybe it's a list ref? - */ - } else if (tmpl_attr_list_from_substr(&tmpl_list, &FR_SBUFF_IN(attr, (end - attr))) > 0) { - char *p; - - attr_is_list: - snprintf(attr_buffer, sizeof(attr_buffer), "%s", attr); - list = attr_buffer; - attr = NULL; - - p = strchr(attr_buffer, '.'); - if (p) { - *(p++) = '\0'; - attr = p; + if ((tmpl_attr_num_elements(child->lhs) - tmpl_attr_num_elements(map->lhs)) > 1) { + cf_log_err(child->ci, "List deletion must operate directly on the final child"); + goto fail; } - } - } - switch (op) { - /* - * FOO !* ANY - * - * The RHS doesn't matter, so we ignore it. - */ - case T_OP_CMP_FALSE: - if (!attr) { /* - * Set list to empty value. + * We don't do list comparisons either. */ - rcode = edit_section_alloc(group, NULL, list, T_OP_SET); - - } else { - if (strchr(attr, '[') == NULL) { - snprintf(value_buffer, sizeof(value_buffer), "%s[*]", attr); - } else { - snprintf(value_buffer, sizeof(value_buffer), "%s", attr); - } - - rcode = edit_pair_alloc(group, cp, list, T_OP_SUB_EQ, value_buffer, T_INVALID); - } - break; - - case T_OP_SET: - /* - * Must be a list-to-list operation - */ - if (!attr) { - list_op: - rcode = edit_pair_alloc(group, cp, list, op, value, T_INVALID); - break; - } - goto pair_op; - - case T_OP_EQ: - /* - * Allow &list = "foo" - */ - if (!attr) { - if (!value) { - cf_log_err(cp, "Missing value"); - return NULL; + if (fr_type_is_structural(tmpl_attr_tail_da(child->lhs)->type)) { + cf_log_err(child->ci, "List deletion cannot operate on lists"); + goto fail; } - - rcode = edit_pair_alloc(group, cp, list, op, value, T_INVALID); - break; - } - - pair_op: - fr_assert(*attr != '&'); - if (snprintf(value_buffer, sizeof(value_buffer), "%s.%s", list, attr) < 0) { - cf_log_err(cp, "RHS of update too long to convert to edit automatically"); - return NULL; - } - - rcode = edit_pair_alloc(group, cp, value_buffer, op, value, T_INVALID); - break; - - case T_OP_ADD_EQ: - case T_OP_PREPEND: - if (!attr) goto list_op; - - rcode = edit_section_alloc(group, &child, list, op); - if (rcode < 0) break; - - rcode = edit_pair_alloc(child, cp, attr, T_OP_EQ, value, op); - break; - - /* - * Remove matching attributes - */ - case T_OP_SUB_EQ: - op = T_OP_CMP_EQ; - - filter: - if (!attr) { - cf_log_err(cp, "Invalid operator for list assignment"); - return NULL; - } - - rcode = edit_section_alloc(group, &child, list, T_OP_SUB_EQ); - if (rcode < 0) break; - - if (strchr(attr, '[') != 0) { - cf_log_err(cp, "Cannot do filtering with array indexes"); - return NULL; } - - rcode = edit_pair_alloc(child, cp, attr, op, value, T_OP_SUB_EQ); - break; - - /* - * Keep matching attributes, i.e. remove non-matching ones. - */ - case T_OP_CMP_EQ: - op = T_OP_NE; - goto filter; - - case T_OP_NE: - op = T_OP_CMP_EQ; - goto filter; - - case T_OP_LT: - op = T_OP_GE; - goto filter; - - case T_OP_LE: - op = T_OP_GT; - goto filter; - - case T_OP_GT: - op = T_OP_LE; - goto filter; - - case T_OP_GE: - op = T_OP_LT; - goto filter; - - default: - cf_log_err(cp, "Unsupported operator - cannot auto-convert to edit section"); - return NULL; + } + } else { + /* + * &foo := { a, b, c } + */ + if (map_list_afrom_cs(map, &map->child, cs, &t_rules, NULL, NULL, 256) < 0) { + goto fail; } - if (rcode < 0) { - cf_log_err(cp, "Failed converting entry"); - return NULL; + if ((map->op != T_OP_SET) && !map_list_num_elements(&map->child)) { + cf_log_err(cs, "Cannot use operator '%s' for assigning empty list to '%s' data type.", + fr_tokens[map->op], fr_type_to_str(parent_da->type)); + goto fail; } } + /* + * Do basic sanity checks and resolving. + */ + if (!pass2_fixup_map(map, unlang_ctx->rules, NULL)) goto fail; - return UNLANG_IGNORE; -} + /* + * Check operators, and ensure that the RHS has been + * resolved. + */ +// if (unlang_fixup_update(map, NULL) < 0) goto fail; -static unlang_t *compile_update(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci) -{ - CONF_SECTION *cs = cf_item_to_section(ci); - int rcode; + map_list_insert_tail(&edit->maps, map); - unlang_group_t *g; - unlang_map_t *gext; + return out; +} - unlang_t *c; - char const *name2 = cf_section_name2(cs); +/** Compile one edit pair + * + */ +static unlang_t *compile_edit_pair(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_PAIR *cp) +{ + unlang_edit_t *edit; + unlang_t *c = NULL, *out = UNLANG_IGNORE; + map_t *map; + int num; tmpl_rules_t t_rules; - - if (main_config_migrate_option_get("forbid_update")) { - cf_log_err(cs, "The use of 'update' sections is forbidden by the server configuration"); - return NULL; - } - - /* - * If we're migrating "update" sections to edit, then go - * do that now. - */ - if (main_config_migrate_option_get("rewrite_update")) { - return compile_update_to_edit(parent, unlang_ctx, cs); - } + fr_token_t op; /* * We allow unknown attributes here. */ t_rules = *(unlang_ctx->rules); t_rules.attr.allow_unknown = true; - t_rules.attr.allow_wildcard = true; RULES_VERIFY(&t_rules); - g = group_allocate(parent, cs, UNLANG_TYPE_UPDATE); - if (!g) return NULL; + edit = talloc_zero(parent, unlang_edit_t); + if (!edit) return NULL; + + c = out = unlang_edit_to_generic(edit); + c->parent = parent; + c->next = NULL; + c->name = cf_pair_attr(cp); + c->debug_name = c->name; + c->type = UNLANG_TYPE_EDIT; + c->ci = CF_TO_ITEM(cp); + + map_list_init(&edit->maps); + + unlang_compile_action_defaults(c, unlang_ctx); - gext = unlang_group_to_map(g); + op = cf_pair_operator(cp); + if ((op == T_OP_CMP_TRUE) || (op == T_OP_CMP_FALSE)) { + cf_log_err(cp, "Invalid operator \"%s\".", + fr_table_str_by_value(fr_tokens_table, op, "")); + return NULL; + } /* - * This looks at cs->name2 to determine which list to update + * Convert this particular map. */ - map_list_init(&gext->map); - rcode = map_afrom_cs(gext, &gext->map, cs, &t_rules, &t_rules, unlang_fixup_update, NULL, 128); - if (rcode < 0) return NULL; /* message already printed */ - if (map_list_empty(&gext->map)) { - cf_log_err(cs, "'update' sections cannot be empty"); - error: - talloc_free(g); + if (map_afrom_cp(edit, &map, map_list_tail(&edit->maps), cp, &t_rules, NULL, true) < 0) { + fail: + talloc_free(edit); return NULL; } - c = unlang_group_to_generic(g); - if (name2) { - c->name = name2; - c->debug_name = talloc_typed_asprintf(c, "update %s", name2); - } else { - c->name = "update"; - c->debug_name = c->name; + /* + * @todo - we still want to do fixups on the RHS? + */ + if (tmpl_is_attr(map->lhs)) { + /* + * Can't assign to [*] or [#] + */ + num = tmpl_attr_tail_num(map->lhs); + if ((num == NUM_ALL) || (num == NUM_COUNT)) { + cf_log_err(cp, "Invalid array reference in %s", map->lhs->name); + goto fail; + } + + if ((map->op == T_OP_SUB_EQ) && fr_type_is_structural(tmpl_attr_tail_da(map->lhs)->type) && + tmpl_is_attr(map->rhs) && tmpl_attr_tail_da(map->rhs)->flags.local) { + cf_log_err(cp, "Cannot delete local variable %s", map->rhs->name); + goto fail; + } } - if (!pass2_fixup_update(g, unlang_ctx->rules)) goto error; + /* + * Do basic sanity checks and resolving. + */ + if (!pass2_fixup_map(map, unlang_ctx->rules, NULL)) goto fail; + + /* + * Check operators, and ensure that the RHS has been + * resolved. + */ + if (unlang_fixup_update(map, c) < 0) goto fail; - compile_action_defaults(c, unlang_ctx); + map_list_insert_tail(&edit->maps, map); - return c; + return out; } -#define T(_x) [T_OP_ ## _x] = true +#define debug_braces(_type) (unlang_ops[_type].flag & UNLANG_OP_FLAG_DEBUG_BRACES) -static const bool edit_list_sub_op[T_TOKEN_LAST] = { - T(NE), - T(GE), - T(GT), - T(LE), - T(LT), - T(CMP_EQ), -}; - -/** Validate and fixup a map that's part of an edit section. - * - * @param map to validate. - * @param ctx data to pass to fixup function (currently unused). - * @return 0 if valid else -1. - * - * @todo - this is only called for CONF_PAIR maps, not for - * CONF_SECTION. So when we parse nested maps, there's no validation - * done of the CONF_SECTION. In order to fix this, we need to have - * map_afrom_cs() call the validation function for the CONF_SECTION - * *before* recursing. - */ -static int unlang_fixup_edit(map_t *map, void *ctx) -{ - CONF_PAIR *cp = cf_item_to_pair(map->ci); - fr_dict_attr_t const *da; - fr_dict_attr_t const *parent = NULL; - map_t *parent_map = ctx; - - fr_assert(parent_map); -#ifdef STATIC_ANALYZER - if (!parent_map) return -1; -#endif - - fr_assert(tmpl_is_attr(parent_map->lhs)); - - if (parent_map && (parent_map->op == T_OP_SUB_EQ)) { - if (!edit_list_sub_op[map->op]) { - cf_log_err(cp, "Invalid operator '%s' for right-hand side list. It must be a comparison operator", fr_tokens[map->op]); - return -1; - } - - } else if (map->op != T_OP_EQ) { - cf_log_err(cp, "Invalid operator '%s' for right-hand side list. It must be '='", fr_tokens[map->op]); - return -1; - } - - /* - * map_afrom_cs() will build its tree recursively, and call us for each child map. - */ - if (map->parent && (map->parent != parent_map)) parent_map = map->parent; - - parent = tmpl_attr_tail_da(parent_map->lhs); - - switch (map->lhs->type) { - case TMPL_TYPE_ATTR: - da = tmpl_attr_tail_da(map->lhs); - if (!da->flags.internal && parent && (parent->type != FR_TYPE_GROUP) && - (da->parent != parent)) { - /* FIXME - Broken check, doesn't work for key attributes */ - cf_log_err(cp, "Invalid location for %s - it is not a child of %s", - da->name, parent->name); - return 0; - } - break; - - case TMPL_TYPE_XLAT_UNRESOLVED: - case TMPL_TYPE_XLAT: - break; - - default: - cf_log_err(map->ci, "Left side of map must be an attribute " - "or an xlat (that expands to an attribute), not a %s", - tmpl_type_to_str(map->lhs->type)); - return -1; - } - - fr_assert(map->rhs); - - switch (map->rhs->type) { - case TMPL_TYPE_DATA_UNRESOLVED: - case TMPL_TYPE_XLAT_UNRESOLVED: - case TMPL_TYPE_XLAT: - case TMPL_TYPE_DATA: - case TMPL_TYPE_ATTR: - case TMPL_TYPE_EXEC: - break; - - default: - cf_log_err(map->ci, "Right side of map must be an attribute, literal, xlat or exec, got type %s", - tmpl_type_to_str(map->rhs->type)); - return -1; - } - - return 0; -} - -/** Compile one edit section. - */ -static unlang_t *compile_edit_section(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_SECTION *cs) -{ - unlang_edit_t *edit; - unlang_t *c, *out = UNLANG_IGNORE; - map_t *map; - char const *name; - fr_token_t op; - ssize_t slen; - fr_dict_attr_t const *parent_da; - int num; - - tmpl_rules_t t_rules; - - name = cf_section_name2(cs); - if (name) { - cf_log_err(cs, "Unexpected name2 '%s' for editing list %s ", name, cf_section_name1(cs)); - return NULL; - } - - op = cf_section_name2_quote(cs); - if ((op == T_INVALID) || !fr_list_assignment_op[op]) { - cf_log_err(cs, "Invalid operator '%s' for editing list %s.", fr_tokens[op], cf_section_name1(cs)); - return NULL; - } - - if ((op == T_OP_CMP_TRUE) || (op == T_OP_CMP_FALSE)) { - cf_log_err(cs, "Invalid operator \"%s\".", - fr_table_str_by_value(fr_tokens_table, op, "")); - return NULL; - } - - /* - * We allow unknown attributes here. - */ - t_rules = *(unlang_ctx->rules); - t_rules.attr.allow_unknown = true; - RULES_VERIFY(&t_rules); - - edit = talloc_zero(parent, unlang_edit_t); - if (!edit) return NULL; - - c = out = unlang_edit_to_generic(edit); - c->parent = parent; - c->next = NULL; - c->name = cf_section_name1(cs); - c->debug_name = c->name; - c->type = UNLANG_TYPE_EDIT; - c->ci = CF_TO_ITEM(cs); - - map_list_init(&edit->maps); - - compile_action_defaults(c, unlang_ctx); - - /* - * Allocate the map and initialize it. - */ - MEM(map = talloc_zero(parent, map_t)); - map->op = op; - map->ci = cf_section_to_item(cs); - map_list_init(&map->child); - - name = cf_section_name1(cs); - - slen = tmpl_afrom_attr_str(map, NULL, &map->lhs, name, &t_rules); - if (slen <= 0) { - cf_log_err(cs, "Failed parsing list reference %s - %s", name, fr_strerror()); - fail: - talloc_free(edit); - return NULL; - } - - /* - * Can't assign to [*] or [#] - */ - num = tmpl_attr_tail_num(map->lhs); - if ((num == NUM_ALL) || (num == NUM_COUNT)) { - cf_log_err(cs, "Invalid array reference in %s", name); - goto fail; - } - - /* - * If the DA isn't structural, then it can't have children. - */ - parent_da = tmpl_attr_tail_da(map->lhs); - if (fr_type_is_structural(parent_da->type)) { - map_t *child; - - /* - * Don't update namespace for &reply += { ... } - * - * Do update namespace for &reply.foo += { ... } - * - * Don't update if the LHS is an internal group. - */ - if ((tmpl_attr_num_elements(map->lhs) > 1) && (t_rules.attr.list_def != parent_da) && - !((parent_da->type == FR_TYPE_GROUP) && parent_da->flags.internal)) { - t_rules.attr.namespace = parent_da; - } - - if (map_afrom_cs_edit(map, &map->child, cs, &t_rules, &t_rules, unlang_fixup_edit, map, 256) < 0) { - goto fail; - } - - /* - * As a set of fixups... we can't do array references in -= - */ - if (map->op == T_OP_SUB_EQ) { - for (child = map_list_head(&map->child); child != NULL; child = map_list_next(&map->child, child)) { - if (!tmpl_is_attr(child->lhs)) continue; - - if (tmpl_attr_tail_num(child->lhs) != NUM_UNSPEC) { - cf_log_err(child->ci, "Cannot use array references and values when deleting from a list"); - goto fail; - } - - /* - * The edit code doesn't do this correctly, so we just forbid it. - */ - if ((tmpl_attr_num_elements(child->lhs) - tmpl_attr_num_elements(map->lhs)) > 1) { - cf_log_err(child->ci, "List deletion must operate directly on the final child"); - goto fail; - } - - /* - * We don't do list comparisons either. - */ - if (fr_type_is_structural(tmpl_attr_tail_da(child->lhs)->type)) { - cf_log_err(child->ci, "List deletion cannot operate on lists"); - goto fail; - } - } - } - } else { - /* - * &foo := { a, b, c } - */ - if (map_list_afrom_cs(map, &map->child, cs, &t_rules, NULL, NULL, 256) < 0) { - goto fail; - } - - if ((map->op != T_OP_SET) && !map_list_num_elements(&map->child)) { - cf_log_err(cs, "Cannot use operator '%s' for assigning empty list to '%s' data type.", - fr_tokens[map->op], fr_type_to_str(parent_da->type)); - goto fail; - } - } - /* - * Do basic sanity checks and resolving. - */ - if (!pass2_fixup_map(map, unlang_ctx->rules, NULL)) goto fail; - - /* - * Check operators, and ensure that the RHS has been - * resolved. - */ -// if (unlang_fixup_update(map, NULL) < 0) goto fail; - - map_list_insert_tail(&edit->maps, map); - - return out; -} - -/** Compile one edit pair - * - */ -static unlang_t *compile_edit_pair(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_PAIR *cp) -{ - unlang_edit_t *edit; - unlang_t *c = NULL, *out = UNLANG_IGNORE; - map_t *map; - int num; - - tmpl_rules_t t_rules; - fr_token_t op; - - /* - * We allow unknown attributes here. - */ - t_rules = *(unlang_ctx->rules); - t_rules.attr.allow_unknown = true; - RULES_VERIFY(&t_rules); - - edit = talloc_zero(parent, unlang_edit_t); - if (!edit) return NULL; - - c = out = unlang_edit_to_generic(edit); - c->parent = parent; - c->next = NULL; - c->name = cf_pair_attr(cp); - c->debug_name = c->name; - c->type = UNLANG_TYPE_EDIT; - c->ci = CF_TO_ITEM(cp); - - map_list_init(&edit->maps); - - compile_action_defaults(c, unlang_ctx); - - op = cf_pair_operator(cp); - if ((op == T_OP_CMP_TRUE) || (op == T_OP_CMP_FALSE)) { - cf_log_err(cp, "Invalid operator \"%s\".", - fr_table_str_by_value(fr_tokens_table, op, "")); - return NULL; - } - - /* - * Convert this particular map. - */ - if (map_afrom_cp(edit, &map, map_list_tail(&edit->maps), cp, &t_rules, NULL, true) < 0) { - fail: - talloc_free(edit); - return NULL; - } - - /* - * @todo - we still want to do fixups on the RHS? - */ - if (tmpl_is_attr(map->lhs)) { - /* - * Can't assign to [*] or [#] - */ - num = tmpl_attr_tail_num(map->lhs); - if ((num == NUM_ALL) || (num == NUM_COUNT)) { - cf_log_err(cp, "Invalid array reference in %s", map->lhs->name); - goto fail; - } - - if ((map->op == T_OP_SUB_EQ) && fr_type_is_structural(tmpl_attr_tail_da(map->lhs)->type) && - tmpl_is_attr(map->rhs) && tmpl_attr_tail_da(map->rhs)->flags.local) { - cf_log_err(cp, "Cannot delete local variable %s", map->rhs->name); - goto fail; - } - } - - /* - * Do basic sanity checks and resolving. - */ - if (!pass2_fixup_map(map, unlang_ctx->rules, NULL)) goto fail; - - /* - * Check operators, and ensure that the RHS has been - * resolved. - */ - if (unlang_fixup_update(map, c) < 0) goto fail; - - map_list_insert_tail(&edit->maps, map); - - return out; -} - -static int define_local_variable(CONF_ITEM *ci, unlang_variable_t *var, tmpl_rules_t *t_rules, fr_type_t type, char const *name, - fr_dict_attr_t const *ref); - -#define debug_braces(_type) (unlang_ops[_type].flag & UNLANG_OP_FLAG_DEBUG_BRACES) - -/** Compile a variable definition. - * - * Definitions which are adjacent to one another are automatically merged - * into one larger variable definition. - */ -static int compile_variable(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_PAIR *cp, tmpl_rules_t *t_rules) -{ - unlang_variable_t *var; - fr_type_t type; - char const *attr, *value; - unlang_group_t *group; - - fr_assert(debug_braces(parent->type)); - - /* - * Enforce locations for local variables. - */ - switch (parent->type) { - case UNLANG_TYPE_CASE: - case UNLANG_TYPE_ELSE: - case UNLANG_TYPE_ELSIF: - case UNLANG_TYPE_FOREACH: - case UNLANG_TYPE_GROUP: - case UNLANG_TYPE_IF: - case UNLANG_TYPE_TIMEOUT: - case UNLANG_TYPE_LIMIT: - case UNLANG_TYPE_POLICY: - case UNLANG_TYPE_REDUNDANT: - case UNLANG_TYPE_SUBREQUEST: - case UNLANG_TYPE_LOAD_BALANCE: - case UNLANG_TYPE_REDUNDANT_LOAD_BALANCE: - break; - - default: - cf_log_err(cp, "Local variables cannot be used here"); - return -1; - } - - /* - * The variables exist in the parent block. - */ - group = unlang_generic_to_group(parent); - if (group->variables) { - var = group->variables; - - } else { - group->variables = var = talloc_zero(parent, unlang_variable_t); - if (!var) return -1; - - var->dict = fr_dict_protocol_alloc(unlang_ctx->rules->attr.dict_def); - if (!var->dict) { - talloc_free(var); - return -1; - } - var->root = fr_dict_root(var->dict); - - var->max_attr = 1; - - /* - * Initialize the new rules, and point them to the parent rules. - * - * Then replace the parse rules with our rules, and our dictionary. - */ - *t_rules = *unlang_ctx->rules; - t_rules->parent = unlang_ctx->rules; - - t_rules->attr.dict_def = var->dict; - t_rules->attr.namespace = NULL; - - unlang_ctx->rules = t_rules; - } - - attr = cf_pair_attr(cp); /* data type */ - value = cf_pair_value(cp); /* variable name */ - - type = fr_table_value_by_str(fr_type_table, attr, FR_TYPE_NULL); - if (type == FR_TYPE_NULL) { -invalid_type: - cf_log_err(cp, "Invalid data type '%s'", attr); - return -1; - } - - /* - * Leaf and group are OK. TLV, Vendor, Struct, VSA, etc. are not. - */ - if (!(fr_type_is_leaf(type) || (type == FR_TYPE_GROUP))) goto invalid_type; - - return define_local_variable(cf_pair_to_item(cp), var, t_rules, type, value, NULL); -} - -/* - * Compile action && rcode for later use. - */ -static int compile_action_pair(unlang_mod_actions_t *actions, CONF_PAIR *cp) -{ - int action; - char const *attr, *value; - - attr = cf_pair_attr(cp); - value = cf_pair_value(cp); - if (!value) return 0; - - if (!strcasecmp(value, "return")) - action = MOD_ACTION_RETURN; - - else if (!strcasecmp(value, "break")) - action = MOD_ACTION_RETURN; - - else if (!strcasecmp(value, "reject")) - action = MOD_ACTION_REJECT; - - else if (!strcasecmp(value, "retry")) - action = MOD_ACTION_RETRY; - - else if (strspn(value, "0123456789")==strlen(value)) { - action = atoi(value); - - if (!action || (action > MOD_PRIORITY_MAX)) { - cf_log_err(cp, "Priorities MUST be between 1 and 64."); - return 0; - } - - } else { - cf_log_err(cp, "Unknown action '%s'.\n", - value); - return 0; - } - - if (strcasecmp(attr, "default") != 0) { - int rcode; - - rcode = fr_table_value_by_str(mod_rcode_table, attr, -1); - if (rcode < 0) { - cf_log_err(cp, - "Unknown module rcode '%s'.", - attr); - return 0; - } - actions->actions[rcode] = action; - - } else { /* set all unset values to the default */ - int i; - - for (i = 0; i < RLM_MODULE_NUMCODES; i++) { - if (!actions->actions[i]) actions->actions[i] = action; - } - } - - return 1; -} - -static bool compile_retry_section(unlang_mod_actions_t *actions, CONF_ITEM *ci) -{ - CONF_ITEM *csi; - CONF_SECTION *cs; - - cs = cf_item_to_section(ci); - for (csi=cf_item_next(cs, NULL); - csi != NULL; - csi=cf_item_next(cs, csi)) { - CONF_PAIR *cp; - char const *name, *value; - - if (cf_item_is_section(csi)) { - cf_log_err(csi, "Invalid subsection in 'retry' configuration."); - return false; - } - - if (!cf_item_is_pair(csi)) continue; - - cp = cf_item_to_pair(csi); - name = cf_pair_attr(cp); - value = cf_pair_value(cp); - - if (!value) { - cf_log_err(csi, "Retry configuration must specify a value"); - return false; - } - -#define CLAMP(_name, _field, _limit) do { \ - if (!fr_time_delta_ispos(actions->retry._field)) { \ - cf_log_err(csi, "Invalid value for '" STRINGIFY(_name) " = %s' - value must be positive", \ - value); \ - return false; \ - } \ - if (fr_time_delta_cmp(actions->retry._field, fr_time_delta_from_sec(_limit)) > 0) { \ - cf_log_err(csi, "Invalid value for '" STRINGIFY(_name) " = %s' - value must be less than " STRINGIFY(_limit) "s", \ - value); \ - return false; \ - } \ - } while (0) - - /* - * We don't use conf_parser_t here for various - * magical reasons. - */ - if (strcmp(name, "initial_rtx_time") == 0) { - if (fr_time_delta_from_str(&actions->retry.irt, value, strlen(value), FR_TIME_RES_SEC) < 0) { - error: - cf_log_err(csi, "Failed parsing '%s = %s' - %s", - name, value, fr_strerror()); - return false; - } - CLAMP(initial_rtx_time, irt, 2); - - } else if (strcmp(name, "max_rtx_time") == 0) { - if (fr_time_delta_from_str(&actions->retry.mrt, value, strlen(value), FR_TIME_RES_SEC) < 0) goto error; - - CLAMP(max_rtx_time, mrt, 10); - - } else if (strcmp(name, "max_rtx_count") == 0) { - unsigned long v = strtoul(value, 0, 0); - - if (v > 10) { - cf_log_err(csi, "Invalid value for 'max_rtx_count = %s' - value must be between 0 and 10", - value); - return false; - } - - actions->retry.mrc = v; - - } else if (strcmp(name, "max_rtx_duration") == 0) { - if (fr_time_delta_from_str(&actions->retry.mrd, value, strlen(value), FR_TIME_RES_SEC) < 0) goto error; - - CLAMP(max_rtx_duration, mrd, 20); - - } else { - cf_log_err(csi, "Invalid item '%s' in 'retry' configuration.", name); - return false; - } - } - - return true; -} - -bool unlang_compile_actions(unlang_mod_actions_t *actions, CONF_SECTION *action_cs, bool module_retry) -{ - int i; - bool disallow_retry_action = false; - CONF_ITEM *csi; - CONF_SECTION *cs; - - /* - * Over-ride the default return codes of the module. - */ - cs = cf_item_to_section(cf_section_to_item(action_cs)); - for (csi=cf_item_next(cs, NULL); - csi != NULL; - csi=cf_item_next(cs, csi)) { - char const *name; - CONF_PAIR *cp; - - if (cf_item_is_section(csi)) { - CONF_SECTION *subcs = cf_item_to_section(csi); - - name = cf_section_name1(subcs); - - /* - * Look for a "retry" section. - */ - if (name && (strcmp(name, "retry") == 0) && !cf_section_name2(subcs)) { - if (!compile_retry_section(actions, csi)) return false; - continue; - } - - cf_log_err(csi, "Invalid subsection. Expected 'action = value'"); - return false; - } - - if (!cf_item_is_pair(csi)) continue; - - cp = cf_item_to_pair(csi); - - /* - * Allow 'retry = path.to.retry.config' - */ - name = cf_pair_attr(cp); - if (strcmp(name, "retry") == 0) { - CONF_ITEM *subci; - char const *value = cf_pair_value(cp); - - if (!value) { - cf_log_err(csi, "Missing reference string"); - return false; - } - - subci = cf_reference_item(cs, cf_root(cf_section_to_item(action_cs)), value); - if (!subci) { - cf_log_perr(csi, "Failed finding reference '%s'", value); - return false; - } - - if (!compile_retry_section(actions, subci)) return false; - continue; - } - - if (!compile_action_pair(actions, cp)) { - return false; - } - } - - if (module_retry) { - if (!fr_time_delta_ispos(actions->retry.irt)) { - cf_log_err(csi, "initial_rtx_time MUST be non-zero for modules which support retries."); - return false; - } - } else { - if (fr_time_delta_ispos(actions->retry.irt)) { - cf_log_err(csi, "initial_rtx_time MUST be zero, as only max_rtx_count and max_rtx_duration are used."); - return false; - } - - if (!actions->retry.mrc && !fr_time_delta_ispos(actions->retry.mrd)) { - disallow_retry_action = true; - } - } - - /* - * Sanity check that "fail = retry", we actually have a - * retry section. - */ - for (i = 0; i < RLM_MODULE_NUMCODES; i++) { - if (actions->actions[i] != MOD_ACTION_RETRY) continue; - - if (module_retry) { - cf_log_err(csi, "Cannot use a '%s = retry' action for a module which has its own retries", - fr_table_str_by_value(mod_rcode_table, i, "")); - return false; - } - - if (disallow_retry_action) { - cf_log_err(csi, "max_rtx_count and max_rtx_duration cannot both be zero when using '%s = retry'", - fr_table_str_by_value(mod_rcode_table, i, "")); - return false; - } - - if (!fr_time_delta_ispos(actions->retry.irt) && - !actions->retry.mrc && - !fr_time_delta_ispos(actions->retry.mrd)) { - cf_log_err(csi, "Cannot use a '%s = retry' action without a 'retry { ... }' section.", - fr_table_str_by_value(mod_rcode_table, i, "")); - return false; - } - } - - return true; -} - -static unlang_t *compile_empty(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_SECTION *cs, unlang_type_t type) -{ - unlang_group_t *g; - unlang_t *c; - - /* - * If we're compiling an empty section, then the - * *interpreter* type is GROUP, even if the *debug names* - * are something else. - */ - g = group_allocate(parent, cs, type); - if (!g) return NULL; - - c = unlang_group_to_generic(g); - if (!cs) { - c->name = unlang_ops[type].name; - c->debug_name = c->name; - - } else { - char const *name2; - - name2 = cf_section_name2(cs); - if (!name2) { - c->name = cf_section_name1(cs); - c->debug_name = c->name; - } else { - c->name = name2; - c->debug_name = talloc_typed_asprintf(c, "%s %s", cf_section_name1(cs), name2); - } - } - - compile_action_defaults(c, unlang_ctx); - return c; -} - - -static unlang_t *compile_item(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM *ci); - -/* - * compile 'actions { ... }' inside of another group. - */ -static bool compile_action_subsection(unlang_t *c, CONF_SECTION *cs, CONF_SECTION *subcs) -{ - CONF_ITEM *ci, *next; - - ci = cf_section_to_item(subcs); - - next = cf_item_next(cs, ci); - if (next && (cf_item_is_pair(next) || cf_item_is_section(next))) { - cf_log_err(ci, "'actions' MUST be the last block in a section"); - return false; - } - - if (cf_section_name2(subcs) != NULL) { - cf_log_err(ci, "Invalid name for 'actions' section"); - return false; - } - - /* - * Over-riding the actions can be done in certain limited - * situations. In other situations (e.g. "redundant", - * "load-balance"), it doesn't make sense. - * - * Note that this limitation also applies to "retry" - * timers. We can do retries of a "group". We cannot do - * retries of "load-balance", as the "load-balance" - * section already takes care of redundancy. - * - * We may need to loosen that limitation in the future. - */ - switch (c->type) { - case UNLANG_TYPE_CASE: - case UNLANG_TYPE_IF: - case UNLANG_TYPE_ELSE: - case UNLANG_TYPE_ELSIF: - case UNLANG_TYPE_FOREACH: - case UNLANG_TYPE_GROUP: - case UNLANG_TYPE_LIMIT: - case UNLANG_TYPE_SWITCH: - case UNLANG_TYPE_TIMEOUT: - case UNLANG_TYPE_TRANSACTION: - break; - - default: - cf_log_err(ci, "'actions' MUST NOT be in a '%s' block", unlang_ops[c->type].name); - return false; - } - - return unlang_compile_actions(&c->actions, subcs, false); -} - - -static unlang_t *compile_children(unlang_group_t *g, unlang_compile_t *unlang_ctx_in, bool set_action_defaults) -{ - CONF_ITEM *ci = NULL; - unlang_t *c, *single; - bool was_if = false; - char const *skip_else = NULL; - unlang_compile_t *unlang_ctx; - unlang_compile_t unlang_ctx2; - tmpl_rules_t t_rules; - - c = unlang_group_to_generic(g); - - /* - * Create our own compilation context which can be edited - * by a variable definition. - */ - compile_copy_context(&unlang_ctx2, unlang_ctx_in); - unlang_ctx = &unlang_ctx2; - t_rules = *unlang_ctx_in->rules; - - /* - * Loop over the children of this group. - */ - while ((ci = cf_item_next(g->cs, ci))) { - if (cf_item_is_data(ci)) continue; - - /* - * Sections are keywords, or references to - * modules with updated return codes. - */ - if (cf_item_is_section(ci)) { - char const *name = NULL; - CONF_SECTION *subcs = cf_item_to_section(ci); - - /* - * Skip precompiled blocks. This is - * mainly for policies. - */ - if (cf_data_find(subcs, unlang_group_t, NULL)) continue; - - /* - * "actions" apply to the current group. - * It's not a subgroup. - */ - name = cf_section_name1(subcs); - - /* - * In-line attribute editing. Nothing else in the parse has list assignments, so this must be it. - */ - if (fr_list_assignment_op[cf_section_name2_quote(subcs)]) { - single = compile_edit_section(c, unlang_ctx, subcs); - if (!single) { - talloc_free(c); - return NULL; - } - - goto add_child; - } - - if (strcmp(name, "actions") == 0) { - if (!compile_action_subsection(c, g->cs, subcs)) { - talloc_free(c); - return NULL; - } - - continue; - } - - /* - * Special checks for "else" and "elsif". - */ - if ((strcmp(name, "else") == 0) || (strcmp(name, "elsif") == 0)) { - /* - * We ran into one without a preceding "if" or "elsif". - * That's not allowed. - */ - if (!was_if) { - cf_log_err(ci, "Invalid location for '%s'. There is no preceding " - "'if' or 'elsif' statement", name); - talloc_free(c); - return NULL; - } - - /* - * There was a previous "if" or "elsif" which was always taken. - * So we skip this "elsif" or "else". - */ - if (skip_else) { - void *ptr; - - /* - * And manually free this. - */ - ptr = cf_data_remove(subcs, xlat_exp_head_t, NULL); - talloc_free(ptr); - - cf_section_free_children(subcs); - - cf_log_debug_prefix(ci, "Skipping contents of '%s' due to previous " - "'%s' being always being taken.", - name, skip_else); - continue; - } - } - - /* - * Otherwise it's a real keyword. - */ - single = compile_item(c, unlang_ctx, ci); - if (!single) { - cf_log_err(ci, "Failed to parse \"%s\" subsection", cf_section_name1(subcs)); - talloc_free(c); - return NULL; - } - - goto add_child; - - } else if (cf_item_is_pair(ci)) { - CONF_PAIR *cp = cf_item_to_pair(ci); - - /* - * Variable definition. - */ - if (cf_pair_operator(cp) == T_OP_CMP_TRUE) { - if (compile_variable(c, unlang_ctx, cp, &t_rules) < 0) { - talloc_free(c); - return NULL; - } - - single = UNLANG_IGNORE; - goto add_child; - } - - if (!cf_pair_value(cp)) { - single = compile_item(c, unlang_ctx, ci); - if (!single) { - cf_log_err(ci, "Invalid keyword \"%s\".", cf_pair_attr(cp)); - talloc_free(c); - return NULL; - } - - goto add_child; - } - - /* - * What remains MUST be an edit pair. At this point, the state of the compiler - * tells us what it is, and we don't really care if there's a leading '&'. - */ - single = compile_edit_pair(c, unlang_ctx, cp); - if (!single) { - talloc_free(c); - return NULL; - } - - goto add_child; - } else { - cf_log_err(ci, "Asked to compile unknown conf type"); - talloc_free(c); - return NULL; - } - - add_child: - if (single == UNLANG_IGNORE) continue; - - /* - * Do optimizations for "if" and "elsif" - * conditions. - */ - switch (single->type) { - case UNLANG_TYPE_ELSIF: - case UNLANG_TYPE_IF: - was_if = true; - - { - unlang_group_t *f; - unlang_cond_t *gext; - - /* - * Skip else, and/or omit things which will never be run. - */ - f = unlang_generic_to_group(single); - gext = unlang_group_to_cond(f); - - if (gext->is_truthy) { - if (gext->value) { - skip_else = single->debug_name; - } else { - /* - * The condition never - * matches, so we can - * avoid putting it into - * the unlang tree. - */ - talloc_free(single); - continue; - } - } - } - break; - - default: - was_if = false; - skip_else = NULL; - break; - } - - /* - * unlang_group_t is grown by adding a unlang_t to the end - */ - fr_assert(g == talloc_parent(single)); - fr_assert(single->parent == unlang_group_to_generic(g)); - fr_assert(!single->next); - - *g->tail = single; - g->tail = &single->next; - g->num_children++; - - /* - * If it's not possible to execute statement - * after the current one, then just stop - * processing the children. - */ - if (g->self.closed) { - cf_log_warn(ci, "Skipping remaining instructions due to '%s'", - single->name); - break; - } - } - - /* - * Set the default actions, if they haven't already been - * set by an "actions" section above. - */ - if (set_action_defaults) compile_action_defaults(c, unlang_ctx); - - return c; -} - - -/* - * Generic "compile a section with more unlang inside of it". - */ -static unlang_t *compile_section(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_SECTION *cs, unlang_type_t type) -{ - unlang_group_t *g; - unlang_t *c; - char const *name1, *name2; - - fr_assert(unlang_ctx->rules != NULL); - fr_assert(unlang_ctx->rules->attr.list_def); - - /* - * We always create a group, even if the section is empty. - */ - g = group_allocate(parent, cs, type); - if (!g) return NULL; - - c = unlang_group_to_generic(g); - - /* - * Remember the name for printing, etc. - */ - name1 = cf_section_name1(cs); - name2 = cf_section_name2(cs); - c->name = name1; - - /* - * Make sure to tell the user that we're running a - * policy, and not anything else. - */ - if (type == UNLANG_TYPE_POLICY) { - MEM(c->debug_name = talloc_typed_asprintf(c, "policy %s", name1)); - - } else if (!name2) { - c->debug_name = c->name; - - } else { - MEM(c->debug_name = talloc_typed_asprintf(c, "%s %s", name1, name2)); - } - - return compile_children(g, unlang_ctx, true); -} - - -static unlang_t *compile_group(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci) -{ - if (!cf_item_next(ci, NULL)) return UNLANG_IGNORE; - - return compile_section(parent, unlang_ctx, cf_item_to_section(ci), UNLANG_TYPE_GROUP); -} - -static fr_table_num_sorted_t transaction_keywords[] = { - { L("case"), 1 }, - { L("else"), 1 }, - { L("elsif"), 1 }, - { L("foreach"), 1 }, - { L("group"), 1 }, - { L("if"), 1 }, - { L("limit"), 1 }, - { L("load-balance"), 1 }, - { L("redundant"), 1 }, - { L("redundant-load-balance"), 1 }, - { L("switch"), 1 }, - { L("timeout"), 1 }, - { L("transaction"), 1 }, -}; -static int transaction_keywords_len = NUM_ELEMENTS(transaction_keywords); - -/** Limit the operations which can appear in a transaction. - */ -static bool transaction_ok(CONF_SECTION *cs) -{ - CONF_ITEM *ci = NULL; - - while ((ci = cf_item_next(cs, ci)) != NULL) { - char const *name; - - if (cf_item_is_section(ci)) { - CONF_SECTION *subcs; - - subcs = cf_item_to_section(ci); - name = cf_section_name1(subcs); - - if (strcmp(name, "actions") == 0) continue; - - /* - * Definitely an attribute editing thing. - */ - if (*name == '&') continue; - - if (fr_list_assignment_op[cf_section_name2_quote(cs)]) continue; - - if (fr_table_value_by_str(transaction_keywords, name, -1) < 0) { - cf_log_err(ci, "Invalid keyword in 'transaction'"); - return false; - } - - if (!transaction_ok(subcs)) return false; - - continue; - - } else if (cf_item_is_pair(ci)) { - CONF_PAIR *cp; - - cp = cf_item_to_pair(ci); - name = cf_pair_attr(cp); - - /* - * If there's a value then it's not a module call. - */ - if (cf_pair_value(cp)) continue; - - if (*name == '&') continue; - - /* - * Allow rcodes via the "always" module. - */ - if (fr_table_value_by_str(mod_rcode_table, name, -1) >= 0) { - continue; - } - - cf_log_err(ci, "Invalid module reference in 'transaction'"); - return false; - - } else { - continue; - } - } - - return true; -} - -static unlang_t *compile_transaction(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci) -{ - CONF_SECTION *cs = cf_item_to_section(ci); - unlang_group_t *g; - unlang_t *c; - unlang_compile_t unlang_ctx2; - - if (cf_section_name2(cs) != NULL) { - cf_log_err(cs, "Unexpected argument to 'transaction' section"); - cf_log_err(ci, DOC_KEYWORD_REF(transaction)); - return NULL; - } - - /* - * The transaction is empty, ignore it. - */ - if (!cf_item_next(cs, NULL)) return UNLANG_IGNORE; - - if (!transaction_ok(cs)) return NULL; - - /* - * Any failure is return, not continue. - */ - compile_copy_context(&unlang_ctx2, unlang_ctx); - - unlang_ctx2.actions.actions[RLM_MODULE_REJECT] = MOD_ACTION_RETURN; - unlang_ctx2.actions.actions[RLM_MODULE_FAIL] = MOD_ACTION_RETURN; - unlang_ctx2.actions.actions[RLM_MODULE_INVALID] = MOD_ACTION_RETURN; - unlang_ctx2.actions.actions[RLM_MODULE_DISALLOW] = MOD_ACTION_RETURN; - - g = group_allocate(parent, cs, UNLANG_TYPE_TRANSACTION); - if (!g) return NULL; - - c = unlang_group_to_generic(g); - c->debug_name = c->name = cf_section_name1(cs); - - if (!compile_children(g, &unlang_ctx2, false)) return NULL; - - /* - * The default for a failed transaction is to continue on - * failure. - */ - if (!c->actions.actions[RLM_MODULE_FAIL]) c->actions.actions[RLM_MODULE_FAIL] = 1; - if (!c->actions.actions[RLM_MODULE_INVALID]) c->actions.actions[RLM_MODULE_INVALID] = 1; - if (!c->actions.actions[RLM_MODULE_DISALLOW]) c->actions.actions[RLM_MODULE_DISALLOW] = 1; - - compile_action_defaults(c, unlang_ctx); - - return c; -} - -static unlang_t *compile_try(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci) -{ - CONF_SECTION *cs = cf_item_to_section(ci); - unlang_group_t *g; - unlang_t *c; - CONF_ITEM *next; - - /* - * The transaction is empty, ignore it. - */ - if (!cf_item_next(cs, NULL)) { - cf_log_err(cs, "'try' sections cannot be empty"); - print_url: - cf_log_err(ci, DOC_KEYWORD_REF(try)); - return NULL; - } - - if (cf_section_name2(cs) != NULL) { - cf_log_err(cs, "Unexpected argument to 'try' section"); - goto print_url; - } - - next = cf_item_next(cf_parent(cs), ci); - while (next && cf_item_is_data(next)) next = cf_item_next(cf_parent(cs), next); - - if (!next || !cf_item_is_section(next) || - (strcmp(cf_section_name1(cf_item_to_section(next)), "catch") != 0)) { - cf_log_err(cs, "'try' sections must be followed by a 'catch'"); - goto print_url; - } - - g = group_allocate(parent, cs, UNLANG_TYPE_TRY); - if (!g) return NULL; - - c = unlang_group_to_generic(g); - c->debug_name = c->name = cf_section_name1(cs); - - return compile_children(g, unlang_ctx, true); -} - -static int catch_argv(CONF_SECTION *cs, unlang_catch_t *ca, char const *name) -{ - int rcode; - - rcode = fr_table_value_by_str(mod_rcode_table, name, -1); - if (rcode < 0) { - cf_log_err(cs, "Unknown rcode '%s'.", name); - return -1; - } - - if (ca->catching[rcode]) { - cf_log_err(cs, "Duplicate rcode '%s'.", name); - return -1; - } - - ca->catching[rcode] = true; - - return 0; -} - -static unlang_t *compile_catch(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci) -{ - CONF_SECTION *cs = cf_item_to_section(ci); - unlang_group_t *g; - unlang_t *c; - unlang_catch_t *ca; - CONF_ITEM *prev; - char const *name; - - prev = cf_item_prev(cf_parent(ci), ci); - while (prev && cf_item_is_data(prev)) prev = cf_item_prev(cf_parent(ci), prev); - - if (!prev || !cf_item_is_section(prev)) { - fail: - cf_log_err(cs, "Found 'catch' section with no previous 'try'"); - cf_log_err(ci, DOC_KEYWORD_REF(catch)); - return NULL; - } - - name = cf_section_name1(cf_item_to_section(prev)); - fr_assert(name != NULL); - - if ((strcmp(name, "try") != 0) && (strcmp(name, "catch") != 0)) { - /* - * The previous thing has to be a section. And it has to - * be either a "try" or a "catch". - */ - goto fail; - } - - g = group_allocate(parent, cs, UNLANG_TYPE_CATCH); - if (!g) return NULL; - - c = unlang_group_to_generic(g); - - /* - * Want to log what we caught - */ - c->debug_name = c->name = talloc_typed_asprintf(c, "%s %s", cf_section_name1(cs), cf_section_name2(cs)); - - ca = unlang_group_to_catch(g); - if (!cf_section_name2(cs)) { - /* - * No arg2: catch errors - */ - ca->catching[RLM_MODULE_REJECT] = true; - ca->catching[RLM_MODULE_FAIL] = true; - ca->catching[RLM_MODULE_INVALID] = true; - ca->catching[RLM_MODULE_DISALLOW] = true; - - } else { - int i; - - name = cf_section_name2(cs); - - if (catch_argv(cs, ca, name) < 0) { - talloc_free(c); - return NULL; - } - - for (i = 0; (name = cf_section_argv(cs, i)) != NULL; i++) { - if (catch_argv(cs, ca, name) < 0) { - talloc_free(c); - return NULL; - } - } - } - - /* - * @todo - Else parse and limit the things we catch - */ - - return compile_children(g, unlang_ctx, true); -} - - -static int8_t case_cmp(void const *one, void const *two) -{ - unlang_case_t const *a = (unlang_case_t const *) one; /* may not be talloc'd! See switch.c */ - unlang_case_t const *b = (unlang_case_t const *) two; /* may not be talloc'd! */ - - return fr_value_box_cmp(tmpl_value(a->vpt), tmpl_value(b->vpt)); -} - -static uint32_t case_hash(void const *data) -{ - unlang_case_t const *a = (unlang_case_t const *) data; /* may not be talloc'd! */ - - return fr_value_box_hash(tmpl_value(a->vpt)); -} - -static int case_to_key(uint8_t **out, size_t *outlen, void const *data) -{ - unlang_case_t const *a = (unlang_case_t const *) data; /* may not be talloc'd! */ - - return fr_value_box_to_key(out, outlen, tmpl_value(a->vpt)); -} - -static unlang_t *compile_case(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci); - -static unlang_t *compile_switch(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci) -{ - CONF_SECTION *cs = cf_item_to_section(ci); - CONF_ITEM *subci; - fr_token_t token; - char const *name1, *name2; - char const *type_name; - - unlang_group_t *g; - unlang_switch_t *gext; - - unlang_t *c; - ssize_t slen; - - tmpl_rules_t t_rules; - - fr_type_t type; - fr_htrie_type_t htype; - - /* - * We allow unknown attributes here. - */ - t_rules = *(unlang_ctx->rules); - t_rules.attr.allow_unknown = true; - RULES_VERIFY(&t_rules); - - name2 = cf_section_name2(cs); - if (!name2) { - cf_log_err(cs, "You must specify a variable to switch over for 'switch'"); - print_url: - cf_log_err(ci, DOC_KEYWORD_REF(switch)); - return NULL; - } - - if (!cf_item_next(cs, NULL)) return UNLANG_IGNORE; - - g = group_allocate(parent, cs, UNLANG_TYPE_SWITCH); - if (!g) return NULL; - - gext = unlang_group_to_switch(g); - - /* - * Create the template. All attributes and xlats are - * defined by now. - * - * The 'case' statements need g->vpt filled out to ensure - * that the data types match. - */ - token = cf_section_name2_quote(cs); - - if ((token == T_BARE_WORD) && (name2[0] != '%')) { - slen = tmpl_afrom_attr_substr(gext, NULL, &gext->vpt, - &FR_SBUFF_IN(name2, strlen(name2)), - NULL, - &t_rules); - } else { - slen = tmpl_afrom_substr(gext, &gext->vpt, - &FR_SBUFF_IN(name2, strlen(name2)), - token, - NULL, - &t_rules); - } - if (!gext->vpt) { - cf_canonicalize_error(cs, slen, "Failed parsing argument to 'switch'", name2); - talloc_free(g); - return NULL; - } - - c = unlang_group_to_generic(g); - c->name = "switch"; - c->debug_name = talloc_typed_asprintf(c, "switch %s", name2); - - /* - * Fixup the template before compiling the children. - * This is so that compile_case() can do attribute type - * checks / casts against us. - */ - if (!pass2_fixup_tmpl(g, &gext->vpt, cf_section_to_item(cs), unlang_ctx->rules->attr.dict_def)) { - talloc_free(g); - return NULL; - } - - if (tmpl_is_list(gext->vpt)) { - cf_log_err(cs, "Cannot use list for 'switch' statement"); - error: - talloc_free(g); - goto print_url; - } - - if (tmpl_contains_regex(gext->vpt)) { - cf_log_err(cs, "Cannot use regular expression for 'switch' statement"); - goto error; - } - - if (tmpl_is_data(gext->vpt)) { - cf_log_err(cs, "Cannot use constant data for 'switch' statement"); - goto error; - } - - if (tmpl_is_xlat(gext->vpt)) { - xlat_exp_head_t *xlat = tmpl_xlat(gext->vpt); - - if (xlat->flags.constant || xlat->flags.pure) { - cf_log_err(cs, "Cannot use constant data for 'switch' statement"); - goto error; - } - } - - - if (tmpl_needs_resolving(gext->vpt)) { - cf_log_err(cs, "Cannot resolve key for 'switch' statement"); - goto error; - } - - type_name = cf_section_argv(cs, 0); /* AFTER name1, name2 */ - if (type_name) { - type = fr_table_value_by_str(fr_type_table, type_name, FR_TYPE_NULL); - - /* - * Should have been caught in cf_file.c, process_switch() - */ - fr_assert(type != FR_TYPE_NULL); - fr_assert(fr_type_is_leaf(type)); - - do_cast: - if (tmpl_cast_set(gext->vpt, type) < 0) { - cf_log_perr(cs, "Failed setting cast type"); - goto error; - } - - } else { - /* - * Get the return type of the tmpl. If we don't know, - * mash it all to string. - */ - type = tmpl_data_type(gext->vpt); - if ((type == FR_TYPE_NULL) || (type == FR_TYPE_VOID)) { - type = FR_TYPE_STRING; - goto do_cast; - } - } - - htype = fr_htrie_hint(type); - if (htype == FR_HTRIE_INVALID) { - cf_log_err(cs, "Invalid data type '%s' used for 'switch' statement", - fr_type_to_str(type)); - goto error; - } - - gext->ht = fr_htrie_alloc(gext, htype, - (fr_hash_t) case_hash, - (fr_cmp_t) case_cmp, - (fr_trie_key_t) case_to_key, - NULL); - if (!gext->ht) { - cf_log_err(cs, "Failed initializing internal data structures"); - talloc_free(g); - return NULL; - } - - /* - * Walk through the children of the switch section, - * ensuring that they're all 'case' statements, and then compiling them. - */ - for (subci = cf_item_next(cs, NULL); - subci != NULL; - subci = cf_item_next(cs, subci)) { - CONF_SECTION *subcs; - unlang_t *single; - unlang_case_t *case_gext; - - if (!cf_item_is_section(subci)) { - if (!cf_item_is_pair(subci)) continue; - - cf_log_err(subci, "\"switch\" sections can only have \"case\" subsections"); - goto error; - } - - subcs = cf_item_to_section(subci); /* can't return NULL */ - name1 = cf_section_name1(subcs); - - if (strcmp(name1, "case") != 0) { - /* - * We finally support "default" sections for "switch". - */ - if (strcmp(name1, "default") == 0) { - if (cf_section_name2(subcs) != 0) { - cf_log_err(subci, "\"default\" sections cannot have a match argument"); - goto error; - } - goto handle_default; - } - - cf_log_err(subci, "\"switch\" sections can only have \"case\" subsections"); - goto error; - } - - name2 = cf_section_name2(subcs); - if (!name2) { - handle_default: - if (gext->default_case) { - cf_log_err(subci, "Cannot have two 'default' case statements"); - goto error; - } - } - - /* - * Compile the subsection. - */ - single = compile_case(c, unlang_ctx, subci); - if (!single) goto error; - - fr_assert(single->type == UNLANG_TYPE_CASE); - - /* - * Remember the "default" section, and insert the - * non-default "case" into the htrie. - */ - case_gext = unlang_group_to_case(unlang_generic_to_group(single)); - if (!case_gext->vpt) { - gext->default_case = single; - - } else if (!fr_htrie_insert(gext->ht, single)) { - single = fr_htrie_find(gext->ht, single); - - /* - * @todo - look up the key and get the previous one? - */ - cf_log_err(subci, "Failed inserting 'case' statement. Is there a duplicate?"); - - if (single) cf_log_err(unlang_generic_to_group(single)->cs, "Duplicate may be here."); - - goto error; - } - - *g->tail = single; - g->tail = &single->next; - g->num_children++; - } - - compile_action_defaults(c, unlang_ctx); - - return c; -} - -static unlang_t *compile_case(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci) -{ - CONF_SECTION *cs = cf_item_to_section(ci); - int i; - char const *name2; - unlang_t *c; - unlang_group_t *case_g; - unlang_case_t *case_gext; - tmpl_t *vpt = NULL; - tmpl_rules_t t_rules; - - /* - * We allow unknown attributes here. - */ - t_rules = *(unlang_ctx->rules); - t_rules.attr.allow_unknown = true; - RULES_VERIFY(&t_rules); - - if (!parent || (parent->type != UNLANG_TYPE_SWITCH)) { - cf_log_err(cs, "\"case\" statements may only appear within a \"switch\" section"); - cf_log_err(ci, DOC_KEYWORD_REF(case)); - return NULL; - } - - /* - * case THING means "match THING" - * case means "match anything" - */ - name2 = cf_section_name2(cs); - if (name2) { - ssize_t slen; - fr_token_t quote; - unlang_group_t *switch_g; - unlang_switch_t *switch_gext; - - switch_g = unlang_generic_to_group(parent); - switch_gext = unlang_group_to_switch(switch_g); - - fr_assert(switch_gext->vpt != NULL); - - /* - * We need to cast case values to match - * what we're switching over, otherwise - * integers of different widths won't - * match. - */ - t_rules.cast = tmpl_expanded_type(switch_gext->vpt); - - /* - * Need to pass the attribute from switch - * to tmpl rules so we can convert the - * case string to an integer value. - */ - if (tmpl_is_attr(switch_gext->vpt)) { - fr_dict_attr_t const *da = tmpl_attr_tail_da(switch_gext->vpt); - if (da->flags.has_value) t_rules.enumv = da; - } - - quote = cf_section_name2_quote(cs); - - slen = tmpl_afrom_substr(cs, &vpt, - &FR_SBUFF_IN(name2, strlen(name2)), - quote, - NULL, - &t_rules); - if (!vpt) { - cf_canonicalize_error(cs, slen, "Failed parsing argument to 'case'", name2); - return NULL; - } - - /* - * Bare word strings are attribute references - */ - if (tmpl_is_attr(vpt) || tmpl_is_attr_unresolved(vpt)) { - fail_attr: - cf_log_err(cs, "arguments to 'case' statements MUST NOT be attribute references."); - goto fail; - } - - if (!tmpl_is_data(vpt) || tmpl_is_data_unresolved(vpt)) { - cf_log_err(cs, "arguments to 'case' statements MUST be static data."); - fail: - talloc_free(vpt); - return NULL; - } - - /* - * References to unresolved attributes are forbidden. They are no longer "bare word - * strings". - */ - if ((quote == T_BARE_WORD) && (tmpl_value_type(vpt) == FR_TYPE_STRING)) { - goto fail_attr; - } - - } /* else it's a default 'case' statement */ - - /* - * If we were asked to match something, then we MUST - * match it, even if the section is empty. Otherwise we - * will silently skip the match, and then fall through to - * the "default" statement. - */ - c = compile_section(parent, unlang_ctx, cs, UNLANG_TYPE_CASE); - if (!c) { - talloc_free(vpt); - return NULL; - } - - case_g = unlang_generic_to_group(c); - case_gext = unlang_group_to_case(case_g); - case_gext->vpt = talloc_steal(case_gext, vpt); - - /* - * Set all of it's codes to return, so that - * when we pick a 'case' statement, we don't - * fall through to processing the next one. - */ - for (i = 0; i < RLM_MODULE_NUMCODES; i++) c->actions.actions[i] = MOD_ACTION_RETURN; - - return c; -} - -static unlang_t *compile_timeout(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci) -{ - CONF_SECTION *cs = cf_item_to_section(ci); - char const *name2; - unlang_t *c; - unlang_group_t *g; - unlang_timeout_t *gext; - fr_time_delta_t timeout = fr_time_delta_from_sec(0); - tmpl_t *vpt = NULL; - fr_token_t token; - - /* - * Timeout