/* $Id: sfsconnect.C,v 1.5 2001/07/10 17:44:15 ericp Exp $ */

/*
 *
 * Copyright (C) 1999 David Mazieres (dm@uun.org)
 * Copyright (C) 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 "sfsconnect.h"

void
sfs_initci (sfs_connectinfo *ci, str path, sfs_service service,
	    vec<sfs_extension> *exts)
{
  ci->set_civers (5);
  ci->ci5->release = sfs_release;
  ci->ci5->service = service;
  ci->ci5->sname = path;
  if (exts)
    ci->ci5->extensions.set (exts->base (), exts->size (),
			     freemode::NOFREE);
  else
    ci->ci5->extensions.setsize (0);
}

bool
sfs_nextci (sfs_connectinfo *ci)
{
  if (ci->civers == 5) {
    sfs_service service = ci->ci5->service;
    str sname = ci->ci5->sname;
    rpc_vec<sfs_extension, RPC_INFINITY> exts;
    exts.swap (ci->ci5->extensions);

    ci->set_civers (4);
    ci->ci4->service = service;
    if (!sfs_parsepath (sname, &ci->ci4->name, &ci->ci4->hostid))
      ci->ci4->name = sname;
    exts.swap (ci->ci4->extensions);
    return true;
  }
  return false;
}

struct constate {
  static ptr<rabin_priv> ckey;

  const sfs_connect_cb cb;

  str sname;
  u_int16_t port;
  str location;

  sfs_connectarg carg;
  sfs_connectres cres;

  ref<sfscon> sc;
  ptr<aclnt> c;
  
  bool encrypt;
  bool check_hostid;
  
  constate (const sfs_connect_cb &c)
    : cb (c), port (sfs_port), sc (New refcounted<sfscon>),
      encrypt (true), check_hostid (true) {}

  void fail (str msg) { (*cb) (NULL, msg); delete this; }
  void succeed () { c = NULL; (*cb) (sc, NULL); delete this; }

  void start ();
  void getfd (int fd);
  void sendconnect ();
  void getconres (enum clnt_stat err);
  void cryptcb (const sfs_hash *sessidp);
};

ptr<rabin_priv> constate::ckey;

void
constate::start ()
{
  /* XXX */
//   if (encrypt)
//     rndstart ();
  if (sname == "-" || (location && location == "-")) {
    location = "localhost";
    int fd = suidgetfd ("authserv");
    errno = ECONNREFUSED;
    getfd (fd);
  }
  else if (!location && !sfs_parsepath (sname, &location, NULL, &port))
    fail (sname << ": cannot parse path\n");
  else if (!strchr (location, '.') && check_hostid)
    fail (location << ": must contain fully qualified domain name\n");
  else
    tcpconnect (location, port, wrap (this, &constate::getfd));
}

void
constate::getfd (int fd)
{
  if (fd < 0) {
    fail (location << ": " << strerror (errno));
    return;
  }
  sc->x = axprt_crypt::alloc (fd);
  c = aclnt::alloc (sc->x, sfs_program_1);
  sendconnect ();
}

void
constate::sendconnect ()
{
  c->call (SFSPROC_CONNECT, &carg, &cres, wrap (this, &constate::getconres));
}

void
constate::getconres (enum clnt_stat err)
{
  if (err == RPC_CANTDECODEARGS && sfs_nextci (&carg)) {
    sendconnect ();
    return;
  }
  if (err) {
    fail (location << ": " << err);
    return;
  }
  if (cres.status) {
    fail (location << ": " << cres.status);
    return;
  }

  sc->servinfo = cres.reply->servinfo;
  if (!sfs_mkhostid (&sc->hostid, sc->servinfo.host)) {
    fail (location << ": Server returned garbage hostinfo");
    return;
  }

  if (sfs_ckpath (sname, sc->servinfo.host))
    sc->hostid_valid = true;
  else if (check_hostid) {
    fail (location << ": Server key does not match hostid");
    return;
  }
  if (port == sfs_port)
    sc->path = sfs_hostinfo2path (sc->servinfo.host);
  else
    sc->path = strbuf ("%d@", port) << sfs_hostinfo2path (sc->servinfo.host);

  if (encrypt) {
    if (!ckey) {
      //      rndsync (); /* XXX */
      if (!ckey)
	ckey = New refcounted<rabin_priv> (rabin_keygen (sfs_minpubkeysize));
    }
    sfs_client_crypt (c, ckey, carg, *cres.reply,
		      wrap (this, &constate::cryptcb));
  }
  else
    succeed ();
}

void
constate::cryptcb (const sfs_hash *sidp)
{
  if (!sidp) {
    fail (location << ": Session key negotiation failed");
    return;
  }
  sc->sessid = *sidp;
  sfs_get_authid (&sc->authid,
		  carg.civers == 4 ? carg.ci4->service : carg.ci5->service,
		  sc->servinfo.host.hostname, &sc->hostid, &sc->sessid);
  succeed ();
}

void
sfs_connect (const sfs_connectarg &carg, sfs_connect_cb cb,
	     bool encrypt, bool check_hostid)
{
  constate *sc = New constate (cb);
  assert (carg.civers == 5);
  sc->carg = carg;
  sc->sname = carg.ci5->sname;
  sc->encrypt = encrypt;
  sc->check_hostid = check_hostid;
  sc->start ();
}

void
sfs_connect_path (str path, sfs_service service, sfs_connect_cb cb,
		  bool encrypt, bool check_hostid)
{
  constate *cs = New constate (cb);
  cs->sname = path;
  sfs_initci (&cs->carg, path, service);
  cs->encrypt = encrypt;
  cs->check_hostid = check_hostid;
  cs->start ();
}

void
sfs_connect_local_cb (constate *cs, ptr<sfscon> sc, str err)
{
  if (sc) {
    cs->sname = sc->path;
    cs->check_hostid = true;
    cs->start ();
  }
  else
    cs->fail (err);
}

void
sfs_connect_host (str host, sfs_service service, sfs_connect_cb cb,
		  bool encrypt)
{
  constate *cs = New constate (cb);
  cs->sname = cs->location = host;
  sfs_initci (&cs->carg, host, service);
  cs->encrypt = encrypt;
  cs->check_hostid = false;
  if (host == "-") {
    sfs_connectarg carg;
    sfs_initci (&carg, host, SFS_AUTHSERV);
    sfs_connect (carg, wrap (sfs_connect_local_cb, cs), false, false);
    return;
  }
  cs->start ();
}
