/* $Cambridge: hermes/src/prayer/accountd/config.c,v 1.5 2008/09/16 09:59:54 dpc22 Exp $ */
/************************************************
 *    Prayer - a Webmail Interface              *
 ************************************************/

/* Copyright (c) University of Cambridge 2000 - 2002 */
/* See the file NOTICE for conditions of use and distribution. */

#include "accountd.h"

/* config_create() *********************************************************
 *
 * Create a new config structure using own pool
 **************************************************************************/

struct config *config_create(void)
{
    struct pool *pool = pool_create(0);
    struct config *config = pool_alloc(pool, sizeof(struct config));

    /* General options */
    config->pool = pool;
    config->accountd_port = 0L;
    config->log_debug = NIL;
    config->child_timeout = 10 * 60;
    config->authtype = AUTHTYPE_UNKNOWN;
    config->raven_enable = NIL;
    config->raven_key_path = "";

    /* Location of filter files */
    config->msforward_name = ".MSforward";
    config->forward_name = ".forward";
    config->aliases_name = "vacation.aliases";
    config->filter_restricted = NIL;

    /* Password changing */
    config->pwd_cmdline = NIL;
    config->pwd_script = NIL;
    config->pwd_pty    = T;

    /* Fullname checking */
    config->checkfull_cmdline = NIL;
    config->checkfull_script = NIL;
    config->checkfull_pty    = T;

    /* Fullname changing */
    config->full_cmdline = NIL;
    config->full_script = NIL;
    config->full_pty    = T;

    /* Quota check */
    config->quota_cmdline = NIL;
    config->quota_hermes = NIL;

    /* SSL configuration */
    config->use_ssl = NIL;
    config->ssl_cert_file = NIL;
    config->ssl_privatekey_file = NIL;
    config->ssl_dh_file = NIL;
    config->ssl_rsakey_lifespan = (10 * 60);    /* 10 mins */
    config->ssl_rsakey_freshen = (10 * 60);     /* 10 mins */
    config->ssl_session_timeout = (24 * 60 * 60);       /* 60 mins */
    config->ssl_session_dir = NIL;
    config->egd_socket = NIL;

    return (config);
}

/* config_free() *********************************************************
 *
 * Free config structure
 ************************************************************************/

void config_free(struct config *config)
{
    pool_free(config->pool);
}

/* config_paniclog() *******************************************************
 *
 * Special interation of paniclog() to print errrors consistently when
 * frontend or session starting up with introducing dependancies on code
 * which is specific to the frontend or session processes.
 **************************************************************************/

static void config_paniclog(struct config *config, char *fmt, ...)
{
    unsigned long len;
    va_list ap;

    va_start(ap, fmt);
    len = log_entry_size(fmt, ap);
    va_end(ap);

    va_start(ap, fmt);
    log_panic_ap(len, fmt, ap);
    va_end(ap);
}

/* ====================================================================== */

/* conf used only to calculate offsets into live config structure */
static struct config conf;
#define OFFSET(x) ((char*)(&conf.x) - (char *)&conf)

/* Config options. Searched by binary chop => must be sorted correctly
 * However config_init() checks that order is correct, bails out otherwise */
static struct {
    char *name;
    enum { config_bool, config_number, config_time, config_string,
        config_path, config_perm, config_authtype, config_unknown
    } type;
    int offset;
} config_options[] = {
    {
    "accountd_port", config_number, OFFSET(accountd_port)}, {
    "aliases_name", config_string, OFFSET(aliases_name)}, {
    "authtype", config_authtype, OFFSET(authtype)}, {
    "checkfull_cmdline", config_string, OFFSET(checkfull_cmdline)}, {
    "checkfull_pty", config_bool, OFFSET(checkfull_pty)}, {
    "checkfull_script", config_string, OFFSET(checkfull_script)}, {
    "child_timeout", config_time, OFFSET(child_timeout)}, {
    "egd_socket", config_path, OFFSET(egd_socket)}, {
    "filter_restricted", config_bool, OFFSET(filter_restricted)}, {
    "forward_name", config_string, OFFSET(forward_name)}, {
    "full_cmdline", config_string, OFFSET(full_cmdline)}, {
    "full_pty", config_bool, OFFSET(full_pty)}, {
    "full_script", config_string, OFFSET(full_script)}, {
    "msforward_name", config_string, OFFSET(msforward_name)}, {
    "pwd_cmdline", config_string, OFFSET(pwd_cmdline)}, {
    "pwd_pty", config_bool, OFFSET(pwd_pty)}, {
    "pwd_script", config_string, OFFSET(pwd_script)}, {
    "quota_cmdline", config_string, OFFSET(quota_cmdline)}, {
    "quota_hermes", config_bool, OFFSET(quota_hermes)}, {
    "raven_enable", config_bool, OFFSET(raven_enable)}, {
    "raven_key_path", config_path, OFFSET(raven_key_path)}, {
    "ssl_cert_file", config_path, OFFSET(ssl_cert_file)}, {
    "ssl_dh_file", config_path, OFFSET(ssl_dh_file)}, {
    "ssl_privatekey_file", config_path, OFFSET(ssl_privatekey_file)}, {
    "ssl_rsakey_freshen", config_time, OFFSET(ssl_rsakey_freshen)}, {
    "ssl_rsakey_lifespan", config_time, OFFSET(ssl_rsakey_lifespan)}, {
    "use_ssl", config_bool, OFFSET(use_ssl)}, {
    NIL, config_unknown, 0}
};

static unsigned long options_size = 0L;

/* ====================================================================== */

/* config_init() *********************************************************
 *
 * Initialise config engine. Counts number of options in the "options"
 * array, and makes sure that they are properly sorted for binary chop
 * searches.
 ************************************************************************/

static BOOL config_init(void)
{
    unsigned long offset;

    offset = 0;
    while (config_options[offset].name) {
        if (config_options[offset + 1].name) {
            if (strcmp(config_options[offset].name,
                       config_options[offset + 1].name) > 0) {
                config_paniclog(NIL,
                                "config.c: options array sorted incorrectly at %s",
                                config_options[offset].name);
                return (NIL);
            }
        }
        offset++;
    }
    options_size = offset;
    return (T);
}

/* ====================================================================== */

/* config_find_key() *****************************************************
 *
 * Find offset to "options" structure which corresponds to key
 *    key: Key to lookup
 *
 * Returns: Array offset, (-1) on error.
 ************************************************************************/

static int config_find_option(char *key)
{
    int first = 0;
    int last = options_size;
    int middle = 0;
    int rc;

    /* Binary chop */
    while (first < last) {
        middle = (first + last) / 2;
        rc = strcmp(config_options[middle].name, key);

        if (rc == 0)
            return (middle);
        else if (rc < 0)
            first = middle + 1;
        else
            last = middle;
    }

    /* Check whether last interaction of loop found match */
    if (!strcmp(config_options[middle].name, key))
        return (middle);

    return (-1);                /* Not found */
}

/* ====================================================================== */

/* Various static utility routines for parsing elements of config file */

static BOOL config_parse_bool(BOOL * result, char *s)
{
    if (!strcasecmp(s, "TRUE") || !strcasecmp(s, "T") || !strcmp(s, "1")) {
        *result = T;
        return (T);
    }

    if (!strcasecmp(s, "FALSE") || !strcasecmp(s, "NIL")
        || !strcmp(s, "0")) {
        *result = NIL;
        return (T);
    }

    return (NIL);
}

static BOOL config_parse_number(unsigned long *result, char *text)
{
    char *s;

    for (s = text; *s; s++)
        if (!Uisdigit(*s))
            return (NIL);

    *result = atoi(text);
    return (T);
}

static BOOL config_parse_perm(unsigned long *result, char *s)
{
    if (s[0] != '0')
        return (NIL);

    if ((s[1] < '0') || (s[1] > '7'))
        return (NIL);

    if ((s[2] < '0') || (s[2] > '7'))
        return (NIL);

    if ((s[3] < '0') || (s[3] > '7'))
        return (NIL);

    *result =
        (((s[1] - '0') * (8 * 8)) + ((s[2] - '0') * (8)) + (s[3] - '0'));
    return (T);
}

static BOOL config_parse_authtype(AUTHTYPE * result, char *s)
{
    *result = AUTHTYPE_UNKNOWN;

    if (!strcasecmp(s, "pam"))
        *result = AUTHTYPE_PAM;
    else if (!strcasecmp(s, "pwd"))
        *result = AUTHTYPE_PWD;
    else if (!strcasecmp(s, "pwd_md5"))
        *result = AUTHTYPE_PWD_MD5;
    else if (!strcasecmp(s, "shadow"))
        *result = AUTHTYPE_SHADOW;
    else if (!strcasecmp(s, "shadow_md5"))
        *result = AUTHTYPE_SHADOW_MD5;

    return ((*result != AUTHTYPE_UNKNOWN) ? T : NIL);
}

static BOOL config_parse_time(unsigned long *result, char *text)
{
    char *s;
    unsigned long multiple;

    if (!(text && text[0]))
        return (NIL);

    for (s = text; s[1]; s++)
        if (!Uisdigit(*s))
            return (NIL);

    if (Uisdigit(*s))
        multiple = 1;
    else {
        switch (Utoupper(*s)) {
        case 'S':
            multiple = 1;
            break;
        case 'M':
            multiple = 60;
            break;
        case 'H':
            multiple = 60 * 60;
            break;
        case 'D':
            multiple = 24 * 60 * 60;
            break;
        default:
            return (NIL);
        }
        *s = '\0';
    }

    *result = (multiple * atoi(text));
    return (T);
}

static BOOL
config_parse_string(char **result, char *text, struct pool *pool)
{
    char *s;

    text = string_trim_whitespace(text);

    if (*text == '"') {
        text++;
        for (s = text; *s; s++) {
            if (*s == '"') {
                *s = '\0';
                *result = pool_strdup(pool, text);
                return (T);
            }
        }
        return (NIL);
    }

    *result = pool_strdup(pool, text);
    return (T);
}

/* ====================================================================== */

/* config_parse_single() *************************************************
 *
 * Parse a single (key, value) pair extracted from configuration file
 * or passed as command line option.
 *   config:
 *      key: Text for key
 *    value: Text for value
 *   lineno: Line number in configuration file (for error messages)
 *           0 if this option passed in on the command line.
 ************************************************************************/

static BOOL
config_parse_single(struct config *config, char *key, char *value,
                    unsigned long lineno)
{
    struct pool *pool = config->pool;
    int i;
    char *ptr;

    if ((i = config_find_option(key)) < 0) {
        if (lineno > 0L)
            config_paniclog(config,
                            ("Error parsing configuration file at line %lu: "
                             "Unknown option \"%s\""), lineno, key);
        else
            config_paniclog(config, "Unknown option \"%s\"", key);
        return (NIL);
    }

    ptr = ((char *) config) + config_options[i].offset;

    switch (config_options[i].type) {
    case config_bool:
        if (!config_parse_bool((BOOL *) ptr, value)) {
            if (lineno > 0L)
                config_paniclog(config,
                                ("Error parsing configuration file at line %lu: "
                                 "Option %s takes boolean argument"),
                                lineno, key);
            else
                config_paniclog(config,
                                "option \"%s\" takes boolean argument",
                                key);
            return (NIL);
        }
        break;
    case config_number:
        if (!config_parse_number((unsigned long *) ptr, value)) {
            if (lineno > 0L)
                config_paniclog(config,
                                ("Error parsing configuration file at line %lu: "
                                 "Option %s takes a numeric argument"),
                                lineno, key);
            else
                config_paniclog(config,
                                "option \"%s\" takes a numeric argument",
                                key);
            return (NIL);
        }
        break;
    case config_perm:
        if (!config_parse_perm((unsigned long *) ptr, value)) {
            if (lineno > 0L)
                config_paniclog(config,
                                ("Error parsing configuration file at line %lu: "
                                 "Option %s takes a permission argument"),
                                lineno, key);
            else
                config_paniclog(config,
                                "option \"%s\" takes a permission argument",
                                key);
            return (NIL);
        }
        break;
    case config_time:
        if (!config_parse_time((unsigned long *) ptr, value)) {
            if (lineno > 0L)
                config_paniclog(config,
                                ("Error parsing configuration file at line %lu: "
                                 "Option %s takes a time argument"),
                                lineno, key);
            else
                config_paniclog(config,
                                "option \"%s\" takes a time argument",
                                key);
            return (NIL);
        }
        break;
    case config_string:
    case config_path:
        if (!config_parse_string((char **) ptr, value, pool)) {
            if (lineno > 0L)
                config_paniclog(config,
                                ("Error parsing configuration file at line %lu: "
                                 "Option %s takes string argument"),
                                lineno, key);
            else
                config_paniclog(config,
                                "option \"%s\" takes string argument",
                                key);
            return (NIL);
        }
        break;
    case config_authtype:
        if (!config_parse_authtype((AUTHTYPE *) ptr, value)) {
            if (lineno > 0L)
                config_paniclog(config,
                                ("Error parsing configuration file at line %lu: "
                                 "Option %s takes a authtype argument"),
                                lineno, key);
            else
                config_paniclog(config,
                                "option \"%s\" takes a authtype argument",
                                key);
            return (NIL);
        }
        break;
    default:
        return (NIL);
    }
    return (T);
}

/* ====================================================================== */

/* config_skip_whitespace() **********************************************
 *
 * Skip over whitespace in string (clone of string_skip_whitespace?)
 *     sp:  Ptr to string. Updated to reflect location of next token
 *
 * Returns: Location of the next token.
 ************************************************************************/

static char *config_skip_whitespace(char **sp)
{
    char *s = *sp;

    if (!s)
        return (NIL);

    /* Skip leading whitespace */
    while ((*s == ' ') || (*s == '\t'))
        s++;

    *sp = s;

    return ((*s) ? s : NIL);
}

/* config_get_key() *******************************************************
 *
 * Get key token from config line (token terminated by whitespace or '='
 * character).
 *     sp:  Ptr to string. Updated to reflect location of next token
 *
 * Returns: Location of the next token.
 *************************************************************************/

static char *config_get_key(char **sp)
{
    char *s = *sp, *result;

    if (!s)
        return (NIL);

    while ((*s == ' ') || (*s == '\t'))
        s++;

    /* Record position of this token */
    result = s;

    /* Find next whitespace character, '=', or end of string */
    while ((*s) && (*s != ' ') && (*s != '\t') && (*s != '='))
        s++;

    /* Tie off the string unless \0 already reached, or '=' separator */
    if (*s && (*s != '=')) {
        *s++ = '\0';

        while ((*s == ' ') || (*s == '\t'))
            s++;
    }

    if (*s != '=')
        return (NIL);

    *s++ = '\0';

    while ((*s == ' ') || (*s == '\t'))
        s++;

    /* Record position of first non-whitespace character for next caller */
    *sp = s;

    return (result);
}

/* ====================================================================== */

/* config_parse_option() *************************************************
 *
 * Parse a single option passed in from the command line
 *   config:
 *   option: "key=value" or "<special> <args>" to parse
 *
 * Returns: T if option parsed successfully
 *          NIL otherwise (message will be sent to paniclog).
 ************************************************************************/

BOOL config_parse_option(struct config * config, char *option)
{
    char *key, *value;

    option = string_trim_whitespace(option);

    if (option[0] == '\0')
        return (T);

    if ((key = config_get_key(&option)) == NIL) {
        config_paniclog(config, "Option badly formatted: \"%s\"", option);
        return (NIL);
    }

    if ((value = config_skip_whitespace(&option)) == NIL) {
        config_paniclog(config,
                        "Option badly formatted: \"%s %s\"", key, option);
        return (NIL);
    }

    return (config_parse_single(config, key, value, 0L));
}


/* ====================================================================== */

/* config_get_line() ****************************************************
 *
 * Get a linear whitespace line (e.g: folded RFC822 or HTTP header line)
 *       sp: Ptr to current string location
 *           (updated to point to following line)
 *  squeeze: Remove superfluous spaces from result.
 *
 * Returns: Ptr to this line
 ***********************************************************************/

static char *config_get_line(char **sp, BOOL squeeze)
{
    char *s, *result;

    s = *sp;

    if (!(s && s[0]))
        return (NIL);

    /* CR, LF or CRLF before data proper starts? */
    if ((s[0] == '\015') || (s[0] == '\012')) {
        result = s;
        if ((s[0] == '\015') && (s[1] == '\012')) {     /* CRLF */
            *s = '\0';
            s += 2;
        } else
            *s++ = '\0';        /* CR or LF */

        *sp = s;
        return (result);
    }

    result = s;                 /* Record position of non-LWS */

    while (*s) {
        if ((*s == '\015') || (*s == '\012')) { /* CR, LF or CRLF */
            char *t = s;

            s += ((s[0] == '\015') && (s[1] == '\012')) ? 2 : 1;

            if ((*s != ' ') && (*s != '\t')) {
                *t = '\0';
                break;
            }
        } else
            s++;
    }
    *sp = s;                    /* Set up for next caller */
    return (result);
}

/* config_count_line() ***************************************************
 *
 * Count number of lines in string
 ************************************************************************/

static unsigned long config_count_line(char *s)
{
    unsigned long lines = 1;

    while (*s) {
        if ((*s == '\015') || (*s == '\012')) {
            s += ((s[0] == '\015') && (s[1] == '\012')) ? 2 : 1;
            lines++;
        } else
            s++;
    }
    return (lines);
}


/* config_compress_line() ************************************************
 *
 * Remove continuation sequences from LWS line
 ************************************************************************/

static BOOL config_compress_line(char *s)
{
    char *t = s;

    while (*s) {
        /* Check for correctly folded line */
        if ((s[0] == '\\') && ((s[1] == '\015') || (s[1] == '\012'))) {
            s += ((s[1] == '\015') && (s[2] == '\012')) ? 3 : 2;

            while (string_isspace(*s))
                s++;

            continue;
        }

        /* Check for unexpected line break */
        if ((s[0] == '\015') || (s[0] == '\012'))
            return (NIL);

        if (s > t)
            *t++ = *s++;
        else
            t++, s++;
    }
    *t = '\0';

    return (T);
}

/* ====================================================================== */

/* config_parse_file() ***************************************************
 *
 * Parse configuration file
 *     config:
 *   filename: Name of configuration file (main() routine will pass
 *             compile time default or user specified version).
 *
 * Returns: T if option parsed successfully
 *          NIL otherwise (message will be sent to paniclog).
 ************************************************************************/

BOOL config_parse_file(struct config * config, char *filename)
{
    char *buffer, *text;
    char *line, *key, *value;
    unsigned long next_lineno = 1;
    unsigned long cur_lineno = 1;
    BOOL okay;
    int fd;
    struct stat sbuf;
    unsigned long size;

    if ((fd = open(filename, O_RDONLY, 0)) < 0) {
        config_paniclog(config,
                        "Couldn't open configuration file: \"%s\"",
                        filename);
        return (NIL);
    }

    if ((fstat(fd, &sbuf)) < 0) {
        config_paniclog(config,
                        "Couldn't fstat configuration file: \"%s\"",
                        filename);
        return (NIL);
    }

    if ((text = buffer = malloc((size = sbuf.st_size) + 1)) == NIL) {
        config_paniclog(config, "Out of memory");
        return (NIL);
    }

    if (read(fd, buffer, size) < size) {
        free(buffer);
        config_paniclog(config,
                        "Couldn't read in configuration file: \"%s\"",
                        filename);
        return (NIL);
    }
    buffer[size] = '\0';

    /* Prep options array if we need to */
    if ((options_size == 0L) && !config_init()) {
        free(buffer);
        return (NIL);
    }

    okay = T;

    while ((line = config_get_line(&text, NIL))) {
        cur_lineno = next_lineno;
        next_lineno += config_count_line(line);

        if (!config_compress_line(line)) {
            config_paniclog(config,
                            "Invalid folded line starting at line %d: \"%s\"",
                            cur_lineno, line);
            okay = NIL;
            continue;
        }

        if (line[0] == '#')
            continue;

        line = string_trim_whitespace(line);

        if (line[0] == '\0')
            continue;

        if ((key = config_get_key(&line)) == NIL) {
            config_paniclog(config,
                            "Line %lu of config file badly formatted: \"%s\"",
                            cur_lineno, line);
            okay = NIL;
            continue;
        }

        if ((value = config_skip_whitespace(&line)) == NIL) {
            config_paniclog(config,
                            "Line %lu of config file badly formatted: \"%s %s\"",
                            cur_lineno, key, line);
            okay = NIL;
            continue;
        }

        if (!config_parse_single(config, key, value, cur_lineno))
            okay = NIL;
    }
    free(buffer);
    return (T);
}

/* ====================================================================== */

/* Couple of support macros to help test the integrity of the configuration
 * that we have just parsed */

#define TEST_STRING(x, y)                               \
{                                                       \
  if (!(x && x[0])) {                                   \
    config_paniclog(config, "config: "y" not defined"); \
    return(NIL);                                        \
  }                                                     \
}

#define TEST_NUMBER(x, y)                               \
{                                                       \
  if (x == 0) {                                         \
    config_paniclog(config, "config: "y" not defined"); \
    return(NIL);                                        \
  }                                                     \
}

/* config_check() *********************************************************
 *
 * Check that a parsed configuration is sane and self-consistent.
 *   config:
 *
 * Returns: T if config okay, NIL otherwise
 *************************************************************************/

BOOL config_check(struct config * config)
{
    TEST_STRING(config->msforward_name, "msforward_name");
    TEST_STRING(config->forward_name, "forward_name");
    TEST_STRING(config->forward_name, "forward_name");
    TEST_STRING(config->pwd_cmdline, "pwd_cmdline");
    TEST_STRING(config->full_cmdline, "full_cmdline");
    TEST_STRING(config->quota_cmdline, "quota_cmdline");

    if (config->authtype == AUTHTYPE_UNKNOWN) {
        config_paniclog(config, "authtype not defined");
        return (NIL);
    }

    return (T);
}


void
config_extract_ssl(struct config *src, struct ssl_config *dst)
{
    dst->ssl_session_dir      = src->ssl_session_dir;
    dst->ssl_cert_file        = src->ssl_cert_file;
    dst->ssl_privatekey_file  = src->ssl_privatekey_file;
    dst->ssl_dh_file          = src->ssl_dh_file;
    dst->ssl_session_timeout  = src->ssl_session_timeout;
    dst->ssl_rsakey_lifespan  = src->ssl_rsakey_lifespan;
    dst->ssl_rsakey_freshen   = src->ssl_rsakey_freshen;
    dst->ssl_default_port     = 0; /* XXX Not used */
    dst->egd_socket           = src->egd_socket;
    dst->log_debug            = src->log_debug;
}
