/* listener.c

   Subroutines that support the generic listener object. */

/*
 * Copyright (c) 2012,2014 by Internet Systems Consortium, Inc. ("ISC")
 * Copyright (c) 2004,2007,2009 by Internet Systems Consortium, Inc. ("ISC")
 * Copyright (c) 1999-2003 by Internet Software Consortium
 *
 * 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 ISC DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS.  IN NO EVENT SHALL ISC 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.
 *
 *   Internet Systems Consortium, Inc.
 *   950 Charter Street
 *   Redwood City, CA 94063
 *   <info@isc.org>
 *   https://www.isc.org/
 *
 */

#include "dhcpd.h"

#include <omapip/omapip_p.h>
#include <errno.h>

#if defined (TRACING)
omapi_array_t *trace_listeners;
static void trace_listener_accept_input (trace_type_t *, unsigned, char *);
static void trace_listener_remember (omapi_listener_object_t *,
				     const char *, int);
static void trace_listener_accept_stop (trace_type_t *);
trace_type_t *trace_listener_accept;
#endif

OMAPI_OBJECT_ALLOC (omapi_listener,
		    omapi_listener_object_t, omapi_type_listener)

isc_result_t omapi_listen (omapi_object_t *h,
			   unsigned port,
			   int max)
{
	omapi_addr_t addr;

#ifdef DEBUG_PROTOCOL
	log_debug ("omapi_listen(port=%d, max=%d)", port, max);
#endif

	addr.addrtype = AF_INET;
	addr.addrlen = sizeof (struct in_addr);
	memset (addr.address, 0, sizeof addr.address); /* INADDR_ANY */
	addr.port = port;

	return omapi_listen_addr (h, &addr, max);
}

isc_result_t omapi_listen_addr (omapi_object_t *h,
				omapi_addr_t *addr,
				int max)
{
	isc_result_t status;
	omapi_listener_object_t *obj;
	int i;

	/* Currently only support IPv4 addresses. */
	if (addr->addrtype != AF_INET)
		return DHCP_R_INVALIDARG;

	/* Get the handle. */
	obj = (omapi_listener_object_t *)0;
	status = omapi_listener_allocate (&obj, MDL);
	if (status != ISC_R_SUCCESS)
		/*
		 * we could simply return here but by going to
		 * error_exit we keep the code check tools happy
		 * without removing the NULL check on obj at
		 * the exit, which we could skip curently but
		 * might want in the future.
		 */
		goto error_exit;
	obj->socket = -1;

	/* Connect this object to the inner object. */
	status = omapi_object_reference (&h -> outer,
					 (omapi_object_t *)obj, MDL);
	if (status != ISC_R_SUCCESS)
		goto error_exit;
	status = omapi_object_reference (&obj -> inner, h, MDL);
	if (status != ISC_R_SUCCESS)
		goto error_exit;

	/* Set up the address on which we will listen... */
	obj -> address.sin_port = htons (addr -> port);
	memcpy (&obj -> address.sin_addr,
		addr -> address, sizeof obj -> address.sin_addr);
#if defined (HAVE_SA_LEN)
	obj -> address.sin_len =
		sizeof (struct sockaddr_in);
#endif
	obj -> address.sin_family = AF_INET;
	memset (&(obj -> address.sin_zero), 0,
		sizeof obj -> address.sin_zero);

#if defined (TRACING)
	/* If we're playing back a trace file, we remember the object
	   on the trace listener queue. */
	if (trace_playback ()) {
		trace_listener_remember (obj, MDL);
	}  else {
#endif
		/* Create a socket on which to listen. */
		obj -> socket = socket (PF_INET, SOCK_STREAM, IPPROTO_TCP);
		if (obj->socket == -1) {
			if (errno == EMFILE
			    || errno == ENFILE || errno == ENOBUFS)
				status = ISC_R_NORESOURCES;
			else
				status = ISC_R_UNEXPECTED;
			goto error_exit;
		}

#if defined (HAVE_SETFD)
		if (fcntl (obj -> socket, F_SETFD, 1) < 0) {
			status = ISC_R_UNEXPECTED;
			goto error_exit;
		}
#endif

		/* Set the REUSEADDR option so that we don't fail to start if
		   we're being restarted. */
		i = 1;
		if (setsockopt (obj -> socket, SOL_SOCKET, SO_REUSEADDR,
				(char *)&i, sizeof i) < 0) {
			status = ISC_R_UNEXPECTED;
			goto error_exit;
		}

		/* Try to bind to the wildcard address using the port number
		   we were given. */
		i = bind (obj -> socket,
			  (struct sockaddr *)&obj -> address,
			  sizeof obj -> address);
		if (i < 0) {
			if (errno == EADDRINUSE)
				status = ISC_R_ADDRNOTAVAIL;
			else if (errno == EPERM)
				status = ISC_R_NOPERM;
			else
				status = ISC_R_UNEXPECTED;
			goto error_exit;
		}

		/* Now tell the kernel to listen for connections. */
		if (listen (obj -> socket, max)) {
			status = ISC_R_UNEXPECTED;
			goto error_exit;
		}

		if (fcntl (obj -> socket, F_SETFL, O_NONBLOCK) < 0) {
			status = ISC_R_UNEXPECTED;
			goto error_exit;
		}

		status = omapi_register_io_object ((omapi_object_t *)obj,
						   omapi_listener_readfd, 0,
						   omapi_accept, 0, 0);
#if defined (TRACING)
	}
#endif

	omapi_listener_dereference (&obj, MDL);
	return status;

error_exit:
	if (obj != NULL) {
		if (h->outer == (omapi_object_t *)obj) {
			omapi_object_dereference((omapi_object_t **)&h->outer, 
						 MDL);
		}
		if (obj->inner == h) {
			omapi_object_dereference((omapi_object_t **)&obj->inner,
						 MDL);
		}
		if (obj->socket != -1) {
			close(obj->socket);
		}
		omapi_listener_dereference(&obj, MDL);
	}
	return status;
}

/* Return the socket on which the dispatcher should wait for readiness
   to read, for a listener object. */
int omapi_listener_readfd (omapi_object_t *h)
{
	omapi_listener_object_t *l;

	if (h -> type != omapi_type_listener)
		return -1;
	l = (omapi_listener_object_t *)h;
	
	return l -> socket;
}

/* Reader callback for a listener object.   Accept an incoming connection. */
isc_result_t omapi_accept (omapi_object_t *h)
{
	isc_result_t status;
	socklen_t len;
	omapi_connection_object_t *obj;
	omapi_listener_object_t *listener;
	struct sockaddr_in addr;
	int socket;

	if (h -> type != omapi_type_listener)
		return DHCP_R_INVALIDARG;
	listener = (omapi_listener_object_t *)h;

	/* Accept the connection. */
	len = sizeof addr;
	socket = accept (listener -> socket,
			 ((struct sockaddr *)&(addr)), &len);
	if (socket < 0) {
		if (errno == EMFILE || errno == ENFILE || errno == ENOBUFS)
			return ISC_R_NORESOURCES;
		return ISC_R_UNEXPECTED;
	}
	
#if defined (TRACING)
	/* If we're recording a trace, remember the connection. */
	if (trace_record ()) {
		trace_iov_t iov [3];
		iov [0].buf = (char *)&addr.sin_port;
		iov [0].len = sizeof addr.sin_port;
		iov [1].buf = (char *)&addr.sin_addr;
		iov [1].len = sizeof addr.sin_addr;
		iov [2].buf = (char *)&listener -> address.sin_port;
		iov [2].len = sizeof listener -> address.sin_port;
		trace_write_packet_iov (trace_listener_accept,
					3, iov, MDL);
	}
#endif

	obj = (omapi_connection_object_t *)0;
	status = omapi_listener_connect (&obj, listener, socket, &addr);
	if (status != ISC_R_SUCCESS) {
		close (socket);
		return status;
	}

	status = omapi_register_io_object ((omapi_object_t *)obj,
					   omapi_connection_readfd,
					   omapi_connection_writefd,
					   omapi_connection_reader,
					   omapi_connection_writer,
					   omapi_connection_reaper);

	/* Lose our reference to the connection, so it'll be gc'd when it's
	   reaped. */
	omapi_connection_dereference (&obj, MDL);
	if (status != ISC_R_SUCCESS)
		omapi_disconnect ((omapi_object_t *)(obj), 1);
	return status;
}

isc_result_t omapi_listener_connect (omapi_connection_object_t **obj,
				     omapi_listener_object_t *listener,
				     int socket,
				     struct sockaddr_in *remote_addr)
{
	isc_result_t status;
	omapi_object_t *h = (omapi_object_t *)listener;
	omapi_addr_t addr;

#ifdef DEBUG_PROTOCOL
	log_debug ("omapi_accept()");
#endif
	
	/* Get the handle. */
	status = omapi_connection_allocate (obj, MDL);
	if (status != ISC_R_SUCCESS)
		return status;

	(*obj) -> state = omapi_connection_connected;
	(*obj) -> remote_addr = *remote_addr;
	(*obj) -> socket = socket;

	/* Verify that this host is allowed to connect. */
	if (listener -> verify_addr) {
		addr.addrtype = AF_INET;
		addr.addrlen = sizeof (remote_addr -> sin_addr);
		memcpy (addr.address, &remote_addr -> sin_addr,
			sizeof (remote_addr -> sin_addr));
		addr.port = ntohs(remote_addr -> sin_port);

		status = (listener -> verify_addr) (h, &addr);
		if (status != ISC_R_SUCCESS) {
			omapi_disconnect ((omapi_object_t *)(*obj), 1);
			omapi_connection_dereference (obj, MDL);
			return status;
		}
	}

	omapi_listener_reference (&(*obj) -> listener, listener, MDL);
#if defined (TRACING)
	omapi_connection_register (*obj, MDL);
#endif
	status = omapi_signal (h, "connect", (*obj));
	return status;
}

#if defined (TRACING)
OMAPI_ARRAY_TYPE(omapi_listener, omapi_listener_object_t)

void omapi_listener_trace_setup (void) {
	trace_listener_accept =
		trace_type_register ("listener-accept", (void *)0,
				     trace_listener_accept_input,
				     trace_listener_accept_stop, MDL);
}

static void trace_listener_remember (omapi_listener_object_t *obj,
				     const char *file, int line)
{
	isc_result_t status;
	if (!trace_listeners) {
		status = omapi_listener_array_allocate (&trace_listeners,
							file, line);
		if (status != ISC_R_SUCCESS) {
		      foo:
			log_error ("trace_listener_remember: %s",
				   isc_result_totext (status));
			return;
		}
	}
	status = omapi_listener_array_extend (trace_listeners, obj,
					      &obj -> index, MDL);
	if (status != ISC_R_SUCCESS)
		goto foo;
}

static void trace_listener_accept_input (trace_type_t *ttype,
					 unsigned length, char *buf)
{
	struct in_addr *addr;
	u_int16_t *remote_port;
	u_int16_t *local_port;
	omapi_connection_object_t *obj;
	isc_result_t status;
	struct sockaddr_in remote_addr;

	addr = (struct in_addr *)buf;
	remote_port = (u_int16_t *)(addr + 1);
	local_port = remote_port + 1;

	memset (&remote_addr, 0, sizeof remote_addr);
	remote_addr.sin_addr = *addr;
	remote_addr.sin_port = *remote_port;

	omapi_array_foreach_begin (trace_listeners,
				   omapi_listener_object_t, lp) {
		if (lp -> address.sin_port == *local_port) {
			obj = (omapi_connection_object_t *)0;
			status = omapi_listener_connect (&obj,
							 lp, 0, &remote_addr);
			if (status != ISC_R_SUCCESS) {
				log_error("%s:%d: OMAPI: Failed to connect "
					  "a listener.", MDL);
			}
			omapi_listener_dereference (&lp, MDL);
			return;
		}
	} omapi_array_foreach_end (trace_listeners,
				   omapi_listener_object_t, lp);
	log_error ("trace_listener_accept: %s from %s/%d to port %d",
		   "unexpected connect",
		   inet_ntoa (*addr), *remote_port, *local_port);
}

static void trace_listener_accept_stop (trace_type_t *ttype) { }


#endif

isc_result_t omapi_listener_configure_security (omapi_object_t *h,
						isc_result_t (*verify_addr)
						 (omapi_object_t *,
						  omapi_addr_t *))
{
	omapi_listener_object_t *l;

	if (h -> type != omapi_type_listener)
		return DHCP_R_INVALIDARG;
	l = (omapi_listener_object_t *)h;

	l -> verify_addr = verify_addr;

	return ISC_R_SUCCESS;
}

isc_result_t omapi_listener_set_value (omapi_object_t *h,
				      omapi_object_t *id,
				      omapi_data_string_t *name,
				      omapi_typed_data_t *value)
{
	if (h -> type != omapi_type_listener)
		return DHCP_R_INVALIDARG;
	
	if (h -> inner && h -> inner -> type -> set_value)
		return (*(h -> inner -> type -> set_value))
			(h -> inner, id, name, value);
	return ISC_R_NOTFOUND;
}

isc_result_t omapi_listener_get_value (omapi_object_t *h,
				       omapi_object_t *id,
				       omapi_data_string_t *name,
				       omapi_value_t **value)
{
	if (h -> type != omapi_type_listener)
		return DHCP_R_INVALIDARG;
	
	if (h -> inner && h -> inner -> type -> get_value)
		return (*(h -> inner -> type -> get_value))
			(h -> inner, id, name, value);
	return ISC_R_NOTFOUND;
}

isc_result_t omapi_listener_destroy (omapi_object_t *h,
				     const char *file, int line)
{
	omapi_listener_object_t *l;

	if (h -> type != omapi_type_listener)
		return DHCP_R_INVALIDARG;
	l = (omapi_listener_object_t *)h;

#ifdef DEBUG_PROTOCOL
	log_debug ("omapi_listener_destroy()");
#endif
	
	if (l -> socket != -1) {
		close (l -> socket);
		l -> socket = -1;
	}
	return ISC_R_SUCCESS;
}

isc_result_t omapi_listener_signal_handler (omapi_object_t *h,
					    const char *name, va_list ap)
{
	if (h -> type != omapi_type_listener)
		return DHCP_R_INVALIDARG;
	
	if (h -> inner && h -> inner -> type -> signal_handler)
		return (*(h -> inner -> type -> signal_handler)) (h -> inner,
								  name, ap);
	return ISC_R_NOTFOUND;
}

/* Write all the published values associated with the object through the
   specified connection. */

isc_result_t omapi_listener_stuff_values (omapi_object_t *c,
					  omapi_object_t *id,
					  omapi_object_t *l)
{
	if (l -> type != omapi_type_listener)
		return DHCP_R_INVALIDARG;

	if (l -> inner && l -> inner -> type -> stuff_values)
		return (*(l -> inner -> type -> stuff_values)) (c, id,
								l -> inner);
	return ISC_R_SUCCESS;
}

