/*
 * Copyright (c) 2003
 *	Fraunhofer Institute for Open Communication Systems (FhG Fokus).
 *	All rights reserved.
 *
 * Author: Harti Brandt <harti@freebsd.org>
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * $Begemot: bsnmp/snmpd/trans_lsock.c,v 1.6 2005/02/25 11:50:25 brandt_h Exp $
 *
 * Local domain socket transport
 */
#include <sys/types.h>
#include <sys/un.h>
#include <sys/stat.h>

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <syslog.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

#include "snmpmod.h"
#include "snmpd.h"
#include "trans_lsock.h"
#include "tree.h"
#include "oid.h"

static const struct asn_oid
	oid_begemotSnmpdLocalPortTable = OIDX_begemotSnmpdLocalPortTable;

static int lsock_start(void);
static int lsock_stop(int);
static void lsock_close_port(struct tport *);
static int lsock_init_port(struct tport *);
static ssize_t lsock_send(struct tport *, const u_char *, size_t,
    const struct sockaddr *, size_t);

/* exported */
const struct transport_def lsock_trans = {
	"lsock",
	OIDX_begemotSnmpdTransLsock,
	lsock_start,
	lsock_stop,
	lsock_close_port,
	lsock_init_port,
	lsock_send
};
static struct transport *my_trans;

static int
lsock_remove(struct tport *tp, intptr_t arg __unused)
{
	struct lsock_port *port = (struct lsock_port *)tp;

	(void)remove(port->name);

	return (-1);
}

static int
lsock_stop(int force)
{

	if (my_trans != NULL) {
		if (!force && trans_first_port(my_trans) != NULL)
			return (SNMP_ERR_GENERR);
		trans_iter_port(my_trans, lsock_remove, 0);
		return (trans_unregister(my_trans));
	}
	return (SNMP_ERR_NOERROR);
}

static int
lsock_start(void)
{
	return (trans_register(&lsock_trans, &my_trans));
}

/*
 * Open a local port. If this is a datagram socket create also the
 * one and only peer.
 */
static int
lsock_open_port(u_char *name, size_t namelen, struct lsock_port **pp,
    int type)
{
	struct lsock_port *port;
	struct lsock_peer *peer = NULL;
	int is_stream, need_cred;
	size_t u;
	int err;
	struct sockaddr_un sa;

	if (namelen == 0 || namelen + 1 > sizeof(sa.sun_path))
		return (SNMP_ERR_BADVALUE);

	switch (type) {
	  case LOCP_DGRAM_UNPRIV:
		is_stream = 0;
		need_cred = 0;
		break;

	  case LOCP_DGRAM_PRIV:
		is_stream = 0;
		need_cred = 1;
		break;

	  case LOCP_STREAM_UNPRIV:
		is_stream = 1;
		need_cred = 0;
		break;

	  case LOCP_STREAM_PRIV:
		is_stream = 1;
		need_cred = 1;
		break;

	  default:
		return (SNMP_ERR_BADVALUE);
	}

	if ((port = malloc(sizeof(*port))) == NULL)
		return (SNMP_ERR_GENERR);

	memset(port, 0, sizeof(*port));
	if (!is_stream) {
		if ((peer = malloc(sizeof(*peer))) == NULL) {
			free(port);
			return (SNMP_ERR_GENERR);
		}
		memset(peer, 0, sizeof(*peer));
	}
	if ((port->name = malloc(namelen + 1)) == NULL) {
		free(port);
		if (!is_stream)
			free(peer);
		return (SNMP_ERR_GENERR);
	}
	strncpy(port->name, name, namelen);
	port->name[namelen] = '\0';

	port->type = type;
	port->str_sock = -1;
	LIST_INIT(&port->peers);

	port->tport.index.len = namelen + 1;
	port->tport.index.subs[0] = namelen;
	for (u = 0; u < namelen; u++)
		port->tport.index.subs[u + 1] = name[u];

	if (peer != NULL) {
		LIST_INSERT_HEAD(&port->peers, peer, link);

		peer->port = port;

		peer->input.fd = -1;
		peer->input.id = NULL;
		peer->input.stream = is_stream;
		peer->input.cred = need_cred;
		peer->input.peer = (struct sockaddr *)&peer->peer;
	}

	trans_insert_port(my_trans, &port->tport);

	if (community != COMM_INITIALIZE &&
	    (err = lsock_init_port(&port->tport)) != SNMP_ERR_NOERROR) {
		lsock_close_port(&port->tport);
		return (err);
	}

	*pp = port;

	return (SNMP_ERR_NOERROR);
}

/*
 * Close a local domain peer
 */
static void
lsock_peer_close(struct lsock_peer *peer)
{

	LIST_REMOVE(peer, link);
	snmpd_input_close(&peer->input);
	free(peer);
}

/*
 * Close a local port
 */
static void
lsock_close_port(struct tport *tp)
{
	struct lsock_port *port = (struct lsock_port *)tp;
	struct lsock_peer *peer;

	if (port->str_id != NULL)
		fd_deselect(port->str_id);
	if (port->str_sock >= 0)
		(void)close(port->str_sock);
	(void)remove(port->name);

	trans_remove_port(tp);

	while ((peer = LIST_FIRST(&port->peers)) != NULL)
		lsock_peer_close(peer);

	free(port->name);
	free(port);
}

/*
 * Input on a local socket (either datagram or stream)
 */
static void
lsock_input(int fd __unused, void *udata)
{
	struct lsock_peer *peer = udata;
	struct lsock_port *p = peer->port;

	peer->input.peerlen = sizeof(peer->peer);
	if (snmpd_input(&peer->input, &p->tport) == -1 && peer->input.stream)
		/* framing or other input error */
		lsock_peer_close(peer);
}

/*
 * A UNIX domain listening socket is ready. This means we have a peer
 * that we need to accept
 */
static void
lsock_listen_input(int fd, void *udata)
{
	struct lsock_port *p = udata;
	struct lsock_peer *peer;

	if ((peer = malloc(sizeof(*peer))) == NULL) {
		syslog(LOG_WARNING, "%s: peer malloc failed", p->name);
		(void)close(accept(fd, NULL, NULL));
		return;
	}
	memset(peer, 0, sizeof(*peer));

	peer->port = p;

	peer->input.stream = 1;
	peer->input.cred = (p->type == LOCP_DGRAM_PRIV ||
	    p->type == LOCP_STREAM_PRIV);
	peer->input.peerlen = sizeof(peer->peer);
	peer->input.peer = (struct sockaddr *)&peer->peer;

	peer->input.fd = accept(fd, peer->input.peer, &peer->input.peerlen);
	if (peer->input.fd == -1) {
		syslog(LOG_WARNING, "%s: accept failed: %m", p->name);
		free(peer);
		return;
	}

	if ((peer->input.id = fd_select(peer->input.fd, lsock_input,
	    peer, NULL)) == NULL) {
		close(peer->input.fd);
		free(peer);
		return;
	}

	LIST_INSERT_HEAD(&p->peers, peer, link);
}

/*
 * Create a local socket
 */
static int
lsock_init_port(struct tport *tp)
{
	struct lsock_port *p = (struct lsock_port *)tp;
	struct sockaddr_un sa;

	if (p->type == LOCP_STREAM_PRIV || p->type == LOCP_STREAM_UNPRIV) {
		if ((p->str_sock = socket(PF_LOCAL, SOCK_STREAM, 0)) < 0) {
			syslog(LOG_ERR, "creating local socket: %m");
			return (SNMP_ERR_RES_UNAVAIL);
		}

		strcpy(sa.sun_path, p->name);
		sa.sun_family = AF_LOCAL;
		sa.sun_len = strlen(p->name) +
		    offsetof(struct sockaddr_un, sun_path);

		(void)remove(p->name);

		if (bind(p->str_sock, (struct sockaddr *)&sa, sizeof(sa))) {
			if (errno == EADDRNOTAVAIL) {
				close(p->str_sock);
				p->str_sock = -1;
				return (SNMP_ERR_INCONS_NAME);
			}
			syslog(LOG_ERR, "bind: %s %m", p->name);
			close(p->str_sock);
			p->str_sock = -1;
			return (SNMP_ERR_GENERR);
		}
		if (chmod(p->name, 0666) == -1)
			syslog(LOG_WARNING, "chmod(%s,0666): %m", p->name);

		if (listen(p->str_sock, 10) == -1) {
			syslog(LOG_ERR, "listen: %s %m", p->name);
			(void)remove(p->name);
			close(p->str_sock);
			p->str_sock = -1;
			return (SNMP_ERR_GENERR);
		}

		p->str_id = fd_select(p->str_sock, lsock_listen_input, p, NULL);
		if (p->str_id == NULL) {
			(void)remove(p->name);
			close(p->str_sock);
			p->str_sock = -1;
			return (SNMP_ERR_GENERR);
		}
	} else {
		struct lsock_peer *peer;

		peer = LIST_FIRST(&p->peers);

		if ((peer->input.fd = socket(PF_LOCAL, SOCK_DGRAM, 0)) < 0) {
			syslog(LOG_ERR, "creating local socket: %m");
			return (SNMP_ERR_RES_UNAVAIL);
		}

		strcpy(sa.sun_path, p->name);
		sa.sun_family = AF_LOCAL;
		sa.sun_len = strlen(p->name) +
		    offsetof(struct sockaddr_un, sun_path);

		(void)remove(p->name);

		if (bind(peer->input.fd, (struct sockaddr *)&sa, sizeof(sa))) {
			if (errno == EADDRNOTAVAIL) {
				close(peer->input.fd);
				peer->input.fd = -1;
				return (SNMP_ERR_INCONS_NAME);
			}
			syslog(LOG_ERR, "bind: %s %m", p->name);
			close(peer->input.fd);
			peer->input.fd = -1;
			return (SNMP_ERR_GENERR);
		}
		if (chmod(p->name, 0666) == -1)
			syslog(LOG_WARNING, "chmod(%s,0666): %m", p->name);

		peer->input.id = fd_select(peer->input.fd, lsock_input,
		    peer, NULL);
		if (peer->input.id == NULL) {
			(void)remove(p->name);
			close(peer->input.fd);
			peer->input.fd = -1;
			return (SNMP_ERR_GENERR);
		}
	}
	return (SNMP_ERR_NOERROR);
}

/*
 * Send something
 */
static ssize_t
lsock_send(struct tport *tp, const u_char *buf, size_t len,
    const struct sockaddr *addr, size_t addrlen)
{
	struct lsock_port *p = (struct lsock_port *)tp;
	struct lsock_peer *peer;

	if (p->type == LOCP_DGRAM_PRIV || p->type == LOCP_DGRAM_UNPRIV) {
		peer = LIST_FIRST(&p->peers);

	} else {
		/* search for the peer */
		LIST_FOREACH(peer, &p->peers, link)
			if (peer->input.peerlen == addrlen &&
			    memcmp(peer->input.peer, addr, addrlen) == 0)
				break;
		if (peer == NULL) {
			errno = ENOTCONN;
			return (-1);
		}
	}
	
	return (sendto(peer->input.fd, buf, len, 0, addr, addrlen));
}

/*
 * Dependency to create a lsock port
 */
struct lsock_dep {
	struct snmp_dependency dep;

	/* index (path name) */
	u_char *path;
	size_t pathlen;

	/* the port */
	struct lsock_port *port;

	/* which of the fields are set */
	u_int set;

	/* type of the port */
	int type;

	/* status */
	int status;
};
#define	LD_TYPE		0x01
#define	LD_STATUS	0x02
#define	LD_CREATE	0x04	/* rollback create */
#define	LD_DELETE	0x08	/* rollback delete */

/*
 * dependency handler for lsock ports
 */
static int
lsock_func(struct snmp_context *ctx, struct snmp_dependency *dep,
    enum snmp_depop op)
{
	struct lsock_dep *ld = (struct lsock_dep *)(void *)dep;
	int err = SNMP_ERR_NOERROR;

	switch (op) {

	  case SNMP_DEPOP_COMMIT:
		if (!(ld->set & LD_STATUS))
			err = SNMP_ERR_BADVALUE;
		else if (ld->port == NULL) {
			if (!ld->status)
				err = SNMP_ERR_BADVALUE;

			else {
				/* create */
				err = lsock_open_port(ld->path, ld->pathlen,
				    &ld->port, ld->type);
				if (err == SNMP_ERR_NOERROR)
					ld->set |= LD_CREATE;
			}
		} else if (!ld->status) {
			/* delete - hard to roll back so defer to finalizer */
			ld->set |= LD_DELETE;
		} else
			/* modify - read-only */
			err = SNMP_ERR_READONLY;

		return (err);

	  case SNMP_DEPOP_ROLLBACK:
		if (ld->set & LD_CREATE) {
			/* was create */
			lsock_close_port(&ld->port->tport);
		}
		return (SNMP_ERR_NOERROR);

	  case SNMP_DEPOP_FINISH:
		if ((ld->set & LD_DELETE) && ctx->code == SNMP_RET_OK)
			lsock_close_port(&ld->port->tport);
		free(ld->path);
		return (SNMP_ERR_NOERROR);
	}
	abort();
}

/*
 * Local port table
 */
int
op_lsock_port(struct snmp_context *ctx, struct snmp_value *value,
    u_int sub, u_int iidx, enum snmp_op op)
{
	asn_subid_t which = value->var.subs[sub-1];
	struct lsock_port *p;
	u_char *name;
	size_t namelen;
	struct lsock_dep *ld;
	struct asn_oid didx;

	switch (op) {

	  case SNMP_OP_GETNEXT:
		if ((p = (struct lsock_port *)trans_next_port(my_trans,
		    &value->var, sub)) == NULL)
			return (SNMP_ERR_NOSUCHNAME);
		index_append(&value->var, sub, &p->tport.index);
		break;

	  case SNMP_OP_GET:
		if ((p = (struct lsock_port *)trans_find_port(my_trans,
		    &value->var, sub)) == NULL)
			return (SNMP_ERR_NOSUCHNAME);
		break;

	  case SNMP_OP_SET:
		p = (struct lsock_port *)trans_find_port(my_trans,
		    &value->var, sub);

		if (index_decode(&value->var, sub, iidx, &name, &namelen))
			return (SNMP_ERR_NO_CREATION);

		asn_slice_oid(&didx, &value->var, sub, value->var.len);
		if ((ld = (struct lsock_dep *)(void *)snmp_dep_lookup(ctx,
		    &oid_begemotSnmpdLocalPortTable, &didx, sizeof(*ld),
		    lsock_func)) == NULL) {
			free(name);
			return (SNMP_ERR_GENERR);
		}

		if (ld->path == NULL) {
			ld->path = name;
			ld->pathlen = namelen;
		} else {
			free(name);
		}
		ld->port = p;

		switch (which) {

		  case LEAF_begemotSnmpdLocalPortStatus:
			if (ld->set & LD_STATUS)
				return (SNMP_ERR_INCONS_VALUE);
			if (!TRUTH_OK(value->v.integer))
				return (SNMP_ERR_WRONG_VALUE);

			ld->status = TRUTH_GET(value->v.integer);
			ld->set |= LD_STATUS;
			break;

		  case LEAF_begemotSnmpdLocalPortType:
			if (ld->set & LD_TYPE)
				return (SNMP_ERR_INCONS_VALUE);
			if (value->v.integer < 1 || value->v.integer > 4)
				return (SNMP_ERR_WRONG_VALUE);

			ld->type = value->v.integer;
			ld->set |= LD_TYPE;
			break;
		}
		return (SNMP_ERR_NOERROR);

	  case SNMP_OP_ROLLBACK:
	  case SNMP_OP_COMMIT:
		return (SNMP_ERR_NOERROR);

	  default:
		abort();
	}

	/*
	 * Come here to fetch the value
	 */
	switch (which) {

	  case LEAF_begemotSnmpdLocalPortStatus:
		value->v.integer = 1;
		break;

	  case LEAF_begemotSnmpdLocalPortType:
		value->v.integer = p->type;
		break;

	  default:
		abort();
	}

	return (SNMP_ERR_NOERROR);
}
