/* $Id: sfsagent.C,v 1.53 2001/10/29 23:05:49 ericp 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);

list<sfsagent, &sfsagent::link> agents;
list<key, &key::link> keys;
sfsagent *cdagent;

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

static sfs_time exp_time;
static timecb_t *exp_tmo;

static void start_listening ();

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

void
key_timeout (bool cb = false)
{
  if (!cb)
    timecb_remove (exp_tmo);
  exp_tmo = NULL;
  exp_time = 0;
  sfs_time now = time (NULL);
  for (key *k = keys.first, *nk; k; k = nk) {
    nk = keys.next (k);
    if (k->expire && k->expire <= now) {
      keys.remove (k);
      delete k;
    }
    else if (k->expire && (!exp_time || k->expire < exp_time))
      exp_time = k->expire;
  }
  if (exp_time)
    exp_tmo = timecb (exp_time, wrap (key_timeout, true));
}

static key *
klookup (const bigint &n)
{
  key *k;
  for (k = keys.first; k; k = k->link.next)
    if (k->k->n == n)
      return k;
  return NULL;
}

static key *
clookup (str c)
{
  key *k;
  for (k = keys.first; k; k = k->link.next)
    if (k->comment == c)
      return k;
  return NULL;
}

key *
keynum (u_int n)
{
  key *k = keys.first;
  while (k && n--)
    k = k->link.next;
  return k;
}

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::authinit (svccb *sbp)
{
  sfsagent_authinit_arg *aa = sbp->template getarg<sfsagent_authinit_arg> ();
  key *k = keynum (aa->ntries);
  if (!k || aa->authinfo.type != SFS_AUTHINFO) {
    sbp->replyref (sfsagent_auth_res (false));
    return;
  }

  sfsagent_auth_res res (true);
  sfs_autharg ar (SFS_AUTHREQ);
  sfs_signed_authreq sar;
  str rawsar;

  ar.req->usrkey = k->k->n;
  sar.type = SFS_SIGNED_AUTHREQ;
  sar.seqno = aa->seqno;
  bzero (sar.usrinfo.base (), sar.usrinfo.size ());

  if (!sha1_hashxdr (sar.authid.base (), aa->authinfo)
      || !(rawsar = xdr2str (sar))
      || !(ar.req->signed_req = k->k->sign_r (rawsar),
	   xdr2bytes (*res.certificate, ar))) {
    warn ("sfsagent::authinit: xdr failed\n");
    res.set_authenticate (false);
  }

  warn << aa->requestor << ": " << aa->authinfo.name << ":"
       << armor32 (str (aa->authinfo.hostid.base (),
			aa->authinfo.hostid.size ()))
       << " (" << implicit_cast<int> (aa->authinfo.service) << ")\n";

  sbp->replyref (res);
}

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, str path)
{
  sfsagent_lookup_res res;
  if (path && path.len () <= sfsagent_path::maxsize) {
    res.set_makelink (true);
    *res.path = 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:
    authinit (sbp);
    break;
  case AGENTCB_AUTHMORE:
    sbp->replyref (sfsagent_auth_res (false));
    break;
  case AGENTCB_LOOKUP:
    sfslookup (*sbp->template getarg<sfs_filename> (), wrap (lookupres, sbp));
    break;
  case AGENTCB_REVOKED:
    revcheck (*sbp->template 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
sfsagent::ctldisp (svccb *sbp)
{
  switch (sbp->proc ()) {
  case AGENTCTL_NULL:
    sbp->reply (NULL);
    break;
  case AGENTCTL_ADDKEY:
    {
      sfs_addkey_arg *aa = sbp->template getarg<sfs_addkey_arg> ();
      key *nk = New key;
      nk->k = rabin_priv::make (aa->p, aa->q);
      if (key *k = klookup (nk->k->n)) {
	keys.remove (k);
	delete k;
      }
      nk->expire = aa->expire;
      nk->comment = aa->comment;
      keys.insert_head (nk);
      key_timeout ();
      agentmsg (AGENT_START);
      sbp->replyref (true);
      break;
    }
  case AGENTCTL_REMKEY:
    {
      sfs_remkey_arg *ra = sbp->template getarg<sfs_remkey_arg> ();
      key *k;
      bool ok = false;
      switch (ra->type) {
      case SFS_REM_PUBKEY:
	if ((k = klookup (*ra->pubkey))) {
	  ok = true;
	  keys.remove (k);
	  delete k;
	}
	break;
      case SFS_REM_COMMENT:
	while ((k = clookup (*ra->comment))) {
	  ok = true;
	  keys.remove (k);
	  delete k;
	}
	break;
      }
      if (ok)
	agentmsg (AGENT_START);
      sbp->replyref (ok);
      break;
    }
  case AGENTCTL_REMALLKEYS:
    {
      while (key *k = keys.first) {
	keys.remove (k);
	delete k;
      }
      agentmsg (AGENT_START);
      sbp->replyref (NULL);	// XXX race-prone
      break;
    }
  case AGENTCTL_DUMPKEYS:
    {
      sfs_keylist kl;
      sfs_keylist *klp = &kl;
      for (key *k = keys.first; k; k = k->link.next) {
	(*klp).alloc ();
	(*klp)->key = k->k->n;
	(*klp)->expire = k->expire;
	(*klp)->comment = k->comment;
	klp = &(*klp)->next;
      }
      sbp->reply (&kl);
      break;
    }
  case AGENTCTL_ADDCERTPROG:
    {
      sfsagent_certprog *arg = sbp->template getarg<sfsagent_certprog> ();
      str av0;
      if (arg->av.size () > 0 && (av0 = find_program (arg->av[0]))) {
	arg->av[0] = av0;
	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_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->template 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->template 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->template getarg<sfs_hostname> ());
      sbp->replyref ((int32_t) 0);
    }
    break;
  case AGENTCTL_REX:
    {
      sfsagent_rex_arg *prca = sbp->template getarg<sfsagent_rex_arg> ();
      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->template getarg<sfs_hostname> ()));
      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 ("sfskey 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
startagent (bool optc, int argc, char **argv)
{
  char **arge = argv + argc;
  vec<str> av;

  if (optc) {
    if (!argc) {
      agent_daemonize ();
      return;
    }
    av.push_back (find_program (*argv));
    if (!av[0])
      fatal ("cannot find program %s\n", *argv);
    argv++;
  }
  else {
    av.push_back (find_program ("sfskey"));
    if (!av[0])
      fatal ("cannot find sfskey\n");
    av.push_back ("add");
    if (opt_keytime) {
      av.push_back ("-t");
      av.push_back (opt_keytime);
    }
  }
  while (argv < arge)
    av.push_back (*argv++);

  putenv ("SFS_AGENTSOCK=-0");
  ptr<axprt_unix> x = axprt_unix_spawnv (av[0], av);
  if (!x)
    fatal << av[0] << ": " << strerror (errno) << "\n";
  chldcb (axprt_unix_spawn_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 ();
}
