/* standalone discard and other services
   2000.10.16 Rafal Maszkowski <rzm@icm.edu.pl>
   based on Richard Stevens unpv12e/tcpcliserv/tcpserv09.c

   Adding a new service: see MODE_TEMPLATE

   TODO:
   - binding a specific IPv[46] local address
*/

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <errno.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>	/* for Solaris */
#include "compat.h"

/* Following could be derived from SOMAXCONN in <sys/socket.h>, but many
   kernels still #define it as 5, while actually supporting many more */
#define LISTENQ		1024	/* 2nd argument to listen() */

/* Following shortens all the type casts of pointer arguments */
#define	SA	struct sockaddr

/* Put the program in the background, and dissociate from the controlling
   terminal.  If NOCHDIR is zero, do `chdir ("/")'.  If NOCLOSE is zero,
   redirects stdin, stdout, and stderr to /dev/null.  */
/* extern int daemon __P ((int __nochdir, int __noclose)); */

#define	MODE_ECHO	1
#define	MODE_DISCARD	2
#define	MODE_CHARGEN	3
#define	MODE_TEMPLATE	999

#define	CHARGEN_START	32
#define	CHARGEN_STOP	126
#define	CHARGEN_WIDTH	72

int	defports[] = { 0, 7, 9, 19, 0 };

int	opt_proto = SOCK_STREAM;

void
process(int fdin, int fdout, int opt_mode) {
	int	bytes, ch, ind;
	char	buf[4096];

	switch(opt_mode) {
	case MODE_ECHO:		for ( ; ; ) {
			if ((bytes = read(fdin, buf, sizeof(buf))) <= 0) break;
			if ((bytes = write(fdin, buf, bytes)) <= 0) break;
		}
		break;

	case MODE_CHARGEN:	
		for (ch = CHARGEN_START; ch<=CHARGEN_STOP; ch++) buf[ch-CHARGEN_START] = ch;
		bcopy(buf, buf+CHARGEN_STOP-CHARGEN_START+1, CHARGEN_STOP-CHARGEN_START+1);
		for (ind = 0; ; ind++) {
			if ((bytes = write(fdout, buf+ind, CHARGEN_WIDTH)) <= 0) break;
			if ((bytes = write(fdout, "\r\n", 2)) <= 0) break;
			if (ind>CHARGEN_STOP-CHARGEN_START) ind = 0;
		}
		break;

	case MODE_TEMPLATE:	
		strncpy(buf, "Just a template for new services.\r\n", sizeof(buf));
		write(fdout, buf, strlen(buf));
		break;

	default: /* MODE_DISCARD */	for ( ; ; ) {
			if ((bytes = read(fdin, buf, sizeof(buf))) <= 0) break;
		}
	}
}

void
sig_chld(int signo) {
	pid_t	pid;
	int	stat;

	while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0) {
		/* printf("child %d terminated\n", pid); */
	}
	return;
}

int
name2mode(char *name) {
	if (strncmp(name, "discard",  8)==0) return MODE_DISCARD;
	if (strncmp(name, "echo",     5)==0) return MODE_ECHO;
	if (strncmp(name, "chargen",  8)==0) return MODE_CHARGEN;
	if (strncmp(name, "template", 9)==0) return MODE_TEMPLATE;
	return -1;
}

void
usage(char *name) {
	fprintf(stderr, "\n");
	fprintf(stderr, "Usage: %s [options]\n\n", name);
	fprintf(stderr, "%s is a standalone echo/discard/chargen service.\n", name);
	fprintf(stderr, "\n");
	fprintf(stderr, "Options:\n");
	fprintf(stderr, "\n");
#ifdef HAVE_GETADDRINFO
        fprintf(stderr, "-4, --ipv4         bind ipv4 address (default)\n");
        fprintf(stderr, "-6, --ipv6         bind ipv6 address (means: both?)\n");
#endif /* HAVE_GETADDRINFO */
	fprintf(stderr, "-f, --foreground   do not be a daemon (ignored with -i)\n");
	fprintf(stderr, "-h, --help         this help\n");
	fprintf(stderr, "-i, --inetd        runs as an inetd service\n");
	fprintf(stderr, "-m MODE            MODE can be echo, discard or chargen (default: %s)\n", name);
	fprintf(stderr, "-p PORT            listen on PORT instead of default 7/9/19\n");
        fprintf(stderr, "-t, --tcp          use TCP%s\n", opt_proto==SOCK_STREAM ? " (default)" : "" );
        fprintf(stderr, "-u, --udp          use UDP%s\n", opt_proto==SOCK_DGRAM  ? " (default)" : "" );
	exit(0);
}

void usage_small(char *name) {
	fprintf(stderr, "type %s --help for help\n", name);
	exit(0);
}

int
main(int argc, char **argv)
{
	int			listenfd, connfd, opt_fg = 0, opt_inetd = 0,
		opt_port = -1, opt_mode = MODE_DISCARD, on = 1, opt_family = AF_INET;
	pid_t			childpid;
	void			sig_chld(int);
	char			fname[100], *name = NULL;

	/* changing default, based on program name */
	strncpy(fname, argv[0], sizeof(fname));
	if ( (name = rindex(fname, '/')) ) {
		name++;
	} else {
		name = fname;
	}
	opt_mode = name2mode(name);

	while (1) {
		static const struct option long_options[] = {
			{ "foreground", no_argument,		NULL, 'f' },
			{ "help",	no_argument,		NULL, 'h' },
			{ "inetd",	no_argument,		NULL, 'i' },
			{ "ipv4",	no_argument,		NULL, '4' },
			{ "ipv6",	no_argument,		NULL, '6' },
			{ "mode",	required_argument,	NULL, 'm' },
			{ "tcp",	no_argument,		NULL, 't' },
			{ "udp",	no_argument,		NULL, 'u' },
			{ NULL,		0,			NULL, 0 } };
		int     optchar;

		if ((optchar = getopt_long (argc, argv, "46fhim:p:tu", long_options, NULL)) == -1) break;
		switch (optchar) {
#ifdef HAVE_GETADDRINFO
			case '4':	opt_family = AF_INET;		break;
			case '6':	opt_family = AF_INET6;		break;
#endif /* HAVE_GETADDRINFO */
			case 'f':       opt_fg = 1;			break;
			case 'h':       usage(name);			break;
			case 'i':       opt_inetd = 1;			break;
			case 'm':	if ( (opt_mode = name2mode(optarg)) == -1 ) { printf("Unknown mode\n"); exit(1); }
									break;
			case 'p': opt_port = atoi(optarg);
				if ((opt_port < 0) || (opt_port > 65535)) {
					printf("Local port must be in 0..65535 range\n"); exit(1);
				}					break;
			case 't':	opt_proto = SOCK_STREAM;	break;
			case 'u':	opt_proto = SOCK_DGRAM; 	break;
			default:	usage_small(name);
		}
	}

        if (opt_port == -1) opt_port = defports[opt_mode];

	if (opt_inetd) {
		process(STDIN_FILENO, STDOUT_FILENO, opt_mode);
		exit(0);
	}

	if (!opt_fg) daemon(0, 1);

	if ( (listenfd = socket(opt_family, opt_proto, 0)) == -1 ) {
		printf("socket(%d): %s\n", opt_family, strerror(errno)); exit(1);
	}

	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on));

	if (opt_family == AF_INET) {
		struct sockaddr_in	servaddr;
		bzero(&servaddr, sizeof(servaddr));
		servaddr.sin_family      = opt_family;
		servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
		servaddr.sin_port        = htons(opt_port);
		if (bind(listenfd, (SA *) &servaddr, sizeof(servaddr)) == -1) {
			printf("bind(%d): %s\n", opt_family, strerror(errno)); exit(1);
		}
#ifdef HAVE_GETADDRINFO
	} else {
		struct sockaddr_in6	servaddr;
		bzero(&servaddr, sizeof(servaddr));
		servaddr.sin6_family = opt_family;
		servaddr.sin6_flowinfo = 0;
		servaddr.sin6_port = htons(opt_port);
		servaddr.sin6_addr = in6addr_any;  /* structure assignment */
		if (bind(listenfd, (SA *) &servaddr, sizeof(servaddr)) == -1) {
			printf("bind(%d): %s\n", opt_family, strerror(errno)); exit(1);
		}
#endif /* HAVE_GETADDRINFO */
	}

	listen(listenfd, LISTENQ);

	signal(SIGCHLD, sig_chld);

	for ( ; ; ) {
	socklen_t		clilen;
	struct sockaddr_in	cliaddr;
		clilen = sizeof(cliaddr);
		if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
			if (errno == EINTR) continue;		/* back to for() */
			else strerror(errno);
		}

		if ( (childpid = fork()) == 0) {	/* child process */
			close(listenfd);	/* close listening socket */
			process(connfd, connfd, opt_mode);	/* process the request */
			exit(0);
		}
		close(connfd);			/* parent closes connected socket */
	}

	return 0;
}
