/* $Id: sfsagent.C,v 1.72 2002/11/27 22:51:39 max Exp $ */

/*
 *
 * Copyright (C) 1998, 1999 David Mazieres (dm@uun.org)
 * Copyright (C) 1999, 2000 Michael Kaminsky (kaminsky@lcs.mit.edu)
 *
 * 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 "agent.h"


EXITFN (cleanup);

authmgr gmgr;

list<sfsagent, &sfsagent::link> agents;

sfsagent *cdagent;

bool opt_forwarding = true;
bool opt_killstart;
bool opt_nodaemon;
str opt_keytime;
str opt_socket;

static void start_listening ();

static bool rndok;
static void
setrnd ()
{
  random_update ();
  rndok = true;
}
inline void
checkrnd ()
{
  while (!rndok)
    acheck ();
}

static inline axprt_unix *
xprt2unix (axprt *x)
{
  // XXX - dynamic_cast is busted in egcs
  axprt_unix *ux = static_cast<axprt_unix *> (&*x);
  if (typeid (*ux) == typeid (refcounted<axprt_unix>))
    return ux;
  else
    return NULL;
}

sfsagent::sfsagent (int fd)
  : x (axprt_unix::alloc (fd)),
    cs (asrv::alloc (x, agentctl_prog_1, wrap (this, &sfsagent::dispatch)))
{
  agents.insert_head (this);
}

sfsagent::sfsagent (ref<axprt> xx)
  : x (xx),
    cs (asrv::alloc (x, agentctl_prog_1, wrap (this, &sfsagent::dispatch)))
{
  agents.insert_head (this);
}

sfsagent::~sfsagent ()
{
  agents.remove (this);
  if (this == cdagent) {
    warn << "exiting\n";
    exit (0);
  }
}

void
sfsagent::dispatch (svccb *sbp)
{
  if (!sbp) {
    if (name)
      warn << "EOF from " << name << "\n";
    delete this;
    return;
  }

  if (sbp->prog () == AGENTCB_PROG && sbp->vers () == AGENTCB_VERS)
    agentdisp (sbp);
  else if (sbp->prog () == AGENTCTL_PROG && sbp->vers () == AGENTCTL_VERS)
    ctldisp (sbp);
  else
    panic ("invalid prog/vers\n");
}

static void
lookupres (svccb *sbp, sfsagent_lookup_type typ, str path)
{
  sfsagent_lookup_res res (LOOKUP_NOOP);
  if (path && path.len () <= sfsagent_path::maxsize) {
    if (typ == LOOKUP_MAKELINK) {
      res.set_type (typ);
      *res.path = path;
    } else if (typ == LOOKUP_MAKEDIR) {
      res.set_type (typ);
      *res.dir = path;
    }
  }
  sbp->replyref (res);
}

static void
revokedres (svccb *sbp, const sfsagent_revoked_res *res)
{
  sbp->reply (res);
}

void
rexres (svccb *sbp, ptr<sfsagent_rex_res> res)
{
  if (!res)
    sbp->replyref (sfsagent_rex_res (false));
  else
    sbp->reply (res);
}

void
sfsagent::agentdisp (svccb *sbp)
{
  checkrnd ();
  switch (sbp->proc ()) {
  case AGENTCB_NULL:
    sbp->reply (NULL);
    break;
  case AGENTCB_AUTHINIT:
    if (name) {
      sfsagent_authinit_arg *aa = sbp->getarg<sfsagent_authinit_arg> ();
      if (aa->requestor) {
	str s;
	if (name == "sfscd")
	  s = "@LOCALHOST";
	else
	  s = name;
	aa->requestor = strbuf () << aa->requestor << s;
      }
    }
    gmgr.authinit (sbp);
    break;
  case AGENTCB_AUTHMORE:
    gmgr.authmore (sbp);
    break;
  case AGENTCB_LOOKUP:
    sfslookup (*sbp->getarg<sfs_filename> (), wrap (lookupres, sbp));
    break;
  case AGENTCB_REVOKED:
    revcheck (*sbp->getarg<filename3> (), wrap (revokedres, sbp));
    break;
  case AGENTCB_CLONE:
    if (axprt_unix *ux = xprt2unix (x)) {
      int fd = ux->recvfd ();
      if (fd < 0)
	warn << "failed to receive fd for AGENTCB_CLONE\n";
      else 
	vNew sfsagent (fd);
    }
    sbp->reply (NULL);
    break;
  default:
    warn ("invalid AGENTCB procno %d\n", sbp->proc ());
    sbp->reject (PROC_UNAVAIL);
  }
}

template<class T> void
pushit (vec<T> *vecp, const T &obj)
{
  vecp->push_back (obj);
}

void
pushsrpname (vec<sfsagent_srpname_pair> *vecp, const str &obj1, str *obj2)
{
  sfsagent_srpname_pair sn;
  sn.srpname = obj1;
  sn.sfsname = *obj2;
  vecp->push_back (sn);
}

void
sfsagent::keyinitcb (svccb *sbp, key *nk, str err)
{
  if (err) {
    warn << nk->name << ": initilization failure\n" << err << "\n";
    gmgr.remove (nk);
    delete nk;
    sbp->replyref (false);
    return;
  }
  sbp->replyref (true);
}

void
sfsagent::ctldisp (svccb *sbp)
{
  switch (sbp->proc ()) {
  case AGENTCTL_NULL:
    sbp->reply (NULL);
    break;
  case AGENTCTL_ADDEXTAUTH:
    {
      sfsagent_addextauth_arg *aa = 
	sbp->getarg<sfsagent_addextauth_arg> ();
      extauth *ea = New extauth (x, &gmgr, this);
      ea->pid = aa->pid;
      ea->expire = aa->expire;
      ea->name = aa->name;
      
      // what if a key has the same comment?  leave it in for now
      if (authmeth *am = gmgr.plookup (aa->pid)) {
	gmgr.remove (am);
	delete am;
      }
      gmgr.insert (ea);
      gmgr.timeout ();
      
      // tells sfscd that we've just added a new key
      agentmsg (AGENT_START);
      sbp->replyref (true);
      break;
    }
  case AGENTCTL_ADDKEY:
    {
      sfs_addkey_arg *aa = sbp->getarg<sfs_addkey_arg> ();
      key *nk; 
      nk = New key (&gmgr);
      // XXX - hack for testing new version of protocol. 
      nk->vers = aa->key_version;
      if (!(nk->privkey = sfscrypt.alloc (aa->privkey, SFS_SIGN))) {
	warn << "Cannot parse keypair\n";
	sbp->replyref (false);
	break;
      }
      if (authmeth *k = gmgr.klookup (*nk->privkey)) {
	gmgr.remove (k);
	delete k;
      }
      nk->expire = aa->expire;
      nk->name = aa->name;
      gmgr.insert (nk);
      gmgr.timeout ();
      agentmsg (AGENT_START);
      nk->privkey->init (wrap (this, &sfsagent::keyinitcb, sbp, nk));
      break;
    }
  case AGENTCTL_REMAUTH:
    {
      sfs_remauth_arg *ra = sbp->getarg<sfs_remauth_arg> ();
      authmeth *a;
      bool ok = false;
      switch (ra->type) {
      case SFS_REM_PUBKEY:
	{
	  ptr<sfspub> pk = sfscrypt.alloc (*ra->pubkey);
	  if (pk && (a = gmgr.klookup (*pk))) {
	    ok = true;
	    gmgr.remove (a);
	    delete a;
	  }
	  break;
	}
      case SFS_REM_NAME:
	while ((a = gmgr.clookup(*ra->name))) {
	  ok = true;
	  gmgr.remove (a);
	  delete a;
	}
	break;
      case SFS_REM_PID:
	if ((a = gmgr.plookup (*ra->pid))) {
	  ok = true;
	  gmgr.remove (a);
	  delete a;
	}
	break;
      }
      if (ok)
	agentmsg (AGENT_START);
      sbp->replyref (ok);
      break;
    }
  case AGENTCTL_REMALLKEYS:
    {
      gmgr.remove_all();
      agentmsg (AGENT_START);
      sbp->replyref (NULL);	// XXX race-prone
      break;
    }
  case AGENTCTL_DUMPKEYS:
    {
      sfs_keylist kl;
      gmgr.fill_keylist (&kl);
      sbp->reply (&kl);
      break;
    }
  case AGENTCTL_ADDCERTPROG:
    {
      sfsagent_certprog *arg = sbp->getarg<sfsagent_certprog> ();
      str av0;
      if (arg->av.size () > 0 && (av0 = find_program (arg->av[0]))) {
	bool found = false;
	size_t i, n = certprogs.size ();

	arg->av[0] = av0;
	while (n--)
	  if (arg->prefix == certprogs[n].prefix
	      && arg->filter == certprogs[n].filter
	      && arg->exclude == certprogs[n].exclude) {
	    for (i = 0; i < arg->av.size (); i++)
	      if (arg->av[i] != certprogs[n].av[i])
		break;
	    if (i == arg->av.size ())
	      found = true;
	  }
	if (!found) {
	  certprogs.push_back (*arg);
	  certfilters.push_back (rxfilter (arg->filter, arg->exclude));
	  agentmsg (AGENT_FLUSHNEG);
	}
	sbp->replyref (true);
      }
      else
	sbp->replyref (false);
      break;
    }
  case AGENTCTL_CLRCERTPROG_BYREALM:
    {
      size_t n = certprogs.size ();
      sfsagent_certprog cp;
      rxfilter rxf;
      sfsauth_realm *arg = sbp->getarg<sfsauth_realm> ();
      bool found = false;

      while (n--) {
	cp = certprogs.pop_front ();
	rxf = certfilters.pop_front ();
	if (cp.prefix != *arg) {
	  certprogs.push_back (cp);
	  certfilters.push_back (rxf);
	}
	else
	  found = true;
      }
      agentmsg (AGENT_FLUSHNEG);
      sbp->replyref (found);
      break;
    }
  case AGENTCTL_CLRCERTPROGS:
    certprogs.clear ();
    certfilters.clear ();
    agentmsg (AGENT_KILLSTART);	// XXX
    sbp->reply (NULL);
    break;
  case AGENTCTL_DUMPCERTPROGS:
    {
      sfsagent_certprogs res;
      res.set (certprogs.base (), certprogs.size (), freemode::NOFREE);
      sbp->replyref (res);
      break;
    }
  case AGENTCTL_ADDREVOKEPROG:
    {
      sfsagent_revokeprog *arg = sbp->getarg<sfsagent_revokeprog> ();
      str av0;
      if (arg->av.size () > 0 && (av0 = find_program (arg->av[0]))) {
	arg->av[0] = av0;
	revokeprogs.push_back (*arg);
	if (arg->block)
	  revokefilters.push_back (rxfilter (arg->block->filter,
					     arg->block->exclude));
	else
	  revokefilters.push_back ();
	sbp->replyref (true);
      }
      else
	sbp->replyref (false);
      break;
    }
  case AGENTCTL_CLRREVOKEPROGS:
    revokeprogs.clear ();
    revokefilters.clear ();
    agentmsg (AGENT_KILLSTART);	// XXX
    norevoke.clear ();
    sbp->reply (NULL);
    break;
  case AGENTCTL_DUMPREVOKEPROGS:
    {
      sfsagent_revokeprogs res;
      res.set (revokeprogs.base (), revokeprogs.size (), freemode::NOFREE);
      sbp->replyref (res);
      break;
    }
  case AGENTCTL_SETNOREVOKE:
    {
      sfsagent_norevoke_list *arg
	= sbp->getarg<sfsagent_norevoke_list> ();
      for (sfs_hash *hid = arg->base (); hid < arg->lim (); hid++)
	norevoke.insert (*hid);
      sbp->reply (NULL);
      break;
    }
  case AGENTCTL_GETNOREVOKE:
    {
      vec<sfs_hash> revvec;
      norevoke.traverse (wrap (pushit<sfs_hash>, &revvec));
      sfsagent_norevoke_list res;
      res.set (revvec.base (), revvec.size (), freemode::NOFREE);
      sbp->reply (&res);
      break;
    }
  case AGENTCTL_SYMLINK:
    agentmsg (AGENT_SYMLINK, sbp->getvoidarg ());
    sbp->reply (NULL);
    break;
  case AGENTCTL_RESET:
    agentmsg (AGENT_KILLSTART);
    sbp->reply (NULL);
    break;
  case AGENTCTL_FORWARD:
    if (!opt_forwarding)
      sbp->replyref ((int32_t) EPERM);
    else if (name)
      sbp->replyref ((int32_t) EBUSY);
    else {
      setname (*sbp->getarg<sfs_hostname> ());
      sbp->replyref ((int32_t) 0);
    }
    break;
  case AGENTCTL_REX:
    {
      sfsagent_rex_arg *prca = sbp->getarg<sfsagent_rex_arg> ();
      if (name) {
	warn << name << ": " << prca->dest << "(rexsess)\n";
      }
      rex_connect (prca->dest, name, prca->forwardagent, wrap (rexres, sbp));
      break;
    }
  case AGENTCTL_LISTSESS:
    {
      list_rexsess (sbp);
      break;
    }
  case AGENTCTL_KILLSESS:
    {
      sbp->replyref (kill_rexsess (*sbp->getarg<sfs_hostname> ()));
      break;
    }
  case AGENTCTL_ADDSRPNAME:
    {
      sfsagent_srpname_pair *arg = 
	sbp->getarg<sfsagent_srpname_pair> ();

      if (arg->srpname && arg->srpname.len () > 0
	  && arg->sfsname && arg->sfsname.len () > 0) {
	srpnames.insert (arg->srpname, arg->sfsname);
	sbp->replyref (true);
      }
      else
	sbp->replyref (false);
      break;
    }
  case AGENTCTL_CLRSRPNAMES:
    {
      srpnames.clear ();
      agentmsg (AGENT_KILLSTART);	// XXX
      sbp->reply (NULL);
      break;
    }
  case AGENTCTL_DUMPSRPNAMES:
    {
      vec<sfsagent_srpname_pair> resvec;
      srpnames.traverse (wrap (pushsrpname, &resvec));
      sfsagent_srpname_pairs res;
      res.set (resvec.base (), resvec.size (), freemode::NOFREE);
      sbp->reply (&res);
      break;
    }
  case AGENTCTL_LOOKUPSRPNAME:
    {
      sfsagent_srpname *arg = sbp->getarg<sfsagent_srpname> ();
      str *sfsname = srpnames[*arg];

      sfsagent_srpname_res res (false);
      if (sfsname) {
	res.set_status (true);
	*res.sfsname = *sfsname;
      }
      sbp->replyref (res);

      break;
    }
  default:
    warn ("invalid AGENTCTL procno %d\n", sbp->proc ());
    sbp->reject (PROC_UNAVAIL);
    break;
  }
}

void
sfsagent::setname (str n)
{
  if (!name) {
    name = n;
    ac = aclnt::alloc (x, agent_prog_1);
    as = asrv::alloc (x, agentcb_prog_1, wrap (this, &sfsagent::dispatch));
  }
}

void
agentmsg (u_int32_t proc, const void *arg)
{
  static int32_t garbage_int;
  for (sfsagent *a = agents.first; a; a = agents.next (a))
    if (a->ac)
      a->ac->call (proc, arg, &garbage_int, aclnt_cb_null);
}

static void
ctlaccept (int lfd)
{
  sockaddr_un sun;
  socklen_t sunlen = sizeof (sun);
  bzero (&sun, sizeof (sun));
  int fd = accept (lfd, (sockaddr *) &sun, &sunlen);
  if (fd > 0)
    vNew sfsagent (fd);
  else if (errno != EAGAIN)
    warn ("ctlaccept: %m\n");
}

static bool socket_bound;

static void
cleanup ()
{
  // XXX - race-prone when dying from "sfsagent -k"
  if (socket_bound)
    unlink (opt_socket);
}

static void
start_listening_cb (int fd, int status)
{
  if (status)
    close (fd);
  else if (listen (fd, 5) < 0) {
    warn ("not listening for sfskey: listen: %m\n");
    close (fd);
  }
  else {
    socket_bound = true;
    fdcb (fd, selread, wrap (ctlaccept, fd));
  }
}

static void
start_listening ()
{
  if (!opt_socket)
    return;

  if (opt_socket == "-") {
    vNew sfsagent (0);
    return;
  }

  sockaddr_un sun;
  if (opt_socket.len () >= sizeof (sun.sun_path)) {
    warn ("not listening on socket: path too long: %s\n", opt_socket.cstr ());
    return;
  }
  int ctlfd = socket (AF_UNIX, SOCK_STREAM, 0);
  if (ctlfd < 0) {
    warn ("not listening on socket: socket: %m\n");
    return;
  }
  make_async (ctlfd);

  bzero (&sun, sizeof (sun));
  sun.sun_family = AF_UNIX;
  strcpy (sun.sun_path, opt_socket);

  pid_t pid = afork ();
  if (pid == -1) {
    warn ("not listening on socket: fork: %m\n");
    return;
  }
  else if (!pid) {
    umask (077);
    if (bind (ctlfd, (sockaddr *) &sun, sizeof (sun)) < 0) {
      if (errno == EADDRINUSE)
	unlink (sun.sun_path);
      if (bind (ctlfd, (sockaddr *) &sun, sizeof (sun)) < 0) {
	warn ("not listening on socket: %s: %m\n", sun.sun_path);
	err_flush ();
	_exit (1);
      }
    }
    err_flush ();
    _exit (0);
  }

  chldcb (pid, wrap (start_listening_cb, ctlfd));
}

static void
agent_daemonize (int status = 0)
{
  if (status)
    fatal ("agentrc failed\n");
  checkrnd ();
  int32_t err = EIO;
  if (opt_killstart
      && (cdagent->ac->scall (AGENT_KILLSTART, NULL, &err) || err))
    fatal ("could not start agent: %s\n", strerror (err));
  if (!opt_nodaemon) {
    switch (fork ()) {
    case -1:
      fatal ("fork: %m\n");
    case 0:
      break;
    default:
      _exit (0);
    }
    if (setsid () < 0)
      fatal ("setsid: %m\n");
    if (!runinplace)
      chdir ("/");
  }
  start_listening ();
}

static void
setenvfd (int fd)
{
  str var (strbuf ("SFS_AGENTSOCK=-%d", fd));
  xputenv (var);
}

static void
startagent (bool optc, int argc, char **argv)
{
  char **arge = argv + argc;
  vec<const char *> av;

  str rc;
  if (!optc)
    rc = sfsconst_etcfile_required ("agentrc");
  else if (!argc) {
    agent_daemonize ();
    return;
  }
  else {
    if (strchr (*argv, '/'))
      rc = *argv;
    else {
      rc = find_program (*argv);
      if (!rc)
	fatal ("cannot find program %s\n", *argv);
    }
    argv++;
  }
 
  struct stat sb;
  if (!stat (rc, &sb) && S_ISREG (sb.st_mode) && !(sb.st_mode & 0111))
    av.push_back ("/bin/sh");
  av.push_back (rc);
  if (opt_keytime) {
    av.push_back ("-t");
    av.push_back (opt_keytime);
  }
  for (char *const *ap = argv; ap < arge; ap++)
    av.push_back (*ap);
  av.push_back (NULL);

  int fds[2];
  if (socketpair (AF_UNIX, SOCK_STREAM, 0, fds) < 0)
    fatal ("socketpair: %m\n");
  close_on_exec (fds[0]);

  pid_t pid = aspawn (av[0], av.base (), 0, 1, 2, wrap (setenvfd, fds[1]));
  if (pid == -1)
    fatal ("fork: %m\n");
  close (fds[1]);
  ptr<axprt_unix> x = axprt_unix::alloc (fds[0]);
  chldcb (pid, wrap (agent_daemonize));
  vNew sfsagent (x);
}

static void
usage ()
{
  warnx << "usage: " << progname
	<< " [-S socket] [-dnkF] [-c [sfskey ...] | key-source]\n";
  exit (1);
}

int
main (int argc, char **argv)
{
  setprogname (argv[0]);
  sfsconst_init ();
  if (!runinplace) {
    /* In any case, try to avoid core dumps */
#ifdef RLIMIT_CORE
    struct rlimit rlcore;
    if (getrlimit (RLIMIT_CORE, &rlcore) >= 0) {
      rlcore.rlim_cur = 0;
      setrlimit (RLIMIT_CORE, &rlcore);
    }
#endif /* RLIMIT_CORE */
  }

  bool opt_c = false, opt_n = false;
  int ch;
  while ((ch = getopt (argc, argv, "FS:cdknt:")) != -1)
    switch (ch) {
    case 'F':
      opt_forwarding = false;
      break;
    case 'S':
      opt_socket = optarg;
      break;
    case 'c':
      opt_c = true;
      break;
    case 'd':
      opt_nodaemon = true;
      break;
    case 'k':
      opt_killstart = true;
      break;
    case 'n':
      opt_n = true;
      break;
    case 't':
      opt_keytime = optarg;
      break;
    default:
      usage ();
      break;
    }
  argc -= optind;
  argv += optind;

  if (!runinplace && !opt_nodaemon)
    /* setuid makes ptrace fail on some OS's (a good thing, when we
     * hold private keys). */
    setuid (getuid ());
  if (opt_n) {
    if (!opt_socket)
      fatal ("-S <socket> option required with -n option\n");
    random_set_seedfile ("~/.sfs/random_seed");
  }
  else {
    int fd = suidgetfd_required ("agent");
    sfsagent *a = cdagent = New sfsagent (fd);
    a->setname ("sfscd");
    a->cs = NULL;

    int32_t err = EIO;
    if (!opt_killstart && (a->ac->scall (AGENT_START, NULL, &err) || err))
      fatal ("could not start agent: %s\n", strerror (err));
    sfsagent_seed seed;
    if (a->ac->scall (AGENT_RNDSEED, NULL, &seed))
      fatal ("I/O error from sfscd\n");
    rnd_input.update (seed.base (), seed.size ());
    bzero (seed.base (), seed.size ());
    random_update ();		// Unnecessary paranoia - done in checkrnd()
  }

  getsysnoise (&rnd_input, wrap (setrnd));
  sigcb (SIGTERM, wrap (exit, 1));
  startagent (opt_c, argc, argv);
  amain ();
}


