/*
 * SPDX-License-Identifier: ISC
 *
 * Copyright (c) 2019-2022 Todd C. Miller <Todd.Miller@sudo.ws>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

/*
 * This is an open source non-commercial project. Dear PVS-Studio, please check it.
 * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
 */

#include <config.h>

#include <sys/stat.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#ifdef HAVE_STDBOOL_H
# include <stdbool.h>
#else
# include "compat/stdbool.h"
#endif /* HAVE_STDBOOL_H */
#if defined(HAVE_STDINT_H)
# include <stdint.h>
#elif defined(HAVE_INTTYPES_H)
# include <inttypes.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#ifdef HAVE_GETOPT_LONG
# include <getopt.h>
# else
# include "compat/getopt.h"
#endif /* HAVE_GETOPT_LONG */

#define NEED_INET_NTOP		/* to expose sudo_inet_ntop in sudo_compat.h */

#include "pathnames.h"
#include "sudo_compat.h"
#include "sudo_conf.h"
#include "sudo_debug.h"
#include "sudo_event.h"
#include "sudo_eventlog.h"
#include "sudo_fatal.h"
#include "sudo_gettext.h"
#include "sudo_json.h"
#include "sudo_iolog.h"
#include "sudo_queue.h"
#include "sudo_rand.h"
#include "sudo_util.h"

#include "logsrvd.h"
#include "hostcheck.h"

#ifndef O_NOFOLLOW
# define O_NOFOLLOW 0
#endif

/*
 * Sudo I/O audit server.
 */
static int logsrvd_debug_instance = SUDO_DEBUG_INSTANCE_INITIALIZER;
TAILQ_HEAD(connection_list, connection_closure);
static struct connection_list connections = TAILQ_HEAD_INITIALIZER(connections);
static struct listener_list listeners = TAILQ_HEAD_INITIALIZER(listeners);
static const char server_id[] = "Sudo Audit Server " PACKAGE_VERSION;
static const char *conf_file = NULL;

/* Event loop callbacks. */
static void client_msg_cb(int fd, int what, void *v);
static void server_msg_cb(int fd, int what, void *v);
static void server_commit_cb(int fd, int what, void *v);
#if defined(HAVE_OPENSSL)
static void tls_handshake_cb(int fd, int what, void *v);
#endif

/*
 * Free a struct connection_closure container and its contents.
 */
static void
connection_closure_free(struct connection_closure *closure)
{
    debug_decl(connection_closure_free, SUDO_DEBUG_UTIL);

    if (closure != NULL) {
	bool shutting_down = closure->state == SHUTDOWN;
	struct sudo_event_base *evbase = closure->evbase;
	struct connection_buffer *buf;

	TAILQ_REMOVE(&connections, closure, entries);

	if (closure->state == CONNECTING && closure->journal != NULL) {
	    /* Failed to relay journal file, retry later. */
	    logsrvd_queue_insert(closure);
	}
	if (closure->relay_closure != NULL)
	    relay_closure_free(closure->relay_closure);
#if defined(HAVE_OPENSSL)
	if (closure->ssl != NULL) {
	    /* Must call SSL_shutdown() before closing closure->sock. */
	    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		"closing down TLS connection from %s", closure->ipaddr);
	    if (SSL_shutdown(closure->ssl) == 0)
		SSL_shutdown(closure->ssl);
	    SSL_free(closure->ssl);
	}
#endif
	if (closure->sock != -1) {
	    shutdown(closure->sock, SHUT_RDWR);
	    close(closure->sock);
	}
	iolog_close_all(closure);
	sudo_ev_free(closure->commit_ev);
	sudo_ev_free(closure->read_ev);
	sudo_ev_free(closure->write_ev);
#if defined(HAVE_OPENSSL)
	sudo_ev_free(closure->ssl_accept_ev);
#endif
	eventlog_free(closure->evlog);
	free(closure->read_buf.data);
	while ((buf = TAILQ_FIRST(&closure->write_bufs)) != NULL) {
	    sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
		"discarding write buffer %p, len %u", buf, buf->len - buf->off);
	    TAILQ_REMOVE(&closure->write_bufs, buf, entries);
	    free(buf->data);
	    free(buf);
	}
	while ((buf = TAILQ_FIRST(&closure->free_bufs)) != NULL) {
	    TAILQ_REMOVE(&closure->free_bufs, buf, entries);
	    free(buf->data);
	    free(buf);
	}
	free(closure->journal_path);
	if (closure->journal != NULL)
	    fclose(closure->journal);
	free(closure);

	if (shutting_down && TAILQ_EMPTY(&connections))
	    sudo_ev_loopbreak(evbase);
    }

    debug_return;
}

/*
 * Allocate a new connection closure.
 */
struct connection_closure *
connection_closure_alloc(int fd, bool tls, bool relay_only,
    struct sudo_event_base *base)
{
    struct connection_closure *closure;
    debug_decl(connection_closure_alloc, SUDO_DEBUG_UTIL);

    if ((closure = calloc(1, sizeof(*closure))) == NULL)
	debug_return_ptr(NULL);

    closure->iolog_dir_fd = -1;
    closure->sock = relay_only ? -1 : fd;
    closure->evbase = base;
    TAILQ_INIT(&closure->write_bufs);
    TAILQ_INIT(&closure->free_bufs);

    /* Use different message handlers depending on the operating mode. */
    if (relay_only) {
	closure->cms = &cms_relay;
    } else if (logsrvd_conf_relay_store_first()) {
	closure->store_first = true;
	closure->cms = &cms_journal;
    } else {
	closure->cms = &cms_local;
    }

    TAILQ_INSERT_TAIL(&connections, closure, entries);

    closure->read_buf.size = 64 * 1024;
    closure->read_buf.data = malloc(closure->read_buf.size);
    if (closure->read_buf.data == NULL)
	goto bad;

    closure->read_ev = sudo_ev_alloc(fd, SUDO_EV_READ|SUDO_EV_PERSIST,
	client_msg_cb, closure);
    if (closure->read_ev == NULL)
	goto bad;

    if (!relay_only) {
	closure->write_ev = sudo_ev_alloc(fd, SUDO_EV_WRITE|SUDO_EV_PERSIST,
	    server_msg_cb, closure);
	if (closure->write_ev == NULL)
	    goto bad;

	closure->commit_ev = sudo_ev_alloc(-1, SUDO_EV_TIMEOUT,
	    server_commit_cb, closure);
	if (closure->commit_ev == NULL)
	    goto bad;
    }
#if defined(HAVE_OPENSSL)
    if (tls) {
	closure->ssl_accept_ev = sudo_ev_alloc(fd, SUDO_EV_READ,
	    tls_handshake_cb, closure);
	if (closure->ssl_accept_ev == NULL)
	    goto bad;
    }
#endif

    debug_return_ptr(closure);
bad:
    connection_closure_free(closure);
    debug_return_ptr(NULL);
}

/*
 * Close the client connection when finished.
 * If in store-and-forward mode, initiate a relay connection.
 * Otherwise, free the connection closure, removing any events.
 */
void
connection_close(struct connection_closure *closure)
{
    struct connection_closure *new_closure;
    debug_decl(connection_close, SUDO_DEBUG_UTIL);

    if (closure == NULL)
	debug_return;

    /* Final state should be FINISHED except on error. */
    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
	"%s: closure %p, final state %d, relay_closure %p, "
	"journal file %p, journal path %s", __func__, closure,
	closure->state, closure->relay_closure, closure->journal,
	closure->journal_path ? closure->journal_path : "");

    /*
     * If we finished a client connection in store-and-forward mode,
     * create a new connection for the relay and replay the journal.
     */
    if (closure->store_first && closure->state == FINISHED &&
	    closure->relay_closure == NULL && closure->journal != NULL) {
	new_closure = connection_closure_alloc(fileno(closure->journal), false,
	    true, closure->evbase);
	if (new_closure != NULL) {
	    /* Re-parent journal settings. */
	    new_closure->journal = closure->journal;
	    closure->journal = NULL;
	    new_closure->journal_path = closure->journal_path;
	    closure->journal_path = NULL;

	    /* Connect to the first relay available asynchronously. */
	    if (!connect_relay(new_closure)) {
		sudo_warnx("%s", U_("unable to connect to relay"));
		connection_closure_free(new_closure);
	    }
	}
    }
    if (closure->state == FINISHED && closure->journal_path != NULL) {
	/* Journal relayed successfully, remove backing file. */
	sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
	    "removing journal file %s", closure->journal_path);
	unlink(closure->journal_path);

	/* Process the next outgoing file (if any). */
	logsrvd_queue_enable(0, closure->evbase);
    }
    connection_closure_free(closure);

    debug_return;
}

struct connection_buffer *
get_free_buf(size_t len, struct connection_closure *closure)
{
    struct connection_buffer *buf;
    debug_decl(get_free_buf, SUDO_DEBUG_UTIL);

    buf = TAILQ_FIRST(&closure->free_bufs);
    if (buf != NULL) {
        TAILQ_REMOVE(&closure->free_bufs, buf, entries);
    } else {
        if ((buf = calloc(1, sizeof(*buf))) == NULL)
	    goto oom;
    }

    if (len > buf->size) {
	const unsigned int new_size = sudo_pow2_roundup(len);
	if (new_size < len) {
	    /* overflow */
	    errno = ENOMEM;
	    goto oom;
	}
	free(buf->data);
	if ((buf->data = malloc(new_size)) == NULL)
	    goto oom;
	buf->size = new_size;
    }

    debug_return_ptr(buf);
oom:
    if (buf != NULL) {
	free(buf->data);
	free(buf);
    }
    sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
    debug_return_ptr(NULL);
}

static bool
fmt_server_message(struct connection_closure *closure, ServerMessage *msg)
{
    struct connection_buffer *buf = NULL;
    uint32_t msg_len;
    bool ret = false;
    size_t len;
    debug_decl(fmt_server_message, SUDO_DEBUG_UTIL);

    len = server_message__get_packed_size(msg);
    if (len > MESSAGE_SIZE_MAX) {
	sudo_warnx(U_("server message too large: %zu"), len);
        goto done;
    }

    /* Wire message size is used for length encoding, precedes message. */
    msg_len = htonl((uint32_t)len);
    len += sizeof(msg_len);

    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
	"size + server message %zu bytes", len);

    if ((buf = get_free_buf(len, closure)) == NULL) {
	sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
	    "unable to allocate connection_buffer");
        goto done;
    }
    memcpy(buf->data, &msg_len, sizeof(msg_len));
    server_message__pack(msg, buf->data + sizeof(msg_len));
    buf->len = len;
    TAILQ_INSERT_TAIL(&closure->write_bufs, buf, entries);

    ret = true;

done:
    debug_return_bool(ret);
}

static bool
fmt_hello_message(struct connection_closure *closure)
{
    ServerMessage msg = SERVER_MESSAGE__INIT;
    ServerHello hello = SERVER_HELLO__INIT;
    debug_decl(fmt_hello_message, SUDO_DEBUG_UTIL);

    /* TODO: implement redirect and servers array.  */
    hello.server_id = (char *)server_id;
    hello.subcommands = true;
    msg.u.hello = &hello;
    msg.type_case = SERVER_MESSAGE__TYPE_HELLO;

    debug_return_bool(fmt_server_message(closure, &msg));
}

bool
fmt_log_id_message(const char *id, struct connection_closure *closure)
{
    ServerMessage msg = SERVER_MESSAGE__INIT;
    debug_decl(fmt_log_id_message, SUDO_DEBUG_UTIL);

    msg.u.log_id = (char *)id;
    msg.type_case = SERVER_MESSAGE__TYPE_LOG_ID;

    debug_return_bool(fmt_server_message(closure, &msg));
}

static bool
fmt_error_message(const char *errstr, struct connection_closure *closure)
{
    ServerMessage msg = SERVER_MESSAGE__INIT;
    debug_decl(fmt_error_message, SUDO_DEBUG_UTIL);

    msg.u.error = (char *)errstr;
    msg.type_case = SERVER_MESSAGE__TYPE_ERROR;

    debug_return_bool(fmt_server_message(closure, &msg));
}

/*
 * Format a ServerMessage with the error string and add it to the write queue.
 * Also sets the error flag state to true.
 * Returns true if successfully scheduled, else false.
 */
bool
schedule_error_message(const char *errstr, struct connection_closure *closure)
{
    bool ret = false;
    debug_decl(schedule_error_message, SUDO_DEBUG_UTIL);

    sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
	"send error to client: %s", errstr ? errstr : "none");

    /* Prevent further reads from the client, just write the error. */
    sudo_ev_del(closure->evbase, closure->read_ev);

    if (errstr == NULL || closure->error || closure->write_ev == NULL)
	goto done;

    /* Format error message and add to the write queue. */
    if (!fmt_error_message(errstr, closure))
	goto done;
    if (sudo_ev_add(closure->evbase, closure->write_ev,
	    logsrvd_conf_server_timeout(), true) == -1) {
	sudo_warnx("%s", U_("unable to add event to queue"));
	goto done;
    }
    ret = true;

done:
    closure->error = true;
    debug_return_bool(ret);
}

/*
 * AcceptMessage handler.
 */
static bool
handle_accept(AcceptMessage *msg, uint8_t *buf, size_t len,
    struct connection_closure *closure)
{
    const char *source = closure->journal_path ? closure->journal_path :
        closure->ipaddr;
    bool ret;
    debug_decl(handle_accept, SUDO_DEBUG_UTIL);

    /* We can get an AcceptMessage for a sub-command during a session. */
    if (closure->state == EXITED || closure->state == FINISHED) {
	sudo_warnx(U_("unexpected state %d for %s"), closure->state, source);
	closure->errstr = _("state machine error");
	debug_return_bool(false);
    }

    /* Check that message is valid. */
    if (msg->submit_time == NULL || msg->n_info_msgs == 0) {
	sudo_warnx(U_("%s: %s"), source, U_("invalid AcceptMessage"));
	closure->errstr = _("invalid AcceptMessage");
	debug_return_bool(false);
    }
    sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received AcceptMessage from %s",
	__func__, source);

    ret = closure->cms->accept(msg, buf, len, closure);
    if (ret && closure->state == INITIAL) {
	if (msg->expect_iobufs)
	    closure->log_io = true;
	closure->state = RUNNING;
    }
    debug_return_bool(ret);
}

/*
 * RejectMessage handler.
 */
static bool
handle_reject(RejectMessage *msg, uint8_t *buf, size_t len,
    struct connection_closure *closure)
{
    const char *source = closure->journal_path ? closure->journal_path :
        closure->ipaddr;
    bool ret;
    debug_decl(handle_reject, SUDO_DEBUG_UTIL);

    /* We can get a RejectMessage for a sub-command during a session. */
    if (closure->state == EXITED || closure->state == FINISHED) {
	sudo_warnx(U_("unexpected state %d for %s"), closure->state, source);
	closure->errstr = _("state machine error");
	debug_return_bool(false);
    }

    /* Check that message is valid. */
    if (msg->submit_time == NULL || msg->n_info_msgs == 0) {
	sudo_warnx(U_("%s: %s"), source, U_("invalid RejectMessage"));
	closure->errstr = _("invalid RejectMessage");
	debug_return_bool(false);
    }
    sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received RejectMessage from %s",
	__func__, source);

    ret = closure->cms->reject(msg, buf, len, closure);
    if (ret && closure->state == INITIAL) {
	closure->state = FINISHED;
    }

    debug_return_bool(ret);
}

static bool
handle_exit(ExitMessage *msg, uint8_t *buf, size_t len,
    struct connection_closure *closure)
{
    const char *source = closure->journal_path ? closure->journal_path :
	closure->ipaddr;
    bool ret;
    debug_decl(handle_exit, SUDO_DEBUG_UTIL);

    if (closure->state != RUNNING) {
	sudo_warnx(U_("unexpected state %d for %s"), closure->state, source);
	closure->errstr = _("state machine error");
	debug_return_bool(false);
    }

    /* Check that message is valid. */
    if (msg->run_time == NULL) {
	sudo_warnx(U_("%s: %s"), source, U_("invalid ExitMessage"));
	closure->errstr = _("invalid ExitMessage");
	debug_return_bool(false);
    }
    sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received ExitMessage from %s",
	source, __func__);

    ret = closure->cms->exit(msg, buf, len, closure);
    if (ret) {
	if (sudo_timespecisset(&closure->elapsed_time)) {
	    sudo_debug_printf(SUDO_DEBUG_INFO, "%s: elapsed time: [%lld, %ld]",
		__func__, (long long)closure->elapsed_time.tv_sec,
		closure->elapsed_time.tv_nsec);
	}

	if (closure->log_io) {
	    /* Command exited, client waiting for final commit point. */
	    closure->state = EXITED;

	    /* Relay host will send the final commit point. */
	    if (closure->relay_closure == NULL) {
		struct timespec tv = { 0, 0 };
		if (sudo_ev_add(closure->evbase, closure->commit_ev, &tv, false) == -1) {
		    sudo_warnx("%s", U_("unable to add event to queue"));
		    ret = false;
		}
	    }
	} else {
	    /* No commit point to send to client, we are finished. */
	    closure->state = FINISHED;
	}
    }
    sudo_ev_del(closure->evbase, closure->read_ev);

    debug_return_bool(ret);
}

static bool
handle_restart(RestartMessage *msg, uint8_t *buf, size_t len,
    struct connection_closure *closure)
{
    const char *source = closure->journal_path ? closure->journal_path :
	closure->ipaddr;
    bool ret = true;
    debug_decl(handle_restart, SUDO_DEBUG_UTIL);

    if (closure->state != INITIAL) {
	sudo_warnx(U_("unexpected state %d for %s"), closure->state, source);
	closure->errstr = _("state machine error");
	debug_return_bool(false);
    }

    /* Check that message is valid. */
    if (msg->log_id == NULL || msg->resume_point == NULL) {
	sudo_warnx(U_("%s: %s"), source, U_("invalid RestartMessage"));
	closure->errstr = _("invalid RestartMessage");
	debug_return_bool(false);
    }
    sudo_debug_printf(SUDO_DEBUG_INFO,
	"%s: received RestartMessage for %s from %s", __func__, msg->log_id,
	source);

    /* Only I/O logs are restartable. */
    closure->log_io = true;

    if (closure->cms->restart(msg, buf, len, closure)) {
	/* Successfully restarted. */
	closure->state = RUNNING;
    } else {
	/* Report error to client before closing the connection. */
	sudo_debug_printf(SUDO_DEBUG_WARN, "%s: unable to restart I/O log",
	    __func__);
	if (!schedule_error_message(closure->errstr, closure))
	    ret = false;
    }

    debug_return_bool(ret);
}

static bool
handle_alert(AlertMessage *msg, uint8_t *buf, size_t len,
    struct connection_closure *closure)
{
    const char *source = closure->journal_path ? closure->journal_path :
	closure->ipaddr;
    debug_decl(handle_alert, SUDO_DEBUG_UTIL);

    /* Check that message is valid. */
    if (msg->alert_time == NULL || msg->reason == NULL) {
	sudo_warnx(U_("%s: %s"), source, U_("invalid AlertMessage"));
	closure->errstr = _("invalid AlertMessage");
	debug_return_bool(false);
    }
    sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received AlertMessage from %s",
	source, __func__);

    debug_return_bool(closure->cms->alert(msg, buf, len, closure));
}

/* Enable a commit event if not relaying and it is not already pending. */
static bool
enable_commit(struct connection_closure *closure)
{
    debug_decl(enable_commit, SUDO_DEBUG_UTIL);

    if (closure->relay_closure == NULL) {
	if (!ISSET(closure->commit_ev->flags, SUDO_EVQ_INSERTED)) {
	    struct timespec tv = { ACK_FREQUENCY, 0 };
	    if (sudo_ev_add(closure->evbase, closure->commit_ev, &tv, false) == -1) {
		sudo_warnx("%s", U_("unable to add event to queue"));
		debug_return_bool(false);
	    }
	}
    }
    debug_return_bool(true);
}

static bool
handle_iobuf(int iofd, IoBuffer *iobuf, uint8_t *buf, size_t len,
    struct connection_closure *closure)
{
    const char *source = closure->journal_path ? closure->journal_path :
	closure->ipaddr;
    debug_decl(handle_iobuf, SUDO_DEBUG_UTIL);

    if (closure->state != RUNNING) {
	sudo_warnx(U_("unexpected state %d for %s"), closure->state, source);
	closure->errstr = _("state machine error");
	debug_return_bool(false);
    }
    if (!closure->log_io) {
	sudo_warnx(U_("%s: unexpected IoBuffer"), source);
	closure->errstr = _("protocol error");
	debug_return_bool(false);
    }

    /* Check that message is valid. */
    if (iobuf->delay == NULL) {
	sudo_warnx(U_("%s: %s"), source, U_("invalid IoBuffer"));
	closure->errstr = _("invalid IoBuffer");
	debug_return_bool(false);
    }
    sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received IoBuffer from %s",
	source, __func__);

    if (!closure->cms->iobuf(iofd, iobuf, buf, len, closure))
	debug_return_bool(false);
    if (!enable_commit(closure))
	debug_return_bool(false);

    debug_return_bool(true);
}

static bool
handle_winsize(ChangeWindowSize *msg, uint8_t *buf, size_t len,
    struct connection_closure *closure)
{
    const char *source = closure->journal_path ? closure->journal_path :
	closure->ipaddr;
    debug_decl(handle_winsize, SUDO_DEBUG_UTIL);

    if (closure->state != RUNNING) {
	sudo_warnx(U_("unexpected state %d for %s"), closure->state, source);
	closure->errstr = _("state machine error");
	debug_return_bool(false);
    }
    if (!closure->log_io) {
	sudo_warnx(U_("%s: unexpected IoBuffer"), source);
	closure->errstr = _("protocol error");
	debug_return_bool(false);
    }

    /* Check that message is valid. */
    if (msg->delay == NULL) {
	sudo_warnx(U_("%s: %s"), source, U_("invalid ChangeWindowSize"));
	closure->errstr = _("invalid ChangeWindowSize");
	debug_return_bool(false);
    }
    sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received ChangeWindowSize from %s",
	source, __func__);

    if (!closure->cms->winsize(msg, buf, len, closure))
	debug_return_bool(false);
    if (!enable_commit(closure))
	debug_return_bool(false);

    debug_return_bool(true);
}

static bool
handle_suspend(CommandSuspend *msg, uint8_t *buf, size_t len,
    struct connection_closure *closure)
{
    const char *source = closure->journal_path ? closure->journal_path :
	closure->ipaddr;
    debug_decl(handle_syspend, SUDO_DEBUG_UTIL);

    if (closure->state != RUNNING) {
	sudo_warnx(U_("unexpected state %d for %s"), closure->state, source);
	closure->errstr = _("state machine error");
	debug_return_bool(false);
    }
    if (!closure->log_io) {
	sudo_warnx(U_("%s: unexpected IoBuffer"), source);
	closure->errstr = _("protocol error");
	debug_return_bool(false);
    }

    /* Check that message is valid. */
    if (msg->delay == NULL || msg->signal == NULL) {
	sudo_warnx(U_("%s: %s"), source, U_("invalid CommandSuspend"));
	closure->errstr = _("invalid CommandSuspend");
	debug_return_bool(false);
    }
    sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received CommandSuspend from %s",
	source, __func__);

    if (!closure->cms->suspend(msg, buf, len, closure))
	debug_return_bool(false);
    if (!enable_commit(closure))
	debug_return_bool(false);

    debug_return_bool(true);
}

static bool
handle_client_hello(ClientHello *msg, uint8_t *buf, size_t len,
    struct connection_closure *closure)
{
    const char *source = closure->journal_path ? closure->journal_path :
	closure->ipaddr;
    debug_decl(handle_client_hello, SUDO_DEBUG_UTIL);

    if (closure->state != INITIAL) {
	sudo_warnx(U_("unexpected state %d for %s"), closure->state, source);
	closure->errstr = _("state machine error");
	debug_return_bool(false);
    }

    sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received ClientHello",
	__func__);
    sudo_debug_printf(SUDO_DEBUG_INFO, "%s: client ID %s",
	__func__, msg->client_id ? msg->client_id : "unknown");

    debug_return_bool(true);
}

static bool
handle_client_message(uint8_t *buf, size_t len,
    struct connection_closure *closure)
{
    const char *source = closure->journal_path ? closure->journal_path :
        closure->ipaddr;
    ClientMessage *msg;
    bool ret = false;
    debug_decl(handle_client_message, SUDO_DEBUG_UTIL);

    /* TODO: can we extract type_case without unpacking for relay case? */
    msg = client_message__unpack(NULL, len, buf);
    if (msg == NULL) {
	sudo_warnx(U_("unable to unpack %s size %zu"), "ClientMessage", len);
	debug_return_bool(false);
    }

    switch (msg->type_case) {
    case CLIENT_MESSAGE__TYPE_ACCEPT_MSG:
	ret = handle_accept(msg->u.accept_msg, buf, len, closure);
	break;
    case CLIENT_MESSAGE__TYPE_REJECT_MSG:
	ret = handle_reject(msg->u.reject_msg, buf, len, closure);
	break;
    case CLIENT_MESSAGE__TYPE_EXIT_MSG:
	ret = handle_exit(msg->u.exit_msg, buf, len, closure);
	break;
    case CLIENT_MESSAGE__TYPE_RESTART_MSG:
	ret = handle_restart(msg->u.restart_msg, buf, len, closure);
	break;
    case CLIENT_MESSAGE__TYPE_ALERT_MSG:
	ret = handle_alert(msg->u.alert_msg, buf, len, closure);
	break;
    case CLIENT_MESSAGE__TYPE_TTYIN_BUF:
	ret = handle_iobuf(IOFD_TTYIN, msg->u.ttyin_buf, buf, len, closure);
	break;
    case CLIENT_MESSAGE__TYPE_TTYOUT_BUF:
	ret = handle_iobuf(IOFD_TTYOUT, msg->u.ttyout_buf, buf, len, closure);
	break;
    case CLIENT_MESSAGE__TYPE_STDIN_BUF:
	ret = handle_iobuf(IOFD_STDIN, msg->u.stdin_buf, buf, len, closure);
	break;
    case CLIENT_MESSAGE__TYPE_STDOUT_BUF:
	ret = handle_iobuf(IOFD_STDOUT, msg->u.stdout_buf, buf, len, closure);
	break;
    case CLIENT_MESSAGE__TYPE_STDERR_BUF:
	ret = handle_iobuf(IOFD_STDERR, msg->u.stderr_buf, buf, len, closure);
	break;
    case CLIENT_MESSAGE__TYPE_WINSIZE_EVENT:
	ret = handle_winsize(msg->u.winsize_event, buf, len, closure);
	break;
    case CLIENT_MESSAGE__TYPE_SUSPEND_EVENT:
	ret = handle_suspend(msg->u.suspend_event, buf, len, closure);
	break;
    case CLIENT_MESSAGE__TYPE_HELLO_MSG:
	ret = handle_client_hello(msg->u.hello_msg, buf, len, closure);
	break;
    default:
	sudo_warnx(U_("unexpected type_case value %d in %s from %s"),
	    msg->type_case, "ClientMessage", source);
	closure->errstr = _("unrecognized ClientMessage type");
	break;
    }
    client_message__free_unpacked(msg, NULL);

    debug_return_bool(ret);
}

static void
shutdown_cb(int unused, int what, void *v)
{
    struct sudo_event_base *base = v;
    debug_decl(shutdown_cb, SUDO_DEBUG_UTIL);

    sudo_ev_loopbreak(base);

    debug_return;
}

/*
 * Shut down active client connections if any, or exit immediately.
 */
static void
server_shutdown(struct sudo_event_base *base)
{
    struct connection_closure *closure, *next;
    struct sudo_event *ev;
    struct timespec tv = { 0, 0 };
    debug_decl(server_shutdown, SUDO_DEBUG_UTIL);

    if (TAILQ_EMPTY(&connections)) {
	sudo_ev_loopbreak(base);
	debug_return;
    }

    TAILQ_FOREACH_SAFE(closure, &connections, entries, next) {
	closure->state = SHUTDOWN;
	sudo_ev_del(base, closure->read_ev);
	if (closure->relay_closure != NULL) {
	    /* Connection being relayed, check for pending I/O. */
	    relay_shutdown(closure);
	} else if (closure->log_io) {
	    /* Schedule final commit point for the connection. */
	    if (sudo_ev_add(base, closure->commit_ev, &tv, false) == -1) {
		sudo_warnx("%s", U_("unable to add event to queue"));
	    }
	} else {
	    /* No commit point, close connection immediately. */
	    connection_close(closure);
	}
    }

    if (!TAILQ_EMPTY(&connections)) {
	/* We need a timed event to exit even if clients time out. */
	ev = sudo_ev_alloc(-1, SUDO_EV_TIMEOUT, shutdown_cb, base);
	if (ev != NULL) {
	    tv.tv_sec = SHUTDOWN_TIMEO;
	    if (sudo_ev_add(base, ev, &tv, false) == -1) {
		sudo_warnx("%s", U_("unable to add event to queue"));
	    }
	}
    }

    debug_return;
}

/*
 * Send a server message to the client.
 */
static void
server_msg_cb(int fd, int what, void *v)
{
    struct connection_closure *closure = v;
    struct connection_buffer *buf;
    ssize_t nwritten;
    debug_decl(server_msg_cb, SUDO_DEBUG_UTIL);

    /* For TLS we may need to write as part of SSL_read(). */
    if (closure->read_instead_of_write) {
	closure->read_instead_of_write = false;
	/* Delete write event if it was only due to SSL_read(). */
	if (closure->temporary_write_event) {
	    closure->temporary_write_event = false;
	    sudo_ev_del(closure->evbase, closure->write_ev);
	}
	client_msg_cb(fd, what, v);
	debug_return;
    }

    if (what == SUDO_EV_TIMEOUT) {
	sudo_warnx(U_("timed out writing to client %s"), closure->ipaddr);
        goto finished;
    }

    if ((buf = TAILQ_FIRST(&closure->write_bufs)) == NULL) {
	sudo_warnx(U_("missing write buffer for client %s"), closure->ipaddr);
        goto finished;
    }

    sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending %u bytes to client (%s)",
	__func__, buf->len - buf->off, closure->ipaddr);

#if defined(HAVE_OPENSSL)
    if (closure->ssl != NULL) {
        nwritten = SSL_write(closure->ssl, buf->data + buf->off,
	    buf->len - buf->off);
        if (nwritten <= 0) {
	    const char *errstr;
            int err = SSL_get_error(closure->ssl, nwritten);
            switch (err) {
                case SSL_ERROR_WANT_READ:
		    /* ssl wants to read, read event always active */
		    sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
			"SSL_write returns SSL_ERROR_WANT_READ");
		    /* Redirect persistent read event to finish SSL_write() */
		    closure->write_instead_of_read = true;
                    debug_return;
                case SSL_ERROR_WANT_WRITE:
		    /* ssl wants to write more, write event remains active */
		    sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
			"SSL_write returns SSL_ERROR_WANT_WRITE");
                    debug_return;
		case SSL_ERROR_SYSCALL:
		    sudo_warn("%s: SSL_write", closure->ipaddr);
		    goto finished;
                default:
		    errstr = ERR_reason_error_string(ERR_get_error());
		    sudo_warnx("%s: SSL_write: %s", closure->ipaddr,
			errstr ? errstr : strerror(errno));
                    goto finished;
            }
        }
    } else
#endif
    {
	nwritten = write(fd, buf->data + buf->off, buf->len - buf->off);
    }

    if (nwritten == -1) {
	if (errno == EAGAIN || errno == EINTR)
	    debug_return;
	sudo_warn("%s: write", closure->ipaddr);
	goto finished;
    }
    buf->off += nwritten;

    if (buf->off == buf->len) {
	/* sent entire message, move buf to free list */
	sudo_debug_printf(SUDO_DEBUG_INFO,
	    "%s: finished sending %u bytes to client", __func__, buf->len);
	buf->off = 0;
	buf->len = 0;
	TAILQ_REMOVE(&closure->write_bufs, buf, entries);
	TAILQ_INSERT_TAIL(&closure->free_bufs, buf, entries);
	if (TAILQ_EMPTY(&closure->write_bufs)) {
	    /* Write queue empty, check state. */
	    sudo_ev_del(closure->evbase, closure->write_ev);
	    if (closure->error || closure->state == FINISHED ||
		    closure->state == SHUTDOWN)
		goto finished;
	}
    }
    debug_return;

finished:
    connection_close(closure);
    debug_return;
}

/*
 * Receive client message(s).
 */
static void
client_msg_cb(int fd, int what, void *v)
{
    struct connection_closure *closure = v;
    struct connection_buffer *buf = &closure->read_buf;
    const char *source = closure->journal_path ? closure->journal_path :
        closure->ipaddr;
    uint32_t msg_len;
    ssize_t nread;
    debug_decl(client_msg_cb, SUDO_DEBUG_UTIL);

    /* For TLS we may need to read as part of SSL_write(). */
    if (closure->write_instead_of_read) {
	closure->write_instead_of_read = false;
	server_msg_cb(fd, what, v);
	debug_return;
    }

    if (what == SUDO_EV_TIMEOUT) {
	sudo_warnx(U_("timed out reading from client %s"), closure->ipaddr);
        goto close_connection;
    }

#if defined(HAVE_OPENSSL)
    if (closure->ssl != NULL) {
       nread = SSL_read(closure->ssl, buf->data + buf->len, buf->size);
        if (nread <= 0) {
	    const char *errstr;
            int err = SSL_get_error(closure->ssl, nread);
            switch (err) {
		case SSL_ERROR_ZERO_RETURN:
		    /* ssl connection shutdown cleanly */
		    nread = 0;
		    break;
                case SSL_ERROR_WANT_READ:
		    /* ssl wants to read more, read event is always active */
		    sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
			"SSL_read returns SSL_ERROR_WANT_READ");
		    /* Read event is always active. */
                    debug_return;
                case SSL_ERROR_WANT_WRITE:
		    /* ssl wants to write, schedule a write if not pending */
		    sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
			"SSL_read returns SSL_ERROR_WANT_WRITE");
		    if (!sudo_ev_pending(closure->write_ev, SUDO_EV_WRITE, NULL)) {
			/* Enable a temporary write event. */
			if (sudo_ev_add(closure->evbase, closure->write_ev,
			    logsrvd_conf_server_timeout(), false) == -1) {
			    sudo_warnx("%s", U_("unable to add event to queue"));
			    closure->errstr = _("unable to allocate memory");
			    goto send_error;
			}
			closure->temporary_write_event = true;
		    }
		    /* Redirect write event to finish SSL_read() */
		    closure->read_instead_of_write = true;
                    debug_return;
		case SSL_ERROR_SYSCALL:
		    if (nread == 0) {
			/* EOF, handled below */
			sudo_warnx(U_("EOF from %s without proper TLS shutdown"),
			    closure->ipaddr);
			break;
		    }
		    sudo_warn("%s: SSL_read", closure->ipaddr);
                    goto close_connection;
                default:
		    errstr = ERR_reason_error_string(ERR_get_error());
		    sudo_warnx("%s: SSL_read: %s", closure->ipaddr,
			errstr ? errstr : strerror(errno));
		    goto close_connection;
            }
        }
    } else
#endif
    {
        nread = read(fd, buf->data + buf->len, buf->size - buf->len);
    }

    sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received %zd bytes from client %s",
	__func__, nread, closure->ipaddr);
    switch (nread) {
    case -1:
	if (errno == EAGAIN || errno == EINTR)
	    debug_return;
	sudo_warn("%s: read", closure->ipaddr);
	goto close_connection;
    case 0:
        if (closure->state != FINISHED) {
            sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
                "unexpected EOF");
        }
        goto close_connection;
    default:
	break;
    }
    buf->len += nread;

    while (buf->len - buf->off >= sizeof(msg_len)) {
	/* Read wire message size (uint32_t in network byte order). */
	memcpy(&msg_len, buf->data + buf->off, sizeof(msg_len));
	msg_len = ntohl(msg_len);

	if (msg_len > MESSAGE_SIZE_MAX) {
	    sudo_warnx(U_("client message too large: %zu"), (size_t)msg_len);
	    closure->errstr = _("client message too large");
	    goto send_error;
	}

	if (msg_len + sizeof(msg_len) > buf->len - buf->off) {
	    /* Incomplete message, we'll read the rest next time. */
	    if (!expand_buf(buf, msg_len + sizeof(msg_len))) {
		closure->errstr = _("unable to allocate memory");
		goto send_error;
	    }
	    debug_return;
	}

	/* Parse ClientMessage (could be zero bytes). */
	sudo_debug_printf(SUDO_DEBUG_INFO,
	    "%s: parsing ClientMessage, size %u", __func__, msg_len);
	buf->off += sizeof(msg_len);
	if (!handle_client_message(buf->data + buf->off, msg_len, closure)) {
	    sudo_warnx(U_("%s: %s"), source, U_("invalid ClientMessage"));
	    closure->errstr = _("invalid ClientMessage");
	    goto send_error;
	}
	buf->off += msg_len;
    }
    buf->len -= buf->off;
    buf->off = 0;

    if (closure->state == FINISHED)
	goto close_connection;

    debug_return;

send_error:
    /*
     * Try to send client an error message before closing the connection.
     */
    if (!schedule_error_message(closure->errstr, closure))
	goto close_connection;
    debug_return;

close_connection:
    connection_close(closure);
    debug_return;
}

/*
 * Format and schedule a commit_point message.
 */
bool
schedule_commit_point(TimeSpec *commit_point,
    struct connection_closure *closure)
{
    debug_decl(schedule_commit_point, SUDO_DEBUG_UTIL);

    if (closure->write_ev != NULL) {
	/* Send an acknowledgement of what we've committed to disk. */
	ServerMessage msg = SERVER_MESSAGE__INIT;
	msg.u.commit_point = commit_point;
	msg.type_case = SERVER_MESSAGE__TYPE_COMMIT_POINT;

	sudo_debug_printf(SUDO_DEBUG_INFO,
	    "%s: sending commit point [%lld, %ld]", __func__,
	    (long long)commit_point->tv_sec, (long)commit_point->tv_nsec);

	if (!fmt_server_message(closure, &msg))
	    goto bad;
	if (sudo_ev_add(closure->evbase, closure->write_ev,
	    logsrvd_conf_server_timeout(), false) == -1) {
	    sudo_warnx("%s", U_("unable to add event to queue"));
	    goto bad;
	}
    }

    if (closure->state == EXITED)
	closure->state = FINISHED;
    debug_return_bool(true);
bad:
    debug_return_bool(false);
}

/*
 * Time-based event that fires periodically to report to the client
 * what has been committed to disk.
 */
static void
server_commit_cb(int unused, int what, void *v)
{
    struct connection_closure *closure = v;
    TimeSpec commit_point = TIME_SPEC__INIT;
    debug_decl(server_commit_cb, SUDO_DEBUG_UTIL);

    /* Flush I/O logs before sending commit point if needed. */
    if (!iolog_get_flush())
	iolog_flush_all(closure);

    commit_point.tv_sec = closure->elapsed_time.tv_sec;
    commit_point.tv_nsec = closure->elapsed_time.tv_nsec;
    if (!schedule_commit_point(&commit_point, closure))
	connection_close(closure);

    debug_return;
}

/*
 * Begin the sudo logserver protocol.
 * When we enter the event loop the ServerHello message will be written
 * and any pending ClientMessage will be read.
 */
bool
start_protocol(struct connection_closure *closure)
{
    const struct timespec *timeout = logsrvd_conf_server_timeout();
    debug_decl(start_protocol, SUDO_DEBUG_UTIL);

    if (closure->relay_closure != NULL && closure->relay_closure->relays != NULL) {
	/* No longer need the stashed relays list. */
	address_list_delref(closure->relay_closure->relays);
	closure->relay_closure->relays = NULL;
	closure->relay_closure->relay_addr = NULL;
    }

    /* When replaying a journal there is no write event. */
    if (closure->write_ev != NULL) {
	if (!fmt_hello_message(closure))
	    debug_return_bool(false);

	if (sudo_ev_add(closure->evbase, closure->write_ev, timeout, false) == -1)
	    debug_return_bool(false);
    }

    /* No read timeout, client messages may happen at arbitrary times. */
    if (sudo_ev_add(closure->evbase, closure->read_ev, NULL, false) == -1)
	debug_return_bool(false);

    debug_return_bool(true);
}

#if defined(HAVE_OPENSSL)
static int
verify_peer_identity(int preverify_ok, X509_STORE_CTX *ctx)
{
    HostnameValidationResult result;
    struct connection_closure *closure;
    SSL *ssl;
    X509 *current_cert;
    X509 *peer_cert;
    debug_decl(verify_peer_identity, SUDO_DEBUG_UTIL);

    /* if pre-verification of the cert failed, just propagate that result back */
    if (preverify_ok != 1) {
        debug_return_int(0);
    }

    /* since this callback is called for each cert in the chain,
     * check that current cert is the peer's certificate
     */
    current_cert = X509_STORE_CTX_get_current_cert(ctx);
    peer_cert = X509_STORE_CTX_get0_cert(ctx);

    if (current_cert != peer_cert) {
        debug_return_int(1);
    }

    /* read out the attached object (closure) from the ssl connection object */
    ssl = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx());
    closure = (struct connection_closure *)SSL_get_ex_data(ssl, 1);

    result = validate_hostname(peer_cert, closure->ipaddr, closure->ipaddr, 1);

    switch(result)
    {
        case MatchFound:
            debug_return_int(1);
        default:
            sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
                "hostname validation failed");
            debug_return_int(0);
    }
}

/*
 * Set the TLS verify callback to verify_peer_identity().
 */
static void
set_tls_verify_peer(void)
{
    SSL_CTX *server_ctx = logsrvd_server_tls_ctx();
    SSL_CTX *relay_ctx = logsrvd_relay_tls_ctx();
    debug_decl(set_tls_verify_peer, SUDO_DEBUG_UTIL);

    if (server_ctx != NULL && logsrvd_conf_server_tls_check_peer()) {
	/* Verify server cert during the handshake. */
	SSL_CTX_set_verify(server_ctx,
	    SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
	    verify_peer_identity);
    }
    if (relay_ctx != NULL && logsrvd_conf_relay_tls_check_peer()) {
	/* Verify relay cert during the handshake. */
	SSL_CTX_set_verify(relay_ctx,
	    SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
	    verify_peer_identity);
    }

    debug_return;
}

static void
tls_handshake_cb(int fd, int what, void *v)
{
    struct connection_closure *closure = v;
    const char *errstr;
    int err, handshake_status;
    debug_decl(tls_handshake_cb, SUDO_DEBUG_UTIL);

    if (what == SUDO_EV_TIMEOUT) {
	sudo_warnx("TLS handshake with %s timed out", closure->ipaddr);
        goto bad;
    }

    handshake_status = SSL_accept(closure->ssl);
    err = SSL_get_error(closure->ssl, handshake_status);
    switch (err) {
        case SSL_ERROR_NONE:
	    /* ssl handshake was successful */
	    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		"TLS handshake successful");
            break;
        case SSL_ERROR_WANT_READ:
	    /* ssl handshake is ongoing, re-schedule the SSL_accept() call */
	    sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
		"SSL_accept returns SSL_ERROR_WANT_READ");
	    if (what != SUDO_EV_READ) {
		if (sudo_ev_set(closure->ssl_accept_ev, closure->sock,
			SUDO_EV_READ, tls_handshake_cb, closure) == -1) {
		    sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
			"unable to set ssl_accept_ev to SUDO_EV_READ");
		    goto bad;
		}
	    }
            if (sudo_ev_add(closure->evbase, closure->ssl_accept_ev,
                logsrvd_conf_server_timeout(), false) == -1) {
		sudo_warnx("%s", U_("unable to add event to queue"));
                goto bad;
            }
            debug_return;
        case SSL_ERROR_WANT_WRITE:
	    /* ssl handshake is ongoing, re-schedule the SSL_accept() call */
	    sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
		"SSL_accept returns SSL_ERROR_WANT_WRITE");
	    if (what != SUDO_EV_WRITE) {
		if (sudo_ev_set(closure->ssl_accept_ev, closure->sock,
			SUDO_EV_WRITE, tls_handshake_cb, closure) == -1) {
		    sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
			"unable to set ssl_accept_ev to SUDO_EV_WRITE");
		    goto bad;
		}
	    }
            if (sudo_ev_add(closure->evbase, closure->ssl_accept_ev,
		    logsrvd_conf_server_timeout(), false) == -1) {
		sudo_warnx("%s", U_("unable to add event to queue"));
                goto bad;
            }
            debug_return;
	case SSL_ERROR_SYSCALL:
	    sudo_warn("%s: SSL_accept", closure->ipaddr);
            goto bad;
        default:
	    errstr = ERR_reason_error_string(ERR_get_error());
	    sudo_warnx("%s: SSL_accept: %s", closure->ipaddr,
		errstr ? errstr : strerror(errno));
            goto bad;
    }

    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
        "TLS version: %s, negotiated cipher suite: %s",
        SSL_get_version(closure->ssl),
        SSL_get_cipher(closure->ssl));

    /* Start the actual protocol now that the TLS handshake is complete. */
    if (!TAILQ_EMPTY(logsrvd_conf_relay_address()) && !closure->store_first) {
	if (!connect_relay(closure))
	    goto bad;
    } else {
	if (!start_protocol(closure))
	    goto bad;
    }

    debug_return;
bad:
    connection_close(closure);
    debug_return;
}
#endif /* HAVE_OPENSSL */

/*
 * New connection.
 * Allocate a connection closure and optionally perform TLS handshake.
 */
static bool
new_connection(int sock, bool tls, const union sockaddr_union *sa_un,
    struct sudo_event_base *evbase)
{
    struct connection_closure *closure;
    debug_decl(new_connection, SUDO_DEBUG_UTIL);

    if ((closure = connection_closure_alloc(sock, tls, false, evbase)) == NULL)
	goto bad;

    /* store the peer's IP address in the closure object */
    if (sa_un->sa.sa_family == AF_INET) {
        inet_ntop(AF_INET, &sa_un->sin.sin_addr, closure->ipaddr,
            sizeof(closure->ipaddr));
#ifdef HAVE_STRUCT_IN6_ADDR
    } else if (sa_un->sa.sa_family == AF_INET6) {
        inet_ntop(AF_INET6, &sa_un->sin6.sin6_addr, closure->ipaddr,
            sizeof(closure->ipaddr));
#endif /* HAVE_STRUCT_IN6_ADDR */
    } else {
	errno = EAFNOSUPPORT;
        sudo_warn("%s", U_("unable to get remote IP addr"));
        goto bad;
    }
    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
	"connection from %s", closure->ipaddr);

#if defined(HAVE_OPENSSL)
    /* If TLS is enabled, perform the TLS handshake first. */
    if (tls) {
	const char *errstr;

        /* Create the SSL object for the closure and attach it to the socket */
        if ((closure->ssl = SSL_new(logsrvd_server_tls_ctx())) == NULL) {
	    errstr = ERR_reason_error_string(ERR_get_error());
	    sudo_warnx(U_("%s: %s"), "SSL_new",
		errstr ? errstr : strerror(errno));
            goto bad;
        }

        if (SSL_set_fd(closure->ssl, closure->sock) != 1) {
	    errstr = ERR_reason_error_string(ERR_get_error());
	    sudo_warnx(U_("%s: %s"), "SSL_set_fd",
		errstr ? errstr : strerror(errno));
            goto bad;
        }

        /* attach the closure object to the ssl connection object to make it
        available during hostname matching
        */
        if (SSL_set_ex_data(closure->ssl, 1, closure) <= 0) {
	    errstr = ERR_reason_error_string(ERR_get_error());
            sudo_warnx(U_("Unable to attach user data to the ssl object: %s"),
		errstr ? errstr : strerror(errno));
            goto bad;
        }

        /* Enable SSL_accept to begin handshake with client. */
        if (sudo_ev_add(evbase, closure->ssl_accept_ev,
		logsrvd_conf_server_timeout(), false) == -1) {
	    sudo_warnx("%s", U_("unable to add event to queue"));
            goto bad;
        }
    }
#endif
    /* If no TLS handshake, start the protocol immediately. */
    if (!tls) {
	if (!TAILQ_EMPTY(logsrvd_conf_relay_address()) && !closure->store_first) {
	    if (!connect_relay(closure))
		goto bad;
	} else {
	    if (!start_protocol(closure))
		goto bad;
	}
    }

    debug_return_bool(true);
bad:
    connection_close(closure);
    debug_return_bool(false);
}

static int
create_listener(struct server_address *addr)
{
    int flags, on, sock;
    const char *family = "inet4";
    debug_decl(create_listener, SUDO_DEBUG_UTIL);

    if ((sock = socket(addr->sa_un.sa.sa_family, SOCK_STREAM, 0)) == -1) {
	sudo_warn("socket");
	goto bad;
    }
    on = 1;
#ifdef HAVE_STRUCT_IN6_ADDR
    if (addr->sa_un.sa.sa_family == AF_INET6) {
	family = "inet6";
# ifdef IPV6_V6ONLY
	/* Disable IPv4-mapped IPv6 addresses. */
	if (setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) == -1)
	    sudo_warn("IPV6_V6ONLY");
# endif
    }
#endif
    if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1)
	sudo_warn("SO_REUSEADDR");
    if (bind(sock, &addr->sa_un.sa, addr->sa_size) == -1) {
	/* TODO: only warn once for IPv4 and IPv6 or disambiguate */
	sudo_warn("%s (%s)", addr->sa_str, family);
	goto bad;
    }
    if (listen(sock, SOMAXCONN) == -1) {
	sudo_warn("listen");
	goto bad;
    }
    flags = fcntl(sock, F_GETFL, 0);
    if (flags == -1 || fcntl(sock, F_SETFL, flags | O_NONBLOCK) == -1) {
	sudo_warn("fcntl(O_NONBLOCK)");
	goto bad;
    }
    sudo_debug_printf(SUDO_DEBUG_INFO, "listening on %s (%s)", addr->sa_str,
	family);

    debug_return_int(sock);
bad:
    if (sock != -1)
	close(sock);
    debug_return_int(-1);
}

static void
listener_cb(int fd, int what, void *v)
{
    struct listener *l = v;
    struct sudo_event_base *evbase = sudo_ev_get_base(l->ev);
    union sockaddr_union sa_un;
    socklen_t salen = sizeof(sa_un);
    int sock;
    debug_decl(listener_cb, SUDO_DEBUG_UTIL);

    memset(&sa_un, 0, sizeof(sa_un));
    sock = accept(fd, &sa_un.sa, &salen);
    if (sock != -1) {
	if (logsrvd_conf_server_tcp_keepalive()) {
	    int keepalive = 1;
	    if (setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &keepalive,
		    sizeof(keepalive)) == -1) {
		sudo_warn("SO_KEEPALIVE");
	    }
	}
	if (!new_connection(sock, l->tls, &sa_un, evbase)) {
	    /* TODO: pause accepting on ENOMEM */
	    sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
		"unable to start new connection");
	}
    } else {
	if (errno == EAGAIN || errno == EINTR)
	    debug_return;
	/* TODO: pause accepting on ENFILE and EMFILE */
	sudo_warn("accept");
    }

    debug_return;
}

static bool
register_listener(struct server_address *addr, struct sudo_event_base *evbase)
{
    struct listener *l;
    int sock;
    debug_decl(register_listener, SUDO_DEBUG_UTIL);

    sock = create_listener(addr);
    if (sock == -1)
	debug_return_bool(false);

    /* TODO: make non-fatal */
    if ((l = malloc(sizeof(*l))) == NULL)
	sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
    l->sock = sock;
    l->tls = addr->tls;
    l->ev = sudo_ev_alloc(sock, SUDO_EV_READ|SUDO_EV_PERSIST, listener_cb, l);
    if (l->ev == NULL)
	sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
    if (sudo_ev_add(evbase, l->ev, NULL, false) == -1)
	sudo_fatal("%s", U_("unable to add event to queue"));
    TAILQ_INSERT_TAIL(&listeners, l, entries);

    debug_return_bool(true);
}

/*
 * Register listeners and set the TLS verify callback.
 */
static bool
server_setup(struct sudo_event_base *base)
{
    struct server_address *addr;
    struct listener *l;
    int nlisteners = 0;
    bool ret;
    debug_decl(server_setup, SUDO_DEBUG_UTIL);

    /* Free old listeners (if any) and register new ones. */
    while ((l = TAILQ_FIRST(&listeners)) != NULL) {
	TAILQ_REMOVE(&listeners, l, entries);
	sudo_ev_free(l->ev);
	close(l->sock);
	free(l);
    }
    TAILQ_FOREACH(addr, logsrvd_conf_server_listen_address(), entries) {
	nlisteners += register_listener(addr, base);
    }
    ret = nlisteners > 0;

#if defined(HAVE_OPENSSL)
    if (ret)
	set_tls_verify_peer();
#endif

    debug_return_bool(ret);
}

/*
 * Reload config and re-initialize listeners.
 */
static void
server_reload(struct sudo_event_base *evbase)
{
    debug_decl(server_reload, SUDO_DEBUG_UTIL);

    sudo_debug_printf(SUDO_DEBUG_INFO, "reloading server config");
    if (logsrvd_conf_read(conf_file)) {
	/* Re-initialize listeners. */
	if (!server_setup(evbase))
	    sudo_fatalx("%s", U_("unable to setup listen socket"));

	/* Re-read sudo.conf and re-initialize debugging. */
	sudo_debug_deregister(logsrvd_debug_instance);
	logsrvd_debug_instance = SUDO_DEBUG_INSTANCE_INITIALIZER;
	if (sudo_conf_read(NULL, SUDO_CONF_DEBUG) != -1) {
	    logsrvd_debug_instance = sudo_debug_register(getprogname(),
		NULL, NULL, sudo_conf_debug_files(getprogname()), -1);
	}
    }

    debug_return;
}

/*
 * Dump server information to the debug file.
 * Includes information about listeners and client connections.
 */
static void
server_dump_stats(void)
{
    struct server_address *addr;
    struct connection_closure *closure;
    int n;
    debug_decl(server_dump_stats, SUDO_DEBUG_UTIL);

    sudo_debug_printf(SUDO_DEBUG_INFO, "%s", server_id);
    sudo_debug_printf(SUDO_DEBUG_INFO, "configuration file: %s",
	conf_file ? conf_file : _PATH_SUDO_LOGSRVD_CONF);

    sudo_debug_printf(SUDO_DEBUG_INFO, "listen addresses:");
    n = 0;
    TAILQ_FOREACH(addr, logsrvd_conf_server_listen_address(), entries) {
	union sockaddr_union *sa_un = &addr->sa_un;
	char ipaddr[INET6_ADDRSTRLEN];

	switch (sa_un->sa.sa_family) {
	case AF_INET:
	    inet_ntop(AF_INET, &sa_un->sin.sin_addr, ipaddr, sizeof(ipaddr));
	    break;
#ifdef HAVE_STRUCT_IN6_ADDR
	case AF_INET6:
	    inet_ntop(AF_INET6, &sa_un->sin6.sin6_addr, ipaddr, sizeof(ipaddr));
	    break;
#endif /* HAVE_STRUCT_IN6_ADDR */
	default:
	    (void)strlcpy(ipaddr, "[unknown]", sizeof(ipaddr));
	    break;
	}
	sudo_debug_printf(SUDO_DEBUG_INFO, "  %d: %s [%s]", ++n,
	    addr->sa_str, ipaddr);
    }

    if (!TAILQ_EMPTY(&connections)) {
	n = 0;
	sudo_debug_printf(SUDO_DEBUG_INFO, "client connections:");
	TAILQ_FOREACH(closure, &connections, entries) {
	    struct relay_closure *relay_closure = closure->relay_closure;

	    n++;
	    if (closure->sock == -1) {
		sudo_debug_printf(SUDO_DEBUG_INFO, "  %2d: journal %s", n,
		    closure->journal_path ? closure->journal_path : "none");
		sudo_debug_printf(SUDO_DEBUG_INFO, "  %2d: fd %d", n,
		    closure->journal ? fileno(closure->journal) : -1);
	    } else {
		sudo_debug_printf(SUDO_DEBUG_INFO, "  %2d: addr %s%s", n,
		    closure->ipaddr, closure->tls ? " (TLS)" : "");
		sudo_debug_printf(SUDO_DEBUG_INFO, "  %2d: sock %d", n,
		    closure->sock);
	    }
	    if (relay_closure != NULL) {
		sudo_debug_printf(SUDO_DEBUG_INFO, "      relay: %s (%s)",
		    relay_closure->relay_name.name,
		    relay_closure->relay_name.ipaddr);
		sudo_debug_printf(SUDO_DEBUG_INFO, "      relay sock: %d",
		    relay_closure->sock);
	    }
	    sudo_debug_printf(SUDO_DEBUG_INFO, "      state: %d", closure->state);
	    if (closure->errstr != NULL) {
		sudo_debug_printf(SUDO_DEBUG_INFO, "      error: %s",
		    closure->errstr);
	    }
	    sudo_debug_printf(SUDO_DEBUG_INFO, "      log I/O: %s",
		closure->log_io ? "true" : "false");
	    sudo_debug_printf(SUDO_DEBUG_INFO, "      store first: %s",
		closure->store_first ? "true" : "false");
	    if (sudo_timespecisset(&closure->elapsed_time)) {
		sudo_debug_printf(SUDO_DEBUG_INFO,
		    "      elapsed time: [%lld, %ld]",
		    (long long)closure->elapsed_time.tv_sec,
		    (long)closure->elapsed_time.tv_nsec);
	    }
	}
	sudo_debug_printf(SUDO_DEBUG_INFO, "%d client connection(s)\n", n);
    }
    logsrvd_queue_dump();

    debug_return;
}

static void
signal_cb(int signo, int what, void *v)
{
    struct sudo_event_base *base = v;
    debug_decl(signal_cb, SUDO_DEBUG_UTIL);

    switch (signo) {
	case SIGHUP:
	    server_reload(base);
	    break;
	case SIGINT:
	case SIGTERM:
	    /* Shut down active connections. */
	    server_shutdown(base);
	    break;
	case SIGUSR1:
	    server_dump_stats();
	    break;
	default:
	    sudo_warnx(U_("unexpected signal %d"), signo);
	    break;
    }

    debug_return;
}

static void
register_signal(int signo, struct sudo_event_base *base)
{
    struct sudo_event *ev;
    debug_decl(register_signal, SUDO_DEBUG_UTIL);

    ev = sudo_ev_alloc(signo, SUDO_EV_SIGNAL, signal_cb, base);
    if (ev == NULL)
	sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
    if (sudo_ev_add(base, ev, NULL, false) == -1)
	sudo_fatal("%s", U_("unable to add event to queue"));

    debug_return;
}

static void
logsrvd_cleanup(void)
{
    /* TODO: cleanup like on signal */
    return;
}

/*
 * Write the process ID into a file, typically /var/run/sudo/sudo_logsrvd.pid.
 * If the parent directory doesn't exist, it will be created.
 */
static void
write_pidfile(void)
{
    FILE *fp;
    int dfd, fd;
    mode_t oldmask;
    const char *pid_file = logsrvd_conf_pid_file();
    debug_decl(write_pidfile, SUDO_DEBUG_UTIL);

    if (pid_file == NULL)
	debug_return;

    /* Default logsrvd umask is more restrictive (077). */
    oldmask = umask(S_IWGRP|S_IWOTH);

    dfd = sudo_open_parent_dir(pid_file, ROOT_UID, ROOT_GID,
	S_IRWXU|S_IXGRP|S_IXOTH, false);
    if (dfd != -1) {
	const char *base = sudo_basename(pid_file);
	fd = openat(dfd, base, O_WRONLY|O_CREAT|O_NOFOLLOW, 0644);
	if (fd == -1 || (fp = fdopen(fd, "w")) == NULL) {
	    sudo_warn("%s", pid_file);
	    if (fd != -1)
		close(fd);
	} else {
	    fprintf(fp, "%u\n", (unsigned int)getpid());
	    fflush(fp);
	    if (ferror(fp))
		sudo_warn("%s", pid_file);
	    fclose(fp);
	}
	close(dfd);
    }
    umask(oldmask);

    debug_return;
}

/*
 * Fork, detach from the terminal and write pid file unless nofork set.
 */
static void
daemonize(bool nofork)
{
    int fd;
    debug_decl(daemonize, SUDO_DEBUG_UTIL);

    if (chdir("/") == -1)
	sudo_warn("chdir(\"/\")");

    if (!nofork) {
	switch (sudo_debug_fork()) {
	case -1:
	    sudo_fatal("fork");
	case 0:
	    /* child */
	    break;
	default:
	    /* parent, exit */
	    _exit(EXIT_SUCCESS);
	}

	/* detach from terminal and write pid file. */
	if (setsid() == -1)
	    sudo_fatal("setsid");
	write_pidfile();

	if ((fd = open(_PATH_DEVNULL, O_RDWR)) != -1) {
	    (void) dup2(fd, STDIN_FILENO);
	    (void) dup2(fd, STDOUT_FILENO);
	    (void) dup2(fd, STDERR_FILENO);
	    if (fd > STDERR_FILENO)
		(void) close(fd);
	}
    } else {
	if ((fd = open(_PATH_DEVNULL, O_RDWR)) != -1) {
	    /* Preserve stdout/stderr in nofork mode (if open). */
	    (void) dup2(fd, STDIN_FILENO);
	    if (fcntl(STDOUT_FILENO, F_GETFL) == -1)
		(void) dup2(fd, STDOUT_FILENO);
	    if (fcntl(STDERR_FILENO, F_GETFL) == -1)
		(void) dup2(fd, STDERR_FILENO);
	    if (fd > STDERR_FILENO)
		(void) close(fd);
	}
    }

    /* Disable logging to stderr after we become a daemon. */
    logsrvd_warn_stderr(false);

    debug_return;
}

static void
usage(bool fatal)
{
    fprintf(stderr, "usage: %s [-n] [-f conf_file] [-R percentage]\n",
	getprogname());
    if (fatal)
	exit(EXIT_FAILURE);
}

static void
help(void)
{
    printf("%s - %s\n\n", getprogname(), _("sudo log server"));
    usage(false);
    printf("\n%s\n", _("Options:"));
    printf("  -f, --file            %s\n",
	_("path to configuration file"));
    printf("  -h, --help            %s\n",
        _("display help message and exit"));
    printf("  -n, --no-fork         %s\n",
	_("do not fork, run in the foreground"));
    printf("  -R, --random-drop     %s\n",
	_("percent chance connections will drop"));
    printf("  -V, --version         %s\n",
	_("display version information and exit"));
    putchar('\n');
    exit(EXIT_SUCCESS);
}

static const char short_opts[] = "f:hnR:V";
static struct option long_opts[] = {
    { "file",		required_argument,	NULL,	'f' },
    { "help",		no_argument,		NULL,	'h' },
    { "no-fork",	no_argument,		NULL,	'n' },
    { "random-drop",	required_argument,	NULL,	'R' },
    { "version",	no_argument,		NULL,	'V' },
    { NULL,		no_argument,		NULL,	0 },
};

sudo_dso_public int main(int argc, char *argv[]);

int
main(int argc, char *argv[])
{
    struct sudo_event_base *evbase;
    bool nofork = false;
    int ch;
    debug_decl_vars(main, SUDO_DEBUG_MAIN);

#if defined(SUDO_DEVEL) && defined(__OpenBSD__)
    {
	extern char *malloc_options;
	malloc_options = "S";
    }
#endif

    initprogname(argc > 0 ? argv[0] : "sudo_logsrvd");
    setlocale(LC_ALL, "");
    bindtextdomain("sudo", LOCALEDIR); /* XXX - add logsrvd domain */
    textdomain("sudo");

    /* Create files readable/writable only by owner. */
    umask(S_IRWXG|S_IRWXO);

    /* Register fatal/fatalx callback. */
    sudo_fatal_callback_register(logsrvd_cleanup);

    /* Read sudo.conf and initialize the debug subsystem. */
    if (sudo_conf_read(NULL, SUDO_CONF_DEBUG) == -1)
        exit(EXIT_FAILURE);
    logsrvd_debug_instance = sudo_debug_register(getprogname(), NULL, NULL,
        sudo_conf_debug_files(getprogname()), -1);

    if (protobuf_c_version_number() < 1003000)
	sudo_fatalx("%s", U_("Protobuf-C version 1.3 or higher required"));

    while ((ch = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) {
	switch (ch) {
	case 'f':
	    conf_file = optarg;
	    break;
	case 'h':
	    help();
	    break;
	case 'n':
	    nofork = true;
	    break;
	case 'R':
	    /* random connection drop probability as a percentage (debug) */
	    if (!set_random_drop(optarg))
                sudo_fatalx(U_("invalid random drop value: %s"), optarg);
	    break;
	case 'V':
	    (void)printf(_("%s version %s\n"), getprogname(),
		PACKAGE_VERSION);
	    return 0;
	default:
	    usage(true);
	}
    }

    /* Read sudo_logsrvd.conf */
    if (!logsrvd_conf_read(conf_file))
        exit(EXIT_FAILURE);

    if ((evbase = sudo_ev_base_alloc()) == NULL)
	sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));

    /* Initialize listeners. */
    if (!server_setup(evbase))
	sudo_fatalx("%s", U_("unable to setup listen socket"));

    register_signal(SIGHUP, evbase);
    register_signal(SIGINT, evbase);
    register_signal(SIGTERM, evbase);
    register_signal(SIGUSR1, evbase);

    /* Point of no return. */
    daemonize(nofork);
    signal(SIGPIPE, SIG_IGN);

    logsrvd_queue_scan(evbase);
    sudo_ev_dispatch(evbase);
    if (!nofork && logsrvd_conf_pid_file() != NULL)
	unlink(logsrvd_conf_pid_file());
    logsrvd_conf_cleanup();

    debug_return_int(1);
}
