/*
 * xforward.c
 *
 * Program that acts as a x relay
 */

/*              Copyright 1992, 1993 Digital Equipment Corporation
 *                        All Rights Reserved
 *
 * Permission to use, copy, and modify this software and its documentation is
 * hereby granted only under the following terms and conditions.  Both the
 * above copyright notice and this permission notice must appear in all copies
 * of the software, derivative works or modified versions, and any portions
 * thereof, and both notices must appear in supporting documentation.
 *
 * Users of this software agree to the terms and conditions set forth herein,
 * and hereby grant back to Digital a non-exclusive, unrestricted, royalty-free
 * right and license under any changes, enhancements or extensions made to the
 * core functions of the software, including but not limited to those affording
 * compatibility with other hardware or software environments, but excluding
 * applications which incorporate this software.  Users further agree to use
 * their best efforts to return to Digital any such changes, enhancements or
 * extensions that they make and inform Digital of noteworthy uses of this
 * software.  Correspondence should be provided to Digital at:
 *
 *                      Director of Licensing
 *                      Cambridge Research Laboratory
 *                      Digital Equipment Corporation
 *                      One Kendall Square, Bld. 700
 *                      Cambridge, MA 02139  
 *
 * This software may be distributed (but not offered for sale or transferred 
 * for compensation except on systems manufactured by Digital Equipment 
 * Corporation) to third parties, provided such third parties agree to abide 
 * by the terms and conditions of this notice.  
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND DIGITAL EQUIPMENT CORP. DISCLAIMS ALL
 * WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS.   IN NO EVENT SHALL DIGITAL EQUIPMENT
 * CORPORATION 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.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <netdb.h>
#include <signal.h>
#include <errno.h>
#include <sys/param.h>

#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Intrinsic.h>
#include <X11/StringDefs.h>
#include <Xm/Xm.h>
#include <Xm/MessageB.h>

extern int errno;

#define min(a,b) ((a) < (b) ? (a) : (b))
#define max(a,b) ((a) > (b) ? (a) : (b))

#define TRUE 1
#define FALSE 0

#define CANCEL                  4
#define OK                      7

void usage(), doxmit(), readwrite();

void sigchld();

void alert_user_CB();

struct sockaddr_in forward_to;

struct pend_conn {
    pid_t child;
    int newsock;
    int done;
    struct pend_conn *next;
};

struct rwbuf {
    char readbuffer[BUFSIZ+5];
    char writebuffer[BUFSIZ+5];
    int rbytes;				/* #bytes active in readbuffer */
    int wbytes;				/* #bytes active in wbytes */
    int connto;				/* fd this one should be connected to */
    int wclose;				/* if set, close after finished writing */
};

fd_set rinit, winit;

int net16;

int AlertResponse = 0;
 
struct rwbuf *rwbuf;

main(argc, argv)
int argc;
char *argv[];
{
    struct hostent *hp;
    char myhostname[MAXHOSTNAMELEN];
    struct sockaddr_in local, dummy;
    unsigned int okhosts[16];
    char *okhost_names[16];
    unsigned int nokhosts = 0;
    unsigned short port;
    register int i;
    int lsock;
    int dummylen;
    int one = 1;
    int nready, nfds;
    fd_set readable, writable;
    int dtsize;
    Display *display;
    int nhosts;
    Bool state;
    char *hname;
    size_t len;
    pid_t child_pid;
    struct pend_conn *pend_head = NULL;
    int tcount = 0;
    char *disp_str = NULL;
    int terse = 0;

    if (argc < 3)
	usage();

    /* Get the current hostname, and make sure it's fully qualifed.*/
    gethostname(myhostname, MAXHOSTNAMELEN);
    if ((hp = gethostbyname(myhostname)) == NULL) {
	    fprintf(stderr, "Can't lookup up my own hostname %s\n",myhostname);
	    exit(1);
    }
    strcpy(myhostname, hp->h_name);

    for ( i = 1; i < argc; i++) {
	if (strcmp(argv[i],"-display") == 0) {
	    disp_str = argv[i+1];
	    i++;
        } else if (strcmp(argv[i], "-terse") == 0) {
            terse = 1;
	} else if (strcmp(argv[i],"-allow") == 0) {
	    do {
		if ((hp = gethostbyname(argv[i+1])) == NULL) {
		    fprintf(stderr, "no such host %s\n",argv[i+1]);
		    exit(1);
		}	
  	        len = strlen(argv[i+1]);
	        okhost_names[nokhosts] = malloc(len + 1);
	        strcpy(okhost_names[nokhosts],argv[i+1]);
                bcopy(hp->h_addr, (char *)&okhosts[nokhosts], 4);
		nokhosts++;
		i++;
	    } while (argv[i+1] != NULL && argv[i+1][0] != '-');
	} else {
	    usage();
	}
    }

    /* if not set in argument list, get DISPLAY from environment variable */
    if (disp_str == NULL) {
	disp_str = getenv("DISPLAY");
	if (disp_str == NULL) {
	    fprintf(stderr,"display not specified\n");
	    exit(1);
	}
    }
    len = strcspn(disp_str,":");
    hname = malloc(len+1); /* allocate space, including null */
    (void)strncpy(hname,disp_str,len);
    hname[len] = '\0';
    hp = gethostbyname(hname);
    if (!hp) {
	fprintf(stderr, "no such host %s\n",hname);
	exit(1);
    }
    bzero(&local, sizeof(local));
    bzero(&forward_to, sizeof(forward_to));

    port = 6000;
    local.sin_port = htons(port);
    local.sin_family = AF_INET;
    /* assign a local display */
    if ((lsock = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
	perror("socket");
	exit(1);
    }
    if (setsockopt(lsock, SOL_SOCKET, SO_REUSEADDR,
		   &one, sizeof(one)) < 0) {
	perror("SO_REUSEADDR:");
	exit(1);
    }
    while ( bind(lsock, (struct sockaddr *)&local, sizeof(local)) < 0) {
	if (errno == EADDRINUSE) {
	    port++;
	    if (port > 6100) {
		fprintf(stderr,"all ports in use.\n");
		exit(1);
	    }
	    local.sin_port = htons(port);
	} else {
	    perror("bind");
	    exit(1);
	}
    }

    if (listen(lsock, SOMAXCONN) < 0) {
	perror("listen");
	exit(1);
    }

    if (!terse)
       printf("display is ");
    printf("%s:%d\n", myhostname, port - 6000);
    fflush(stdout);

    port = atoi(disp_str+len+1);
    if (port < 0) {
	fprintf(stderr, "display must be a non-negative integer!\n");
	exit(1);
    }
    port += 6000;
    if (port < 6000 || port > 6100) {
	fprintf(stderr, "display must be >= X11.0 and <= X11.100\n");
	exit(1);
    }

    if (nokhosts == 0) {
	fprintf(stderr, "must specify at least one allowed host.\n");
	exit(1);
    }

    forward_to.sin_port = htons(port);
    forward_to.sin_family = AF_INET;
    bcopy(hp->h_addr, &forward_to.sin_addr, sizeof(forward_to.sin_addr));

    /* Check that we can open the destination display. */

    if ( (display = XOpenDisplay(disp_str)) == NULL) {
	fprintf(stderr,"Unable to open display at destination.\n");
	exit(1);
    }

    if ((ntohl(forward_to.sin_addr.s_addr) & 0xff000000) == 0x10) {
	/* net 16 */
	net16 = 1;
    } else {
	net16 = 0;
    }

    /* clean up to conserve descriptors */
    close(0);
    close(1);
    /* close(2); -- leave open for stderr */

    signal(SIGPIPE, SIG_IGN);

    dtsize = getdtablesize();

    /* set up buffers & sizes */
    rwbuf = (struct rwbuf *)calloc(dtsize, sizeof *rwbuf);
    if (!rwbuf) {
	fprintf(stderr, "can't allocate buffers\n");
	exit(1);
    }
    /* -1 means not connected/invalid */
    while (dtsize--) {
	rwbuf[dtsize].connto = -1;
    }

    FD_ZERO(&rinit);
    FD_ZERO(&winit);
    FD_SET(lsock, &rinit);
    
    nfds = lsock + 1;
    while (1) {
	struct timeval timeout;
	struct pend_conn *cur,*prev;
	pid_t temppid;
	union wait wstatus;
	int exitstat,wopts,newoutgoing;

	/* for each entry, call waitpid */
	for (cur = pend_head; cur != NULL; cur=cur->next ) {
	    wopts = WNOHANG;
	    temppid = waitpid(cur->child,&wstatus,wopts);
	    /* if there's no status, try next one */
	    if (temppid == 0) {
		continue;
	    }
	    if (WIFSIGNALED(wstatus)) {
		fprintf(stderr,"child died abnormally\n");
		cur->done = TRUE;
		continue;
	    }
	    if (WIFEXITED(wstatus)) {
		exitstat = WEXITSTATUS(wstatus);
		if (exitstat == OK) {
		    if ((newoutgoing = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
			perror("socket");
			close(cur->newsock);
			cur->done = TRUE;
			continue;
		    }
		    if (connect(newoutgoing, (struct sockaddr *)&forward_to,
				sizeof(forward_to)) < 0) {
			perror("connect");
			close(newoutgoing);
			close(cur->newsock);
			cur->done = TRUE;
			continue;
		    }
		    
		    nfds = max(nfds, (cur->newsock)+1);
		    nfds = max(nfds, newoutgoing+1);
		    
		    rwbuf[(cur->newsock)].connto = newoutgoing;
		    rwbuf[newoutgoing].connto = (cur->newsock);
		    
		    rwbuf[newoutgoing].rbytes = rwbuf[(cur->newsock)].rbytes = 0;
		    rwbuf[newoutgoing].wbytes = rwbuf[(cur->newsock)].wbytes = 0;
		    FD_SET(cur->newsock, &rinit);
		    FD_SET(cur->newsock, &winit);
		    FD_SET(newoutgoing, &rinit);
		    FD_SET(newoutgoing, &winit);
		    cur->done = TRUE;
		} else if (exitstat == CANCEL) {
		    close(cur->newsock);
		    cur->done = TRUE;
		} else {
		    fprintf(stderr,"Unknown exit status of child\n");
		    close(cur->newsock);
		    cur->done = TRUE;
		}
	    }
	}
	/* clean up list */
	prev = NULL;
	cur = pend_head;
	while (cur != NULL) {
	    if (cur->done == TRUE) {
		if (prev == NULL) {
		    pend_head = cur->next;
		    free(cur);
		    cur = pend_head;
		} else {
		    prev->next = cur->next;
		    free(cur);
		    cur = prev->next;
		}
	    } else {
		prev = cur;
		cur = cur->next;
	    }
	}

	for (i = 0; i < nfds; i++)
	    if (rwbuf[i].wclose)
		FD_SET(i, &winit);

	timeout.tv_usec = 0;
	timeout.tv_sec = 3;

	readable = rinit;
	writable = winit;
	if ((nready = select(nfds, &readable, &writable, 0, &timeout)) == -1) {
	    if (errno == EINTR)
		continue;
	    perror("select");
	    exit(1);
	}
	if (nready == 0) {
	    /* each increment of tcount represents 3 seconds of idleness */
	    tcount++;
	    /* 1 hr 30 min timeout */
	    if (tcount > 1800) {
		fprintf(stderr, "connections timed out\n");
	        exit(1);
	    }
	} else {
	    /* reset timeout counter if there is some activity */
	    tcount = 0;
	}

	for (i = 0; i < nfds && nready; i++) {
	    /* loop through descriptors */
	    if (FD_ISSET(i, &writable)) {
		int cc, leftover, connto;
		connto = rwbuf[i].connto;
		nready--;
		/* write what we can */
		if (rwbuf[i].wbytes) {
		    cc = write(i, rwbuf[i].writebuffer, rwbuf[i].wbytes);
		    if (cc == -1) {
			if (errno != EPIPE) {
			    fprintf(stderr, "fd %d:", i);
			    perror("write");
			}
			/* asynchrony on close probs? */
			FD_CLR(i, &winit);
			FD_CLR(i, &rinit);
			close(i);
			if (connto != -1) {
			    FD_CLR(connto, &rinit);
			    FD_CLR(connto, &winit);
			    close(rwbuf[i].connto);
			}
			cc = rwbuf[i].connto;
			rwbuf[i].connto = rwbuf[cc].connto = -1;
			continue;
		    } else {
			leftover = rwbuf[i].wbytes - cc;
			if (leftover) {
			    /* didn't write it all */
			    /* copy down */
			    bcopy(rwbuf[i].writebuffer + cc,
				  rwbuf[i].writebuffer,
				  leftover);
			    rwbuf[i].wbytes = leftover;
			} else
			    rwbuf[i].wbytes = 0; /* buffer empty */
			if (connto != -1 && rwbuf[connto].rbytes) {
			    /* more stuff to copy in */
			    copyfromto(connto, i);
			}
			if (!rwbuf[i].wbytes) {
			    /* nothing left to write */
			    if (rwbuf[i].wclose) {
				/* close after flushing */
				FD_CLR(i, &rinit);
				close(i);
				rwbuf[i].connto = -1;
				rwbuf[i].wclose = 0;
			    }
			    FD_CLR(i, &winit);
			}
			if (connto != -1)
			/* since we wrote some, go look for more */
			    FD_SET(connto, &rinit);
		    }
		} else {
		    if (connto != -1 && rwbuf[connto].rbytes) {
			    /* more stuff to copy in */
			    copyfromto(connto, i);
			    if (rwbuf[i].wbytes)
				continue;
		    }
		    /* nothing to write at the moment */
		    if (rwbuf[i].wclose) {
			/* close after flushing */
			FD_CLR(i, &rinit);
			close(i);
			rwbuf[i].connto = -1;
			rwbuf[i].wclose = 0;
		    }
		    /* nothing to write, so clear */
		    FD_CLR(i, &winit);
		}
	    }
	    if (FD_ISSET(i, &readable)) {
		nready--;
		/* something is readable */
		if (i == lsock) {
		    int newsock, i;
		    struct pend_conn *npc;
		    Display *dpy;
		    /* new connection ready */
		    dummylen = sizeof(dummy);
		    if ((newsock = accept(lsock, (struct sockaddr *)&dummy,
					  &dummylen)) < 0) {
			if (errno == EINTR)
			    continue;
			perror("accept");
			exit(1);
		    }
		    for (i = 0; i < nokhosts; i++) {
			if (dummy.sin_addr.s_addr == okhosts[i])
			    break;
		    }
		    if (i == nokhosts) {
			hp = gethostbyaddr((char *) &dummy.sin_addr,
					   sizeof(dummy.sin_addr),
					   dummy.sin_family);
			fprintf(stderr,
				"bad host connect from %s (%s)\n",
				hp->h_name,
				inet_ntoa(dummy.sin_addr.s_addr));
			close(newsock);
			continue;
		    }
		    /* after check of allowed hosts, create popup on
		     * destination
		     */
		    /* fork here */
		    child_pid = fork();
		    if ( child_pid < 0) {
			fprintf(stderr,"Fork of child failed.\n");
			exit(1);
		    }
		    if ( child_pid == 0 ) {
			/* we're the child, so create pop-up */
			int n=0;
			Arg arg[4];
			XtAppContext app_con;
			Widget topshell,alert,help;
			char dialog_message[200];
			XmString msg_str,msg_str2,msg_str3;

			XtToolkitInitialize();
			app_con = XtCreateApplicationContext();
			dpy = XtOpenDisplay(app_con,disp_str,NULL,"Xforward",
					    NULL,0,&argc,argv);
			topshell = XtAppCreateShell(NULL,"Xforward",applicationShellWidgetClass, dpy, NULL,NULL);
			sprintf(dialog_message,"Allow X connection from %s ?",
				okhost_names[i]);
			msg_str = XmStringCreateSimple(dialog_message);
			XtSetArg(arg[n],XmNmessageString,msg_str); n++;
			msg_str2 = XmStringCreateSimple("Yes");
			XtSetArg(arg[n],XmNokLabelString,msg_str2); n++;
			msg_str3 = XmStringCreateSimple("No");
			XtSetArg(arg[n],XmNcancelLabelString,msg_str3); n++;
			XtSetArg(arg[n],XmNdefaultButtonType,XmDIALOG_CANCEL_BUTTON); n++;
			alert = XmCreateWarningDialog(topshell,"alert",arg,n);
			XmStringFree(msg_str); XmStringFree(msg_str2); XmStringFree(msg_str3);
			XtAddCallback(alert,XmNokCallback,alert_user_CB,(caddr_t) OK);
			XtAddCallback(alert,XmNcancelCallback,alert_user_CB,(caddr_t) CANCEL);
			help = XmMessageBoxGetChild(alert,XmDIALOG_HELP_BUTTON);
			XtUnmanageChild(help);
			AlertResponse = -1;
			XtManageChild(alert);
			while(AlertResponse == -1)
			{
			    XEvent event;
			    XtAppNextEvent(app_con,&event);
			    XtDispatchEvent(&event);
			}
			/* use value of AlertResponse as exit code */
			exit(AlertResponse);
		    }
		    /* add child to list of procs that parent must check
		       exit status of */
		    npc = (struct pend_conn *)malloc(sizeof(struct pend_conn));
		    npc->child = child_pid;
		    npc->newsock = newsock;
		    npc->done = FALSE;
		    npc->next = NULL;
		    /* add npc to beginning of list */
		    if (pend_head == NULL) {
			pend_head = npc;
		    } else {
			npc->next = pend_head;
			pend_head = npc;
		    }
		} else {
		    int cc, connto;
		    connto = rwbuf[i].connto;
		    /* normal fd is readable */
		    if (rwbuf[i].rbytes < BUFSIZ) {
			/* read what we have room for */
			cc = read(i, rwbuf[i].readbuffer + rwbuf[i].rbytes,
				  BUFSIZ-rwbuf[i].rbytes);
			if (cc == -1) {
			    fprintf(stderr, "fd %d:", i);
			    perror("read");
			    /* asynchrony on close probs? */
			    FD_CLR(i, &winit);
			    FD_CLR(i, &rinit);
			    FD_CLR(connto, &rinit);
			    FD_CLR(connto, &winit);
			    close(i);
			    close(connto);
			    rwbuf[i].connto = rwbuf[connto].connto = -1;
			    continue;
			} else if (cc == 0) {
			    /* closedown */
			    FD_CLR(i, &rinit);
			    FD_CLR(i, &winit);
			    close(i);
			    if (connto != -1) {
				/* set close after finishing write */
				rwbuf[connto].wclose = 1;
				rwbuf[connto].connto = -1;
				/* force a write cycle to clean up */
				FD_SET(connto, &winit);
			    }
			    rwbuf[i].rbytes = 0;
			    rwbuf[i].wbytes = 0;
			    rwbuf[i].connto = -1;
			    /* XXX what else */
			} else {
			    rwbuf[i].rbytes += cc;
			    /* try to put onto write buffer */

			    if (connto != -1)
				copyfromto(i, connto);
			    if (rwbuf[i].rbytes >= BUFSIZ) {
				/* buffer is full */
				FD_CLR(i, &rinit);
			    }
			}
		    }
		}
	    } 
	} /* for loop through descriptors */
    } /* while (1) */
}

void
usage()
{
    fprintf(stderr, "usage: xforward [-terse] [-display dispname] -allow host1 [host2 ... host16]\n");
    exit(1);
}

copyfromto(from, to)
int from, to;
{
    int ncopy;
    if (rwbuf[to].wbytes < BUFSIZ) {
	ncopy = min(rwbuf[from].rbytes,
		    BUFSIZ-rwbuf[to].wbytes);
	
	bcopy(rwbuf[from].readbuffer,
	      rwbuf[to].writebuffer + rwbuf[to].wbytes,
	      ncopy);
	rwbuf[to].wbytes += ncopy;
	FD_SET(to, &winit);
	if (ncopy == rwbuf[from].rbytes)
	    rwbuf[from].rbytes = 0;
	else {
	    bcopy(rwbuf[from].readbuffer + ncopy,
		  rwbuf[from].readbuffer,
		  rwbuf[from].rbytes - ncopy);
	    rwbuf[from].rbytes -= ncopy;
	}
	/* we have room */
	FD_SET(to, &rinit);
    }
}

/* ARGSUSED */
void alert_user_CB(w,user_data,call_data)
Widget w;
caddr_t user_data, call_data;
{
	AlertResponse = (int) user_data;
}
