/* $Id: rex.C,v 1.63 2001/12/17 21:51:59 kaminsky Exp $ */

/*
 *
 * Copyright (C) 2000-2001 Eric Peterson (ericp@lcs.mit.edu)
 * Copyright (C) 2000-2001 Michael Kaminsky (kaminsky@lcs.mit.edu)
 * Copyright (C) 2000 David Mazieres (dm@uun.org)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 *
 */

#include "rex.h"
#include <termios.h>
#ifndef TIOCGWINSZ
#include <sys/ioctl.h>
#endif

static bool garbage_bool;

struct module_pair {
  str local;
  str remote;
};


/* todo: this should be moved into global include, since it's defined
         elsewhere */
#define XPREFIX "/tmp/.X11-unix/X"

static rexsession *sess;
static vec<u_int16_t> rpforward;
vec<module_pair> modules;
struct in_addr localhost;
static bool opt_notty = false;
static bool opt_noX = false;
static bool opt_verbose = false;
static bool opt_forceconnect = false;
static int exitstatus = 0;

void madesession (vec <str> command);

//////////////////////////////////tty functions

/* Terminal modes, as saved by enter_raw_mode. */
static struct termios saved_tio;
static bool in_raw_mode = false;

EXITFN (leave_raw_mode);

str
windowsizetostring (struct winsize *size)
{
  if (ioctl (STDIN_FILENO, TIOCGWINSZ, (char *) size) < 0)
    fatal ("TIOCGWINSZ error\n");

  return strbuf() << size->ws_row << "\n" << size->ws_col <<
    "\n" << size->ws_xpixel << "\n" << size->ws_ypixel << "\n";
}

/* The following code (2 functions) is from the OpenSSH source. */
static void
leave_raw_mode()
{
  if (!in_raw_mode)
    return;

  if (tcsetattr (fileno (stdin), TCSADRAIN, &saved_tio) < 0)
    warn << "leave_raw_mode: tcsetattr: " << strerror (errno) << "\n";
  else
    in_raw_mode = false;
}

/*from openssh*/
static void
enter_raw_mode()
{   
  struct termios tio;   

  if (in_raw_mode)
    return;

  if (tcgetattr (fileno (stdin), &tio) < 0)
    warn << "enter_raw_mode: tcgetattr: " << strerror (errno) << "\n";

  saved_tio = tio;
  tio.c_iflag |= IGNPAR;
  tio.c_iflag &= ~(ISTRIP | INLCR | IGNCR | ICRNL | IXON | IXANY | IXOFF);
  tio.c_lflag &= ~(ISIG | ICANON | ECHO | ECHOE | ECHOK | ECHONL);
#ifdef IEXTEN
  tio.c_lflag &= ~IEXTEN;
#endif /* IEXTEN */
  tio.c_oflag &= ~OPOST;
  tio.c_cc[VMIN] = 1;
  tio.c_cc[VTIME] = 0;

  if (tcsetattr (fileno (stdin), TCSADRAIN, &tio) < 0)
    warn << "enter_raw_mode: tcsetattr: " << strerror (errno) << "\n";
  else
    in_raw_mode = true;
        
  // XXX: this should also exit
  //  sess->setendcb (wrap (leave_raw_mode));
}

////////////X forwarding utility functions


static void
endcbcalled () {
//   warn ("endcb called\n");
  exit (exitstatus);
}
  
//////////////////////////////////tty channel classes


class ttyfd : public unixfd
{
private:
  bool saw_newline;
  bool saw_escape;
  char escape_switch_char;

  void rcb (const str data, int err) {
    vec<u_int> escape_positions;
    str new_data ("");

    for (u_int i = 0; i < data.len (); i++) {
      if (saw_escape) {
	escape_switch_char = data[i];
	switch (escape_switch_char) {
	case '?':
	  escape_positions.push_back (i);
	  warnx << "Escape sequences:\n\r"
		<< "~.  - terminate connection\n\r"
		<< "~^Z - suspend connection\n\r"
		<< "~?  - help\n\r"
		<< "~~  - send the escape character\n\r"
		<< "(Escape sequences are recognized only after a newline.)\r\n";
	  break;
	case '.':
	  leave_raw_mode ();
	  warn << "\nTerminating rex connection\n";
	  unixfd::rcb (NULL, 0);
	  exit (-1);
	case 'Z' - 64:
	  escape_positions.push_back (i);
	  leave_raw_mode ();
	  warn << "\nSuspending rex connection\n";
	  kill(getpid(), SIGTSTP);
	  enter_raw_mode ();
	  break;
	case '~':
	  break;
	default:
	  if (i > 0)
	    escape_positions.pop_back ();
	  else
	    new_data = strbuf () << "~";
	  break;
	}
      }
      if (saw_newline && data[i] == '~') {
	saw_escape = true;
	escape_positions.push_back (i);
      }
      else
	saw_escape = false;
      if (data[i] == '\r' || data[i] == '\n')
	saw_newline = true;
      else
	saw_newline = false;
    }

    u_int prev_pos = 0;
    for (u_int i = 0; i < escape_positions.size (); i++) {
      new_data = strbuf () << new_data 
			   << substr (data, prev_pos, 
				      escape_positions[i] - prev_pos);
      prev_pos = escape_positions[i] + 1;
    }
    new_data = strbuf () << new_data 
			 << substr (data, prev_pos, data.len () - prev_pos);
    unixfd::rcb (new_data, err);
  }

public:
  ttyfd (rexchannel *pch, int fd, int localfd)
    : unixfd::unixfd (pch, fd, localfd, true, true),
      saw_newline (false), saw_escape (false) {}
    
  ~ttyfd ()
  {
    //todo : should check for port forwarding channels and
    //       prompt user (like ssh)
    //     warn << "--entering ~ttyfd\n";
    leave_raw_mode ();
  }
};

class ttychannel : public rexchannel
{

  int masterfd;

  static vec <str>
  fixttypath (vec <str> command)
  {
    vec <str> ttycommand;
    
    ttycommand.push_back ("ttyd");  
    ttycommand.push_back (myname ());

    for (int ix = 0; implicit_cast <u_int32_t> (ix) < command.size (); ix++)
      ttycommand.push_back (command[ix]);

    return ttycommand;
  }

  
public:
  ttychannel (rexsession *sess, vec <str> command) :
    rexchannel::rexchannel (sess, 1, fixttypath (command)) {}

  void
  newfd (svccb *sbp)
  {
    rexcb_newfd_arg *arg = sbp->template getarg<rexcb_newfd_arg> ();

    masterfd = arg->newfd;
    vNew refcounted<ttyfd> (this, masterfd, 0);
    
    enter_raw_mode ();
    sbp->replyref (true);

    sendnewwindowsize();   
  }

  void
  sendnewwindowsize ()
  {
    struct winsize windowsize;
    str sws = windowsizetostring (&windowsize);

    rex_payload arg;
    arg.channel = channo;
    arg.fd = 0;
  
    arg.data.set (const_cast<char *> (sws.cstr ()), 
		  sws.len (), freemode::NOFREE);

    proxy->call (REX_DATA, &arg, &garbage_bool, aclnt_cb_null);

    sigcb (SIGWINCH, wrap (this, &ttychannel::sendnewwindowsize));
  }
    
  void
  exited (int status) {
    exitstatus = status;
    rexchannel::exited (status);
  }

  void
  madechannel (int error)
  {
    if (error)
      exit (error);
    //pure rexfd acts like dummy fd
    vNew refcounted<rexfd> (this, 0);
    ::sess->setendcb (wrap (endcbcalled));
  }

  virtual
  ~ttychannel ()
  {
    //     warn << "reached ~ttychannel\n";
    sigcb (SIGWINCH, NULL);
    ::sess->quit ();
  }

};



////////////////////////////channels used to exec command w/o tty support

class execfd : public unixfd
{
private:
  int *numleft;

  void rcb (const str data, int err) {
    unixfd::rcb (data, err);
  }

public:
  execfd (rexchannel *pch, int fd, int localfd, int *numleft) :
    unixfd (pch, fd, localfd, localfd == 2, true), numleft (numleft)
  {}

  virtual ~execfd ()
  {
    //todo : this should be changed so that it is ssh like, prompting
    //       that there are remaining connections, etc.
    //     warn ("--reached ~execfd with %d execfds left\n", *numleft);
    if (!--*numleft)
      sess->quit ();
  }
};

class execchannel : public rexchannel
{
private:
  int fdsleft;
  
public:
  execchannel (rexsession *sess, vec <str> command) :
    rexchannel::rexchannel (sess, 3, command), fdsleft(3) {}

  void
  madechannel (int error)
  {
    if (error)
      exit (error);
    vNew refcounted<execfd> (this, 0, 0, &fdsleft);
    vNew refcounted<execfd> (this, 1, 1, &fdsleft);
    vNew refcounted<execfd> (this, 2, 2, &fdsleft);
    ::sess->setendcb (wrap (endcbcalled));
  }

  void
  exited (int status) {
    exitstatus = status;
    rexchannel::exited (status);
  }
};

class rpfsocklistenfd : public rexfd {
  bool isunix;
  u_int16_t port;
  str unixpath;
  cbv::ptr bindcb;

  void
  connected (svccb *sbp, int fd)
  {   
    if (fd < 0) {
      warn << "reverse tcp port forward connection : " << strerror (errno);
      sbp->replyref (false);
      return;
    }

    rexcb_newfd_arg *parg = sbp->template getarg<rexcb_newfd_arg> ();
    vNew refcounted<unixfd> (pch, parg->newfd, fd);
    sbp->replyref (true);
  }

public:
  rpfsocklistenfd (rexchannel *pch, str unixpath, cbv::ptr bindcb)
    : rexfd (pch, 0), isunix (true), unixpath (unixpath), bindcb (bindcb) {}
  rpfsocklistenfd (rexchannel *pch, u_int16_t port, cbv::ptr bindcb)
    : rexfd (pch, 0), isunix (false), port (port), bindcb (bindcb) {}
  
  void newfd (svccb *sbp) {
    rexcb_newfd_arg *parg = sbp->template getarg<rexcb_newfd_arg> ();
        
    if (isunix) {
      int fd = unixsocket_connect (unixpath.cstr ());
      if (fd < 0) {
	warn << "\nunix domain socket connect to "
	     << unixpath << " failed: " << strerror (errno);
	sbp->replyref (false);
	return;
      }
      else {
	vNew refcounted<unixfd> (pch, parg->newfd, fd);
	warn << "\nreverse forwarding channel created\n";
	sbp->replyref (true);
      }
    }
    else
      tcpconnect (localhost, port,
		  wrap(this, &rpfsocklistenfd::connected, sbp));
  }

  void data (svccb *sbp) {
    rex_payload *argp = sbp->template getarg<rex_payload> ();
    
    if (!argp->data.size ()) {
      // let rexfd handle eof
      rexfd::data (sbp);
      return;
    }

    sbp->replyref (true);
    if (bindcb) {
      bindcb ();
      bindcb = NULL;
    }
  }
};

class rpfchannel : public rexchannel {
  /* todo : might want to add descriptive name field to these for
     pending connection listing during shutdown  */

  bool isunix;
  u_int16_t port;
  str unixpath;
  cbv::ptr bindcb;

  void
  connected (svccb *sbp, int fd)
  {   
    if (fd < 0) {
      warn << "reverse tcp port forward connection : " << strerror (errno);
      sbp->replyref (false);
      return;
    }

    rexcb_newfd_arg *parg = sbp->template getarg<rexcb_newfd_arg> ();
    vNew refcounted<unixfd> (this, parg->newfd, fd);
    sbp->replyref (true);
  }

  static vec<str>
  port2command (u_int16_t port)
  {
    vec <str> command;
    command.push_back ("socklisten");
    command.push_back (strbuf () << port);
    return command;
  }

  static vec<str>
  port2command (str remoteunixpath)
  {
    vec <str> command;
    command.push_back ("socklisten");
    command.push_back ("-u");
    command.push_back (remoteunixpath);
    return command;
  }

  
 
public:
  rpfchannel (rexsession *sess, u_int16_t port, u_int16_t rport,
	      cbv::ptr bindcb) :
    rexchannel::rexchannel (sess, 1, port2command (rport)),
    isunix (false), port (port), bindcb (bindcb) {}

  rpfchannel (rexsession *sess, str runixpath, str localunixpath,
	      cbv::ptr bindcb) :
    rexchannel::rexchannel (sess, 1, port2command (runixpath)),
    isunix (true), unixpath (localunixpath), bindcb (bindcb) {}


  void madechannel (int) {
    if (isunix)
      vNew refcounted<rpfsocklistenfd> (this, unixpath, bindcb);
    else
      vNew refcounted<rpfsocklistenfd> (this, port, bindcb);
    bindcb = NULL;
  }

  virtual
  ~rpfchannel ()
  {
    rex_int_arg arg;
    arg.channel = channo;
    arg.val = 15;
    proxy->call (REX_KILL, &arg, &garbage_bool, aclnt_cb_null);
  }
    
};






class xclientfd : public unixfd {
  size_t x11_data_len;
  char *x11_real_data;
  char *x11_fake_data;
  char *x11_proto;

  bool cookie_replaced;
  str conn_head;

public:
  xclientfd (rexchannel *pch, int fd, int localfd,
	     char *x11_real_data, char *x11_fake_data, size_t x11_data_len,
	     char *x11_proto) :
    unixfd (pch, fd, localfd), x11_data_len (x11_data_len),
    x11_real_data(x11_real_data), x11_fake_data(x11_fake_data),
    x11_proto(x11_proto), cookie_replaced (false), conn_head ("")
  {}

  void
  data (svccb *sbp) {
    //XXX: replace this with unixfd, so we don't have to dispatch to unixfd
    //     on every data write, inefficient
    if (cookie_replaced) {
      unixfd::data (sbp);
      return;
    }
    
    rex_payload *argp = sbp->template getarg<rex_payload> ();
    size_t len = argp->data.size (); 
    
    if (len) {
      u_int proto_len, data_len;
      conn_head = strbuf () << conn_head << str (argp->data.base (), len);
      size_t chlen = conn_head.len ();
      if (chlen >= 12) {
	if (conn_head[0] == 0x42) {	/* Byte order MSB first. */
	  proto_len = 256 * conn_head[6] + conn_head[7];
	  data_len = 256 * conn_head[8] + conn_head[9];
	} else if (conn_head[0] == 0x6c) {	/* Byte order LSB first. */
	  proto_len = conn_head[6] + 256 * conn_head[7];
	  data_len = conn_head[8] + 256 * conn_head[9];
	} else {
	  warn ("Initial X11 packet contains bad byte order byte: 0x%x",
		conn_head[0]);
	  sbp->replyref (false);
	  delete this;
	  return;
	}

	/* Check if the whole packet is in buffer. */
	if (chlen < 12 + ((proto_len + 3) & ~3) +
	    ((data_len + 3) & ~3)) {
	  sbp->replyref (true);
	  return;
	}

	/* Check if authentication protocol matches. */
	if (proto_len != strlen (x11_proto) ||
	    memcmp (conn_head.cstr () + 12, x11_proto, proto_len) != 0) {
	  warn ("X11 connection uses different authentication protocol.");
	  sbp->replyref (false);
	  delete this;
	  return;
	}

	/* Check if authentication data matches our fake data. */
	if (data_len != x11_data_len ||
	    memcmp (conn_head.cstr () + 12 + ((proto_len + 3) & ~3),
		    x11_fake_data, x11_data_len) != 0) {
	  warn ("X11 auth data does not match fake data.");
	  sbp->replyref (false);
	  delete this;
	  return;
	}
	
	/*
	 * Received authentication protocol and data match
	 * our fake data. Substitute the fake data with real
	 * data.
	 */
	char *buf = (char *)xmalloc (chlen);
	memcpy (buf, conn_head.cstr (), chlen);
	memcpy (buf + 12 + ((proto_len + 3) & ~3),
		x11_real_data, x11_data_len);
	
	cookie_replaced = true;
	paios << str (buf, chlen);
	free (buf);
      }
    }
    sbp->replyref (true);
  }

  
};

#ifdef PATH_XAUTH
struct xauthfd : public rexfd {  
  xauthfd (rexchannel *pch, int fd, str xauthcmd) : rexfd (pch, fd) {
    static bool garbage_bool;
    rex_payload arg;
    arg.channel = channo;
    arg.fd = fd;
      
    arg.data.set (const_cast<char *> (xauthcmd.cstr ()), xauthcmd.len ());

    proxy->call (REX_DATA, &arg, &garbage_bool,
		 aclnt_cb_null);

    arg.data.set (NULL, 0);
    proxy->call (REX_DATA, &arg, &garbage_bool,
		 aclnt_cb_null);
  }
};

class xauthchannel : public rexchannel {
  str xauthcmd;
  cbv::ptr xauthdone_cb;
  
  str xauthcmdline (str rdisplay, const char *protocol, const char *hexcookie)
  {
    return strbuf ("add %s %s %s\n", rdisplay.cstr (), protocol, hexcookie);
  }

  vec<str> authcmd () {
    vec<str> cmd;
    cmd.push_back (PATH_XAUTH);
    return cmd;
  }

public:
  xauthchannel (rexsession *sess, str rdisplay, const char *protocol,
		const char *hexcookie, cbv xauthdone_cb)
               : rexchannel::rexchannel (sess, 1, authcmd ()),
		 xauthcmd (xauthcmdline (rdisplay, protocol, hexcookie)),
		 xauthdone_cb (xauthdone_cb)
  {}

  ~xauthchannel () { xauthdone_cb (); }

  void madechannel (int error) {
    if (error) {
      warn << "failed to run xauth on server\n";
      sess->remove_chan (channo);
    }
    else
      vNew refcounted<xauthfd> (this, 0, xauthcmd);
  }
};


class xsocklistenfd : public rexfd {
  bool isunix;

  //isunix == false
  struct in_addr displayhost;
  u_int16_t displayport;

  //isunix == true
  str unixpath;
  
  str rdisplay;
  bool gotrdisplay;
  cbv::ptr xfreadycb;

  size_t x11_data_len;
  char *x11_real_data;
  char *x11_fake_data;
  char x11_proto[512];
  
  void
  tcpconnected (svccb *sbp, int fd) {   
    if (fd < 0) {
      warn << "X port forward connection to localhost:" << displayport <<
	" failed: " << strerror (errno);
      sbp->replyref (false);
      return;
    }
    
    rexcb_newfd_arg *parg = sbp->template getarg<rexcb_newfd_arg> ();
    vNew refcounted<xclientfd> (pch, parg->newfd, fd, x11_real_data,
				x11_fake_data, x11_data_len,
				implicit_cast<char *> (x11_proto));
    sbp->replyref (true);
  }

  // returns display number if unix domain display, otherwise -1
  static int xconninfo_unix (const char *display) {
    int displaynum;
    
    if (!display)
      return -1;

    char *ocolon = strrchr (display, ':');
    if (!ocolon)
      return -1;
    
    if (display[0] == ':' || !strncmp (display, "unix:", 5)) {
      if (sscanf(ocolon + 1, "%d", &displaynum) != 1)
	return -1;
      return displaynum;
    }
    
    return -1;
  }
  
  // returns 1 on success, -1 on failure
  static int xconninfo_tcp (const char *display, struct in_addr &pia,
			    u_int16_t &port) {
    if (!display)
      return -1;

    char *ocolon = strrchr (display, ':');
    if (!ocolon || ocolon < (display + 1))
      return -1;
    
    str hostname (display, ocolon - display);
    struct hostent *ph;
    
    if ((ph = gethostbyname(hostname.cstr ())) &&
	sscanf (ocolon + 1, "%hu", &port) == 1) {
      port += 6000;
      pia = * (struct in_addr *) (ph->h_addr);
      return 1;
    }
    else
      return -1;
  }
  
  
public:
  xsocklistenfd (rexchannel *pch, const char *display,
		 cbv xfreadycb = cbv_null) : rexfd (pch, 0), rdisplay (""),
					     gotrdisplay (false),
					     xfreadycb (xfreadycb) {
    int displaynum;
    if ((displaynum = xconninfo_unix (display)) >= 0) {
      isunix = true;
      unixpath = strbuf () << XPREFIX << displaynum;
    }
    else if (xconninfo_tcp (display, displayhost, displayport) > 0)
      isunix = false; 
  }

  void
  newfd (svccb *sbp) {
    rexcb_newfd_arg *parg = sbp->template getarg<rexcb_newfd_arg> ();
      
    if (isunix) {
      int fd = unixsocket_connect (unixpath.cstr ());
      if (fd < 0) {
	warn << "X port forward connection to localhost:" << displayport <<
	  " failed: " << strerror (errno);
	return;
      }
      else {
	vNew refcounted<xclientfd> (pch, parg->newfd, fd, x11_real_data,
				    x11_fake_data, x11_data_len,
				    implicit_cast<char *> (x11_proto));
	//warn << "X forwarding channel created\n";
	sbp->replyref (true);
      }
    }
    else
      tcpconnect (displayhost, displayport,
		  wrap(this, &xsocklistenfd::tcpconnected, sbp));
  }

  void
  mkauthchannel (clnt_stat) {
    //XXX: replace these arrays with str's, w/o hardcoded length
    char line[512];
    char data[512];
    char fake_data[512];
    
    FILE *f;
    
    snprintf (line, sizeof (line), "%s list %.200s", 
	     PATH_XAUTH, getenv("DISPLAY"));
    f = popen (line, "r");
    if (f && fgets (line, sizeof (line), f) &&
	sscanf (line, "%*s %s %s", x11_proto, data) == 2) {
      if (opt_verbose)
	warn << "got xauth info\n";
    }
    else {
      //if client side doesn't support xauth, fill in with junk, it will ignore
      strcpy (x11_proto, "MIT-MAGIC-COOKIE-1");
      for (int ix = 0; ix < 32; ix++)
	data[ix] = '0';
      data[32] = NULL;
    }
    if (f)
      pclose (f);

    size_t data_len = strlen (data) / 2;

    x11_fake_data = (char *) xmalloc(data_len);
    rnd.getbytes (x11_fake_data, data_len);

    x11_real_data = (char *) xmalloc (data_len);
    for (u_int i = 0; i < data_len; i++) {
      u_int32_t value;
      if (sscanf (data + 2 * i, "%2x", &value) != 1)
	fatal("x11_request_forwarding: bad authentication data: %.100s", data);
      x11_real_data[i] = value;

      //x11_fake_data to string so it can be installed on server with xauth cmd
      sprintf (fake_data + 2 * i, "%02x", (u_char) x11_fake_data[i]);
    }
    x11_data_len = data_len;

    ::sess->makechannel (New refcounted <xauthchannel>(::sess, rdisplay, 
						       x11_proto, fake_data, 
						       xfreadycb));
    xfreadycb = NULL;
  }

  void
  data (svccb *sbp) {
    rex_payload *argp = sbp->template getarg<rex_payload> ();

    // length w/o newline
    size_t lenwonl = argp->data.size ();
    if (!lenwonl) {
      // let rexfd handle eof
      rexfd::data (sbp);
      return;
    }

    // ignore anything after DISPLAY has been sent
    if (gotrdisplay) {
      sbp->replyref (true);
      return;
    }

    if (argp->data.base ()[lenwonl - 1] == '\n') {
      lenwonl--;
      gotrdisplay = true;
    }

    rdisplay = strbuf () << rdisplay << str (argp->data.base (), lenwonl);

    if (gotrdisplay) {
      rex_setenv_arg arg;
      arg.name = str ("DISPLAY");
      arg.value = rdisplay;
      proxy->call (REX_SETENV, &arg, &garbage_bool,
		   wrap (this, &xsocklistenfd::mkauthchannel));
    }
    sbp->replyref (true);
  }

};

class xfchannel : public rexchannel {

  const char *display;
  cbv::ptr xfreadycb;

  vec <str> xcommand () {    
    vec <str> cmd;
    cmd.push_back ("socklisten");
    cmd.push_back ("-x");
    return cmd;
  }
  
public:
  xfchannel (rexsession *sess, const char *display, cbv xfreadycb = cbv_null):
            rexchannel (sess, 1, xcommand ()),
            display (display), xfreadycb (xfreadycb) {}

  void madechannel (int) {
    vNew refcounted<xsocklistenfd> (this, display, xfreadycb);
    xfreadycb = NULL;
  }

  void
  exited (int status) {
    // Check to make sure really an error and not just
    // the end of the connection
    if (!opt_noX) { 
      if (status < 0)
	fatal << "Could not set up X-forwarding (socklisten caught "
	      << "signal " << -status << ").\n";
      else if (status > 0)
	fatal << "Could not set up X-forwarding (socklisten exited "
	      << "with status = " << status << ").\n";
    }
    rexchannel::exited (status);
  }
};
#endif /* PATH_XAUTH */

class binmodulechannel : public rexchannel {

  str local;
  str remote;
  vec<int> pfds;
  cbv::ptr modulereadycb;
  
  vec<str> cmd2vec (str cmdstr) {
    char *cmd = const_cast<char *> (cmdstr.cstr ());
    vec<str> cmdvec;
    char *word, *sep = "\t ";
    for (word = strtok(cmd, sep); word; word = strtok(NULL, sep))
      cmdvec.push_back (word);
    return cmdvec;
  }
  
public:
  binmodulechannel (rexsession *sess, str localcmd, str remotecmd,
		    cbv::ptr modulereadycb = NULL)
    : rexchannel (sess, 3, cmd2vec (remotecmd)), local (localcmd),
      remote (remotecmd), modulereadycb (modulereadycb)
  {
    vec<str> localargs = cmd2vec (localcmd);
    if (!localargs.size ()) {
      warn << "null module command specified, ignoring\n";
      return;
    }
    str s = find_program_plus_libsfs (localargs[0]);
    if (!s) {
      warn << "Could not locate program: " << localargs[0] << "\n";
      return;
    }

    vec<char *> av;
    for (u_int i = 0; i < localargs.size (); i++)
      av.push_back (const_cast<char *> (localargs[i].cstr ()));
    av.push_back (NULL);
    
    vec<int> cfds;
    for (int ix = 0; ix <= 2; ix++) {
      int socks[2];
      if (socketpair (AF_UNIX, SOCK_STREAM, 0, socks) < 0) {
	warn ("failed to create socketpair for local module: %m\n");
      	return;
      }
      pfds.push_back (socks[0]);
      cfds.push_back (socks[1]);
    }
        
    aspawn (s, av.base (), cfds[0], cfds[1], cfds[2]);
    for (int i = 0; implicit_cast<unsigned> (i) < cfds.size (); i++)
      close (cfds[i]);  
  }

  void madechannel (int) {
    vNew refcounted<unixfd> (this, 0, pfds[0], false, true);
    vNew refcounted<unixfd> (this, 1, pfds[1], false, true);
    vNew refcounted<unixfd> (this, 2, pfds[2], false, true);

    if (modulereadycb) {
      modulereadycb ();
      modulereadycb = NULL;
    }
  }
};


static void
usage ()
{
  fatal << "usage: " << progname << " [options] destination [command]\noptions:\n"
        << "-T            Disable pseudo-tty allocation\n"
        << "-A            Disable SFS agent forwarding\n"
        << "-X            Disable X forwarding\n"
        << "-R port:lport Forward tcp connections made to port on remote host\n"
        << "              to lport on localhost\n"
	<< "-p            Force connect to destination\n"
	<< "-v            Verbose"
	<< "\n"
	<< "destination is one of the following:\n"
	<< "  * a self-certifying hostname (location:hostid)\n"
	<< "  * a self-certifying pathname (/sfs/... or /symlink-to-sfs/...)\n"
	<< "  * any identifier which when processed through certification programs\n"
	<< "    will yield a self-certifying pathname\n";
}

int
main (int argc, char **argv)
{
  setprogname (argv[0]);
  sfsconst_init ();
  random_init ();

  u_int16_t port, rport;
  int ch;
  bool forwardagent = true;
  
  while ((ch = getopt (argc, argv, "Avm:TXpR:")) != -1)
    switch (ch) {
    case 'A':
      forwardagent = false;
      break;
    case 'm':
      {
	module_pair mp;
	if (optind < argc - 1 && argv[optind][0] != '-') {
	  mp.local = argv[optind - 1];
	  mp.remote = argv[optind];
	  modules.push_back (mp);
	  optind++;                 //skip over second argument
	}
	else
	  usage ();
	break;
      }
    case 'T':
	opt_notty = true;
	break;
    case 'v':
	opt_verbose = true;
	break;
    case 'X':
	opt_noX = true;
	break;
    case 'p':
	opt_forceconnect = true;
	break;
    case 'R':
      if(sscanf(optarg, "%hu:%hu", &port, &rport) == 2) {
	rpforward.push_back(port);
	rpforward.push_back(rport);
      }
      else usage ();
      break;
    default:
      usage ();
    }
  argc -= optind;
  argv += optind;

  if (argc < 1)
    usage ();

  str sfs_schost;
  int sfs_scherr;
  sfs_scherr = path2sch (argv[0], &sfs_schost);

  if ((!sfs_schost || !sfs_schost.len ())
      || (sfs_scherr && !opt_forceconnect)) {
    warn ("could not resolve self-certifying hostname from %s\n", argv[0]);
    fatal ("lookup returned: %s\n", strerror (sfs_scherr));
  }

  if (sfs_scherr && opt_forceconnect)
    warn << "Ignoring error: " << strerror (sfs_scherr) << "\n";
  if (opt_verbose)
    warn << "Connecting to " << sfs_schost << "\n";

  if (!isatty (STDIN_FILENO))
    opt_notty = true;
    
  vec <str> command;
  if (argc == 1) {
    //special value signifying default shell
    command.push_back (".");
  }
  else {
    for (int i = 1; i < argc; i++)
      command.push_back (argv[i]);
  }

  struct hostent *plh;
  if(!(plh = gethostbyname("localhost")))
      fatal << "could not look up localhost address";

  localhost = *(struct in_addr *) plh->h_addr;

  sess = New rexsession (wrap (madesession, command), sfs_schost,
			 forwardagent);
  if (opt_verbose)
    sess->set_verbose (true);

  amain ();
}

void
create_interactive_chan (ref<int> pfchanleft, vec <str> command) 
{
  if (--*pfchanleft <= 0) {
    vec<rex_envvar> env;
    rex_env renv;

    if (opt_noX)
      env.push_back (strbuf () << "DISPLAY=");

    if (opt_notty) {
      renv.set (env.base (), env.size ());
      sess->makechannel (New refcounted <execchannel> (sess, command), renv);
    } else {
      if (char *term = getenv ("TERM"))
	env.push_back (strbuf () << "TERM=" << term);
      renv.set (env.base (), env.size ());
      sess->makechannel (New refcounted <ttychannel> (sess, command), renv);
    }
  }
}

void
create_interactive_chan_acb (ref<int> pfchanleft, vec <str> command,
			     clnt_stat err) {
  create_interactive_chan (pfchanleft, command);
}

void
madesession (vec<str> command)
{
  ref<int> left = New refcounted<int> (rpforward.size () / 2 +
				       modules.size ());
  
#ifdef PATH_XAUTH
  char *display = getenv ("DISPLAY");
  if (!display && !opt_noX) {
    warn << "DISPLAY environment variable is not set..."
	 << "disabling X forwarding\n";
    opt_noX = true;
  }
  if (!opt_noX) {
    ++*left;
    sess->makechannel
      (New refcounted<xfchannel> (sess, display, wrap (create_interactive_chan,
						       left, command)));
  }
#else /* PATH_XAUTH not defined */
  if (!opt_noX) {
    warn << "The xauth program was not found at compile time..."
	 << "disabling X forwarding\n";
    opt_noX = true;
  }
#endif /* PATH_XAUTH */

  if (!*left) {
    create_interactive_chan (left, command);
    return;
  }
  
  for (int ix = 0; implicit_cast<size_t> (ix) < modules.size (); ix++)
    sess->makechannel
      (New refcounted<binmodulechannel> (sess, modules[ix].local,
					 modules[ix].remote,
					 wrap (create_interactive_chan,
					       left, command)));
  
  //make port forwarding channels in parallel but last one should
  //trigger tty channel creation
  for (int ix = 0; implicit_cast<size_t> (ix) < rpforward.size (); ix+=2)
    sess->makechannel (New refcounted <rpfchannel>
		       (sess, rpforward[ix], rpforward[ix + 1],
			wrap (create_interactive_chan, left, command)));
  
}
