diff options
Diffstat (limited to '')
-rw-r--r-- | src/lib/var-expand.c | 833 |
1 files changed, 833 insertions, 0 deletions
diff --git a/src/lib/var-expand.c b/src/lib/var-expand.c new file mode 100644 index 0000000..7ddaec0 --- /dev/null +++ b/src/lib/var-expand.c @@ -0,0 +1,833 @@ +/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "md5.h" +#include "hash.h" +#include "hex-binary.h" +#include "base64.h" +#include "hostpid.h" +#include "hmac.h" +#include "pkcs5.h" +#include "hash-method.h" +#include "str.h" +#include "strescape.h" +#include "var-expand.h" +#include "var-expand-private.h" + +#include <unistd.h> +#include <ctype.h> + +#define TABLE_LAST(t) \ + ((t)->key == '\0' && (t)->long_key == NULL) + +struct var_expand_modifier { + char key; + const char *(*func)(const char *, struct var_expand_context *); +}; + +static ARRAY(struct var_expand_extension_func_table) var_expand_extensions; + +static const char * +m_str_lcase(const char *str, struct var_expand_context *ctx ATTR_UNUSED) +{ + return t_str_lcase(str); +} + +static const char * +m_str_ucase(const char *str, struct var_expand_context *ctx ATTR_UNUSED) +{ + return t_str_ucase(str); +} + +static const char * +m_str_escape(const char *str, struct var_expand_context *ctx ATTR_UNUSED) +{ + return str_escape(str); +} + +static const char * +m_str_hex(const char *str, struct var_expand_context *ctx ATTR_UNUSED) +{ + unsigned long long l; + + if (str_to_ullong(str, &l) < 0) + l = 0; + return t_strdup_printf("%llx", l); +} + +static const char * +m_str_reverse(const char *str, struct var_expand_context *ctx ATTR_UNUSED) +{ + size_t len = strlen(str); + char *p, *rev; + + rev = t_malloc_no0(len + 1); + rev[len] = '\0'; + + for (p = rev + len - 1; *str != '\0'; str++) + *p-- = *str; + return rev; +} + +static const char *m_str_hash(const char *str, struct var_expand_context *ctx) +{ + unsigned int value = str_hash(str); + string_t *hash = t_str_new(20); + + if (ctx->width != 0) { + value %= ctx->width; + ctx->width = 0; + } + + str_printfa(hash, "%x", value); + while ((int)str_len(hash) < ctx->offset) + str_insert(hash, 0, "0"); + ctx->offset = 0; + + return str_c(hash); +} + +static const char * +m_str_newhash(const char *str, struct var_expand_context *ctx) +{ + string_t *hash = t_str_new(20); + unsigned char result[MD5_RESULTLEN]; + unsigned int i; + uint64_t value = 0; + + md5_get_digest(str, strlen(str), result); + for (i = 0; i < sizeof(value); i++) { + value <<= 8; + value |= result[i]; + } + + if (ctx->width != 0) { + value %= ctx->width; + ctx->width = 0; + } + + str_printfa(hash, "%x", (unsigned int)value); + while ((int)str_len(hash) < ctx->offset) + str_insert(hash, 0, "0"); + ctx->offset = 0; + + return str_c(hash); +} + +static const char * +m_str_md5(const char *str, struct var_expand_context *ctx ATTR_UNUSED) +{ + unsigned char digest[16]; + + md5_get_digest(str, strlen(str), digest); + + return binary_to_hex(digest, sizeof(digest)); +} + +static const char * +m_str_ldap_dn(const char *str, struct var_expand_context *ctx ATTR_UNUSED) +{ + string_t *ret = t_str_new(256); + + while (*str != '\0') { + if (*str == '.') + str_append(ret, ",dc="); + else + str_append_c(ret, *str); + str++; + } + + return str_free_without_data(&ret); +} + +static const char * +m_str_trim(const char *str, struct var_expand_context *ctx ATTR_UNUSED) +{ + size_t len; + + len = strlen(str); + while (len > 0 && i_isspace(str[len-1])) + len--; + return t_strndup(str, len); +} + +#define MAX_MODIFIER_COUNT 10 +static const struct var_expand_modifier modifiers[] = { + { 'L', m_str_lcase }, + { 'U', m_str_ucase }, + { 'E', m_str_escape }, + { 'X', m_str_hex }, + { 'R', m_str_reverse }, + { 'H', m_str_hash }, + { 'N', m_str_newhash }, + { 'M', m_str_md5 }, + { 'D', m_str_ldap_dn }, + { 'T', m_str_trim }, + { '\0', NULL } +}; + +static int +var_expand_short(const struct var_expand_table *table, char key, + const char **var_r, const char **error_r) +{ + const struct var_expand_table *t; + + if (table != NULL) { + for (t = table; !TABLE_LAST(t); t++) { + if (t->key == key) { + *var_r = t->value != NULL ? t->value : ""; + return 1; + } + } + } + + /* not found */ + if (key == '%') { + *var_r = "%"; + return 1; + } + if (*error_r == NULL) + *error_r = t_strdup_printf("Unknown variable '%%%c'", key); + *var_r = t_strdup_printf("UNSUPPORTED_VARIABLE_%c", key); + return 0; +} + +static int +var_expand_hash(struct var_expand_context *ctx, + const char *key, const char *field, + const char **result_r, const char **error_r) +{ + enum { + FORMAT_HEX, + FORMAT_HEX_UC, + FORMAT_BASE64 + } format = FORMAT_HEX; + + const char *p = strchr(key, ';'); + const char *const *args = NULL; + const char *algo = key; + const char *value; + int ret; + + if (p != NULL) { + algo = t_strcut(key, ';'); + args = t_strsplit(p+1, ","); + } + + const struct hash_method *method; + if (strcmp(algo, "pkcs5") == 0) { + method = hash_method_lookup("sha256"); + } else if ((method = hash_method_lookup(algo)) == NULL) { + return 0; + } + + string_t *field_value = t_str_new(64); + string_t *salt = t_str_new(64); + string_t *tmp = t_str_new(method->digest_size); + + if ((ret = var_expand_long(ctx, field, strlen(field), + &value, error_r)) < 1) { + return ret; + } + + str_append(field_value, value); + + /* default values */ + unsigned int rounds = 1; + unsigned int truncbits = 0; + + if (strcmp(algo, "pkcs5") == 0) { + rounds = 2048; + str_append(salt, field); + } + + while(args != NULL && *args != NULL) { + const char *k = t_strcut(*args, '='); + const char *value = strchr(*args, '='); + if (value == NULL) { + args++; + continue; + } else { + value++; + } + if (strcmp(k, "rounds") == 0) { + if (str_to_uint(value, &rounds)<0) { + *error_r = t_strdup_printf( + "Cannot parse hash arguments:" + "'%s' is not number for rounds", + value); + return -1; + } + if (rounds < 1) { + *error_r = t_strdup_printf( + "Cannot parse hash arguments:" + "rounds must be at least 1"); + return -1; + } + } else if (strcmp(k, "truncate") == 0) { + if (str_to_uint(value, &truncbits)<0) { + *error_r = t_strdup_printf( + "Cannot parse hash arguments:" + "'%s' is not number for truncbits", + value); + return -1; + } + truncbits = I_MIN(truncbits, method->digest_size*8); + } else if (strcmp(k, "salt") == 0) { + str_truncate(salt, 0); + if (var_expand_with_funcs(salt, value, ctx->table, + ctx->func_table, ctx->context, + error_r) < 0) { + return -1; + } + break; + } else if (strcmp(k, "format") == 0) { + if (strcmp(value, "hex") == 0) { + format = FORMAT_HEX; + } else if (strcmp(value, "hexuc") == 0){ + format = FORMAT_HEX_UC; + } else if (strcmp(value, "base64") == 0) { + format = FORMAT_BASE64; + } else { + *error_r = t_strdup_printf( + "Cannot parse hash arguments:" + "'%s' is not supported format", + value); + return -1; + } + } + args++; + } + + str_truncate(tmp, 0); + + if (strcmp(algo, "pkcs5") == 0) { + if (pkcs5_pbkdf(PKCS5_PBKDF2, method, + field_value->data, field_value->used, + salt->data, salt->used, + rounds, HMAC_MAX_CONTEXT_SIZE, tmp) != 0) { + *error_r = "Cannot hash: PKCS5_PBKDF2 failed"; + return -1; + } + } else { + void *context = t_malloc_no0(method->context_size); + + str_append_str(tmp, field_value); + + for(;rounds>0;rounds--) { + method->init(context); + if (salt->used > 0) + method->loop(context, salt->data, salt->used); + method->loop(context, tmp->data, tmp->used); + unsigned char *digest = + buffer_get_modifiable_data(tmp, NULL); + method->result(context, digest); + if (tmp->used != method->digest_size) + buffer_set_used_size(tmp, method->digest_size); + } + } + + if (truncbits > 0) + buffer_truncate_rshift_bits(tmp, truncbits); + + switch(format) { + case FORMAT_HEX: + *result_r = binary_to_hex(tmp->data, tmp->used); + return 1; + case FORMAT_HEX_UC: + *result_r = binary_to_hex(tmp->data, tmp->used); + return 1; + case FORMAT_BASE64: { + string_t *dest = t_str_new(64); + base64_encode(tmp->data, tmp->used, dest); + *result_r = str_c(dest); + return 1; + } + } + + i_unreached(); +} + +static int +var_expand_func(const struct var_expand_func_table *func_table, + const char *key, const char *data, void *context, + const char **var_r, const char **error_r) +{ + const char *value = NULL; + int ret; + + if (strcmp(key, "env") == 0) { + value = getenv(data); + *var_r = value != NULL ? value : ""; + return 1; + } + if (func_table != NULL) { + for (; func_table->key != NULL; func_table++) { + if (strcmp(func_table->key, key) == 0) { + ret = func_table->func(data, context, &value, error_r); + *var_r = value != NULL ? value : ""; + return ret; + } + } + } + if (*error_r == NULL) + *error_r = t_strdup_printf("Unknown variable '%%%s'", key); + *var_r = t_strdup_printf("UNSUPPORTED_VARIABLE_%s", key); + return 0; +} + +static int +var_expand_try_extension(struct var_expand_context *ctx, + const char *key, const char *data, + const char **var_r, const char **error_r) +{ + int ret; + const char *sep = strchr(key, ';'); + + if (sep == NULL) sep = key + strlen(key); + + /* try with extensions */ + const struct var_expand_extension_func_table *f; + array_foreach(&var_expand_extensions, f) { + /* ensure we won't match abbreviations */ + size_t len = sep-key; + if (strncasecmp(key, f->key, len) == 0 && f->key[len] == '\0') + return f->func(ctx, key, data, var_r, error_r); + } + if ((ret = var_expand_func(ctx->func_table, key, data, + ctx->context, var_r, error_r)) == 0) { + *error_r = t_strdup_printf("Unknown variable '%%%s'", key); + } + return ret; +} + + +int +var_expand_long(struct var_expand_context *ctx, + const void *key_start, size_t key_len, + const char **var_r, const char **error_r) +{ + const struct var_expand_table *t; + const char *key, *value = NULL; + int ret = 1; + + if (ctx->table != NULL) { + for (t = ctx->table; !TABLE_LAST(t); t++) { + if (t->long_key != NULL && + strncmp(t->long_key, key_start, key_len) == 0 && + t->long_key[key_len] == '\0') { + *var_r = t->value != NULL ? t->value : ""; + return 1; + } + } + } + key = t_strndup(key_start, key_len); + + /* built-in variables: */ + switch (key_len) { + case 3: + if (strcmp(key, "pid") == 0) + value = my_pid; + else if (strcmp(key, "uid") == 0) + value = dec2str(geteuid()); + else if (strcmp(key, "gid") == 0) + value = dec2str(getegid()); + break; + case 8: + if (strcmp(key, "hostname") == 0) + value = my_hostname; + break; + } + + if (value == NULL) { + const char *data = strchr(key, ':'); + + if (data != NULL) + key = t_strdup_until(key, data++); + else + data = ""; + + ret = var_expand_try_extension(ctx, key, data, &value, error_r); + + if (ret <= 0 && value == NULL) { + value = ""; + } + } + *var_r = value; + return ret; +} + +int var_expand_with_funcs(string_t *dest, const char *str, + const struct var_expand_table *table, + const struct var_expand_func_table *func_table, + void *context, const char **error_r) +{ + const struct var_expand_modifier *m; + const char *var; + struct var_expand_context ctx; + const char *(*modifier[MAX_MODIFIER_COUNT]) + (const char *, struct var_expand_context *); + const char *end; + unsigned int i, modifier_count; + size_t len; + int ret, final_ret = 1; + + *error_r = NULL; + + i_zero(&ctx); + ctx.table = table; + ctx.func_table = func_table; + ctx.context = context; + + for (; *str != '\0'; str++) { + if (*str != '%') + str_append_c(dest, *str); + else { + int sign = 1; + + str++; + + /* reset per-field modifiers */ + ctx.offset = 0; + ctx.width = 0; + ctx.zero_padding = FALSE; + + /* [<offset>.]<width>[<modifiers>]<variable> */ + if (*str == '-') { + sign = -1; + str++; + } + if (*str == '0') { + ctx.zero_padding = TRUE; + str++; + } + while (*str >= '0' && *str <= '9') { + ctx.width = ctx.width*10 + (*str - '0'); + str++; + } + + if (*str == '.') { + ctx.offset = sign * ctx.width; + sign = 1; + ctx.width = 0; + str++; + + /* if offset was prefixed with zero (or it was + plain zero), just ignore that. zero padding + is done with the width. */ + ctx.zero_padding = FALSE; + if (*str == '0') { + ctx.zero_padding = TRUE; + str++; + } + if (*str == '-') { + sign = -1; + str++; + } + + while (*str >= '0' && *str <= '9') { + ctx.width = ctx.width*10 + (*str - '0'); + str++; + } + ctx.width = sign * ctx.width; + } + + modifier_count = 0; + while (modifier_count < MAX_MODIFIER_COUNT) { + modifier[modifier_count] = NULL; + for (m = modifiers; m->key != '\0'; m++) { + if (m->key == *str) { + /* @UNSAFE */ + modifier[modifier_count] = + m->func; + str++; + break; + } + } + if (modifier[modifier_count] == NULL) + break; + modifier_count++; + } + + if (*str == '\0') + break; + + var = NULL; + if (*str == '{' && strchr(str, '}') != NULL) { + /* %{long_key} */ + unsigned int ctr = 1; + bool escape = FALSE; + end = str; + while(*++end != '\0' && ctr > 0) { + if (!escape && *end == '\\') { + escape = TRUE; + continue; + } + if (escape) { + escape = FALSE; + continue; + } + if (*end == '{') ctr++; + if (*end == '}') ctr--; + } + if (ctr == 0) + /* it needs to come back a bit */ + end--; + /* if there is no } it will consume rest of the + string */ + len = end - (str + 1); + ret = var_expand_long(&ctx, str+1, len, + &var, error_r); + str = end; + } else { + ret = var_expand_short(ctx.table, *str, + &var, error_r); + } + i_assert(var != NULL); + + if (final_ret > ret) + final_ret = ret; + + if (ret <= 0) + str_append(dest, var); + else { + for (i = 0; i < modifier_count; i++) + var = modifier[i](var, &ctx); + + if (ctx.offset < 0) { + /* if offset is < 0 then we want to + start at the end */ + size_t len = strlen(var); + size_t offset_from_end = -ctx.offset; + + if (len > offset_from_end) + var += len - offset_from_end; + } else { + while (*var != '\0' && ctx.offset > 0) { + ctx.offset--; + var++; + } + } + if (ctx.width == 0) + str_append(dest, var); + else if (!ctx.zero_padding) { + if (ctx.width < 0) + ctx.width = strlen(var) - (-ctx.width); + str_append_max(dest, var, ctx.width); + } else { + /* %05d -like padding. no truncation. */ + ssize_t len = strlen(var); + while (len < ctx.width) { + str_append_c(dest, '0'); + ctx.width--; + } + str_append(dest, var); + } + } + } + } + return final_ret; +} + +int var_expand(string_t *dest, const char *str, + const struct var_expand_table *table, const char **error_r) +{ + return var_expand_with_funcs(dest, str, table, NULL, NULL, error_r); +} + +static bool +var_get_key_range_full(const char *str, unsigned int *idx_r, + unsigned int *size_r) +{ + const struct var_expand_modifier *m; + unsigned int i = 0; + + /* [<offset>.]<width>[<modifiers>]<variable> */ + while ((str[i] >= '0' && str[i] <= '9') || str[i] == '-') + i++; + + if (str[i] == '.') { + i++; + while ((str[i] >= '0' && str[i] <= '9') || str[i] == '-') + i++; + } + + do { + for (m = modifiers; m->key != '\0'; m++) { + if (m->key == str[i]) { + i++; + break; + } + } + } while (m->key != '\0'); + + if (str[i] != '{') { + /* short key */ + *idx_r = i; + *size_r = str[i] == '\0' ? 0 : 1; + return FALSE; + } else { + unsigned int depth = 1; + bool escape = FALSE; + /* long key */ + *idx_r = ++i; + for (; str[i] != '\0'; i++) { + if (!escape && str[i] == '\\') { + escape = TRUE; + continue; + } + if (escape) { + escape = FALSE; + continue; + } + if (str[i] == '{') + depth++; + if (str[i] == '}') { + if (--depth==0) + break; + } + } + *size_r = i - *idx_r; + return TRUE; + } +} + +char var_get_key(const char *str) +{ + unsigned int idx, size; + + if (var_get_key_range_full(str, &idx, &size)) + return '{'; + return str[idx]; +} + +void var_get_key_range(const char *str, unsigned int *idx_r, + unsigned int *size_r) +{ + (void)var_get_key_range_full(str, idx_r, size_r); +} + +static bool var_has_long_key(const char **str, const char *long_key) +{ + const char *start, *end; + + start = strchr(*str, '{'); + i_assert(start != NULL); + + end = strchr(++start, '}'); + if (end == NULL) + return FALSE; + + if (strncmp(start, long_key, end-start) == 0 && + long_key[end-start] == '\0') + return TRUE; + + *str = end; + return FALSE; +} + +bool var_has_key(const char *str, char key, const char *long_key) +{ + char c; + + for (; *str != '\0'; str++) { + if (*str == '%' && str[1] != '\0') { + str++; + c = var_get_key(str); + if (c == key && key != '\0') + return TRUE; + + if (c == '{' && long_key != NULL) { + if (var_has_long_key(&str, long_key)) + return TRUE; + } + } + } + return FALSE; +} + +void var_expand_extensions_deinit(void) +{ + array_free(&var_expand_extensions); +} + +void var_expand_extensions_init(void) +{ + i_array_init(&var_expand_extensions, 32); + + /* put all hash methods there */ + for(const struct hash_method **meth = hash_methods; + *meth != NULL; + meth++) { + struct var_expand_extension_func_table *func = + array_append_space(&var_expand_extensions); + func->key = (*meth)->name; + func->func = var_expand_hash; + } + + /* pkcs5 */ + struct var_expand_extension_func_table *func = + array_append_space(&var_expand_extensions); + func->key = "pkcs5"; + func->func = var_expand_hash; + + /* if */ + func = array_append_space(&var_expand_extensions); + func->key = "if"; + func->func = var_expand_if; +} + +void +var_expand_register_func_array(const struct var_expand_extension_func_table *funcs) +{ + for(const struct var_expand_extension_func_table *ptr = funcs; + ptr->key != NULL; + ptr++) { + i_assert(*ptr->key != '\0'); + array_push_front(&var_expand_extensions, ptr); + } +} + +void +var_expand_unregister_func_array(const struct var_expand_extension_func_table *funcs) +{ + for(const struct var_expand_extension_func_table *ptr = funcs; + ptr->key != NULL; + ptr++) { + i_assert(ptr->func != NULL); + for(unsigned int i = 0; i < array_count(&var_expand_extensions); i++) { + const struct var_expand_extension_func_table *func = + array_idx(&var_expand_extensions, i); + if (strcasecmp(func->key, ptr->key) == 0) { + array_delete(&var_expand_extensions, i, 1); + } + } + } +} + +struct var_expand_table * +var_expand_merge_tables(pool_t pool, const struct var_expand_table *a, + const struct var_expand_table *b) +{ + ARRAY(struct var_expand_table) table; + size_t a_size = var_expand_table_size(a); + size_t b_size = var_expand_table_size(b); + p_array_init(&table, pool, a_size + b_size + 1); + for(size_t i=0; i<a_size; i++) { + struct var_expand_table *entry = + array_append_space(&table); + entry->key = a[i].key; + entry->value = p_strdup(pool, a[i].value); + entry->long_key = p_strdup(pool, a[i].long_key); + } + for(size_t i=0; i<b_size; i++) { + struct var_expand_table *entry = + array_append_space(&table); + entry->key = b[i].key; + entry->value = p_strdup(pool, b[i].value); + entry->long_key = p_strdup(pool, b[i].long_key); + } + array_append_zero(&table); + return array_front_modifiable(&table); +} |