#include <stdio.h>
#include <ctype.h>
#include <fcntl.h>
#include <termio.h>
#include <signal.h>
#include <sys/time.h>

/*
 * MODEM -- Kind of like CU.  Connects your terminal to the modem port.
 */

#define	TERM		"/dev/tty"	/* user terminal device */
#define PORT		"/dev/tty0"	/* default to modem port */
#define	BAUD		2400		/* default baud rate */
#define	ESCAPE		'\001'		/* escape character */

#define	SZ_OBUF		4096		/* max output data to buffer */
#define	SZ_LINE		1024		/* max size line */
#define	SZ_DBUF		128		/* size of a data buffer for copies */
#define	SZ_FNAME	256		/* max size filename */
#define	NLWAIT		200		/* pause, msec/line, for copies */
#define	IOWAIT		50		/* delay between reads, heavy i/o */
#define	CMDWAIT		1000		/* wait after command, msec */
#define	EOFWAIT		200		/* wait after sending EOF, msec */
#define	NFAST		8		/* number of "fast" reads after type */
#define	TOP		128		/* flush output every TOP bytes  */

extern	int	errno;
static	int	rfd = 0;
static	int	debug = 0;
static	int	watch = 0;
static	int	top = TOP;
static	int	nonblock = 0;
static	int	keepalive = 1;
static	int	binary = 0;
static	int	dataline = 0;
static	int	baud = BAUD;
static	int	hwflow = 0;
static	char	cmd[SZ_LINE];
static	char	obuf[SZ_OBUF];
static	char	lbuf[SZ_LINE];
static	char	dbuf[SZ_DBUF];
static	char	port[SZ_FNAME];
static	char	lockfile[SZ_FNAME];
static	char	infile[SZ_FNAME];
static	char	outfile[SZ_FNAME];
static	char	initmsg[SZ_LINE];
static	char	exitmsg[SZ_LINE];
static	char	eofstr[2] = { 0x1a, 0 };
static	int	ringring, terminal, modem;
static	FILE	*logfile = NULL;


/* MODEM -- Open a virtual terminal connection to a modem port.  The port is
 * assumed to already be active, i.e., this program does not do any dialing.
 * On my system I just type "modem" and I am talking to the modem, or if I
 * have already dialed into a remote system, to the remote system.  One can
 * suspend or exit/restart the modem program without losing the connection.
 *
 * Usage:
 *
 *	modem [-b baud] [-p port] [-i initmsg] [-e exitmsg] [-data]
 *
 * Baud rates of up to 19200 are supported.  The default baud rate is 19200.
 * The escape character is ctrl/a.  The following escapes are recognized:
 *
 *	ctrl/a ctrl/a		exit
 *	ctrl/a ctrl/x		suspend
 *	ctrl/a ctrl/p		put ascii file (can lose data)
 *	ctrl/a ctrl/l		log [stop logging] output to a file
 *	ctrl/a ctrl/d		toggle debug output (to file "modem.io")
 *
 * Edit the program if you want a different default baud rate, escape char,
 * or modem port.
 */
main (argc, argv)
int	argc;
char	**argv;
{
	register char	*op, *otop;
	register int	fast;

	int	efd, bits, stat, arg, nchars, locked;
	int	kb, bytes, tflag, mflag, exitstatus;
	struct	termio tty, o_tty;
	struct	termio mty, o_mty;
	struct	timeval delay, idle;
	char	*argp, *ip, *cp;
	FILE	*db, *fp;
	char	ch;

	locked = 0;
	exitstatus = 0;
	initmsg[0] = '\0';
	exitmsg[0] = '\0';
	strcpy (port, PORT);

	/* Process the argument list. */
	for (arg=1;  arg < argc;  arg++)
	    if (argp = argv[arg]) {
		if (!strcmp (argp, "-d")) {		/* debug output */
		    debug++;
		} else if (!strcmp (argp, "-w")) {	/* local echo */
		    watch++;
		} else if (!strcmp (argp, "-nb")) {	/* nonblocking */
		    nonblock++;
		} else if (!strcmp (argp, "-hw")) {	/* hw control */
		    hwflow++;
		} else if (!strcmp (argp, "-b")
		        || !strcmp (argp, "-s")) {	/* baud rate */
		    if (argp = argv[++arg])
			baud = atoi(argp);
		} else if (!strcmp (argp, "-data")) {
		    dataline++;
		} else if (!strcmp (argp, "-p")) {	/* port */
		    if (argp = argv[++arg])
			if (argp[0] == '/')
			    strcpy (port, argp);
			else
			    sprintf (port, "/dev/%s", argp);
		} else if (!strcmp (argp, "-i")) {	/* initmsg */
		    if (argp = argv[++arg]) {
			for (ip=argp, op=initmsg;  *ip;  ip++) {
			    if (*ip == '\\') {
				switch (*(++ip)) {
				case 'n':
				    *op++ = '\n';
				    break;
				case 'r':
				    *op++ = '\r';
				    break;
				default:
				    *op++ = *ip;
				    break;
				}
			    } else
				*op++ = *ip;
			}
			*op = '\0';
		    }
		} else if (!strcmp (argp, "-e")) {	/* exitmsg */
		    if (argp = argv[++arg]) {
			for (ip=argp, op=exitmsg;  *ip;  ip++) {
			    if (*ip == '\\') {
				switch (*(++ip)) {
				case 'n':
				    *op++ = '\n';
				    break;
				case 'r':
				    *op++ = '\r';
				    break;
				default:
				    *op++ = *ip;
				    break;
				}
			    } else
				*op++ = *ip;
			}
			*op = '\0';
		    }
		} else if (!strcmp (argp, "-obuf")) {	/* output buf size */
		    if (argp = argv[++arg]) {
			top = atoi(argp);
			if (top > SZ_OBUF)
			    top = SZ_OBUF;
		    }
		}
	    }

	/* Check that the desired port is not locked. */
	for (ip=cp=port;  *ip;  ip++)
	    if (*ip == '/')
		cp = ip + 1;
	sprintf (lockfile, "/usr/spool/uucp/LCK..%s", cp);
	if (access (lockfile, 0) == 0) {
	    fprintf (stderr, "port %s is already in use\n", cp);
	    exitstatus = 1; goto abort;
	}

	/* Open the modem port. */
	errno = 0;
	if ((modem = open (port, 2)) < 0) {
	    fprintf (stderr, "cannot open %s\n", port);
	    exitstatus = 1; goto abort;
	} else {
	    /* Lockfile creation will fail unless the modem program is suid
	     * root or uucp or otherwise has write perm in spool/uucp. Ignore
	     * the error if we cannot create a lockfile.
	     */
	    int     fd;

	    if ((fd = creat (lockfile, 0666)) < 0)
		errno = 0;
	    else {
		locked++;
		close (fd);
	    }
	}

	/* Set raw mode no echo on modem port. */
	if (ioctl (modem, TCGETA, &o_mty) < 0) {
	    fprintf (stderr, "cannot read modem port status\n");
	    exitstatus = 2; goto abort;
	} else {
	    mty = o_mty;
	    if (dataline) {
		mty.c_iflag = IGNBRK;
		mty.c_oflag = 0;
		mty.c_cflag = (CS8|CREAD|HUPCL);
	    } else {
		mty.c_iflag &= ~(INLCR|ICRNL|IUCLC|BRKINT);
		mty.c_iflag |= (IGNBRK|IGNPAR|IXON|IXOFF|ISTRIP);
		mty.c_oflag &= ~(OPOST|ONLCR|TAB3);
		mty.c_lflag &= ~(ICANON|ISIG|ECHO|ECHOK);
		mty.c_cflag &= ~(PARENB|CSTOPB|HUPCL|CLOCAL);
		mty.c_cc[4]  = 0;  /* min */
		mty.c_cc[5]  = 0;  /* time */
	    }

	    switch (baud) {
	    case 1200:
		mty.c_cflag = ((mty.c_cflag & ~CBAUD) | B1200);
		break;
	    case 2400:
		mty.c_cflag = ((mty.c_cflag & ~CBAUD) | B2400);
		break;
	    case 4800:
		mty.c_cflag = ((mty.c_cflag & ~CBAUD) | B4800);
		break;
	    case 19200:
		mty.c_cflag = ((mty.c_cflag & ~CBAUD) | B19200);
		break;
	    default:
		mty.c_cflag = ((mty.c_cflag & ~CBAUD) | B9600);
		break;
	    }

	    if (ioctl (modem, TCSETAF, &mty) < 0) {
		fprintf (stderr, "cannot set modem port mode\n");
		perror ("modem");
		exitstatus = 3; goto abort;
	    }

	    /* Enable hardware flow control. */
	    if (hwflow) {
		if (ioctl (modem, UIOCFLOW, 0) < 0) {
		    fprintf (stderr, "cannot enable flow control\n");
		    perror ("modem");
		    exitstatus = 3; goto abort;
		}
	    }

	    /* Output the initialization string, if any, to the modem. */
	    if (initmsg[0])
		write (modem, initmsg, strlen(initmsg));
	}

	/* Open the user terminal. */
	if ((terminal = open (TERM, 2)) < 0) {
	    fprintf (stderr, "cannot open %s\n", TERM);
	    exitstatus = 1; goto abort;
	}

	/* Set raw mode no echo on user terminal. */
	if (ioctl (terminal, TCGETA, &o_tty) < 0) {
	    fprintf (stderr, "cannot read terminal status\n");
	    exitstatus = 2; goto abort;
	} else {
	    tty = o_tty;
	    tty.c_iflag &= ~(INLCR|ICRNL|IUCLC|ISTRIP|BRKINT);
	    tty.c_oflag &= ~OPOST;
	    tty.c_lflag &= ~(ICANON|ISIG|ECHO);
	    tty.c_cc[4]  = 0;  /* min */
	    tty.c_cc[5]  = 0;  /* time */
	    if (ioctl (terminal, TCSETAF, &tty) < 0) {
		fprintf (stderr, "cannot set terminal mode\n");
		exitstatus = 3; goto abort;
	    }
	}

	/* Enable nonblocking i/o for terminal ouptut. */
	do_fcntl (terminal, F_SETFD, O_NDELAY|O_RDWR);

	if (debug) {
	    if ((db = fopen ("modem.io", "a")) == NULL) {
		ioctl (terminal, TCSETAF, &o_tty);
		fprintf (stderr, "cannot open debug output file\n");
		exitstatus = 6; goto abort;
	    }
	    fprintf (db, "------ terminal on %d, modem on %d\n",
		terminal, modem);
	    fflush (db);
	}

	/* Check for any errors. */
	if (errno) {
	    ioctl (terminal, TCSETAF, &o_tty);
	    perror ("Warning");
	    ioctl (terminal, TCSETAF, &tty);
	}

	/* Compute the select bitmask. */
	tflag = (1 << terminal);
	mflag = (1 << modem);
	bits = (tflag|mflag);
	delay.tv_sec = 0;
	delay.tv_usec = 10000;		/* 10 milliseconds */
	idle.tv_sec = 5*60;
	idle.tv_usec = 0;
	otop = &obuf[SZ_OBUF];
	errno = 0;
	op = obuf;
	fast = 0;

	/* Read successive characters from the terminal and copy to the remote
	 * job, and read output from the remote job and copy to the local
	 * terminal.
	 */
	do {
	    if ((stat = select (32, (rfd=bits,&rfd), 0, 0, &delay)) < 0) {
		fprintf (stderr, "modem: select error\n");
		fflush (db);
		break;
	    } else if (stat == 0) {	/* timeout */
		if (op > obuf)
		    goto flush;
		else {
		    while ((stat = select(32,(rfd=bits,&rfd),0,0,&idle)) <= 0) {
			if (stat < 0) {
			    fprintf (stderr, "modem: select error\n");
			    fflush (db);
			    break;
			}
			if (keepalive)
			    write (modem, " \177", 2);
		    }
		}
	    }

	    if (debug)
		fprintf (db, "select returns %08o\n", rfd);

	    /* Read from the user terminal. */
	    if (rfd & tflag) {
		if (read (terminal, &ch, 1) < 1)
		    break;
		if (dataline)
		    ch &= 0177;
		if (debug)
		    fprintf (db, "read %03x from %d\n", ch, terminal);

		if (ch == ESCAPE) {			/* ctrl/a */
		    if (debug)
			fprintf (db, "ctrl/a seen\n");

		    /* Get the escape command. */
		    if (select (32, (efd=tflag,&efd), 0,0,NULL) < 0)
			goto quit;
		    if (read (terminal, &ch, 1) < 1)
			goto quit;

		    /* Process the escape. */
		    switch (ch) {
		    case 'A' - 64:			/* ctrl/a ctrl/a */
			goto quit;
			break;
		    case 'X' - 64:			/* ctrl/a ctrl/x */
			/* Suspend. */
			if (logfile)
			    fflush (logfile);
			ioctl (terminal, TCSETAF, &o_tty);
			kill (getpid(), SIGSTOP);
			ioctl (terminal, TCSETAF, &tty);
			break;

		    case 'D' - 64:			/* ctrl/a ctrl/d */
			/* Toggle debug output. */
			if (debug) {
			    fprintf (stderr, "[debug off] ");
			    fflush (stderr);
			    fclose (db);
			    debug = 0;
			    db = NULL;
			} else {
			    if ((db = fopen ("modem.io", "a")) == NULL) {
				ioctl (terminal, TCSETAF, &o_tty);
				fprintf (stderr,
				    "cannot open debug output file\n");
				exitstatus = 6; goto abort;
			    }
			    fprintf (db, "------ terminal on %d, modem on %d\n",
				terminal, modem);
			    fflush (db);
			    debug = 1;
			    fprintf (stderr, "[debug on] ");
			    fflush (stderr);
			}
			break;

		    case 'B' - 64:			/* ctrl/a ctrl/b */
			binary = 0;			/* (disabled) */
			goto putfile;

		    case 'P' - 64:			/* ctrl/a ctrl/p */
			/* Put a file. */
			binary = 0;
putfile:
			ioctl (terminal, TCSETAF, &o_tty);
			do_fcntl (modem, F_SETFD, O_RDWR);

			/* Get the file names. */
			printf ("put: ");  fflush (stdout);
			if (fgets (lbuf, SZ_LINE, stdin) != NULL) {
			    for (ip=lbuf;  isspace(*ip);  ip++)
				;
			    for (cp=infile;  *ip;  ip++)
				if (isspace (*ip))
				    break;
				else
				    *cp++ = *ip;
			    *cp = '\0';

			    for (;  isspace(*ip);  ip++)
				;
			    for (cp=outfile;  *ip;  ip++)
				if (isspace (*ip))
				    break;
				else
				    *cp++ = *ip;
			    *cp = '\0';
			}
			if (!infile[0] || ((fp = fopen(infile,"r")) == NULL)) {
			    printf ("cannot open %s\n", infile);
			    ioctl (terminal, TCSETAF, &tty);
			    do_fcntl (modem, F_SETFD, O_NDELAY|O_RDWR);
			    goto next;
			}

			if (!outfile[0])
			    strcpy (outfile, infile);

			/* Send the file. */
			sprintf (cmd, "stty %s ; cat - >%s; stty %s\n",
			    binary ? "raw -echo" : "-echo",
			    outfile,
			    binary ? "-raw echo" : "echo"
			    );
			puttext (modem, cmd, strlen(cmd));
			wmsec (CMDWAIT);

			kb = bytes = 0;
			if (binary) {
			    while ((nchars=fread(dbuf,1,SZ_DBUF,fp)) != NULL) {
				puttext (modem, dbuf, nchars);
				bytes += nchars;
				if (!watch && bytes/1024 > kb) {
				    printf ("\r%d", ++kb);
				    fflush (stdout);
				}
			    }
			} else {
			    while ((fgets (dbuf, SZ_DBUF, fp)) != NULL) {
				puttext (modem, dbuf, nchars=strlen(dbuf));
				bytes += nchars;
				if (!watch && bytes/1024 > kb) {
				    printf ("\r%d", ++kb);
				    fflush (stdout);
				}
			    }
			}

			fclose (fp);
			puttext (modem, eofstr, 1);
			wmsec (EOFWAIT);
			ioctl (terminal, TCSETA, &tty);
			do_fcntl (modem, F_SETFD, O_NDELAY|O_RDWR);
			op = obuf;
			break;

		    case 'L' - 64:			/* ctrl/a ctrl/l */
			/* Toggle logging of output to a file. */

			if (logfile) {
			    fclose (logfile);
			    logfile = NULL;
			    fprintf (stderr, "[logging off] ");
			    fflush (stderr);

			} else {
			    ioctl (terminal, TCSETAF, &o_tty);

			    /* Get the logfile name. */
			    printf ("logfile: ");  fflush (stdout);
			    if (fgets (lbuf, SZ_LINE, stdin) != NULL) {
				for (ip=lbuf;  isspace(*ip);  ip++)
				    ;
				for (cp=outfile;  *ip;  ip++)
				    if (isspace (*ip))
					break;
				    else
					*cp++ = *ip;
				*cp = '\0';
			    }
			    ioctl (terminal, TCSETAF, &tty);

			    if (outfile[0]) {
				if ((logfile = fopen (outfile, "a")) == NULL) {
				    fprintf (stderr,
					"cannot append to %s\r\n", outfile);
				    goto next;
				}
			    }
			}

			op = obuf;
			break;

		    default:
			goto next;
			break;
		    }

		} else {
		    stat = write (modem, &ch, 1);
		    if (debug)
			fprintf (db, "write %03x to %d: stat=%d\n",
			    ch, modem, stat);
		    if (stat != 1)
			goto quit;

		    /* Cancel the output buffer if ctrl/c. */
		    if (ch == '\003')
			op = obuf;
		}

		/* Typing causes the time delays to be adjusted to increase
		 * responsiveness at the expense of shorter i/o transfers and
		 * decreased bandwidth.  If the result is a lot of output the
		 * delays will quickly adjust back to the greater data volume.
		 */
		fast = NFAST;
	    }
next:
	    /* Read from the remote job. */
	    if (rfd & mflag) {
		if (!fast)
		    wmsec (IOWAIT);
		if ((nchars = read (modem, op, otop-op)) < 0)
		    break;
		else if (nchars > 0)
		    op += nchars;

		if (debug) {
		    char    cbuf[80], *ii, *oo;
		    int     n = (nchars < 8) ? nchars : 8;

		    /* Dump the first 8 character values in printable form. */
		    for (ii=(op-nchars), oo=cbuf;  --n >= 0;  ii++) {
			*oo++ = '\'';
			if (isprint (*ii))
			    *oo++ = *ii;
			else {
			    *oo++ = '^';
			    *oo++ = *ii + 'A' - 1;
			}
			*oo++ = '\'';
			*oo++ = ' ';
		    }
		    *oo++ = '\0';
		    fprintf (db, "read %03d chars from %d\t\t%s\n",
			nchars, modem, cbuf);
		}
	    }

	    /* Flush the output buffer periodically or if it fills. */
	    if (op > obuf && (fast || op - obuf > top)) {
flush:		if (debug)
		    fprintf (db, "flush output buffer, %d chars\n", op-obuf);
		if ((stat = write (terminal, obuf, op - obuf)) != op-obuf) {
		    if (stat < 0) {
			fprintf (stderr,
			    "modem write error, errno=%d\n", errno);
			break;
		    } else {
			op += stat;
			wmsec (IOWAIT);
			goto flush;
		    }
		}
		putlog (obuf, op-obuf);
		if (fast)
		    --fast;
		op = obuf;
	    }
	} while (1);
quit:
	/* Shutdown. */
	while (op > obuf) {
	    stat = write (terminal, obuf, op - obuf);
	    if (stat < 0) {
		fprintf (stderr,
		    "modem write error, errno=%d\n", errno);
		break;
	    } else {
		op += stat;
		wmsec (IOWAIT);
	    }
	}
	if (logfile) {
	    putlog (obuf, op-obuf);
	    fclose (logfile);
	}
	if (debug)
	    fclose (db);

	ioctl (terminal, TCSETA, &o_tty);
	close (terminal);


	/* Output the exit string, if any, to the modem, and close. */
	if (exitmsg[0])
	    write (modem, exitmsg, strlen(exitmsg));
	ioctl (modem, TCSETA, &o_mty);
	close (modem);
	if (!exitstatus)
	    printf ("\n");

abort:
	if (locked)
	    unlink (lockfile);
	exit (exitstatus);
}


/* PUTTEXT -- Put a line of text to the output device, then pause to allow
 * the system on the receiving end to process the line of input.
 */
puttext (fd, lbuf, nchars)
int	fd;			/* output file */
char	*lbuf;			/* line of text */
int	nchars;			/* nchars to be output */
{
	if (watch) {
	    write (1, lbuf, nchars);
	    write (1, "\r", 1);
	}

	write (fd, lbuf, nchars);
	wmsec (NLWAIT + nchars * 2);
}


/* PUTLOG -- Put a line of text to the log file.  Convert CRLF into LF in
 * the process.
 */
putlog (lbuf, nchars)
char	*lbuf;			/* line of text */
int	nchars;			/* nchars to be written */
{
	register char *ip;
	register int n, ch;
	static int cr;

	if (logfile)
	    for (n=nchars, ip=lbuf;  --n >= 0 && *ip;  ip++) {
		ch = *ip;
		if (cr && ch != '\n')
		    fputc ('\r', logfile);
		else if (cr = (ch == '\r'))
		    continue;
		fputc (ch, logfile);
	    }
}


/* WMSEC -- Suspend task execution (sleep) for the specified number
 * of milliseconds.
 */
wmsec (msec)
int	msec;
{
	struct	itimerval itv, oitv;
	register struct itimerval *itp = &itv;
	int	(*old)();
	static	int napmsx();
	int	stat;

	if (msec <= 0)
	    return;

	timerclear (&itp->it_interval);
	timerclear (&itp->it_value);
	if (setitimer (ITIMER_REAL, itp, &oitv) < 0)
	    return;

	itp->it_value.tv_usec = (msec % 1000) * 1000;
	itp->it_value.tv_sec  = (msec / 1000);

	if (timerisset (&oitv.it_value)) {
	    if (timercmp(&oitv.it_value, &itp->it_value, >))
		oitv.it_value.tv_sec -= itp->it_value.tv_sec;
	    else {
		itp->it_value = oitv.it_value;
		oitv.it_value.tv_sec  = 1;
		oitv.it_value.tv_usec = 0;
	    }
	}

	old = signal (SIGALRM, napmsx);

	ringring = 0;
	setitimer (ITIMER_REAL, itp, (struct itimerval *)0);
	while (!ringring)
	    sigpause (0);
	setitimer (ITIMER_REAL, &oitv, (struct itimerval *)0);

	signal (SIGALRM, old);
}


static int
napmsx()
{
	ringring = 1;
}


/* DO_FCNTL -- Conditionally execute fcntl.
 */
do_fcntl (fd, flag, arg)
int	fd;
int	flag;
int	arg;
{
	if (nonblock)
	    fcntl (fd, flag, arg);
}
