/* $Id: authdb.C,v 1.19 2001/01/13 19:46:11 dm Exp $ */

/*
 *
 * Copyright (C) 1999 David Mazieres (dm@uun.org)
 * Copyright (C) 1999 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 "authserv.h"
#include "authdb.h"
#include "rxx.h"
#include "sha1.h"
#include "serial.h"
#include "sfsmisc.h"
#include "ihash.h"
#include "tempfile.h"

vec<userfile> userfiles;

class authfile {
  str filename;
  int fd;
  int lineno;
  suio in;
  struct stat sb;

  void reset ();
public:
  authfile () : fd (-1) {}
  ~authfile () { close (fd); }

  bool openfile (str path);
  bool checkfile ();
  void closefile () { reset (); }
  bool getentry (authentry *);
};

str
userfile::mkdbpath (str path)
{
  char hname[sha1::hashsize];
  sha1_hash (hname, path, path.len ());
  return strbuf () << sfsauthcachedir << "/"
		   << armor32 (hname, sizeof(hname));
}

userfile::userfile (const str &p, const str &pp, bool r,
		    const str &pre, const str &map, bool reg)
  : path (p), pub (pp), ro (r), prefix (pre), mapall (map),
    reg (reg), dbpath (ro ? mkdbpath (path) : path), xferpid (-1)
{
}

inline str
authentry2str (authentry *e)
{
  strbuf b;
  b << e->keyname << ":" << e->pubkey << ":" << e->privs << ":";
  if (e->srpinfo) {
    assert (!strchr (e->srpinfo, '\n'));
    b << e->srpinfo << ":";
    if (e->privkey) {
      assert (!strchr (e->privkey, '\n'));
      b << e->privkey;
    }
  }
  else
    b << ":";
  b << "\n";
  return b;
}

void
authfile::reset ()
{
  filename = NULL;
  if (fd >= 0)
    close (fd);
  fd = -1;
  lineno = 0;
  in.clear ();
}

bool
authfile::openfile (str path)
{
  reset ();
  if (lstat (path, &sb) < 0 || (fd = open (path, O_RDONLY)) < 0) {
    warn << path << ": " << strerror (errno) << "\n";
    return false;
  }
  if (S_ISLNK (sb.st_mode)) {
    warn << path << ": cannot be a symbolic link\n";
    reset ();
    return false;
  }
  filename = path;
  return true;
}

bool
authfile::checkfile ()
{
  struct stat sb2;
  return lstat (filename, &sb2) >= 0 && stat_unchanged (&sb, &sb2);
}

/* Line format is:
 * keyname:key:privs:SRPinfo:privkey
 */
static rxx authrx ("^([\\w_/-]+):([^:]+):([^:]+):([^:]*)(:([^:]*))?$");
bool
authfile::getentry (authentry *e)
{
  str line;

  do {
    while (!(line = suio_getline (&in)))
      if (in.input (fd, 8192) <= 0) {
	if (in.resid ()) {
	  warn << filename << ":" << lineno + 1 << ": incomplete last line\n";
	  in.clear ();
	}
	return false;
      }
    lineno++;
    authrx.search (line);
    if (!authrx.success ())
      warn << filename << ":" << lineno << ": malformed line ignored\n";
  } while (!authrx.success ());

  e->keyname = authrx[1];
  e->pubkey  = authrx[2];
  e->privs   = authrx[3];
  e->srpinfo = authrx[4];
  e->privkey = authrx[6];
  if (!e->privkey)
    e->privkey = "";

  return true;
}

str
userfile::remprefix (str keyname)
{
  if (keyname && prefix) {
    if (keyname.len () < prefix.len () + 2
	|| memcmp (keyname, prefix, prefix.len ())
	|| keyname[prefix.len ()] != '/')
      return NULL;
    return substr (keyname, prefix.len () + 1);
  }
  return keyname;
}


sfsauth_stat
userfile::update (authentry *e, bool add)
{
  assert (!ro);

  str keyname;
  if (e) {
    keyname = remprefix (e->keyname);
    if (!keyname)
      return SFSAUTH_BADKEYNAME;
    if (e->srpinfo && (e->srpinfo.len () != strlen (e->srpinfo)
		       || strchr (e->srpinfo, '\n')
		       || !srp_server::sane (e->srpinfo))) {
      warn << "ignoring bad SRP data for user " << e->keyname << "\n";
      e->srpinfo = NULL;
    }
    if (e->privkey && (!e->srpinfo
		       || e->privkey.len () != strlen (e->privkey)
		       || strchr (e->privkey, '\n'))) {
      warn << "ignoring bad private key data for user " << e->keyname << "\n";
      e->privkey = NULL;
    }
  }

  authfile af;
  if (!af.openfile (dbpath))
    return SFSAUTH_FAILED;

  str newdbpath (strbuf () << path << "#" << getpid () << "~");
  tempfile newdb (newdbpath, 0400);

  str newpubpath;
  if (pub)
    newpubpath = strbuf () << pub << "#" << getpid () << "~";
  tempfile newpub (newpubpath, 0444);

  sfsauth_stat ret = SFSAUTH_NOCHANGES;
  authentry ae;
  while (af.getentry (&ae)) {
    if (e) {
      if (keyname == ae.keyname) {
	ae = *e;
	if (ret == SFSAUTH_NOCHANGES)
	  ret = SFSAUTH_OK;
	else if (ret == SFSAUTH_OK)
	  warn << path << ": duplicate keyname " << keyname << "\n";
      }
      else if (e->pubkey == ae.pubkey)
	ret = SFSAUTH_KEYEXISTS;
    }

    newdb << authentry2str (&ae);
    if (newpubpath) {
      ae.srpinfo = ae.privkey = "";
      newpub << authentry2str (&ae);
    }
  }

  if (e && add && ret == SFSAUTH_NOCHANGES) {
    newdb << authentry2str (e);
    if (newpubpath) {
      ae = *e;
      ae.srpinfo = ae.privkey = "";
      newpub << authentry2str (&ae);
    }
    ret = SFSAUTH_OK;
  }
  if (ret)
    if (e || ret != SFSAUTH_NOCHANGES)
      return ret;

  if (!af.checkfile ()) {
    warn ("%s: file modified during update\n", path.cstr ());
    return SFSAUTH_FAILED;
  }
  if (!newdb.rename (path)) {
    warn ("%s: %m\n", path.cstr ());
    return SFSAUTH_FAILED;
  }
  if (newpubpath && !newpub.rename (pub))
    warn ("%s: %m\n", pub.cstr ());
  return SFSAUTH_OK;
}

bool
userfile::lookup (authentry *e)
{
  str keyname = remprefix (e->keyname);
  authfile af;
  if (!af.openfile (dbpath))
    return false;

  authentry ae;
  while (af.getentry (&ae)) {
    if (keyname && keyname != ae.keyname)
      continue;
    if (e->pubkey && e->pubkey != ae.pubkey)
      continue;
    *e = ae;
    if (mapall)
      e->privs = mapall;
    return true;
  }
  return false;
}

bool
authlookup (authentry *e)
{
  for (userfile *uf = userfiles.base (); uf < userfiles.lim (); uf++)
    if (uf->lookup (e))
      return true;
  return false;
}

bool
authadd (authentry *e)
{
  for (userfile *uf = userfiles.base (); uf < userfiles.lim (); uf++)
    if (uf->reg)
      return !uf->update (e, true);
  return false;
}

bool
authupdate (authentry *e)
{
  for (userfile *uf = userfiles.base (); uf < userfiles.lim (); uf++)
    if (!uf->ro)
      switch (uf->update (e)) {
      case SFSAUTH_OK:
	return true;
      case SFSAUTH_NOCHANGES:
	break;
      default:
	return false;
      }
  return false;
}

void
auth_cache_refresh ()
{
  static str xferpath;
  if (!xferpath)
    xferpath = fix_exec_path ("xfer");
  static str timeoutarg;
  if (!timeoutarg)
    timeoutarg = strbuf ("-t%d", xfer_timeout);
  
  for (userfile *uf = userfiles.base (); uf < userfiles.lim (); uf++) {
    if (!uf->ro || uf->xferpid >= 0)
      continue;
    char *av[] = {
      "xfer", const_cast <char *> (timeoutarg.cstr ()), "--",
      const_cast <char *> (uf->path.cstr ()),
      const_cast <char *> (uf->dbpath.cstr ()), NULL
    };
    if ((uf->xferpid = aspawn (xferpath, av)) >= 0)
      chldcb (uf->xferpid, wrap (uf, &userfile::xferdone));
  }
}

void
auth_cache_schedule ()
{
  auth_cache_refresh ();
  timecb (timenow + cache_recheck + rnd.getword () % 8,
	  wrap (auth_cache_schedule));
}

void
authdbinit ()
{
  auth_cache_schedule ();
  for (userfile *uf = userfiles.base (); uf < userfiles.lim (); uf++) {
    if (uf->ro) {
      warn << "caching " << uf->path << " -> " << uf->dbpath << "\n";
      continue;
    }
    if (access (uf->path, F_OK) < 0) {
      if (errno == ENOENT) {
	int fd = open (uf->path, O_RDWR|O_CREAT, 0600);
	if (fd < 0)
	  warn ("%s: %m\n", uf->path.cstr ());
	else {
	  warn << "created " << uf->path << "\n";
	  close (fd);
	}
      }
      else
	warn ("%s: %m\n", uf->path.cstr ());
    }
    if (uf->pub)
      uf->update (NULL);
  }
}
