/* vim: set shiftwidth=4 tabstop=8 softtabstop=4: */
/* $Id: sptconn.c,v 1.12 2003/10/11 14:18:39 shinra Exp $ */

#include "sptprivate.h"
#include <rpc/types.h>
#include <rpc/xdr.h>
#include <utmp.h>
#include <signal.h>

/*
 * minimal implementation of simple RPC functionality, but too simple
 * for general purpose.
 */

#define UTMP_HOST_MAX 512   /* XXX: sufficient? */
#define MSG_SIZE_MAX 768
#define MSG_SIZE_MIN 4
#define B_TO_UI32_CONV(x) ((spt_uint32_t)(spt_uint8_t)(x))
#define B_TO_UI32_GET(p, i) (B_TO_UI32_CONV((p)[i]) << (24-8*(i)))
#define B_TO_UI32(p) (B_TO_UI32_GET(p, 0) | B_TO_UI32_GET(p, 1) \
	| B_TO_UI32_GET(p, 2) | B_TO_UI32_GET(p, 3))
#define UI32_TO_B_CONV(x, s) ((char)( (((spt_uint32_t)(x)) >> (s)) & 0xff ))
#define UI32_TO_B_SET(p, x, i) ((p)[i] = UI32_TO_B_CONV(x, 24-8*(i)))
#define UI32_TO_B(p, x) (UI32_TO_B_SET(p, x, 0), UI32_TO_B_SET(p, x, 1), \
	UI32_TO_B_SET(p, x, 2), UI32_TO_B_SET(p, x, 3))

/*
 * typedefs and structs
 */
struct connection_tag {
    const dispatch_table *table;
    int fd;
    int in_body;
    char sizebuf[4];
    size_t msgsize;
    size_t n_got;
    size_t bufsize;
    char *buf;
};

/*
 * function declarations
 */
#ifdef SPT_USE_SOCKET
static void spt_p_setmin(int fd, int size);
# define SET_MINIMUM(fd, size) spt_p_setmin(fd, size)
#else
# define SET_MINIMUM(fd, size)
#endif

static int spt_p_interpret_msg(connection *pconn);
static bool_t spt_p_xdr_msg(XDR *xdrs, msgunion_with_type *pmsg);
static bool_t spt_p_xdr_connect(XDR *xdrs, msg_connect *pmsgconn);
static bool_t spt_p_xdr_utmp(XDR *xdrs, msg_utmp *pmsgutmp);

/*
 * variables
 */
static struct xdr_discrim choice_table [] = {
    { MSG_RESULT, (xdrproc_t)&xdr_int },
    { MSG_CONNECT, (xdrproc_t)&spt_p_xdr_connect },
    { MSG_PROTO_VIOLATION, (xdrproc_t)&xdr_void },
    { MSG_INIT_TTY, (xdrproc_t)&xdr_void },
    { MSG_RELEASE_TTY, (xdrproc_t)&xdr_void },
    { MSG_LOGIN_UTMP, (xdrproc_t)&spt_p_xdr_utmp },
    { MSG_LOGOUT_UTMP, (xdrproc_t)&xdr_int },
    { MSG_LOGIN_WTMP, (xdrproc_t)&spt_p_xdr_utmp },
    { MSG_LOGOUT_WTMP, (xdrproc_t)&xdr_int },
    { MSG_UPDATE_LASTLOG, (xdrproc_t)&spt_p_xdr_utmp },
    { MSG_DISCONNECT, (xdrproc_t)&xdr_void },
};

/*
 * function definitions
 */
#ifdef SPT_USE_SOCKET
static void
spt_p_setmin(int fd, int size)
{
    /* ignore any errors, because this is just a optimization */
    setsockopt(fd, SOL_SOCKET, SO_RCVLOWAT,
	    &size, (spt_socklen_t)(sizeof size));
}
#endif

int
spt_p_create_conn(connection **ppconn, const dispatch_table *table, int fd)
{
    connection *pconn = MALLOC(sizeof(connection));
    if (pconn == NULL)
	return SPT_E_NOMEM;
    pconn->table = table;
    pconn->fd = fd;
    pconn->in_body = 0;
    pconn->n_got = 0;
    pconn->bufsize = 0;
    pconn->buf = NULL;
    SET_MINIMUM(fd, 4);
    *ppconn = pconn;
    return SPT_E_NONE;
}

void
spt_p_destroy_conn(connection *pconn)
{
    FREE(pconn->buf);
    FREE(pconn);
}

/* both blocking and nonblocking are OK */
int
spt_p_do_read(connection *pconn)
{
    ssize_t r;
    if (!pconn->in_body) {
	assert(pconn->n_got < 4);
	r = read(pconn->fd, pconn->sizebuf + pconn->n_got, 4 - pconn->n_got);
	if (r < 0) {
	    if (errno == EAGAIN || errno == EINTR)
		return SPT_IE_AGAIN;
	    return SPT_E_UNKNOWN;
	} else if (r == 0)
	    return SPT_IE_PEERDEAD;
	pconn->n_got += r;
	if (pconn->n_got < 4)
	    return SPT_IE_AGAIN;

	pconn->msgsize = B_TO_UI32(pconn->sizebuf);
	if (pconn->msgsize >= MSG_SIZE_MAX
		|| pconn->msgsize < MSG_SIZE_MIN)
	    return SPT_IE_VIOLATION;
	if (pconn->bufsize < pconn->msgsize) {
	    if ((pconn->buf = REALLOCF(pconn->buf, pconn->msgsize)) == NULL)
		return SPT_E_NOMEM;
	    pconn->bufsize = pconn->msgsize;
	}
	assert(pconn->n_got == 4);
	pconn->n_got = 0;
	pconn->in_body = 1;
	SET_MINIMUM(pconn->fd, pconn->msgsize);
	return SPT_IE_AGAIN;
    } else {
	assert(pconn->n_got < pconn->msgsize);
	r = read(pconn->fd, pconn->buf + pconn->n_got,
		pconn->msgsize - pconn->n_got);
	if (r < 0) {
	    if (errno == EAGAIN || errno == EINTR)
		return SPT_IE_AGAIN;
	    return SPT_E_UNKNOWN;
	} else if (r == 0)
	    return SPT_IE_PEERDEAD;
	pconn->n_got += r;
	if (pconn->n_got < pconn->msgsize)
	    return SPT_IE_AGAIN;
	assert(pconn->n_got == pconn->msgsize);
	pconn->n_got = 0;
	pconn->in_body = 0;
	SET_MINIMUM(pconn->fd, 4);

	return spt_p_interpret_msg(pconn);
    }
}

static int
spt_p_interpret_msg(connection *pconn)
{
    XDR xdrs;
    bool_t r;
    int retval;
    msgunion_with_type msg;

    xdrmem_create(&xdrs, pconn->buf, pconn->msgsize, XDR_DECODE);
    errno = 0;
    BZERO(&msg, sizeof msg);
    r = spt_p_xdr_msg(&xdrs, &msg);
    if (r == TRUE) {
	const dispatch_table *table = pconn->table;
	const dispatch_table_item *pitem;
	const dispatch_table_item *pend;
	if (xdr_getpos(&xdrs) < pconn->msgsize) {
	    retval = SPT_IE_VIOLATION;
	    goto last;
	}
	pitem = table->items;
	pend = pitem + table->n_items;
	for (; pitem < pend; ++pitem) {
	    if (pitem->msgtype == msg.msgtype) {
		retval = (*pitem->proc) (table->arg, msg.msgtype, &msg.mu);
		goto last;
	    }
	}
	if (table->defproc != NULL)
	    retval = (*table->defproc) (table->arg, msg.msgtype, &msg.mu);
	else
	    retval = SPT_IE_VIOLATION;
    } else {
	retval = (errno == ENOMEM) ? SPT_E_NOMEM : SPT_IE_VIOLATION;
    }
last:
    xdr_free((xdrproc_t)&spt_p_xdr_msg, (char *)&msg);
    xdr_destroy(&xdrs);
    return retval;
}

static bool_t
spt_p_xdr_msg(XDR *xdrs, msgunion_with_type *pmsg)
{
    return xdr_union(xdrs, &pmsg->msgtype,
	    (char *)&pmsg->mu, choice_table, NULL);
}

static bool_t
spt_p_xdr_connect(XDR *xdrs, msg_connect *pmsgconn)
{
    if (xdr_u_int(xdrs, &pmsgconn->version) == FALSE)
	return FALSE;
    if (xdr_string(xdrs, (char **)&pmsgconn->ttyname, PTY_NAME_MAX) == FALSE)
	return FALSE;
    return xdr_u_int(xdrs, &pmsgconn->ptytype);
}

static bool_t
spt_p_xdr_utmp(XDR *xdrs, msg_utmp *pmsgutmp)
{
    if (xdr_string(xdrs, (char **)&pmsgutmp->host, UTMP_HOST_MAX) == FALSE)
	return FALSE;
    return xdr_int(xdrs, &pmsgutmp->pid);
}

int
spt_p_send_void(connection *pconn, spt_int32_t msgtype)
{
    msgunion_with_type msg;
    msg.msgtype = msgtype;
    return spt_p_send_msg(pconn, &msg);
}

int
spt_p_send_int(connection *pconn, spt_int32_t msgtype, spt_int32_t val)
{
    msgunion_with_type msg;
    msg.msgtype = msgtype;
    msg.mu.mu_int = val;
    return spt_p_send_msg(pconn, &msg);
}

int
spt_p_send_str(connection *pconn, spt_int32_t msgtype, const char *str)
{
    msgunion_with_type msg;
    msg.msgtype = msgtype;
    msg.mu.mu_str = (char *)str;
    return spt_p_send_msg(pconn, &msg);
}

int
spt_p_send_connect(connection *pconn, const msg_connect *pmsgconn)
{
    msgunion_with_type msg;
    msg.msgtype = MSG_CONNECT;
    msg.mu.mu_connect = *pmsgconn;
    return spt_p_send_msg(pconn, &msg);
}

int
spt_p_send_msg(connection *pconn, const msgunion_with_type *pmsg)
{
    XDR xdrs;
    size_t size;
    char wbuf[4 + MSG_SIZE_MAX];
    char *pnext, *pend;
    sighandler_type sigpipe;
    int retval;
    int fflags;

    xdrmem_create(&xdrs, wbuf + 4, MSG_SIZE_MAX, XDR_ENCODE);
    errno = 0;
    if (spt_p_xdr_msg(&xdrs, (msgunion_with_type *)pmsg) == FALSE) {
	retval = (errno == ENOMEM) ? SPT_E_NOMEM : SPT_E_UNKNOWN;
	goto end1;
    }
    size = xdr_getpos(&xdrs);
    xdr_destroy(&xdrs);
    UI32_TO_B(wbuf, (spt_uint32_t)size);
    TRAP_SIGNAL(SIGPIPE, SIG_IGN, &sigpipe);
    fflags = fcntl(pconn->fd, F_GETFL);
    fcntl(pconn->fd, F_SETFL, fflags & (~O_NONBLOCK));
    for (pnext = wbuf, pend = pnext + size + 4; pnext < pend; ) {
	ssize_t r;

	r = write(pconn->fd, pnext, pend - pnext);
	if (r < 0) {
	    if (errno == EINTR)
		continue;
	    else if (errno == EPIPE)
		retval = SPT_IE_PEERDEAD;
	    else
		retval = SPT_E_UNKNOWN;
	    goto end2;
	}
	pnext += r;
    }
    retval = SPT_E_NONE;
end2:
    fcntl(pconn->fd, F_SETFL, fflags);
    RESTORE_SIGNAL(SIGPIPE, &sigpipe);
end1:
    xdr_destroy(&xdrs);
    return retval;
}
