diff --git a/src/modules/mod_auth.c b/src/modules/mod_auth.c new file mode 100644 index 0000000..1805b70 --- /dev/null +++ b/src/modules/mod_auth.c @@ -0,0 +1,336 @@ +/* + * mod_auth - require authentication from clients using username + password + * + * Description: + * mod_auth lets you require authentication from clients using a username and password. + * It supports basic and digest authentication methods as well as plaintext, htpass and htdigest backends. + * + * Basic: + * The "basic" method transfers the username and the password in cleartext over the network (base64 encoded) + * and might result in security problems if not used in conjunction with a crypted channel between client and server. + * It is recommend to use https in conjunction with basic authentication. + * + * Digest: + * The "digest" method only transfers a hashed value over the network which performs a lot of work to harden + * the authentication process in insecure networks (like the internet). + * + * Relevant RFCs: 2617 + * + * Setups: + * none + * Options: + * auth.debug = ; + * - if set, debug information is written to the log + * Actions: + * auth.plain ["method": method, "realm": realm, "file": path]; + * - requires authentication using a plaintext file containing user:password pairs seperated by newlines (\n) + * auth.htpasswd ["realm": realm, "file": path]; + * - requires authentication using a plaintext 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 + * - only supports "basic" method + * auth.htdigest ["method": method, "realm": realm, "file": path]; + * - requires authentication using a plaintext file containing user:realm:hashed_password tuples seperated by newlines (\n) + * - passwords are saved as (modified) md5 hashes + * + * Example config: + * # /members/ is for known users only + * if request.path =^ "/members/" { + * auth ["method": "basic", "realm": "members only", "file": "/etc/lighttpd/users.txt"]; + * } + * + * + * Tip: + * The digest method is broken in Internet Explorer < 7. Use basic instead if this is a problem for you. + * + * Todo: + * - method: digest + * - auth.htdigest + * - auth.htpasswd + * - anti bruteforce protection + * + * Author: + * Copyright (c) 2009 Thomas Porzelt + * License: + * MIT, see COPYING file in the lighttpd 2 tree + */ + +#include +#include + +LI_API gboolean mod_auth_init(liModules *mods, liModule *mod); +LI_API gboolean mod_auth_free(liModules *mods, liModule *mod); + +typedef gboolean (*AuthBackend)(liVRequest *vr, const gchar *auth_info, gpointer param); + +struct AuthData { + liPlugin *p; + GString *realm; + AuthBackend backend; + gpointer data; +}; +typedef struct AuthData AuthData; + +struct AuthFileData { + GString *path; + GHashTable *users; + ev_tstamp last_check; +}; +typedef struct AuthFileData AuthFileData; + +static GHashTable *auth_file_load(liServer *srv, GString *path, gboolean has_realm) { + GHashTable *users; + gchar *contents; + gsize len; + gchar *c; + gchar *user_start, *user_end; + GString *user, *pass; + GError *err = NULL; + + + if (!g_file_get_contents(path->str, &contents, &len, &err)) { + ERROR(srv, "failed to load auth file \"%s\": %s", path->str, err->message); + g_error_free(err); + return NULL; + } + + users = g_hash_table_new_full( + (GHashFunc) g_string_hash, (GEqualFunc) g_string_equal, + (GDestroyNotify) li_string_destroy_notify, (GDestroyNotify) li_string_destroy_notify + ); + + /* parse file */ + user_start = contents; + for (c = strchr(contents, ':'); c != NULL; c = strchr(c, ':')) { + if (has_realm) { + /* file is of type htdigest (user:realm:pass) */ + c = strchr(c + 1, ':'); + + if (!c) { + /* missing delimiter for realm:pass => bogus file */ + ERROR(srv, "failed to parse auth file \"%s\", doesn't look like a htdigest file", path->str); + g_hash_table_destroy(users); + return NULL; + } + } + + user_end = c - 1; + c = strchr(c + 1, '\n'); + + if (!c) { + /* missing \n */ + ERROR(srv, "failed to parse auth file \"%s\"", path->str); + g_hash_table_destroy(users); + return NULL; + } + + user = g_string_new_len(user_start, user_end - user_start + 1); + pass = g_string_new_len(user_end + 2, c - user_end - 2); + g_hash_table_insert(users, user, pass); + + c++; + user_start = c; + } + + /* c == NULL, last check if we are really at the end of the file */ + if (*user_start) { + ERROR(srv, "failed to parse auth file \"%s\"", path->str); + g_hash_table_destroy(users); + return NULL; + } + + return users; +} + +static gboolean auth_backend_plain(liVRequest *vr, const gchar *auth_info, gpointer param) { + gchar *decoded; + gsize len; + gchar *c; + GString user; + GString *pass; + AuthData *ad = param; + + UNUSED(vr); + + /* auth_info contains username:password encoded in base64 */ + if (!(decoded = (gchar*)g_base64_decode(auth_info, &len))) + return FALSE; + + /* bogus data? */ + if (!(c = strchr(decoded, ':'))) { + g_free(decoded); + return FALSE; + } + + user.str = decoded; + user.len = c - decoded; + user.allocated_len = 0; + + /* unknown user? */ + if (!(pass = g_hash_table_lookup(ad->data, &user))) { + g_free(decoded); + return FALSE; + } + + /* wrong password? */ + if (!g_str_equal(c+1, pass->str)) { + g_free(decoded); + return FALSE; + } + + g_free(decoded); + + return TRUE; +} + +static liHandlerResult auth_basic(liVRequest *vr, gpointer param, gpointer *context) { + liHttpHeader *hdr; + gboolean auth_ok = TRUE; + AuthData *ad = param; + gboolean debug = _OPTION(vr, ad->p, 0).boolean; + + UNUSED(context); + + /* check for Authorization header */ + hdr = li_http_header_lookup(vr->request.headers, CONST_STR_LEN("Authorization")); + + if (!hdr || !g_str_has_prefix(HEADER_VALUE(hdr), "Basic ")) { + auth_ok = FALSE; + + if (debug) + VR_DEBUG(vr, "requesting authorization from client for realm \"%s\"", ad->realm->str); + } else if (!ad->backend(vr, HEADER_VALUE(hdr) + sizeof("Basic ") - 1, ad)) { + auth_ok = FALSE; + + if (debug) + VR_DEBUG(vr, "wrong authorization info from client for realm \"%s\"", ad->realm->str); + } + + if (!auth_ok) { + /* if the request already has a handler like mod_access, we assume everything is ok */ + if (!li_vrequest_handle_direct(vr)) + return LI_HANDLER_GO_ON; + + vr->response.http_status = 401; + g_string_truncate(vr->wrk->tmp_str, 0); + g_string_append_len(vr->wrk->tmp_str, CONST_STR_LEN("Basic realm=\"")); + g_string_append_len(vr->wrk->tmp_str, GSTR_LEN(ad->realm)); + g_string_append_c(vr->wrk->tmp_str, '"'); + li_http_header_overwrite(vr->response.headers, CONST_STR_LEN("WWW-Authenticate"), GSTR_LEN(vr->wrk->tmp_str)); + + return LI_HANDLER_GO_ON; + } else if (debug) { + VR_DEBUG(vr, "client authorization successful for realm \"%s\"", ad->realm->str); + } + + return LI_HANDLER_GO_ON; +} + +static void auth_plain_free(liServer *srv, gpointer param) { + AuthData *ad = param; + + UNUSED(srv); + + g_string_free(ad->realm, TRUE); + g_hash_table_destroy(ad->data); + g_slice_free(AuthData, ad); +} + +static liAction* auth_plain_create(liServer *srv, liPlugin* p, liValue *val) { + AuthData *ad; + liValue *method, *realm, *file; + GString str; + GHashTable *users; + + + if (!val || val->type != LI_VALUE_HASH || g_hash_table_size(val->data.hash) != 3) { + ERROR(srv, "%s", "auth.plain expects a hashtable with 3 elements: method, realm and file"); + return NULL; + } + + str.allocated_len = 0; + str.str = "method"; + str.len = sizeof("method") - 1; + method = g_hash_table_lookup(val->data.hash, &str); + str.str = "realm"; + str.len = sizeof("realm") - 1; + realm = g_hash_table_lookup(val->data.hash, &str); + str.str = "file"; + str.len = sizeof("file") - 1; + file = g_hash_table_lookup(val->data.hash, &str); + + if (!method || method->type != LI_VALUE_STRING || !realm || realm->type != LI_VALUE_STRING || !file || file->type != LI_VALUE_STRING) { + ERROR(srv, "%s", "auth.plain expects a hashtable with 3 elements: method, realm and file"); + return NULL; + } + + if (!g_str_equal(method->data.string->str, "basic") && !g_str_equal(method->data.string->str, "digest")) { + ERROR(srv, "auth.plain: unknown method: %s", method->data.string->str); + return NULL; + } + + if (g_str_equal(method->data.string->str, "digest")) { + ERROR(srv, "%s", "auth.plain: digest authentication not implemented yet"); + return NULL; + } + + /* load users from file */ + users = auth_file_load(srv, file->data.string, FALSE); + + if (!users) + return FALSE; + + ad = g_slice_new(AuthData); + ad->p = p; + ad->realm = li_value_extract(realm).string; + ad->backend = auth_backend_plain; + ad->data = users; + + if (g_str_equal(method->data.string->str, "basic")) + return li_action_new_function(auth_basic, NULL, auth_plain_free, ad); + else + return NULL; /* li_action_new_function(NULL, NULL, auth_backend_plain_free, ad); */ +} + + + +static const liPluginOption options[] = { + { "auth.debug", LI_VALUE_BOOLEAN, NULL, NULL, NULL }, + + { NULL, 0, NULL, NULL, NULL } +}; + +static const liPluginAction actions[] = { + { "auth.plain", auth_plain_create }, + + { NULL, NULL } +}; + +static const liPluginSetup setups[] = { + { NULL, NULL } +}; + +static void plugin_auth_init(liServer *srv, liPlugin *p) { + UNUSED(srv); + + p->options = options; + p->actions = actions; + p->setups = setups; +} + + +gboolean mod_auth_init(liModules *mods, liModule *mod) { + UNUSED(mod); + + MODULE_VERSION_CHECK(mods); + + mod->config = li_plugin_register(mods->main, "mod_auth", plugin_auth_init); + + return mod->config != NULL; +} + +gboolean mod_auth_free(liModules *mods, liModule *mod) { + if (mod->config) + li_plugin_free(mods->main, mod->config); + + return TRUE; +} diff --git a/src/modules/wscript b/src/modules/wscript index 398790f..f43e850 100644 --- a/src/modules/wscript +++ b/src/modules/wscript @@ -9,15 +9,12 @@ License and Copyright: see COPYING file import Options, sys -def lighty_mod(bld, target, src, uselib = [], option = ''): - if option and not getattr(Options.options, option): - return - +def lighty_mod(bld, target, src, uselib = []): mod = bld.new_task_gen( features = 'cc cshlib', source = src, defines = ['HAVE_CONFIG_H=1'], - uselib = ['glib', 'gthread', 'gmodule', 'ev', 'lighty_mod'], + uselib = ['glib', 'gthread', 'gmodule', 'ev', 'lighty_mod'] + uselib, uselib_local = ['common'], includes = ['#/include/'], target = target,) @@ -30,8 +27,11 @@ def configure(conf): def build(bld): lighty_mod(bld, 'mod_access', 'mod_access.c') lighty_mod(bld, 'mod_accesslog', 'mod_accesslog.c') + lighty_mod(bld, 'mod_auth', 'mod_auth.c') lighty_mod(bld, 'mod_balancer', 'mod_balancer.c') lighty_mod(bld, 'mod_cache_disk_etag', 'mod_cache_disk_etag.c') + if bld.env['USE_ZLIB'] == 1: + lighty_mod(bld, 'mod_compress', 'mod_compress.c', uselib=['z']) lighty_mod(bld, 'mod_debug', 'mod_debug.c') lighty_mod(bld, 'mod_dirlist', 'mod_dirlist.c') lighty_mod(bld, 'mod_expire', 'mod_expire.c')