#include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include "hash.h" #include "isasl.h" #include "memcached.h" #define MTIME_STABILITY_THRESHOLD 2 static struct stat prev_stat = { 0 }; static int mtime_stability_counter; static pthread_mutex_t uhash_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t sasl_db_thread_lock = PTHREAD_MUTEX_INITIALIZER; static bool run_sasl_db_thread; static pthread_t sasl_db_thread_tid; static user_db_entry_t **user_ht; static const int n_uht_buckets = 12289; static void kill_whitey(char *s) { for(int i = strlen(s) - 1; i > 0 && isspace(s[i]); i--) { s[i] = '\0'; } } static int u_hash_key(const char *u) { uint32_t h = hash(u, strlen(u), 0) % n_uht_buckets; assert(h < n_uht_buckets); return h; } static char *find_pw(const char *u, char **cfg) { assert(u); assert(user_ht); int h = u_hash_key(u); user_db_entry_t *e = user_ht[h]; while (e && strcmp(e->username, u) != 0) { e = e->next; } if (e != NULL) { *cfg = e->config; return e->password; } else { return NULL; } } static void store_pw(user_db_entry_t **ht, const char *u, const char *p, const char *cfg) { assert(ht); assert(u); assert(p); user_db_entry_t *e = calloc(1, sizeof(user_db_entry_t)); assert(e); e->username = strdup(u); assert(e->username); e->password = strdup(p); assert(e->password); e->config = cfg ? strdup(cfg) : NULL; assert(!cfg || e->config); int h = u_hash_key(u); e->next = ht[h]; ht[h] = e; } static void free_user_ht(void) { if (user_ht) { for (int i = 0; i < n_uht_buckets; i++) { while (user_ht[i]) { user_db_entry_t *e = user_ht[i]; user_db_entry_t *n = e->next; free(e->username); free(e->password); free(e->config); free(e); user_ht[i] = n; } } free(user_ht); user_ht = NULL; } } static const char *get_isasl_filename(void) { return getenv("ISASL_PWFILE"); } static int load_user_db(void) { user_db_entry_t **new_ut = calloc(n_uht_buckets, sizeof(user_db_entry_t*)); if (!new_ut) { return SASL_NOMEM; } pthread_mutex_lock(&uhash_lock); free_user_ht(); user_ht = new_ut; pthread_mutex_unlock(&uhash_lock); const char *filename = get_isasl_filename(); if (!filename) { return SASL_OK; } FILE *sfile = fopen(filename, "r"); if (!sfile) { return SASL_OK; } // File has lines that are newline terminated. // File may have comment lines that must being with '#'. // Lines should look like... // // char up[128]; while (fgets(up, sizeof(up), sfile)) { if (up[0] != '#') { char *uname = up, *p = up, *cfg = NULL; kill_whitey(up); while (*p && !isspace(p[0])) { p++; } // If p is pointing at a NUL, there's nothing after the username. if (p[0] != '\0') { p[0] = '\0'; p++; } // p now points to the first character after the (now) // null-terminated username. while (*p && isspace(*p)) { p++; } // p now points to the first non-whitespace character // after the above cfg = p; if (cfg[0] != '\0') { // move cfg past the password while (*cfg && !isspace(cfg[0])) { cfg++; } if (cfg[0] != '\0') { cfg[0] = '\0'; cfg++; // Skip whitespace while (*cfg && isspace(cfg[0])) { cfg++; } } } store_pw(new_ut, uname, p, cfg); } } fclose(sfile); if (settings.verbose) { settings.extensions.logger->log(EXTENSION_LOG_INFO, NULL, "Loaded isasl db from %s\n", filename); } return SASL_OK; } void sasl_dispose(sasl_conn_t **pconn) { free((*pconn)->username); free((*pconn)->config); free(*pconn); *pconn = NULL; } static bool isasl_is_fresh(void) { bool rv = true; struct stat st; const char *filename = get_isasl_filename(); if (filename) { if (stat(get_isasl_filename(), &st) < 0) { perror(get_isasl_filename()); } else { rv = (prev_stat.st_mtime != st.st_mtime); if (rv) { /* if mtime changes, reset stability counter */ mtime_stability_counter = MTIME_STABILITY_THRESHOLD; } else if (mtime_stability_counter) { /* if mtime haven't changed, but counter hasn't * reached zero, reply true (fresh data) and * decrement counter */ mtime_stability_counter--; rv = true; } prev_stat = st; } } return rv; } static void* check_isasl_db_thread(void* arg) { uint32_t sleep_time = *(int*)arg; if (settings.verbose > 1) { settings.extensions.logger->log(EXTENSION_LOG_INFO, NULL, "isasl checking DB every %ds", sleep_time); } run_sasl_db_thread = true; bool run = true; while (run) { sleep(sleep_time); if (isasl_is_fresh()) { load_user_db(); } pthread_mutex_lock(&sasl_db_thread_lock); if (!run_sasl_db_thread) { run = false; } pthread_mutex_unlock(&sasl_db_thread_lock); } return NULL; } void shutdown_sasl(void) { pthread_mutex_lock(&sasl_db_thread_lock); run_sasl_db_thread = false; pthread_mutex_unlock(&sasl_db_thread_lock); pthread_join(sasl_db_thread_tid, NULL); } int sasl_server_init(const sasl_callback_t *callbacks, const char *appname) { int rv = load_user_db(); if (rv == SASL_OK) { static uint32_t sleep_time; const char *sleep_time_str = getenv("ISASL_DB_CHECK_TIME"); pthread_attr_t attr; if (pthread_attr_init(&attr) != 0 || pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) != 0) { settings.extensions.logger->log(EXTENSION_LOG_WARNING, NULL, "Failed to initialize pthread attributes: %s", strerror(errno)); exit(EX_OSERR); } if (! (sleep_time_str && safe_strtoul(sleep_time_str, &sleep_time))) { // If we can't find a more frequent sleep time, set it to 60s. sleep_time = 60; } if (get_isasl_filename() != NULL && pthread_create(&sasl_db_thread_tid, &attr, check_isasl_db_thread, &sleep_time) != 0) { settings.extensions.logger->log(EXTENSION_LOG_WARNING, NULL, "couldn't create isasl db update thread."); exit(EX_OSERR); } } return rv; } int sasl_server_new(const char *service, const char *serverFQDN, const char *user_realm, const char *iplocalport, const char *ipremoteport, const sasl_callback_t *callbacks, unsigned flags, sasl_conn_t **pconn) { *pconn = calloc(1, sizeof(sasl_conn_t)); return *pconn ? SASL_OK : SASL_NOMEM; } int sasl_listmech(sasl_conn_t *conn, const char *user, const char *prefix, const char *sep, const char *suffix, const char **result, unsigned *plen, int *pcount) { // We use this in a very specific way in the codebase. If that ever // changes, detect it quickly. assert(strcmp(prefix, "") == 0); assert(strcmp(sep, " ") == 0); assert(strcmp(suffix, "") == 0); *result = "PLAIN"; *plen = strlen(*result); return SASL_OK; } static bool check_up(const char *username, const char *password, char **cfg) { pthread_mutex_lock(&uhash_lock); char *pw = find_pw(username, cfg); bool rv = pw && (strcmp(password, pw) == 0); pthread_mutex_unlock(&uhash_lock); return rv; } int sasl_server_start(sasl_conn_t *conn, const char *mech, const char *clientin, unsigned clientinlen, const char **serverout, unsigned *serveroutlen) { int rv = SASL_FAIL; *serverout = ""; *serveroutlen = 0; if(strcmp(mech, "PLAIN") == 0) { // The clientin string looks like "[authzid]\0username\0password" while (clientinlen > 0 && clientin[0] != '\0') { // Skip authzid clientin++; clientinlen--; } if (clientinlen > 2 && clientinlen < 128 && clientin[0] == '\0') { const char *username = clientin + 1; char password[256]; int pwlen = clientinlen - 2 - strlen(username); assert(pwlen >= 0); if (pwlen < 256) { char *cfg = NULL; password[pwlen] = '\0'; memcpy(password, clientin + 2 + strlen(username), pwlen); if (check_up(username, password, &cfg)) { if (conn->username) { free(conn->username); conn->username = NULL; } if (conn->config) { free(conn->config); conn->config = NULL; } conn->username = strdup(username); assert(conn->username); conn->config = strdup(cfg); assert(conn->config); rv = SASL_OK; } } } } return rv; } int sasl_server_step(sasl_conn_t *conn, const char *clientin, unsigned clientinlen, const char **serverout, unsigned *serveroutlen) { // This is only useful when the above returns SASL_CONTINUE. In this // implementation, only PLAIN is supported, so it never will. return SASL_FAIL; } int sasl_getprop(sasl_conn_t *conn, int propnum, const void **pvalue) { switch (propnum) { case SASL_USERNAME: *pvalue = conn->username; break; case ISASL_CONFIG: *pvalue = conn->config; break; default: return SASL_BADPARAM; } return SASL_OK; }