/*
 * authcache.c:
 * Cache authentication data.
 *
 * Copyright (c) 2003 Chris Lightfoot. All rights reserved.
 * Email: chris@ex-parrot.com; WWW: http://www.ex-parrot.com/~chris/
 *
 */

static const char rcsid[] = "$Id: authcache.c,v 1.2 2003/11/24 20:23:23 chris Exp $";

#include <sys/types.h>

#include <stdio.h>
#include <string.h>
#include <syslog.h>
#include <time.h>

#include "authswitch.h"
#include "config.h"
#include "md5.h"
#include "util.h"

/* 
 * Obviously we can only cache USER+PASS authentication attempts, since the 
 * APOP method uses a random challenge for each authentication. This is fine,
 * since most sites don't use APOP anyway.
 *
 * The strategy is to use an MD5 checksum of the parameters passed to the
 * authentication driver as a key in a hash table. We return a copy of any
 * cached authentication information, unless it's older than a certain
 * threshold, in which case it's discarded. We don't cache negative
 * authentication results.
 */

/* Is the cache enabled? */
static int use_cache;

/* Optionally, the authentication cache can use the client host as part of the
 * hash table host. Typically this is undesirable, since then the cache will
 * only work for connections from a single IP address. */
static int key_by_client_host;

/* How long an entry persists in the cache. */
static int entry_lifetime;

/* authcontext_copy CONTEXT
 * Return a copy of the passed authentication CONTEXT. */
static authcontext authcontext_copy(const authcontext A) {
    authcontext B;
    alloc_struct(_authcontext, B);

    B->uid = A->uid;
    B->gid = A->gid;
#define COPYS(x)        B->x = A->x ? xstrdup(A->x) : NULL
    COPYS(mboxdrv);
    COPYS(mailbox);
    COPYS(auth);
    COPYS(user);
    COPYS(home);
    COPYS(local_part);
    COPYS(domain);
#undef COPYS

    return B;
}

static struct {
    size_t nbits;
    size_t nfilled;
    struct cacheentry {
        time_t when;
        unsigned char hash[16];
        authcontext A;          /* NULL == slot empty */
    } *slots;
} authcache;

#define CACHESLOTS      (1 << authcache.nbits)

/* hashval MD5 NUM
 * Return a hash formed from the first NUM bytes of the MD5 hash. */
static unsigned long hashval(const unsigned char hash[16], const size_t nbits) {
    unsigned long u;
    size_t i;
    for (i = 0, u = 0; i < 4; ++i)
        u |= ((unsigned long)hash[i]) << (i * 8);
    return u & ((1 << nbits) - 1);
}

/* resize_cache NUM
 * Resize the hash table so that it uses the first NUM bytes of the argument
 * digest as a hash key. */
static void resize_cache(const size_t nbits) {
    struct cacheentry *newslots;
    size_t N, i;
    N = 1 << nbits;
    newslots = xmalloc(N * sizeof *newslots);
    for (i = 0; i < N; ++i)
        newslots[i].A = NULL;

    if (authcache.slots) {
        /* Copy old cache entries into new. */
        for (i = 0; i < CACHESLOTS; ++i) {
            if (authcache.slots[i].A) {
                unsigned long u;
                u = hashval(authcache.slots[i].hash, nbits);
                while (newslots[u].A)
                    u = (u + 1) % N;
                newslots[u] = authcache.slots[i];
            }
        }
        
        xfree(authcache.slots);
    }

    authcache.slots = newslots;
    authcache.nbits = nbits;
}

/* authcache_init
 * Initialise the authentication cache. */
void authcache_init(void) {
    if ((use_cache = config_get_bool("authcache-enable"))) {
        key_by_client_host = config_get_bool("authcache-use-client-host");
        if (!config_get_int("authcache-entry-lifetime", &entry_lifetime) || entry_lifetime <= 0)
            /* To be useful, most authentications must go through the cache.
             * Because we can assume that passwords are changed infrequently
             * by comparison with POP sessions, we should just make this much
             * longer than the interval between sessions. As a default, assume
             * one hour. */
            entry_lifetime = 3600;
        resize_cache(8);
    }
}

/* authcache_close
 * Close down the authentication cache. */
void authcache_close(void) {
    size_t i;
    if (!use_cache)
        return;
    if (authcache.slots) {
        for (i = 0; i < CACHESLOTS; ++i)
            if (authcache.slots[i].A)
                authcontext_delete(authcache.slots[i].A);
        xfree(authcache.slots);
        authcache.slots = NULL;
    }
}

/* make_arg_hash HASH USER LOCALPART DOMAIN PASSWORD CLIENTHOST SERVERHOST
 * Generate an MD5 checksum of the arguments, to use as a hash key. */
static void make_arg_hash(unsigned char hash[16], const char *user, const char *local_part, const char *domain, const char *pass, const char *clienthost, const char *serverhost) {
    md5_ctx c;
    MD5Init(&c);
#define ADDTOHASH(a)        if ((a)) MD5Update(&c, (unsigned char*)(a), strlen((a)) + 1)
    ADDTOHASH(user);
    ADDTOHASH(local_part);
    ADDTOHASH(domain);
    ADDTOHASH(pass);
    if (key_by_client_host)
        ADDTOHASH(clienthost);
    ADDTOHASH(serverhost);
#undef ADDTOHASH
    MD5Final(hash, &c);
}

/* remove_cache_entry INDEX
 * Remove the cache entry with the given INDEX. */
static void remove_cache_entry(unsigned long u0) {
    unsigned long u, uprev;
    authcontext_delete(authcache.slots[u0].A);
    authcache.slots[u0].A = NULL;
    /* Need to close up any other entries in the table. */
    for (uprev = u0, u = (u0 + 1) % CACHESLOTS; hashval(authcache.slots[u].hash, authcache.nbits) == u0; uprev = u, u = (u + 1) % CACHESLOTS) {
        authcache.slots[uprev] = authcache.slots[u];
        authcache.slots[u].A = NULL;
    }
    --authcache.nfilled;
}

/* authcache_new_user_pass USER LOCALPART DOMAIN PASSWORD CLIENTHOST SERVERHOST
 * Return any cached authentication context for the given arguments. */
authcontext authcache_new_user_pass(const char *user, const char *local_part, const char *domain, const char *pass, const char *clienthost, const char *serverhost) {
    unsigned char hash[16];
    unsigned long u, u0;
    if (!use_cache)
        return NULL;
    make_arg_hash(hash, user, local_part, domain, pass, clienthost, serverhost);
    u = u0 = hashval(hash, authcache.nbits);
    do {
        if (!authcache.slots[u].A)
            break;
        else if (0 == memcmp(authcache.slots[u].hash, hash, 16)) {
            if (authcache.slots[u].when < time(NULL) - entry_lifetime) {
                log_print(LOG_DEBUG, _("authcache_new_user_pass: dropped old cache entry for %s from slot %u"), username_string(user, local_part, domain), u);
                remove_cache_entry(u);
                return NULL;
            } else {
                log_print(LOG_DEBUG, _("authcache_new_user_pass: returning saved entry for %s (%ds old) from slot %u"), username_string(user, local_part, domain), (int)(time(NULL) - authcache.slots[u].when), u);
                return authcontext_copy(authcache.slots[u].A);
            }
        } else
            u = (u + 1) % CACHESLOTS;
    } while (u != u0);
    log_print(LOG_DEBUG, _("authcache_new_user_pass: no entry for %s"), username_string(user, local_part, domain));
    return NULL;
}

/* authcache_save CONTEXT USER LOCALPART DOMAIN PASSWORD CLIENTHOST SERVERHOST
 * Save the given authentication CONTEXT under the given arguments. */
void authcache_save(authcontext A, const char *user, const char *local_part, const char *domain, const char *pass, const char *clienthost, const char *serverhost) {
    unsigned char hash[16];
    unsigned long u;
    authcontext Acopy;

    if (!use_cache)
        return;
    
    /* Add `+cache' to the end of the authcontext name. */
    Acopy = authcontext_copy(A);
    xfree(Acopy->auth);
    Acopy->auth = xmalloc(strlen(A->auth) + sizeof "+cache");
    sprintf(Acopy->auth, "%s+cache", A->auth);

    /* Find a free hash slot. */
    make_arg_hash(hash, user, local_part, domain, pass, clienthost, serverhost);
    
    if (authcache.nfilled + 1 == CACHESLOTS) {
        resize_cache(authcache.nbits + 1);
        log_print(LOG_DEBUG, _("authcache_save: resized cache to %u byte key"), (unsigned)authcache.nbits);
    }

    for (u = hashval(hash, authcache.nbits); authcache.slots[u].A; u = (u + 1) % CACHESLOTS);
    log_print(LOG_DEBUG, _("authcache_save: saved entry for %s in slot %u"), username_string(user, local_part, domain), u);
    memcpy(authcache.slots[u].hash, hash, 16);
    authcache.slots[u].A = Acopy;
    time(&authcache.slots[u].when);
    ++authcache.nfilled;
}


syntax highlighted by Code2HTML, v. 0.9.1