/* $Cambridge: hermes/src/prayer/servers/session_server.c,v 1.3 2008/09/16 09:59:57 dpc22 Exp $ */
/************************************************
 *    Prayer - a Webmail Interface              *
 ************************************************/

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

#include "server.h"

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

/* Structure containing information pertaining to a single login request */

struct session_server_request {
    struct pool *pool;          /* Scratch pool for this request   */
    char *username;             /* Username for this login request */
    char *password;             /* Password for this login request */
    unsigned long port;         /* Frontend port for this login request */
    BOOL use_ssl;               /* Using SSL on this port          */
    char *ua_options;           /* User interface override options */
    struct ipaddr *ipaddr;      /* IP address for this request */
};

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

/* session_server_request_create() **************************************
 *
 * Create new session_server_request structure with its own pool.
 ************************************************************************/

static struct session_server_request *session_server_request_create(void)
{
    struct pool *pool =
        pool_create((sizeof(struct session_server_request)) + 256);
    struct session_server_request *bsr =
        pool_alloc(pool, sizeof(struct session_server_request));

    memset((void *) bsr, 0, sizeof(struct session_server_request));
    bsr->pool = pool;
    bsr->ipaddr = ipaddr_create(pool);

    return (bsr);
}

/* session_server_request_free() *****************************************
 *
 * Discard (pool of) sesssion_server_request
 ************************************************************************/

static void session_server_request_free(struct session_server_request *bsr)
{
    pool_free(bsr->pool);
}

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

/* session_server_parse_request() ****************************************
 *
 * Parse login request from prayer frontend process
 *     bsr: session server request
 *  stream: iostream connection to frontend server.
 *
 * Returns: T if valid login request.
 *          NIL on protocol error or unexpected disconnect
 ************************************************************************/

static BOOL
session_server_parse_request(struct session_server_request *bsr,
                             struct iostream *stream)
{
    struct buffer *b = buffer_create(bsr->pool, 0);
    int c;
    char *line;
    char *portstr, *sslstr;
    char *iptext;

    while (((c = iogetc(stream)) != EOF) && (c != '\015') && (c != '\012'))
        bputc(b, c);

    if (c == EOF) {
        log_panic("[session_server_parse_request()] Truncated input line");
        return (NIL);
    }

    line = buffer_fetch(b, 0L, buffer_size(b), NIL);

    if (!((bsr->username = string_get_token(&line)) &&
          (bsr->password = string_get_token(&line)) &&
          (bsr->ua_options = string_get_token(&line)))) {
        log_panic("[session_server_parse_request()] Truncated input line");
        return (NIL);
    }

    bsr->username = string_canon_decode(bsr->username);
    bsr->password = string_canon_decode(bsr->password);
    bsr->ua_options = string_canon_decode(bsr->ua_options);

    /* Some people insist on entering username as  upper case... */
    string_lcase(bsr->username);

    if (!((portstr = string_get_token(&line)) &&
          (sslstr = string_get_token(&line)) &&
          (iptext = string_get_token(&line)))) {
        log_panic("[session_server_parse_request()] Truncated input line");
        return (NIL);
    }

    bsr->port = atoi(portstr);
    bsr->use_ssl = (atoi(sslstr) != 0) ? T : NIL;

    if (bsr->port == 0L) {
        log_panic(("[session_server_parse_request()] "
                   "Invalid port number (%s) supplied"), portstr);
        return (NIL);
    }

    if (!ipaddr_parse(bsr->ipaddr, iptext)) {
        log_panic("[session_server_parse_request()] Invalid IP address");
        return (NIL);
    }

    return (T);
}

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

/* session_server_process_request() **************************************
 *
 * Process login request in either foreground or (more normal background)
 *             bsr: session_login_request
 *    ssl_portlist: Portlist for SSL if direct connection mode
 *  plain_portlist: Portlist for plaintext if direct connection mode
 *          sockfd: Socket for master listener
 *                  (passed here only so that child process can close).
 *          stream: iostream connection to prayer frontend
 *                  (Used to return consequence of login request)
 *      foreground: Foreground login session (used for debugging)
 *
 * Returns: T if login request processed sucessfully
 *          NIL on error.
 ************************************************************************/

static BOOL
session_server_process_request(struct session_server_request *bsr,
                               struct config *config,
                               struct portlist *ssl_portlist,
                               struct portlist *plain_portlist,
                               int sockfd,
                               struct iostream *stream, BOOL foreground)
{
    struct session *session = NIL;
    struct port *port = NIL;
    pid_t childpid;
    struct user_agent *user_agent = user_agent_create(bsr->pool);

    /* Parse user agent options into temporary structure */
    user_agent_parse(user_agent, pool_strdup(bsr->pool, bsr->ua_options));

    if (foreground) {
        /* Set up a new, clean session */
        if ((session = session_create(config)) == NIL) {
            log_panic("[session_server()] session_create() failed");
            return (NIL);
        }

        if (!session_login(session,
                           bsr->username, bsr->password,
                           bsr->port, bsr->use_ssl,
                           bsr->ua_options, bsr->ipaddr)) {
            session_log(session, "Attempted login failed: \"%s\"",
                        ml_errmsg());

            if (ml_have_error()) {
                ioprintf(stream, "NO %s" CRLF, ml_errmsg());
            } else
                ioputs(stream, "NO Login Incorrect" CRLF);
            ioflush(stream);
            session_free(session);
            return (NIL);
        }
        session_unix(session, stream);

        session_free(session);
        return (T);
    }

    if ((childpid = fork()) < 0) {
        /* May be transcient error! */
        session_paniclog(session, "[session_server()] fork() failed, %s",
                         strerror(errno));
        return (NIL);
    }

    if (port)
        port->pid = childpid;

    if (childpid != 0)
        return (T);             /* Parent process */

  /***** Child process *******/

    /* Shut down file descriptors that the child neither wants or needs */
    close(sockfd);

    if (config->direct_enable) {
        if (port) {
            portlist_close_all_except(ssl_portlist, port->sockfd);
            portlist_close_all_except(plain_portlist, port->sockfd);
        } else {
            portlist_close_all(ssl_portlist);
            portlist_close_all(plain_portlist);
        }
    }

    /* Clear out child reaper: interferes with waitpid */
    if (!os_signal_child_clear())
        return (NIL);

    /* Set up a new, clean session */
    if ((session = session_create(config)) == NIL)
        log_fatal("[session_server()] session_create() failed");

    if (!session_login(session,
                       bsr->username, bsr->password,
                       bsr->port, bsr->use_ssl, bsr->ua_options,
                       bsr->ipaddr)) {
        session_log(session, "Attempted login failed: \"%s\"",
                    ml_errmsg());
        if (ml_have_error())
            ioprintf(stream, "NO %s" CRLF, ml_errmsg());
        else
            ioputs(stream, "NO Login Incorrect" CRLF);
        ioflush(stream);
        session_free(session);
        exit(0);
        /* NOTREACHED */
        return (NIL);
    }

    session_unix(session, stream);

    session_free(session);
    exit(0);
    /* NOTREACHED */
    return (NIL);
}

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

/* session_server_ping() *************************************************
 *
 * Ping various log files once in a while.
 *   config:  Prayer configuration
 ************************************************************************/

static void session_server_ping(struct config *config)
{
    /* Reopen misc log file */
    log_misc_ping();

    /* Reopen local domain maps */
    session_config_local_domain_ping(config);
}

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

/* session_server() ******************************************************
 *
 * Master session server routine
 *     config: Prayer configuration
 * foreground: Run as foreground server (for debugging purposes)
 *
 * Returns: NIL on fatal error during startup. Otherwise never!
 ************************************************************************/

BOOL session_server(struct config *config, BOOL foreground)
{
    struct portlist *ssl_portlist = NIL;
    struct portlist *plain_portlist = NIL;
    int sockfd, newsockfd;
    struct session_server_request *bsr;
    char *name;
    struct iostream *stream;
    int maxfd, rc;
    fd_set readfds;
    pid_t child;
    unsigned long timeout = 0L;

    if (config->direct_enable) {
        ssl_portlist
            =
            portlist_create(config->direct_ssl_first,
                            config->direct_ssl_count);
        plain_portlist =
            portlist_create(config->direct_plain_first,
                            config->direct_plain_count);
    }

    if (config->socket_dir == NIL) {
        /* Should be impossible! */
        log_panic("[session_server()] socket_dir not defined");
        return (NIL);
    }

    if (!os_signal_child_init(os_child_reaper))
        return (NIL);

    name = pool_printf(NIL, "%s/%s",
		       config->socket_dir, config->init_socket_name);

    if ((sockfd = os_bind_unix_socket(name)) < 0)
        return (NIL);

    log_misc("Backend master server started okay");

    for (;;) {
        setproctitle("Master server");

        do {
            do {
                if (((child = os_waitpid_nohang()) > 0) &&
                    !portlist_free_port_bypid(ssl_portlist, child))
                    portlist_free_port_bypid(plain_portlist, child);
            }
            while (child > 0);
            /* Requests should queue on sockfd until we are ready to serve them */
            /* SIGCHLD breaks select() with EINTR, readfds recalculated */

            FD_ZERO(&readfds);
            FD_SET(sockfd, &readfds);
            maxfd = sockfd;

            timeout = config->log_ping_interval;

            if (config->direct_enable && ((timeout == 0)
                                          || (config->ssl_rsakey_lifespan <
                                              timeout)))
                timeout = config->ssl_rsakey_lifespan;

            if (timeout > 0) {
                struct timeval timeval;

                timeval.tv_sec = timeout;
                timeval.tv_usec = 0;

                rc = select(maxfd + 1, &readfds, NIL, NIL, &timeval);
            } else {
                rc = select(maxfd + 1, &readfds, NIL, NIL, NIL);
            }
        }
        while ((rc < 0) && (errno == EINTR));

        if (rc == 0) {
            /* Ping various log files and recalculate SSL RSA key once in a while */
            session_server_ping(config);
            continue;
        }

        if (!FD_ISSET(sockfd, &readfds))
            continue;

        /* Have new incoming connection */
        if ((newsockfd = os_accept_unix(sockfd)) < 0) {
            log_panic("[session_server()] accept() failed, %s",
                      strerror(errno));
            continue;           /* Might be transient failure */
        }

        if ((stream = iostream_create(NIL, newsockfd, 0)) == NIL) {
            log_panic
                ("[session_server()] Couldn't bind iostream to socket");
            continue;
        }
        iostream_set_timeout(stream, config->session_timeout);

        bsr = session_server_request_create();
        if (!session_server_parse_request(bsr, stream)) {
            ioputs(stream, "BAD Invalid request from frontend" CRLF);
            log_panic
                ("[session_server_inet()] Invalid request from frontend ");
        } else {
            session_server_process_request(bsr, config,
                                           ssl_portlist, plain_portlist,
                                           sockfd, stream, foreground);
        }
        iostream_close(stream);
        session_server_request_free(bsr);

        /* Ping various log files and recalculate SSL RSA key once in a while */
        session_server_ping(config);
    }
}
