From 86e45cfbd0655193e363be6daadbfd5434566a03 Mon Sep 17 00:00:00 2001 From: Stef Walter Date: Thu, 12 Aug 2004 00:50:29 +0000 Subject: - Added postgresql database support - Lots of changes to properly abstract bd handlers - Handle multiple passwords and ha1s properly. --- daemon/pgsql.c | 749 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 749 insertions(+) create mode 100644 daemon/pgsql.c (limited to 'daemon/pgsql.c') 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 + +/* LDAP library */ +#include + +/* ------------------------------------------------------------------------------- + * 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) +}; -- cgit v1.2.3