/* $Id: sfsauthd.C,v 1.36 2002/12/03 20:17:19 max Exp $ */

/*
 *
 * Copyright (C) 2001 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 "authdb.h"
#include "sfsauthd.h"
#include "parseopt.h"
#include "rxx.h"

ptr<sfspriv> myprivkey;
sfs_servinfo myservinfo;
ptr<sfs_servinfo_w> siw;
str sfsauthcachedir;
sfs_hash myhostid;
vec<dbfile> dbfiles;
str sfsauthrealm = "";
vec<sfsauth_certpath> sfsauthcertpaths;
str logfile = "";
str authd_syslog_priority = "";
int logfd = -1;
str srpparms = NULL;

void
xfer::start ()
{
  static str xferpath;
  if (pid != -1)
    return;
  if (!xferpath)
    xferpath = fix_exec_path ("xfer");
  const char *av[] = { xferpath, "--", src, dst, NULL };
  if ((pid = aspawn (xferpath, av)) != -1)
    chldcb (pid, wrap (mkref (this), &xfer::stop));
}

inline str
path2name (str path)
{
  char hash[sha1::hashsize];
  sha1_hash (hash, path, path.len ());
  return armor64A (hash, sizeof (hash));
}

ptr<xfer>
mkcache (str src)
{
  assert (src[0] == '/');
  str hash = path2name (src);
  str srclink =  sfsauthcachedir << "/source." << hash;

  errno = 0;
  char buf[256];
  int n = readlink (srclink, buf, sizeof (buf) - 1);
  if (n >= 0) {
    buf[n] = '\0';
    if (src != buf) {
      unlink (srclink);
      errno = ENOENT;
    }
  }
  if (errno && (errno != ENOENT || symlink (src, srclink) < 0)) {
    warn << srclink << ": " << strerror (errno) << "\n";
    return NULL;
  }
  return New refcounted<xfer> (src, sfsauthcachedir << "/cache." << hash);
}

static str
expandcertpath (str s)
{
  str res;

  if (s[0] == '/') {
    if (s.len () == 1 || s[1] != '/')
      res = strbuf () << siw->mkpath () << s;
    else
      res = substr (s, 1);
  }
  else {
    str s1;
    char *firstslash = strchr (s.cstr (), '/');
    if (!firstslash)
      s1 = s;
    else
      s1 = substr (s, 0, firstslash - s.cstr ());
    if (sfs_parsepath (s1))
      res = s;
  }
  if (!res)
    fatal << "Could not expand certpath: " << s << "\n";
  return res;
}

void
cache_refresh ()
{
  for (dbfile *dbp = dbfiles.base (); dbp < dbfiles.lim (); dbp++)
    if (dbp->refresh)
      (*dbp->refresh) ();
  delaycb (60, wrap (cache_refresh));
}

void
dbfile::mkpub ()
{
  if (pubfiles.empty ())
    return;
  vec<ref<authcursor> > acv;
  for (str *np = pubfiles.base (); np < pubfiles.lim (); np++) {
    ptr<lockfile> lf = lockfile::alloc (*np << ".lock", false);
    if (!lf)
      continue;
    if (ptr<authcursor> ac = authcursor_file_append::alloc (*np, 0444, lf))
      acv.push_back (ac);
  }
  if (acv.empty ())
    return;
  ptr<authcursor> ac = db->open ();
  if (!ac)
    return;
  for (ac->reset (); ac->next ();) {
    switch (ac->ae.type) {
    case SFSAUTH_USER:
      ac->ae.userinfo->pwauth = "";
      ac->ae.userinfo->privkey.setsize (0);
      break;
    default:
      break;
    }
    for (ref<authcursor> *acp = acv.base (); acp < acv.lim (); acp++) {
      (*acp)->ae = ac->ae;
      (*acp)->update ();
    }
  }
  for (ref<authcursor> *acp = acv.base (); acp < acv.lim (); acp++)
    (*acp)->commit ();
}

str
dbfile::strip_prefix (str name)
{
  assert (name);
  if (!prefix)
    return name;
  if (name.len () <= prefix.len () || name[prefix.len ()] != '/'
      || memcmp (name.cstr (), prefix.cstr (), prefix.len ()))
    return NULL;
  return name;
}

static void
parseconfig (str cf)
{
  parseargs pa (cf);
  bool errors = false;
  bool empty = true;
  bool unixpwd = false;
  bool userfile_p = false;
  int line;
  vec<str> av;
  ptr<idmap> mygidmap;

  myservinfo.set_sivers (7);
  myservinfo.cr7->release = sfs_release;
  myservinfo.cr7->host.hostname = ""; // XXX - in case inited before 
                                      // rpc_emptystr

  while (pa.getline (&av, &line)) {
    if (!strcasecmp (av[0], "hostname")) {
      if (av.size () != 2) {
	errors = true;
	warn << cf << ":" << line << ": usage: hostname name\n";
      }
      else if (myservinfo.cr7->host.hostname != "") {
	errors = true;
	warn << cf << ":" << line << ": hostname already specified\n";
      }
      else
	myservinfo.cr7->host.hostname = av[1];
    }
    else if (!strcasecmp (av[0], "srpfile")) {
      if (srpparms) {
	errors = true;
	warn << cf << ":" << line << ": srpfile already specified\n";
      } else if (av.size () == 2) {
	str srpfile (av[1]);
	char *cp = strchr (srpfile, '/');
	if (!cp) {
	  srpfile = sfsconst_etcfile (srpfile);
	  if (!srpfile) {
	    errors = true;
	    warn << cf << ":" << line << ": file not found\n";
	  }
	}
	if (!(srpparms = file2str (srpfile))) {
	  errors = true;
	  warn << cf << ":" << line << ": cannot read file\n";
	}
      }
    }
    else if (!strcasecmp (av[0], "keyfile")) {
      if (myprivkey) {
	errors = true;
	warn << cf << ":" << line << ": keyfile already specified\n";
      }
      else if (av.size () == 2) {
	str keyfile (av[1]);
	if (keyfile[0] != '/')
	  keyfile = strbuf ("%s/", etc1dir) << keyfile;
	str key = file2wstr (keyfile);

	if (!key) {
	  errors = true;
	  warn << keyfile << ": " << strerror (errno) << "\n";
	  warn << cf << ":" << line << ": could not read keyfile\n";
	}
	else if (!(myprivkey = sfscrypt.alloc_priv (key, SFS_SIGN))) {
	  errors = true;
	  warn << cf << ":" << line << ": could not decode keyfile\n";
	}
      }
      else {
	errors = true;
	warn << cf << ":" << line << ": usage: keyfile path\n";
      }
    }
    else if (!strcasecmp (av[0], "userfile")) {
      userfile_p = true;
      static rxx pubrx ("^-pub=(.+)$", "i");
      static rxx prefixrx ("^-prefix=([\\w/]+)$", "i");
      static rxx uidid ("-uid=(\\d+)", "i");
      //static rxx uidnam ("-uid=([\w_\-]+)", "i");
      static rxx uidmap ("-uidmap=(\\d+)-(\\d+)\\+(\\d+)", "i");
      static rxx gidid ("-gid=(\\d+)", "i");
      //static rxx gidnam ("-gid=([\w_\-]+)", "i");
      static rxx gidmap ("-gidmap=(\\d+)-(\\d+)\\+(\\d+)", "i");
      dbfile *dbp = &dbfiles.push_back (dbfile ());
      av.pop_front ();
      for (; !av.empty () && av[0][0] == '-'; av.pop_front ())
	if (!strcasecmp (av[0], "-update"))
	  dbp->allow_update = true;
	else if (!strcasecmp (av[0], "-passwd")) {
	  dbp->allow_unix_pwd = true;
	  unixpwd = true;
	} else if (!strcasecmp (av[0], "-create"))
	  dbp->allow_create = true;
	else if (!strcasecmp (av[0], "-admin"))
	  dbp->allow_admin = true;
	else if (pubrx.match (av[0])) {
	  str path = pubrx[1];
	  if (path[0] != '/')
	    path = strbuf () << etc1dir << "/" << path;
	  dbp->pubfiles.push_back (path);
	}
	else if (prefixrx.match (av[0])) {
	  if (dbp->prefix) {
	    errors = true;
	    warn << cf << ":" << line << ": extra -prefix\n";
	  }
	  dbp->prefix = prefixrx[1];
	}
	else if (uidid.match (av[0])) {
	  u_int32_t id = badid;
	  convertint (uidid[1], &id);
	  if (dbp->uidmap) {
	    errors = true;
	    warn << cf << ":" << line << ": only one uid/uidmap allowed\n";
	  }
	  dbp->uidmap = New refcounted<idmap_const> (id);
	}
	else if (uidmap.match (av[0])) {
	  u_int32_t min, max, offset;
	  convertint (uidid[1], &min);
	  convertint (uidid[2], &max);
	  convertint (uidid[3], &offset);
	  if (dbp->uidmap) {
	    errors = true;
	    warn << cf << ":" << line << ": only one uid/uidmap allowed\n";
	  }
	  dbp->uidmap = New refcounted<idmap_range> (min, max, offset);
	}
	else if (gidid.match (av[0])) {
	  u_int32_t id = badid;
	  convertint (gidid[1], &id);
	  if (dbp->gidmap) {
	    errors = true;
	    warn << cf << ":" << line << ": only one gid/gidmap allowed\n";
	  }
	  dbp->gidmap = New refcounted<idmap_const> (id);
	}
	else if (gidmap.match (av[0])) {
	  u_int32_t min, max, offset;
	  convertint (gidid[1], &min);
	  convertint (gidid[2], &max);
	  convertint (gidid[3], &offset);
	  if (dbp->gidmap) {
	    errors = true;
	    warn << cf << ":" << line << ": only one gid/gidmap allowed\n";
	  }
	  dbp->gidmap = New refcounted<idmap_range> (min, max, offset);
	}
	else {
	  errors = true;
	  warn << cf << ":" << line << ": unknown option " << av[0] << "\n";
	}
      if (av.size () == 0 || !av[0].len ()) {
	errors = true;
	warn << cf << ":" << line << ": missing file name\n";
      }
      else if (av.size () > 1) {
	errors = true;
	warn << cf << ":" << line << ": extra arguments after file name\n";
      }
      else if (dbp->allow_unix_pwd && dbp->uidmap) {
	errors = true;
	warn << cf << ":" << line
	     << ": same DB cannot have -passwd and -uidmap\n";
      }
      else {
	str path = av[0];
	if (path[0] != '/')
	  path = strbuf () << etc1dir << "/" << path;
	if (!dbp->allow_update) {
	  if (ptr<xfer> xp = mkcache (path)) {
	    dbp->refresh = wrap (xp, &xfer::start);
	    path = xp->dst;
	  }
	  else {
	    errors = true;
	    warn << cf << ":" << line
		 << ": cannot create cache file\n";
	  }
	}
	dbp->db = New refcounted<authdb_file> (path);
	if (!dbp->uidmap) {
	  dbp->uidmap = New refcounted<idmap_id> ();
	  if (!dbp->gidmap)
	    dbp->gidmap = New refcounted<idmap_id> ();
	}
	else if (!dbp->gidmap) {
	  warn << cf << ":" << line << ": assuming -gid=-1\n";
	  dbp->gidmap = New refcounted<idmap_const> (badid);
	}
	dbp->mkpub ();
	mygidmap = dbp->gidmap;
      }
    }
#if 0
    else if (!strcasecmp (av[0], "userdir")) {
      static rxx prefixrx ("^-prefix=([\\w/]+)$", "i");
      static rxx uidrange ("-uidrange=(\\d+)-(\\d+)", "i");
      static rxx gidid ("-gid=(\\d+)", "i");
      static rxx d ("-depth=(\\d+)", "i");

      u_int32_t uidstart = badid;
      u_int32_t uidend = badid;
      u_int32_t gid = badid;
      int depth = -1;

      dbfile *dbp = &dbfiles.push_back (dbfile ());
      av.pop_front ();
      for (; !av.empty () && av[0][0] == '-'; av.pop_front ())
	if (prefixrx.match (av[0])) {
	  if (dbp->prefix) {
	    errors = true;
	    warn << cf << ":" << line << ": extra -prefix\n";
	  }
	  dbp->prefix = prefixrx[1];
	}
	else if (uidrange.match (av[0])) {

	  if (uidstart != badid || uidend != badid) {
	    errors = true;
	    warn << cf << ":" << line << ": only one uidrange allowed\n";
	  } 
	  else {
	    convertint (uidrange[1], &uidstart);
	    convertint (uidrange[2], &uidend);
	  }

	}
	else if (gidid.match (av[0])) {
	  
	  if (gid != badid) {
	    errors = true;
	    warn << cf << ":" << line << ": only one gid allowed\n";
	  } 
	  else 
	    convertint (gidid[1], &gid);

	}
	else if (d.match (av[0])) {

	  if (depth != -1) {
	    errors = true;
	    warn << cf << ":" << line << ": only one depth allowed\n";
	  } 
	  else
	    convertint (d[1], &depth);

	}
	else {
	  errors = true;
	  warn << cf << ":" << line << ": unknown option " << av[0] << "\n";
	}
      if (av.size () != 2) {
	errors = true;
	warn << cf << ":" << line 
	     << ": missing directory name or cache file name\n"
	     << cf << ":" << line 
	     << ": usage: userdir [-uidrange=n-m] [gid=n] "
	     << "[-depth=n] dirpath cachepath\n";
      }
      else {
	str dirpath = av[0];
	str cachepath = av[1];
	if (dirpath[0] != '/' || cachepath[0] != '/') {
	  errors = true;
	  warn << cf << ":" << line 
	       << ": path names must be absolute\n";
	} else {
	  ptr<lockfile> lf;
	  if (!(lf = lockfile::alloc (cachepath << ".lock", false))) {
	    warn << cf << ":" << line 
		 << ": lockfile " << cachepath << ".lock\n";
	    errors = true;
	  } else {

	    dbp->db = New refcounted<authdb_dir> (lf, dirpath, cachepath, 
						  uidstart, uidend, gid,
						  depth);
	    dbp->uidmap = New refcounted<idmap_id> ();
	    dbp->gidmap = New refcounted<idmap_id> ();
	    dbp->mkpub ();
	    dbp->allow_userdir_shell = true;
	  }
	}
      }
    }
#endif
    else if (!strcasecmp (av[0], "realm")) {
      if (av.size () != 2) {
	errors = true;
	warn << cf << ":" << line << ": usage: realm name\n";
      }
      else if (sfsauthrealm != "") {
	errors = true;
	warn << cf << ":" << line << ": realm already specified\n";
      }
      else
	sfsauthrealm = av[1];
    }
    else if (!strcasecmp (av[0], "certpath")) {
      if (av.size () < 2) {
	errors = true;
	warn << cf << ":" << line << ": usage: certpath path1 [path2 ...]\n";
      }
      else {
	for (size_t i = 1; i < av.size (); i++ ) {
	  sfsauthcertpaths.push_back (av[i]);
	}
      }
    }
    else if (!strcasecmp (av[0], "logfile")) {
      if (av.size () < 2) {
	errors = true;
	warn << cf << ":" << line << ": usage: logfile path\n";
      } else if (logfile != "") {
	errors = true;
	warn << cf << ":" << line << ": logfile already specified\n";
      } else 
	logfile = av[1];
    } else if (!strcasecmp (av[0], "syslog_priority")) {
      if (av.size () < 2) {
	errors = true;
	warn << cf << ":" << line << ": usage: syslog_prioriy priority\n";
      } else if (authd_syslog_priority != "") {
	errors = true;
	warn << cf << ":" << line << ": syslog_priority already specified\n";
      } else 
	authd_syslog_priority = av[1];
    } else {
      errors = true;
      warn << cf << ":" << line << ": unknown keyword " << av[0] << "\n";
    }
    empty = false;
  }

  if (logfile == "")
    logfile = sfs_logfile;
  if (authd_syslog_priority == "")
    authd_syslog_priority = sfs_authd_syslog_priority;

  if ((logfd = open (logfile, O_CREAT|O_APPEND|O_WRONLY, 0600)) < 0) {
    warn ("%s: %m\n", logfile.cstr ());
    errors = true;
  }

  // Default: Userfile -passwd -update -create -pub=<file>.pub <file>
  if (empty) {
    userfile_p = true;
    warn << "Empty sfsauthd_config file. Using default:\n";
    warn << "  Userfile -passwd -update -create -pub=sfs_users.pub "
	 << "sfs_users\n";
    str userfile = sfsconst_etcfile ("sfs_users");
    if (!userfile) {
      strbuf b;
      b << etc1dir << "/sfs_users";
      userfile = b;
      warn << "Will create userfile on write: " << userfile << "\n";
    }
    unixpwd = true;
    strbuf pubuserfile = userfile << ".pub";
    dbfile *dbp = &dbfiles.push_back (dbfile ());
    dbp->pubfiles.push_back (pubuserfile);
    dbp->allow_update = true;
    dbp->allow_unix_pwd = true;
    dbp->allow_create = true;
    dbp->db = New refcounted<authdb_file> (userfile);
    dbp->uidmap = New refcounted<idmap_id> ();
    dbp->gidmap = New refcounted<idmap_id> ();
    dbp->mkpub ();
  }

  if (!userfile_p) 
    fatal << cf << ": no Userfile directive in configuration file\n";

  if (unixpwd) {
    dbfile *dbp = &dbfiles.push_back (dbfile ());
    dbp->allow_update = false;
    dbp->allow_create = false;
    dbp->allow_unix_pwd = true;
    dbp->db = New refcounted<authdb_etc_group> ();
    dbp->uidmap = New refcounted<idmap_id> ();
    if (mygidmap) {
      dbp->gidmap = mygidmap;
    } else {
      dbp->gidmap = New refcounted<idmap_id> ();
    }
  }

  if (!myservinfo.cr7->host.hostname.len ())
    myservinfo.cr7->host.hostname = sfshostname ();
  if (!myprivkey) {
    str keyfile = sfsconst_etcfile ("sfs_host_key");
    if (!keyfile) {
      errors = true;
      warn << "cannot locate default file sfs_host_key\n";
    }
    else {
      str key = file2wstr (keyfile);
      if (!key) {
	errors = true;
	warn << keyfile << ": " << strerror (errno) << "\n";
      }
      else if (!(myprivkey = sfscrypt.alloc_priv (key, SFS_DECRYPT))) { 
	errors = true;
	warn << "could not decode " << keyfile << "\n";
      }
    }
  }
  if (errors)
    fatal ("errors in config file\n");

  myservinfo.cr7->host.type = SFS_HOSTINFO;
  myprivkey->export_pubkey (&myservinfo.cr7->host.pubkey);
  siw = sfs_servinfo_w::alloc (myservinfo);
  if (!siw->mkhostid (&myhostid)) {
    const char *p = siw->get_hostname ();
    if (p) 
      fatal << "Hostname is incomplete or is of invalid syntax: " << p << "\n";
    else 
      fatal << "Cannot determine hostname.\n";
  }
  myservinfo.cr7->prog = SFSAUTH_V2;

  if (sfsauthrealm.len () == 0 && sfsauthcertpaths.size () > 0) 
    warn << "certpath specified, but no realm...ignoring certpath\n";
  for (size_t i = 0; i < sfsauthcertpaths.size (); i++ )
    sfsauthcertpaths[i] = expandcertpath (sfsauthcertpaths[i]);

  cache_refresh ();
}

void
unixaccept (ptr<axprt_unix> x, const authunix_parms *aup)
{
  if (x) {
    vNew authclnt (axprt_crypt::alloc (x->reclaim ()), aup);
  }
}

ptr<axprt_stream>
cloneaccept (bool primary, int fd)
{
  if (fd < 0) {
    if (primary)
      fatal ("EOF from sfssd\n");
    return NULL;
  }
  tcp_nodelay (fd);
  ref<axprt_crypt> x = axprt_crypt::alloc (fd);
  vNew authclnt (x);
  return x;
}

void
clonegen (int fd)
{
  if (fd >= 0)
    if (!cloneserv (fd, wrap (cloneaccept, false))) {
      warn ("clonegen: cloneserv: %m\n");
      close (fd);
    }
}

static void
usage ()
{
  warnx << "usage: " << progname << " [-u sockpath] [-f configfile]\n";
  exit (1);
}

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

  // XXX debug code

  str configfile;
  str upath;
  int ch;
  while ((ch = getopt (argc, argv, "u:f:")) != -1)
    switch (ch) {
    case 'u':
      upath = optarg;
      break;
    case 'f':
      configfile = optarg;
      break;
    case '?':
    default:
      usage ();
    }

  argc -= optind;
  argv += optind;
  if (argc > 0)
    usage ();

  warn ("version %s, pid %d\n", VERSION, getpid ());

  sfsconst_init ();

  sfs_suidserv ("authd", wrap (unixaccept));

  if (!runinplace) {
    if (!sfsauthcachedir)
      sfsauthcachedir = sfsdir << "/authdb";
    mksfsdir (sfsauthcachedir, 0755, NULL, 0);
  }
  else {
    if (!sfsauthcachedir)
      sfsauthcachedir = sfsdir << "/authdb";
    struct stat sb;
    if (stat (sfsdir, &sb) >= 0) {
      if (pid_t pid = fork ())
	waitpid (pid, NULL, 0);
      else {
	umask (0);
	setuid (sb.st_uid);
	mkdir (sfsauthcachedir, sb.st_mode & 0777);
	_exit (0);
      }
    }
  }
  if (!configfile)
    configfile = sfsconst_etcfile_required ("sfsauthd_config");
  parseconfig (configfile);

  // timenow is initialized in amain (); we can only write
  // the initial log line after it is initialized
  delaycb (0, wrap (&siglogv)); 

  if (!runinplace)
    mksfsdir (sfsauthcachedir, 0755, NULL, 0);
  else {
    struct stat sb;
    sb.st_uid = 0;
    stat (sfsdir, &sb);
    mksfsdir (sfsauthcachedir, 0755, NULL, sb.st_uid);
  }

  random_init_file (sfsdir << "/random_seed");

  warn << "serving " << siw->mkpath () << "\n";
  if (sfsauthrealm.len () > 0)
    warn << "serving realm " << sfsauthrealm << "\n";

  if (!cloneserv (0, wrap (cloneaccept, true)) && !upath)
    warn ("No sfssd detected, only accepting unix-domain clients\n");
  if (upath)
    sfs_unixserv (upath, wrap (clonegen));

  amain ();
}
