diff options
-rw-r--r-- | ChangeLog | 3 | ||||
-rw-r--r-- | configure.in | 18 | ||||
-rw-r--r-- | daemon/Makefile.am | 8 | ||||
-rw-r--r-- | daemon/bd.c | 238 | ||||
-rw-r--r-- | daemon/bd.h | 23 | ||||
-rw-r--r-- | daemon/digest.c | 202 | ||||
-rw-r--r-- | daemon/digest.h | 42 | ||||
-rw-r--r-- | daemon/httpauthd.c | 4 | ||||
-rw-r--r-- | daemon/ldap.c | 176 | ||||
-rw-r--r-- | daemon/misc.c | 6 | ||||
-rw-r--r-- | daemon/pgsql.c | 749 | ||||
-rw-r--r-- | daemon/simple.c | 200 |
12 files changed, 1306 insertions, 363 deletions
@@ -1,3 +1,6 @@ +0.5 + - Postgresql support + 0.4.2 - Separated base handler functionality, reorganized files, code - Conditional compilation of various handlers diff --git a/configure.in b/configure.in index 5b0a37e..6fc6add 100644 --- a/configure.in +++ b/configure.in @@ -114,6 +114,24 @@ if test -n "$with_ldap"; then AC_DEFINE_UNQUOTED(WITH_LDAP, 1, [With LDAP Support] ) fi +# PGSQL support +AM_CONDITIONAL(WITH_PGSQL, test -n "$with_pgsql") +if test -n "$with_pgsql"; then + + echo "enabling PGSQL support" + + if test "$with_pgsql" != "yes"; then + LDFLAGS="$LDFLAGS -L$with_pgsql/lib" + CFLAGS="$CFLAGS -L$with_pgsql/include" + fi + + AC_CHECK_LIB([pq], [PQexec], , + [ echo "ERROR: Postgres libpq library required."; exit 1] ) + + AC_CHECK_HEADERS(libpq-fe.h) + AC_DEFINE_UNQUOTED(WITH_PGSQL, 1, [With PGSQL Support] ) +fi + # NTLM Support AM_CONDITIONAL(WITH_NTLM, test -n "$enable_ntlm") if test -n "$enable_ntlm"; then diff --git a/daemon/Makefile.am b/daemon/Makefile.am index 7eb7087..db83f8b 100644 --- a/daemon/Makefile.am +++ b/daemon/Makefile.am @@ -10,6 +10,8 @@ httpauthd_SOURCES = httpauthd.c httpauthd.h usuals.h bd.h bd.c misc.c basic.h ba LDAP_SOURCES = ldap.c +PGSQL_SOURCES = pgsql.c + NTLM_SOURCES = ntlm.c ntlmssp.h ntlmssp.c \ smblib/smblib.c smblib/smblib-util.c smblib/file.c smblib/smb-errors.c \ smblib/exper.c smblib/smblib-api.c smblib/smblib.h smblib/std-defines.h \ @@ -26,8 +28,12 @@ if WITH_NTLM httpauthd_SOURCES += $(NTLM_SOURCES) endif +if WITH_PGSQL +httpauthd_SOURCES += $(PGSQL_SOURCES) +endif + httpauthd_CFLAGS = -D_THREAD_SAFE -pthread -DLinux \ -I${top_srcdir}/common/ -I${top_srcdir} httpauthd_LDFLAGS = -pthread -EXTRA_DIST = $(LDAP_SOURCES) +EXTRA_DIST = $(LDAP_SOURCES) $(NTLM_SOURCES) $(PGSQL_SOURCES) diff --git a/daemon/bd.c b/daemon/bd.c index 6cd7b6b..5519cfa 100644 --- a/daemon/bd.c +++ b/daemon/bd.c @@ -12,12 +12,23 @@ static unsigned char g_digest_secret[DIGEST_SECRET_LEN]; + + /* ------------------------------------------------------------------------------- * Defaults and Constants */ #define BASIC_ESTABLISHED (void*)1 +/* Kept by the us for validating the client */ +typedef struct digest_record +{ + unsigned char nonce[DIGEST_NONCE_LEN]; + unsigned char userhash[MD5_LEN]; + unsigned char ha1[MD5_LEN]; + unsigned int nc; +} +digest_record_t; /* ------------------------------------------------------------------------------- * Internal Functions @@ -127,6 +138,25 @@ static int add_cached_basic(bd_context_t* ctx, ha_context_t* c, return HA_OK; } +digest_record_t* make_digest_rec(unsigned char* nonce, const char* user) +{ + digest_record_t* rec = (digest_record_t*)malloc(sizeof(*rec)); + + ASSERT(nonce && user); + + if(!rec) + { + ha_messagex(NULL, LOG_CRIT, "out of memory"); + return NULL; + } + + memset(rec, 0, sizeof(*rec)); + memcpy(rec->nonce, nonce, DIGEST_NONCE_LEN); + + md5_string(rec->userhash, user); + return rec; +} + static int do_basic_response(ha_request_t* rq, bd_context_t* ctx, const char* header) { basic_header_t basic; @@ -217,7 +247,7 @@ static int do_digest_challenge(ha_request_t* rq, bd_context_t* ctx, int stale) static int do_digest_response(ha_request_t* rq, bd_context_t* ctx, const char* header) { unsigned char nonce[DIGEST_NONCE_LEN]; - digest_header_t dg; + digest_context_t dg; digest_record_t* rec = NULL; const char* t; time_t expiry; @@ -230,15 +260,20 @@ static int do_digest_response(ha_request_t* rq, bd_context_t* ctx, const char* h /* We use this below to send a default response */ rq->resp_code = -1; - if((r = digest_parse(header, rq->buf, &dg, nonce)) < 0) + if((r = digest_parse(header, rq->buf, &(dg.client))) < 0) return r; + if(!dg.client.username) + { + ha_messagex(rq, LOG_WARNING, "digest response contains no user name"); + goto finally; + } + #ifdef _DEBUG if(rq->context->digest_debugnonce) { - if(dg.nonce && strcmp(dg.nonce, rq->context->digest_debugnonce) != 0) + if(dg.client.nonce && strcmp(dg.client.nonce, rq->context->digest_debugnonce) != 0) { - ret = HA_FALSE; ha_messagex(rq, LOG_WARNING, "digest response contains invalid nonce"); goto finally; } @@ -252,6 +287,18 @@ static int do_digest_response(ha_request_t* rq, bd_context_t* ctx, const char* h else #endif { + /* Parse out the nonce from that header */ + memset(nonce, 0, DIGEST_NONCE_LEN); + + if(dg.client.nonce) + { + size_t len = DIGEST_NONCE_LEN; + void* d = ha_bufdechex(rq->buf, dg.client.nonce, &len); + + if(d && len == DIGEST_NONCE_LEN) + memcpy(nonce, d, DIGEST_NONCE_LEN); + } + r = digest_checknonce(nonce, g_digest_secret, &expiry); if(r != HA_OK) { @@ -260,95 +307,142 @@ static int do_digest_response(ha_request_t* rq, bd_context_t* ctx, const char* h goto finally; } + + /* Check to see if we're stale */ + if((expiry + rq->context->cache_timeout) <= time(NULL)) + { + ha_messagex(rq, LOG_INFO, "nonce expired, sending stale challenge: %s", + dg.client.username); + + stale = 1; + goto finally; + } } + /* See if we have this one from before. */ rec = get_cached_digest(ctx, rq->context, nonce); - /* Check to see if we're stale */ - if((expiry + rq->context->cache_timeout) <= time(NULL)) + /* + * Fill in the required fields. + */ + dg.server_nc = rec ? ++(rec->nc) : 0; /* Note bumping up nc */ + dg.server_uri = rq->req_args[AUTH_ARG_URI]; + dg.server_method = rq->req_args[AUTH_ARG_METHOD]; + + /* Check the majority of the fields */ + ret = digest_pre_check(&dg, rq->context, rq->buf); + + if(ret != HA_OK) { - ha_messagex(rq, LOG_INFO, "nonce expired, sending stale challenge: %s", - dg.username); + if(ret == HA_BADREQ) + { + ret = HA_FALSE; + rq->resp_code = HA_SERVER_BADREQ; + } - stale = 1; goto finally; } + /* + * If this is the first instance then we pass off to our derived + * handler for validation and completion of the ha1. This completes + * the authentication, and leaves us the ha1 caching. + */ if(!rec) { - ha_messagex(rq, LOG_INFO, "no record in cache, creating one: %s", dg.username); + ha_messagex(rq, LOG_INFO, "no record in cache, creating one: %s", dg.client.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); + rec = make_digest_rec(nonce, dg.client.username); if(!rec) { ret = HA_CRITERROR; goto finally; } - ASSERT(ctx->f_complete_digest); - r = ctx->f_complete_digest(rq, dg.username, rec->ha1); + + ASSERT(ctx->f_validate_digest); + r = ctx->f_validate_digest(rq, dg.client.username, &dg); if(r != HA_OK) { ret = r; goto finally; } + + /* Save away pertinent information when successful*/ + memcpy(rec->ha1, dg.ha1, MD5_LEN); } /* We had a record so ... */ else { + /* Bump up the ncount */ rec->nc++; - } - ret = digest_check(&dg, rec, rq->context, rq->buf, - rq->req_args[AUTH_ARG_METHOD], rq->req_args[AUTH_ARG_URI]); + /* Check the user name */ + if(md5_strcmp(rec->userhash, dg.client.username) != 0) + { + ha_messagex(NULL, LOG_ERR, "digest response contains invalid username"); + ret = HA_FALSE; + goto finally; + } - if(ret == HA_BADREQ) - { - ret = HA_FALSE; - rq->resp_code = HA_SERVER_BADREQ; - } + /* And do the validation ourselves */ + memcpy(dg.ha1, rec->ha1, MD5_LEN); - else if(ret == HA_OK) - { - rq->resp_code = HA_SERVER_OK; - rq->resp_detail = dg.username; + ret = digest_complete_check(&dg, rq->buf); - /* Figure out if we need a new nonce */ - if((expiry + (rq->context->cache_timeout - - (rq->context->cache_timeout / 8))) < time(NULL)) + if(ret != HA_OK) { - ha_messagex(rq, LOG_INFO, "nonce almost expired, creating new one: %s", - dg.username); + if(ret == HA_BADREQ) + { + ret = HA_FALSE; + rq->resp_code = HA_SERVER_BADREQ; + } - digest_makenonce(nonce, g_digest_secret, NULL); - stale = 1; - } + if(ret == HA_FALSE) + ha_messagex(NULL, LOG_WARNING, "digest re-authentication failed for user: %s", + dg.client.username); - t = digest_respond(rq->buf, &dg, rec, stale ? nonce : NULL); - if(!t) - { - ret = HA_CRITERROR; goto finally; } + } - if(t[0]) - ha_addheader(rq, "Authentication-Info", t); - ha_messagex(rq, LOG_NOTICE, "validated digest user: %s", dg.username); + rq->resp_code = HA_SERVER_OK; + rq->resp_detail = dg.client.username; - /* Put the connection into the cache */ - if((r = save_cached_digest(ctx, rq->context, rec)) < 0) - ret = r; + /* Figure out if we need a new nonce */ + if((expiry + (rq->context->cache_timeout - + (rq->context->cache_timeout / 8))) < time(NULL)) + { + ha_messagex(rq, LOG_INFO, "nonce almost expired, creating new one: %s", + dg.client.username); - rec = NULL; + digest_makenonce(nonce, g_digest_secret, NULL); + stale = 1; } + t = digest_respond(&dg, rq->buf, stale ? nonce : NULL); + if(!t) + { + ret = HA_CRITERROR; + goto finally; + } + + if(t[0]) + ha_addheader(rq, "Authentication-Info", t); + + ha_messagex(rq, LOG_NOTICE, "validated digest user: %s", dg.client.username); + + /* Put the connection into the cache */ + if((ret = save_cached_digest(ctx, rq->context, rec)) == HA_OK) + rec = NULL; + finally: /* If the record wasn't stored away then free it */ @@ -363,6 +457,62 @@ finally: } + +const char* bd_substitute(const ha_request_t* rq, const char* user, const char* str) +{ + bd_context_t* ctx = (bd_context_t*)rq->context->ctx_data; + const char* t; + + ASSERT(rq && user && str); + ASSERT(ctx->f_escape_value); + + /* TODO: We need to be escaping the user and realm properly */ + /* This starts a new block to join */ + ha_bufcpy(rq->buf, ""); + + while(str[0]) + { + t = strchr(str, '%'); + if(!t) + { + ha_bufjoin(rq->buf); + ha_bufcpy(rq->buf, str); + break; + } + + ha_bufjoin(rq->buf); + ha_bufncpy(rq->buf, str, t - str); + + t++; + + switch(t[0]) + { + case 'u': + ha_bufjoin(rq->buf); + (ctx->f_escape_value)(rq, rq->buf, user); + t++; + break; + + case 'r': + ha_bufjoin(rq->buf); + (ctx->f_escape_value)(rq, rq->buf, rq->context->realm); + t++; + break; + + case '%': + ha_bufjoin(rq->buf); + ha_bufcpy(rq->buf, "%"); + t++; + break; + }; + + str = t; + } + + return ha_bufdata(rq->buf); +} + + /* ------------------------------------------------------------------------------- * Handler Functions */ diff --git a/daemon/bd.h b/daemon/bd.h index 272e09a..a59b1e1 100644 --- a/daemon/bd.h +++ b/daemon/bd.h @@ -3,6 +3,8 @@ #define BD_H #include "hash.h" +#include "digest.h" +#include "httpauthd.h" /* ---------------------------------------------------------------------------------- * Callbacks @@ -17,8 +19,8 @@ * HA_OK: completed successfully * HA_FAILED: error retrieving hash (should have logged error) */ -typedef int (*bd_complete_digest)(ha_request_t* rq, - const char* user, unsigned char* ha1); +typedef int (*bd_validate_digest)(ha_request_t* rq, + const char* user, digest_context_t* dg); /* * A callback for validating a given user's password. @@ -31,6 +33,12 @@ typedef int (*bd_complete_digest)(ha_request_t* rq, typedef int (*bd_validate_basic)(ha_request_t* rq, const char* user, const char* password); +/* + * Escapes a value for sending to 'server' + */ +typedef void (*bd_escape_value)(const ha_request_t* rq, ha_buffer_t* buf, + const char* value); + /* ---------------------------------------------------------------------------------- * Base Context */ @@ -38,12 +46,13 @@ typedef struct bd_context { hsh_t* cache; /* Some cached records or basic */ - bd_complete_digest f_complete_digest; + bd_validate_digest f_validate_digest; bd_validate_basic f_validate_basic; + bd_escape_value f_escape_value; } bd_context_t; -#define BD_CALLBACKS(a, b) { NULL, (a), (b) } +#define BD_CALLBACKS(a, b, c) { NULL, (a), (b), (c) } #define BD_DEFAULTS { NULL, NULL, NULL } /* ---------------------------------------------------------------------------------- @@ -68,4 +77,10 @@ void bd_destroy(ha_context_t* context); */ int bd_process(ha_request_t* rq); +/* ---------------------------------------------------------------------------------- + * Other helper functionts + */ + +const char* bd_substitute(const ha_request_t* rq, const char* user, const char* str); + #endif /* BD_H */ diff --git a/daemon/digest.c b/daemon/digest.c index 099bf31..bce588e 100644 --- a/daemon/digest.c +++ b/daemon/digest.c @@ -69,25 +69,6 @@ int digest_checknonce(unsigned char* nonce, unsigned char* secret, time_t* tm) return HA_FALSE; } -digest_record_t* digest_makerec(unsigned char* nonce, const char* user) -{ - digest_record_t* rec = (digest_record_t*)malloc(sizeof(*rec)); - - ASSERT(nonce && user); - - if(!rec) - { - ha_messagex(NULL, LOG_CRIT, "out of memory"); - return NULL; - } - - memset(rec, 0, sizeof(*rec)); - memcpy(rec->nonce, nonce, DIGEST_NONCE_LEN); - - md5_string(rec->userhash, user); - return rec; -} - const char* digest_challenge(ha_buffer_t* buf, const char* nonce_str, const char* realm, const char* domains, int stale) { @@ -127,16 +108,16 @@ const char* digest_challenge(ha_buffer_t* buf, const char* nonce_str, * limitations under the License. */ -int digest_parse(char* header, ha_buffer_t* buf, digest_header_t* rec, - unsigned char* nonce) +int digest_parse(const char* head, ha_buffer_t* buf, digest_header_t* rec) { char next; char* key; char* value; + char* header; - ASSERT(header && buf && rec); + ASSERT(head && buf && rec); - header = ha_bufcpy(buf, header); + header = ha_bufcpy(buf, head); if(!header) return HA_CRITERROR; @@ -230,93 +211,78 @@ int digest_parse(char* header, ha_buffer_t* buf, digest_header_t* rec, } } - if(nonce) - { - memset(nonce, 0, DIGEST_NONCE_LEN); + return HA_OK; +} - if(rec->nonce) - { - size_t len = DIGEST_NONCE_LEN; - void* d = ha_bufdechex(buf, rec->nonce, &len); +int digest_check(digest_context_t* dg, const ha_context_t* opts, ha_buffer_t* buf) +{ + int r; - if(d && len == DIGEST_NONCE_LEN) - memcpy(nonce, d, DIGEST_NONCE_LEN); - } - } + r = digest_pre_check(dg, opts, buf); + if(r == HA_OK) + r = digest_complete_check(dg, buf); - return HA_OK; + return r; } -int digest_check(digest_header_t* dg, digest_record_t* rec, const ha_context_t* ctx, - ha_buffer_t* buf, const char* method, const char* uri) +int digest_pre_check(digest_context_t* dg, const ha_context_t* opts, ha_buffer_t* buf) { - unsigned char hash[MD5_LEN]; - md5_ctx_t md5; - const char* digest; - const char* t; - - ASSERT(ctx && method && buf && dg && rec); + ASSERT(buf && buf && dg); /* TODO: This stuff should probably go into bd.c */ /* Check for digest */ - if(!dg->digest || !dg->digest[0]) + if(!dg->client.digest || !dg->client.digest[0]) { ha_messagex(NULL, LOG_WARNING, "digest response missing digest"); return HA_BADREQ; } /* Username */ - if(!dg->username || !dg->username[0]) + if(!dg->client.username || !dg->client.username[0]) { ha_messagex(NULL, LOG_WARNING, "digest response missing username"); return HA_BADREQ; } - if(md5_strcmp(rec->userhash, dg->username) != 0) - { - ha_messagex(NULL, LOG_ERR, "digest response contains invalid username"); - return HA_FALSE; - } - /* The realm */ - if(!dg->realm) + if(!dg->client.realm) { ha_messagex(NULL, LOG_WARNING, "digest response contains missing realm"); return HA_BADREQ; } - if(strcmp(dg->realm, ctx->realm) != 0) + if(strcmp(dg->client.realm, opts->realm) != 0) { - ha_messagex(NULL, LOG_ERR, "digest response contains invalid realm: %s", dg->realm); + ha_messagex(NULL, LOG_ERR, "digest response contains invalid realm: %s", dg->client.realm); return HA_FALSE; } /* Components in the new RFC */ - if(dg->qop) + if(dg->client.qop) { /* * We only support 'auth' qop. We don't have access to the data * and wouldn't be able to support anything else. */ - if(strcmp(dg->qop, "auth") != 0) + if(strcmp(dg->client.qop, "auth") != 0) { ha_messagex(NULL, LOG_WARNING, "digest response contains unknown or unsupported qop: '%s'", - dg->qop ? dg->qop : ""); + dg->client.qop ? dg->client.qop : ""); return HA_BADREQ; } /* The cnonce */ - if(!dg->cnonce || !dg->cnonce[0]) + if(!dg->client.cnonce || !dg->client.cnonce[0]) { ha_messagex(NULL, LOG_WARNING, "digest response is missing cnonce value"); return HA_BADREQ; } - if(!ctx->digest_ignorenc) + if(!opts->digest_ignorenc) { /* The nonce count */ - if(!dg->nc || !dg->nc[0]) + if(!dg->client.nc || !dg->client.nc[0]) { ha_messagex(NULL, LOG_WARNING, "digest response is missing nc value"); return HA_BADREQ; @@ -326,22 +292,22 @@ int digest_check(digest_header_t* dg, digest_record_t* rec, const ha_context_t* else { char* e; - long nc = strtol(dg->nc, &e, 16); + long nc = strtol(dg->client.nc, &e, 16); if(*e) { - ha_messagex(NULL, LOG_ERR, "digest response has invalid nc value: %s", dg->nc); + ha_messagex(NULL, LOG_ERR, "digest response has invalid nc value: %s", dg->client.nc); return HA_BADREQ; } /* If we didn't a nc then save it away */ - if(!*e && rec->nc == 0) - rec->nc = nc; + if(!*e && dg->server_nc == 0) + dg->server_nc = nc; - if(*e || nc != rec->nc) + if(*e || nc != dg->server_nc) { ha_messagex(NULL, LOG_WARNING, "digest response has wrong nc value. " - "possible replay attack: %s", dg->nc); + "possible replay attack: %s", dg->client.nc); return HA_FALSE; } } @@ -349,47 +315,49 @@ int digest_check(digest_header_t* dg, digest_record_t* rec, const ha_context_t* } /* The algorithm */ - if(dg->algorithm && strcasecmp(dg->algorithm, "MD5") != 0) + if(dg->client.algorithm && strcasecmp(dg->client.algorithm, "MD5") != 0) { ha_messagex(NULL, LOG_WARNING, "digest response contains unknown or unsupported algorithm: '%s'", - dg->algorithm ? dg->algorithm : ""); + dg->client.algorithm ? dg->client.algorithm : ""); return HA_BADREQ; } /* Request URI */ - if(!dg->uri) + if(!dg->client.uri) { ha_messagex(NULL, LOG_WARNING, "digest response is missing uri"); return HA_BADREQ; } - if(!ctx->digest_ignoreuri && strcmp(dg->uri, uri) != 0) + if(!opts->digest_ignoreuri && strcmp(dg->client.uri, dg->server_uri) != 0) { ha_uri_t d_uri; ha_uri_t s_uri; - if(ha_uriparse(buf, dg->uri, &d_uri) < 0) + if(ha_uriparse(buf, dg->client.uri, &d_uri) < 0) { if(ha_buferr(buf)) return HA_CRITERROR; - ha_messagex(NULL, LOG_WARNING, "digest response constains invalid uri: %s", dg->uri); + ha_messagex(NULL, LOG_WARNING, "digest response constains invalid uri: %s", dg->client.uri); return HA_BADREQ; } - if(ha_uriparse(buf, uri, &s_uri) < 0) + ASSERT(dg->server_uri); + + if(ha_uriparse(buf, dg->server_uri, &s_uri) < 0) { if(ha_buferr(buf)) return HA_CRITERROR; - ha_messagex(NULL, LOG_ERR, "server sent us an invalid uri"); + ha_messagex(NULL, LOG_ERR, "server sent us an invalid uri: %s", dg->server_uri); return HA_BADREQ; } if(ha_uricmp(&d_uri, &s_uri) != 0) { ha_messagex(NULL, LOG_ERR, "digest response contains wrong uri: %s " - "(should be %s)", dg->uri, uri); + "(should be %s)", dg->client.uri, dg->server_uri); return HA_FALSE; } } @@ -402,21 +370,36 @@ int digest_check(digest_header_t* dg, digest_record_t* rec, const ha_context_t* * if it's used there. */ + return HA_OK; +} + + +int digest_complete_check(digest_context_t* dg, ha_buffer_t* buf) +{ + unsigned char hash[MD5_LEN]; + md5_ctx_t md5; + const char* t; + const char* digest; + + ASSERT(dg && buf); + /* * Now we validate the digest response */ /* Encode ha1 */ - t = ha_bufenchex(buf, rec->ha1, MD5_LEN); + t = ha_bufenchex(buf, dg->ha1, MD5_LEN); if(t == NULL) return HA_CRITERROR; + ASSERT(dg->server_method); + /* Encode ha2 */ md5_init(&md5); - md5_update(&md5, method, strlen(method)); + md5_update(&md5, dg->server_method, strlen(dg->server_method)); md5_update(&md5, ":", 1); - md5_update(&md5, dg->uri, strlen(dg->uri)); + md5_update(&md5, dg->client.uri, strlen(dg->client.uri)); md5_final(hash, &md5); ha_bufenchex(buf, hash, MD5_LEN); @@ -424,14 +407,15 @@ int digest_check(digest_header_t* dg, digest_record_t* rec, const ha_context_t* if(!ha_bufdata(buf)) return HA_CRITERROR; + ASSERT(dg->client.nonce); /* Old style digest (RFC 2069) */ - if(!dg->qop) + if(!dg->client.qop) { md5_init(&md5); md5_update(&md5, t, MD5_LEN * 2); /* ha1 */ md5_update(&md5, ":", 1); - md5_update(&md5, dg->nonce, strlen(dg->nonce)); /* nonce */ + md5_update(&md5, dg->client.nonce, strlen(dg->client.nonce)); /* nonce */ md5_update(&md5, ":", 1); md5_update(&md5, ha_bufdata(buf), MD5_LEN * 2); /* ha1 */ md5_final(hash, &md5); @@ -440,16 +424,20 @@ int digest_check(digest_header_t* dg, digest_record_t* rec, const ha_context_t* /* New style 'auth' digest (RFC 2617) */ else { + ASSERT(dg->client.nc); + ASSERT(dg->client.cnonce); + ASSERT(dg->client.qop); + md5_init(&md5); - md5_update(&md5, t, MD5_LEN * 2); /* ha1 */ + md5_update(&md5, t, MD5_LEN * 2); /* ha1 */ md5_update(&md5, ":", 1); - md5_update(&md5, dg->nonce, strlen(dg->nonce)); /* nonce */ + md5_update(&md5, dg->client.nonce, strlen(dg->client.nonce)); /* nonce */ md5_update(&md5, ":", 1); - md5_update(&md5, dg->nc, strlen(dg->nc)); /* nc */ + md5_update(&md5, dg->client.nc, strlen(dg->client.nc)); /* nc */ md5_update(&md5, ":", 1); - md5_update(&md5, dg->cnonce, strlen(dg->cnonce)); /* cnonce */ + md5_update(&md5, dg->client.cnonce, strlen(dg->client.cnonce)); /* cnonce */ md5_update(&md5, ":", 1); - md5_update(&md5, dg->qop, strlen(dg->qop)); /* qop */ + md5_update(&md5, dg->client.qop, strlen(dg->client.qop)); /* qop */ md5_update(&md5, ":", 1); md5_update(&md5, ha_bufdata(buf), MD5_LEN * 2); /* ha2 */ md5_final(hash, &md5); @@ -461,24 +449,23 @@ int digest_check(digest_header_t* dg, digest_record_t* rec, const ha_context_t* if(digest == NULL) return HA_CRITERROR; - if(strcasecmp(dg->digest, digest) != 0) - { - ha_messagex(NULL, LOG_WARNING, "digest authentication failed for user: %s", dg->username); + ASSERT(dg->client.digest); + + if(strcasecmp(dg->client.digest, digest) != 0) return HA_FALSE; - } return HA_OK; } -const char* digest_respond(ha_buffer_t* buf, digest_header_t* dg, - digest_record_t* rec, unsigned char* next) +const char* digest_respond(digest_context_t* dg, ha_buffer_t* buf, + unsigned char* next) { unsigned char hash[MD5_LEN]; md5_ctx_t md5; const char* nextnonce = NULL; const char* t; - ASSERT(buf && dg && rec); + ASSERT(buf && dg); /* This makes a new buffer */ ha_bufcpy(buf, ""); @@ -492,7 +479,7 @@ const char* digest_respond(ha_buffer_t* buf, digest_header_t* dg, } /* For older clients RFC 2069 */ - if(!dg->qop) + if(!dg->client.qop) { if(nextnonce) ha_bufmcat(buf, "nextnonce=\"", nextnonce, "\"", NULL); @@ -503,15 +490,17 @@ const char* digest_respond(ha_buffer_t* buf, digest_header_t* dg, /* Otherwise we do the whole song and dance */ /* Encode ha1 */ - t = ha_bufenchex(buf, rec->ha1, MD5_LEN); + t = ha_bufenchex(buf, dg->ha1, MD5_LEN); if(t == NULL) return NULL; + ASSERT(dg->client.uri); + /* Encode ha2 */ md5_init(&md5); md5_update(&md5, ":", 1); - md5_update(&md5, dg->uri, strlen(dg->uri)); + md5_update(&md5, dg->client.uri, strlen(dg->client.uri)); md5_final(hash, &md5); ha_bufenchex(buf, hash, MD5_LEN); @@ -519,19 +508,24 @@ const char* digest_respond(ha_buffer_t* buf, digest_header_t* dg, if(!ha_bufdata(buf)) return NULL; + ASSERT(dg->client.nonce); + ASSERT(dg->client.nc); + ASSERT(dg->client.cnonce); + ASSERT(dg->client.qop); + /* New style 'auth' digest (RFC 2617) */ md5_init(&md5); - md5_update(&md5, t, MD5_LEN * 2); /* ha1 */ + md5_update(&md5, t, MD5_LEN * 2); /* ha1 */ md5_update(&md5, ":", 1); - md5_update(&md5, dg->nonce, strlen(dg->nonce)); /* nonce */ + md5_update(&md5, dg->client.nonce, strlen(dg->client.nonce)); /* nonce */ md5_update(&md5, ":", 1); - md5_update(&md5, dg->nc, strlen(dg->nc)); /* nc */ + md5_update(&md5, dg->client.nc, strlen(dg->client.nc)); /* nc */ md5_update(&md5, ":", 1); - md5_update(&md5, dg->cnonce, strlen(dg->cnonce)); /* cnonce */ + md5_update(&md5, dg->client.cnonce, strlen(dg->client.cnonce)); /* cnonce */ md5_update(&md5, ":", 1); - md5_update(&md5, dg->qop, strlen(dg->qop)); /* qop */ + md5_update(&md5, dg->client.qop, strlen(dg->client.qop)); /* qop */ md5_update(&md5, ":", 1); - md5_update(&md5, ha_bufdata(buf), MD5_LEN * 2); /* ha2 */ + md5_update(&md5, ha_bufdata(buf), MD5_LEN * 2); /* ha2 */ md5_final(hash, &md5); /* Encode the digest */ @@ -541,9 +535,9 @@ const char* digest_respond(ha_buffer_t* buf, digest_header_t* dg, return NULL; ha_bufmcat(buf, "rspauth=\"", t, "\"", - ", qop=", dg->qop, - ", nc=", dg->nc, - ", cnonce=\"", dg->cnonce, "\"", NULL); + ", qop=", dg->client.qop, + ", nc=", dg->client.nc, + ", cnonce=\"", dg->client.cnonce, "\"", NULL); if(nextnonce) { diff --git a/daemon/digest.h b/daemon/digest.h index f763133..a889024 100644 --- a/daemon/digest.h +++ b/daemon/digest.h @@ -25,33 +25,37 @@ typedef struct digest_header } digest_header_t; -/* Kept by the server for validating the client */ -typedef struct digest_record +typedef struct digest_context { - unsigned char nonce[DIGEST_NONCE_LEN]; - unsigned char userhash[MD5_LEN]; - unsigned char ha1[MD5_LEN]; - unsigned int nc; -} -digest_record_t; - -digest_record_t* digest_makerec(unsigned char* nonce, const char* user); + digest_header_t client; -int ha_digestparse(char* header, ha_buffer_t* buf, digest_header_t* rec, - unsigned char* nonce); + long server_nc; + const char* server_uri; + const char* server_method; -int ha_digestnonce(time_t* tm, unsigned char* nonce); + unsigned char ha1[MD5_LEN]; +} +digest_context_t; -int digest_check(digest_header_t* dg, digest_record_t* rec, const ha_context_t* opts, - ha_buffer_t* buf, const char* method, const char* uri); +void digest_makeha1(unsigned char* digest, const char* user, + const char* realm, const char* password); -const char* digest_respond(ha_buffer_t* buf, digest_header_t* dg, - digest_record_t* rec, unsigned char* next); +int digest_parse(const char* header, ha_buffer_t* buf, digest_header_t* dg); const char* digest_challenge(ha_buffer_t* buf, const char* nonce_str, const char* realm, const char* domains, int stale); -void digest_makeha1(unsigned char* digest, const char* user, - const char* realm, const char* password); +/* + * Validate digest headers once they've been parsed. Note that it's up + * to the caller to validate the 'username' and 'nonce' fields. + */ +int digest_check(digest_context_t* dg, const ha_context_t* opts, ha_buffer_t* buf); +int digest_pre_check(digest_context_t* dg, const ha_context_t* opts, ha_buffer_t* buf); + +/* This assumes a digest_context that's been prechecked successfully */ +int digest_complete_check(digest_context_t* dg, ha_buffer_t* buf); + +/* This assumes a digest_context that's been checked and validated successfully */ +const char* digest_respond(digest_context_t* dg, ha_buffer_t* buf, unsigned char* next); #endif /* __DIGEST_H__ */ diff --git a/daemon/httpauthd.c b/daemon/httpauthd.c index f995d6d..bae758a 100644 --- a/daemon/httpauthd.c +++ b/daemon/httpauthd.c @@ -32,6 +32,7 @@ extern ha_handler_t simple_handler; extern ha_handler_t ldap_handler; extern ha_handler_t ntlm_handler; +extern ha_handler_t pgsql_handler; /* This is the list of all available handlers */ ha_handler_t* g_handlerlist[] = @@ -42,6 +43,9 @@ ha_handler_t* g_handlerlist[] = #if WITH_NTLM &ntlm_handler, #endif +#if WITH_PGSQL + &pgsql_handler, +#endif &simple_handler, }; diff --git a/daemon/ldap.c b/daemon/ldap.c index 719ac0d..1df7651 100644 --- a/daemon/ldap.c +++ b/daemon/ldap.c @@ -72,13 +72,15 @@ typedef struct ldap_context ldap_context_t; /* Forward declarations for callbacks */ -static int complete_digest(ha_request_t* rq, const char* user, unsigned char* ha1); +static int validate_digest(ha_request_t* rq, const char* user, digest_context_t* dg); static int validate_basic(ha_request_t* rq, const char* user, const char* password); +static void escape_ldap(const ha_request_t* rq, ha_buffer_t* buf, const char* value); /* The defaults for the context */ static const ldap_context_t ldap_defaults = { - BD_CALLBACKS(complete_digest, validate_basic), + BD_CALLBACKS(validate_digest, + validate_basic, escape_ldap), NULL, /* servers */ NULL, /* filter */ NULL, /* base */ @@ -122,7 +124,7 @@ static int report_ldap(const ha_request_t* rq, const char* msg, int code) #define LDAP_NO_ESCAPE "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_" #define LDAP_HEX "0123456789abcdef" -static const char* escape_ldap(ha_buffer_t* buf, const char* str) +static void escape_ldap(const ha_request_t* rq, ha_buffer_t* buf, const char* str) { const char* t = str; size_t pos; @@ -155,62 +157,8 @@ static const char* escape_ldap(ha_buffer_t* buf, const char* str) t++; } } - - return ha_bufdata(buf); } -static const char* substitute_params(const ha_request_t* rq, ldap_context_t* ctx, - const char* user, const char* str) -{ - const char* t; - - ASSERT(ctx && rq && user && str); - - /* TODO: We need to be escaping the user and realm properly */ - /* This starts a new block to join */ - ha_bufcpy(rq->buf, ""); - - while(str[0]) - { - t = strchr(str, '%'); - if(!t) - { - ha_bufjoin(rq->buf); - ha_bufcpy(rq->buf, str); - break; - } - - ha_bufjoin(rq->buf); - ha_bufncpy(rq->buf, str, t - str); - - t++; - - switch(t[0]) - { - case 'u': - ha_bufjoin(rq->buf); - escape_ldap(rq->buf, user); - t++; - break; - - case 'r': - ha_bufjoin(rq->buf); - escape_ldap(rq->buf, rq->context->realm); - t++; - break; - - case '%': - ha_bufjoin(rq->buf); - ha_bufcpy(rq->buf, "%"); - t++; - break; - }; - - str = t; - } - - return ha_bufdata(rq->buf); -} static const char* make_password_md5(ha_buffer_t* buf, const char* clearpw) { @@ -280,16 +228,14 @@ static int parse_ldap_password(const char** password) return LDAP_PW_UNKNOWN; } -static const char* find_cleartext_password(ha_buffer_t* buf, const char** pws) +static const char** find_cleartext_password(ha_buffer_t* buf, const char** pws) { ASSERT(buf); for(; pws && *pws; pws++) { - const char* pw = *pws; - - if(parse_ldap_password(&pw) == LDAP_PW_CLEAR) - return pw; + if(parse_ldap_password(pws) == LDAP_PW_CLEAR) + return pws; } return NULL; @@ -478,12 +424,14 @@ static LDAP* get_ldap_connection(const ha_request_t* rq, ldap_context_t* ctx) ASSERT(ctx); + /* TODO: What about multiple threads here? */ + for(i = 0; i < ctx->ldap_max; i++) { /* An open connection in the pool */ if(ctx->pool[i]) { - ha_messagex(rq, LOG_DEBUG, "using cached connection"); + ha_messagex(rq, LOG_DEBUG, "using cached LDAP connection"); ld = ctx->pool[i]; ctx->pool[i] = NULL; return ld; @@ -492,14 +440,14 @@ static LDAP* get_ldap_connection(const ha_request_t* rq, ldap_context_t* ctx) if(ctx->pool_mark >= ctx->ldap_max) { - ha_messagex(rq, LOG_ERR, "too many open connections"); + ha_messagex(rq, LOG_ERR, "too many open LDAP connections"); return NULL; } ld = ldap_init(ctx->servers, ctx->port); if(!ld) { - ha_message(rq, LOG_ERR, "couldn't initialize connection"); + ha_message(rq, LOG_ERR, "couldn't initialize LDAP connection"); return NULL; } @@ -544,7 +492,6 @@ static void save_ldap_connection(const ha_request_t* rq, ldap_context_t* ctx, LD case LDAP_SERVER_DOWN: case LDAP_LOCAL_ERROR: case LDAP_NO_MEMORY: - discard_ldap_connection(rq, ctx, ld); break; default: @@ -553,7 +500,7 @@ static void save_ldap_connection(const ha_request_t* rq, ldap_context_t* ctx, LD /* An open connection in the pool */ if(!ctx->pool[i]) { - ha_messagex(rq, LOG_DEBUG, "caching connection for later use"); + ha_messagex(rq, LOG_DEBUG, "caching ldap connection for later use"); ctx->pool[i] = ld; ld = NULL; break; @@ -561,6 +508,9 @@ static void save_ldap_connection(const ha_request_t* rq, ldap_context_t* ctx, LD } break; }; + + if(ld != NULL) + discard_ldap_connection(rq, ctx, ld); } static int retrieve_user_entry(const ha_request_t* rq, ldap_context_t* ctx, LDAP* ld, @@ -579,7 +529,7 @@ static int retrieve_user_entry(const ha_request_t* rq, ldap_context_t* ctx, LDAP if(ctx->filter) { /* Filters can also have %u and %r */ - filter = substitute_params(rq, ctx, user, ctx->filter); + filter = bd_substitute(rq, user, ctx->filter); if(!filter) return HA_CRITERROR; } @@ -639,19 +589,20 @@ static int retrieve_user_entry(const ha_request_t* rq, ldap_context_t* ctx, LDAP return HA_FALSE; } -static int complete_digest(ha_request_t* rq, const char* user, unsigned char* ha1) +static int validate_digest(ha_request_t* rq, const char* user, digest_context_t* dg) { ldap_context_t* ctx = (ldap_context_t*)rq->context->ctx_data; LDAP* ld = NULL; /* freed in finally */ LDAPMessage* results = NULL; /* freed in finally */ LDAPMessage* entry = NULL; /* no need to free */ struct berval** ha1s = NULL; /* freed manually */ - char** pws; + const char** pws; int ret = HA_FALSE; const char* dn = NULL; int r; + int foundany = 0; - ASSERT(rq && ha1 && user); + ASSERT(rq && dg && user); ld = get_ldap_connection(rq, ctx); if(!ld) @@ -668,7 +619,7 @@ static int complete_digest(ha_request_t* rq, const char* user, unsigned char* ha if(ctx->dnmap) { /* The map can have %u and %r to denote user and realm */ - dn = substitute_params(rq, ctx, user, ctx->dnmap); + dn = bd_substitute(rq, user, ctx->dnmap); if(!dn) { ret = HA_FAILED; @@ -690,56 +641,77 @@ static int complete_digest(ha_request_t* rq, const char* user, unsigned char* ha if(ctx->ha1_attr) ha1s = ldap_get_values_len(ld, entry, ctx->ha1_attr); - if(ha1s) + if(ha1s && *ha1s) { - if(*ha1s) + int foundinvalid = 0; + + while(*ha1s) { - ret = parse_ldap_ha1(rq, *ha1s, ha1); - if(ret != HA_OK) + r = parse_ldap_ha1(rq, *ha1s, dg->ha1); + + if(r == HA_FALSE) + { + foundinvalid = 1; + } + + else if(r == HA_OK) { - if(ret == HA_FALSE) - ha_messagex(rq, LOG_ERR, "server contains invalid HA1 for user: %s", user); + foundany = 1; + + /* Run the actual check */ + ret = digest_complete_check(dg, rq->buf); + + if(ret != HA_FALSE) + goto finally; } + + else if(r < 0) + goto finally; + + ha1s++; } - ha_messagex(rq, LOG_DEBUG, "using HA1 from ldap"); - ldap_value_free_len(ha1s); - goto finally; + if(foundinvalid) + ha_messagex(rq, LOG_ERR, "server contains invalid HA1 for user: %s", user); } /* If no ha1 set or none found, use password and make a HA1 */ - pws = ldap_get_values(ld, entry, ctx->pw_attr); + pws = (const char**)ldap_get_values(ld, entry, ctx->pw_attr); - if(pws) + if(pws && *pws) { /* Find a cleartext password */ - const char* t = find_cleartext_password(rq->buf, (const char**)pws); - - if(t) + while((pws = find_cleartext_password(rq->buf, pws))) { - digest_makeha1(ha1, user, rq->context->realm, t); - ret = HA_OK; - } + foundany = 1; - ldap_value_free(pws); + digest_makeha1(dg->ha1, user, rq->context->realm, *pws); - if(ret == HA_OK) - { - ha_messagex(rq, LOG_DEBUG, "using clear password from ldap"); - goto finally; + /* Run the actual check */ + ret = digest_complete_check(dg, rq->buf); + + if(ret != HA_FALSE) + goto finally; } } - ha_messagex(rq, LOG_ERR, "server contains no clear password or HA1 for user: %s", user); + if(!foundany) + ha_messagex(rq, LOG_WARNING, "server contains no clear password or HA1 for user: %s", user); finally: - if(ld) - save_ldap_connection(rq, ctx, ld); + if(ha1s) + ldap_value_free_len(ha1s); + + if(pws) + ldap_value_free((char**)pws); if(results) ldap_msgfree(results); + if(ld) + save_ldap_connection(rq, ctx, ld); + return ret; } @@ -772,7 +744,7 @@ static int validate_basic(ha_request_t* rq, const char* user, const char* passwo if(ctx->dnmap) { /* The map can have %u and %r to denote user and realm */ - dn = substitute_params(rq, ctx, user, ctx->dnmap); + dn = bd_substitute(rq, user, ctx->dnmap); if(!dn) { ret = HA_CRITERROR; @@ -867,12 +839,12 @@ static int validate_basic(ha_request_t* rq, const char* user, const char* passwo finally: - if(ld) - save_ldap_connection(rq, ctx, ld); - if(results) ldap_msgfree(results); + if(ld) + save_ldap_connection(rq, ctx, ld); + /* TODO: Check to make sure this fits within the return values */ return ret; } @@ -1015,7 +987,7 @@ int ldap_inithand(ha_context_t* context) } memset(ctx->pool, 0, sizeof(LDAP*) * ctx->ldap_max); - ha_messagex(NULL, LOG_INFO, "initialized handler"); + ha_messagex(NULL, LOG_INFO, "initialized ldap handler"); } return HA_OK; diff --git a/daemon/misc.c b/daemon/misc.c index f48d801..7ab8147 100644 --- a/daemon/misc.c +++ b/daemon/misc.c @@ -265,7 +265,7 @@ int ha_confint(const char* name, const char* conf, int min, int max, int* value) /* ----------------------------------------------------------------------- - * Client Authentication Functions + * URI Functions */ char* ha_uriformat(ha_buffer_t* buf, const ha_uri_t* uri) @@ -586,6 +586,10 @@ int ha_uricmp(ha_uri_t* one, ha_uri_t* two) return 0; } +/* ---------------------------------------------------------------------------------- + * Other miscellaneous stuff + */ + int ha_genrandom(unsigned char* data, size_t len) { int r, dd; diff --git a/daemon/pgsql.c b/daemon/pgsql.c new file mode 100644 index 0000000..225bc65 --- /dev/null +++ b/daemon/pgsql.c @@ -0,0 +1,749 @@ + +#include "usuals.h" +#include "httpauthd.h" +#include "md5.h" +#include "sha1.h" +#include "bd.h" + +#include <sys/time.h> + +/* LDAP library */ +#include <libpq-fe.h> + +/* ------------------------------------------------------------------------------- + * Structures + */ + +#define DB_PW_CLEAR 0 +#define DB_PW_SHA1 1 +#define DB_PW_MD5 2 +#define DB_PW_CRYPT 3 + +/* Our hanler context */ +typedef struct pgsql_context +{ + /* Base Handler ------------------------------------------------------ */ + bd_context_t bd; + + /* Settings ---------------------------------------------------------- */ + const char* host; /* The connection host or path */ + const char* port; /* The connection port */ + const char* user; /* The pgsql user name */ + const char* password; /* The pgsql password */ + const char* database; /* The database name */ + const char* query; /* The query */ + const char* pw_column; /* The database query to retrieve a password */ + int pw_type; /* The type of password encoded in database */ + const char* ha1_column; /* The database query to retrieve a ha1 */ + + int pgsql_max; /* Number of open connections allowed */ + int pgsql_timeout; /* Maximum amount of time to dedicate to an ldap query */ + + /* Context ----------------------------------------------------------- */ + PGconn** pool; /* Pool of available connections */ + int pool_mark; /* Amount of connections allocated */ +} +pgsql_context_t; + +/* Forward declarations for callbacks */ +static int validate_digest(ha_request_t* rq, const char* user, digest_context_t* dg); +static int validate_basic(ha_request_t* rq, const char* user, const char* password); +static void escape_pgsql(const ha_request_t* rq, ha_buffer_t* buf, const char* value); + +/* The defaults for the context */ +static const pgsql_context_t pgsql_defaults = +{ + BD_CALLBACKS(validate_digest, + validate_basic, escape_pgsql), + NULL, /* host */ + 0, /* port */ + NULL, /* user */ + NULL, /* password */ + NULL, /* database */ + NULL, /* query */ + NULL, /* pw_attr */ + DB_PW_CLEAR, /* pw_type */ + NULL, /* ha1_attr */ + 10, /* pgsql_max */ + 30, /* pgsql_timeout */ + NULL, /* pool */ + 0 /* pool_mark */ +}; + + +/* ------------------------------------------------------------------------------- + * Internal Functions + */ + +static void escape_pgsql(const ha_request_t* rq, ha_buffer_t* buf, const char* value) +{ + size_t len; + char* t; + + ASSERT(value); + + len = strlen(value); + + /* Bit of a hack, we copy the string in twice to give enough room. */ + if((t = (char*)ha_bufmalloc(buf, (len * 2) + 1)) != NULL) + { + PQescapeString(t, value, len); + ha_bufcpy(buf, t); + } +} + +static int dec_pgsql_binary(const ha_request_t* rq, const char* enc, + unsigned char* bytes, size_t len) +{ + size_t enclen; + void* d; + + ASSERT(rq && enc && bytes && len); + enclen = strlen(enc); + + /* Hex encoded */ + if(enclen == (len * 2)) + { + d = ha_bufdechex(rq->buf, enc, &enclen); + + if(d && enclen == len) + { + ha_messagex(rq, LOG_DEBUG, "found value in hex encoded format"); + memcpy(bytes, d, len); + return HA_OK; + } + } + + /* Raw binary postgres encoded */ + d = PQunescapeBytea(enc, &enclen); + if(d != NULL) + { + if(enclen == len) + memcpy(bytes, d, len); + + PQfreemem(d); + + if(enclen == len) + { + ha_messagex(rq, LOG_DEBUG, "found value in raw binary format"); + return HA_OK; + } + } + + /* B64 Encoded */ + enclen = strlen(enc); + d = ha_bufdec64(rq->buf, enc, &enclen); + + if(d && len == enclen) + { + ha_messagex(rq, LOG_DEBUG, "found value in b64 encoded format"); + memcpy(bytes, d, len); + return HA_OK; + } + + return ha_buferr(rq->buf) ? HA_CRITERROR : HA_FALSE; +} + +static int validate_ha1(ha_request_t* rq, pgsql_context_t* ctx, const char* user, + const char* clearpw, const char* dbpw) +{ + unsigned char dbha1[MD5_LEN]; + unsigned char ha1[MD5_LEN]; + const char* p; + + int r = dec_pgsql_binary(rq, dbpw, dbha1, MD5_LEN); + + if(r < 0) + return r; + + if(r == HA_OK) + { + digest_makeha1(ha1, user, rq->context->realm, clearpw); + if(memcmp(ha1, dbha1, MD5_LEN) == 0) + return HA_OK; + } + + return HA_FALSE; +} + +static int validate_password(ha_request_t* rq, pgsql_context_t* ctx, const char* user, + const char* clearpw, const char* dbpw) +{ + unsigned char buf[SHA1_LEN]; + const char* p; + int r; + + ASSERT(SHA1_LEN > MD5_LEN); + + switch(ctx->pw_type) + { + + /* Clear text */ + case DB_PW_CLEAR: + + if(strcmp(clearpw, dbpw) == 0) + { + ha_messagex(rq, LOG_DEBUG, "found matching clear text password"); + return HA_OK; + } + + break; + + /* Crypt pw */ + case DB_PW_CRYPT: + + ha_lock(); + p = crypt(clearpw, dbpw); + ha_unlock(); + + if(p && strcmp(clearpw, p) == 0) + { + ha_messagex(rq, LOG_DEBUG, "found matching crypt password"); + return HA_OK; + } + + break; + + /* MD5 */ + case DB_PW_MD5: + + r = dec_pgsql_binary(rq, dbpw, buf, MD5_LEN); + if(r < 0) return r; + + if(r == HA_OK && md5_strcmp(buf, clearpw) == 0) + { + ha_messagex(rq, LOG_DEBUG, "found matching md5 password"); + return HA_OK; + } + + break; + + + /* SHA1 */ + case DB_PW_SHA1: + + r = dec_pgsql_binary(rq, dbpw, buf, SHA1_LEN); + if(r < 0) return r; + + if(r == HA_OK && sha1_strcmp(buf, clearpw) == 0) + { + ha_messagex(rq, LOG_DEBUG, "found matching sha1 password"); + return HA_OK; + } + + break; + + default: + ASSERT(0 && "invalid password type"); + break; + }; + + return HA_FALSE; +} + +static PGconn* get_pgsql_connection(const ha_request_t* rq, pgsql_context_t* ctx) +{ + PGconn* pg; + int i; + + ASSERT(ctx); + + for(i = 0; i < ctx->pgsql_max; i++) + { + /* An open connection in the pool */ + if(ctx->pool[i]) + { + ha_messagex(rq, LOG_DEBUG, "using cached pgsql connection"); + pg = ctx->pool[i]; + ctx->pool[i] = NULL; + return pg; + } + } + + if(ctx->pool_mark >= ctx->pgsql_max) + { + ha_messagex(rq, LOG_ERR, "too many open pgsql connections"); + return NULL; + } + + pg = PQsetdbLogin(ctx->host, ctx->port, NULL, NULL, ctx->database, + ctx->user, ctx->password); + if(!pg) + { + ha_messagex(rq, LOG_CRIT, "internal error in postgres library"); + return NULL; + } + + if(PQstatus(pg) == CONNECTION_BAD) + { + ha_messagex(rq, LOG_ERR, "error opening pgsql connection: %s", PQerrorMessage(pg)); + PQfinish(pg); + return NULL; + } + + ctx->pool_mark++; + ha_messagex(rq, LOG_DEBUG, "opened new pgsql connection (total %d)", ctx->pool_mark); + return pg; +} + +static void discard_pgsql_connection(const ha_request_t* rq, pgsql_context_t* ctx, PGconn* pg) +{ + PQfinish(pg); + ctx->pool_mark--; + ha_messagex(rq, LOG_DEBUG, "discarding pgsql connection (total %d)", ctx->pool_mark); +} + +static void save_pgsql_connection(const ha_request_t* rq, pgsql_context_t* ctx, PGconn* pg) +{ + int i, e; + + ASSERT(ctx); + + if(!pg) + return; + + /* Make sure it's worth saving */ + if(PQstatus(pg) != CONNECTION_BAD) + { + for(i = 0; i < ctx->pgsql_max; i++) + { + /* An open connection in the pool */ + if(!ctx->pool[i]) + { + ha_messagex(rq, LOG_DEBUG, "caching pgsql connection for later use"); + ctx->pool[i] = pg; + pg = NULL; + break; + } + } + } + + if(pg != NULL) + discard_pgsql_connection(rq, ctx, pg); +} + +static int check_pgsql_result(ha_request_t* rq, PGresult* res) +{ + switch(PQresultStatus(res)) + { + case PGRES_COMMAND_OK: + case PGRES_EMPTY_QUERY: + ha_messagex(rq, LOG_WARNING, "query did not return data"); + return HA_FALSE; + case PGRES_TUPLES_OK: + return HA_OK; + case PGRES_BAD_RESPONSE: + ha_messagex(rq, LOG_ERR, "error communicating with pgsql server"); + return HA_FAILED; + case PGRES_NONFATAL_ERROR: + ha_messagex(rq, LOG_ERR, "error querying database: %s", PQresultErrorMessage(res)); + return HA_FAILED; + case PGRES_FATAL_ERROR: + ha_messagex(rq, LOG_CRIT, "internal error in postgres library"); + return HA_CRITERROR; + case PGRES_COPY_OUT: + case PGRES_COPY_IN: + default: + ASSERT(0 && "unexpected response"); + return HA_FAILED; + }; + + return HA_OK; +} + +static int resolve_column(PGresult* res, const char* column) +{ + int i; + + if(column) + { + for(i = 0; i < PQnfields(res); i++) + { + if(strcasecmp(column, PQfname(res, i)) == 0) + return i; + } + } + + return -1; +} + +static int retrieve_user_rows(ha_request_t* rq, pgsql_context_t* ctx, + const char* user, PGresult** results) +{ + PGconn* pg = NULL; + PGresult* res = NULL; + const char* query; + int ret = HA_OK; + + ASSERT(rq && ctx && user && res); + *results = NULL; + + pg = get_pgsql_connection(rq, ctx); + if(!pg) + { + ret = HA_FAILED; + goto finally; + } + + ASSERT(ctx->query); + + /* The map can have %u and %r to denote user and realm */ + query = bd_substitute(rq, user, ctx->query); + if(!query) + { + ret = HA_CRITERROR; + goto finally; + } + + ha_messagex(rq, LOG_DEBUG, "executing query: %s", query); + res = PQexec(pg, query); + + + ret = check_pgsql_result(rq, res); + if(ret != HA_OK) + goto finally; + + if(PQntuples(res) == 0) + { + ha_messagex(rq, LOG_WARNING, "login failed. couldn't find user: %s", user); + ret = HA_FALSE; + goto finally; + } + + if(PQnfields(res) <= 0) + { + ha_messagex(rq, LOG_ERR, "query returned 0 columns: %s", query); + ret = HA_FAILED; + goto finally; + } + + *results = res; + ha_messagex(rq, LOG_DEBUG, "received %d result rows", PQntuples(res)); + +finally: + + /* According to libpg we can close/save the connection + * before the returned results are freed, no worries there */ + if(pg != NULL) + save_pgsql_connection(rq, ctx, pg); + + return ret; +} + +static int validate_digest(ha_request_t* rq, const char* user, digest_context_t* dg) +{ + pgsql_context_t* ctx = (pgsql_context_t*)rq->context->ctx_data; + PGresult* res = NULL; + int ret = HA_FALSE; + int pw_column = -1; + int ha1_column = -1; + int r, i, foundany = 0; + + ASSERT(rq && user && dg); + + ret = retrieve_user_rows(rq, ctx, user, &res); + if(ret != HA_OK) + goto finally; + + ASSERT(res); + + pw_column = resolve_column(res, ctx->pw_column); + ha1_column = resolve_column(res, ctx->ha1_column); + + if(pw_column == -1 && ha1_column == -1) + { + if(PQnfields(res) > 1) + ha_messagex(rq, LOG_WARNING, "query returned more than 1 column, using first as password"); + + pw_column = 0; + } + + for(i = 0; i < PQntuples(res); i++) + { + if(pw_column != -1) + { + if(ctx->pw_type == DB_PW_CLEAR && !PQgetisnull(res, i, pw_column)) + { + foundany = 1; + + digest_makeha1(dg->ha1, user, rq->context->realm, PQgetvalue(res, i, pw_column)); + + /* Run the actual check */ + ret = digest_complete_check(dg, rq->buf); + + if(ret != HA_FALSE) + goto finally; + } + } + + if(ha1_column != -1) + { + if(!PQgetisnull(res, i, ha1_column)) + { + ret = dec_pgsql_binary(rq, PQgetvalue(res, i, ha1_column), dg->ha1, MD5_LEN); + if(ret < 0) + goto finally; + else if(ret == HA_FALSE) + continue; + + foundany = 1; + + /* Run the actual check */ + ret = digest_complete_check(dg, rq->buf); + + if(ret != HA_FALSE) + goto finally; + } + } + } + + if(!foundany) + ha_messagex(rq, LOG_WARNING, "no clear password or ha1 present for user: %s", user); + +finally: + if(res) + PQclear(res); + + return ret; +} + +static int validate_basic(ha_request_t* rq, const char* user, const char* password) +{ + pgsql_context_t* ctx = (pgsql_context_t*)rq->context->ctx_data; + PGresult* res = NULL; + int ret = HA_FALSE; + int pw_column = -1; + int ha1_column = -1; + int i, foundany = 0; + + ASSERT(rq && user && password); + + ret = retrieve_user_rows(rq, ctx, user, &res); + if(ret != HA_OK) + goto finally; + + ASSERT(res); + + pw_column = resolve_column(res, ctx->pw_column); + ha1_column = resolve_column(res, ctx->ha1_column); + + if(pw_column == -1 && ha1_column == -1) + { + if(PQnfields(res) > 1) + ha_messagex(rq, LOG_WARNING, "query returned more than 1 column, using first as password"); + pw_column = 0; + } + + + for(i = 0; i < PQntuples(res); i++) + { + if(pw_column != -1) + { + if(!PQgetisnull(res, i, pw_column)) + { + foundany = 1; + ret = validate_password(rq, ctx, user, password, PQgetvalue(res, i, pw_column)); + if(ret != HA_FALSE) + goto finally; + } + } + + if(ha1_column != -1) + { + if(!PQgetisnull(res, i, ha1_column)) + { + foundany = 1; + ret = validate_ha1(rq, ctx, user, password, PQgetvalue(res, i, ha1_column)); + if(ret != HA_FALSE) + goto finally; + } + } + } + + if(!foundany) + ha_messagex(rq, LOG_WARNING, "no password present for user: %s", user); + +finally: + if(res) + PQclear(res); + + return ret; +} + + +/* ------------------------------------------------------------------------------- + * Handler Functions + */ + +int pgsql_config(ha_context_t* context, const char* name, const char* value) +{ + pgsql_context_t* ctx = (pgsql_context_t*)(context->ctx_data); + + ASSERT(name && value && value[0]); + + if(strcmp(name, "dbserver") == 0) + { + ctx->host = value; + return HA_OK; + } + + if(strcmp(name, "dbport") == 0) + { + ctx->port = value; + return HA_OK; + } + + if(strcmp(name, "dbuser") == 0) + { + ctx->user = value; + return HA_OK; + } + + if(strcmp(name, "dbpassword") == 0) + { + ctx->password = value; + return HA_OK; + } + + if(strcmp(name, "dbdatabase") == 0) + { + ctx->database = value; + return HA_OK; + } + + if(strcmp(name, "dbquery") == 0) + { + ctx->query = value; + return HA_OK; + } + + if(strcmp(name, "dbpwcolumn") == 0) + { + ctx->pw_column = value; + return HA_OK; + } + + if(strcmp(name, "dbpwtype") == 0) + { + if(strcmp(value, "clear") == 0) + ctx->pw_type = DB_PW_CLEAR; + else if(strcmp(value, "crypt") == 0) + ctx->pw_type = DB_PW_CRYPT; + else if(strcmp(value, "md5") == 0) + ctx->pw_type = DB_PW_MD5; + else if(strcmp(value, "sha1") == 0) + ctx->pw_type = DB_PW_SHA1; + else + { + ha_messagex(NULL, LOG_ERR, "invalid value for '%s' (must be 'clear', 'crypt', 'md5' or 'sha1')", name); + return HA_FAILED; + } + } + + if(strcmp(name, "dbha1column") == 0) + { + ctx->ha1_column = value; + return HA_OK; + } + + else if(strcmp(name, "dbmax") == 0) + { + return ha_confint(name, value, 1, 256, &(ctx->pgsql_max)); + } + + else if(strcmp(name, "dbtimeout") == 0) + { + /* TODO: Implement database timeouts */ + return ha_confint(name, value, 0, 86400, &(ctx->pgsql_timeout)); + } + + return HA_FALSE; +} + +int pgsql_init(ha_context_t* context) +{ + int r; + + if((r = bd_init(context)) != HA_OK) + return r; + + /* Context specific initialization */ + if(context) + { + pgsql_context_t* ctx = (pgsql_context_t*)(context->ctx_data); + ASSERT(ctx); + + /* Check for mandatory configuration */ + if(!ctx->database || !ctx->query) + { + ha_messagex(NULL, LOG_ERR, "configuration incomplete. " + "Must have DBDatabase and DBQuery."); + return HA_FAILED; + } + + ASSERT(!ctx->pool); + ASSERT(ctx->pgsql_max > 0); + + /* + * Our connection pool. It's the size of our maximum + * amount of pending connections as that's the max + * we'd be able to use at a time anyway. + */ + ctx->pool = (PGconn**)malloc(sizeof(PGconn*) * ctx->pgsql_max); + if(!ctx->pool) + { + ha_messagex(NULL, LOG_CRIT, "out of memory"); + return HA_CRITERROR; + } + + memset(ctx->pool, 0, sizeof(PGconn*) * ctx->pgsql_max); + ha_messagex(NULL, LOG_INFO, "initialized pgsql handler"); + } + + return HA_OK; +} + +void pgsql_destroy(ha_context_t* context) +{ + if(context) + { + /* Note: We don't need to be thread safe here anymore */ + pgsql_context_t* ctx = (pgsql_context_t*)(context->ctx_data); + int i; + + ASSERT(ctx); + + if(ctx->pool) + { + /* Close any connections we have open */ + for(i = 0; i < ctx->pgsql_max; i++) + { + if(ctx->pool[i]) + PQfinish(ctx->pool[i]); + } + + /* And free the connection pool */ + free(ctx->pool); + } + } + + bd_destroy(context); + ha_messagex(NULL, LOG_INFO, "uninitialized pgsql handler"); +} + + + +/* ------------------------------------------------------------------------------- + * Handler Definition + */ + +ha_handler_t pgsql_handler = +{ + "PGSQL", /* The type */ + pgsql_init, /* Initialization function */ + pgsql_destroy, /* Uninitialization routine */ + pgsql_config, /* Config routine */ + bd_process, /* Processing routine */ + &pgsql_defaults, /* The context defaults */ + sizeof(pgsql_context_t) +}; diff --git a/daemon/simple.c b/daemon/simple.c index bda60c3..8e4f8df 100644 --- a/daemon/simple.c +++ b/daemon/simple.c @@ -29,13 +29,13 @@ typedef struct simple_context simple_context_t; /* Forward declarations for callbacks */ -static int complete_digest(ha_request_t* rq, const char* user, unsigned char* ha1); +static int validate_digest(ha_request_t* rq, const char* user, digest_context_t* dg); static int validate_basic(ha_request_t* rq, const char* user, const char* password); /* The defaults for the context */ static const simple_context_t simple_defaults = { - BD_CALLBACKS(complete_digest, validate_basic), + BD_CALLBACKS(validate_digest, validate_basic, NULL), NULL /* filename */ }; @@ -43,7 +43,7 @@ static const simple_context_t simple_defaults = * Internal Functions */ -static int complete_digest(ha_request_t* rq, const char* user, unsigned char* ha1) +static int validate_digest(ha_request_t* rq, const char* user, digest_context_t* dg) { simple_context_t* ctx = (simple_context_t*)rq->context->ctx_data; FILE* f; @@ -52,6 +52,8 @@ static int complete_digest(ha_request_t* rq, const char* user, unsigned char* ha size_t len; char line[SIMPLE_MAXLINE]; int ret = HA_FALSE; + int foundinvalid = 0; + int foundgood = 0; ASSERT(ctx && rq && user && user[0]); ha_messagex(rq, LOG_DEBUG, "searching password file for user's ha1: %s", user); @@ -59,7 +61,7 @@ static int complete_digest(ha_request_t* rq, const char* user, unsigned char* ha f = fopen(ctx->filename, "r"); if(!f) { - ha_message(rq, LOG_ERR, "can't open file for basic auth: %s", ctx->filename); + ha_message(rq, LOG_ERR, "can't open file for digest auth: %s", ctx->filename); return HA_FAILED; } @@ -81,45 +83,63 @@ static int complete_digest(ha_request_t* rq, const char* user, unsigned char* ha } t = strchr(line, ':'); - if(t) + if(!t) + continue; + + /* Split the line */ + *t = 0; + t++; + + /* Check the user */ + if(strcmp(line, user) != 0) + continue; + + t2 = strchr(t, ':'); + if(!t2) { - /* 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, rq->context->realm) == 0) - { - len = MD5_LEN; - - /* Now try and decode the ha1 */ - t = ha_bufdechex(rq->buf, t2, &len); - if(t && len == MD5_LEN) - { - ha_messagex(rq, LOG_DEBUG, "found ha1 for user: %s", user); - memcpy(ha1, t, MD5_LEN); - ret = HA_OK; - break; - } - } - } - - if(!t2 || ret != HA_OK) - ha_messagex(rq, LOG_WARNING, "user '%s' found in file, but password not in digest format", user); - } + /* An invalid line without a realm */ + foundinvalid = 1; + continue; } + + /* Split the line */ + *t2 = 0; + t2++; + + /* Check the realm */ + if(strcmp(t, rq->context->realm) != 0) + continue; + + /* Now try and decode the ha1 */ + len = MD5_LEN; + t = ha_bufdechex(rq->buf, t2, &len); + + if(!t || len != MD5_LEN) + { + /* An invalid ha1 */ + foundinvalid = 1; + continue; + } + + ha_messagex(rq, LOG_DEBUG, "found ha1 for user: %s", user); + memcpy(dg->ha1, t, MD5_LEN); + foundgood = 1; + + /* Try to do the validation */ + ret = digest_complete_check(dg, rq->buf); + + /* If invalid then continue search */ + if(ret == HA_FALSE) + continue; + + /* Success or an error ends here */ + break; } + + if(foundinvalid && !foundgood) + ha_messagex(rq, LOG_WARNING, "user '%s' found in file, but password not in digest format", user); + fclose(f); if(ha_buferr(rq->buf)) @@ -173,57 +193,61 @@ static int validate_basic(ha_request_t* rq, const char* user, const char* passwo trim_end(line); t = strchr(line, ':'); - if(t) + if(!t) + continue; + + /* Split the line */ + *t = 0; + t++; + + /* Check the user */ + if(strcmp(line, user) != 0) + continue; + + /* Not sure if crypt is thread safe so we lock */ + ha_lock(NULL); + + /* Check the password */ + t2 = crypt(password, t); + + ha_unlock(NULL); + + if(strcmp(t2, t) == 0) { - /* 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(password, t); - - ha_unlock(NULL); - - if(strcmp(t2, t) == 0) - { - ha_messagex(rq, LOG_DEBUG, "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, rq->context->realm) == 0) - { - len = MD5_LEN; - - /* Now try antd decode the ha1 */ - t = ha_bufdechex(rq->buf, t2, &len); - if(t && len == MD5_LEN && memcmp(ha1, t, MD5_LEN) == 0) - { - ha_messagex(rq, LOG_DEBUG, "found valid ha1 for user: %s", user); - ret = HA_OK; - break; - } - } - } - - if(ha_buferr(rq->buf)) - break; - } + ha_messagex(rq, LOG_DEBUG, "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) + continue; + + /* Split the line */ + *t2 = 0; + t2++; + + /* Check the realm */ + if(strcmp(t, rq->context->realm) != 0) + continue; + + /* Now try antd decode the ha1 */ + len = MD5_LEN; + + t = ha_bufdechex(rq->buf, t2, &len); + if(!t || len != MD5_LEN) + continue; + + if(memcmp(ha1, t, MD5_LEN) == 0) + { + ha_messagex(rq, LOG_DEBUG, "found valid ha1 for user: %s", user); + ret = HA_OK; + break; + } + + if(ha_buferr(rq->buf)) + break; } fclose(f); |