diff options
Diffstat (limited to '')
-rw-r--r-- | src/auth/db-passwd-file.c | 493 |
1 files changed, 493 insertions, 0 deletions
diff --git a/src/auth/db-passwd-file.c b/src/auth/db-passwd-file.c new file mode 100644 index 0000000..e26c146 --- /dev/null +++ b/src/auth/db-passwd-file.c @@ -0,0 +1,493 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" + +#if defined (USERDB_PASSWD_FILE) || defined(PASSDB_PASSWD_FILE) + +#include "userdb.h" +#include "db-passwd-file.h" + +#include "array.h" +#include "buffer.h" +#include "istream.h" +#include "hash.h" +#include "str.h" +#include "eacces-error.h" +#include "ioloop.h" + +#include <unistd.h> +#include <fcntl.h> +#include <time.h> +#include <sys/stat.h> + +#define PARSE_TIME_STARTUP_WARN_SECS 60 +#define PARSE_TIME_RELOAD_WARN_SECS 10 + +static struct db_passwd_file *passwd_files; + +static void ATTR_NULL(3) +passwd_file_add(struct passwd_file *pw, const char *username, + const char *pass, const char *const *args) +{ + /* args = uid, gid, user info, home dir, shell, extra_fields */ + struct passwd_user *pu; + const char *extra_fields = NULL; + char *user; + size_t len; + + if (hash_table_lookup(pw->users, username) != NULL) { + e_error(pw->event, "User %s exists more than once", username); + return; + } + + pu = p_new(pw->pool, struct passwd_user, 1); + user = p_strdup(pw->pool, username); + + len = pass == NULL ? 0 : strlen(pass); + if (len > 4 && pass[0] != '{' && pass[0] != '$' && + pass[len-1] == ']' && pass[len-4] == '[') { + /* password[type] - we're being libpam-pwdfile compatible + here. it uses 13 = DES and 34 = MD5. For backwards + compatibility with ourself, we have also 56 = Digest-MD5. */ + int num = (pass[len-3] - '0') * 10 + (pass[len-2] - '0'); + + pass = t_strndup(pass, len-4); + if (num == 34) { + pu->password = p_strconcat(pw->pool, "{PLAIN-MD5}", + pass, NULL); + } else if (num == 56) { + pu->password = p_strconcat(pw->pool, "{DIGEST-MD5}", + pass, NULL); + if (strlen(pu->password) != 32 + 12) { + e_error(pw->event, "User %s " + "has invalid password", username); + return; + } + } else { + pu->password = p_strconcat(pw->pool, "{CRYPT}", + pass, NULL); + } + } else { + pu->password = p_strdup(pw->pool, pass); + } + + pu->uid = (uid_t)-1; + pu->gid = (gid_t)-1; + + if (*args == NULL) + ; + else if (!pw->db->userdb || **args == '\0') { + args++; + } else { + pu->uid = userdb_parse_uid(NULL, *args); + if (pu->uid == 0 || pu->uid == (uid_t)-1) { + e_error(pw->event, "User %s has invalid UID '%s'", + username, *args); + return; + } + args++; + } + + if (*args == NULL) { + if (pw->db->userdb_warn_missing) { + e_error(pw->event, "User %s is missing userdb info", + username); + } + /* don't allow userdb lookups */ + pu->uid = 0; + pu->gid = 0; + } else if (!pw->db->userdb || **args == '\0') + args++; + else { + pu->gid = userdb_parse_gid(NULL, *args); + if (pu->gid == 0 || pu->gid == (gid_t)-1) { + e_error(pw->event, "User %s has invalid GID '%s'", + username, *args); + return; + } + args++; + } + + /* user info */ + if (*args != NULL) + args++; + + /* home */ + if (*args != NULL) { + if (pw->db->userdb) + pu->home = p_strdup_empty(pw->pool, *args); + args++; + } + + /* shell */ + if (*args != NULL) + args++; + + if (*args != NULL && **args == '\0') { + /* old format, this field is empty and next field may + contain MAIL */ + args++; + if (*args != NULL && **args != '\0' && pw->db->userdb) { + extra_fields = + t_strconcat("userdb_mail=", + t_strarray_join(args, ":"), NULL); + } + } else if (*args != NULL) { + /* new format, contains a space separated list of + extra fields */ + extra_fields = t_strarray_join(args, ":"); + } + + if (extra_fields != NULL) { + pu->extra_fields = + p_strsplit_spaces(pw->pool, extra_fields, " "); + } + + hash_table_insert(pw->users, user, pu); +} + +static struct passwd_file * +passwd_file_new(struct db_passwd_file *db, const char *expanded_path) +{ + struct passwd_file *pw; + + pw = i_new(struct passwd_file, 1); + pw->db = db; + pw->path = i_strdup(expanded_path); + pw->fd = -1; + pw->event = event_create(db->event); + event_set_append_log_prefix(pw->event, + t_strdup_printf("passwd-file %s:", pw->path)); + + if (hash_table_is_created(db->files)) + hash_table_insert(db->files, pw->path, pw); + return pw; +} + +static int passwd_file_open(struct passwd_file *pw, bool startup, + const char **error_r) +{ + const char *no_args = NULL; + struct istream *input; + const char *line; + struct stat st; + time_t start_time, end_time; + unsigned int time_secs; + int fd; + + fd = open(pw->path, O_RDONLY); + if (fd == -1) { + if (errno == EACCES) + *error_r = eacces_error_get("open", pw->path); + else { + *error_r = t_strdup_printf("open(%s) failed: %m", + pw->path); + } + return -1; + } + + if (fstat(fd, &st) != 0) { + *error_r = t_strdup_printf("fstat(%s) failed: %m", + pw->path); + i_close_fd(&fd); + return -1; + } + + pw->fd = fd; + pw->stamp = st.st_mtime; + pw->size = st.st_size; + + pw->pool = pool_alloconly_create(MEMPOOL_GROWING"passwd_file", 10240); + hash_table_create(&pw->users, pw->pool, 0, str_hash, strcmp); + + start_time = time(NULL); + input = i_stream_create_fd(pw->fd, SIZE_MAX); + i_stream_set_return_partial_line(input, TRUE); + while ((line = i_stream_read_next_line(input)) != NULL) { + if (*line == '\0' || *line == ':' || *line == '#') + continue; /* no username or comment */ + + T_BEGIN { + const char *const *args = t_strsplit(line, ":"); + if (args[1] != NULL) { + /* at least username+password */ + passwd_file_add(pw, args[0], args[1], args+2); + } else { + /* only username */ + passwd_file_add(pw, args[0], NULL, &no_args); + } + } T_END; + } + i_stream_destroy(&input); + end_time = time(NULL); + time_secs = end_time - start_time; + + if ((time_secs > PARSE_TIME_STARTUP_WARN_SECS && startup) || + (time_secs > PARSE_TIME_RELOAD_WARN_SECS && !startup)) { + e_warning(pw->event, "Reading %u users took %u secs", + hash_table_count(pw->users), time_secs); + } else { + e_debug(pw->event, "Read %u users in %u secs", + hash_table_count(pw->users), time_secs); + } + return 0; +} + +static void passwd_file_close(struct passwd_file *pw) +{ + i_close_fd_path(&pw->fd, pw->path); + + hash_table_destroy(&pw->users); + pool_unref(&pw->pool); +} + +static void passwd_file_free(struct passwd_file *pw) +{ + if (hash_table_is_created(pw->db->files)) + hash_table_remove(pw->db->files, pw->path); + + passwd_file_close(pw); + event_unref(&pw->event); + i_free(pw->path); + i_free(pw); +} + +static int passwd_file_sync(struct auth_request *request, + struct passwd_file *pw) +{ + struct stat st; + const char *error; + + if (pw->last_sync_time == ioloop_time) + return hash_table_is_created(pw->users) ? 1 : -1; + pw->last_sync_time = ioloop_time; + + if (stat(pw->path, &st) < 0) { + /* with variables don't give hard errors, or errors about + nonexistent files */ + int ret = -1; + + if (errno == EACCES) { + e_error(authdb_event(request), + "%s", eacces_error_get("stat", pw->path)); + } else if (errno == ENOENT) { + auth_request_log_info(request, "passwd-file", + "missing passwd file: %s", pw->path); + ret = 0; + } else { + e_error(authdb_event(request), + "stat(%s) failed: %m", pw->path); + } + + if (pw->db->default_file != pw) + passwd_file_free(pw); + return ret; + } + + if (st.st_mtime != pw->stamp || st.st_size != pw->size) { + passwd_file_close(pw); + if (passwd_file_open(pw, FALSE, &error) < 0) { + e_error(authdb_event(request), + "%s", error); + return -1; + } + } + return 1; +} + +static struct db_passwd_file *db_passwd_file_find(const char *path) +{ + struct db_passwd_file *f; + + for (f = passwd_files; f != NULL; f = f->next) { + if (strcmp(f->path, path) == 0) + return f; + } + + return NULL; +} + +static void db_passwd_file_set_userdb(struct db_passwd_file *db) +{ + db->userdb = TRUE; + /* warn about missing userdb fields only when there aren't any other + userdbs. */ + db->userdb_warn_missing = + array_is_created(&global_auth_settings->userdbs) && + array_count(&global_auth_settings->userdbs) == 1; +} + +struct db_passwd_file * +db_passwd_file_init(const char *path, bool userdb, bool debug) +{ + struct db_passwd_file *db; + const char *p; + bool percents = FALSE; + + db = db_passwd_file_find(path); + if (db != NULL) { + db->refcount++; + if (userdb) + db_passwd_file_set_userdb(db); + return db; + } + + db = i_new(struct db_passwd_file, 1); + db->refcount = 1; + if (userdb) + db_passwd_file_set_userdb(db); + db->event = event_create(auth_event); + event_set_forced_debug(db->event, debug); + + for (p = path; *p != '\0'; p++) { + if (*p == '%' && p[1] != '\0') { + if (var_get_key(++p) == '%') + percents = TRUE; + else + db->vars = TRUE; + } + } + + if (percents && !db->vars) { + /* just extra escaped % chars. remove them. */ + struct var_expand_table empty_table[1] = { + { .key = '\0' }, + }; + string_t *dest; + const char *error; + + dest = t_str_new(256); + if (var_expand(dest, path, empty_table, &error) <= 0) + i_unreached(); + path = str_c(dest); + } + + db->path = i_strdup(path); + if (db->vars) { + hash_table_create(&db->files, default_pool, 0, + str_hash, strcmp); + } else { + db->default_file = passwd_file_new(db, path); + } + + db->next = passwd_files; + passwd_files = db; + return db; +} + +void db_passwd_file_parse(struct db_passwd_file *db) +{ + const char *error; + + if (db->default_file != NULL && db->default_file->stamp == 0) { + /* no variables, open the file immediately */ + if (passwd_file_open(db->default_file, TRUE, &error) < 0) + e_error(db->default_file->event, "%s", error); + } +} + +void db_passwd_file_unref(struct db_passwd_file **_db) +{ + struct db_passwd_file *db = *_db; + struct db_passwd_file **p; + struct hash_iterate_context *iter; + char *path; + struct passwd_file *file; + + *_db = NULL; + i_assert(db->refcount >= 0); + if (--db->refcount > 0) + return; + + for (p = &passwd_files; *p != NULL; p = &(*p)->next) { + if (*p == db) { + *p = db->next; + break; + } + } + + if (db->default_file != NULL) + passwd_file_free(db->default_file); + else { + iter = hash_table_iterate_init(db->files); + while (hash_table_iterate(iter, db->files, &path, &file)) + passwd_file_free(file); + hash_table_iterate_deinit(&iter); + hash_table_destroy(&db->files); + } + event_unref(&db->event); + i_free(db->path); + i_free(db); +} + +static const char * +path_fix(const char *path, + const struct auth_request *auth_request ATTR_UNUSED) +{ + const char *p; + + p = strchr(path, '/'); + if (p == NULL) + return path; + + /* most likely this is an invalid request. just cut off the '/' and + everything after it. */ + return t_strdup_until(path, p); +} + +int db_passwd_file_lookup(struct db_passwd_file *db, + struct auth_request *request, + const char *username_format, + struct passwd_user **user_r) +{ + struct passwd_file *pw; + string_t *username, *dest; + const char *error; + int ret; + + if (!db->vars) + pw = db->default_file; + else { + dest = t_str_new(256); + if (auth_request_var_expand(dest, db->path, request, path_fix, + &error) <= 0) { + e_error(authdb_event(request), + "Failed to expand passwd-file path %s: %s", + db->path, error); + return -1; + } + + pw = hash_table_lookup(db->files, str_c(dest)); + if (pw == NULL) { + /* doesn't exist yet. create lookup for it. */ + pw = passwd_file_new(db, str_c(dest)); + } + } + + if ((ret = passwd_file_sync(request, pw)) <= 0) { + /* pw may be freed now */ + return ret; + } + + username = t_str_new(256); + if (auth_request_var_expand(username, username_format, request, + auth_request_str_escape, &error) <= 0) { + e_error(authdb_event(request), + "Failed to expand username_format=%s: %s", + username_format, error); + return -1; + } + + e_debug(authdb_event(request), + "lookup: user=%s file=%s", + str_c(username), pw->path); + + *user_r = hash_table_lookup(pw->users, str_c(username)); + if (*user_r == NULL) { + auth_request_log_unknown_user(request, AUTH_SUBSYS_DB); + return 0; + } + return 1; +} + +#endif |