/* AmiTrack.c -- a program to keep a running list of available Amigas. */

#define __DICE_C

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include <exec/types.h>
#include <exec/io.h>
#include <exec/lists.h>
#include <exec/memory.h>

#include <clib/exec_protos.h>
#include <clib/alib_protos.h>
#include <clib/dos_protos.h>

#include <errno.h>
#include <inetd.h>
#include <sys/types.h>

#include <proto/socket.h>
#include <proto/exec.h>
#include <sys/errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/syslog.h>
#include <netdb.h>

#include <pragmas/socket_pragmas.h>
#include <devices/timer.h>

#define UNLESS(x) if(!(x))

#define AMITRACK_TIMEOUT_MINUTES 5

/* Possible Request bits for the first data byte of the UDP packets */
#define REQ_COMMENT 0x01	/* "My comment is now this string:" */
#define REQ_BYE	    0x02	/* "I'm quitting, remove me immediately" */

/* Used internally by the event loops */
#define CODE_UDP_SOCKET    0x01	/* "You've got a UDP packet ready to read" */
#define CODE_TCP_SOCKET    0x02	/* "You've got a TCP connection ready" */
#define CODE_TIMER_EXPIRED 0x04	/* "The timer expired." */
#define CODE_REFRESH       0x08 /* "Please redraw the client list." */

/* Since TCP and UDP have separate port domains, I can use the same port numbers
   for the UDP and TCP ports */
#define SERVER_UDP_PORT 18945	/* UDP port for the server process */
#define SERVER_TCP_PORT 18945	/* TCP port for the server process */
#define CLIENT_UDP_PORT 18946	/* UDP port for the client process */
#define CLIENT_TCP_PORT 18946	/* TCP port for the client process */

struct Client {
	struct Node node;
	time_t tDateStamp;
	char * hostname;
	char * comment;
};
	
struct Library * SocketBase = NULL;
struct Library * TimerBase  = NULL;

struct timerequest *TimerIO  = NULL;
struct MsgPort	   *TimerMP  = NULL;
struct Message	   *TimerMSG = NULL;

BOOL BSendGoodbye = FALSE;	/* used by the client */
ULONG ulPurgeDelay = AMITRACK_TIMEOUT_MINUTES * 60;
LONG sUDPSocket = -1L, sTCPSocket = -1L;
struct sockaddr_in saUDPSocket, saTCPSocket;
struct List * ClientList = NULL;
int nNumClients = 0;	/* number of entries in the server's client list */

void BeAServer(void);
void BeAClient(char * szHostName, char * szComment);
void Cleanup(void);
ULONG TrackWait(ULONG ulMask);
LONG CreateUDPSocket(int nPort);
BOOL SendPacket(ULONG ulReqs, char * szComment);
BOOL ConnectToServer(char * szPeerName);
void HandleClientRequest(void);
void HandleListRequest(void);
BOOL SetupList(BOOL BSetup);
void FreeClientList(void);
struct Client * GetClient(char * szHostName, char * szOptComment);
struct Client * NewClient(char * szHostName, char * szOptComment);
void FreeClient(struct Client * client);
void ReplaceCommentString(struct Client * client, char * szNewComment);
void PrintClientList(void);
void RemoveClient(char * szHostName);
BOOL SetupTimer(BOOL BSetup);
void SetTimer(int nSecs, int nMicros);
void PurgeOldClients(void);
void RemoveUnprintableChars(char * pcString);

int main(int argc, char **argv)
{	
	atexit(Cleanup);
	
	UNLESS(SocketBase = OpenLibrary("bsdsocket.library", 2))
	{
		printf("Couldn't open socket.library v2+\n");
		exit(5);
	}
	UNLESS(SetupTimer(TRUE)) 
	{
		printf("Couldn't setup timer.device.\n");
		exit(5);
	}
	
	     if (argc <= 1) BeAServer();
	else if (argc <= 3) BeAClient(argv[1], (argc==3) ? argv[2] : NULL);
	else
	{
		printf("Usage:  track\nor:     track <servername> [message]\n");
		exit(5);
	}
}



/* Returns TRUE if everything went okay, else FALSE. */
/* SetupTimer(TRUE) sets everything up, SetupTimer(FALSE) tears it down */
BOOL SetupTimer(BOOL BSetup)
{
	#ifdef __DICE_C
	/* DICE seems to have a bug in it that makes it require this...
	   otherwise tv_secs and tv_micro aren't recognized!  :( */
	struct timeval {
		ULONG tv_secs;
		ULONG tv_micro;
	};
	#endif
	
	struct timeval t;
	
	t.tv_secs = 0;
	t.tv_micro = 0;
	
	if (BSetup == (TimerIO != NULL)) return(FALSE);
	if (BSetup)
	{
		/* Set up the timer device ports */
		UNLESS(TimerMP = CreatePort(0,0)) return(FALSE);
	        UNLESS(TimerIO = (struct timerequest *) CreateExtIO(TimerMP, (sizeof (struct timerequest))))
	        {
	        	DeletePort(TimerMP);
	        	return(FALSE);
	        }

		/* Allow access to timer device's library functions */
		TimerBase = (struct Library *) TimerIO->tr_node.io_Device;

		/* Open the timer.device with UNIT_WAITUNTIL for s updates */
		if (OpenDevice(TIMERNAME,UNIT_VBLANK,(struct IORequest *)TimerIO,0)) 
		{
			DeleteExtIO((struct IORequest *) TimerIO);
			DeletePort(TimerMP);
			return(FALSE);
		}

		TimerIO->tr_node.io_Message.mn_ReplyPort = TimerMP;
		TimerIO->tr_node.io_Command 		 = TR_ADDREQUEST;
		TimerIO->tr_node.io_Flags 		 = 0;
		TimerIO->tr_node.io_Error 		 = 0;
		TimerIO->tr_time.tv_secs  		 = 0;
		TimerIO->tr_time.tv_micro 		 = 0; 	
	}
	else
	{
		if (!(CheckIO((struct IORequest *)TimerIO))) 
        	{
        		AbortIO((struct IORequest *)TimerIO);   /* Ask device to abort any pending requests */
        	        WaitIO((struct IORequest *)TimerIO);    /* proceed when ready */
		}
        	CloseDevice((struct IORequest *) TimerIO);
        	DeleteExtIO((struct IORequest *) TimerIO);
	        if (TimerMP) DeletePort(TimerMP);
	}
	return(TRUE);
}



/* Set/Reset timer.device to signal us in nSecs seconds + nMicros microseconds */
void SetTimer(int nSecs, int nMicros)
{
 	/* First make sure there is no previous timer pending */
        if (!(CheckIO((struct IORequest *) TimerIO)))
        {
            AbortIO((struct IORequest *) TimerIO);
            WaitIO((struct IORequest *) TimerIO);
        }
               
        TimerIO->tr_time.tv_secs  = nSecs;                                        
        TimerIO->tr_time.tv_micro = nMicros;
        
        /* Start ze timer */
        SendIO((struct IORequest *)TimerIO);
}



/* Always called when AmiTrack exits, does any necessary cleanup */
void Cleanup(void)
{
	printf("Exiting...\n");
	SetupTimer(FALSE);
	SetupList(FALSE);	

	if (BSendGoodbye) SendPacket(REQ_BYE,"");
	if (sUDPSocket != -1L) CloseSocket(sUDPSocket);
	if (sTCPSocket != -1L) CloseSocket(sTCPSocket);
	if (SocketBase) CloseLibrary(SocketBase);
}



/* Act as a server */
void BeAServer(void)
{
	BOOL BDone = FALSE, BPrintList;
	int nCode;
	
	UNLESS(SetupList(TRUE))
	{
		printf("Couldn't create client list!\n");
		return;
	}
	
	if ((sUDPSocket = CreateUDPSocket(SERVER_UDP_PORT)) == -1L) return; 
	if ((sTCPSocket = CreateTCPSocket(SERVER_TCP_PORT)) == -1L) return;
	
	PrintClientList();
	SetTimer(ulPurgeDelay,0);
	while(nCode = TrackWait(SIGBREAKF_CTRL_C | SIGBREAKF_CTRL_D | (1L<<TimerMP->mp_SigBit))) 
	{
		BPrintList = FALSE;
		
		if (nCode & CODE_TCP_SOCKET) HandleListRequest();
		if (nCode & CODE_UDP_SOCKET) 
		{
			HandleClientRequest();
			BPrintList = TRUE;
		}	
		if (nCode & CODE_TIMER_EXPIRED)   
		{
			PurgeOldClients(); 	
			SetTimer(ulPurgeDelay,0);
			BPrintList = TRUE;
		}
		if (nCode & CODE_REFRESH) BPrintList = TRUE;
		
		if (BPrintList) PrintClientList();
	}
}




/* Called when someone attempts to connect to us via a TCP stream.
   Dumps the current client list out to the connectee, then closes
   the stream so he knows we're done outputting. */
void HandleListRequest(void)
{
	struct hostent * hp;
	struct sockaddr_in saFrom;
	LONG sSizeOf = sizeof(struct sockaddr_in);
	LONG sNewSocket = accept(sTCPSocket, &saFrom, &sSizeOf);
	struct Client * client = (struct Client *) ClientList->lh_Head;
	
	if (hp = gethostbyaddr((caddr_t)&saFrom.sin_addr, sizeof(saFrom.sin_addr), AF_INET))
	{
		printf("Sending Client list to [%s]...\n", hp->h_name);

		/* Now send the list */
		while (client->node.ln_Succ)
		{
			send(sNewSocket, client->hostname, strlen(client->hostname)+1, 0L);
			send(sNewSocket, client->comment,  strlen(client->comment) +1, 0L);
			client = (struct Client *)client->node.ln_Succ;
		}
	}
	else printf("Couldn't determine connection's origin, disconnecting.\n");
	CloseSocket(sNewSocket);
}


/* Go through the client list, and remove anyone who has a timestamp
   that is older than tCutoff. */
void PurgeOldClients(void)
{
	time_t tCutOff = time(NULL) - ulPurgeDelay;
	struct Client * client = (struct Client *) ClientList->lh_Head;
	struct Client * next;
	
	while (client->node.ln_Succ)
	{
		next = client->node.ln_Succ;
		if (client->tDateStamp < tCutOff) 
		{
			Remove(client); FreeClient(client); nNumClients--;
		}
		client = next;
	}
}


/* Called when we are sent a UDP packet by some client */
void HandleClientRequest(void)
{
	char sRecvBuf[1500];
	static struct sockaddr_in saFrom;
	LONG lAddrSize = sizeof(struct sockaddr_in), lReqs;
	struct Client * client;
	struct hostent * hp;
	int nBufLen;
	
	nBufLen = recvfrom(sUDPSocket, sRecvBuf, sizeof(sRecvBuf), 0L, &saFrom, &lAddrSize);
	if (nBufLen < 0) 
	{
		printf("Recvfrom failed.\n");
		return;
	}
	
	hp = gethostbyaddr((caddr_t)&saFrom.sin_addr, sizeof(saFrom.sin_addr), AF_INET);
	UNLESS((hp)&&(hp->h_name))
	{
		printf("Couldn't determine packet's origin.\n");
		return;
	}
	memcpy(&lReqs, sRecvBuf, sizeof(ULONG));
	
	UNLESS(client = GetClient(hp->h_name, (lReqs & REQ_COMMENT) ? &sRecvBuf[sizeof(ULONG)] : NULL)) 
	{
		printf("Add/retrieve of client listing failed.\n");
		return;
	}
	
	if (lReqs & REQ_BYE)  RemoveClient(hp->h_name);
	client->tDateStamp = time(NULL);
}




/* Returns TRUE on success, FALSE on failure.
   SetupList(TRUE) sets everything up, SetupList(FALSE) tears everything down. */
BOOL SetupList(BOOL BSetup)
{
	if (BSetup == (ClientList != NULL)) return(FALSE);

	if (BSetup)
	{
		UNLESS(ClientList = AllocMem(sizeof(struct List),MEMF_CLEAR)) return(FALSE);
		NewList(ClientList);
		nNumClients = 0;
	}
	else
	{
		FreeClientList();
		FreeMem(ClientList, sizeof(struct List));
		ClientList = NULL;
	}
	return(TRUE);
}



/* Empties the client list of all entries & frees their allocated memory */
void FreeClientList(void)
{
	struct Client * current;

	if (ClientList)
	{
		while(current = (struct Client *)RemHead(ClientList))
		{
			nNumClients--;
			FreeClient(current);
		}
		if (nNumClients != 0) printf("FreeClientList:  Warning, client list marked as having %i items after freed!\n",nNumClients);
		nNumClients = 0;
	}
}



/* Replaces all unprintable characters in pcString with '_' */
/* Hopefully this will stop people from sending e.g. "talkbombs" */
void RemoveUnprintableChars(char * pcString)
{
	while(*pcString)
	{
		if (*pcString < ' ') *pcString = '_';
		pcString++;
	}
}



/* Find the client structure with the given hostname in the client list.
   Failing that, create one and insert it into the list.  Failing
   that, return NULL.  If szOptComment is non-NULL, it will be created/
   replaced in the returned Client. */
struct Client * GetClient(char * szHostName, char * szOptComment)
{
	struct Client * client = (struct Client *) ClientList->lh_Head;
	struct Client * prevClient = NULL;
	int nCompare;

	if (szOptComment) RemoveUnprintableChars(szOptComment);
	
	while (client->node.ln_Succ)
	{
		nCompare = strcmp(szHostName, client->hostname);
		if (nCompare == 0)
		{
			if (szOptComment) ReplaceCommentString(client, szOptComment);
			return(client);
		}
		if (nCompare < 0) break;	/* Not in the list--insert it here */
		prevClient = client;
		client = (struct Client *)client->node.ln_Succ;
	}

	/* If we got here, we need to add the client to the list */
	UNLESS(client = NewClient(szHostName, szOptComment)) return(NULL);
	Insert(ClientList, client, prevClient);	nNumClients++;
	return(client);
}


/* Removes the client entry with the hostname szHostName from the ClientList.  */
void RemoveClient(char * szHostName)
{
	struct Client * client = (struct Client *) ClientList->lh_Head;
	
	while(client->node.ln_Succ)
	{		
		UNLESS(strcmp(szHostName, client->hostname))
		{
			/* found our victim! */
			Remove(client); FreeClient(client); nNumClients--;
			return;
		}
		client = client->node.ln_Succ;
	}
}




/* Create/replace the client's comment string with szComment. */
void ReplaceCommentString(struct Client * client, char * szComment)
{
	char * pcTemp;

	if (pcTemp = AllocMem(strlen(szComment)+1, MEMF_ANY))
	{
		if (client->comment) FreeMem(client->comment, strlen(client->comment)+1);
		strcpy(pcTemp, szComment);
		client->comment = pcTemp;
	}
}




/* Allocate a new Client structure and initialize its fields correctly,
   or return NULL on failure. */
struct Client * NewClient(char * szHostName, char * szOptComment)
{
	struct Client * client = NULL;
	
	UNLESS(szOptComment) szOptComment = "";
	UNLESS((client 		       = AllocMem(sizeof(struct Client),  MEMF_CLEAR))
	    && (client->hostname       = AllocMem(strlen(szHostName)+1,   MEMF_ANY))
	    && (strcpy(client->hostname, szHostName))
	    && (client->comment        = AllocMem(strlen(szOptComment)+1, MEMF_ANY))
	    && (strcpy(client->comment,  szOptComment)))
	{	
		FreeClient(client);	/* cleanup any partial allocations */
		return(NULL);
	}
	return(client);
}
		

/* Frees the given client structure.  Safe to use even if
   not all fields were allocated */
void FreeClient(struct Client * client)
{		
	UNLESS(client) return;
	if (client->hostname) FreeMem(client->hostname, strlen(client->hostname)+1);
	if (client->comment)  FreeMem(client->comment,  strlen(client->comment)+1);
	FreeMem(client, sizeof(struct Client));
}		

	



/* Server's output to stdout for the local user to see. */
void PrintClientList(void)
{
	struct Client * client = (struct Client *) ClientList->lh_Head;
	int nTemp = 0;
	time_t tTime = time(NULL);
	
	printf("%c Num | Age | Hostname                  | Comment\n",12);
	  printf("-----+-----+---------------------------+----------------------\n");
	while (client->node.ln_Succ)
	{
		printf("#%03i | %03lu | %-25s | %s\n", 
			nTemp,tTime - client->tDateStamp, 
			client->hostname,client->comment);
		client = client->node.ln_Succ;
		nTemp++;
	}
	putchar('\n');
}



/* Setup the server's UDP socket to receive packets */
/* Returns the socket file descriptor or -1 on failure. */
LONG CreateUDPSocket(int nPort)
{
	LONG sS;
	
	if ((sS = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
	{
		printf("Couldn't create UDP server socket.\n");
		return(-1L);
	}
    	bzero(&saUDPSocket, sizeof(saUDPSocket));
	
	saUDPSocket.sin_family = AF_INET;
	saUDPSocket.sin_addr.s_addr = htonl(INADDR_ANY);	/* Accept UDP packets from any host! */
  	saUDPSocket.sin_port = htons(nPort);
  	
	if (bind(sS, (struct sockaddr *) &saUDPSocket, sizeof(saUDPSocket)) != 0)
	{
		printf("Couldn't bind UDP socket (port %i already in use?)\n", nPort);
		CloseSocket(sS);
		return(-1L);
	}
	return(sS);
}





/* Setup the server's TCP socket to receive connection requests. */
/* Returns the socket file descriptor or -1 on failure. */
LONG CreateTCPSocket(int nPort)
{
	LONG sS;
	
	if ((sS = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		printf("Couldn't create TCP server socket.\n");
		return(-1L);
	}

    	bzero(&saTCPSocket, sizeof(struct sockaddr_in));
	saTCPSocket.sin_family = AF_INET;
	saTCPSocket.sin_addr.s_addr = htonl(INADDR_ANY);	/* Accept TCP connections from any host! */
	saTCPSocket.sin_port = htons(nPort);

	if (bind(sS, (struct sockaddr *) &saTCPSocket, sizeof(saTCPSocket)) != 0)
	{
		printf("Couldn't bind TCP Client socket (port %i already in use?)\n", nPort);
		CloseSocket(sS);
		return(-1L);
	}
	listen(sS, 5);	
	return(sS);
}



/* Wait for incoming packets or other relevant events */
/* Returns a chord of event codes, or zero if the application
   should quit immediately. */
ULONG TrackWait(ULONG ulMask)
{
	struct fd_set fsReadSet;
	int nCode = 0, nMaxPort;
	
	FD_ZERO(&fsReadSet);  			/* Initialize socket read set */
	if (sUDPSocket >= 0L) FD_SET(sUDPSocket, &fsReadSet);		
	if (sTCPSocket >= 0L) FD_SET(sTCPSocket, &fsReadSet);		
		
	nMaxPort = (sTCPSocket > sUDPSocket) ? sTCPSocket : sUDPSocket;
	
	/* Wait for Data to come in */
	/*         socket#     read        write        oob  timeout  other */
	WaitSelect(nMaxPort+1, &fsReadSet,  NULL,        NULL, NULL,   &ulMask);
	
	if (FD_ISSET(sUDPSocket, &fsReadSet))  nCode |= CODE_UDP_SOCKET;
	if (FD_ISSET(sTCPSocket, &fsReadSet))  nCode |= CODE_TCP_SOCKET;
	if (ulMask & (1L<<TimerMP->mp_SigBit)) nCode |= CODE_TIMER_EXPIRED; 
	if (ulMask & SIGBREAKF_CTRL_D) 	       nCode |= CODE_REFRESH;
	
	return((ulMask & SIGBREAKF_CTRL_C) ? 0 : nCode);
}





/* Act like a client.  Connect to szHostName, register yourself
   with this Amiga's IP address, and the given szComment. */
void BeAClient(char * szHostName, char * szComment)
{
	int nCode, nNumErrors = 0;
	
	if ((sUDPSocket = CreateUDPSocket(CLIENT_UDP_PORT)) == -1L) return; 
	UNLESS(ConnectToServer(szHostName)) return;
	SendPacket(REQ_COMMENT, szComment);
	BSendGoodbye = TRUE;

	printf("\nLogged on to [%s].\nYour comment is: [%s]\nPress CTRL_C to log off.\n\n", szHostName, szComment);
	SetTimer(ulPurgeDelay/2, 0);
	while(nCode = TrackWait(SIGBREAKF_CTRL_C | (1L<<TimerMP->mp_SigBit)))
	{
		if (SendPacket(REQ_COMMENT, szComment)) nNumErrors = 0;
		else 
		{
			if (nNumErrors++ > 5)
			{
				printf("Error sending packets.  (Server down?)\n");
				break;
			}
		}
		SetTimer(ulPurgeDelay/2, 0);	/* Send a refresh packet every 1/2 timeout period! */
	}
}




/* Sends a UDP packet to the server.  The first byte
   of the packet is a control byte, after that there
   is just the NULL-terminated string szComment. */
BOOL SendPacket(ULONG ulReqs, char * szComment)
{
	int nBufLen = sizeof(ULONG) + strlen(szComment) + 1;
	char * pcBuf;
	BOOL BRet;
	
	/* Allocate and fill out the transfer buffer */
	UNLESS(pcBuf = (char *)AllocMem(nBufLen, MEMF_ANY)) return(FALSE);
	memcpy(&pcBuf[0], &ulReqs, sizeof(ULONG));
	strcpy(&pcBuf[sizeof(ULONG)], szComment);
	BRet = (send(sUDPSocket, (UBYTE *)pcBuf, nBufLen, 0L) == nBufLen);
	FreeMem(pcBuf, nBufLen);
	return(BRet);
}



/* Used by the client.  Note that this connect() call probably 
   won't fail even if the server IS running on the host named
   szPeerName, because UDP sockets are connectionless, and connect()
   does nothing but affect an AmiTCP-internal setting that governs
   where send() will send to when used on this socket.  Rather,
   if something goes wrong, subsequent send() calls will return
   errors instead. */
BOOL ConnectToServer(char * szPeerName)
{
	struct hostent * hp;
	static struct sockaddr_in saServer;

	UNLESS(hp = gethostbyname(szPeerName))
	{
		printf("Name lookup failed for server [%s]\n",szPeerName);
		return(FALSE);
	}
	bzero(&saServer, sizeof(struct sockaddr_in));
    	bcopy(hp->h_addr, (char *)&saServer.sin_addr, hp->h_length);
	saServer.sin_port   = htons(SERVER_UDP_PORT);
	saServer.sin_family = hp->h_addrtype;
	UNLESS(connect(sUDPSocket, (struct sockaddr *) &saServer, sizeof(struct sockaddr_in)) >= 0)
	{
		printf("Connect to [%s] failed.\n",szPeerName);
		return(FALSE);
	}
	return(TRUE);
}