/* ixocico -- IXO/TAP 1.1 protocol call-in call-out.
**
** Original work by:
**   by Tom Limoncelli, tal@warren.mentorg.com
**   Copyright (c) 1992, Tom Limoncelli
**   The sources can be freely copied for non-commercial use only
**   and only if they are unmodified.
**
** Massively rewritten to adhere to TAP protocol 1.1 (July
** 30, 1992), and make modem handling robust. The protocol
** outputs status messages that reflect the current protocol
** state (by step number as given in the spec) and includes
** a few quite legal enhancements, mostly error handling and
** garbage control. 
**
** Modem handling fails in one crucial manner -- the dialed
** baud rate must be exactly the same as the connect baud rate;
** ie. if you dial at 1200, and the remote server answers at
** 300, you're doomed. I wonder how safe it is to assume that
** "all" servers are now 1200 baud. (The simplest way out
** would be to change to numeric result codes (ATV0) and
** check for the result code digit, and set speed accordingly.)
**
** I am also not a long-time unix programmer, and so don't know
** the subtleties of serial-device handling. I've partitioned
** off the read character routine as much as possible.
**
** tomj@wps.com (Tom Jennings)
** 1 April 94
**
** Version 3.0 -- See file HISTORY for details.
**  $Id: ixocico.c,v 1.4 1992/09/22 17:31:01 root Exp $

$Log: ixocico.c,v $
 * Revision 1.4  1992/09/22  17:31:01  root
 * set serial parameters to 300bps, 7e1
 *
 * Revision 1.3  1992/09/21  19:41:31  root
 * made a small change to a comment
 *
 * Revision 1.2  1992/09/21  19:40:22  root
 * changed parity to ODD
 *
 * Revision 1.1  1992/09/21  19:38:02  root
 * Initial revision
 *

*/

/****************************************************************/
/* USER CONFIGURABLE OPTIONS: */

/* this should be "#define" if you use SunOS, or "#undef" if you
** use HPUX.  This controls the name of LOCKDIR and if getpriority()
** is used.  I'm sure more needs to be done, but that's a small start.
*/
#define REAL_OS


#ifdef REAL_OS
#define LOCKDIR	"/var/spool/uucp"
#else
#define LOCKDIR "/usr/spool/locks"
/* That may not be correct */
#endif

#define DEBUG 0			/* display useful stuff */

/* Modem parameters */

#define SPEED	1200		/* dialout speed; this driver doesn't
				   switch speeds (ie. CONNECT 300
				   vs. CONNECT 1200, etc) so it must
				   actually match the remote system!
				   */

#define INITSTRING "AT X4 M0 E1 Q0 V1\r" /* String to init the
				   modem with. */


void bail_out();

/* not talking to the modem correctly?  Try mucking with
the grabmodem() routine.  */

/* END OF USER CONFIGURABLE OPTIONS */
/****************************************************************/

#include <stdio.h>
#include <fcntl.h>
#include <string.h>
/* #include <strings.h> */
#include <ctype.h>
#include <errno.h>
#include <sys/termios.h>
#include <sys/param.h>
#include <termios.h>

#ifdef REAL_OS
#include <sys/time.h>		/* required for <sys/resource.h> */
#include <sys/resource.h>	/* required for getpriority() */
#endif

/* ASCII constants */

#define NUL (0)
#define STX (2)
#define EOT (4)
#define ACK (6)
#define LF (10)
#define CR (13)
#define NAK (21)
#define ESC (27)
#define RS (30)
#define DEL (127)

#define MAX_PACKET	(10000)	/* we'll never get a packet this big */
#define MAXLINE	(1000)

/* only two little global variables, how's that? */

int modem = 0;
char *lockname = NULL;

#if DEBUG
/* print a string without worrying about unprintable charactors */
void 
safeprint(str)
char           *str;
{
	while (*str) {
		if (isgraph(*str))
			(void) fprintf(stdout, "%c", *str);
		else {
			switch (*str) {
			case LF:
				(void) fprintf(stdout, "\\n");
				break;
			case CR:
				(void) fprintf(stdout, "\\r");
				break;
			case 32:
				(void) fprintf(stdout, "\\s");
				break;
			default:
				(void) fprintf(stdout, "\\%d", *str);
				break;
			}
		}
		str++;
	}
	fflush(stdout);
}
#endif

/* calculate checksum of a packet */
char *checksum(pk)
char *pk;
{
	static char check[10];
	int sum = 0;

	for (;*pk; pk++) sum += *pk;
	check[2] = '0' + (sum & 15); sum = sum >> 4;
	check[1] = '0' + (sum & 15); sum = sum >> 4;
	check[0] = '0' + (sum & 15);
	check[3] = 0;
#if DEBUG
	printf("CHECKSUM=:"); safeprint(check); printf(":\n");
#endif
	return check;
}

/* Get a "packet", ie. an acknowledge/status from the server.
Return the packet type (ID=, ACK, NAK, EOT, RS, 'p'; see the TAP
spec, step 6) or NUL if NOTA. NOTE: It is not clear if a CR always
preceeds a packet (step 10a, 10b see ambiguous) and so we'll forego
the requirement; it should not hurt reliaiblity, but it assumes
that none of the packet type characters appear in the ooptional
<message sequence>'s. The protocol already assumes that <CR><packet
type> won't appear in them, so this seems safe. */

getpacket(fd)
int fd;
{
	char c;			/* char read from modem */
	char badc;		/* for error report */
	char lastc;		/* statemachine */
	int errors;		/* error counter */
	int timeouts;		/* timeout counter */

/* We return NUL upon either: N character sequence errors or a
timeout of 10 seconds. Setting c=DEL flags a character sequence
error.  */

	errors= 0;		/* protocol errors */
	timeouts= 0;		/* no char to read */

	lastc= NUL;		/* reset state machine */

#if DEBUG
	printf("Getting packet.\n");
#endif
	while ((errors < 20) && (timeouts < 10)) {

/* We make a little state machine to detect the responses from the
server, which are always "... CR N1 N2 N3 CR ...". N1 is one of:
ACK, NAK, ESC, [. ACK, NAK, RS have no N2 or N3. ESC has N2 of EOT
or [. N2 of [ is followed by p. */

		c= badc= readmodem(fd);
#if DEBUG
		if (c) printf("Got: %d\n", c);
#endif
		switch (c) {

/* If we read a NUL, it means there was nothing to read. Count
as a timeout. Don't count as a sequence error though, it might
just be a delay. */

			case NUL:
				sleep(1);	/* one sec delay */
				++timeouts; break;

/* The first CR does nothing, except get stored as lastc, below.
A trailing CR triggers the return of the packet type. */

			case CR:
				switch (lastc) {
					case ACK:
					case NAK:
					case EOT:
					case 'p': 
						printf("<CR>\n");
						return(lastc);
						break;

					case NUL:
						break;

					default:
						c= DEL;
						break;
				}
				printf("<CR>");	/* initial CR */
				break;

/* Start-of-packet character. This is where we'd check for the
initial CR (ie. if (lastc != CR) c= DEL) if we were doing that.  */

			case ACK: printf("<ACK>"); break;
			case NAK: printf("<NAK>"); break;
			case ESC: printf("<ESC>"); break;
			case RS: printf("<RS>"); break;

/* Check for "ID=". (It comes across as "ID=" or "ID ="; spaces
are ignored.) */

			case 'I': printf("I"); break;

			case 'D': 
				if (lastc != 'I') c= NUL;
				printf("D"); 
				break;

			case '=':
				if (lastc == 'D') {
					printf("=\n");
					return('=');
				}
				c= NUL;
				break;

/* ESC is followed by EOT or [ only. */

			case EOT:
				if (lastc != ESC) c= DEL;
				else printf("<EOT>");

/* NOTE: NON-PROTOCOL FIX: the disconnect sequence at step 10 seems
to come through with garbage after the <ESC><EOT>, as if the sequence
really ends on <EOT> and not the full <CR><ESC><EOT><CR>. This
lowers reliablity a tiny amount, in exchange for much better hits
on the Step 10 sequence. So just return when we get <CR><ESC><EOT>.
*/

				return(EOT);
				break;

/* Because [ and p may be part of optional <message
sequence>'s, we don't error on [ not preceeded by ESC. */

			case '[':
				if (lastc != ESC) c= NUL;
				else printf("[");
				break;

/* p follows only [ which follows only ESC. (Note that we always
print "p"; this is a hack to display <message sequence>'s, instead
of doing the real job.) */

			case 'p':
				if (lastc != '[') c= NUL;
				printf("p");
				break;

/* Non-protocol character. */

			default:
				printf("%c", c);
				c= NUL; break;
		}

		if (c == DEL) {		/* if sequence error */
			printf(" %d !! sequence error\n", badc);
			++errors;
			c= NUL;
		}
		lastc= c;
	}
	return(NUL);			/* no packet received */
}

/* Read a character from the modem. If one is immediately
available, return it, otherwise return NUL. Ie. this is a
non-blocking read. It is acceptable to this code to lose actual
NUL characters received. Incoming parity is stripped manually
here. All errors can return as NUL. */

readmodem(fd)
int fd;
{
char c;

#if BSD
	if (read(fd, &c, 1) < 1) return(0);
#endif
	return(c & 0x7f);
}

/* open the modem.  You should have done a lockmodem() first */

int grabmodem(dev)
char           *dev;
{
int fd;
struct termios tp;

	if ((fd = open(dev, O_RDWR | O_NDELAY)) < 0) {
		perror(dev);
		exit(1);
	}

	if (tcgetattr (fd, &tp) < 0) {
		perror("#ixocico:grabtty:tcgetattr");
		bail_out();
	};
	cfmakeraw(&tp);

/* set tty params to even parity, 7-1-e */

	tp.c_cflag |= CS8 | CLOCAL | CREAD | HUPCL | CRTSCTS;
	cfsetspeed(&tp, SPEED);

	if (tcsetattr (fd,TCSADRAIN, &tp) < 0) {
		perror("#ixocico:grabtty:tcsetattr");
		bail_out();
	};
	return(fd);
}

/* send data to the modem */
void send(fd, str)
int             fd;
char           *str;
{
#if DEBUG
	printf("Sending: :"); safeprint(str); printf(":\n", str);
#endif
	write(fd, str, strlen(str));
}

/* wait for a particular string from the modem (err = # of retries
permitted ) */

int match(mod, str, err) 
	int mod;
	char *str;
	int err;
{
	int len;
	char *s, c;

#if DEBUG
	printf("MATCHING on :"); safeprint(str); printf(":\n", str);
#endif
	s= str;
	while (err) {
		if (c= readmodem(mod)) {
#if DEBUG
printf("want: %c got: %c\n", *s, c);
#endif
			if (c == *s) s++; else s= str;
			if (!*s) {
#if DEBUG
				printf("MATCHED\n");
#endif
				return(0);
			}
		} else {		/* timeout */
			--err;
			sleep(1);
			continue;
		}
	}
#if DEBUG
	printf("NOT MATCHED\n");
#endif
	return 1;
}

/* hang up the modem */
void 
hangup_modem(fd)
int             fd;
{
	sleep(3);
	send(fd, "+++");
	sleep(3);
	send(fd, "ATH\r");
	sleep(1);
}

/* unlock the modem */
void unlockmodem(name)
char           *name;
{
	printf("Unlocking modem.\n");
    (void)unlink(name);
    return;
}

/* clean up and leave this program */
void bail_out()
{
	if (modem) {
		hangup_modem(modem);
		close(modem);
	}
	if (lockname) unlockmodem(lockname);
	exit(0);
}

/* lock the modem OR DIE*/
char *lockmodem(dev)
char           *dev;
{
	int lock, pid;
	int failcnt = 3;
	char waitcnt = 0;
	char *lkn, lname[200];

	strcpy(lname, LOCKDIR);
	strcat(lname, "/LCK..");
	strcat(lname, 1 + rindex(dev, '/'));

printf("Lockfile = %s\n", lname);
	lkn = strdup(lname);
	while (failcnt--) {
		errno = 0;
		lock = open(lname, O_CREAT | O_WRONLY | O_EXCL, 0777);
		if (lock == -1) {
#ifdef REAL_OS
			printf("Modem is locked, attempting to steal.\n");
			/* locked, let's read the cookie in the lock */
			pid = 0;
			if ((lock = open(lname, O_RDONLY)) != -1) {
				(void)read(lock, &pid, sizeof(int) );
				printf("Device is locked by process %d\n", pid);
				close(lock);
			}
			printf("Lock = %d\n", lock);
			if (pid < 3) {
				printf("#MODOPEN device is locked by pid < 3\n");
				bail_out();
			}
			/* see if the process still is alive */
			errno = 0;
			(void) getpriority(PRIO_PROCESS, pid);
			if (errno == ESRCH) {   /* lock process dead, let's go! */
				if (unlink(lname)) {
					printf("#MODOPEN Can't steal lock.\n");
					bail_out();
				} else {
					printf("Lock is stale, stealing!\n");
				}
			} else {
				printf("#MODOPEN Valid lock in the way.\n");
				bail_out();
			}
#else
			printf("#MODOPEN it's locked, I'm out of here!\n");
			bail_out();
#endif
		} else {
			/* lock opened, stuff and go */
			pid = getpid();
			write(lock, &pid, sizeof(int));
			close(lock);
			break;
		}
	}
	if (failcnt==-1) {
		printf("#MODOPEN Couldn't lock modem after many tries.\n");
		bail_out();
	}
	return lkn;
}

/* get a line from stdin OR DIE */
char *getline(line)
char *line;
{
	int len;
	char *r;

	/* get a line, if EOF return 0 */
	if (!(r = fgets(line, MAXLINE, stdin))) return 0;

#if DEBUG
	printf("Data in queue=:"); safeprint(line); printf(":\n", line);
#endif
	if (!(len = strlen(line))) {
		printf("#BADQUEUE Blank line in queued data\n");
		bail_out();
	}

	if (line[len-1] == '\n') {
		line[len-1] = 0;
	} else {
		/* if fgets didn't return a string ending in \n */
		printf("#BADQUEUE Data in queue has line too long\n");
		bail_out();
	}
	return r;
}

int main(argc, argv)
int argc;
char *argv[];
{
	char dialstring[MAX_PACKET];
	char *pack = dialstring;
	char line[MAXLINE+1];
	char pin[MAXLINE+1];
	char mesg[MAXLINE+1];
	int mesnum, failcnt, len;
	char c;

	/* check arguments */
	if (argc != 3) {
		printf("#WRONGARGS wrong number of arguments\n");
		bail_out();
	}

	/* lock modem or die */
	lockname = lockmodem( argv[1] );
	/* open modem or die */
	printf("Opening modem.\n");
	modem = grabmodem( argv[1] );
	if (!modem) bail_out();

/* Wake up the modem. COmmand it to what we want. */

	failcnt = 3;
	while (failcnt--) {
		printf("Initialize the modem.\n");
		send(modem, INITSTRING);
		sleep(1);
		send(modem, INITSTRING);

		if (match(modem, "OK", 10)) {
			printf("No response.  Hang up and try again.\n");
			hangup_modem(modem);
		} else break;
	}
	if (failcnt==-1) bail_out();

	sleep(1);		/* modem settle time */

printf("Dialing %s\n", argv[2]);

	/* send the "A" of "ATDT" */
	do {
		send(modem, "A");
	} while (match(modem, "A", 2));

	/* send the rest of the dial string */

	sprintf( dialstring, "TDT%s\r", argv[2] );
	send(modem, dialstring);
	(void) match(modem, argv[2], 1000);

	/* wait for the modem to connect */
	printf("Waiting for \"CONNECT\".\n");
	if (match(modem, "CONNECT", 60)) {
		printf("#NOCONN no connect\n");
		bail_out();
	}
	match(modem, "\r", 50);


/* Wait for the opening response of "ID=". It waits for an initial
CR to start (autobaud?). To take care of line noise (eg. "ID~i=")
we issue "?\r". If indeed in autobaud, the ? is extraneous and
ignored; if we simply missed the ID= prompt, the incorrect response
causes the prompt to be reissued. */

	printf("Step 2: Carrier up.\n");
	sleep(2);				/* let modem settle */

	failcnt = 3;				/* try n1 times */
	while (failcnt--) {			/* loop t1 seconds */
		printf("Step 3: Send CR.\n");
		send(modem, "\r");		/* send initial CR */
		sleep(1);			/* "ID=" or "ID =" ? */
		printf("Step 4: Wait for \"ID=\".\n");
		if (!match(modem, "=", 1)) break; /* t2 seconds */
	}
	if (failcnt==-1) bail_out();

printf("Enhancement: Waiting for quiet line.\n");

	sleep(2);				/* wait for output */
	while (readmodem(modem));		/* then flush */

	failcnt = 3;
	while (failcnt--) {

		printf("Step 5a: select automatic mode.\n");
		c= ESC; write(modem, &c, 1);
		send(modem, "PG1\r");

		c= getpacket(modem);		/* sync from server */
		if (c == ACK) {			/* success */
			printf("Step 6: Logon accepted.\n");
			break;

		} else if (c == '=') {		/* resend mode */
			printf("Step 6: Request again.\n");
			continue;

		} else if (c == NAK) {		/* xmit error likely */
			printf("Step 6: Request again.\n");
			continue;

		} else if (c == EOT) {
			printf("Step 6: Forced disconnect.\n");
			bail_out();

		} else {
			printf("#UNKNOWNPROTO Step 6: Unexpected response?\n");
			bail_out();
		}
	}
	if (failcnt < 0) bail_out();

/* The protocol doesn't seem to send more than one ESC [ p sequences,
nor allow for resend, etc. If it fails, let's just retry anyways,
since if it doesn't resend we're dead anyways!  */

	failcnt= 3;
	while (failcnt--) {
		if (getpacket(modem) == 'p') {
			printf("Step 7: Mesage go-ahead.\n");
			break;
		}
	}
	if (failcnt < 0) bail_out();

	for (mesnum=0; getline(pin); ) {
		getline(mesg);

		for (failcnt= 100; failcnt--;) {
			printf("Step 8: Transaction.\n");
			pack[0] = STX;		/* build packet */
			pack[1] = 0;
			strcat(pack, pin);
			strcat(pack, "\r");
			strcat(pack, mesg);
			strcat(pack, "\r\3");	/* CR then ETX */
			strcat(pack, checksum(pack));
			strcat(pack, "\r");
			send(modem, pack);
	
			if ((c= getpacket(modem)) == ACK) {
				printf("#MESOK %d Step 8: Block OK.\n", 
				    mesnum++);
				break;

			} else if (c == NAK) {
				printf("Step 8: Error, resend block %d.\n", 
				  mesnum);

			} else if (c == RS) {
				printf("#MESREJECT %d Step 8: Abandon transaction.\n", 
				    mesnum++);
				break;

			} else if (c == EOT) {
				printf("#FORDIS Step 8: Begin disconnect.\n");
				bail_out();
			}
		}
		if (failcnt < 0) {
			printf("#PROTERR couldn't send packets\n");
			bail_out();
		}
	}
	printf("#DONE Step 9: Done; send EOT.\n");
	send(modem, "\4\r");		/* EOT then CR */

	if ((c= getpacket(modem)) == RS) {
		printf("#WRONGANY Step 10b: Server says, back in step 8, data was bad. Duh.\n");
		c= getpacket(modem);
	}
	if (c == EOT) printf("Step 10c: Got disconnect sequence.\n");
	else printf("Step 10: Didn't get disconnect sequence (hardly matters).\n");


	printf("#BYE we're leaving.\n");
	bail_out();
}
