/*
 * auth_other.c:
 * authenticate using an external program
 *
 * We send a number of variables in the form
 *
 *  key\0value\0
 *
 * terminated by a \0. Variables sent:
 *
 *  key         value
 *  method      PASS or APOP
 *  timestamp   server's RFC1939 timestamp
 *  user        client's username sent with USER or APOP command
 *  pass        client's password from PASS command
 *  digest      client's digest sent with APOP command, in hex
 *  clienthost  client's IP address/hostname
 *  serverhost  server's IP address/hostname
 *
 * The program should respond with a similarly formatted string containing the
 * following variables:
 *
 *  key         value
 *  result      YES or NO
 *  uid         username/uid with which to access mailspool
 *  gid         groupname/gid with which to access mailspool
 *  domain      (optional) domain in which the user has been authenticated
 *  mailbox     (optional) location of mailbox
 *  mboxtype    (optional) name of mailbox driver
 *  logmsg      (optional) message to log
 *
 * The called program will be sent SIGTERM when the authentication driver
 * closes, or in the event that there is a protocol failure. At present,
 * responses need to be timely, since authentication drivers are called
 * synchronously. This may be improved in a later version.
 *
 * The total length of the data sent or received will not exceed 4096 bytes.
 *
 * Copyright (c) 2001 Chris Lightfoot. All rights reserved.
 *
 */

#ifdef HAVE_CONFIG_H
#include "configuration.h"
#endif /* HAVE_CONFIG_H */

#ifdef AUTH_OTHER
static const char rcsid[] = "$Id: auth_other.c,v 1.26 2003/07/14 23:31:20 chris Exp $";

#include <sys/types.h>

#include <errno.h>
#include <fcntl.h>
#include <pwd.h>
#include <signal.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <time.h>
#include <unistd.h>

#include <sys/time.h>

#include "authswitch.h"
#include "auth_other.h"
#include "config.h"
#include "math.h"
#include "stringmap.h"
#include "util.h"

#define MAX_DATA_SIZE   4096

char *auth_program;
time_t auth_other_childstart_time;
uid_t auth_other_childuid;
gid_t auth_other_childgid;
volatile pid_t auth_other_child_pid, auth_other_childdied;
volatile int auth_other_childstatus;
struct timeval auth_other_childtimeout;

/* File descriptors used to talk to child. */
volatile int auth_other_childwr = -1, auth_other_childrd = -1;

/* dump:
 * Debugging method. */
void dump(unsigned char *b, size_t len) {
    unsigned char *p;
    char *q;
    char *str = xmalloc(len * 4 + 1);
    for (p = b, q = str; p < b + len; ++p)
        if (*p >= 32 && *p <= 127) *q++ = *p;
        else {
            sprintf(q, "\\x%02x", (unsigned int)*p);
            q += 4;
        }
    *q = 0;
    log_print(LOG_INFO, "dump %s", str);
    xfree(str);
}

static void tvsub(struct timeval *t1, const struct timeval *t2);
static void tvadd(struct timeval *t1, const struct timeval *t2);
static int tvcmp(const struct timeval *t1, const struct timeval *t2);

/* tvadd:
 * t1 += t2 on timevals. */
static void tvadd(struct timeval *t1, const struct timeval *t2) {
    t1->tv_sec  += t2->tv_sec;
    t1->tv_usec += t2->tv_usec;
    while (t1->tv_usec > 1000000) {
        t1->tv_usec -= 1000000;
        t1->tv_sec++;
    }
}

/* tvsub:
 * t1 -= t2 on timevals. */
static void tvsub(struct timeval *t1, const struct timeval *t2) {
    t1->tv_sec  -= t2->tv_sec;
    t1->tv_usec -= t2->tv_usec;
    while (t1->tv_usec < 0) {
        t1->tv_usec += 1000000;
        t1->tv_sec--;
    }
}

/* tvcmp:
 * Is t1 before (-1), after (1), or the same as (0) t2? */
static int tvcmp(const struct timeval *t1, const struct timeval *t2) {
    if (t1->tv_sec < t2->tv_sec) return -1;
    else if (t1->tv_sec > t2->tv_sec) return 1;
    else {
        if (t1->tv_usec < t2->tv_usec) return -1;
        else if (t1->tv_usec > t2->tv_usec) return 1;
        else return 0;
    }
}

/* auth_other_start_child:
 * Start the authentication program, setting up pipes on which to talk to it.
 * Returns 1 on success or 0 on failure. */
int auth_other_start_child() {
    int p1[2], p2[2];
    char *argv[2] = {0};
    char *envp[3] = {"PATH=/bin",  /* XXX path? */
                     "TPOP3D_CONTEXT=auth_other", NULL};

    argv[0] = auth_program;

    /* Generate pipes to talk to the child. */
    if (pipe(p1) == -1 || pipe(p2) == -1) {
        log_print(LOG_ERR, "auth_other_start_child: pipe: %m");
        return 0;
    }

    /* p1[0] becomes the child's standard input.
     * p2[1] becomes the child's standard output.
     * We want p1[1] to be non-blocking. */
    if (fcntl(p1[1], F_SETFL, O_NONBLOCK) == -1) {
        log_print(LOG_ERR, "auth_other_start_child: fcntl: %m");
        close(p1[0]); close(p1[1]); close(p2[0]); close(p2[1]);
        return 0;
    }
   
    switch (auth_other_child_pid = fork()) {
        case 0:
            if (setgid(auth_other_childgid) == -1) {
                log_print(LOG_ERR, "auth_other_start_child: setgid(%d): %m", (int)auth_other_childgid);
                _exit(1);
            } else if (setuid(auth_other_childuid) == -1) {
                log_print(LOG_ERR, "auth_other_start_child: setuid(%d): %m", (int)auth_other_childuid);
                _exit(1);
            }
            
            /* Child. */
            close(0); close(1); close(2);

            /* Set up standard input and output. */
            dup2(p1[0], 0); dup2(p2[1], 1); 
            close(p1[1]); close(p2[0]); 
            
            execve(auth_program, argv, envp);

            /* Failed. */
            _exit(1);

            break;
            
        case -1:
            /* Error. */
            log_print(LOG_ERR, "auth_other_start_child: fork: %m");

            close(p1[0]); close(p1[1]); close(p2[0]); close(p2[1]);
            return 0;

        default:
            /* Parent. */
            auth_other_childwr = p1[1];
            auth_other_childrd = p2[0];
 
            close(p1[0]);
            close(p2[1]);

            log_print(LOG_INFO, "auth_other_start_child: started authentication child `%s'", auth_program);

            return 1;
    }

    return 0; /* NOTREACHED */
}

/* auth_other_kill_child:
 * Kill the authentication child, with SIGTERM at first and then with added
 * SIGKILL-shaped violence if that fails. */
void auth_other_kill_child() {
    struct timeval deadline, now;
    if (auth_other_child_pid == 0) return; /* Already dead. */
    kill(auth_other_child_pid, SIGTERM);
    
    /* Wait for it to expire. */
    gettimeofday(&now, NULL);
    deadline = now;
    tvadd(&deadline, &auth_other_childtimeout);
    
    while (auth_other_child_pid && tvcmp(&now, &deadline) < 0) {
        struct timespec delay = {0, 100000000}; /* 0.1s; note nano not microseconds */
        nanosleep(&delay, NULL);
        gettimeofday(&now, NULL);
    }

    if (auth_other_child_pid) {
        log_print(LOG_WARNING, _("auth_other_kill_child: child failed to die; killing with SIGKILL"));
        kill(auth_other_child_pid, SIGKILL);
        /* Assume this works; it ought to! */
    }
}

/* auth_other_init:
 * Initialise the authentication driver for external programs. This starts the
 * program specified by the auth-other-program config directive, running under
 * the user and group ids in auth-other-user and auth-other-group. */
extern stringmap config;    /* in main.c */
int auth_other_init() {
    char *s;
    float f;

    if (!(s = config_get_string("auth-other-program"))) {
        log_print(LOG_ERR, _("auth_other_init: no program specified"));
        return 0;
    } else {
        struct stat st;
        auth_program = s;
        if (*auth_program != '/') {
            log_print(LOG_ERR, _("auth_other_init: auth-program %s must be an absolute path"), auth_program);
            return 0;
        } else if (stat(auth_program, &st) == -1) {
            log_print(LOG_ERR, _("auth_other_init: auth-program %s: %m"), auth_program);
            return 0;
        }
        /* We should, perhaps, fail if it turns out that the program is not
         * executable by the given group and user; but this is a pain to work
         * out. */
    }

    /* Find out the timeout for talking to the program. */
    switch (config_get_float("auth-other-timeout", &f)) {
        case -1:
            log_print(LOG_ERR, _("auth_other_init: value given for auth-other-timeout does not make sense"));
            return 0;

        case 1:
            if (f < 0.0 || f > 10.0) {
                log_print(LOG_ERR, _("auth_other_init: value %f for auth-other-timeout is out of range"), f);
                return 0;
            }
            break;

        default:
            f = 0.75;
    }

    auth_other_childtimeout.tv_sec  = (long)floor(f);
    auth_other_childtimeout.tv_usec = (long)((f - floor(f)) * 1e6);

    /* Find out user and group under which program will run. */
    if (!(s = config_get_string("auth-other-user"))) {
        log_print(LOG_ERR, _("auth_other_init: no user specified"));
        return 0;
    } else if (!parse_uid(s, &auth_other_childuid)) {
        log_print(LOG_ERR, _("auth_other_init: auth-other-user directive `%s' does not make sense"), s);
        return 0;
    }

    if (!(s = config_get_string("auth-other-group"))) {
        log_print(LOG_ERR, _("auth_other_init: no group specified"));
        return 0;
    } else if (!parse_gid(s, &auth_other_childgid)) {
        log_print(LOG_ERR, _("auth_other_init: auth-other-group directive `%s' does not make sense"), s);
        return 0;
    }

    log_print(LOG_INFO, "auth_other_init: %s: will run as uid %d, gid %d", auth_program, (int)auth_other_childuid, (int)auth_other_childgid);

    if (!auth_other_start_child()) {
        log_print(LOG_ERR, _("auth_other_init: failed to start authentication child for first time"));
        return 0;
    }

    return 1;
}

/* auth_other_postfork:
 * Post-fork cleanup: close our copies of the file descriptors. */
void auth_other_postfork() {
    close(auth_other_childwr);
    close(auth_other_childrd);
}

/* auth_other_close:
 * Shut down the authentication driver, killing the external program. */
void auth_other_close() {
    auth_other_kill_child();
}

/* auth_other_send_request:
 * Send the auth child a request consisting of several key/value pairs, as
 * above. Returns 1 on success or 0 on failure. */
int auth_other_send_request(const int nvars, ...) {
    va_list ap;
    int i, ret = 0;
    char buffer[MAX_DATA_SIZE] = {0};
    char *p;
    size_t nn;
    
    if (!auth_other_child_pid) return 0;

    va_start(ap, nvars);

    for (i = 0, p = buffer, nn = 0; i < nvars; ++i) {
        const char *key, *val;
        key = va_arg(ap, const char *);
        nn += strlen(key) + 1;
        if (nn > sizeof(buffer)) goto fail;
        memcpy(p, key, strlen(key) + 1);
        p += strlen(key) + 1;

        val = va_arg(ap, const char *);
        nn += strlen(val) + 1;
        if (nn > sizeof(buffer)) goto fail;
        memcpy(p, val, strlen(val) + 1);
        p += strlen(val) + 1;
    }

    ++nn; /* Terminating \0 */
    if (nn > sizeof(buffer)) {
        log_print(LOG_ERR, _("auth_other_send_request: total size of request would exceed %d bytes"), sizeof(buffer));
        goto fail;
    }

    /* Since write operations are atomic, this will either succeed entirely or
     * fail. In the latter case, it may be with EAGAIN because the child
     * process is blocking; we interpret this as a protocol error. */
    if (try_write(auth_other_childwr, buffer, nn)) ret = 1;
    else {
        if (errno == EAGAIN)
            log_print(LOG_ERR, _("auth_other_send_request: write: write on pipe blocked; killing child"));
        else
            log_print(LOG_ERR, _("auth_other_send_request: write: %m; killing child"));
        auth_other_kill_child();
    }
    
fail:
    va_end(ap);

    return ret;
}

/* auth_other_recv_response:
 * Receive a response from the auth child, as above. Returns a stringmap of
 * responses on success, or NULL on failure. */
stringmap auth_other_recv_response() {
    stringmap S = NULL;
    char buffer[MAX_DATA_SIZE], ends[2] = {0, 0};
    char *p, *q, *r, *s;
    struct timeval deadline;

    if (!auth_other_child_pid) return NULL;

    gettimeofday(&deadline, 0);
    tvadd(&deadline, &auth_other_childtimeout);

    p = buffer;
    do {
        struct timeval timeout = deadline, tt;
        ssize_t rr;
        fd_set readfds;

        FD_ZERO(&readfds);
        FD_SET(auth_other_childrd, &readfds);
        gettimeofday(&tt, NULL);
        if (tvcmp(&deadline, &tt) == -1) {
            log_print(LOG_ERR, _("auth_other_recv_response: timed out waiting for a response; killing child"));
            goto fail;
        }
        tvsub(&timeout, &tt);

        switch (select(auth_other_childrd + 1, &readfds, NULL, NULL, &timeout)) {
            case 1:
                rr = read(auth_other_childrd, p, (buffer + sizeof(buffer) - p));

                switch (rr) {
                    case 0:
                        log_print(LOG_ERR, _("auth_other_recv_response: read: child closed pipe; killing child"));
                        goto fail;

                    case -1:
                        if (errno != EINTR) {
                            log_print(LOG_ERR, _("auth_other_recv_response: read: %m; killing child"));
                            goto fail;
                        } else break;

                    default:
                        p += rr;
                        if (p == buffer + sizeof(buffer)) {
                            log_print(LOG_ERR, _("auth_other_recv_response: total size of response exceeds %d bytes; killing child"), sizeof(buffer));
                            goto fail;
                        }
                }
                break;

            case -1:
                if (errno != EINTR) {
                    log_print(LOG_ERR, _("auth_other_recv_repsonse: select: %m; killing child"));
                    goto fail;
                }

            default:
                break;
        }
    } while (p < buffer + 2 || memcmp(p - 2, ends, 2) != 0); /* Now see whether we have some valid data. */

    /* Try to interpret the passed data. We want to find pairs of
     * \0-terminated strings and put them into the stringmap. */
    S = stringmap_new();
    
    /* q points to the beginning of a key, r to the end of the key, and s to
     * the end of the value, so that [q...r] is the key and [r + 1...s] is the
     * value. */
    q = buffer;
    while (*q && S) {
        r = memchr(q, 0, p - q);
        if (!r || r > (p - 3)) goto formaterror;
        
        s = memchr(r + 1, 0, p - (r + 1));
        if (!s) goto formaterror;

        stringmap_insert(S, q, item_ptr(xstrdup(r + 1)));

        q = s + 1;
        continue;

formaterror:
        log_print(LOG_ERR, _("auth_other_recv_response: response data not correctly formatted; killing child"));
        stringmap_delete_free(S);
        S = NULL;
    }

fail:
    if (!S) auth_other_kill_child();
    return S;
}

/* auth_other_new_apop:
 * Attempt to authenticate a user using APOP, via the child program. */
authcontext auth_other_new_apop(const char *name, const char *local_part, const char *domain, const char *timestamp, const unsigned char *digest, const char *clienthost, const char *serverhost) {
#define MISSING(k)     do { log_print(LOG_ERR, _("auth_other_new_apop: missing key `%s' in response"), (k)); goto fail; } while(0)
#define INVALID(k, v)  do { log_print(LOG_ERR, _("auth_other_new_apop: invalid value `%s' for key `%s' in response"), (v), (k)); goto fail; } while(0)
    char digeststr[33];
    char *p;
    const unsigned char *q;
    stringmap S;
    item *I;
    authcontext a = NULL;
 
    if (!auth_other_child_pid) auth_other_start_child();
    
    for (p = digeststr, q = digest; q < digest + 16; p += 2, ++q)
        sprintf(p, "%02x", (unsigned int)*q);
    if (local_part && domain) {
        if (!auth_other_send_request(8, "method", "APOP", "user", name, "local_part", local_part, "domain", domain, "timestamp", timestamp, "digest", digeststr, "clienthost", clienthost, "serverhost", serverhost))
            return NULL;
    } else if (!auth_other_send_request(6, "method", "APOP", "user", name, "timestamp", timestamp, "digest", digeststr, "clienthost", clienthost, "serverhost", serverhost))
        return NULL;

    if (!(S = auth_other_recv_response()))
        return NULL;

    I = stringmap_find(S, "logmsg");
    if (I) log_print(LOG_INFO, "auth_other_new_apop: child: %s", (char*)I->v);

    I = stringmap_find(S, "result");
    if (!I) MISSING("result");
    
    if (strcmp((char*)I->v, "YES") == 0) {
        uid_t uid;
        gid_t gid;
        struct passwd *pw;
        char *mailbox = NULL, *mboxdrv = NULL, *domain = NULL;

        I = stringmap_find(S, "uid");
        if (!I) MISSING("uid");
        else if (!parse_uid(I->v, &uid)) INVALID("uid", (char*)I->v);
 
        pw = getpwuid(uid);
        if (!pw) INVALID("uid", (char*)I->v);
       
        I = stringmap_find(S, "gid");
        if (!I) MISSING("gid");
        else if (!parse_gid(I->v, &gid)) INVALID("gid", (char*)I->v);

        I = stringmap_find(S, "mailbox");
        if (I) mailbox = (char*)I->v;

        I = stringmap_find(S, "mboxtype");
        if (I) mboxdrv = (char*)I->v;

        I = stringmap_find(S, "domain");
        if (I) domain = (char*)I->v;

        a = authcontext_new(uid, gid, mboxdrv, mailbox, pw->pw_dir);
    } else if (strcmp((char*)I->v, "NO") != 0) INVALID("result", (char*)I->v);
        
fail:
    stringmap_delete_free(S);
    return a;
#undef MISSING
#undef INVALID
}

/* auth_other_new_user_pass:
 * Attempt to authenticate a user using USER/PASS, via the child program. */
authcontext auth_other_new_user_pass(const char *user, const char *local_part, const char *domain, const char *pass, const char *clienthost, const char *serverhost) {
#define MISSING(k)     do { log_print(LOG_ERR, _("auth_other_new_user_pass: missing key `%s' in response"), (k)); goto fail; } while(0)
#define INVALID(k, v)  do { log_print(LOG_ERR, _("auth_other_new_user_pass: invalid value `%s' for key `%s' in response"), (v), (k)); goto fail; } while(0)
    stringmap S;
    item *I;
    authcontext a = NULL;

    if (!auth_other_child_pid) auth_other_start_child();

    if (local_part && domain) {
        if (!auth_other_send_request(7, "method", "PASS", "user", user, "local_part", local_part, "domain", domain, "pass", pass, "clienthost", clienthost, "serverhost", serverhost))
            return NULL;
    } else if (!auth_other_send_request(5, "method", "PASS", "user", user, "pass", pass, "clienthost", clienthost, "serverhost", serverhost))
        return NULL;

    if (!(S = auth_other_recv_response()))
        return NULL;
    
    I = stringmap_find(S, "logmsg");
    if (I) log_print(LOG_INFO, "auth_other_new_user_pass: child: %s", (char*)I->v);

    I = stringmap_find(S, "result");
    if (!I) MISSING("result");
    
    if (strcmp((char*)I->v, "YES") == 0) {
        uid_t uid;
        gid_t gid;
        struct passwd *pw;
        char *mailbox = NULL, *mboxdrv = NULL, *domain = NULL;

        I = stringmap_find(S, "uid");
        if (!I) MISSING("uid");
        else if (!parse_uid(I->v, &uid)) INVALID("uid", (char*)I->v);
 
        pw = getpwuid(uid);
        if (!pw) INVALID("uid", (char*)I->v);
       
        I = stringmap_find(S, "gid");
        if (!I) MISSING("gid");
        else if (!parse_gid(I->v, &gid)) INVALID("gid", (char*)I->v);

        I = stringmap_find(S, "mailbox");
        if (I) mailbox = (char*)I->v;

        I = stringmap_find(S, "mboxtype");
        if (I) mboxdrv = (char*)I->v;

        I = stringmap_find(S, "domain");
        if (I) domain = (char*)I->v;

        a = authcontext_new(uid, gid, mboxdrv, mailbox, pw->pw_dir);
    } else if (strcmp((char*)I->v, "NO") != 0) INVALID("result", (char*)I->v);
        
fail:
    stringmap_delete_free(S);
    return a;
#undef MISSING
#undef INVALID
}

/* auth_other_onlogin:
 * Pass details of a successful login to the authentication program. */
void auth_other_onlogin(const authcontext A, const char *clienthost, const char *serverhost) {
    stringmap S;
    item *I;

    if (!auth_other_child_pid) auth_other_start_child();

    if (!auth_other_send_request(6, "method", "ONLOGIN", "user", A->user, "local_part", A->local_part, "domain", A->domain, "clienthost", clienthost, "serverhost", serverhost)
        || !(S = auth_other_recv_response()))
        return;
    
    I = stringmap_find(S, "logmsg");
    if (I) log_print(LOG_INFO, "auth_other_new_user_pass: child: %s", (char*)I->v);
        
    stringmap_delete_free(S);
}


#endif /* AUTH_OTHER */


syntax highlighted by Code2HTML, v. 0.9.1