/* TCP/IP interface for the XANT program */

#include <stdio.h>
#include <memory.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/file.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define TELCMDS
#define TELOPTS
#include <arpa/telnet.h>

#ifndef NULL
#define NULL 0
#endif

typedef char Boolean;



/* Global variables */
static int port;		/* Port number to use */
static Boolean extended;	/* Extended data streams are supported */
static int s;			/* Socket descriptor */
static unsigned char *inbuff;	/* Input buffer */
static unsigned char *inp;	/* Input buffer pointer */
static unsigned char *outbuff;	/* Output buffer */
static unsigned char *outp;	/* Output buffer pointer */
static int incount;		/* Number of characters in input buffer */
static int instate;		/* Input state */
static int getcstate;		/* Getcmd state */
static int optstate[256];	/* State of options negotiation */
extern Boolean trace;		/* Tracing flag */
extern int errno;		/* Last error number */
#define BUFSIZE 8192

static void negotiate(), sendcmd(), sendbuff(), flush(), sendit(), prcmd();
static int getdata(), getcmd();


/* Establish TCP/IP connection to remote telnet server.  Returns
   file descriptor of socket for select() call.  */

int tel_init(hostname, port_setting, ext_setting) 
     char *hostname;
     int port_setting;
     Boolean ext_setting;
{
  int i;
  struct hostent h, *hp;
  struct in_addr addr;
  char *addr_list[2];
  struct sockaddr_in sin;
  static int on = 1;

  /* If port wasn't specified, look up default value */
  port = htons(port_setting);
  if (!port) 
    {
      struct servent *sp;
      if (!(sp = getservbyname("telnet", "tcp")))
	error("Telnet port is not defined in service name database");
      port = sp->s_port;
    }

  /* Handle host name in dotted decimal notation */
  if ((addr.s_addr = inet_addr(hostname)) != -1) 
    {
      addr_list[0] = (char *) &addr;
      addr_list[1] = NULL;
      h.h_addrtype = AF_INET;
      h.h_length = sizeof addr;
      h.h_addr_list = addr_list;
      hp = &h;
    }

  /* If not dotted decimal, then call the name server */
  else if (!(hp = gethostbyname(hostname)))
    error("Host \"%s\" is unknown", hostname);

  /* Create a socket */
  if ((s = socket(hp->h_addrtype, SOCK_STREAM, 0)) < 0)
    errorm("Socket");

  /* Tell TCP not to wait before sending out packet */
  if (setsockopt(s, IPPROTO_TCP, 1, &on, sizeof(on)) < 0)
    errorm("setsockopt");
  
  /* Initialize socket address information */
  sin.sin_family = hp->h_addrtype;
  sin.sin_port = port;

  /* Try to connect to each address in the host entry */
  printf("Trying...");
  fflush(stdout);
  for (i = 0; hp->h_addr_list[i]; i++) 
    {
      memcpy((char *) &sin.sin_addr, hp->h_addr_list[i], hp->h_length);
      if (!connect(s, (struct sockaddr *) &sin, sizeof sin))
	break;
    }
  if (!hp->h_addr_list[i])
    {
      putchar('\n');
      errorm("Cannot connect to %s", hostname);
    }
  else printf("Open\n");

  /* Allocate input and output buffers */
  if (!(inbuff = (unsigned char *) malloc(BUFSIZE)) ||
      !(outbuff = (unsigned char *) malloc(BUFSIZE)))
    error("Not enough memory");
  inp = inbuff;
  outp = outbuff;
  incount = 0;
  instate = 0;
  getcstate = 0;
  extended = ext_setting;
  return s;
}


/* Do option negotiation to get into transparent mode */
tel_setup(cols, rows, newcols, newrows)
     int cols, rows;
     int *newcols, *newrows;
{
  register int i;
  int telcmd;
  static int on = 1;
  static unsigned char sendtt[] = { IAC, SB, TELOPT_TTYPE, TELQUAL_IS };
  static unsigned char sendse[] = { IAC, SE };
  static struct termtype
    {
      int cols;
      int rows;
      char *name;
    }
  termtab[] = {
    132, 27, "IBM-3278-5", 
    80,  43, "IBM-3278-4", 
    80,  32, "IBM-3278-3", 
    80,  24, "IBM-3278-2"
  };
#define NTERMS (sizeof termtab / (sizeof(struct termtype)))

  *newcols = *newrows = 0;

  /* Show how we want the options to be negotiated */
  memset(optstate, 0, sizeof optstate);
  optstate[TELOPT_TTYPE] = 1;
  optstate[TELOPT_BINARY] = -1;
  optstate[TELOPT_EOR] = -1;
  optstate[TELOPT_SGA] = 1;

  /* Perform the negotiations */
  while (1)
    {
      /* If input buffer is empty, read some more.  We are doing
         blocking I/O, so if no data is available we'll wait for it.  */

      if (getdata() <= 0) error("No data received");
      telcmd = getcmd();

      /* Leave if something other than a telnet command is received */
      if (telcmd < 0) break;

      /* Interpret the command */
      if ((telcmd>>16) > 0)
	{
	  /* We only support one sub-negotiation command */
	  if (telcmd != (SB<<16 | TELOPT_TTYPE<<8 | TELQUAL_SEND))
	    error("Invalid telnet sub-negotiation command received");

	  /* Figure out which terminal type to use and send it */
	  for (i = 0; i < NTERMS; i++) 
	    if (termtab[i].cols <= cols && termtab[i].rows <= rows) break;
	  if (i == NTERMS) i--;

	  sendbuff(sendtt, sizeof sendtt);
	  sendbuff(termtab[i].name, strlen(termtab[i].name));
	  if (extended) sendbuff("-E", 2);
	  sendbuff(sendse, sizeof sendse);
	  flush();
	  if (trace) printf("Sent: SB TERMINAL TYPE IS\n");

	  /* Pass back actual terminal size to caller */
	  *newcols = extended ? cols : termtab[i].cols;
	  *newrows = extended ? rows : termtab[i].rows;
	}
      else if ((telcmd>>8) > 0)
	negotiate(telcmd);
      else if (telcmd > 0)
	error("Unexpected telnet command received");
    }
  
  /* If terminal type was not requested, indicate that negotiations failed */
  if (!*newcols) optstate[0] = -9;

  /* See if all options were successfully negotiated */
  for (i = 0; i < 256; i++) 
    if (optstate[i] < 0)
      error("Cannot switch to transparent mode (this program only works with hosts\n\
that use the 3270 protocol)");
  if (trace) printf("Transparent mode!\n");

  /* Set for non-blocking I/O */
  if (ioctl(s, FIONBIO, &on) < 0)
    errorm("Ioctl failed");
}


/* Negotiate a telnet option */
static void negotiate(telcmd)
     int telcmd;
{
  register int opt;

  /* Note:  The option states are defined so that when negotiations are
     completed successfully, the optstate array will not have any negative
     values.  */

  opt = telcmd & 255;
  telcmd >>= 8;
  switch (optstate[opt])
    {
    case 0:
      /* Undesired option */
      switch (telcmd)
	{
	case DO:
	  sendcmd(WONT, opt);
	  break;
	case WILL:
	  sendcmd(DONT, opt);
	  break;
	}
      break;
      
    case 1:
      /* Option that we'll accept, but won't request */
      switch (telcmd)
	{
	case DO:
	  sendcmd(WILL, opt);
	  optstate[opt] = 2;
	  break;
	case WILL:
	  sendcmd(DO, opt);
	  optstate[opt] = 3;
	  break;
	}
      break;
      
    case 2:
      /* Got DO; Accept a WILL */
      switch (telcmd)
	{
	case WILL:
	  sendcmd(DO, opt);
	  optstate[opt] = 9;
	  break;
	}
      break;
      
    case 3:
      /* Got WILL; Accept a DO */
      switch (telcmd)
	{
	case DO:
	  sendcmd(WILL, opt);
	  optstate[opt] = 9;
	  break;
	}
      break;
      
    case -1:
      /* Option that should be negotiated in both directions */
      switch (telcmd)
	{
	case DO:
	  sendcmd(WILL, opt);
	  optstate[opt] = -2;
	  break;
	case WILL:
	  sendcmd(DO, opt);
	  optstate[opt] = -3;
	  break;
	case DONT:
	case WONT:
	  optstate[opt] = -9;
	  break;
	}
      break;
      
    case -2:
      /* Got DO; Sent WILL; Waiting for WILL */
      switch (telcmd)
	{
	case WILL:
	  sendcmd(DO, opt);
	  optstate[opt] = 9;
	  break;
	case DONT:
	case WONT:
	  optstate[opt] = -9;
	  break;
	}
      break;
      
    case -3:
      /* Got WILL; Sent DO; Waiting for DO */
      switch (telcmd)
	{
	case DO:
	  sendcmd(WILL, opt);
	  optstate[opt] = 9;
	  break;
	case DONT:
	case WONT:
	  optstate[opt] = -9;
	  break;
	}
      break;
      
    case -9:
    case 9:
      /* Negotiations complete */
      switch (telcmd)
	{
	case DO:
	case WILL:
	case DONT:
	case WONT:
	  break;
	}
      break;
    }
}


/* Process any input from remote host.  Returns -1 if remote system
   closed the connection, 1 if data was found, and 0 otherwise.  */
int tel_check()
{
  register unsigned char *bp;
  int retval, telcmd;

  retval = 0;
  while (1) 
    {
      int j = getdata();
      if (j < 0) return -1;
      else if (j == 0) return retval;
      retval = 1;

      switch (instate) 
	{
	case 0:			/* Data character in transparent mode */
	  
	  /* In this state, the current input pointer is guaranteed
	     to point to a data character.  Scan forward to the next
	     IAC or to end of buffer.  */
	     
	  bp = NULL;
	  if (incount > 1)
	    bp = (unsigned char *) memchr(inp + 1, IAC, incount - 1);
	  
	  /* If no IACs found, accept the whole buffer */
	  if (!bp) 
	    {
	      s3270_accept(inp, incount);
	      incount = 0;
	    }

	  /* Otherwise accept up to the IAC */
	  else 
	    {
	      s3270_accept(inp, bp - inp);
	      incount -= bp - inp;
	      inp = bp;
	    }
	  instate++;
	  break;

	case 1:			/* Checking for IAC */
	  telcmd = getcmd();
	  if (telcmd < 0) instate--;
	  else if (telcmd == EOR) s3270_eor();
	  else if (telcmd == GA) break;	/* Ignore GA */
	  else if ((telcmd>>16) == 0 && (telcmd>>8) > 0) negotiate(telcmd);
	  else if (telcmd)
	    error("Unexpected telnet command (%d) received", telcmd);
	  break;

	default:
	  error("Invalid state");
	}
    }
}


/* If input buffer is empty, get some more data.  Returns -1 if the
   remote system closed the connection, 1 if data was gotten, and
   0 otherwise.  */

static int getdata() 
{
  int i;
  if (incount) return 1;
  if ((i = recv(s, (char *) inbuff, BUFSIZE, 0)) < 0)
    {
      if (errno == EWOULDBLOCK) return 0;
      errorm("Receive data failed");
    }
  if (!i) return -1;
  inp = inbuff;
  incount = i;
  
  if (trace) 
    {
      printf("Received %d bytes:\n", incount);
      dumpbuff(inbuff, incount);
      putchar('\n');
    }
  return 1;
}


/* Receive a telnet command

   Returns:	0 - More input needed
   	       -1 - Non-telnet command
	     code - Telnet command code       */

static int getcmd()
{
  register unsigned char c;
  static int curcmd;
  int retval;
  
  retval = 0;
  c = *inp;
  switch (getcstate)
    {
    case 0:			/* Initial state */
      if (c != IAC) return -1;
      getcstate++;
      break;

    case 1:			/* IAC seen */
      if (c == IAC)
	{
	  getcstate = 0;
	  return -1;
	}
      curcmd = c;
      if (c != DO && c != WILL && c != DONT && c != WONT && c != SB)
	{
	  if (c != NOP) retval = curcmd;
	  getcstate = 0;
	}
      else getcstate++;
      break;

    case 2:			/* IAC + command seen */
      if (curcmd == SB)
	{
	  curcmd = SB << 16 | c << 8;
	  getcstate++;
	}
      else 
	{
	  retval = curcmd << 8 | c;
	  getcstate = 0;
	}
      break;

    case 3:			/* Subnegotion command seen */
      curcmd |= c;
      getcstate++;
      break;

    case 4:			/* Looking for end of subnegotiation */
      if (c == IAC) getcstate++;
      break;

    case 5:			/* IAC seen during subnegotiation */
      getcstate = 0;
      if (c != SE) return -1;
      retval = curcmd;
      break;
    }
  inp++;
  incount--;
  if (retval && trace) 
    {
      int cmd, opt, qualifier;

      cmd = retval >> 16;
      opt = (retval >> 8) & 255;
      qualifier = retval & 255;
      if (!cmd) 
	{
	  cmd = opt;
	  opt = qualifier;
	  qualifier = -1;
	}
      if (!cmd) 
	{
	  cmd = opt;
	  opt = -1;
	}
      printf("Got:  ");
      prcmd(cmd, opt, qualifier);
    }
  return retval;
}


/* Send a telnet command */
static void sendcmd(cmd, opt)
     int cmd, opt;
{
  static unsigned char msg[] = { IAC, 0, 0 };
  msg[1] = cmd;
  msg[2] = opt;
  sendbuff(msg, sizeof msg);
  flush();
  if (trace) 
    {
      printf("Sent: ");
      prcmd(cmd, opt, -1);
    }
}


/* Send data to remote system, checking for imbedded IACs */
tel_put(buff, length)
     register unsigned char *buff;
     register int length;
{
  register unsigned char *bp;
  static unsigned char iac[] = { IAC };
  while (length > 0) 
    {
      /* Look for an IAC */
      bp = (unsigned char *) memchr(buff, IAC, length);

      /* If none found, just send the whole buffer */
      if (!bp) 
	{
	  sendbuff(buff, length);
	  return 0;
	}

      /* Otherwise double the IAC */
      sendbuff(buff, bp - buff + 1);
      sendbuff(iac, sizeof iac);
      length -= bp - buff + 1;
      buff = bp + 1;
    }
  return 0;
}


/* Gather data to be sent in output buffer */
static void sendbuff(buff, length)
     register unsigned char *buff;
     int length;
{
  int freelen;

  /* Get amount of free space left in output buffer */
  freelen = BUFSIZE - (outp - outbuff);

  /* If there's room for this message then copy it */
  if (freelen >= length) 
    {
      memcpy(outp, buff, length);
      outp += length;
    }

  /* If output buffer is empty and this message is huge, send it directly */
  else if (outp == outbuff)
    {
      sendit(buff, BUFSIZE);
      sendbuff(&buff[BUFSIZE], length - BUFSIZE);
    }
  
  /* Otherwise fill up the buffer, flush it, and try again */
  else 
    {
      memcpy(outp, buff, freelen);
      flush();
      sendbuff(&buff[freelen], length - freelen);
    }
}


/* Flush the output buffer */
static void flush()
{
  sendit(outbuff, outp - outbuff);
  outp = outbuff;
}


/* Send data to remote system */
static void sendit(buff, length)
     register unsigned char *buff;
     register int length;
{
  static int off = 0, on = 1;
  if (trace) 
    {
      printf("Sending %d bytes:\n", length);
      dumpbuff(buff, length);
      putchar('\n');
    }
  while (length > 0)
    {
      int n = send(s, buff, length, 0);
      if (n < 0)
	{
	  /* Normally, I/O is non-blocking.  In the rare event that no
	     space is available in the sending socket, we switch
	     temporarily to blocking I/O to do the send.  */

	  if (errno != EWOULDBLOCK) errorm("Send data failed");
	  if (ioctl(s, FIONBIO, &off) < 0) errorm("Ioctl failed");
	  n = send(s, buff, length, 0);
	  if (n < 0) errorm("Send data failed");
	  if (ioctl(s, FIONBIO, &on) < 0) errorm("Ioctl failed");
	}
      if (n == 0) error("Send data failed:  No data sent");
      buff += n;
      length -= n;
    }
}


/* Send end of record */
tel_eor()
{
  static unsigned char msg[] = { IAC, EOR };
  sendbuff(msg, sizeof msg);
  flush();
  if (trace) printf("Sent: EOR\n");
}


/* Send break signal */
tel_break()
{
  static unsigned char msg[] = { IAC, BREAK };
  sendbuff(msg, sizeof msg);
  flush();
  if (trace) printf("Sent: BREAK\n");
}


/* Close the TCP/IP connection */
tel_close()
{
  close(s);
}


/* Print a readable version of a telnet command code */
static void prcmd(cmd, opt, qualifier)
     int cmd, opt, qualifier;
{
  if (cmd >= SE) printf(telcmds[cmd - SE]);
  else if (cmd == EOR) printf("EOR");
  else printf("<CMD %d>", cmd);
  putchar(' ');
  if (opt >= 0 && opt < NTELOPTS) printf(telopts[opt]);
  else if (opt > 0) printf("<Option %d>", opt);
  if (qualifier == 0) printf(" IS");
  else if (qualifier == 1) printf(" SEND");
  putchar('\n');
}
