/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */ #include "lib.h" #include "array.h" #include "hash.h" #include "str.h" #include "strescape.h" #include "home-expand.h" #include "mkdir-parents.h" #include "eacces-error.h" #include "file-lock.h" #include "file-dotlock.h" #include "nfs-workarounds.h" #include "istream.h" #include "ostream.h" #include "dict-transaction-memory.h" #include "dict-private.h" #include #include #include #include struct file_dict { struct dict dict; pool_t hash_pool; enum file_lock_method lock_method; char *path; char *home_dir; bool dict_path_checked; HASH_TABLE(char *, char *) hash; int fd; bool refreshed; }; struct file_dict_iterate_context { struct dict_iterate_context ctx; pool_t pool; struct hash_iterate_context *iter; const char *path; size_t path_len; enum dict_iterate_flags flags; const char *values[2]; const char *error; }; static struct dotlock_settings file_dict_dotlock_settings = { .timeout = 60*2, .stale_timeout = 60, .use_io_notify = TRUE }; static int file_dict_ensure_path_home_dir(struct file_dict *dict, const char *home_dir, const char **error_r) { if (null_strcmp(dict->home_dir, home_dir) == 0) return 0; if (dict->dict_path_checked) { *error_r = t_strdup_printf("home_dir changed from %s to %s " "(requested dict was: %s)", dict->home_dir, home_dir, dict->path); return -1; } char *_p = dict->path; dict->path = i_strdup(home_expand_tilde(dict->path, home_dir)); dict->home_dir = i_strdup(home_dir); i_free(_p); dict->dict_path_checked = TRUE; return 0; } static int file_dict_init(struct dict *driver, const char *uri, const struct dict_settings *set ATTR_UNUSED, struct dict **dict_r, const char **error_r) { struct file_dict *dict; const char *p, *path; dict = i_new(struct file_dict, 1); dict->lock_method = FILE_LOCK_METHOD_DOTLOCK; p = strchr(uri, ':'); if (p == NULL) { /* no parameters */ path = uri; } else { path = t_strdup_until(uri, p++); if (strcmp(p, "lock=fcntl") == 0) dict->lock_method = FILE_LOCK_METHOD_FCNTL; else if (strcmp(p, "lock=flock") == 0) dict->lock_method = FILE_LOCK_METHOD_FLOCK; else { *error_r = t_strdup_printf("Invalid parameter: %s", p+1); i_free(dict); return -1; } } /* keep the path for now, later in dict operations check if home_dir should be prepended. */ dict->path = i_strdup(path); dict->dict = *driver; dict->hash_pool = pool_alloconly_create("file dict", 1024); hash_table_create(&dict->hash, dict->hash_pool, 0, str_hash, strcmp); dict->fd = -1; *dict_r = &dict->dict; return 0; } static void file_dict_deinit(struct dict *_dict) { struct file_dict *dict = (struct file_dict *)_dict; i_close_fd_path(&dict->fd, dict->path); hash_table_destroy(&dict->hash); pool_unref(&dict->hash_pool); i_free(dict->path); i_free(dict->home_dir); i_free(dict); } static bool file_dict_need_refresh(struct file_dict *dict) { struct stat st1, st2; if (dict->dict.iter_count > 0) { /* Change nothing while there are iterators or they can crash because the hash table content recreated. */ return FALSE; } if (dict->fd == -1) return TRUE; /* Disable NFS flushing for now since it can cause unnecessary problems and there's no easy way for us to know here if mail_nfs_storage=yes. In any case it's pretty much an unsupported setting nowadays. */ /*nfs_flush_file_handle_cache(dict->path);*/ if (nfs_safe_stat(dict->path, &st1) < 0) { e_error(dict->dict.event, "stat(%s) failed: %m", dict->path); return FALSE; } if (fstat(dict->fd, &st2) < 0) { if (errno != ESTALE) e_error(dict->dict.event, "fstat(%s) failed: %m", dict->path); return TRUE; } if (st1.st_ino != st2.st_ino || !CMP_DEV_T(st1.st_dev, st2.st_dev)) { /* file changed */ return TRUE; } return FALSE; } static int file_dict_open_latest(struct file_dict *dict, const char **error_r) { int open_type; if (!file_dict_need_refresh(dict)) return 0; i_close_fd_path(&dict->fd, dict->path); open_type = dict->lock_method == FILE_LOCK_METHOD_DOTLOCK ? O_RDONLY : O_RDWR; dict->fd = open(dict->path, open_type); if (dict->fd == -1) { if (errno == ENOENT) return 0; if (errno == EACCES) *error_r = eacces_error_get("open", dict->path); else *error_r = t_strdup_printf("open(%s) failed: %m", dict->path); return -1; } dict->refreshed = FALSE; return 1; } static int file_dict_refresh(struct file_dict *dict, const char **error_r) { struct istream *input; char *key, *value; if (file_dict_open_latest(dict, error_r) < 0) return -1; if (dict->refreshed || dict->dict.iter_count > 0) return 0; hash_table_clear(dict->hash, TRUE); p_clear(dict->hash_pool); if (dict->fd != -1) { input = i_stream_create_fd(dict->fd, SIZE_MAX); while ((key = i_stream_read_next_line(input)) != NULL) { /* strdup() before the second read */ key = str_tabunescape(p_strdup(dict->hash_pool, key)); if ((value = i_stream_read_next_line(input)) == NULL) break; value = str_tabunescape(p_strdup(dict->hash_pool, value)); hash_table_update(dict->hash, key, value); } i_stream_destroy(&input); } dict->refreshed = TRUE; return 0; } static int file_dict_lookup(struct dict *_dict, const struct dict_op_settings *set, pool_t pool, const char *key, const char **value_r, const char **error_r) { struct file_dict *dict = (struct file_dict *)_dict; if (file_dict_ensure_path_home_dir(dict, set->home_dir, error_r) < 0) return -1; if (file_dict_refresh(dict, error_r) < 0) return -1; *value_r = p_strdup(pool, hash_table_lookup(dict->hash, key)); return *value_r == NULL ? 0 : 1; } static struct dict_iterate_context * file_dict_iterate_init(struct dict *_dict, const struct dict_op_settings *set ATTR_UNUSED, const char *path, enum dict_iterate_flags flags) { struct file_dict_iterate_context *ctx; struct file_dict *dict = (struct file_dict *)_dict; const char *error; pool_t pool; pool = pool_alloconly_create("file dict iterate", 256); ctx = p_new(pool, struct file_dict_iterate_context, 1); ctx->ctx.dict = _dict; ctx->pool = pool; ctx->path = p_strdup(pool, path); ctx->path_len = strlen(path); ctx->flags = flags; if (file_dict_ensure_path_home_dir(dict, set->home_dir, &error) < 0 || file_dict_refresh(dict, &error) < 0) ctx->error = p_strdup(pool, error); ctx->iter = hash_table_iterate_init(dict->hash); return &ctx->ctx; } static bool file_dict_iterate_key_matches(struct file_dict_iterate_context *ctx, const char *key) { if (strncmp(ctx->path, key, ctx->path_len) == 0) return TRUE; return FALSE; } static bool file_dict_iterate(struct dict_iterate_context *_ctx, const char **key_r, const char *const **values_r) { struct file_dict_iterate_context *ctx = (struct file_dict_iterate_context *)_ctx; char *key, *value; while (hash_table_iterate(ctx->iter, ((struct file_dict *)_ctx->dict)->hash, &key, &value)) { if (!file_dict_iterate_key_matches(ctx, key)) continue; if ((ctx->flags & DICT_ITERATE_FLAG_RECURSE) != 0) { /* match everything */ } else if ((ctx->flags & DICT_ITERATE_FLAG_EXACT_KEY) != 0) { if (key[ctx->path_len] != '\0') continue; } else { if (strchr(key + ctx->path_len, '/') != NULL) continue; } *key_r = key; ctx->values[0] = value; *values_r = ctx->values; return TRUE; } return FALSE; } static int file_dict_iterate_deinit(struct dict_iterate_context *_ctx, const char **error_r) { struct file_dict_iterate_context *ctx = (struct file_dict_iterate_context *)_ctx; int ret = ctx->error != NULL ? -1 : 0; *error_r = t_strdup(ctx->error); hash_table_iterate_deinit(&ctx->iter); pool_unref(&ctx->pool); return ret; } static struct dict_transaction_context * file_dict_transaction_init(struct dict *_dict) { struct dict_transaction_memory_context *ctx; pool_t pool; pool = pool_alloconly_create("file dict transaction", 2048); ctx = p_new(pool, struct dict_transaction_memory_context, 1); dict_transaction_memory_init(ctx, _dict, pool); return &ctx->ctx; } static void file_dict_apply_changes(struct dict_transaction_memory_context *ctx, bool *atomic_inc_not_found_r) { struct file_dict *dict = (struct file_dict *)ctx->ctx.dict; const char *tmp; char *key, *value, *old_value; char *orig_key, *orig_value; const struct dict_transaction_memory_change *change; size_t new_len; long long diff; array_foreach(&ctx->changes, change) { if (hash_table_lookup_full(dict->hash, change->key, &orig_key, &orig_value)) { key = orig_key; old_value = orig_value; } else { key = NULL; old_value = NULL; } value = NULL; switch (change->type) { case DICT_CHANGE_TYPE_INC: if (old_value == NULL) { *atomic_inc_not_found_r = TRUE; break; } if (str_to_llong(old_value, &diff) < 0) i_unreached(); diff += change->value.diff; tmp = t_strdup_printf("%lld", diff); new_len = strlen(tmp); if (old_value == NULL || new_len > strlen(old_value)) value = p_strdup(dict->hash_pool, tmp); else { memcpy(old_value, tmp, new_len + 1); value = old_value; } /* fall through */ case DICT_CHANGE_TYPE_SET: if (key == NULL) key = p_strdup(dict->hash_pool, change->key); if (value == NULL) { value = p_strdup(dict->hash_pool, change->value.str); } hash_table_update(dict->hash, key, value); break; case DICT_CHANGE_TYPE_UNSET: if (old_value != NULL) hash_table_remove(dict->hash, key); break; } } } static int fd_copy_stat_permissions(const struct stat *src_st, int dest_fd, const char *dest_path, const char **error_r) { struct stat dest_st; if (fstat(dest_fd, &dest_st) < 0) { *error_r = t_strdup_printf("fstat(%s) failed: %m", dest_path); return -1; } if (src_st->st_gid != dest_st.st_gid && ((src_st->st_mode & 0070) >> 3 != (src_st->st_mode & 0007))) { /* group has different permissions from world. preserve the group. */ if (fchown(dest_fd, (uid_t)-1, src_st->st_gid) < 0) { *error_r = t_strdup_printf("fchown(%s, -1, %s) failed: %m", dest_path, dec2str(src_st->st_gid)); return -1; } } if ((src_st->st_mode & 07777) != (dest_st.st_mode & 07777)) { if (fchmod(dest_fd, src_st->st_mode & 07777) < 0) { *error_r = t_strdup_printf("fchmod(%s, %o) failed: %m", dest_path, (int)(src_st->st_mode & 0777)); return -1; } } return 0; } static int fd_copy_permissions(int src_fd, const char *src_path, int dest_fd, const char *dest_path, const char **error_r) { struct stat src_st; if (fstat(src_fd, &src_st) < 0) { *error_r = t_strdup_printf("fstat(%s) failed: %m", src_path); return -1; } return fd_copy_stat_permissions(&src_st, dest_fd, dest_path, error_r); } static int fd_copy_parent_dir_permissions(const char *src_path, int dest_fd, const char *dest_path, const char **error_r) { struct stat src_st; const char *src_dir, *p; p = strrchr(src_path, '/'); if (p == NULL) src_dir = "."; else src_dir = t_strdup_until(src_path, p); if (stat(src_dir, &src_st) < 0) { *error_r = t_strdup_printf("stat(%s) failed: %m", src_dir); return -1; } src_st.st_mode &= 0666; return fd_copy_stat_permissions(&src_st, dest_fd, dest_path, error_r); } static int file_dict_mkdir(struct file_dict *dict, const char **error_r) { const char *path, *p, *root; struct stat st; mode_t mode = 0700; p = strrchr(dict->path, '/'); if (p == NULL) return 0; path = t_strdup_until(dict->path, p); if (stat_first_parent(path, &root, &st) < 0) { if (errno == EACCES) *error_r = eacces_error_get("stat", root); else *error_r = t_strdup_printf("stat(%s) failed: %m", root); return -1; } if ((st.st_mode & S_ISGID) != 0) { /* preserve parent's permissions when it has setgid bit */ mode = st.st_mode; } if (mkdir_parents(path, mode) < 0 && errno != EEXIST) { if (errno == EACCES) *error_r = eacces_error_get("mkdir_parents", path); else *error_r = t_strdup_printf("mkdir_parents(%s) failed: %m", path); return -1; } return 0; } static int file_dict_lock(struct file_dict *dict, struct file_lock **lock_r, const char **error_r) { int ret; const char *error; if (file_dict_open_latest(dict, error_r) < 0) return -1; if (dict->fd == -1) { /* quota file doesn't exist yet, we need to create it */ dict->fd = open(dict->path, O_CREAT | O_RDWR, 0600); if (dict->fd == -1 && errno == ENOENT) { if (file_dict_mkdir(dict, error_r) < 0) return -1; dict->fd = open(dict->path, O_CREAT | O_RDWR, 0600); } if (dict->fd == -1) { if (errno == EACCES) *error_r = eacces_error_get("creat", dict->path); else { *error_r = t_strdup_printf( "creat(%s) failed: %m", dict->path); } return -1; } if (fd_copy_parent_dir_permissions(dict->path, dict->fd, dict->path, &error) < 0) e_error(dict->dict.event, "%s", error); } *lock_r = NULL; struct file_lock_settings lock_set = { .lock_method = dict->lock_method, }; do { file_lock_free(lock_r); if (file_wait_lock(dict->fd, dict->path, F_WRLCK, &lock_set, file_dict_dotlock_settings.timeout, lock_r, &error) <= 0) { *error_r = t_strdup_printf( "file_wait_lock(%s) failed: %s", dict->path, error); return -1; } /* check again if we need to reopen the file because it was just replaced */ } while ((ret = file_dict_open_latest(dict, error_r)) > 0); return ret < 0 ? -1 : 0; } static int file_dict_write_changes(struct dict_transaction_memory_context *ctx, bool *atomic_inc_not_found_r, const char **error_r) { struct file_dict *dict = (struct file_dict *)ctx->ctx.dict; struct dotlock *dotlock = NULL; struct file_lock *lock = NULL; const char *temp_path = NULL; const char *error; struct hash_iterate_context *iter; struct ostream *output; char *key, *value; string_t *str; int fd = -1; *atomic_inc_not_found_r = FALSE; if (file_dict_ensure_path_home_dir(dict, ctx->ctx.set.home_dir, error_r) < 0) return -1; switch (dict->lock_method) { case FILE_LOCK_METHOD_FCNTL: case FILE_LOCK_METHOD_FLOCK: if (file_dict_lock(dict, &lock, error_r) < 0) return -1; temp_path = t_strdup_printf("%s.tmp", dict->path); fd = creat(temp_path, 0600); if (fd == -1) { *error_r = t_strdup_printf( "dict-file: creat(%s) failed: %m", temp_path); file_unlock(&lock); return -1; } break; case FILE_LOCK_METHOD_DOTLOCK: fd = file_dotlock_open(&file_dict_dotlock_settings, dict->path, 0, &dotlock); if (fd == -1 && errno == ENOENT) { if (file_dict_mkdir(dict, error_r) < 0) return -1; fd = file_dotlock_open(&file_dict_dotlock_settings, dict->path, 0, &dotlock); } if (fd == -1) { *error_r = t_strdup_printf( "dict-file: file_dotlock_open(%s) failed: %m", dict->path); return -1; } temp_path = file_dotlock_get_lock_path(dotlock); break; } /* refresh once more now that we're locked */ if (file_dict_refresh(dict, error_r) < 0) { if (dotlock != NULL) file_dotlock_delete(&dotlock); else { i_close_fd(&fd); file_unlock(&lock); } return -1; } if (dict->fd != -1) { /* preserve the permissions */ if (fd_copy_permissions(dict->fd, dict->path, fd, temp_path, &error) < 0) e_error(ctx->ctx.event, "%s", error); } else { /* get initial permissions from parent directory */ if (fd_copy_parent_dir_permissions(dict->path, fd, temp_path, &error) < 0) e_error(ctx->ctx.event, "%s", error); } file_dict_apply_changes(ctx, atomic_inc_not_found_r); output = o_stream_create_fd(fd, 0); o_stream_cork(output); iter = hash_table_iterate_init(dict->hash); str = t_str_new(256); while (hash_table_iterate(iter, dict->hash, &key, &value)) { str_truncate(str, 0); str_append_tabescaped(str, key); str_append_c(str, '\n'); str_append_tabescaped(str, value); str_append_c(str, '\n'); o_stream_nsend(output, str_data(str), str_len(str)); } hash_table_iterate_deinit(&iter); if (o_stream_finish(output) <= 0) { *error_r = t_strdup_printf("write(%s) failed: %s", temp_path, o_stream_get_error(output)); o_stream_destroy(&output); if (dotlock != NULL) file_dotlock_delete(&dotlock); else { i_close_fd(&fd); file_unlock(&lock); } return -1; } o_stream_destroy(&output); if (dotlock != NULL) { if (file_dotlock_replace(&dotlock, DOTLOCK_REPLACE_FLAG_DONT_CLOSE_FD) < 0) { *error_r = t_strdup_printf("file_dotlock_replace() failed: %m"); i_close_fd(&fd); return -1; } } else { if (rename(temp_path, dict->path) < 0) { *error_r = t_strdup_printf("rename(%s, %s) failed: %m", temp_path, dict->path); file_unlock(&lock); i_close_fd(&fd); return -1; } /* dict->fd is locked, not the new fd. We're closing dict->fd so we can just free the lock struct. */ file_lock_free(&lock); } i_close_fd(&dict->fd); dict->fd = fd; return 0; } static void file_dict_transaction_commit(struct dict_transaction_context *_ctx, bool async ATTR_UNUSED, dict_transaction_commit_callback_t *callback, void *context) { struct dict_transaction_memory_context *ctx = (struct dict_transaction_memory_context *)_ctx; struct dict_commit_result result; bool atomic_inc_not_found; i_zero(&result); if (file_dict_write_changes(ctx, &atomic_inc_not_found, &result.error) < 0) result.ret = DICT_COMMIT_RET_FAILED; else if (atomic_inc_not_found) result.ret = DICT_COMMIT_RET_NOTFOUND; else result.ret = DICT_COMMIT_RET_OK; pool_unref(&ctx->pool); callback(&result, context); } struct dict dict_driver_file = { .name = "file", { .init = file_dict_init, .deinit = file_dict_deinit, .lookup = file_dict_lookup, .iterate_init = file_dict_iterate_init, .iterate = file_dict_iterate, .iterate_deinit = file_dict_iterate_deinit, .transaction_init = file_dict_transaction_init, .transaction_commit = file_dict_transaction_commit, .transaction_rollback = dict_transaction_memory_rollback, .set = dict_transaction_memory_set, .unset = dict_transaction_memory_unset, .atomic_inc = dict_transaction_memory_atomic_inc, } };