/*
 *   DIS/x : An implementation of the IEEE 1278.1 protocol
 *
 *   Copyright (C) 1996, Riley Rainey (rainey@netcom.com)
 *
 *   This library is free software; you can redistribute it and/or
 *   modify it under the terms of either:
 *
 *   a) the GNU Library General Public License as published by the Free
 *   Software Foundation; either version 2 of the License, or (at your
 *   option) any later version.  A description of the terms and conditions
 *   of the GLPL may be found in the "COPYING.LIB" file.
 *
 *   b) the "Artistic License" which comes with this Kit.  Information
 *   about this license may be found in the "Artistic" file.
 *
 *   This library 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
 *   Library General Public License or the Artistic License for more details.
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this library; if not, write to the Free
 *   Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 *   Information describing how to contact the author can be found in the
 *   README file.
 */

/**
 * DIS relay daemon.
 * 
 * Currently this program registers as client any host from which it
 * receives an UDP packet on the given server port. Each packet received from a
 * client is then re-sent to all the registered clients. Registered clients are
 * automatically de-registered after they are silent for too much time
 * (currently 60 s).
 * The server listens on any available Internet interface.
 * 
 * Command line options:
 * 
 * --port PORT   Set the UDP port of the relay server, default being 3000.
 * 
 * --debug       Displays several debugging messages, including: new registered
 *               clients; received packets; sent packets; de-registered clients.
 * 
 * --help | -h   Help!
 * 
 * @file
 */

#ifdef WINNT
	// recvmsg() not available under WINNT.
	#undef HAVE_RECVMSG
	#include <ws2tcpip.h>
	// Under MinGW, add missing prototype of a function provided by libws2_32.a:
	WINSOCK_API_LINKAGE const char * WSAAPI inet_ntop(int af, const void *src,
		char *dst, socklen_t size);
#else
	#include <net/if.h>
	#include <sys/ioctl.h>
	#include <sys/socket.h>
	#include <sys/uio.h>
	#include <arpa/inet.h>
	#define HAVE_RECVMSG
	// Cope with slightly different field names of msghdr: Solaris and BSD call
	// them "msg_accessright...", any other system calls them "msg_control...".
	#undef  HAVE_MSG_ACCRIGHTS
	#define HAVE_MSG_CONTROL
#endif

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/file.h>
#include <time.h>

#include "../../util/memory.h"
#include "../../util/error.h"
#include "../dis/dis.h"

/**
 * Remove client silent since so much time (s).
 */
#define STALE_PERIOD 60

typedef struct SwitchClient {
	struct SwitchClient	*prev, *next;
	struct sockaddr		addr;
	/** Timestamp last received PDU. */
	time_t last_got_pdu;
} SwitchClient;

static SwitchClient *client_list = 0;


static void accountErrorOnClient(SwitchClient *sc)
{
	// FIXME: to do: remove unreachable clients after a while
}



/**
 * Returns true if the two sockets are equal.
 * @param a
 * @param b
 * @return True if the two sockets are equal.
 */
static int sockaddrEquals(struct sockaddr *a, struct sockaddr *b)
{
	return a->sa_family == b->sa_family
	&& (
		(
			(a->sa_family == AF_INET) && (memcmp(a, b, sizeof(struct sockaddr_in)) == 0)
		) || (
			(a->sa_family == AF_INET6) && (memcmp(a, b, sizeof(struct sockaddr_in6)) == 0)
/*
		) || (
			(a->sa_family == AF_UNIX) && (strcmp(((struct sockaddr_un *) a)->sun_path, ((struct sockaddr_un *) b)->sun_path) == 0)
*/
		)
	);
}


/**
 * Returns the socket address as a human readable string.
 * @author W. Richard Stevens, UNIX Network Programming vol. 1, par. 3.8.
 * @param sa
 * @return 
 */
static char *
sockaddrToString(const struct sockaddr *sa)
{
	char		portstr[7];
	static char str[128];		/* Unix domain is largest */

	switch (sa->sa_family) {
	case AF_INET: {
		struct sockaddr_in	*sin = (struct sockaddr_in *) sa;

		if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL)
			return(NULL);
		if (ntohs(sin->sin_port) != 0) {
			snprintf(portstr, sizeof(portstr), ":%d", ntohs(sin->sin_port));
			strcat(str, portstr);
		}
		return str;
	}

	case AF_INET6: {
		struct sockaddr_in6	*sin6 = (struct sockaddr_in6 *) sa;

		if (inet_ntop(AF_INET6, &sin6->sin6_addr, str, sizeof(str)) == NULL)
			return(NULL);
		if (ntohs(sin6->sin6_port) != 0) {
			snprintf(portstr, sizeof(portstr), ":%d", ntohs(sin6->sin6_port));
			strcat(str, portstr);
		}
		return str;
	}

/*
	case AF_UNIX: {
		struct sockaddr_un	*unp = (struct sockaddr_un *) sa;

			/ * OK to have no pathname bound to the socket: happens on
			   every connect() unless client calls bind() first. * /
		if (unp->sun_path[0] == 0)
			strcpy(str, "(no pathname bound)");
		else
			snprintf(str, sizeof(str), "%s", unp->sun_path);
		return str;
	}
*/

#ifdef	HAVE_SOCKADDR_DL_STRUCT
	case AF_LINK: {
		struct sockaddr_dl	*sdl = (struct sockaddr_dl *) sa;

		if (sdl->sdl_nlen > 0)
			snprintf(str, sizeof(str), "%*s",
					 sdl->sdl_nlen, &sdl->sdl_data[0]);
		else
			snprintf(str, sizeof(str), "AF_LINK, index=%d", sdl->sdl_index);
		return str;
	}
#endif
	default:
		snprintf(str, sizeof(str), "sock_ntop: unknown AF_xxx: %d",
				 sa->sa_family);
		return str;
	}
}


#ifdef WINNT

static char *errorCodeToString(int code)
{
	static char s[999];
	char win_err_descr[900];
	DWORD err = FormatMessageA(
		FORMAT_MESSAGE_FROM_SYSTEM,
		NULL,
		code,
		LANG_SYSTEM_DEFAULT,
		win_err_descr,
		sizeof(win_err_descr),
		NULL
	);
	if( err > 0 ){
		snprintf(s, sizeof(s), "%s (Windows error code %d)", win_err_descr, code);
	} else {
		snprintf(s, sizeof(s), "error code %d (description not available: FormatMessageA() failed with code %lu)", code, err);
	}
	return s;
}

#endif


/**
 * Reads next PDU.
 * @param xcvr
 * @param pdu_content
 * @param pdu_max_len
 * @param pdu_from
 * @return Length of the read PDU, or zero on error.
 */
static int ReadPDUFrame(dis_Transceiver * xcvr, char *pdu_content, int pdu_max_len, struct sockaddr *pdu_from)
{
	int size;
	
#ifdef HAVE_RECVMSG
	struct msghdr msg;
	struct iovec vec;
#endif

#ifdef HAVE_RECVMSG
	msg.msg_name = (caddr_t) pdu_from;
	msg.msg_namelen = sizeof(struct sockaddr);
	msg.msg_iov = &vec;
	msg.msg_iovlen = 1;
#ifdef HAVE_MSG_CONTROL
	msg.msg_control = (caddr_t) NULL;
	msg.msg_controllen = 0;
#endif    
#ifdef HAVE_MSG_ACCRIGHTS
	msg.msg_accrights = (caddr_t) NULL;
	msg.msg_accrightslen = 0;
#endif    
	vec.iov_base = (caddr_t) pdu_content;
	vec.iov_len = pdu_max_len;

	size = recvmsg(xcvr->s, &msg, 0);
	
	if( size < 0 ){
		if( errno != EAGAIN )
			printf("failed reading socket: %s (%d)\n", strerror(errno), errno);
		size = 0;
	}
	
#else /* WINNT assumed */
	
	int pdu_from_len = sizeof(*pdu_from);
	size = recvfrom(xcvr->s, pdu_content, pdu_max_len, 0, pdu_from, &pdu_from_len);
	
	if( size < 0 ){
		printf("recvfrom(): %s\n", errorCodeToString( WSAGetLastError() ) );
		size = 0;
	}
	
#endif

	return size;
}


static int WritePDU(dis_Transceiver * xcvr, char *buffer, int length, struct sockaddr *to)
{
#ifdef HAVE_RECVMSG
	struct msghdr msg;
	struct iovec vec;

	msg.msg_namelen = sizeof(struct sockaddr);
	msg.msg_iov = &vec;
	msg.msg_iovlen = 1;
#ifdef HAVE_MSG_CONTROL
	msg.msg_control = (caddr_t) NULL;
	msg.msg_controllen = 0;
#endif
#ifdef HAVE_MSG_ACCRIGHTS
	msg.msg_accrights = (caddr_t) NULL;
	msg.msg_accrightslen = 0;
#endif    
	vec.iov_base = buffer;
	vec.iov_len = length;

	msg.msg_name = (caddr_t) to;
	if (sendmsg(xcvr->s, &msg, 0) == -1) {
		fprintf(stderr, "%s:     relaying PDU to %s: %s\n",
			error_prog_name, sockaddrToString(to), strerror(errno));
		return 0;
	} else {
		return 1;
	}
#else
	if (sendto(xcvr->s, buffer, length, 0, to, sizeof(struct sockaddr)) == -1) {
#ifdef WINNT
		fprintf(stderr, "sendto(): %s\n", errorCodeToString(WSAGetLastError()));
#else
		perror("sendto()");
#endif
		return 0;
	} else {
		return 1;
	}
#endif
}

/**
 * Sends the packet received from a client to all the other registered clients.
 * If the sender is not already registered, it is added to the list.
 * Also checks for stale silent clients and removes them from the list.
 * @param xcvr
 * @param pdu_content The packet.
 * @param pdu_len Length of the packet.
 * @param pdu_from Sender of the packet.
 */
static void RelayPDUFrame(dis_Transceiver * xcvr,
		char *pdu_content,
		int pdu_len,
		struct sockaddr *pdu_from)
{
	SwitchClient *c, *new, *stale;
	int found = 0;
	time_t now;

	now = time(NULL);
	c = client_list;
	while(c != NULL){
		if( sockaddrEquals(pdu_from, &c->addr) ){
			/* Found sender in registered clients. Renew stale period. */
			found = 1;
			c->last_got_pdu = now;
			c = c->next;
		} else if( now - c->last_got_pdu > STALE_PERIOD ){
			/* Remove stale client from registered clients. */
			stale = c;
			if( debug )
				printf("%s: removing stale client %s\n",
					error_prog_name, sockaddrToString(&stale->addr));
			if( stale->prev == NULL )
				client_list = stale->next;
			else
				stale->prev->next = stale->next;
			if( stale->next != NULL )
				stale->next->prev = stale->prev;
			c = stale->next;
			memory_dispose(stale);
		} else {
			/* Relay frame to client. */
			if( debug )
				printf("%s:     relaying frame to %s\n",
					error_prog_name, sockaddrToString(&c->addr));
			if( ! WritePDU(xcvr, pdu_content, pdu_len, &c->addr) )
				accountErrorOnClient(c);
			c = c->next;
		}
	}
	if ( ! found ) {
		if( debug )
			printf("%s: adding client %s\n", error_prog_name, sockaddrToString(pdu_from));
		new = memory_allocate(sizeof(SwitchClient), NULL);
		new->prev = NULL;
		new->next = client_list;
		if( new->next != NULL )
			new->next->prev = new;
		memcpy(&new->addr, pdu_from, sizeof(struct sockaddr));
		new->last_got_pdu = time(NULL);
		client_list = new;
    }
}


int
main(int argc, char **argv)
{
	int i, port;
	dis_Transceiver *xcvr;
	struct sockaddr pdu_from;
	int pdu_len;
	char pdu_content[2048];
	
	error_prog_name = argv[0];
	
	/* Set default values: */
	port = 3000;

	for(i = 1; i < argc; i++){
		if( strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0){
			printf("Allowed options:\n");
			printf("  --port N   Use port number N (default: 3000)\n");
			printf("  --debug    Displays several debugging messages.\n");
			printf("  --help|-h  This help.\n");
			exit(0);
		} else if( strcmp(argv[i], "--debug") == 0 ){
			debug = 1;
		} else if( strcmp(argv[i], "--port") == 0 && i+1 < argc ){
			port = atoi(argv[++i]);
		} else {
			error_external("unknown option: %s. Try --help for help.", argv[i]);
		}
	}
	
	xcvr = dis_openTransceiver(1, NULL, port);
	if( xcvr == NULL )
		error_external("failed establishing a connection to the DIS network");
	if( debug )
		printf("%s: running on port %d ...\n", argv[0], port);
	
    while (1) {
		pdu_len = ReadPDUFrame(xcvr, pdu_content, sizeof(pdu_content), &pdu_from);
		if( debug && pdu_len > 0 )
			printf("%s: got PDU size %d from %s\n",
				error_prog_name, pdu_len, sockaddrToString(&pdu_from));
		
		if (pdu_len > 0) {
			RelayPDUFrame(xcvr, pdu_content, pdu_len, &pdu_from);
		}
	}
}
