/* * HttpAuth * * Copyright (C) 2004 Stefan Walter * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the * Free Software Foundation, Inc., * 59 Temple Place, Suite 330, * Boston, MA 02111-1307, USA. */ #include "usuals.h" #include "httpauthd.h" #include "hash.h" #include "defaults.h" #include "md5.h" #include "basic.h" #include "stringx.h" #include /* The NTLM headers */ #include "ntlmssp.h" /* ------------------------------------------------------------------------------- * Defaults and Constants */ #define NTLM_HASH_KEY_LEN MD5_LEN #define NTLM_ESTABLISHED (void*)1 /* ------------------------------------------------------------------------------- * Structures and Globals */ /* A pending connection */ typedef struct ntlm_connection { void* handle; unsigned char nonce[NONCE_LEN]; unsigned int flags; } ntlm_connection_t; /* The main context */ typedef struct ntlm_context { /* Read Only --------------------------------------------------------- */ const char* server; /* Server to authenticate against */ const char* domain; /* NTLM domain to authenticate against */ const char* backup; /* Backup server if primary is down */ int pending_max; /* Maximum number of connections at once */ int pending_timeout; /* Timeout for authentication (in seconds) */ /* Require Locking --------------------------------------------------- */ hsh_t* pending; /* Pending connections */ hsh_t* established; /* Established connections */ } ntlm_context_t; /* The default context settings */ static const ntlm_context_t ntlm_defaults = { NULL, NULL, NULL, DEFAULT_PENDING_MAX, DEFAULT_PENDING_TIMEOUT, NULL, NULL }; /* Mutexes for accessing non-thread-safe smblib */ static pthread_mutex_t g_smblib_mutex; static pthread_mutexattr_t g_smblib_mutexattr; /* ------------------------------------------------------------------------------- * Internal Functions */ static ntlm_connection_t* makeconnection(ha_request_t* rq, ntlm_context_t* ctx) { ntlm_connection_t* conn; ASSERT(ctx); conn = (ntlm_connection_t*)malloc(sizeof(ntlm_connection_t)); if(!conn) { ha_messagex(NULL, LOG_CRIT, "out of memory"); return NULL; } memset(conn, 0, sizeof(*conn)); ha_lock(&g_smblib_mutex); /* * Open a connection to to the domain controller. I don't think * we can cache these connections or use them again as opening * a connection here results in an nonce being generated. */ conn->handle = ntlmssp_connect(ctx->server, ctx->backup, ctx->domain, (char*)conn->nonce); ha_unlock(&g_smblib_mutex); if(!conn->handle) { ha_messagex(rq, LOG_ERR, "couldn't connect to the domain server %s (backup: %s)", ctx->server, ctx->backup ? ctx->backup : "none"); free(conn); return NULL; } ha_messagex(rq, LOG_INFO, "established connection to server"); return conn; } static void freeconnection(ha_request_t* rq, ntlm_connection_t* conn) { ASSERT(conn); if(conn->handle) { ha_lock(&g_smblib_mutex); ntlmssp_disconnect(conn->handle); conn->handle = NULL; ha_unlock(&g_smblib_mutex); ha_messagex(rq, LOG_DEBUG, "disconnected from server"); } free(conn); } static void free_hash_object(void* arg, void* val) { if(val) { ASSERT(val != NTLM_ESTABLISHED); freeconnection(NULL, (ntlm_connection_t*)val); } } static void free_string(void* arg, void* val) { free (val); } static ntlm_connection_t* getpending(ntlm_context_t* ctx, const void* key) { ntlm_connection_t* ret; ASSERT(ctx && key); ha_lock(NULL); ret = (ntlm_connection_t*)hsh_rem(ctx->pending, key); ha_unlock(NULL); return ret; } static int putpending(ntlm_context_t* ctx, const void* key, ntlm_connection_t* conn) { int r = 0; ASSERT(ctx && key && conn); ASSERT(conn->handle); ha_lock(NULL); if(!hsh_get(ctx->pending, key)) { if(!hsh_set(ctx->pending, key, (void*)conn)) { free_hash_object(NULL, conn); ha_messagex(NULL, LOG_ERR, "out of memory"); r = -1; } } ha_unlock(NULL); return r; } int ntlm_auth_basic(ha_request_t* rq, ntlm_context_t* ctx, unsigned char* key, const char* header) { ntlm_connection_t* conn; char* t; basic_header_t basic; const char* domain = NULL; int found = 0; int r; ASSERT(ctx && key && header && rq); /* * We're doing basic authentication on the connection * which invalidates any NTLM authentication we've started * or done on this connection. */ conn = getpending(ctx, key); if(conn) { ha_messagex(rq, LOG_WARNING, "basic auth killed a pending ntlm auth in progress"); freeconnection(rq, conn); } if((r = basic_parse(header, rq->buf, &basic)) < 0) return r; /* Check and see if this connection is in the cache */ ha_lock(NULL); if(hsh_get(ctx->established, basic.key) != NULL) found = 1; ha_unlock(NULL); if(found) { ha_messagex(rq, LOG_NOTICE, "validated basic user against cache: %s", basic.user); } else { /* Try to find a domain in the user */ if((t = strchr(basic.user, '\\')) != NULL || (t = strchr(basic.user, '/')) != NULL) { /* Break at the domain */ domain = basic.user; basic.user = t + 1; *t = 0; /* Make sure this is our domain */ if(strcasecmp(domain, ctx->domain) != 0) domain = NULL; } if(!domain) { /* Use the default domain if none specified */ domain = ctx->domain; } /* Make sure above did not fail */ if(basic.user && basic.user[0] && basic.password && domain && domain[0]) { ha_messagex(rq, LOG_DEBUG, "checking user against server: %s", basic.user); /* We need to lock to go into smblib */ ha_lock(&g_smblib_mutex); /* Found in smbval/valid.h */ if(ntlmssp_validuser(basic.user, basic.password, ctx->server, ctx->backup, domain) == NTV_NO_ERROR) { /* If valid then we return */ found = 1; } ha_unlock(&g_smblib_mutex); } if(found) ha_messagex(rq, LOG_NOTICE, "validated basic user against server: %s", basic.user); } if(found) { int r; rq->resp_code = HA_SERVER_OK; rq->resp_detail = basic.user; ha_lock(NULL); /* We put this connection into the successful connections */ r = hsh_set(ctx->established, basic.key, strdup(basic.user)); ha_unlock(NULL); if(!r) { ha_messagex(NULL, LOG_CRIT, "out of memory"); return HA_CRITERROR; } return HA_OK; } return HA_FALSE; } int ntlm_auth_ntlm(ha_request_t* rq, ntlm_context_t* ctx, void* key, const char* header) { ntlmssp_info_rec ntlmssp; ntlm_connection_t* conn = NULL; unsigned int flags = 0; int ret = HA_FALSE; size_t len = 0; void* d; int r; ASSERT(ctx && key && header && rq); /* * Retrieve and remove the connection from the pending bag. * We add it back again below if that's necessary. */ conn = getpending(ctx, key); /* * We use the flags from an already established connection * if we've been pending and stuff */ if(conn && conn->flags) flags = conn->flags; /* * First we figure out what kind of message the client * is sending us. */ d = ha_bufdec64(rq->buf, header, &len); if(!d || len == 0) RETURN(HA_FALSE); r = ntlmssp_decode_msg(&ntlmssp, d, len, &flags); if(r != 0) { ha_messagex(rq, LOG_WARNING, "decoding NTLMSSP message failed (error %d)", r); rq->resp_code = HA_SERVER_BADREQ; RETURN(HA_FALSE); } switch(ntlmssp.msg_type) { /* An initial NTLM request? */ case 1: { /* Win9x doesn't seem to send a domain or host */ int win9x = !ntlmssp.host[0] && !ntlmssp.domain[0]; /* * If we already have a connection to the domain controller * then we're in trouble. Basically this is the second * type 1 message we've received over this connection. We allow * the second message to authenticate. */ if(conn) freeconnection(rq, conn); /* * Check how many connections we have to the domain controller * and if too many then cut off here. */ if(ctx->pending_max != -1) { ha_lock(NULL); if(hsh_count(ctx->pending) >= ctx->pending_max) hsh_bump(ctx->pending); ha_unlock(NULL); } /* * Open a connection to to the domain controller. I don't think * we can cache these connections or use them again as opening * a connection here results in an nonce being generated. */ conn = makeconnection(rq, ctx); if(!conn) RETURN(HA_FAILED); /* Save away any flags given us by ntlm_decode_msg */ conn->flags = flags; /* Start building the header */ ha_bufcpy(rq->buf, HA_PREFIX_NTLM); if(win9x) { struct ntlm_msg2_win9x msg_win9x; ntlmssp_encode_msg2_win9x(conn->nonce, &msg_win9x, (char*)ctx->domain, flags); ha_bufjoin(rq->buf); ha_bufenc64(rq->buf, (unsigned char*)&msg_win9x, sizeof(msg_win9x)); } else { struct ntlm_msg2 msg; ntlmssp_encode_msg2(conn->nonce, &msg); ha_bufjoin(rq->buf); ha_bufenc64(rq->buf, (unsigned char*)&msg, sizeof(msg)); } if(CHECK_RBUF(rq)) RETURN(HA_CRITERROR); /* Cache this connection in our pending set ... */ r = putpending(ctx, key, conn); /* * By marking this as null, the cleanup code * won't free the connection since it's been * cached above. */ conn = NULL; if(r < 0) { RETURN(HA_CRITERROR); } else { ha_messagex(rq, LOG_DEBUG, "sending ntlm challenge"); ha_addheader(rq, "WWW-Authenticate", ha_bufdata(rq->buf)); rq->resp_code = HA_SERVER_DECLINE; RETURN(HA_FALSE); } } /* A response to a challenge */ case 3: { /* * We need to have a connection at this point or this whole thing * has come in in the wrong order. Actually it's a client error * for stuff to come in wrong. But since some web servers also * kill keep-alives and stuff, we forgive and just ask the client * for the authentication info again. */ if(!conn || !conn->handle) { ha_messagex(rq, LOG_WARNING, "received out of order NTLM response from client. Make sure 'keep alives' are on."); rq->resp_code = HA_SERVER_BADREQ; RETURN(HA_FALSE); } if(!ntlmssp.user) { ha_messagex(rq, LOG_WARNING, "received NTLM response without user name"); rq->resp_code = HA_SERVER_BADREQ; RETURN(HA_FALSE); } /* We have to lock while going into smblib */ ha_lock(&g_smblib_mutex); /* Now authenticate them against the DC */ r = ntlmssp_auth(conn->handle, (const char*)ntlmssp.user, (const char*)ntlmssp.nt, 1); ha_unlock(&g_smblib_mutex); /* The connection gets disconnected below */ if(r == NTV_LOGON_ERROR) { /* * Note that we don't set a code here. This causes our * caller to put in all the proper headers for us. */ ha_messagex(rq, LOG_WARNING, "failed NTLM logon for user '%s'", ntlmssp.user); RETURN(HA_FALSE); } /* A successful login ends here */ else { int r; char *user = strdup((char*)ntlmssp.user); rq->resp_detail = user; rq->resp_code = HA_SERVER_OK; ha_messagex(rq, LOG_NOTICE, "validated ntlm user against server: %s", ntlmssp.user); ha_lock(NULL); /* We put this connection into the successful connections */ r = hsh_set(ctx->established, key, user); ha_unlock(NULL); if(!r) { ha_messagex(NULL, LOG_CRIT, "out of memory"); RETURN(HA_CRITERROR); } RETURN(HA_OK); } } break; default: ha_messagex(rq, LOG_WARNING, "received invalid NTLM message (type %d)", ntlmssp.msg_type); rq->resp_code = HA_SERVER_BADREQ; RETURN(HA_FALSE); }; finally: if(CHECK_RBUF(rq)) ret = HA_CRITERROR; if(conn) freeconnection(rq, conn); return ret; } /* ------------------------------------------------------------------------------- * Handler Functions */ int ntlm_config(ha_context_t* context, const char* name, const char* value) { ntlm_context_t* ctx = (ntlm_context_t*)(context->ctx_data); ASSERT(name && value && value[0]); if(strcmp(name, "ntlmserver") == 0) { ctx->server = value; return HA_OK; } else if(strcmp(name, "ntlmbackup") == 0) { ctx->backup = value; return HA_OK; } else if(strcmp(name, "ntlmdomain") == 0) { ctx->domain = value; return HA_OK; } else if(strcmp(name, "pendingmax") == 0) { return ha_confint(name, value, 1, 256, &(ctx->pending_max)); } else if(strcmp(name, "pendingtimeout") == 0) { return ha_confint(name, value, 1, 86400, &(ctx->pending_timeout)); } return HA_FALSE; } int ntlm_init(ha_context_t* context) { /* Per context initialization */ if(context) { ntlm_context_t* ctx = (ntlm_context_t*)(context->ctx_data); hsh_table_calls_t htc; ASSERT(ctx); /* Make sure there are some types of authentication we can do */ if(!(context->allowed_types & (HA_TYPE_BASIC | HA_TYPE_NTLM))) { ha_messagex(NULL, LOG_ERR, "NTLM module configured, but does not implement any " "configured authentication type."); return HA_FAILED; } /* Check for mandatory configuration */ if(!(ctx->server) || !(ctx->domain)) { ha_messagex(NULL, LOG_ERR, "NTLM configuration incomplete. " "Must have NTLMServer and NTLMDomain configured."); return HA_FAILED; } ASSERT(!ctx->pending); ASSERT(!ctx->established); /* Initialize our tables */ if(!(ctx->pending = hsh_create(NTLM_HASH_KEY_LEN)) || !(ctx->established = hsh_create(NTLM_HASH_KEY_LEN))) { ha_messagex(NULL, LOG_CRIT, "out of memory"); return HA_CRITERROR; } htc.f_freeval = free_hash_object; htc.arg = NULL; hsh_set_table_calls(ctx->pending, &htc); htc.f_freeval = free_string; htc.arg = NULL; hsh_set_table_calls(ctx->established, &htc); ha_messagex(NULL, LOG_INFO, "initialized ntlm handler"); } /* Global Initialization */ else { /* Create the smblib mutex */ if(pthread_mutexattr_init(&g_smblib_mutexattr) != 0 || pthread_mutexattr_settype(&g_smblib_mutexattr, HA_MUTEX_TYPE) || pthread_mutex_init(&g_smblib_mutex, &g_smblib_mutexattr) != 0) { ha_messagex(NULL, LOG_CRIT, "threading problem. can't create mutex"); return HA_CRITERROR; } } return HA_OK; } void ntlm_destroy(ha_context_t* context) { /* Per context destroy */ if(context) { /* Note: We don't need to be thread safe here anymore */ ntlm_context_t* ctx = (ntlm_context_t*)(context->ctx_data); if(ctx->pending) hsh_free(ctx->pending); if(ctx->established) hsh_free(ctx->established); ha_messagex(NULL, LOG_INFO, "uninitialized handler"); } /* Global Destroy */ else { /* Close the mutex */ pthread_mutex_destroy(&g_smblib_mutex); pthread_mutexattr_destroy(&g_smblib_mutexattr); } } int ntlm_process(ha_request_t* rq) { ntlm_context_t* ctx = (ntlm_context_t*)(rq->context->ctx_data); void* ntlm_connection_t = NULL; unsigned char key[NTLM_HASH_KEY_LEN]; const char* header = NULL; time_t t = time(NULL); int ret = 0, r; char *user; ASSERT(rq); ASSERT(rq->req_args[AUTH_ARG_CONN]); rq->resp_code = -1; /* Hash the unique key */ md5_string(key, rq->req_args[AUTH_ARG_CONN]); ha_lock(NULL); /* * Purge out stale connection stuff. This includes * authenticated connections which have expired as * well as half open connections which expire. */ r = hsh_purge(ctx->pending, t - ctx->pending_timeout); r += hsh_purge(ctx->established, t - rq->context->cache_timeout); ha_unlock(NULL); if(r > 0) ha_messagex(rq, LOG_DEBUG, "purged info from cache: %d", r); /* Look for a NTLM header */ if(rq->context->allowed_types & HA_TYPE_NTLM) { header = ha_getheader(rq, "Authorization", HA_PREFIX_NTLM); if(header) { /* Trim off for decoding */ header = trim_start(header); ha_messagex(rq, LOG_DEBUG, "processing ntlm auth header"); ret = ntlm_auth_ntlm(rq, ctx, key, header); if(ret < 0) return ret; } } /* If basic is enabled, and no NTLM */ if(!header && rq->context->allowed_types & HA_TYPE_BASIC) { /* Look for a Basic header */ header = ha_getheader(rq, "Authorization", HA_PREFIX_BASIC); if(header) { /* Trim off for decoding */ header = trim_start(header); ha_messagex(rq, LOG_DEBUG, "processing basic auth header"); ret = ntlm_auth_basic(rq, ctx, key, header); if(ret < 0) return ret; } } /* The authorization header was not found, try cache (only for GET) */ if(!header && rq->req_args[AUTH_ARG_METHOD] && strcmp(rq->req_args[AUTH_ARG_METHOD], "GET") == 0) { ha_lock(NULL); /* * NTLM trusts a connection after it's been authenticated * so just pass success for those. Note that we do this * in the absence of a authorization header so that we * allow connections to be re-authenticated. */ user = hsh_get(ctx->established, key); if(user != NULL) { hsh_touch(ctx->established, key); rq->resp_code = HA_SERVER_OK; rq->resp_detail = user; } ha_unlock(NULL); if(rq->resp_code == HA_SERVER_OK) ha_messagex(rq, LOG_NOTICE, "validated user against connection cache: %s", user); } /* If nobody's set any other response then... */ if(rq->resp_code == -1) { /* If authentication failed tell the browser about it */ rq->resp_code = HA_SERVER_DECLINE; if(rq->context->allowed_types & HA_TYPE_NTLM) { ha_addheader(rq, "WWW-Authenticate", HA_PREFIX_NTLM); ha_messagex(rq, LOG_DEBUG, "sent ntlm auth request"); } if(rq->context->allowed_types & HA_TYPE_BASIC) { ha_bufmcat(rq->buf, HA_PREFIX_BASIC, "realm=\"", rq->context->realm, "\"", NULL); if(CHECK_RBUF(rq)) return HA_CRITERROR; ha_addheader(rq, "WWW-Authenticate", ha_bufdata(rq->buf)); ha_messagex(rq, LOG_DEBUG, "sent basic auth request"); } } return ret; } /* ------------------------------------------------------------------------------- * Handler Definition */ ha_handler_t ntlm_handler = { "NTLM", /* The type */ ntlm_init, /* Initialization function */ ntlm_destroy, /* Uninitialization routine */ ntlm_config, /* Config routine */ ntlm_process, /* Processing routine */ &ntlm_defaults, /* Default settings */ sizeof(ntlm_context_t) };