From 126feee4fd2b2d29031d2191deeddc3e52b2c969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BChler?= Date: Sun, 12 Sep 2010 13:46:43 +0200 Subject: [PATCH] [mod_auth]: Reload password file after changes, added ttl parameter --- src/modules/mod_auth.c | 256 +++++++++++++++++++++++++++++------------ 1 file changed, 180 insertions(+), 76 deletions(-) diff --git a/src/modules/mod_auth.c b/src/modules/mod_auth.c index 3585087..0182a76 100644 --- a/src/modules/mod_auth.c +++ b/src/modules/mod_auth.c @@ -22,19 +22,21 @@ * auth.debug = ; * - if set, debug information is written to the log * Actions: - * auth.plain ["method": method, "realm": realm, "file": path]; + * auth.plain ["method": method, "realm": realm, "file": path, "ttl": 10]; * - requires authentication using a plaintext file containing user:password pairs seperated by newlines (\n) - * auth.htpasswd ["method": method, "realm": realm, "file": path]; + * auth.htpasswd ["method": method, "realm": realm, "file": path, "ttl": 10]; * - requires authentication using a htpasswd file containing user:encrypted_password pairs seperated by newlines (\n) * - passwords are encrypted using crypt(3), use the htpasswd binary from apache to manage the file * + hashes starting with "$apr1$" are NOT supported (htpasswd -m) * + hashes starting with "{SHA}" ARE supported (followed by sha1_base64(password), htpasswd -s) * - only supports "basic" method - * auth.htdigest ["method": method, "realm": realm, "file": path]; + * auth.htdigest ["method": method, "realm": realm, "file": path, "ttl": 10]; * - requires authentication using a htdigest file containing user:realm:hashed_password tuples seperated by newlines (\n) * - passwords are saved as (modified) md5 hashes: * md5hex(username + ":" + realm + ":" + password) * + * ttl specifies how often lighty checks the files for modifications (in seconds), 0 means it will never check after the first load. + * * auth.deny; * - handles request with "401 Unauthorized" * @@ -51,8 +53,6 @@ * * Todo: * - method: digest - * - anti bruteforce protection - * - auth.deny ( using env[] "REMOTE_METHOD"/"REMOTE_REALM"/...? ) * * Author: * Copyright (c) 2009 Thomas Porzelt @@ -86,26 +86,38 @@ struct AuthBasicData { typedef struct AuthFileData AuthFileData; struct AuthFileData { - GString *path; - gboolean has_realm; + int refcount; GHashTable *users; /* doesn't use own strings, the strings are in contents */ gchar *contents; +}; + +typedef struct AuthFile AuthFile; +struct AuthFile { + GString *path; + gboolean has_realm; + + GMutex *lock; - ev_tstamp last_check; /* unused */ + AuthFileData *data; + ev_tstamp last_stat; + + gint ttl; + ev_tstamp next_check; /* unused */ }; -static gboolean auth_file_update(liServer *srv, AuthFileData *data) { +static AuthFileData* auth_file_load(liServer *srv, AuthFile *f) { GHashTable *users; gchar *contents; gchar *c; gchar *username, *password; GError *err = NULL; + AuthFileData *data = NULL; - if (!g_file_get_contents(data->path->str, &contents, NULL, &err)) { - ERROR(srv, "failed to load auth file \"%s\": %s", data->path->str, err->message); + if (!g_file_get_contents(f->path->str, &contents, NULL, &err)) { + ERROR(srv, "failed to load auth file \"%s\": %s", f->path->str, err->message); g_error_free(err); - return FALSE; + return NULL; } users = g_hash_table_new((GHashFunc) g_str_hash, (GEqualFunc) g_str_equal); @@ -135,98 +147,148 @@ static gboolean auth_file_update(liServer *srv, AuthFileData *data) { if (!password) { /* missing delimiter for user:pass => bogus file */ - ERROR(srv, "failed to parse auth file \"%s\", missing user:password delimiter", data->path->str); + ERROR(srv, "failed to parse auth file \"%s\", missing user:password delimiter", f->path->str); goto cleanup_fail; } /* file is of type htdigest (user:realm:pass) */ - if (data->has_realm && !found_realm) { + if (f->has_realm && !found_realm) { /* missing delimiter for realm:pass => bogus file */ - ERROR(srv, "failed to parse auth file \"%s\", missing realm:password delimiter", data->path->str); + ERROR(srv, "failed to parse auth file \"%s\", missing realm:password delimiter", f->path->str); goto cleanup_fail; } g_hash_table_insert(users, username, password); } - /* TODO: protect update with locks? */ - if (data->contents) { - g_free(data->contents); - g_hash_table_destroy(data->users); - } + data = g_slice_new(AuthFileData); + data->refcount = 1; data->contents = contents; data->users = users; - return TRUE; + return data; cleanup_fail: g_hash_table_destroy(users); g_free(contents); - return FALSE; + return NULL; } -static void auth_file_free(AuthFileData* data) { - g_string_free(data->path, TRUE); - if (data->contents) { - g_free(data->contents); - g_hash_table_destroy(data->users); - } +static void auth_file_data_release(AuthFileData *data) { + if (!data) return; + assert(g_atomic_int_get(&data->refcount) > 0); + if (!g_atomic_int_dec_and_test(&data->refcount)) return; + g_hash_table_destroy(data->users); + g_free(data->contents); g_slice_free(AuthFileData, data); } -static AuthFileData* auth_file_new(liServer *srv, const GString *path, gboolean has_realm) { - AuthFileData* data = g_slice_new0(AuthFileData); - data->path = g_string_new_len(GSTR_LEN(path)); - data->has_realm = has_realm; +static AuthFileData* auth_file_get_data(liWorker *wrk, AuthFile *f) { + ev_tstamp now = ev_now(wrk->loop); + AuthFileData *data = NULL; - if (!auth_file_update(srv, data)) { - auth_file_free(data); - return NULL; + g_mutex_lock(f->lock); + + if (f->ttl != 0 && now >= f->next_check) { + struct stat st; + f->next_check = now + f->ttl; + + if (-1 != stat(f->path->str, &st) && st.st_mtime >= f->last_stat - 1) { + g_mutex_unlock(f->lock); + + /* update without lock held */ + data = auth_file_load(wrk->srv, f); + + g_mutex_lock(f->lock); + + if (NULL != data) { + auth_file_data_release(f->data); + f->data = data; + } + } + + f->last_stat = now; } + data = f->data; + if (NULL != data) g_atomic_int_inc(&data->refcount); + + g_mutex_unlock(f->lock); + return data; } +static void auth_file_free(AuthFile* f) { + g_string_free(f->path, TRUE); + + auth_file_data_release(f->data); + g_mutex_free(f->lock); + + g_slice_free(AuthFile, f); +} + +static AuthFile* auth_file_new(liWorker *wrk, const GString *path, gboolean has_realm, gint ttl) { + AuthFile* f = g_slice_new0(AuthFile); + f->path = g_string_new_len(GSTR_LEN(path)); + f->has_realm = has_realm; + f->ttl = ttl; + f->next_check = ev_now(wrk->loop) + ttl; + + if (NULL == (f->data = auth_file_load(wrk->srv, f))) { + auth_file_free(f); + return NULL; + } + + return f; +} + static gboolean auth_backend_plain(liVRequest *vr, const GString *username, const GString *password, AuthBasicData *bdata) { const char *pass; - AuthFileData *afd = bdata->data; + AuthFileData *afd = auth_file_get_data(vr->wrk, bdata->data); + gboolean res = FALSE; - UNUSED(vr); + if (NULL == afd) return FALSE; /* unknown user? */ if (!(pass = g_hash_table_lookup(afd->users, username->str))) { - return FALSE; + goto out; } /* wrong password? */ if (!g_str_equal(password->str, pass)) { - return FALSE; + goto out; } - return TRUE; + res = TRUE; + +out: + auth_file_data_release(afd); + + return res; } static gboolean auth_backend_htpasswd(liVRequest *vr, const GString *username, const GString *password, AuthBasicData *bdata) { const char *pass; - AuthFileData *afd = bdata->data; + AuthFileData *afd = auth_file_get_data(vr->wrk, bdata->data); + gboolean res = FALSE; - UNUSED(vr); + if (NULL == afd) return FALSE; /* unknown user? */ if (!(pass = g_hash_table_lookup(afd->users, username->str))) { - return FALSE; + goto out; } if (g_str_has_prefix(pass, "$apr1$")) { /* We don't support this stupid method. Run around your house 1000 times and use sha1 next time */ - return FALSE; + goto out; } else if (g_str_has_prefix(pass, "{SHA}")) { li_apr_sha1_base64(vr->wrk->tmp_str, password); if (g_str_equal(password->str, vr->wrk->tmp_str->str)) { - return TRUE; + goto out; } } #ifdef HAVE_CRYPT_R @@ -238,25 +300,30 @@ static gboolean auth_backend_htpasswd(liVRequest *vr, const GString *username, c crypted = crypt_r(password->str, pass, &buffer); if (g_str_equal(pass, crypted)) { - return TRUE; + goto out; } } #endif - return FALSE; + res = TRUE; + +out: + auth_file_data_release(afd); + + return res; } static gboolean auth_backend_htdigest(liVRequest *vr, const GString *username, const GString *password, AuthBasicData *bdata) { const char *pass, *realm; - AuthFileData *afd = bdata->data; + AuthFileData *afd = auth_file_get_data(vr->wrk, bdata->data); GChecksum *md5sum; - gboolean res; + gboolean res = FALSE; - UNUSED(vr); + if (NULL == afd) return FALSE; /* unknown user? */ if (!(pass = g_hash_table_lookup(afd->users, username->str))) { - return FALSE; + goto out; } realm = pass; @@ -264,7 +331,7 @@ static gboolean auth_backend_htdigest(liVRequest *vr, const GString *username, c /* no realm/wrong realm? */ if (NULL == pass || 0 != strncmp(realm, bdata->realm->str, bdata->realm->len)) { - return FALSE; + goto out; } pass++; @@ -275,14 +342,16 @@ static gboolean auth_backend_htdigest(liVRequest *vr, const GString *username, c g_checksum_update(md5sum, CONST_USTR_LEN(":")); g_checksum_update(md5sum, GUSTR_LEN(password)); - res = TRUE; /* wrong password? */ - if (!g_str_equal(pass, g_checksum_get_string(md5sum))) { - res = FALSE; + if (g_str_equal(pass, g_checksum_get_string(md5sum))) { + res = TRUE; } g_checksum_free(md5sum); +out: + auth_file_data_release(afd); + return res; } @@ -375,7 +444,7 @@ static liHandlerResult auth_basic(liVRequest *vr, gpointer param, gpointer *cont static void auth_basic_free(liServer *srv, gpointer param) { AuthBasicData *bdata = param; - AuthFileData *afd = bdata->data; + AuthFile *afd = bdata->data; UNUSED(srv); @@ -385,25 +454,60 @@ static void auth_basic_free(liServer *srv, gpointer param) { g_slice_free(AuthBasicData, bdata); } -static liAction* auth_generic_create(liServer *srv, liPlugin* p, liValue *val, const char *actname, AuthBasicBackend basic_action, gboolean has_realm) { - AuthFileData *afd; - liValue *method, *realm, *file; - GString str; +/* auth option names */ +static const GString + aon_method = { CONST_STR_LEN("method"), 0 }, + aon_realm = { CONST_STR_LEN("realm"), 0 }, + aon_file = { CONST_STR_LEN("file"), 0 }, + aon_ttl = { CONST_STR_LEN("ttl"), 0 } +; +static liAction* auth_generic_create(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, const char *actname, AuthBasicBackend basic_action, gboolean has_realm) { + AuthFile *afd; + liValue *method = NULL, *realm = NULL, *file = NULL; + gint ttl = 10; - if (!val || val->type != LI_VALUE_HASH || g_hash_table_size(val->data.hash) != 3) { - ERROR(srv, "%s expects a hashtable with 3 elements: method, realm and file", actname); + GHashTableIter it; + gpointer pkey, pvalue; + + if (!val || val->type != LI_VALUE_HASH) { + ERROR(srv, "%s expects a hashtable with at least 3 elements: method, realm and file", actname); return NULL; } - str = li_const_gstring(CONST_STR_LEN("method")); - method = g_hash_table_lookup(val->data.hash, &str); - str = li_const_gstring(CONST_STR_LEN("realm")); - realm = g_hash_table_lookup(val->data.hash, &str); - str = li_const_gstring(CONST_STR_LEN("file")); - file = g_hash_table_lookup(val->data.hash, &str); + g_hash_table_iter_init(&it, val->data.hash); + while (g_hash_table_iter_next(&it, &pkey, &pvalue)) { + GString *key = pkey; + liValue *value = pvalue; + + if (g_string_equal(key, &aon_method)) { + if (value->type != LI_VALUE_STRING) { + ERROR(srv, "auth option '%s' expects string as parameter", aon_method.str); + return NULL; + } + method = value; + } else if (g_string_equal(key, &aon_realm)) { + if (value->type != LI_VALUE_STRING) { + ERROR(srv, "auth option '%s' expects string as parameter", aon_realm.str); + return NULL; + } + realm = value; + } else if (g_string_equal(key, &aon_file)) { + if (value->type != LI_VALUE_STRING) { + ERROR(srv, "auth option '%s' expects string as parameter", aon_file.str); + return NULL; + } + file = value; + } else if (g_string_equal(key, &aon_ttl)) { + if (value->type != LI_VALUE_NUMBER || value->data.number < 0) { + ERROR(srv, "auth option '%s' expects non-negative number as parameter", aon_ttl.str); + return NULL; + } + ttl = value->data.number; + } + } - if (!method || method->type != LI_VALUE_STRING || !realm || realm->type != LI_VALUE_STRING || !file || file->type != LI_VALUE_STRING) { + if (NULL == method || NULL == realm || NULL == file) { ERROR(srv, "%s expects a hashtable with 3 elements: method, realm and file", actname); return NULL; } @@ -419,7 +523,7 @@ static liAction* auth_generic_create(liServer *srv, liPlugin* p, liValue *val, c } /* load users from file */ - afd = auth_file_new(srv, file->data.string, has_realm); + afd = auth_file_new(wrk, file->data.string, has_realm, ttl); if (!afd) return FALSE; @@ -442,18 +546,18 @@ static liAction* auth_generic_create(liServer *srv, liPlugin* p, liValue *val, c static liAction* auth_plain_create(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) { - UNUSED(wrk); UNUSED(userdata); - return auth_generic_create(srv, p, val, "auth.plain", auth_backend_plain, FALSE); + UNUSED(srv); UNUSED(userdata); + return auth_generic_create(srv, wrk, p, val, "auth.plain", auth_backend_plain, FALSE); } static liAction* auth_htpasswd_create(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) { - UNUSED(wrk); UNUSED(userdata); - return auth_generic_create(srv, p, val, "auth.htpasswd", auth_backend_htpasswd, FALSE); + UNUSED(srv); UNUSED(userdata); + return auth_generic_create(srv, wrk, p, val, "auth.htpasswd", auth_backend_htpasswd, FALSE); } static liAction* auth_htdigest_create(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) { - UNUSED(wrk); UNUSED(userdata); - return auth_generic_create(srv, p, val, "auth.htdigest", auth_backend_htdigest, TRUE); + UNUSED(srv); UNUSED(userdata); + return auth_generic_create(srv, wrk, p, val, "auth.htdigest", auth_backend_htdigest, TRUE); } static liHandlerResult auth_handle_deny(liVRequest *vr, gpointer param, gpointer *context) {