/* Pluto Asynchronous DNS Helper Program -- for internal use only!
 * Copyright (C) 2002  D. Hugh Redelmeier.
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * RCSID $Id: adns.c,v 1.5 2002/03/23 03:33:19 dhr Exp $
 */

/* This program executes as multiple processes.  The Master process
 * receives queries (struct adns_query messages) from Pluto and distributes
 * them amongst Worker processes.  These Worker processes are created
 * by the Master whenever a query arrives and no existing Worker is free.
 * At most MAX_WORKERS will be created; after that, the Master will queue
 * queries until a Worker becomes free.  When a Worker has an answer from
 * the resolver, it sends the answer as a struct adns_answer message to the
 * Master.  The Master then forwards the answer to Pluto, noting that
 * the Worker is free to accept another query.
 *
 * The protocol is simple: Pluto sends a sequence of queries and receives
 * a sequence of answers.  select(2) is used by Pluto and by the Master
 * process to decide when to read, but writes are done without checking
 * for readiness.  Communications is via pipes.  Since only one process
 * can write to each pipe, messages will not be interleaved.  Fixed length
 * records are used for simplicity.
 *
 * Pluto needs a way to indicate to the Master when to shut down
 * and the Master needs to indicate this to each worker.  EOF on the pipe
 * signifies this.
 *
 * The interfaces between these components are considered private to
 * Pluto.  This allows us to get away with less checking.  This is a
 * reason to use pipes instead of TCP/IP.
 *
 * Although the code uses plain old UNIX processes, it could be modified
 * to use threads.  That might reduce resource requirements.  It would
 * preclude running on systems without thread-safe resolvers.
 */

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <syslog.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/nameser.h>
#include <resolv.h>
#include <netdb.h>	/* ??? for h_errno */

#include <freeswan.h>

/* GCC magic! */
#ifdef GCC_LINT
# define UNUSED __attribute__ ((unused))
#else
# define UNUSED /* ignore */
#endif

#include "adns.h"	/* needs <resolv.h> */

typedef int bool;
#define FALSE	0
#define TRUE	1

#define NULL_FD (-1)

/* shared by all processes */

static const char *name;	/* program name, for messages */

static bool debug = FALSE;

/* Read a variable-length record from a pipe (and no more!).
 * First bytes must be a size_t containing the length.
 * HES_CONTINUE if record read
 * HES_OK if EOF
 * HES_IO_ERROR_IN if errno tells the tale.
 * Others are errors.
 */
static enum helper_exit_status
read_pipe(int fd, unsigned char *stuff, size_t minlen, size_t maxlen)
{
    size_t n = 0;
    size_t goal = minlen;

    do {
	ssize_t m = read(fd, stuff + n, goal - n);

	if (m == -1)
	{
	    if (errno != EINTR)
	    {
		syslog(LOG_ERR, "Input error on pipe: %s", strerror(errno));
		return HES_IO_ERROR_IN;
	    }
	}
	else if (m == 0)
	{
	    return HES_OK;	/* treat empty message as EOF */
	}
	else
	{
	    n += m;
	    if (n >= sizeof(size_t))
	    {
		goal = *(size_t *)(void *)stuff;
		if (goal < minlen || maxlen < goal)
		{
		    if (debug)
			fprintf(stderr, "%lu : [%lu, %lu]\n"
			    , (unsigned long)goal
			    , (unsigned long)minlen, (unsigned long)maxlen);
		    return HES_BAD_LEN;
		}
	    }
	}
    } while (n < goal);

    return HES_CONTINUE;
}

/* Write a variable-length record to a pipe.
 * First bytes must be a size_t containing the length.
 * HES_CONTINUE if record written
 * Others are errors.
 */
static enum helper_exit_status
write_pipe(int fd, const unsigned char *stuff)
{
    size_t len = *(const size_t *)(const void *)stuff;
    size_t n = 0;

    do {
	ssize_t m = write(fd, stuff + n, len - n);

	if (m == -1)
	{
	    /* error, but ignore and retry if EINTR */
	    if (errno != EINTR)
	    {
		syslog(LOG_ERR, "Output error from master: %s", strerror(errno));
		return HES_IO_ERROR_OUT;
	    }
	}
	else
	{
	    n += m;
	}
    } while (n != len);
    return HES_CONTINUE;
}

/**************** worker process ****************/

/* The interface in RHL6.x and BIND distribution 8.2.2 are different,
 * so we build some of our own :-(
 */

/* Support deprecated interface to allow for older releases of the resolver.
 * Fake new interface!
 * See resolver(3) bind distribution (should be in RHL6.1, but isn't).
 * __RES was 19960801 in RHL6.2, an old resolver.
 */

#if (__RES) <= 19960801
# define OLD_RESOLVER	1
#endif

#ifdef OLD_RESOLVER

# define res_ninit(statp) res_init()
# define res_nquery(statp, dname, class, type, answer, anslen) \
    res_query(dname, class, type, answer, anslen)
# define res_nclose(statp) res_close()

static struct __res_state *statp = &_res;

#else /* !OLD_RESOLVER */

static struct __res_state my_res_state /* = { 0 } */;
static res_state statp = &my_res_state;

#endif /* !OLD_RESOLVER */

static int
worker(int qfd, int afd)
{
    {
	int r = res_ninit(statp);

	if (r != 0)
	{
	    syslog(LOG_ERR, "cannot initialize resolver");
	    return HES_RES_INIT;
	}
#ifndef OLD_RESOLVER
	statp->options |= RES_ROTATE;
#endif
	statp->options |= RES_DEBUG;
    }

    for (;;)
    {
	struct adns_query q;
	struct adns_answer a;

	enum helper_exit_status r = read_pipe(qfd, (unsigned char *)&q
	    , sizeof(q), sizeof(q));

	if (r != HES_CONTINUE)
	    return r;	/* some kind of exit */

	if (q.qmagic != ADNS_Q_MAGIC)
	{
	    syslog(LOG_ERR, "error in input from master: bad magic");
	    return HES_BAD_MAGIC;
	}

	a.amagic = ADNS_A_MAGIC;
	a.continuation = q.continuation;

	a.result = res_nquery(statp, q.name_buf, C_IN, q.type, a.ans, sizeof(a.ans));
	a.h_errno_val = h_errno;

	a.len = offsetof(struct adns_answer, ans) + (a.result < 0? 0 : a.result);

	/* write answer, possibly a bit at a time */
	r = write_pipe(afd, (const unsigned char *)&a);

	if (r != HES_CONTINUE)
	    return r;	/* some kind of exit */
    }
}

/**************** master process ****************/

bool eof_from_pluto = FALSE;
static int pluto_qfd = NULL_FD;	/* fd for queries from Pluto */
static int pluto_afd = NULL_FD;	/* fd for answers for Pluto */

#ifndef MAX_WORKERS
# define MAX_WORKERS 10	/* number of in-flight queries */
#endif

struct worker_info {
    int qfd;	/* query pipe's file descriptor */
    int afd;	/* answer pipe's file descriptor */
    pid_t pid;
    bool busy;
    void *continuation;	/* of outstanding request */
};

static struct worker_info wi[MAX_WORKERS];
static struct worker_info *wi_roof = wi;

/* request FIFO */

struct query_list {
    struct query_list *next;
    struct adns_query aq;
};

static struct query_list *oldest_query = NULL;
static struct query_list *newest_query;	/* undefined when oldest == NULL */
static struct query_list *free_queries = NULL;

static bool
spawn_worker(void)
{
    int qfds[2];
    int afds[2];
    pid_t p;

    if (pipe(qfds) != 0 || pipe(afds) != 0)
    {
	syslog(LOG_ERR, "pipe(2) failed: %s", strerror(errno));
	exit(HES_PIPE);
    }

    wi_roof->qfd = qfds[1];	/* write end of query pipe */
    wi_roof->afd = afds[0];	/* read end of answer pipe */

    p = fork();
    if (p == -1)
    {
	/* fork failed: ignore if at least one worker exists */
	if (wi_roof == wi)
	{
	    syslog(LOG_ERR, "for(2) error creating first worker: %s", strerror(errno));
	    exit(HES_FORK);
	}
	close(qfds[0]);
	close(qfds[1]);
	close(afds[0]);
	close(afds[1]);
	return FALSE;
    }
    else if (p == 0)
    {
	/* child */
	struct worker_info *w;

	close(pluto_qfd);
	close(pluto_afd);
	/* close all master pipes, including ours */
	for (w = wi; w <= wi_roof; w++)
	{
	    close(w->qfd);
	    close(w->afd);
	}
	exit(worker(qfds[0], afds[1]));
    }
    else
    {
	/* parent */
	struct worker_info *w = wi_roof++;

	w->pid = p;
	w->busy = FALSE;
	close(qfds[0]);
	close(afds[1]);
	return TRUE;
    }
}

static void
send_eof(struct worker_info *w)
{
    pid_t p;
    int status;

    close(w->qfd);
    w->qfd = NULL_FD;

    close(w->afd);
    w->afd = NULL_FD;

    /* reap child */
    p = waitpid(w->pid, &status, 0);
    /* ignore result -- what could we do with it? */
}

static void
forward_query(struct worker_info *w)
{
    struct query_list *q = oldest_query;

    if (q == NULL)
    {
	if (eof_from_pluto)
	    send_eof(w);
    }
    else
    {
	enum helper_exit_status r
	    = write_pipe(w->qfd, (const unsigned char *) &q->aq);

	if (r != HES_CONTINUE)
	    exit(r);

	w->continuation = q->aq.continuation;
	w->busy = TRUE;

	oldest_query = q->next;
	q->next = free_queries;
	free_queries = q;
    }
}

static void
query(void)
{
    struct query_list *q = free_queries;
    enum helper_exit_status r;

    /* find an unused queue entry */
    if (q == NULL)
    {
	q = malloc(sizeof(*q));
	if (q == NULL)
	{
	    syslog(LOG_ERR, "malloc(3) failed");
	    exit(HES_MALLOC);
	}
    }
    else
    {
	free_queries = q->next;
    }

    r = read_pipe(pluto_qfd, (unsigned char *)&q->aq
	, sizeof(q->aq), sizeof(q->aq));

    if (r == HES_OK)
    {
	/* EOF: we're done, except for unanswered queries */
	struct worker_info *w;

	eof_from_pluto = TRUE;
	q->next = free_queries;
	free_queries = q;

	/* Send bye-bye to unbusy processes.
	 * Note that if there are queued queries, there won't be
	 * any non-busy workers.
	 */
	for (w = wi; w != wi_roof; w++)
	    if (!w->busy)
		send_eof(w);
    }
    else if (r != HES_CONTINUE)
    {
	exit(r);
    }
    else if (q->aq.qmagic != ADNS_Q_MAGIC)
    {
	syslog(LOG_ERR, "error in query from Pluto: bad magic");
	exit(HES_BAD_MAGIC);
    }
    else
    {
	struct worker_info *w;

	/* got a query */

	/* add it to FIFO */
	q->next = NULL;
	if (oldest_query == NULL)
	    oldest_query = q;
	else
	    newest_query->next = q;
	newest_query = q;

	/* See if any worker available */
	for (w = wi; ; w++)
	{
	    if (w == wi_roof)
	    {
		/* no free worker */
		if (w == wi + MAX_WORKERS)
		    break;	/* no more to be created */
		/* make a new one */
		if (!spawn_worker())
		    break;	/* cannot create one at this time */
	    }
	    if (!w->busy)
	    {
		/* assign first to free worker */
		forward_query(w);
		break;
	    }
	}
    }
    return;
}

static void
answer(struct worker_info *w)
{
    struct adns_answer a;
    enum helper_exit_status r = read_pipe(w->afd, (unsigned char *)&a
	, offsetof(struct adns_answer, ans), sizeof(a));

    if (r == HES_OK)
    {
	/* unexpected EOF */
	syslog(LOG_ERR, "unexpected EOF from worker");
	exit(HES_IO_ERROR_IN);
    }
    else if (r != HES_CONTINUE)
    {
	exit(r);
    }
    else if (a.amagic != ADNS_A_MAGIC)
    {
	syslog(LOG_ERR, "Input from worker error: bad magic");
	exit(HES_BAD_MAGIC);
    }
    else if (a.continuation != w->continuation)
    {
	/* answer doesn't match query */
	syslog(LOG_ERR, "Input from worker error: continuation mismatch");
	exit(HES_SYNC);
    }
    else
    {
	/* pass the answer on to Pluto */
	enum helper_exit_status r
	    = write_pipe(pluto_afd, (const unsigned char *) &a);

	if (r != HES_CONTINUE)
	    exit(r);
	w->busy = FALSE;
	forward_query(w);
    }
}

/* assumption: input limited; accept blocking on output */
static int
master(void)
{
    for (;;)
    {
	fd_set readfds;
	int maxfd = pluto_qfd;		/* approximate lower bound */
	int ndes = 0;
	struct worker_info *w;

	FD_ZERO(&readfds);
	if (!eof_from_pluto)
	{
	    FD_SET(pluto_qfd, &readfds);
	    ndes++;
	}
	for (w = wi; w != wi_roof; w++)
	{
	    if (w->busy)
	    {
		FD_SET(w->afd, &readfds);
		ndes++;
		if (maxfd < w->afd)
		    maxfd = w->afd;
	    }
	}

	if (ndes == 0)
	    return HES_OK;	/* done! */

	do {
	    ndes = select(maxfd + 1, &readfds, NULL, NULL, NULL);
	} while (ndes == -1 && errno == EINTR);
	if (ndes == -1)
	{
	    syslog(LOG_ERR, "select(2) error: %s", strerror(errno));
	    exit(HES_IO_ERROR_SELECT);
	}
	else if (ndes > 0)
	{
	    if (FD_ISSET(pluto_qfd, &readfds))
	    {
		query();
		ndes--;
	    }
	    for (w = wi; ndes > 0 && w != wi_roof; w++)
	    {
		if (w->busy && FD_ISSET(w->afd, &readfds))
		{
		    answer(w);
		    ndes--;
		}
	    }
	}
    }
}

/* Not to be invoked by strangers -- user hostile.
 * Mandatory args: query-fd answer-fd
 * Optional arg: -d, signifying "debug".
 */

static void
adns_usage(const char *fmt, const char *arg)
{
    const char **sp = ipsec_copyright_notice();

    fprintf(stderr, "INTERNAL TO PLUTO: DO NOT EXECUTE\n");

    fprintf(stderr, fmt, arg);
    fprintf(stderr, "\n%s\n", ipsec_version_string());

    for (; *sp != NULL; sp++)
	fprintf(stderr, "%s\n", *sp);

    syslog(LOG_ERR, fmt, arg);
    exit(HES_INVOCATION);
}

int
main(int argc UNUSED, char **argv)
{
    char **a;
    unsigned long fd;
    char *endptr;

    name = argv[0];

    for (a = argv + 1; *a != NULL; a++)
    {
	if (**a == '-')
	{
	    if (strcmp(*a, "-d") == 0)
		debug = TRUE;
	    else
		adns_usage("unrecognized flag: %s", *a);
	}
	else
	{
	    fd = strtoul(*a, &endptr, 0);

	    if (*endptr != 0 || fd <= 2 || (unsigned long)(int)fd != fd)
		adns_usage("unacceptable file descriptor arg: %s", *a);

	    if (pluto_qfd == NULL_FD)
		pluto_qfd = fd;
	    else if (pluto_afd == NULL_FD)
		pluto_afd = fd;
	    else
		adns_usage("too many file descriptor arguments", NULL);
	}
    }

    if (pluto_afd == NULL_FD)
	adns_usage("too few file descriptor arguments", NULL);

    return master();
}
