410 lines
11 KiB
C
410 lines
11 KiB
C
#include "config.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <string.h>
|
|
#include <assert.h>
|
|
#include <ctype.h>
|
|
#include <stdint.h>
|
|
#include <pthread.h>
|
|
#include <stdbool.h>
|
|
#include <sys/stat.h>
|
|
#include <errno.h>
|
|
|
|
#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...
|
|
// <NAME><whitespace><PASSWORD><whitespace><CONFIG><optional_whitespace>
|
|
//
|
|
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;
|
|
}
|