[Webfunds-commits] java/webfunds/sox/server DirSSDStore.java SSD.java SSDException.java SimpleServer.java SmartServer.java

Ian Grigg iang@cypherpunks.ai
Fri, 6 Apr 2001 18:50:00 -0400 (AST)


iang        01/04/06 18:50:00

  Added:       webfunds/sox/server DirSSDStore.java SSD.java
                        SSDException.java SimpleServer.java
                        SmartServer.java
  Log:
  New package for SSDs, was the SOXServer code in webfunds.ricardian;
  Now Compiled, but not Tested.

Revision  Changes    Path
1.1                  java/webfunds/sox/server/DirSSDStore.java

Index: DirSSDStore.java
===================================================================
/*
 * $Id: DirSSDStore.java,v 1.1 2001/04/06 22:49:59 iang Exp $
 *
 * Copyright (c) Systemics Ltd 1995-1999 on behalf of
 * the WebFunds Development Team.  All Rights Reserved.
 */
package webfunds.sox.server;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;

import java.util.Hashtable;
import java.util.Stack;
import java.util.Enumeration;

import java.net.URL;
import java.net.MalformedURLException;

import webfunds.util.Panic;

import webfunds.utils.Debug;

import webfunds.comms.CommsManager;
import webfunds.comms.BasicCommsManager;

import webfunds.sox.Issuer;
import webfunds.sox.SOXIssuerException;
import webfunds.sox.SOXServerException;
import webfunds.sox.SOXLaterException;
import webfunds.sox.ItemId;

import webfunds.ricardian.*;   // shouldn't be here!



/**
 *  A Store for SSDs (SOX Server Descriptors).
 *  Provides access to SSDs, and caches the latest file on each one.
 *
 *  This was webfunds.ricardian.DirSOXStore
 */
public class DirSSDStore
    extends Debug // implements SSDStore
{

    /**
     * All the SSDs, indexed by unique name.
     * String ==> SSDs
     */
    protected Hashtable   ssds;
    /**
     * All the SSDs, indexed by urls, pointing to unique name.
     * String ==> String
     */
    protected Hashtable   ssdNames;

    protected File        dir = new File(".");
    public    File        getDirectory() { return dir ; }

    final static public String suffix = ".ssd",
                               oldSuffix = ".sox";

    protected String postfix = "     SSDs: ";

    /**
     *  Only way we are allowed onto the net is via this CommsManager.
     */
    protected CommsManager comms = null;
    protected void setCommsManager(CommsManager comms) { this.comms = comms; }




////////  Construction  ////////////////////////////////////

    /**
     * Create a memory store without any backing store.
     */
    public DirSSDStore()              { initHashtables(); }

    /**
     * Creates the store in the given directory.
     * @param dir a directory with server files
    public DirSSDStore(File dir) { initHashtables(); setDir(dir); }
     */

    /**
     * Read in a directory of SSD files and provide access to them.
     * @param dir a directory with server files
     * @param bug place to put debugging during construction
     */
    public DirSSDStore(CommsManager comms, File dir, PrintWriter bug)
    {
        debug(bug, "      dS: ");
         this.comms = comms;
        initHashtables();
        setDir(dir);
    }

    protected void initHashtables()
    {
        ssdNames = new Hashtable();
        ssds = new Hashtable();
    }



    /**
     *  Read in a directory of SSD files.
     *  Make the directory if needed.
     *  It is not an error if files cannot be rewritten.
     */
    protected void setDir(File dir)
    {
        if (!dir.exists())
            dir.mkdirs();
        if (!dir.isDirectory())
            throw new IllegalArgumentException("Not a dir: "+dir.getPath());
        if (!dir.canRead())
            throw new Panic("Cannot read: " + dir.getPath());

        //
        // Read in all the files in the directory.
        //
        this.dir = dir;

        String[] files = dir.list();

        for (int i = 0; i < files.length; i++)
        {
            String name = files[i];
            File f = new File(dir, files[i]);

            if (name.indexOf(":") > 0)     // has a colon - Mac change 1.4.1
            {
                logmsg("removing old server file: " + name);
                f.delete();
            }
            else if (name.endsWith(suffix))      // current server files
            {
                SSD ssdfile;
                try {
                    ssdfile = SSD.getInstance(f, getDebug());
                } catch (SSDException ex) {
                    ex.printStackTrace(System.err);
                    logmsg("removing " + name);
                    f.delete();
                    continue ;
                }

                addSSD(ssdfile);
                // ssdfile.saveAsFile(dir);
            }
            else if (name.endsWith(oldSuffix))  // old server files - 1.7
            {
                logmsg("removing old server file: " + name);
                f.delete();
            }
            else if (name.endsWith(".srv"))     // old server files - 1.3
            {
                logmsg("removing old server file: " + name);
                f.delete();
            }
            else                 // ignore - might belong to another store
                logmsg("ignoring: " + name);
        }
    }


    /**
     *  Index the SSD into the temporary hashtables.
     *  This is the running cache, not the persistant store.
     */
    protected void addSSD(SSD ssd)
    {
        //
        //  Two lists:
        //      ssds           exact name - taken from file
        //      allNames       any name, URLs of file location
        //
        //  The ssds list can be overriden as it is synonymous with
        //  the storage method.
        //
        String name = ssd.getName();
        logmsg("adding to local: " + name);
        ssds.put(name, ssd);

        //
        //  The SSD has a list of names by which it might
        //  be known.  These are the URLs where the file can be got from.
        //
        String[] all = ssd.getAllNames();
        for (int i = 0; i < all.length; i++)
             addThisSSDName(all[i], name);
    }



    /**
     *  Add a name (url) for an SSD.
     */
    protected void addThisSSDName(String name, String unique)
    {
        //
        //  Each URL points to one and one only SSD
        //  (unlike contracts).
        //
        String already = (String) ssdNames.get(name);

        if (already != null)
        {
            if (!already.equals(unique))
                logmsg("Clash: " + name + " points to " + already +
                      " , trying to add " + unique + " (ignored)");
            return ;
        }

        ssdNames.put(name, unique);
        logmsg(" name: " + name);
    }



////////  Main Caller Access ////////////////////////////////////

    static final public String no_urls = "no urls supplied (bad contract?)";

    /**
     * Refresh an SSD from the URLs off the net.
     * For some reason - bounced connections - the caller has decided
     * that the Issuer is bad.  One reason might be that the Issuer
     * has moved.  Refresh the SSD and see if it has changed.
     *
     * Experimental!
     *
     * @return true if it changed (caller should retry activity)
     */
    public boolean refreshSSD(SSD old)
        throws SSDException, SOXLaterException
    {

        String[] urls = old.getArray("server_file_url");
        if (urls.length == 0)
            throw new SSDException(no_urls);

        //
        // Collect the existing cache one and try for the new one on the net.
        //
        // SSD old = getSSDFromCache(urls);
        SSD ssd = getSSDFromNet(urls);

        int oldV = old.getVersion();
        int newV = ssd.getVersion();
        if (newV == 0)
            throw new SSDException("cannot refresh, new version is 0");

        if (oldV >= newV)
            return false ;

        String name = old.getName();
        logmsg("updating " + name + " from " + oldV + " to " + newV);

        //
        //  A quick name check.
        //
        String newName = ssd.getName();
        if (!name.equals(newName))
            throw new SSDException("refreshed SSD changed name: " +
                                         name + " ==> " + newName);

        //
        //  We've found a new version.
        //  Need to store it over the top of the old.
        //

        addAndSave(ssd);
        return true ;
    }

    /**
     *  Request an SSD by its name.  Only call this if you know the name
     *  is available and in the store, elsewise, call get(String, String[]).
     *
     *  @return the SSD that answers to name,
     *           else null if it is unknown, or no name supplied
     */
    public SSD get(String name)
    {

        if (name == null || name.length() == 0)
            return null;

        return (SSD) ssds.get(name);
    }

    /**
     *  Request an SSD by its name or URLs.
     *  This is the backdoor way to get new SSDs into the store,
     *  for when the SSD is expressed differently.
     *
     *  @return the SSD that uniquely matches hash, never null
     *  @except SSDException there is no matching SSD available
     *  @except SOXLaterException try again later
     */
    public SSD get(String name, String[] urls)
        throws SSDException, SOXLaterException
    {
        SSD ssd;
        if ((name != null) && (name.length() > 0))
        {
            ssd = (SSD) ssds.get(name);
            if (ssd != null)
                return ssd;
        }

        if (urls == null || urls.length == 0)
            throw new SSDException(no_urls);

        ssd = getSSDFromCache(urls);
        if (ssd != null)
            return ssd ;
        logmsg(urls[0] + " not in cache, going to net");

        //
        // if we got this far, this must be a new contract.
        //
        ssd = getSSDFromNet(urls);

        addAndSave(ssd);
        return ssd ;
    }


////////  Internal Utility Code ////////////////////////////////////

    /**
     * Request an SSD for URLs but get it from the cache.
     * @return the SSD that matches the urls
     */
    protected SSD getSSDFromCache(String[] urls)
        throws SSDException
    {

        SSD ssd;
        for (int i = 0; i < urls.length; i++)
        {
            logmsg("url (" + i + ") " + urls[i]);
            String name = (String) ssdNames.get(urls[i]);
            if (name == null)
                continue ;

            // found one (unusual to find one, not the other, means a change)
            ssd = (SSD) ssds.get(name);
            if (ssd == null)
               { logmsg("huh? no SSD for name " + name); continue ; }

            // logmsg("found: " + ssd.getName());

            return ssd ;
        }

        return null ;
    }

    /**
     * Browse an SSD from some URLs.
     * @return the first SSD that we find
     */
    protected SSD getSSDFromNet(String[] urls)
        throws SSDException, SOXLaterException
    {
        // only used locally I think
        if (comms == null)
            throw new SSDException("no CommsManager?");

        SSD ssd;

        //
        // Extract the urls - again - and go out on the net.
        // Might want to think about multithreading this stage.
        //
        for (int i = 0; i < urls.length; i++)
        {
            logmsg("net <" + i + "> " + urls[i]);
            URL url;
            try {
                url = new URL(urls[i]);
            } catch (MalformedURLException ex) {
                logmsg(ex + ": " + urls[i]);
                continue ;
            }
            if (url == null)
                continue ;

            ssd = SSD.getInstance(url, comms, getDebug());
            if (ssd != null)
                return ssd ;
            // might want to keep searching here, if we are refreshing
        }

        throw new SSDException("no SSD file from " + urls[0]);
    }

    /**
     * If you wish to change the way that things are stored,
     * override these methods.
     *              // this method not used - deprecate
     */
    protected SSD getFromStore(String name)
        throws SSDException
    {
        //return ssds.get(name);

        File f = new File(dir, name);
        SSD ssdfile = SSD.getInstance(f, getDebug());
        return ssdfile ;
    }

    protected void addAndSave(SSD ssd)
        throws SSDException
    {
        addSSD(ssd);
        savePersistant(ssd);
    }

    /**
     *  Override this method to change the nature of the persistant storage.
     */
    protected void savePersistant(SSD ssd)
        throws SSDException
    {
        try {
            ssd.saveAsFile(dir);
        } catch (IOException ex) {
            ex.printStackTrace(err());
            throw new SSDException(ex.getMessage());
        }
    }

    protected void cleanStore()
        throws IOException
    {
        initHashtables();

        String[] files = dir.list();

        for (int i = 0; i < files.length; i++)
        {
            String name = files[i];
            if (name.endsWith(suffix) || name.endsWith(oldSuffix))
            {                                       // current server files
                File f = new File(dir, name);
                f.delete();
            }
        }
    }



////////  User-level Utility Code ////////////////////////////////////



    /**
     * @return a list of the names of all SSDs
     */
    public String[] getAllNames()
    {
        String[] names = new String[ssds.size()];
        Enumeration e = ssds.keys();
        for (int i = 0; e.hasMoreElements(); i++)
        {
             names[i] = (String)e.nextElement();
        }
        return names;
    }

    /**
     * @return a list of all SSDs
     */
    public SSD[] getAllSSDs()
    {
        SSD[] sss = new SSD[ssds.size()];
        Enumeration e = ssds.elements();
        for (int i = 0; e.hasMoreElements(); i++)
        {
             sss[i] = (SSD)e.nextElement();
        }
        return sss;
    }



////////  Self-test ////////////////////////////////////

    public String toString()
    {
        String retval = "DirSSDStore: " +
                        "\n\t\tSSDs = " + ssds.size() +
                        "\n\t\tnames = "     + ssdNames.size() +
                        "\n\t\tDirectory = " + dir;
        return retval;
    }

    static final String usage =
           "Test Usage: DirSSDStore directory";

    public static void main(String[] arg)
        throws ContractException, ContractDirectoryException,
               SSDException, SOXLaterException
    {
        if (arg.length == 0)
        {
            System.err.println(usage);
            System.exit(1);
        }

        File d = new File(arg[0]);
        PrintWriter bug = new PrintWriter(System.err, true);
        DirContractStore cons = new DirContractStore(d, bug);
        CommsManager cm = new BasicCommsManager();
        DirSSDStore store = new DirSSDStore(cm, d, bug);
        System.err.println("Started: " + store);
        
        //
        //  For all contracts, ask for the SSD file (and the urls).
        //
        Contract[] contracts = cons.getAllContracts();
        int found = 0;
        for (int i = 0; i < contracts.length; i++)
        {
            System.err.println("  " + i + ": " + contracts[i]);

            Contract con = contracts[i];
            String[] locs = SSDFields.getLocation(con);
            String name = SSDFields.getName(con);

            SSD ssd = store.get(name, locs);
            found++;

            System.err.println("  " + ssd);
            String[] urls = ssd.getArray("sox", "server_url");
            for (int k = 0; k < urls.length; k++)
            {
                System.err.println("           " + urls[k]);
            }
        }
        

    }
}



1.1                  java/webfunds/sox/server/SSD.java

Index: SSD.java
===================================================================
/*
 * $Id: SSD.java,v 1.1 2001/04/06 22:49:59 iang Exp $
 *
 * Copyright (c) Systemics Ltd 1995-1999 on behalf of
 * the WebFunds Development Team.  All Rights Reserved.
 */
package webfunds.sox.server;

import java.io.*;
import java.net.URL;
import java.net.MalformedURLException;

import java.security.cert.Certificate;
import java.util.Hashtable;
import java.util.Enumeration;
import java.util.Vector;

import webfunds.utils.Diagnostics;

import webfunds.util.Panic;
import webfunds.util.IniFileReader;
import webfunds.util.FormattedFileException;
import webfunds.util.Support;

import webfunds.comms.RawHttp;
import webfunds.comms.CommsManager;
import webfunds.comms.BasicCommsManager;
import webfunds.comms.SingleRequestor;
import webfunds.comms.RawException;
import webfunds.comms.RawReplyException;
import webfunds.comms.RawURLException;
import webfunds.comms.RawConnectException;

import webfunds.sox.Encodable;
import webfunds.sox.Issuer;
import webfunds.sox.SmartIssuer;
import webfunds.sox.SOXIssuerException;
import webfunds.sox.SOXLaterException;
import webfunds.sox.SOXPacketException;


/**
 *  SOX Server Descriptor.
 *
 *  Every SOX server has file(s) on the web somewhere which
 *  indicates various static paramaters, known as the
 *  SOX Server Descriptor, or SSD.  This class manages the
 *  (duplicated) files into one SSD.
 *
 *  Somewhere there has to be a bridge between contract and server:
 *
 *       Contract ==> SOX Server Descriptor ==> SOX Server
 *
 *  This class is it.
 *
 *  NOTE this was once webfunds.ricardian.SOXServer.
 */
public class SSD
    extends Encodable implements Diagnostics
{
    /**
     * The version of the encoded object:
     *    1 - first Encodable version.
     */
    public static final int VERSION = 1;




    // implements Diagnostics  - EXPERIMENTAL !!!
    // mixing these classes is a bit non-OO.  But, it seems many need both.
    // another option would be to extend from Debug.
    protected PrintWriter bug = null;
    protected String      fix = "      ss- ";
    public void logmsg(String s)  { if (bug != null) bug.println(fix + s); }
    public PrintWriter getDebug() { return bug ; }
    // hmm, no autoflush.
    public PrintWriter err()
    { return (bug == null) ? new PrintWriter(System.err, true) : bug ; }




    protected   byte[]        fileData;
    protected   IniFileReader serverFile;
    // protected   Issuer        issuer;
    protected   String        originalName;



///////  Constructors  /////////////////////////////////////////////

    public SSD(byte[] fileData, String name, PrintWriter bug)
        throws SSDException
    {
        if (bug != null)
            this.bug = new PrintWriter(bug, true);
        this.fileData  = fileData;
        this.originalName = name;
        init();
    }

    public SSD(byte[] data)
        throws SOXPacketException
    {
        decode(data);
        init2();
    }

    public SSD(InputStream is)
        throws SOXPacketException
    {
        try {
            decode(is);
        } catch (IOException ex) {
            ex.printStackTrace(System.err);
            throw new SOXPacketException("IOEx: " + ex);
        }
        init2();
    }


    protected void init2()
        throws SOXPacketException
    {
        try {
            serverFile = new IniFileReader(fileData);
        } catch (FormattedFileException ex) {
            ex.printStackTrace(System.err);
            throw new SOXPacketException("ContractEx: " + ex);
        };
    }

    protected void init()
        throws SSDException
    {
        try {
            serverFile = new IniFileReader(fileData);
        } catch (FormattedFileException ex) {
            ex.printStackTrace(System.err);
            throw new SSDException(ex.getMessage());
        };
        refreshServerData();
    }
    


////// Recover, Save /////////////////////////////////////
       
    public void encode(OutputStream os)  
        throws IOException
    {
        DataOutputStream dos = new DataOutputStream(os);
        dos.writeInt(VERSION);           // match with encode()

        writeString(dos, originalName);
        writeByteArray(dos, fileData);
    }

        
    
    public void decode(InputStream is)
        throws IOException, SOXPacketException
    {
        DataInputStream dis = new DataInputStream(is);

        int v = dis.readInt();
        if (v != VERSION)
            throw new SOXPacketException("wrong version: "+v+" != "+VERSION);

        originalName = readString(dis);
        fileData = readByteArray(dis);
    }



//////  Initial Instances  /////////////////////////////////////

    /**
     * Create a SSD given the file.
     *
     * @param file name for SSD file
     */
    public static SSD getInstance(File serverfile, PrintWriter bug)
        throws SSDException
    {

        // Read ServerFile
        if (!serverfile.exists() || !serverfile.canRead())
            throw new SSDException("cannot read: " + serverfile);

        byte[] serverData;

        try {
            FileInputStream fis = new FileInputStream(serverfile);
            serverData = new byte[fis.available() ];
            fis.read(serverData);
            fis.close();
        } catch (IOException ex) {
            ex.printStackTrace(bug);
            throw new SSDException(ex.getMessage());
        }

        return new SSD(serverData, serverfile+"", bug);
    }

    /**
     * Create a SSD object given an URL
     *
     * @param url URL to read the object from
     * @param comms where to get a comms requestor from
     */
    public static SSD getInstance(URL url, CommsManager comms,
                                        PrintWriter bug)
        throws SSDException, SOXLaterException
    {
        String proto = url.getProtocol();
        if (proto.equals("file"))
            return getInstance(new File(url.getFile()), bug);

        if (!proto.equals("http"))
            throw new SSDException("unknown proto " + proto +
                                         " in URL " + url);

        // RawHttp http = new RawHttp(url, bug);
        SingleRequestor sr = comms.getSingleRequestor(url);
        byte[] getRequest = RawHttp.getGetData(url);

        byte[] buf;
        try {
            // do a Get on the URL
            buf = sr.get(getRequest);
        } catch (RawConnectException ex) {
            throw new SOXLaterException(ex.getErrno(), "no net?\n" + ex);
        } catch (RawURLException ex) {
            ex.printStackTrace();
            throw new SSDException("bad url?\n" + ex);
        } catch (RawException ex) {
            ex.printStackTrace();
            throw new Panic("Bad RawEx: " + ex);
        }

        byte[] data;
        try {
            data = RawHttp.getReply(buf);
        } catch (RawReplyException ex) {
            throw new SSDException(ex.getMessage() +
            "\n\nThe server file <<" + url + ">> is not found.\n" +
            "Check this URL (as found in the contract local file).");
        }

        return new SSD(data, url+"", bug) ;
    }

    /**
     * Take the data and make a SSD object.
     * There is no constructor for this.
     *
     * @return a SSD made from the file data
     */
    public static SSD getInstance(byte[] server)
        throws SSDException
    {
        return new SSD(server, null, null);
    }



///////    Issuers    /////////////////////////////////////////////

    SmartIssuer smart = null;

    /**
     * Return an Issuer.
     * Ready to go?  Tested?  Probably.
     * XXX: deprecate, no CommsManager
    Issuer getIssuer(Contract con)
        throws SOXIssuerException, SOXLaterException
    {
        logmsg("DEPRECATED getIssuer ( " + con + " ) == " + smart);
        if (smart == null)
            smart = initSmartIssuer(con, new BasicCommsManager());

        logmsg("smart.getReady()");
        smart.getReady();
        logmsg("Ready for Action!");

        // to go any further - re-read the SSD file - we need
        // to know whether we are online.

        return (Issuer)smart;
    }
     */

    /**
     * Return an Issuer.
     * Ready to go?  Tested?  Probably.
    Issuer getIssuer(Contract con, CommsManager comms)
        throws SOXIssuerException, SOXLaterException
    {
        logmsg("getIssuer ( " + con + " ) == " + smart);
        if (smart == null)
            smart = initSmartIssuer(con, comms);

        logmsg("smart.getReady()");
        smart.getReady();
        logmsg("Ready for Action!");

        // to go any further - re-read the SSD file - we need
        // to know whether we are online.

        return (Issuer)smart;
    }

    private SmartIssuer initSmartIssuer(Contract con, CommsManager comms)
        throws SOXIssuerException, SOXLaterException
    {
        //
        // Extract the cert.
        //
        Certificate cert;
        try {
            cert = con.getServerCert();
        } catch (ContractException ex) {
            
            throw new SOXIssuerException("contract cert is bad: " + con +
                                         "\n" + ex);
        }

        return initSmartIssuer(cert, comms);
    }

    private SmartIssuer initSmartIssuer(Certificate cert, CommsManager comms)
        throws SOXIssuerException, SOXLaterException
    {
        //
        // Extract the URLs for the server(s).
        //
        String[] urls;
        urls = getArray("server_url");

        logmsg("smart with " + urls.length + " urls");
        SmartIssuer smart = new SmartIssuer(urls, cert, comms, getDebug());

        //
        //  Pass the URLs of other non-SOX servers to the SmartIssuer
        //  so that it can check the status of the net.
        //
        urls = getArray("nearby_urls");
        smart.setOtherURLs(urls);

        return smart;
    }
*/



///////  Contents  /////////////////////////////////////////////

    static final String http = "http://",
                        www = "www.";

    /**
     * Get the name of the server.  This should be useful for a filename.
     * @return unique name of the server
     */
    public String getName()
    {
        /*
         * There should be a name in the file.
         */
        String s = getField("server_name");
        if (s != null && !"".equals(s))
            return s;

        /*
         *  No name in SOX server file, take the file URL and build some
         *  sort of canonical name out of that.  Just so it works.
         *  But, there might be many of these.  Should take the first.
         *  There are some complications here, such as changed orders,
         *  and shared URLs.  This is not a recommended method.
         */
        String[] ss = getArray("server_file_url");
        s = ss[0];
        if (s.startsWith(http))
            s = s.substring(http.length(), s.length());
        if (s.startsWith(www))
            s = s.substring(www.length(), s.length());
        // this should be domain:port type, so drop / at end
        if (s.endsWith("/"))
            s = s.substring(0, s.length() - 1);
        s = s.replace('.', '_');    // doesn't work on Doze
        s = s.replace(':', '_');    // doesn't work on Mac
        return s;
    }

    /**
     * Get all the names of the server file.
     * These will mostly be the URLs where the file can be found.
     * @return list of names
     */
    public String[] getAllNames()
    {
        Hashtable hush = new Hashtable();
        String name = getName();
        hush.put(name, name);

        if (originalName != null)    // this may be some local store
            hush.put(originalName, originalName);

        String[] all = getArray("server_file_url");
        for (int i = 0; i < all.length; i++)
            hush.put(all[i], all[i]);

        String[] names = new String[hush.size()];
        Enumeration e = hush.keys();
        for (int i = 0; i < hush.size(); i++)
            names[i] = (String)e.nextElement();
            
        return names ;
    }

    /**
     *  Convert a list of strings to URLs.
     *  Ignores malformed ones.
     *  Always returns an array.
     */
    public static URL[] convertToURLs(String[] stringURLs, PrintWriter bug)
    {
        Vector v = new Vector();

        for (int i = 0; i < stringURLs.length; i++)
        {
            URL url;
            try {
                url = new URL(stringURLs[i]);
            } catch(MalformedURLException ex) {
                bug.println(i + ": " + ex + ":\n<<" + stringURLs[i] + ">>");
                continue ;
            }
            v.addElement(url);
            bug.println("ok " + stringURLs[i]);
        }

        URL[] urls = new URL[v.size()];
        v.copyInto(urls);

        return urls;
    }

    public URL[] getServerLocation()
    {
        //
        // looks like:
        //     server_url += http://hayek.econ:8888/
        //     server_url += http://mises.econ:8888/
        //
        String[] strings = getArray("server_url" );
        return convertToURLs(strings, bug);
    }

    public URL[] getNearbyURLs()
    {
        //
        // looks like:
        //     nearby_urls += http://www.site.econ/
        //     nearby_urls += http://www.help.econ/
        //
        String[] strings = getArray("nearby_urls" );
        return convertToURLs(strings, bug);
    }

    /**
     * Get the version number of the server file.
     * Useful for seeing which is the most recent copy of the file,
     * a la DNS.
     * @return current version number of the server, 0 if failed
     */
    public int getVersion()
    {
        /*
         * There should be a name in the file.
         */
        String s = getField("server_version");
        if (s == null || "".equals(s))
            return 0 ;

        Integer i;
        try {
            i = new Integer(s);
        } catch (NumberFormatException ex) {
            logmsg("bad version in " + this + ": " + s);
            return 0 ;
        }
        int i2 = i.intValue();
        if (i2 < 0)
        {
            logmsg("bad version in " + this + ": " + s);
            return 0;
        }
        return i2;
    }



///////  Access to Fields  /////////////////////////////////////////////

    protected void setField(String section, String item, String value)
    {
        System.err.println("trying to set field" +
                           "[" + section + "] " + item + " = " + value);
        serverFile.changeSectionItemValue(section, item, value);
    }

    protected String getField(String item) { return getField("sox", item) ; }
    protected String[] getArray(String item) { return getArray("sox", item) ; }

    protected String getField(String section, String item)
    {
        String s = "";
        // blows up if a vector?
        s = serverFile.getSectionItemValue(section, item);

        if (s == null || "".equals(s) )
        {
            item = section + "_" + item;
            s = serverFile.getSectionItemValue(section, item);
        }

        return s;
    }

    protected String[] getArray(String section, String item)
    {
        String[] ss = new String[0];
        ss = serverFile.getSectionItemArray(section, item);

        if (ss == null || ss.length == 0)
        {
            item = section + "_" + item;
            ss = serverFile.getSectionItemArray(section, item);
        }
        return ss;
    }




///////  Save & Restore  /////////////////////////////////////////////

    public void saveAsFile(File dir)
        throws IOException
    {
        saveAsFile(dir, getName());
    }

    /**
     * This file doesn't need to differentiate itself from other
     * files, so it saves itself as name.  Caller can add a suffix.
     */
    public void saveAsFile(File dir, String name)
        throws IOException
    {
        File servfile = new File(dir, name);
        FileOutputStream fos;

        fos = new FileOutputStream(servfile);
        fos.write(fileData);
        fos.close();
    }

    public URL[] getCachedSOXFileLocation()
    {
        String[] strings;
        strings = getArray("server_file_url" );
        URL[] urls = new URL[strings.length];

        for (int i = 0; i < urls.length; i++)
        {
            try {
                urls[i] = new URL(strings[i]);
            } catch (MalformedURLException mfex) {
                logmsg(mfex + " (" + i + ") " + strings[i]);
                urls[i] = null;
            }
        }

        return urls;
    }

    public URL[] getSOXFileLocation()
    {
        URL[] urls = getCachedSOXFileLocation();
        if (urls.length != 0)
            return urls ;

        refreshServerData();

        return getCachedSOXFileLocation();
    }

    /**
     * With a bunch of URLs, go out and get a SOX Server file from the net.
     * Should be called using the contract data by a Store.
     * @except SSDException if the format is duff
     *
     * XXX: deprecate!
    private static SSD getNewSSDFile(URL[] urls)
        throws SSDException
    {

        for (int i = 0; i < urls.length; i++)
        {
            byte data[];
            try {
                data = Support.getURL(urls[i]);
            } catch (IOException ex) {
                continue ;
            }

            if (data == null)
                continue ;

            SSD ssd;
            try {
                ssd = SSD.getInstance(data);
            } catch (SSDException ex) {
                continue ;
            }

            return ssd ;
        }

        return null ;
    }
     */

    /**
     *  Go out on the net and see if it is changed?
     *  Not implemented yet, see the DirSOXStore.
     */
    protected void refreshServerData()
    {
    }



///////  Self-Test  /////////////////////////////////////////////

    // public int hashCode()
    // {
    //    return id.hashCode();
    // }

    public boolean equals(Object obj)
    {
        if(!(obj instanceof SSD))
            return false;

        SSD other = (SSD)obj;
        // check if the data components are equivalent
        boolean ok = Support.equals(other.fileData, this.fileData) ;
        return ok ;
    }

    public String toString()
    {
        return getName();
    }

    public static void main(String[] arg)
        throws IOException
    {
        if (arg.length == 0)
        {
            System.err.println("Test Usage: SSD {filename | URL}");
            System.exit(1);
        }

        String name = arg[0];
        SSD ssd = null;
        PrintWriter bug = new PrintWriter(System.err);
        CommsManager comms = new BasicCommsManager();

        try
        {
            if (name.startsWith("http://"))
            {
                URL url = new URL(name);
                ssd = SSD.getInstance(url, comms, bug);
            }
            else
            {
                File fil = new File(name);
                ssd = SSD.getInstance(fil, bug);
            }
        }
        catch (Exception ex)
        {
            ex.printStackTrace(bug);
            System.exit(1);
        }
        // ssd.debug(new PrintWriter(System.err, true));
        System.err.println("SSD: " + ssd.getName());
        String[] names = ssd.getAllNames();
        for (int i = 0; i < names.length; i++)
            System.err.println("Name " + i + ": <" + names[i] + ">");


        File d = new File("/tmp");
        String ff  = "flooples";
        ssd.saveAsFile(d, ff);

        File f = new File(d, ff);
        SSD ssd2 = null;
        try {
            ssd2 = SSD.getInstance(f, bug);
        } catch (Exception ex) {
            ex.printStackTrace(bug);
            System.exit(1);
        }

        if (ssd2.equals(ssd))
            System.err.println("Equal after saving");
        else
            throw new RuntimeException("!equal: " + d + "/" + f);

        System.exit(0);
        
    }

}



1.1                  java/webfunds/sox/server/SSDException.java

Index: SSDException.java
===================================================================
/*
 * $Id: SSDException.java,v 1.1 2001/04/06 22:49:59 iang Exp $
 *
 * Copyright (c) 2001 Systemics Inc on behalf of
 * the WebFunds Development Team.  All Rights Reserved.
 */
package webfunds.sox.server;

import webfunds.util.ExceptionModel;

public class SSDException extends ExceptionModel
{
    
    public SSDException(int number, String msg) { super(number, msg); }
    public SSDException(String msg)             { super(UNKNOWN, msg); }
}



1.1                  java/webfunds/sox/server/SimpleServer.java

Index: SimpleServer.java
===================================================================
/*
 * $Id: SimpleServer.java,v 1.1 2001/04/06 22:49:59 iang Exp $
 *
 * Copyright (c) Systemics Ltd 1995-1999 on behalf of
 * the WebFunds Development Team.  All Rights Reserved.
 */
package webfunds.sox.server;

import java.io.*;
import java.security.*;
import java.security.cert.Certificate;
import java.util.Date;

import cryptix.openpgp.*;
import cryptix.openpgp.util.PGPArmoury;

import webfunds.utils.Debug;

import webfunds.comms.*;

import webfunds.sox.Server;
import webfunds.sox.Crypto;
import webfunds.sox.SOXException;
import webfunds.sox.SOXServerException;
import webfunds.sox.SOXIssuerException;
import webfunds.sox.SOXLaterException;
import webfunds.sox.SOXPacketException;
import webfunds.sox.SOXKeyException;
import webfunds.sox.BasicAgent;
import webfunds.sox.Request;
import webfunds.sox.EncryptedRequest;
import webfunds.sox.EncryptedReply;
import webfunds.sox.TimeSyncReply;
import webfunds.sox.TimeSyncRequest;
import webfunds.sox.AccountId;


/**
 * This class is a "Server Agent" that passes basic requests to the Server.
 * It should be passive until requested.
 *
 * @version 1.3
 */
public final class SimpleServer
    extends Debug
    implements Server
{
    protected String logfix = "         i-";

    /**
     * The name of the server.
     * Not used here, just carried.
     */
    protected String name;

   /**
     * The agent communicating to the server (at the "basic" level)
     */
    protected BasicAgent basicAgent;

    /**
     *  The PKI is evolving...
     *
     *  The [operator] certificate is the high level key used by
     *  the operator to sign and authenticate different servers
     *  in his administrative domain.  It is infrequently used,
     *  the [server] keys are delegated with the task of day-to-day
     *  authentication of comms keys.
     *
     *  The [server] key is the key that each physical server has
     *  in order to sign ephemeral (temporary, session) comms keys.
     *  The client can expected the [server] key to be around for
     *  many days, even months.  It is signed by the [operator] key.
     *
     *  Each session will request a comms key, which is made by the
     *  lower-level key exchange protocol.  This comms key will be
     *  authenticated by being signed by the [server] key.
     *  The client can expect the comms key to be valid for at least
     *  one request, and hopefully more, up to many hours worth.
     */
    protected Certificate signer = null;

    /** The [server] certificate for this physical server.  */
    protected Certificate serverCert = null;
    /** The communications certificate (key) for this current session.  */
    protected PublicKey commsKey = null;

    protected int reqNo = 0;



/////////  Instantiation  //////////////////////////////////////////

    /**
     * Create a new SimpleServer object
     * The SimpleServer object will normally be cached by the caller,
     * but is not usefully stored on disk.
     * This call is passive, call getReady() to cause action.
     *
     * @param name our name for the server
     * @param signer the certificate which signs this servers certificate
     * @param agent the comms agent that sends requests at the transport layer
     */
    public SimpleServer(String name, Certificate signer, CommsAgent agent,
                  PrintWriter bug)
    {
        debug(bug, "          i ");

        this.name   = name;
        this.signer = signer;
        if (signer == null)
            throw new IllegalArgumentException("signer <null>");
        if (agent == null)
            throw new IllegalArgumentException("agent <null>");
        basicAgent  = new BasicAgent(agent);

        logmsg("SimpleServer(" + name + ", signer, " + agent + ", bug)");
    }

    /**
     * Do the things necessary for being ready for a request.
     */
    public void getReady()
        throws SOXIssuerException, SOXLaterException
    {
        try {
            getReadyToRequest();
        } catch (SOXServerException ex) {
            throw new SOXIssuerException("" + ex);
        }
    }

    /**
     * Do the things necessary for being ready for a request.
     */
    public void getReadyToRequest()
        throws SOXServerException, SOXLaterException
    {
        // checkSync(); now on demand, not pre-emptive
        if (isDead())
            timesync();    // will set Alive if it works
    }


    /**
     * The name of this server
     * @return the name of the server
     */
    public String getName()
    {
        return name;
    }



/////////  Keys and Certs  //////////////////////////////////////////


    private void refetchCommsKey()
        throws SOXServerException, SOXLaterException
    {
        commsKey = null;
        fetchCommsKey();
    }


    /**
     * Fetch the current communications certificate for this server.
     * The certificate signatures are verified before assigning.
     *
     * None of the PKI is done properly yet.
     *
     * This won't do anything if commsKey is already set.
     */
    private void fetchCommsKey()
        throws SOXServerException, SOXLaterException
    {
        if (commsKey != null)
            return;

        logmsg("Fetching the SOX Server comms certificate");

        Certificate commsCert;
        try {
            commsCert = basicAgent.getCommsKey();
        } catch (SOXLaterException ex) {
            setDead(ex.getMessage());      // URL is wrong or server is down
            throw ex ;
        } catch (SOXPacketException ex) {  // what was this for?
            setDead(ex.getMessage());
            throw new SOXServerException(ex.getNumber(),
                                         "SOXPE 1: " + ex.getMessage());
        } catch (SOXServerException ex) {
            setDead(ex.getMessage());      // BA thinks info is wrong
            throw ex ;
        }

        /*
         * If it's X.509, ignore the signature business because the existing
         * server is broken and we don't want to fix it.
         */
        if( !commsCert.getType().equals("X.509") )
        {
            fetchServerCert();

            PublicKey signerKey = Crypto.getPublicKeyFromCert(signer);
            logmsg("Verifying ServerCert is signed by Server CA certificate");
            if (!Crypto.verifyCertificate(serverCert, signerKey)) {

                byte[] b = signerKey.getEncoded();
                PGPArmoury ok = new PGPArmoury(b, PGPArmoury.TYPE_PUBLIC_KEY);
                b = Crypto.getPublicKeyFromCert(serverCert).getEncoded();
                PGPArmoury sk = new PGPArmoury(b, PGPArmoury.TYPE_PUBLIC_KEY);
                logmsg(
                    "serverCert (first) not signed by operator Cert (2nd)\n\n"+
                    sk + "\n\n\n" + ok + "\n\n");

                throw new SOXServerException(SOXException.SERVER_CERT,
                    "serverCert not signed by operator Cert");
            }

            PublicKey serverKey = Crypto.getPublicKeyFromCert(serverCert);
            logmsg("Verifying CommsCert is signed by serverCert");
            if (!Crypto.verifyCertificate(commsCert, serverKey))
            {
                String e = "commsCert not signed by serverCert";

                serverCert = null;       // must have rolled over?
                // fetchServerCert();    // do a retry on the ServerCert?
                setDead(e);              // no, do a high level retry

                throw new SOXServerException(SOXException.COMMS_CERT, e);
            }
            
            logmsg("Using comms key (valid signature by the server)");
        }

        // Careful not to set this before validating the signature
        commsKey = Crypto.getPublicKeyFromCert(commsCert);

        logmsg("Finished fetchCommsKey at " + System.currentTimeMillis() );
    }

    /**
     *  Fetch the current [server] certificate for this server.
     *  
     *  There is one [server] cert for each physical server (i.e.,
     *  the server defined by this object.
     *
     *  The one [operator] key is used across the entire virtual server
     *  (of many entry points, as described by the URLs in the
     *  SOX Server File).
     *
     *  The certificate signatures are verified before assigning.
     *
     *  This won't do anything if [server] is already set.  If a
     *  signature failure has occurred then set the key to null first.
     */
    private void fetchServerCert()
        throws SOXServerException, SOXLaterException
    {
        if (serverCert != null)
            return;

        logmsg("Requesting the SOX Server [server] certificate");

        Certificate cert = null;
        try {
            cert = basicAgent.getServerKey();
            logmsg("Got a cert!" + serverCert);
        } catch (SOXLaterException ex) {
            setDead(ex.getMessage());      // URL is wrong or server is down
            throw ex ;
        } catch (SOXPacketException ex) {  // what was this for?
            setDead(ex.getMessage());
            throw new SOXServerException(ex.getNumber(),
                                         "SOXPE 2: " + ex.getMessage());
        } catch (SOXServerException ex) {
            setDead(ex.getMessage());      // BA thinks info is wrong
            throw ex ;
        }

        PublicKey signerKey = Crypto.getPublicKeyFromCert(signer);
        logmsg("Verifying ServerCert is signed by Server CA certificate");

        if (!Crypto.verifyCertificate(cert, signerKey))
        {
            String e = "serverCert not signed by operator Cert";

            byte[] b = signerKey.getEncoded();
            PGPArmoury ok = new PGPArmoury(b, PGPArmoury.TYPE_PUBLIC_KEY);
            b = Crypto.getPublicKeyFromCert(cert).getEncoded();
            PGPArmoury sk = new PGPArmoury(b, PGPArmoury.TYPE_PUBLIC_KEY);
            logmsg("serverCert (first) not signed by operator Cert (2nd)\n\n"+
                   sk + "\n\n\n" + ok + "\n\n");

            setDead(e);
            throw new SOXServerException(SOXException.SERVER_CERT, e);
        }

        // Careful not to set this before validating the signature
        serverCert = cert;
    }



/////////  Requests  //////////////////////////////////////////

    /**
     * Do the things necessary for being ready for a request.
     * On return, current is set to a hopefully valid Issuer.
     */
    public byte[] request(Request req)
        throws SOXLaterException, SOXIssuerException
    {
        try {
            return sendRequest(req);
        } catch (SOXServerException ex) {
            throw new SOXIssuerException("" + ex);
        }
    }

    /**
     * Issue a request.
     * Checks first to see if timesync is recent.
     * @except SOXServerException if this Server is dead, try another
     */
    public byte[] sendRequest(Request request)
          throws SOXServerException, SOXLaterException
    {
        // checkSync();  no, on demand, not pre-emptive

        return internalRequest(request);
    }

    /**
     * Issue a request, ignoring timesync (used by timesync itself).
     * // Strategy: if the first request fails, fetch a new key and retry.
     * @except SOXServerException if this Server is dead, try another
     */
    private byte[] internalRequest(Request request)
        throws SOXServerException, SOXLaterException
    {
        fetchCommsKey();


        try {

            try {
                return requestOnce(request);

            } catch(SOXKeyException ex) {
                /*
                 * We are here because the key is stale. Try and get a new
                 * CommsKey *once* and retry the request.
                 */
                logmsg("*** first request failed, refetching comms...");
                refetchCommsKey();
                logmsg("*** trying request again...");
                return requestOnce(request);
            }
        }
        catch (SOXKeyException ex) {
            //
            //  SOXKeyException is thrown when my key is duff.
            //  Let parent (SmartServer) sort it out.
            //
            setDead(ex.getMessage());
            throw new SOXServerException(ex.getNumber(),
                                         "request: " + ex.getMessage());

        } catch (SOXPacketException ex) {
            setDead(ex.getMessage());
            throw new SOXServerException(ex.getNumber(),
                                         "SOXPE 3: " + ex.getMessage());

        } catch (SOXServerException ex) {   // Even if I don't deal with it...
            logmsg("catching (and dying on)" + ex);
            setDead(ex.getMessage());       // ...I should still bail out.
            throw ex ;

        } catch (SOXLaterException ex) {    // Even if I don't deal with it...
            logmsg("catching (and dying on)" + ex);
            setDead(ex.getMessage());       // ...I should still bail out.
            throw ex ;
        }
    }

    /**
     * Issue a request once only.
     */
    private byte[] requestOnce(Request request)
        throws SOXPacketException, SOXKeyException,
               SOXLaterException, SOXServerException
    {
        Key key;
        try {
            key = Crypto.generateKey();
        } catch (java.security.KeyException ex) {
            ex.printStackTrace();
            throw new SOXKeyException("cannot generate key: " + ex);
        }

        EncryptedRequest encReq;
        encReq = new EncryptedRequest(request, key, commsKey);

        long tim = System.currentTimeMillis();
        // logmsg("start " + tim + "... ");

        EncryptedReply reply;
        reply = basicAgent.encryptedRequest(encReq);

        setAlive();

        timeLastRequest = System.currentTimeMillis() - tim;
        logmsg(timeLastRequest + "ms only !");
        byte[] kkk = reply.decrypt(key);
        logmsg(kkk.length + " bytes in packet");
        return kkk ;
    }



/////////  Time Syncronisation  //////////////////////////////////////////
    
    // if a fatal exception is found, set me as Dead, force user to re-invoke
    protected boolean      isDead       = false;
    protected String       reason       = "";
    public boolean         isDead()             { return isDead ; }
    public void            setDead()            { isDead = true ; }
    public void            setDead(String s)    { isDead = true; reason = s; }
    public void            setAlive()           { isDead = false ; }
    public String          getDead()            { return reason; }

    /**
     *  Just to make it obvious what the numbers are used for...
     */
    static final long SECOND = 1000,
                      HOUR = 60 * 60 * SECOND;

    /**
     * This is the difference between local time and the server's time.
     * It is used to set time values to within a short range as a possible
     * defence against replay attacks.
     * It is not really relied upon by any application, but is
     * useful to have for payment window settings.
     * (Deposit replay attack is protected by the unique id.)
     *
     * The time should be persistant so that offline payments can be
     * prepared.
     */
    protected long timediff = 0; //Long.MAX_VALUE;
    protected long deviation = 24 * HOUR;
    protected long lastsync = 0;
    protected long timeLastRequest;

    // should we call checkSync() here?
    public long getTimeDifference() { return timediff ; }
    public long getTimeDeviation() { return deviation ; }


    /**
     * Do a timesync request to check on the server time.
     *
     * @except SOXServerException if this Server is dead, try another
     */
    protected void timesync()
        throws SOXServerException, SOXLaterException
    {

        /*
         *  Ha, we need to measure the time from before the creation
         *  of the TimeSyncRequest to the return of the time, as that
         *  is the relevant deviation including any internal slowdowns
         *  (i.e., the first crypto causes a SecureRandom 20 second
         *  delay!  e.g., swapping...)
         */
        long tim = System.currentTimeMillis();
        logmsg("  Timesyncing.. (" + tim + " now, last was " + lastsync + ")");

        TimeSyncRequest tsr = new TimeSyncRequest(""+reqNo++, new AccountId());
        byte[] packet = this.internalRequest(tsr); // avoids timesync :-)

        TimeSyncReply reply;
        try {
            reply = new TimeSyncReply(tsr, packet);
        } catch (SOXPacketException ex) {
            setDead(ex.getMessage());
            throw new SOXServerException(ex.getNumber(),
                                        "SOXPE TSR: " + ex.getMessage());
        }

        timediff = reply.getTimeDifference();   // as seen by SOX Server

        /*
         *  Now measure the delay of the request and use that as deviation.
         */
        // deviation = timeLastRequest;
        lastsync = System.currentTimeMillis();
        deviation = lastsync - tim;

        logmsg("Timediff = " + timediff + " +- " + deviation +
               "   (" + timeLastRequest + ", complete at " + lastsync + ")");
        if ( (timeLastRequest > (2*SECOND)) || deviation > (3*SECOND) )
            logmsg("Warning:  timesync is taking too long?  " +
                   "(dev == "+deviation+", last == "+timeLastRequest+")");
    }

    /**
     *  Check the sync is reasonably new.  If not, do a timesync.
     *
     *  There is a presumption that is used within the requests,
     *  but that is not really the case as it only includes a
     *  timestamp which is ignored for the request code.
     *  (Historically, this was due to confusion as to which layer
     *  would prevent against replay attacks.)
     *
     *  @except SOXServerException if this Server is dead, try another
     */
    public void checkSync()
        throws SOXServerException, SOXLaterException
    {
        long timeNow = System.currentTimeMillis();
        if (timeNow - lastsync > 24 * HOUR)
        {
            logmsg("trying timesync, last has expired...");
            timesync();
        }
        else if (deviation > (10*SECOND))
        {
            logmsg("trying timesync, last was not quick...");
            timesync();
        }
    }



/////////  Misc / Debug  //////////////////////////////////////////

    public String toString()
    {
        String retval = "SimpleServer " + name + ": " + basicAgent;

        //retval += "\tName: "+name +"\n";
        //retval += "\tBasic Agent: "+basicAgent+"\n";
        //retval += "\tPrimary certificate: "+serverCert+"\n";
        //retval += "\tComms certificate: "+commsKey+"\n";

        return retval;
    }
}



1.1                  java/webfunds/sox/server/SmartServer.java

Index: SmartServer.java
===================================================================
/*
 * $Id: SmartServer.java,v 1.1 2001/04/06 22:49:59 iang Exp $
 *
 * Copyright (c) 2001 Systemics Inc on behalf of
 * the WebFunds Development Team.  All Rights Reserved.
 */
package webfunds.sox.server;

import java.io.PrintWriter;
import java.net.URL;
import java.net.MalformedURLException;
import java.security.cert.Certificate;
import java.util.Date;

import webfunds.utils.Debug;

import webfunds.util.Panic;

import webfunds.comms.*;

import webfunds.sox.Server;
import webfunds.sox.SOXServerException;
import webfunds.sox.SOXIssuerException;
import webfunds.sox.SOXLaterException;
import webfunds.sox.Request;


/**
 *  Pretend to be a single SOX Server but actually manage through
 *  a list of equivalent entry points into the same virtual server.
 *  Hold a bunch of URLs, invoke objects on them on demand, and
 *  switch whenever a problem is detected.
 *
 *  This was webfunds.sox.SmartIssuer.
 */
public class SmartServer
    extends Debug  implements Server                // , IssuerFinder
{
    protected URL[]        urls;
    protected URL[]        others;
    protected int          which;
    protected int          size;

    protected String       name;
    protected Certificate  cert;
    protected SSD          ssd;
    protected DirSSDStore  ssds;
    protected CommsManager comms = null;

    /**
     *  Initialise the real data.
     *  Can be called multiple times if a new SSD is needed.
     *
     */
    private void init()
        throws SOXServerException
    {
        this.which  = 0;
        this.others = null;

        ssd = ssds.get(name);
        if (ssd == null)
            throw new Panic("unknown name in SSD store: " + name);
        urls = ssd.getServerLocation();

        if (urls.length == 0)
            throw new SOXServerException("no servers listed");

        // this.urls = convertToURLs(serverURLs);
        size = urls.length;
        if (size == 0)
            throw new SOXServerException("all server URLs are malformed!");

        startSimpleServers(urls);

        others = ssd.getNearbyURLs();
    }

    /**
     * Create a new Issuer object
     * The Issuer object will normally be cached by the caller,
     * but is not usefully stored on disk.
     * This call is passive, call getReady() to cause action.
     *
     * @param name is the unique name of the SSD/SOX Server
     * @param ssds must provide the SSD for name (must already be there)
     * @param cert the [operator] certificate which signs the
     *               server's [server] certificate
     * @param comms can provide requestor objects at the transport layer
     */
    public SmartServer(String name,
                       DirSSDStore ssds,
                       Certificate cert,
                       CommsManager comms,
                       PrintWriter bug)
        throws SOXServerException
    {
        debug(bug, "        I  ");

        this.name   = name;
        this.ssds   = ssds;
        this.cert   = cert;
        this.comms = comms;

        init();

        logmsg("SmartServer(" + size + " urls, operCert, bug);");
    }



    /**
     * For a single server scenario, this works as an IssuerFinder,
     * by simply returning self.
    public Issuer getIssuer(ItemId id) { id = null; return (Issuer)this ; }
     */

    protected SimpleServer current  = null;
    protected SimpleServer[] servers = null;

    /**
     * Do the things necessary for being ready for a request.
     * On return, current is set to a hopefully valid Issuer.
     */
    public void startSimpleServers(URL[] urls)
    {
        int len = urls.length;
        servers = new SimpleServer[len];
        for (int i = 0; i < len; i++)
        {
            URL url = urls[which % len];
            logmsg("new Simple at " + url);

            CommsAgent agent = new HttpSocketAgent(url, comms, getDebug());
            SimpleServer simp = new SimpleServer(""+i, cert, agent, getDebug());

            servers[i] = simp;
        }
    }

    /**
     * Do the things necessary for being ready for a request.
     * On return, current is set to a hopefully valid Issuer.
     */
    public void getReady()
        throws SOXLaterException, SOXIssuerException
    {
        try {
            getReadyToRequest();
        } catch (SOXServerException ex) {
            throw new SOXIssuerException("" + ex);
        }
    }

    /**
     * Do the things necessary for being ready for a request.
     * On return, current is set to a hopefully valid Issuer.
     */
    public void getReadyToRequest()
        throws SOXLaterException, SOXServerException
    {

        logmsg("getReady()");
        if (current != null)
        {
            if (!current.isDead())
            {
                logmsg(" ... smart 'n ready!");
                return ;
            }

            current = null;
            logmsg("old is dead, need new SimpleServer");
        }
        else
            logmsg("first time, need new SimpleServer");

        URL url;
        HttpAgent http;
        SimpleServer simp;
        int failures = 0;
        int laters   = 0;

        int guard = which + servers.length;     // record where we stop

        while (which++ < guard)
        {
            simp = servers[which % servers.length];

            logmsg(which + ": " + simp);
            try {
                simp.getReadyToRequest();
            } catch (SOXLaterException ex) {
                laters++;
                logmsg(simp + " : later?");
                continue ;
            } catch (SOXServerException ex) {
                failures++;
                logmsg(simp + " : dead!  " + ex);
                continue ;
            }

            logmsg(which + " is good");
            if (!simp.isDead())               // found a good one
            {
                current = simp;
                // checkTimes();
                logmsg("times " + timediff + " / " + deviation);
                return ;
            }
            failures++;  // does this ever happen?
        }

        //
        //  All issuers gave grief.
        //  What we are left with is all issuers down or
        //  unavailable.  However, there may be no net.
        //
        checkNet();

        if (failures == 0 && laters == 0)   // cannot happen?
            throw new Panic("failures == laters == 0: no Issuers?");

        if (failures == 0)
            throw new SOXLaterException(SOXLaterException.LATER_DOWN,
                                        "tried them all, but all are down");

        else if (laters == 0)
            throw new SOXServerException("tried them all, all dead.  Fatal?");
        else
            throw new SOXServerException("tried them all, " +
                                   laters + " later, " + failures + " failed");
    }

    /**
     * Do the things necessary for being ready for a request.
     * On return, current is set to a hopefully valid Issuer.
     */
    public byte[] request(Request req)
        throws SOXLaterException, SOXIssuerException
    {
//        try {
            return sendRequest(req);
//        } catch (SOXServerException ex) {
//            throw new SOXIssuerException("" + ex);
//        }
    }

    public byte[] sendRequest(Request req)
        throws SOXLaterException, SOXServerException
    {
        int guard = which + 2 * servers.length;     // record where we stop
        int failures = 0;
        int laters   = 0;
        while (which < guard)
        {
            if (current.isDead())
                getReadyToRequest();

            byte[] b;
            try {
                b = current.sendRequest(req);
            } catch (SOXLaterException ex) {
                laters++;
                continue ;
            } catch (SOXServerException ex) {
                failures++;
                continue ;
            }
            // checkTimes();
            return b ;
        }

        checkNet();

        if (failures == 0 && laters == 0)
            throw new Panic("failures == laters == 0: no Issuers?");
        if (failures == 0)
            throw new SOXLaterException(SOXLaterException.LATER_DOWN,
                                        "tried them all, but all are down");
        else if (laters == 0)
            throw new SOXServerException("tried them all, all dead.  Fatal?");
        else // mixed results
            throw new SOXLaterException(SOXLaterException.LATER_DOWN,
                                   "tried them all, " +
                                   laters + " later, " + failures + " failed");
    }

    static final String httpGet = "GET / HTTP/1.1\r\nHost: ";
    static final String httpEnd = "\r\nConnection: close\r\n\r\n";
    // static final String httpEnd = "\r\n\r\n";

    /**
     * Try some net hits.
     */
    public int checkNet()
        throws SOXLaterException
    {
        logmsg("checkNet()");
        int num = others.length;
        if (num == 0)
            return 0;

        URL url;
        HttpAgent http;
        int failures = 0;
        int good     = 0;

        for (int i = 0; i < num; i++)
        {
            logmsg("ping: " + i + " of " + num);
            url = others[i];
            logmsg("other URL at " + url);

            String host = url.getHost();

            String getRequest = httpGet + host + httpEnd;
            logmsg(i + ": " + host);
            byte[] page;
            SingleRequestor sr = comms.getSingleRequestor(url);
            sr.setMax(256);

            try {
                page = sr.get(getRequest.getBytes());

            } catch (RawURLException ex) {
                failures++;
                logmsg(url + " : is bad?");
                continue ;
            } catch (RawConnectException ex) {
                failures++;
                logmsg(url + " : later?");
                continue ;
            } catch (RawException ex) {
                String s = "Unknown RawEx: " + ex;
                logmsg(s);
                ex.printStackTrace();
                throw new Panic(s);
            }

            if (page == null)     // why is this?
                throw new Panic("http.get() returned null?");
            if (page.length == 0)
            {
                failures++;
                logmsg(url + " : was empty?");
                continue ;
            }

            logmsg(i + " is good: " + page.length);
            logmsg("-----------\n" + new String(page) + "\n----------");
            good++;
        }

        if (good > 0)
            return good ;

        if (failures == 0)   // cannot happen?
            throw new Panic("failures == laters == 0: no Issuers?");

        throw new SOXLaterException(SOXLaterException.LATER_NET,
                        "cannot see net, check your connectivity");
    }

    /**
     * This is the difference between local time and the server's time.
     * It is used to set time values to within a short range as a possible
     * defence against replay attacks.
     * It is not really relied upon by any application, but is
     * useful to have for payment window settings.
     * (Deposit replay attack is protected by the unique id.)
     *
     * The time should be persistant so that offline payments can be
     * prepared.
     */
    protected long timediff = 0;
    protected long deviation = 24 * SimpleServer.HOUR;

    public long getTimeDifference() { checkTimes(); return timediff ; }
    public long getTimeDeviation()  { return deviation ; }

    protected void checkTimes()
    {
        if (!current.isDead())
        {
            try {
                current.checkSync();
            } catch (SOXLaterException ex) {
                logmsg("checkTimes: (ignoring) current throws Later " + ex);
                return ;
            } catch (SOXServerException ex) {
                logmsg("checkTimes: (ignoring) current throws ServerEx " + ex);
                return ;
            }
        }
        timediff = current.getTimeDifference();
        deviation = current.getTimeDeviation();
    }



    public String toString()
    {
        String retval = "SmartServer. ";

        if (current != null)
            retval += "\tName: " + current.toString();

        return retval;
    }
}