/* On some linux this is required to get crypt to show itself */ #define _XOPEN_SOURCE #include "usuals.h" #include "httpauthd.h" #include "defaults.h" #include "basic.h" #include "digest.h" #include "hash.h" #include "md5.h" #include #include #include unsigned char g_simple_secret[DIGEST_SECRET_LEN]; #define SIMPLE_MAXLINE 256 #define BASIC_ESTABLISHED (void*)1 /* ------------------------------------------------------------------------------- * Structures */ typedef struct simple_context { /* Settings ----------------------------------------------------------- */ const char* filename; /* The file name with the user names */ ha_options_t* opts; /* Options from httpauthd.c */ /* Context ----------------------------------------------------------- */ hash_t* cache; /* Some cached records or basic */ } simple_context_t; /* ------------------------------------------------------------------------------- * Internal Functions */ static void free_hash_object(void* arg, void* val) { if(val && val != BASIC_ESTABLISHED) free(val); } static digest_record_t* get_cached_digest(simple_context_t* ctx, unsigned char* nonce) { digest_record_t* rec; ASSERT(ctx && nonce); if(ctx->opts->cache_max == 0) return NULL; ha_lock(NULL); rec = (digest_record_t*)hash_get(ctx->cache, nonce); /* Just in case it's a basic :) */ if(rec && rec != BASIC_ESTABLISHED) hash_rem(ctx->cache, nonce); ha_unlock(NULL); ASSERT(!rec || memcmp(nonce, rec->nonce, DIGEST_NONCE_LEN) == 0); return rec; } static int have_cached_basic(simple_context_t* ctx, unsigned char* key) { int ret = 0; ASSERT(ctx && key); ha_lock(NULL); ret = (hash_get(ctx->cache, key) == BASIC_ESTABLISHED); ha_unlock(NULL); return ret; } static int save_cached_digest(simple_context_t* ctx, digest_record_t* rec) { int r; ASSERT(ctx && rec); if(ctx->opts->cache_max == 0) { free_hash_object(NULL, rec); return HA_FALSE; } ha_lock(NULL); while(hash_count(ctx->cache) >= ctx->opts->cache_max) hash_bump(ctx->cache); r = hash_set(ctx->cache, rec->nonce, rec); ha_unlock(NULL); if(!r) { free_hash_object(NULL, rec); ha_messagex(LOG_CRIT, "out of memory"); return HA_CRITERROR; } return HA_OK; } static int add_cached_basic(simple_context_t* ctx, unsigned char* key) { int r; ASSERT(ctx && key); if(ctx->opts->cache_max == 0) return HA_FALSE; ha_lock(NULL); while(hash_count(ctx->cache) >= ctx->opts->cache_max) hash_bump(ctx->cache); r = hash_set(ctx->cache, key, BASIC_ESTABLISHED); ha_unlock(NULL); if(!r) { ha_messagex(LOG_CRIT, "out of memory"); return HA_CRITERROR; } return HA_OK; } static int complete_digest_ha1(simple_context_t* ctx, digest_record_t* rec, ha_buffer_t* buf, const char* user) { FILE* f; char* t; char* t2; size_t len; char line[SIMPLE_MAXLINE]; int ret = HA_FALSE; ASSERT(ctx && rec && buf && user && user[0]); ha_messagex(LOG_DEBUG, "searching password file for user's ha1: %s", user); f = fopen(ctx->filename, "r"); if(!f) { ha_message(LOG_ERR, "simple: can't open file for basic auth: %s", ctx->filename); return HA_FAILED; } /* * Note: There should be no returns or jumps between * this point and the closing of the file below. */ /* Now look through the whole file */ while(!feof(f)) { fgets(line, SIMPLE_MAXLINE, f); if(ferror(f)) { ha_message(LOG_ERR, "simple: error reading basic password file"); ret = HA_FAILED; break; } t = strchr(line, ':'); if(t) { /* Split the line */ *t = 0; t++; /* Check the user */ if(strcmp(line, user) == 0) { /* Otherwise it might be a digest type ha1. */ t2 = strchr(t, ':'); if(t2) { *t2 = 0; t2++; /* Check the realm */ if(strcmp(t, ctx->opts->realm) == 0) { len = MD5_LEN; /* Now try antd decode the ha1 */ t = ha_bufdechex(buf, t2, &len); if(t && len == MD5_LEN) { ha_messagex(LOG_DEBUG, "simple: found ha1 for user: %s", user); memcpy(rec->ha1, t, MD5_LEN); ret = HA_OK; break; } } } if(!t2 || ret != HA_OK) ha_messagex(LOG_WARNING, "simple: user '%s' found in file, but password not in digest format", user); } } } fclose(f); if(ha_buferr(buf)) return HA_CRITERROR; return ret; } static int validate_user_password(simple_context_t* ctx, ha_buffer_t* buf, const char* user, const char* clearpw) { FILE* f; char line[SIMPLE_MAXLINE]; unsigned char ha1[MD5_LEN]; char* t; char* t2; size_t len; int ret = HA_FALSE; ASSERT(ctx && buf); ASSERT(user && user[0] && clearpw); ha_messagex(LOG_DEBUG, "simple: validating user against password file: %s", user); f = fopen(ctx->filename, "r"); if(!f) { ha_message(LOG_ERR, "simple: can't open file for basic auth: %s", ctx->filename); return HA_FAILED; } digest_makeha1(ha1, user, ctx->opts->realm, clearpw); /* * Note: There should be no returns or jumps between * this point and the closing of the file below. */ /* Now look through the whole file */ while(!feof(f)) { fgets(line, SIMPLE_MAXLINE, f); if(ferror(f)) { ha_message(LOG_ERR, "simple: error reading basic password file"); ret = HA_FAILED; break; } /* Take white space off end of line */ t = line + strlen(line); while(t != line && isspace(*(t - 1))) { *(t - 1) = 0; t--; } t = strchr(line, ':'); if(t) { /* Split the line */ *t = 0; t++; /* Check the user */ if(strcmp(line, user) == 0) { /* Not sure if crypt is thread safe so we lock */ ha_lock(NULL); /* Check the password */ t2 = crypt(clearpw, t); ha_unlock(NULL); if(strcmp(crypt(clearpw, t), t) == 0) { ha_messagex(LOG_DEBUG, "simple: found valid crypt password for user: %s", user); ret = HA_OK; break; } /* Otherwise it might be a digest type ha1. */ t2 = strchr(t, ':'); if(t2) { *t2 = 0; t2++; /* Check the realm */ if(strcmp(t, ctx->opts->realm) == 0) { len = MD5_LEN; /* Now try antd decode the ha1 */ t = ha_bufdechex(buf, t2, &len); if(t && len == MD5_LEN && memcmp(ha1, t, MD5_LEN) == 0) { ha_messagex(LOG_DEBUG, "simple: found valid ha1 for user: %s", user); ret = HA_OK; break; } } } if(ha_buferr(buf)) break; } } } fclose(f); if(ha_buferr(buf)) return HA_CRITERROR; return ret; } static int simple_basic_response(simple_context_t* ctx, const char* header, ha_response_t* resp, ha_buffer_t* buf) { basic_header_t basic; int ret = HA_FALSE; int found = 0; int r; ASSERT(buf && header && resp && buf); if((r = basic_parse(header, buf, &basic)) < 0) return r; /* Past this point we don't return directly */ /* Check and see if this connection is in the cache */ if(have_cached_basic(ctx, basic.key)) { ha_messagex(LOG_NOTICE, "simple: validated basic user against cache: %s", basic.user); found = 1; ret = HA_OK; goto finally; } /* If we have a user name and password */ if(!basic.user || !basic.user[0] || !basic.password || !basic.password[0]) goto finally; ret = validate_user_password(ctx, buf, basic.user, basic.password); if(ret == HA_OK) ha_messagex(LOG_NOTICE, "simple: validated basic user against file: %s", basic.user); else ha_messagex(LOG_WARNING, "simple: basic authentication failed for user: %s", basic.user); finally: if(ret == HA_OK) { resp->code = HA_SERVER_ACCEPT; resp->detail = basic.user; /* We put this connection into the successful connections */ ret = add_cached_basic(ctx, basic.key); } return ret; } static int simple_digest_challenge(simple_context_t* ctx, ha_response_t* resp, ha_buffer_t* buf, int stale) { const char* nonce_str; const char* header; ASSERT(ctx && resp && buf); /* Generate an nonce */ #ifdef _DEBUG if(ctx->opts->digest_debugnonce) { nonce_str = ctx->opts->digest_debugnonce; ha_messagex(LOG_WARNING, "simple: using debug nonce. security non-existant."); } else #endif { unsigned char nonce[DIGEST_NONCE_LEN]; digest_makenonce(nonce, g_simple_secret, NULL); nonce_str = ha_bufenchex(buf, nonce, DIGEST_NONCE_LEN); if(!nonce_str) return HA_CRITERROR; } /* Now generate a message to send */ header = digest_challenge(buf, nonce_str, ctx->opts->realm, ctx->opts->digest_domains, stale); if(!header) return HA_CRITERROR; /* And append it nicely */ resp->code = HA_SERVER_DECLINE; ha_addheader(resp, "WWW-Authenticate", header); ha_messagex(LOG_DEBUG, "simple: created digest challenge with nonce: %s", nonce_str); return HA_OK; } static int simple_digest_response(simple_context_t* ctx, const char* header, const char* method, const char* uri, ha_response_t* resp, ha_buffer_t* buf) { unsigned char nonce[DIGEST_NONCE_LEN]; digest_header_t dg; digest_record_t* rec = NULL; const char* t; time_t expiry; int ret = HA_FALSE; int stale = 0; int r; ASSERT(ctx && header && method && uri && resp && buf); /* We use this below to send a default response */ resp->code = -1; if((r = digest_parse(header, buf, &dg, nonce)) < 0) return r; #ifdef _DEBUG if(ctx->opts->digest_debugnonce) { if(dg.nonce && strcmp(dg.nonce, ctx->opts->digest_debugnonce) != 0) { resp->code = HA_SERVER_BADREQ; ret = HA_FALSE; ha_messagex(LOG_WARNING, "simple: digest response contains invalid nonce"); goto finally; } /* Do a rough hash into the real nonce, for use as a key */ md5_string(nonce, ctx->opts->digest_debugnonce); /* Debug nonce's never expire */ expiry = time(NULL); } else #endif { r = digest_checknonce(nonce, g_simple_secret, &expiry); if(r != HA_OK) { if(r == HA_FALSE) { resp->code = HA_SERVER_BADREQ; ha_messagex(LOG_WARNING, "simple: digest response contains invalid nonce"); } ret = r; goto finally; } } rec = get_cached_digest(ctx, nonce); /* Check to see if we're stale */ if((expiry + ctx->opts->cache_timeout) <= time(NULL)) { ha_messagex(LOG_INFO, "simple: nonce expired, sending stale challenge: %s", dg.username); stale = 1; goto finally; } if(!rec) { ha_messagex(LOG_INFO, "simple: no record in cache, creating one: %s", dg.username); /* * If we're valid but don't have a record in the * cache then complete the record properly. */ rec = digest_makerec(nonce, dg.username); if(!rec) { ret = HA_CRITERROR; goto finally; } r = complete_digest_ha1(ctx, rec, buf, dg.username); if(r != HA_OK) { ret = r; goto finally; } } /* Increment our nonce count */ rec->nc++; ret = digest_check(ctx->opts->realm, method, ctx->opts->digest_ignoreuri ? NULL : uri, buf, &dg, rec); if(ret == HA_BADREQ) { ret = HA_FALSE; resp->code = HA_SERVER_BADREQ; } else if(ret == HA_OK) { resp->code = HA_SERVER_ACCEPT; resp->detail = dg.username; /* Figure out if we need a new nonce */ if((expiry + (ctx->opts->cache_timeout - (ctx->opts->cache_timeout / 8))) < time(NULL)) { ha_messagex(LOG_INFO, "simple: nonce almost expired, creating new one: %s", dg.username); digest_makenonce(nonce, g_simple_secret, NULL); stale = 1; } t = digest_respond(buf, &dg, rec, stale ? nonce : NULL); if(!t) { ret = HA_CRITERROR; goto finally; } if(t[0]) ha_addheader(resp, "Authentication-Info", t); ha_messagex(LOG_NOTICE, "simple: validated digest user: %s", dg.username); /* Put the connection into the cache */ if((r = save_cached_digest(ctx, rec)) < 0) ret = r; rec = NULL; } finally: /* If the record wasn't stored away then free it */ if(rec) free(rec); /* If nobody above responded then challenge the client again */ if(resp->code == -1) return simple_digest_challenge(ctx, resp, buf, stale); return ret; } /* ------------------------------------------------------------------------------- * Handler Functions */ int simple_config(ha_context_t* context, const char* name, const char* value) { simple_context_t* ctx = (simple_context_t*)(context->data); ASSERT(name && name[0] && value && value[0]); if(strcmp(name, "passwordfile") == 0) { ctx->filename = value; return HA_OK; } return HA_FALSE; } int simple_init(ha_context_t* context) { /* Global initialization */ if(!context) { ha_messagex(LOG_DEBUG, "simple: generating secret"); return ha_genrandom(g_simple_secret, DIGEST_SECRET_LEN); } /* Context specific initialization */ else { simple_context_t* ctx = (simple_context_t*)(context->data); int fd; ASSERT(ctx); /* Make sure there are some types of authentication we can do */ if(!(context->opts.types & (HA_TYPE_BASIC | HA_TYPE_DIGEST))) { ha_messagex(LOG_ERR, "simple: module configured, but does not implement any " "configured authentication type."); return HA_FAILED; } /* Check to make sure the file exists */ if(!ctx->filename) { ha_messagex(LOG_ERR, "simple: configuration incomplete. " "Must have a PasswordFile configured."); return HA_FAILED; } fd = open(ctx->filename, O_RDONLY); if(fd == -1) { ha_message(LOG_ERR, "simple: can't open file for authentication: %s", ctx->filename); return HA_FAILED; } close(fd); ASSERT(!ctx->cache); /* The cache for digest records and basic */ if(!(ctx->cache = hash_create(MD5_LEN, free_hash_object, NULL))) { ha_messagex(LOG_CRIT, "out of memory"); return HA_CRITERROR; } /* Copy some settings over for easy access */ ctx->opts = &(context->opts); ha_messagex(LOG_INFO, "simple: initialized handler"); } return HA_OK; } void simple_destroy(ha_context_t* context) { /* Per context destroy */ if(context) { /* Note: We don't need to be thread safe here anymore */ simple_context_t* ctx = (simple_context_t*)(context->data); if(ctx->cache) hash_free(ctx->cache); ha_messagex(LOG_INFO, "simple: uninitialized handler"); } } int simple_process(ha_context_t* context, ha_request_t* req, ha_response_t* resp, ha_buffer_t* buf) { simple_context_t* ctx = (simple_context_t*)(context->data); const char* header = NULL; int ret = HA_FALSE; int found = 0; basic_header_t basic; int r; ASSERT(context && req && resp && buf); ASSERT(req->args[AUTH_ARG_METHOD]); ASSERT(req->args[AUTH_ARG_URI]); ha_lock(NULL); /* Purge the cache */ r = hash_purge(ctx->cache, time(NULL) - ctx->opts->cache_timeout); ha_unlock(NULL); if(r > 0) ha_messagex(LOG_DEBUG, "simple: purged cache records: %d", r); /* We use this below to detect whether to send a default response */ resp->code = -1; /* Check the headers and see if we got a response thingy */ if(context->opts.types & HA_TYPE_DIGEST) { header = ha_getheader(req, "Authorization", HA_PREFIX_DIGEST); if(header) { ha_messagex(LOG_DEBUG, "simple: processing digest auth header"); ret = simple_digest_response(ctx, header, req->args[AUTH_ARG_METHOD], req->args[AUTH_ARG_URI], resp, buf); if(ret < 0) return ret; } } /* Or a basic authentication */ if(!header && context->opts.types & HA_TYPE_BASIC) { ha_messagex(LOG_DEBUG, "simple: processing basic auth header"); header = ha_getheader(req, "Authorization", HA_PREFIX_BASIC); if(header) { ret = simple_basic_response(ctx, header, resp, buf); if(ret < 0) return ret; } } /* Send a default response if that's what we need */ if(resp->code == -1) { resp->code = HA_SERVER_DECLINE; if(context->opts.types & HA_TYPE_BASIC) { ha_bufmcat(buf, "BASIC realm=\"", ctx->opts->realm , "\"", NULL); if(ha_buferr(buf)) return HA_CRITERROR; ha_addheader(resp, "WWW-Authenticate", ha_bufdata(buf)); ha_messagex(LOG_DEBUG, "simple: sent basic auth request"); } if(context->opts.types & HA_TYPE_DIGEST) { ret = simple_digest_challenge(ctx, resp, buf, 0); if(ret < 0) return ret; } } return ret; } /* ------------------------------------------------------------------------------- * Handler Definition */ ha_handler_t simple_handler = { "SIMPLE", /* The type */ simple_init, /* Initialization function */ simple_destroy, /* Uninitialization routine */ simple_config, /* Config routine */ simple_process, /* Processing routine */ NULL, /* A default context */ sizeof(simple_context_t) /* Size of the context */ };