diff options
Diffstat (limited to '')
-rw-r--r-- | src/main/map.c | 1712 |
1 files changed, 1712 insertions, 0 deletions
diff --git a/src/main/map.c b/src/main/map.c new file mode 100644 index 0000000..17988d2 --- /dev/null +++ b/src/main/map.c @@ -0,0 +1,1712 @@ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/* + * $Id$ + * + * @brief map / template functions + * @file main/map.c + * + * @ingroup AVP + * + * @copyright 2013 The FreeRADIUS server project + * @copyright 2013 Alan DeKok <aland@freeradius.org> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/rad_assert.h> + +#include <ctype.h> + +#ifdef DEBUG_MAP +static void map_dump(REQUEST *request, vp_map_t const *map) +{ + RDEBUG(">>> MAP TYPES LHS: %s, RHS: %s", + fr_int2str(tmpl_names, map->lhs->type, "???"), + fr_int2str(tmpl_names, map->rhs->type, "???")); + + if (map->rhs) { + RDEBUG(">>> MAP NAMES %s %s", map->lhs->name, map->rhs->name); + } +} +#endif + + +/** re-parse a map where the lhs is an unknown attribute. + * + * + * @param map to process. + * @param rhs_type quotation type around rhs. + * @param rhs string to re-parse. + */ +bool map_cast_from_hex(vp_map_t *map, FR_TOKEN rhs_type, char const *rhs) +{ + size_t len; + ssize_t rlen; + uint8_t *ptr; + char const *p; + pair_lists_t list; + + DICT_ATTR const *da; + VALUE_PAIR *vp; + vp_tmpl_t *vpt; + + rad_assert(map != NULL); + + rad_assert(map->lhs != NULL); + rad_assert(map->lhs->type == TMPL_TYPE_ATTR); + + rad_assert(map->rhs == NULL); + rad_assert(rhs != NULL); + + VERIFY_MAP(map); + + /* + * If the attribute is still unknown, go parse the RHS. + */ + da = dict_attrbyvalue(map->lhs->tmpl_da->attr, map->lhs->tmpl_da->vendor); + if (!da || da->flags.is_unknown) return false; + + /* + * If the RHS is something OTHER than an octet + * string, go parse it as that. + */ + if (rhs_type != T_BARE_WORD) return false; + if ((rhs[0] != '0') || (tolower((int)rhs[1]) != 'x')) return false; + if (!rhs[2]) return false; + + len = strlen(rhs + 2); + + ptr = talloc_array(map, uint8_t, len >> 1); + if (!ptr) return false; + + len = fr_hex2bin(ptr, len >> 1, rhs + 2, len); + + /* + * If we can't parse it, or if it's malformed, + * it's still unknown. + */ + rlen = data2vp(NULL, NULL, NULL, NULL, da, ptr, len, len, &vp); + talloc_free(ptr); + + if (rlen < 0) return false; + + if ((size_t) rlen < len) { + free_vp: + fr_pair_list_free(&vp); + return false; + } + + /* + * Was still parsed as an unknown attribute. + */ + if (vp->da->flags.is_unknown) goto free_vp; + + /* + * Set the RHS to the PARSED name, not the crap octet + * string which was input. + */ + map->rhs = tmpl_alloc(map, TMPL_TYPE_DATA, NULL, 0); + if (!map->rhs) goto free_vp; + + map->rhs->tmpl_data_type = da->type; + map->rhs->tmpl_data_length = vp->vp_length; + if (vp->da->flags.is_pointer) { + if (vp->da->type == PW_TYPE_STRING) { + map->rhs->tmpl_data_value.ptr = talloc_bstrndup(map->rhs, vp->data.ptr, vp->vp_length); + } else { + map->rhs->tmpl_data_value.ptr = talloc_memdup(map->rhs, vp->data.ptr, vp->vp_length); + } + } else { + memcpy(&map->rhs->tmpl_data_value, &vp->data, sizeof(map->rhs->tmpl_data_value)); + } + map->rhs->name = vp_aprints_value(map->rhs, vp, '"'); + map->rhs->len = talloc_array_length(map->rhs->name) - 1; + + /* + * Set the LHS to the REAL attribute name. + */ + vpt = tmpl_alloc(map, TMPL_TYPE_ATTR, map->lhs->tmpl_da->name, -1); + memcpy(&vpt->data.attribute, &map->lhs->data.attribute, sizeof(vpt->data.attribute)); + vpt->tmpl_da = da; + + /* + * Be sure to keep the "&control:" or "control:" prefix. + * If it's there, we re-generate it from whatever was in + * the original name, including the '&'. + */ + p = map->lhs->name; + if (*p == '&') p++; + len = radius_list_name(&list, p, PAIR_LIST_UNKNOWN); + + if (list != PAIR_LIST_UNKNOWN) { + rad_const_free(vpt->name); + + vpt->name = talloc_asprintf(vpt, "%.*s:%s", + (int) len, map->lhs->name, + map->lhs->tmpl_da->name); + vpt->len = strlen(vpt->name); + } + + talloc_free(map->lhs); + map->lhs = vpt; + + fr_pair_list_free(&vp); + + VERIFY_MAP(map); + + return true; +} + +/** Convert CONFIG_PAIR (which may contain refs) to vp_map_t. + * + * Treats the left operand as an attribute reference + * @verbatim<request>.<list>.<attribute>@endverbatim + * + * Treatment of left operand depends on quotation, barewords are treated as + * attribute references, double quoted values are treated as expandable strings, + * single quoted values are treated as literal strings. + * + * Return must be freed with talloc_free + * + * @param[in] ctx for talloc. + * @param[in] out Where to write the pointer to the new value_pair_map_struct. + * @param[in] cp to convert to map. + * @param[in] dst_request_def The default request to insert unqualified + * attributes into. + * @param[in] dst_list_def The default list to insert unqualified attributes + * into. + * @param[in] src_request_def The default request to resolve attribute + * references in. + * @param[in] src_list_def The default list to resolve unqualified attributes + * in. + * @return vp_map_t if successful or NULL on error. + */ +int map_afrom_cp(TALLOC_CTX *ctx, vp_map_t **out, CONF_PAIR *cp, + request_refs_t dst_request_def, pair_lists_t dst_list_def, + request_refs_t src_request_def, pair_lists_t src_list_def) +{ + vp_map_t *map; + char const *attr, *value; + ssize_t slen; + FR_TOKEN type; + + *out = NULL; + + if (!cp) return -1; + + map = talloc_zero(ctx, vp_map_t); + map->op = cf_pair_operator(cp); + map->ci = cf_pair_to_item(cp); + + attr = cf_pair_attr(cp); + value = cf_pair_value(cp); + if (!value) { + cf_log_err_cp(cp, "Missing attribute value"); + goto error; + } + + /* + * LHS may be an expansion (that expands to an attribute reference) + * or an attribute reference. Quoting determines which it is. + */ + type = cf_pair_attr_type(cp); + switch (type) { + case T_DOUBLE_QUOTED_STRING: + case T_BACK_QUOTED_STRING: + slen = tmpl_afrom_str(ctx, &map->lhs, attr, talloc_array_length(attr) - 1, + type, dst_request_def, dst_list_def, true); + if (slen <= 0) { + char *spaces, *text; + + marker: + fr_canonicalize_error(ctx, &spaces, &text, slen, attr); + cf_log_err_cp(cp, "%s", text); + cf_log_err_cp(cp, "%s^ %s", spaces, fr_strerror()); + + talloc_free(spaces); + talloc_free(text); + goto error; + } + break; + + case T_BARE_WORD: + /* + * Foo = %{...} + * + * Not allowed! + */ + if ((attr[0] == '%') && (attr[1] == '{')) { + cf_log_err_cp(cp, "Bare expansions are not permitted. They must be in a double-quoted string."); + goto error; + } + /* FALL-THROUGH */ + + default: + slen = tmpl_afrom_attr_str(ctx, &map->lhs, attr, dst_request_def, dst_list_def, true, true); + if (slen <= 0) { + cf_log_err_cp(cp, "Failed parsing attribute reference"); + + goto marker; + } + + if (tmpl_define_unknown_attr(map->lhs) < 0) { + cf_log_err_cp(cp, "Failed creating attribute %s: %s", + map->lhs->name, fr_strerror()); + goto error; + } + + break; + } + + /* + * RHS might be an attribute reference. + */ + type = cf_pair_value_type(cp); + + if ((map->lhs->type == TMPL_TYPE_ATTR) && + map->lhs->tmpl_da->flags.is_unknown && + !map_cast_from_hex(map, type, value)) { + goto error; + + } else { + slen = tmpl_afrom_str(map, &map->rhs, value, strlen(value), type, src_request_def, src_list_def, true); + if (slen < 0) goto marker; + if (tmpl_define_unknown_attr(map->rhs) < 0) { + cf_log_err_cp(cp, "Failed creating attribute %s: %s", map->rhs->name, fr_strerror()); + goto error; + } + } + if (!map->rhs) { + cf_log_err_cp(cp, "%s", fr_strerror()); + goto error; + } + + if (map->rhs->type == TMPL_TYPE_ATTR) { + /* + * We cannot assign a count to an attribute. That must + * be done in an xlat. + */ + if (map->rhs->tmpl_num == NUM_COUNT) { + cf_log_err_cp(cp, "Cannot assign from a count"); + goto error; + } + + if (map->rhs->tmpl_da->flags.virtual) { + cf_log_err_cp(cp, "Virtual attributes must be in an expansion such as \"%%{%s}\".", map->rhs->tmpl_da->name); + goto error; + } + } + + VERIFY_MAP(map); + + *out = map; + + return 0; + +error: + talloc_free(map); + return -1; +} + +/** Convert an 'update' config section into an attribute map. + * + * Uses 'name2' of section to set default request and lists. + * + * @param[in] cs the update section + * @param[out] out Where to store the head of the map. + * @param[in] dst_list_def The default destination list, usually dictated by + * the section the module is being called in. + * @param[in] src_list_def The default source list, usually dictated by the + * section the module is being called in. + * @param[in] validate map using this callback (may be NULL). + * @param[in] ctx to pass to callback. + * @param[in] max number of mappings to process. + * @return -1 on error, else 0. + */ +int map_afrom_cs(vp_map_t **out, CONF_SECTION *cs, + pair_lists_t dst_list_def, pair_lists_t src_list_def, + map_validate_t validate, void *ctx, + unsigned int max) +{ + char const *cs_list, *p; + + request_refs_t request_def = REQUEST_CURRENT; + + CONF_ITEM *ci; + CONF_PAIR *cp; + + unsigned int total = 0; + vp_map_t **tail, *map; + TALLOC_CTX *parent; + + *out = NULL; + tail = out; + + /* + * The first map has cs as the parent. + * The rest have the previous map as the parent. + */ + parent = cs; + + ci = cf_section_to_item(cs); + + cs_list = p = cf_section_name2(cs); + if (cs_list) { + p += radius_request_name(&request_def, p, REQUEST_CURRENT); + if (request_def == REQUEST_UNKNOWN) { + cf_log_err(ci, "Default request specified in mapping section is invalid"); + return -1; + } + + dst_list_def = fr_str2int(pair_lists, p, PAIR_LIST_UNKNOWN); + if (dst_list_def == PAIR_LIST_UNKNOWN) { + cf_log_err(ci, "Default list \"%s\" specified " + "in mapping section is invalid", p); + return -1; + } + } + + for (ci = cf_item_find_next(cs, NULL); + ci != NULL; + ci = cf_item_find_next(cs, ci)) { + if (total++ == max) { + cf_log_err(ci, "Map size exceeded"); + error: + TALLOC_FREE(*out); + return -1; + } + + if (!cf_item_is_pair(ci)) { + cf_log_err(ci, "Entry is not in \"attribute = value\" format"); + goto error; + } + + cp = cf_item_to_pair(ci); + if (map_afrom_cp(parent, &map, cp, request_def, dst_list_def, REQUEST_CURRENT, src_list_def) < 0) { + goto error; + } + + VERIFY_MAP(map); + + /* + * Check the types in the map are valid + */ + if (validate && (validate(map, ctx) < 0)) goto error; + + parent = *tail = map; + tail = &(map->next); + } + + return 0; + +} + + +/** Convert strings to vp_map_t + * + * Treatment of operands depends on quotation, barewords are treated + * as attribute references, double quoted values are treated as + * expandable strings, single quoted values are treated as literal + * strings. + * + * Return must be freed with talloc_free + * + * @param[in] ctx for talloc + * @param[out] out Where to store the head of the map. + * @param[in] lhs of the operation + * @param[in] lhs_type type of the LHS string + * @param[in] op the operation to perform + * @param[in] rhs of the operation + * @param[in] rhs_type type of the RHS string + * @param[in] dst_request_def The default request to insert unqualified + * attributes into. + * @param[in] dst_list_def The default list to insert unqualified attributes + * into. + * @param[in] src_request_def The default request to resolve attribute + * references in. + * @param[in] src_list_def The default list to resolve unqualified attributes + * in. + * @return vp_map_t if successful or NULL on error. + */ +int map_afrom_fields(TALLOC_CTX *ctx, vp_map_t **out, char const *lhs, FR_TOKEN lhs_type, + FR_TOKEN op, char const *rhs, FR_TOKEN rhs_type, + request_refs_t dst_request_def, + pair_lists_t dst_list_def, + request_refs_t src_request_def, + pair_lists_t src_list_def) +{ + ssize_t slen; + vp_map_t *map; + + map = talloc_zero(ctx, vp_map_t); + + slen = tmpl_afrom_str(map, &map->lhs, lhs, strlen(lhs), lhs_type, dst_request_def, dst_list_def, true); + if (slen < 0) { + error: + talloc_free(map); + return -1; + } + + map->op = op; + + if ((map->lhs->type == TMPL_TYPE_ATTR) && + map->lhs->tmpl_da->flags.is_unknown && + map_cast_from_hex(map, rhs_type, rhs)) { + return 0; + } + + slen = tmpl_afrom_str(map, &map->rhs, rhs, strlen(rhs), rhs_type, src_request_def, src_list_def, true); + if (slen < 0) goto error; + + VERIFY_MAP(map); + + *out = map; + + return 0; +} + +/** Convert a value pair string to valuepair map + * + * Takes a valuepair string with list and request qualifiers and converts it into a + * vp_map_t. + * + * @param ctx where to allocate the map. + * @param out Where to write the new map (must be freed with talloc_free()). + * @param vp_str string to parse. + * @param dst_request_def to use if attribute isn't qualified. + * @param dst_list_def to use if attribute isn't qualified. + * @param src_request_def to use if attribute isn't qualified. + * @param src_list_def to use if attribute isn't qualified. + * @return 0 on success, < 0 on error. + */ +int map_afrom_attr_str(TALLOC_CTX *ctx, vp_map_t **out, char const *vp_str, + request_refs_t dst_request_def, pair_lists_t dst_list_def, + request_refs_t src_request_def, pair_lists_t src_list_def) +{ + char const *p = vp_str; + FR_TOKEN quote; + + VALUE_PAIR_RAW raw; + vp_map_t *map = NULL; + + quote = gettoken(&p, raw.l_opand, sizeof(raw.l_opand), false); + switch (quote) { + case T_BARE_WORD: + break; + + case T_INVALID: + error: + return -1; + + default: + fr_strerror_printf("Left operand must be an attribute"); + return -1; + } + + raw.op = getop(&p); + if (raw.op == T_INVALID) goto error; + + raw.quote = gettoken(&p, raw.r_opand, sizeof(raw.r_opand), false); + if (raw.quote == T_INVALID) goto error; + if (!fr_str_tok[raw.quote]) { + fr_strerror_printf("Right operand must be an attribute or string"); + return -1; + } + + if (map_afrom_fields(ctx, &map, raw.l_opand, T_BARE_WORD, raw.op, raw.r_opand, raw.quote, + dst_request_def, dst_list_def, src_request_def, src_list_def) < 0) { + return -1; + } + + rad_assert(map != NULL); + *out = map; + + VERIFY_MAP(map); + + return 0; +} + +/** Compare map where LHS is #TMPL_TYPE_ATTR + * + * Compares maps by lhs->tmpl_da, lhs->tmpl_tag, lhs->tmpl_num + * + * @note both map->lhs must be #TMPL_TYPE_ATTR. + * + * @param a first map. + * @param b second map. + */ +int8_t map_cmp_by_lhs_attr(void const *a, void const *b) +{ + vp_tmpl_t const *my_a = ((vp_map_t const *)a)->lhs; + vp_tmpl_t const *my_b = ((vp_map_t const *)b)->lhs; + + VERIFY_TMPL(my_a); + VERIFY_TMPL(my_b); + + uint8_t cmp; + + rad_assert(my_a->type == TMPL_TYPE_ATTR); + rad_assert(my_b->type == TMPL_TYPE_ATTR); + + cmp = fr_pointer_cmp(my_a->tmpl_da, my_b->tmpl_da); + if (cmp != 0) return cmp; + + if (my_a->tmpl_tag < my_b->tmpl_tag) return -1; + + if (my_a->tmpl_tag > my_b->tmpl_tag) return 1; + + if (my_a->tmpl_num < my_b->tmpl_num) return -1; + + if (my_a->tmpl_num > my_b->tmpl_num) return 1; + + return 0; +} + +static void map_sort_split(vp_map_t *source, vp_map_t **front, vp_map_t **back) +{ + vp_map_t *fast; + vp_map_t *slow; + + /* + * Stopping condition - no more elements left to split + */ + if (!source || !source->next) { + *front = source; + *back = NULL; + + return; + } + + /* + * Fast advances twice as fast as slow, so when it gets to the end, + * slow will point to the middle of the linked list. + */ + slow = source; + fast = source->next; + + while (fast) { + fast = fast->next; + if (fast) { + slow = slow->next; + fast = fast->next; + } + } + + *front = source; + *back = slow->next; + slow->next = NULL; +} + +static vp_map_t *map_sort_merge(vp_map_t *a, vp_map_t *b, fr_cmp_t cmp) +{ + vp_map_t *result = NULL; + + if (!a) return b; + if (!b) return a; + + /* + * Compare things in the maps + */ + if (cmp(a, b) <= 0) { + result = a; + result->next = map_sort_merge(a->next, b, cmp); + } else { + result = b; + result->next = map_sort_merge(a, b->next, cmp); + } + + return result; +} + +/** Sort a linked list of #vp_map_t using merge sort + * + * @param[in,out] maps List of #vp_map_t to sort. + * @param[in] cmp to sort with + */ +void map_sort(vp_map_t **maps, fr_cmp_t cmp) +{ + vp_map_t *head = *maps; + vp_map_t *a; + vp_map_t *b; + + /* + * If there's 0-1 elements it must already be sorted. + */ + if (!head || !head->next) { + return; + } + + map_sort_split(head, &a, &b); /* Split into sublists */ + map_sort(&a, cmp); /* Traverse left */ + map_sort(&b, cmp); /* Traverse right */ + + /* + * merge the two sorted lists together + */ + *maps = map_sort_merge(a, b, cmp); +} + +/** Process map which has exec as a src + * + * Evaluate maps which specify exec as a src. This may be used by various sorts of update sections, + * and so has been broken out into it's own function. + * + * @param[in,out] ctx to allocate new #VALUE_PAIR (s) in. + * @param[out] out Where to write the #VALUE_PAIR (s). + * @param[in] request structure (used only for talloc). + * @param[in] map the map. The LHS (dst) must be TMPL_TYPE_ATTR or TMPL_TYPE_LIST. The RHS (src) + * must be TMPL_TYPE_EXEC. + * @return -1 on failure, 0 on success. + */ +static int map_exec_to_vp(TALLOC_CTX *ctx, VALUE_PAIR **out, REQUEST *request, vp_map_t const *map) +{ + int result; + char *expanded = NULL; + char answer[1024]; + VALUE_PAIR **input_pairs = NULL; + VALUE_PAIR *output_pairs = NULL; + + *out = NULL; + + VERIFY_MAP(map); + + rad_assert(map->rhs->type == TMPL_TYPE_EXEC); + rad_assert((map->lhs->type == TMPL_TYPE_ATTR) || (map->lhs->type == TMPL_TYPE_LIST)); + + /* + * We always put the request pairs into the environment + */ + input_pairs = radius_list(request, PAIR_LIST_REQUEST); + + /* + * Automagically switch output type depending on our destination + * If dst is a list, then we create attributes from the output of the program + * if dst is an attribute, then we create an attribute of that type and then + * call fr_pair_value_from_str on the output of the script. + */ + result = radius_exec_program(ctx, answer, sizeof(answer), + (map->lhs->type == TMPL_TYPE_LIST) ? &output_pairs : NULL, + request, map->rhs->name, input_pairs ? *input_pairs : NULL, + true, true, EXEC_TIMEOUT); + talloc_free(expanded); + if (result != 0) { + talloc_free(output_pairs); + return -1; + } + + switch (map->lhs->type) { + case TMPL_TYPE_LIST: + if (!output_pairs) { + REDEBUG("No valid attributes received from program"); + return -2; + } + *out = output_pairs; + return 0; + + case TMPL_TYPE_ATTR: + { + VALUE_PAIR *vp; + + vp = fr_pair_afrom_da(ctx, map->lhs->tmpl_da); + if (!vp) return -1; + vp->op = map->op; + vp->tag = map->lhs->tmpl_tag; + if (fr_pair_value_from_str(vp, answer, -1) < 0) { + fr_pair_list_free(&vp); + return -2; + } + *out = vp; + + return 0; + } + + default: + rad_assert(0); + } + + return -1; +} + +/** Convert a map to a VALUE_PAIR. + * + * @param[in,out] ctx to allocate #VALUE_PAIR (s) in. + * @param[out] out Where to write the #VALUE_PAIR (s), which may be NULL if not found + * @param[in] request The current request. + * @param[in] map the map. The LHS (dst) has to be #TMPL_TYPE_ATTR or #TMPL_TYPE_LIST. + * @param[in] uctx unused. + * @return + * - 0 on success. + * - -1 on failure. + */ +int map_to_vp(TALLOC_CTX *ctx, VALUE_PAIR **out, REQUEST *request, vp_map_t const *map, UNUSED void *uctx) +{ + int rcode = 0; + ssize_t len; + VALUE_PAIR *vp = NULL, *new, *found = NULL; + REQUEST *context = request; + vp_cursor_t cursor; + ssize_t slen; + char *str; + + *out = NULL; + + VERIFY_MAP(map); + rad_assert(map->lhs != NULL); + rad_assert(map->rhs != NULL); + + rad_assert((map->lhs->type == TMPL_TYPE_LIST) || (map->lhs->type == TMPL_TYPE_ATTR)); + + /* + * Special case for !*, we don't need to parse RHS as this is a unary operator. + */ + if (map->op == T_OP_CMP_FALSE) return 0; + + /* + * List to list found, this is a special case because we don't need + * to allocate any attributes, just finding the current list, and change + * the op. + */ + if ((map->lhs->type == TMPL_TYPE_LIST) && (map->rhs->type == TMPL_TYPE_LIST)) { + VALUE_PAIR **from = NULL; + + if (radius_request(&context, map->rhs->tmpl_request) == 0) { + from = radius_list(context, map->rhs->tmpl_list); + } + if (!from) return 0; + + found = fr_pair_list_copy(ctx, *from); + + /* + * List to list copy is empty if the src list has no attributes. + */ + if (!found) return 0; + + for (vp = fr_cursor_init(&cursor, &found); + vp; + vp = fr_cursor_next(&cursor)) { + vp->op = T_OP_ADD; + } + + *out = found; + + return 0; + } + + /* + * And parse the RHS + */ + switch (map->rhs->type) { + case TMPL_TYPE_XLAT_STRUCT: + rad_assert(map->lhs->type == TMPL_TYPE_ATTR); + rad_assert(map->lhs->tmpl_da); /* We need to know which attribute to create */ + rad_assert(map->rhs->tmpl_xlat != NULL); + + new = fr_pair_afrom_da(ctx, map->lhs->tmpl_da); + if (!new) return -1; + + str = NULL; + slen = radius_axlat_struct(&str, request, map->rhs->tmpl_xlat, NULL, NULL); + if (slen < 0) { + rcode = slen; + goto error; + } + + /* + * We do the debug printing because radius_axlat_struct + * doesn't have access to the original string. It's been + * mangled during the parsing to xlat_exp_t + */ + RDEBUG2("EXPAND %s", map->rhs->name); + RDEBUG2(" --> %s", str); + + rcode = fr_pair_value_from_str(new, str, -1); + talloc_free(str); + if (rcode < 0) { + fr_pair_list_free(&new); + goto error; + } + new->op = map->op; + new->tag = map->lhs->tmpl_tag; + *out = new; + break; + + case TMPL_TYPE_XLAT: + rad_assert(map->lhs->type == TMPL_TYPE_ATTR); + rad_assert(map->lhs->tmpl_da); /* We need to know which attribute to create */ + + new = fr_pair_afrom_da(ctx, map->lhs->tmpl_da); + if (!new) return -1; + + str = NULL; + slen = radius_axlat(&str, request, map->rhs->name, NULL, NULL); + if (slen < 0) { + rcode = slen; + goto error; + } + + rcode = fr_pair_value_from_str(new, str, -1); + talloc_free(str); + if (rcode < 0) { + fr_pair_list_free(&new); + goto error; + } + new->op = map->op; + new->tag = map->lhs->tmpl_tag; + *out = new; + break; + + case TMPL_TYPE_LITERAL: + rad_assert(map->lhs->type == TMPL_TYPE_ATTR); + rad_assert(map->lhs->tmpl_da); /* We need to know which attribute to create */ + + new = fr_pair_afrom_da(ctx, map->lhs->tmpl_da); + if (!new) return -1; + + if (fr_pair_value_from_str(new, map->rhs->name, -1) < 0) { + rcode = 0; + goto error; + } + new->op = map->op; + new->tag = map->lhs->tmpl_tag; + *out = new; + break; + + case TMPL_TYPE_ATTR: + { + vp_cursor_t from; + + rad_assert(((map->lhs->type == TMPL_TYPE_ATTR) && map->lhs->tmpl_da) || + ((map->lhs->type == TMPL_TYPE_LIST) && !map->lhs->tmpl_da)); + + /* + * @todo should log error, and return -1 for v3.1 (causes update to fail) + */ + if (tmpl_copy_vps(ctx, &found, request, map->rhs) < 0) return 0; + + vp = fr_cursor_init(&from, &found); + + /* + * Src/Dst attributes don't match, convert src attributes + * to match dst. + */ + if ((map->lhs->type == TMPL_TYPE_ATTR) && + (map->rhs->tmpl_da->type != map->lhs->tmpl_da->type)) { + vp_cursor_t to; + + (void) fr_cursor_init(&to, out); + for (; vp; vp = fr_cursor_next(&from)) { + new = fr_pair_afrom_da(ctx, map->lhs->tmpl_da); + if (!new) return -1; + + len = value_data_cast(new, &new->data, new->da->type, new->da, + vp->da->type, vp->da, &vp->data, vp->vp_length); + if (len < 0) { + REDEBUG("Attribute conversion failed: %s", fr_strerror()); + fr_pair_list_free(&found); + fr_pair_list_free(&new); + return -1; + } + + new->vp_length = len; + vp = fr_cursor_remove(&from); + talloc_free(vp); + + if (new->da->type == PW_TYPE_STRING) { + rad_assert(new->vp_strvalue != NULL); + } + + new->op = map->op; + new->tag = map->lhs->tmpl_tag; + fr_cursor_insert(&to, new); + } + return 0; + } + + /* + * Otherwise we just need to fixup the attribute types + * and operators + */ + for (; vp; vp = fr_cursor_next(&from)) { + vp->da = map->lhs->tmpl_da; + vp->op = map->op; + vp->tag = map->lhs->tmpl_tag; + } + *out = found; + } + break; + + case TMPL_TYPE_DATA: + rad_assert(map->lhs->tmpl_da); + rad_assert(map->lhs->type == TMPL_TYPE_ATTR); + rad_assert(map->lhs->tmpl_da->type == map->rhs->tmpl_data_type); + + new = fr_pair_afrom_da(ctx, map->lhs->tmpl_da); + if (!new) return -1; + + len = value_data_copy(new, &new->data, new->da->type, &map->rhs->tmpl_data_value, + map->rhs->tmpl_data_length); + if (len < 0) goto error; + + new->vp_length = len; + new->op = map->op; + new->tag = map->lhs->tmpl_tag; + *out = new; + + VERIFY_MAP(map); + break; + + /* + * This essentially does the same as rlm_exec xlat, except it's non-configurable. + * It's only really here as a convenience for people who expect the contents of + * backticks to be executed in a shell. + * + * exec string is xlat expanded and arguments are shell escaped. + */ + case TMPL_TYPE_EXEC: + return map_exec_to_vp(ctx, out, request, map); + + default: + rad_assert(0); /* Should have been caught at parse time */ + + error: + fr_pair_list_free(&vp); + return rcode; + } + + return 0; +} + +#define DEBUG_OVERWRITE(_old, _new) \ +do {\ + if (RDEBUG_ENABLED3) {\ + char *old = vp_aprints_value(request, _old, '"');\ + char *new = vp_aprints_value(request, _new, '"');\ + RDEBUG3("Overwriting value \"%s\" with \"%s\"", old, new);\ + talloc_free(old);\ + talloc_free(new);\ + }\ +} while (0) + +/** Convert vp_map_t to VALUE_PAIR(s) and add them to a REQUEST. + * + * Takes a single vp_map_t, resolves request and list identifiers + * to pointers in the current request, then attempts to retrieve module + * specific value(s) using callback, and adds the resulting values to the + * correct request/list. + * + * @param request The current request. + * @param map specifying destination attribute and location and src identifier. + * @param func to retrieve module specific values and convert them to + * VALUE_PAIRS. + * @param ctx to be passed to func. + * @return -1 if the operation failed, -2 in the source attribute wasn't valid, 0 on success. + */ +int map_to_request(REQUEST *request, vp_map_t const *map, radius_map_getvalue_t func, void *ctx) +{ + int rcode = 0; + int num; + VALUE_PAIR **list, *vp, *dst, *head = NULL; + bool found = false; + REQUEST *context; + TALLOC_CTX *parent; + vp_cursor_t dst_list, src_list; + + vp_map_t exp_map; + vp_tmpl_t exp_lhs; + + VERIFY_MAP(map); + rad_assert(map->lhs != NULL); + rad_assert(map->rhs != NULL); + + /* + * Preprocessing of the LHS of the map. + */ + switch (map->lhs->type) { + /* + * Already in the correct form. + */ + case TMPL_TYPE_LIST: + case TMPL_TYPE_ATTR: + break; + + /* + * Everything else gets expanded, then re-parsed as an + * attribute reference. + */ + case TMPL_TYPE_XLAT: + case TMPL_TYPE_XLAT_STRUCT: + case TMPL_TYPE_EXEC: + { + char *attr; + ssize_t slen; + + slen = tmpl_aexpand(request, &attr, request, map->lhs, NULL, NULL); + if (slen <= 0) { + REDEBUG("Left side \"%.*s\" of map failed expansion", (int)map->lhs->len, map->lhs->name); + rad_assert(!attr); + return -1; + } + + slen = tmpl_from_attr_str(&exp_lhs, attr, REQUEST_CURRENT, PAIR_LIST_REQUEST, false, false) ; + if (slen <= 0) { + REDEBUG("Left side \"%.*s\" expansion not an attribute reference: %s", + (int)map->lhs->len, map->lhs->name, fr_strerror()); + talloc_free(attr); + return -1; + } + rad_assert((exp_lhs.type == TMPL_TYPE_ATTR) || (exp_lhs.type == TMPL_TYPE_LIST)); + + memcpy(&exp_map, map, sizeof(exp_map)); + exp_map.lhs = &exp_lhs; + map = &exp_map; + } + break; + + default: + rad_assert(0); + break; + } + + + /* + * Sanity check inputs. We can have a list or attribute + * as a destination. + */ + if ((map->lhs->type != TMPL_TYPE_LIST) && + (map->lhs->type != TMPL_TYPE_ATTR)) { + REDEBUG("Left side \"%.*s\" of map should be an attr or list but is an %s", + (int)map->lhs->len, map->lhs->name, + fr_int2str(tmpl_names, map->lhs->type, "<INVALID>")); + return -2; + } + + context = request; + if (radius_request(&context, map->lhs->tmpl_request) < 0) { + REDEBUG("Mapping \"%.*s\" -> \"%.*s\" invalid in this context", + (int)map->rhs->len, map->rhs->name, (int)map->lhs->len, map->lhs->name); + return -2; + } + + /* + * If there's no CoA packet and we're updating it, + * auto-allocate it. + */ + if (((map->lhs->tmpl_list == PAIR_LIST_COA) || + (map->lhs->tmpl_list == PAIR_LIST_DM)) && !request->coa) { + if ((request->packet->code == PW_CODE_COA_REQUEST) || + (request->packet->code == PW_CODE_DISCONNECT_REQUEST)) { + REDEBUG("You cannot do 'update coa' when processing a CoA / Disconnect request. Use 'update request' instead."); + return -2; + } + + if (!request_alloc_coa(context)) { + REDEBUG("Failed to create a CoA/Disconnect Request message"); + return -2; + } + context->coa->proxy->code = (map->lhs->tmpl_list == PAIR_LIST_COA) ? + PW_CODE_COA_REQUEST : + PW_CODE_DISCONNECT_REQUEST; + } + + list = radius_list(context, map->lhs->tmpl_list); + if (!list) { + REDEBUG("Mapping \"%.*s\" -> \"%.*s\" invalid in this context", + (int)map->rhs->len, map->rhs->name, (int)map->lhs->len, map->lhs->name); + + return -2; + } + + parent = radius_list_ctx(context, map->lhs->tmpl_list); + if (!parent) { + REDEBUG("Unable to set parent list"); + return -1; + } + + /* + * The callback should either return -1 to signify operations error, + * -2 when it can't find the attribute or list being referenced, or + * 0 to signify success. It may return "success", but still have no + * VPs to work with. + */ + if (map->rhs->type != TMPL_TYPE_NULL) { + rcode = func(parent, &head, request, map, ctx); + if (rcode < 0) { + rad_assert(!head); + return rcode; + } + if (!head) { + RDEBUG2("No attributes updated for RHS %s", map->rhs->name); + return rcode; + } + } else { + if (rad_debug_lvl) map_debug_log(request, map, NULL); + } + + /* + * Print the VPs + */ + for (vp = fr_cursor_init(&src_list, &head); + vp; + vp = fr_cursor_next(&src_list)) { + VERIFY_VP(vp); + + if (rad_debug_lvl) map_debug_log(request, map, vp); + } + + /* + * The destination is a list (which is a completely different set of operations) + */ + if (map->lhs->type == TMPL_TYPE_LIST) { + switch (map->op) { + case T_OP_CMP_FALSE: + /* We don't need the src VPs (should just be 'ANY') */ + rad_assert(!head); + + /* Clear the entire dst list */ + fr_pair_list_free(list); + + if (map->lhs->tmpl_list == PAIR_LIST_REQUEST) { + context->username = NULL; + context->password = NULL; + } + return 0; + + case T_OP_SET: + if (map->rhs->type == TMPL_TYPE_LIST) { + fr_pair_list_free(list); + *list = head; + head = NULL; + } else { /* FALL-THROUGH */ + case T_OP_EQ: + rad_assert(map->rhs->type == TMPL_TYPE_EXEC); + /* FALL-THROUGH */ + case T_OP_ADD: + fr_pair_list_move(parent, list, &head, map->op); + fr_pair_list_free(&head); + } + goto finish; + case T_OP_PREPEND: + fr_pair_list_move(parent, list, &head, T_OP_PREPEND); + fr_pair_list_free(&head); + goto finish; + + default: + fr_pair_list_free(&head); + return -1; + } + } + + /* + * Find the destination attribute. We leave with either + * the dst_list and vp pointing to the attribute or the VP + * being NULL (no attribute at that index). + */ + num = map->lhs->tmpl_num; + (void) fr_cursor_init(&dst_list, list); + if (num != NUM_ANY) { + while ((dst = fr_cursor_next_by_da(&dst_list, map->lhs->tmpl_da, map->lhs->tmpl_tag))) { + if (num-- == 0) break; + } + } else { + dst = fr_cursor_next_by_da(&dst_list, map->lhs->tmpl_da, map->lhs->tmpl_tag); + } + rad_assert(!dst || (map->lhs->tmpl_da == dst->da)); + + /* + * The destination is an attribute + */ + switch (map->op) { + default: + break; + /* + * !* - Remove all attributes which match dst in the specified list. + * This doesn't use attributes returned by the func(), and immediately frees them. + */ + case T_OP_CMP_FALSE: + /* We don't need the src VPs (should just be 'ANY') */ + rad_assert(!head); + if (!dst) return 0; + + /* + * Wildcard: delete all of the matching ones, based on tag. + */ + if (map->lhs->tmpl_num == NUM_ANY) { + fr_pair_delete_by_num(list, map->lhs->tmpl_da->attr, map->lhs->tmpl_da->vendor, map->lhs->tmpl_tag); + dst = NULL; + /* + * We've found the Nth one. Delete it, and only it. + */ + } else { + dst = fr_cursor_remove(&dst_list); + fr_pair_list_free(&dst); + } + + /* + * Check that the User-Name and User-Password + * caches point to the correct attribute. + */ + goto finish; + + /* + * -= - Delete attributes in the dst list which match any of the + * src_list attributes. + * + * This operation has two modes: + * - If map->lhs->tmpl_num > 0, we check each of the src_list attributes against + * the dst attribute, to see if any of their values match. + * - If map->lhs->tmpl_num == NUM_ANY, we compare all instances of the dst attribute + * against each of the src_list attributes. + */ + case T_OP_SUB: + /* We didn't find any attributes earlier */ + if (!dst) { + fr_pair_list_free(&head); + return 0; + } + + /* + * Instance specific[n] delete + */ + if (map->lhs->tmpl_num != NUM_ANY) { + for (vp = fr_cursor_first(&src_list); + vp; + vp = fr_cursor_next(&src_list)) { + head->op = T_OP_CMP_EQ; + rcode = radius_compare_vps(request, vp, dst); + if (rcode == 0) { + dst = fr_cursor_remove(&dst_list); + fr_pair_list_free(&dst); + found = true; + } + } + fr_pair_list_free(&head); + if (!found) return 0; + goto finish; + } + + /* + * All instances[*] delete + */ + for (dst = fr_cursor_current(&dst_list); + dst; + dst = fr_cursor_next_by_da(&dst_list, map->lhs->tmpl_da, map->lhs->tmpl_tag)) { + for (vp = fr_cursor_first(&src_list); + vp; + vp = fr_cursor_next(&src_list)) { + head->op = T_OP_CMP_EQ; + rcode = radius_compare_vps(request, vp, dst); + if (rcode == 0) { + dst = fr_cursor_remove(&dst_list); + fr_pair_list_free(&dst); + found = true; + } + } + } + fr_pair_list_free(&head); + if (!found) return 0; + goto finish; + } + + /* + * Another fixup pass to set tags on attributes were about to insert + */ + if (map->lhs->tmpl_tag != TAG_ANY) { + for (vp = fr_cursor_init(&src_list, &head); + vp; + vp = fr_cursor_next(&src_list)) { + vp->tag = map->lhs->tmpl_tag; + } + } + + switch (map->op) { + /* + * = - Set only if not already set + */ + case T_OP_EQ: + if (dst) { + RDEBUG3("Refusing to overwrite (use :=)"); + fr_pair_list_free(&head); + return 0; + } + + /* Insert first instance (if multiple) */ + fr_cursor_first(&src_list); + fr_cursor_insert(&dst_list, fr_cursor_remove(&src_list)); + /* Free any we didn't insert */ + fr_pair_list_free(&head); + break; + + /* + * := - Overwrite existing attribute with last src_list attribute + */ + case T_OP_SET: + /* Wind to last instance */ + fr_cursor_last(&src_list); + if (dst) { + DEBUG_OVERWRITE(dst, fr_cursor_current(&src_list)); + dst = fr_cursor_replace(&dst_list, fr_cursor_remove(&src_list)); + fr_pair_list_free(&dst); + } else { + fr_cursor_insert(&dst_list, fr_cursor_remove(&src_list)); + } + /* Free any we didn't insert */ + fr_pair_list_free(&head); + break; + + /* + * ^= - Prepend src_list attributes to the destination + */ + case T_OP_PREPEND: + fr_pair_prepend(list, head); + head = NULL; + break; + + /* + * += - Add all src_list attributes to the destination + */ + case T_OP_ADD: + /* Insert all the instances! (if multiple) */ + fr_pair_add(list, head); + head = NULL; + break; + + /* + * Filter operators + */ + case T_OP_REG_NE: + case T_OP_NE: + case T_OP_REG_EQ: + case T_OP_CMP_EQ: + case T_OP_GE: + case T_OP_GT: + case T_OP_LE: + case T_OP_LT: + { + VALUE_PAIR *a, *b; + + fr_pair_list_sort(&head, fr_pair_cmp_by_da_tag); + fr_pair_list_sort(list, fr_pair_cmp_by_da_tag); + + fr_cursor_first(&dst_list); + + for (b = fr_cursor_first(&src_list); + b; + b = fr_cursor_next(&src_list)) { + found = false; + + for (a = fr_cursor_current(&dst_list); + a; + a = fr_cursor_next(&dst_list)) { + int8_t cmp; + + cmp = fr_pair_cmp_by_da_tag(a, b); /* attribute and tag match */ + if (cmp > 0) break; + else if (cmp < 0) continue; + + /* + * The LHS exists. We need to + * limit it's value based on the + * operator, and on the value of + * the RHS. + */ + cmp = (value_data_cmp_op(map->op, a->da->type, &a->data, a->vp_length, b->da->type, &b->data, b->vp_length) == 0); + if (cmp == 1) switch (map->op) { + + /* + * Keep only matching attributes. + */ + default: + case T_OP_REG_NE: + case T_OP_NE: + case T_OP_REG_EQ: + case T_OP_CMP_EQ: + a = fr_cursor_remove(&dst_list); + talloc_free(a); + break; + + /* + * Keep matching + * attribute, and enforce + * matching values. + */ + case T_OP_GE: + case T_OP_GT: + case T_OP_LE: + case T_OP_LT: + DEBUG_OVERWRITE(a, b); + (void) value_data_copy(a, &a->data, a->da->type, + &b->data, b->vp_length); + found = true; + break; + } + } + + /* + * End of the dst list. + */ + if (!a) { + if (found) break; + + switch (map->op) { + default: + break; + + /* + * It wasn't found. Insert it with the given value. + */ + case T_OP_GE: + case T_OP_GT: + case T_OP_LE: + case T_OP_LT: + (void) fr_cursor_insert(&dst_list, fr_pair_copy(parent, b)); + break; + } + break; + } + } + fr_pair_list_free(&head); + } + break; + + default: + rad_assert(0); /* Should have been caught be the caller */ + return -1; + } + +finish: + rad_assert(!head); + + /* + * Update the cached username && password. This is code + * we execute on EVERY update (sigh) so that SOME modules + * MIGHT NOT have to do the search themselves. + * + * TBH, we should probably make each module just do the + * search themselves. + */ + if (map->lhs->tmpl_list == PAIR_LIST_REQUEST) { + context->username = NULL; + context->password = NULL; + + for (vp = fr_cursor_init(&src_list, list); + vp; + vp = fr_cursor_next(&src_list)) { + + if (vp->da->vendor != 0) continue; + if (vp->da->flags.has_tag) continue; + + if (!context->username && (vp->da->attr == PW_USER_NAME)) { + context->username = vp; + continue; + } + + if (vp->da->attr == PW_STRIPPED_USER_NAME) { + context->username = vp; + continue; + } + + if (vp->da->attr == PW_USER_PASSWORD) { + context->password = vp; + continue; + } + } + } + return 0; +} + +/** Check whether the destination of a map is currently valid + * + * @param request The current request. + * @param map to check. + * @return true if the map resolves to a request and list else false. + */ +bool map_dst_valid(REQUEST *request, vp_map_t const *map) +{ + REQUEST *context = request; + + VERIFY_MAP(map); + + if (radius_request(&context, map->lhs->tmpl_request) < 0) return false; + if (!radius_list(context, map->lhs->tmpl_list)) return false; + + return true; +} + +/** Print a map to a string + * + * @param[out] buffer for the output string + * @param[in] bufsize of the buffer + * @param[in] map to print + * @return the size of the string printed + */ +size_t map_prints(char *buffer, size_t bufsize, vp_map_t const *map) +{ + size_t len; + DICT_ATTR const *da = NULL; + char *p = buffer; + char *end = buffer + bufsize; + + VERIFY_MAP(map); + + if (map->lhs->type == TMPL_TYPE_ATTR) da = map->lhs->tmpl_da; + + len = tmpl_prints(buffer, bufsize, map->lhs, da); + p += len; + + *(p++) = ' '; + strlcpy(p, fr_token_name(map->op), end - p); + p += strlen(p); + *(p++) = ' '; + + /* + * The RHS doesn't matter for many operators + */ + if ((map->op == T_OP_CMP_TRUE) || + (map->op == T_OP_CMP_FALSE)) { + strlcpy(p, "ANY", (end - p)); + p += strlen(p); + return p - buffer; + } + + rad_assert(map->rhs != NULL); + + if ((map->lhs->type == TMPL_TYPE_ATTR) && + (map->lhs->tmpl_da->type == PW_TYPE_STRING) && + (map->rhs->type == TMPL_TYPE_LITERAL)) { + *(p++) = '\''; + len = tmpl_prints(p, end - p, map->rhs, da); + p += len; + *(p++) = '\''; + *p = '\0'; + } else { + len = tmpl_prints(p, end - p, map->rhs, da); + p += len; + } + + return p - buffer; +} + +/* + * Debug print a map / VP + */ +void map_debug_log(REQUEST *request, vp_map_t const *map, VALUE_PAIR const *vp) +{ + char *value; + char buffer[1024]; + + VERIFY_MAP(map); + rad_assert(map->lhs != NULL); + rad_assert(map->rhs != NULL); + + rad_assert(vp || (map->rhs->type == TMPL_TYPE_NULL)); + + switch (map->rhs->type) { + /* + * Just print the value being assigned + */ + default: + case TMPL_TYPE_LITERAL: + vp_prints_value(buffer, sizeof(buffer), vp, map->rhs->quote); + value = buffer; + break; + + case TMPL_TYPE_XLAT: + case TMPL_TYPE_XLAT_STRUCT: + vp_prints_value(buffer, sizeof(buffer), vp, map->rhs->quote); + value = buffer; + break; + + case TMPL_TYPE_DATA: + vp_prints_value(buffer, sizeof(buffer), vp, map->rhs->quote); + value = buffer; + break; + + /* + * For the lists, we can't use the original name, and have to + * rebuild it using tmpl_prints, for each attribute we're + * copying. + */ + case TMPL_TYPE_LIST: + { + char attr[256]; + char quote = '\0'; + vp_tmpl_t vpt; + /* + * Fudge a temporary tmpl that describes the attribute we're copying + * this is a combination of the original list tmpl, and values from + * the VALUE_PAIR. This way, we get tag info included. + */ + memcpy(&vpt, map->rhs, sizeof(vpt)); + vpt.tmpl_da = vp->da; + vpt.tmpl_tag = vp->tag; + vpt.type = TMPL_TYPE_ATTR; + + /* + * Not appropriate to use map->rhs->quote here, as that's the quoting + * around the list ref. The attribute value has no quoting, so we choose + * the quoting based on the data type, and whether it's printable. + */ + if (vp->da->type == PW_TYPE_STRING) quote = is_printable(vp->vp_strvalue, + vp->vp_length) ? '\'' : '"'; + vp_prints_value(buffer, sizeof(buffer), vp, quote); + tmpl_prints(attr, sizeof(attr), &vpt, vp->da); + value = talloc_typed_asprintf(request, "%s -> %s", attr, buffer); + } + break; + + case TMPL_TYPE_ATTR: + { + char quote = '\0'; + + /* + * Not appropriate to use map->rhs->quote here, as that's the quoting + * around the attr ref. The attribute value has no quoting, so we choose + * the quoting based on the data type, and whether it's printable. + */ + if (vp->da->type == PW_TYPE_STRING) quote = is_printable(vp->vp_strvalue, + vp->vp_length) ? '\'' : '"'; + vp_prints_value(buffer, sizeof(buffer), vp, quote); + value = talloc_typed_asprintf(request, "%.*s -> %s", (int)map->rhs->len, map->rhs->name, buffer); + } + break; + + case TMPL_TYPE_NULL: + strcpy(buffer, "ANY"); + value = buffer; + break; + } + + switch (map->lhs->type) { + case TMPL_TYPE_LIST: + RDEBUG("%.*s:%s %s %s", (int)map->lhs->len, map->lhs->name, vp ? vp->da->name : "", + fr_int2str(fr_tokens, vp ? vp->op : map->op, "<INVALID>"), value); + break; + + case TMPL_TYPE_ATTR: + RDEBUG("%s %s %s", map->lhs->name, + fr_int2str(fr_tokens, vp ? vp->op : map->op, "<INVALID>"), value); + break; + + default: + RDEBUG("map %s = %s", fr_int2str(tmpl_names, map->lhs->type, "???"), value); + break; + } + + if (value != buffer) talloc_free(value); +} |