package jp;

import java.io.*;
import java.net.*;
import java.util.*;
import java.security.*;
import javax.servlet.*;
import javax.servlet.http.*;

import sdsi.*;
import sdsi.sexp.*;
import servlet.*;
import proof.*;
import Tools.*;

import com.mortbay.Base.*;
import com.mortbay.HTTP.*;
import com.mortbay.HTTP.Handler.NullHandler;
import com.mortbay.Util.*;
import com.mortbay.FTP.*;

/**
 * This handler is called to manage requests on the proxy port<p>
 *
 * The Sf HTTP Authorization protocol is based on the
 * HTTP ``Authentication'' spec in RFC 2617.
 * ftp://ftp.isi.edu/in-notes/rfc2617.txt
 * <p>
 *
 * It's called a SfUserAgent to represent the fact that it is trying
 * to act like part of the user's web browser. (It belongs on the same
 * host, for example.) And the notion of ``proxy'' in Snowflake has to
 * do with protocol translation in the middle of a transaction somewhere.
 * This use of HTTP proxying is meant to be the endpoint of a transaction,
 * as close as we can get to the user.<p>
 * 
 * @todo Ensure that it's always the same user accessing this proxy,
 * perhaps by using <code>identd</code> on localhost.
 * @todo turn history-tracking stuff into a second handler layer that's
 * independent of the authenticating proxy. (Would that require a
 * second snoop-'n'-parse of the incoming headers? yuk!)
 * 
 * @author jonh@cs.dartmouth.edu
 * @author Based on com.mortbay.HTTP.*.ProxyHandler
 */

public class SfUserAgent
	extends NullHandler
	implements SfHttpProtocol, RequestStates {
	/**
	 * the thing that stores prooflets (certificates, maps from hashes
	 * to keys, and perhaps even stores subproofs to save time), and
	 * can be asked to generate needed proofs.
	 */
	Prover2 prover;
	History history;	// history.elementAt(0) is the most recently loaded page

	PrefixMap pathToAuth;
	PrefixMap pathToMac;

	static final int HISTORY_SIZE = 10;

	/**
	 * @deprecated The {@link PrincipalMaganer} used to call this to
	 * get the list of visited URLs maintained by this proxy.
	 */
	public History getHistory() { return history; }

	PrincipalManager principalManager;

//	boolean preventOptimistic = false;
	boolean useMacs = true;
	boolean authenticateServer = false;

    /** Constructor from properties.
     * Calls setProperties. Three properties are defined for this
	 * handler: certDir, useMacs, and authenticateServer.
	 * <dl>
	 * <dt>certDir <dd>is a directory that contains bootstrap certificates, and
	 * where new certificates or keys may be stored.
	 * <dt>useMacs <dd>is a boolean parameter indicating whether the client
	 * should try to use the MAC protocol to speed requests.
	 * <dt>authenticateServer <dd>is a boolean parameter that indicates
	 * whether the client should check for a proof of document authenticity
	 * from the server.
	 * </dl>
	 *
     * @param properties Configuration properties
     */
    public SfUserAgent(Properties properties)
    {
		prover = new Prover2(properties.getProperty("certDir"));
		history = new History(10);
		principalManager = new PrincipalManager(prover, getHistory());
		pathToAuth = new PrefixMap();
		pathToMac = new PrefixMap();
//		preventOptimistic = properties.getProperty("preventOptimistic")
//			.startsWith("t");
		String umStr = properties.getProperty("useMacs");
		if (umStr!=null) {
			useMacs = umStr.startsWith("t");
		}
		String asStr = properties.getProperty("authenticateServer");
		if (asStr!=null) {
			authenticateServer = asStr.startsWith("t");
		}
    }
    
    public SfUserAgent() {
		this(new Properties());
	}

    /** Configure from properties.
     * This handler doesn't support dynamic reconfiguration.
     * @param properties configuration.
     */
    public void setProperties(Properties properties)
    {
		throw new RuntimeException(	
			"This handler doesn't support dynamic reconfiguration.");
	}
    
    /**
	 * Handle proxy requests. Jetty sends requests coming in from the
	 * browser to this method.
	 *
	 * @param request the request from the browser
	 * @param response the object that collects the response to return to the
	 * browser. It is returned once handle() returns.
     */
    public void handle(HttpRequest request,
                       HttpResponse response) {
		 try {
//			timingexp.Timeline.zeroTimer();
//			timingexp.Timeline.timePoint("proxy: transaction initiated");
	
			String[] mup = parseRequestLine(request);
			String url = mup[1];
	        if (url.startsWith("file:")) {
				// Don't handle file: requests -- gaping security
				// hole if we accidentally answer a request from off this machine.
				// If we have an application where we need to proxy file:
				// requests, then we should also be uptight about only
				// receiving requests from browsers on the local host.
	            // getFile(response,url);
	        } else if (url.startsWith("http://security.localhost")) {
				try {
					principalManager.service(request, response);
				} catch (ServletException ex) {
					response.sendError(
						HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
						ex.toString());
				}
	        } else if (url.startsWith("http:")) {
	            getHTTP(request, response);
	        } else if (url.startsWith("ftp:")) {
	            getFTP(response,url);
			}
//			timingexp.Timeline.timePoint("proxy: transaction complete THE END");
		} catch (Exception ex) {
			PageException pex = new PageException(ex);
			pex.sendResponseTo(response);
//			timingexp.Timeline.timePoint(
//				"proxy: transaction exception THE END");
		}
    }

    /* ---------------------------------------------------------------- */
    void getFile(HttpResponse response,String url)
         throws IOException
    {
        String mimeType = httpServer.getMimeType(url);
        String filename = url.substring(url.indexOf(":")+1);
        if (filename.indexOf("?")>=0)
            filename=filename.substring(0,filename.indexOf("?"));
        Code.debug("get File="+filename+" of type "+mimeType);
        response.setContentType(mimeType);
        File file = new File(filename);
        FileInputStream in =new FileInputStream(file);
        IO.copy(in,response.getOutputStream());
    }
    
	/**
	 * Oh, that OTHER method. This code is directly swiped from Jetty's
	 * example, since I don't care about the FTP method.
	 */
    void getFTP(HttpResponse response,String url)
         throws IOException
    {
        String mimeType = httpServer.getMimeType(url);
        Code.debug("FTP="+url+" of type "+mimeType);
        Ftp ftp = new Ftp();
        ftp.getUrl(url,response.getOutputStream());
    }

	/**
	 * @todo this would be better done in
	 * com.mortbay.HTTP.HttpRequest, where the protocol is just sitting
	 * there in a field. 
	 */
	String[] parseRequestLine(HttpRequest request) {
        try{
			String requestLine = request.getRequestLine();
            int s1=requestLine.indexOf(' ',0);
            int s2=requestLine.indexOf(' ',s1+1);
			String method = requestLine.substring(0, s1);
            String url = requestLine.substring(s1+1, s2);
			String protocol = requestLine.substring(s2+1);
//			System.out.println("Protocol from browser is "+protocol);
			return new String[] { method, url, protocol };
        } catch(Exception e) {
            Code.warning(e);
        }
		return null;
	}

	/**
	 * @todo this will all break for things more sophisticated than GET
	 * requests, because we will consume the input stream on the first
	 * transmission, and not have it available for the retransmission.
	 * I wonder what the HTTP model is for uploading a 5MB file with
	 * authorization -- at what point do you find out whether your POST
	 * is going to get turned down?
	 */
    void getHTTP(HttpRequest request, HttpResponse outResp)
		throws IOException {
		try {
			String[] mup = parseRequestLine(request);
        	URL url = new URL(mup[1]);

			InetAddress inetAddress = InetAddress.getByName(url.getHost());
        	int port = url.getPort();
			if (port<0) {
				port = 80;
			}

			IncomingResponse inResp = getHTTP(inetAddress, port, url, request);
			inResp.passAlongResponse(outResp);
			inResp.close();
		} catch (PageException ex) {
			ex.sendResponseTo(outResp);
		}
	}

	/**
	 * If the get fails, the error comes out as a PageException, which
	 * the previous version of getHTTP goes ahead and squirts back to
	 * the browser. This method is factored out so it can be called in
	 * other contexts other than the proxy, such as by the experimental
	 * testing harness {@link timingexp.HttpExp}.
	 */
    public IncomingResponse getHTTP(InetAddress inetAddress, int port,
		URL url, HttpRequest request)
		throws IOException, PageException
    {
		IncomingResponse inResp = null;
			// This object is the reply from the server that we're working with

		RequestState reqState = new RequestState(request);
			// This object keeps track of what we've tried to make this
			// request work.

		stripHttp11(request);

		Proof authToUse = null;
			// different states may suggest an authToUse

		// Hey, I'm a big state machine.
		while (reqState.getState()!=REQ_DONE) {
			switch (reqState.getState()) {
			case TRY_IDENTICAL: {
// System.out.println("case TRY_IDENTICAL:");
				Proof hint = reqState.getHint();
				reqState.setState(TRY_MAC);
				if (hint!=null
					&& hint.getSubject() instanceof ObjectHash
					&& hint.getSubject().equals(reqState.getRequestHash())) {
					// we have a proof for this very request. Use it
					authToUse = hint;
// System.err.println("using TRY_IDENTICAL");
					reqState.setState(SEND_REQUEST);
				}
				break;
			}
			case TRY_MAC:
// System.out.println("case TRY_MAC:");
				MacGuy mg = reqState.getMacHint();
				reqState.setState(TRY_HINT);
				if (useMacs && mg!=null) {
					Proof hint = reqState.getHint();
					if (hint!=null) {
						// Our previous request for a MAC worked, so
						// we don't need another one.
						reqState.clearMacRequest();

						Proof macToIssuer = getAuthorizationFor(mg.macHash,
							hint.getIssuer(), hint.getTag());
						MacProof mp = new MacProof(
							reqState.getRequestBytes(),
							reqState.getRequestHash(), mg.mac, mg.macHash);
						authToUse = new TwoStepProof(macToIssuer, mp);
							// don't even bother digesting this
							// stupid proof; it's very unlikely to
							// be useful again (unless we re-send
							// the identical request).
						if (authToUse!=null) {
// System.err.println("using TRY_MAC");
							reqState.setState(SEND_REQUEST);
						}
					}
				}
				break;
			case TRY_HINT:
// System.out.println("case TRY_HINT:");
				Proof hint = reqState.getHint();
				reqState.setState(SEND_REQUEST);
					// TRY_NOTHING does nothing, so I "optimized" it out.
				if (hint!=null) {
//	System.out.println("hint subject is "+hint.getSubject().getClass());
					if (hint.getSubject() instanceof Quoting) {
						// this proof delegates control to a gateway,
						// so I guess we don't need to authorize
						// this specific request. (But then how does
						// the gateway know this request is from us?
						// TODO: hijacking opportunity.)
						authToUse = hint;
					} else {
						// this will be slow; ask server to help make it
						// faster next time by sending us a MAC
						reqState.requestMac();
						authToUse = getAuthorizationFor(
							reqState.getRequestHash(),
							hint.getIssuer(), hint.getTag());
// if (authToUse!=null) {
// System.out.println("So then I replaced the stinking proof with a slow proof."
// 	+prover.getProofName(authToUse,true));
// }
						if (authToUse!=null) {
							reqState.setState(SEND_REQUEST);
						}
					}
				}
// System.err.println("using TRY_HINT");
				break;
			case SEND_REQUEST:
// System.out.println("case SEND_REQUEST:");
				reqState.countLoops();	// don't go around too many times
// if (authToUse!=null) {
//	System.out.println("=== So then I sez:\n"+prover.getProofName(authToUse,true));
// }
				reqState.setAuth(authToUse);
				inResp = sendRequest(request, reqState, inetAddress, port, url);
				// debugRequests(request, inResp);
				int replyCode;
				try {
					replyCode = inResp.getCode();
				} catch (ParseException ex) {
					throw new PageException(ex);
				}

				if (replyCode != HttpServletResponse.SC_UNAUTHORIZED) {
					// request was as accepted as it's gonna be
					reqState.setState(REQ_DONE);
					break;
				}

				// Before we go back around, we should remove any
				// auth header from the request so we don't include
				// it in a computed hash
				reqState.clearAuth();
				authToUse = null;	// start figuring auth from scratch

				if (satisfyIDRequest(inResp, reqState)) {
					// no point in trying any fancier auth if we've already
					// tried once and server didn't even recognize us.
					inResp.close();		// close up the old connection
					reqState.setState(TRY_NOTHING);
					break;
				}
				if (satisfyAuthDemand(inResp, reqState)) {
					// Server gave us the necessary "hint:" the issuer
					// and tag our request needs to speak for.
					// The satisfyAuthDemand() call stuffed the hint
					// away in the pathToAuth cache, so the next
					// state is TRY_IDENTICAL.
					inResp.close();		// close up the old connection
					reqState.setState(TRY_IDENTICAL);
					break;
				}
				// Hmm. I have no idea what server wants!
				throw new PageException(HttpServletResponse.SC_FORBIDDEN,
					"Proxy doesn't understand server's demands.");
			default:
				throw new PageException("Internal error: undefined state.");
			}
		}

		updateHistory(request, reqState, inResp);
		extractMAC(request, reqState, inResp);
		if (authenticateServer) {
			checkServerAuthentication(request, reqState, inResp);
		}

		return inResp;
	}

	static final byte[] debugBoundary = "---------".getBytes();
	void debugRequests(HttpRequest request, IncomingResponse inResp) {
		try {
			FileOutputStream fos;
			fos = new FileOutputStream("/tmp/sf-req", true);
			fos.write(debugBoundary);
			request.write(fos);
			fos.close();
			fos = new FileOutputStream("/tmp/sf-rep", true);
			fos.write(debugBoundary);
			fos.write(inResp.getResponseLine().getBytes());
			inResp.write(fos);
			fos.close();
		} catch (IOException ex) {
		} catch (ParseException ex) {
		}
	}

	void debugAuth(Proof proof) {
		try {
			FileOutputStream fos;
			fos = new FileOutputStream("/tmp/sf-auth", true);
			fos.write(debugBoundary);
			String foo = proof.toReadableString(80);
			fos.write(foo.getBytes());
			fos.close();
		} catch (IOException ex) {
		}
	}

	void stripHttp11(HttpRequest request) {
		// This code doesn't yet understand HTTP/1.1 streams, so dumb
		// all requests down to HTTP/1.0.
		request.setHeader(HttpHeader.Connection,null);
		request.setHeader("Host",null);
       	// request.setVersion(request.HTTP_1_0);
      	request.setVersion("HTTP/1.0");
	}

	IncomingResponse sendRequest(HttpRequest request, RequestState reqState,
		InetAddress inetAddress, int port, URL url)
		throws PageException {
		try {
	        Socket socket= new Socket(inetAddress, port);
			socket.setTcpNoDelay(true);
	
			// This buffer doesn't seem to be working -- multiple
			// write requests are going out.
			BufferedOutputStream bos =
				new BufferedOutputStream(socket.getOutputStream());
	   		request.write(bos);
			bos.flush();
				// flushing bos should flush os, too.
				// don't close() -- that closes the entire socket fd!
	
			InputStream is = socket.getInputStream();
			BufferedInputStream bis =
				new BufferedInputStream(socket.getInputStream());
			// TODO: Once we start reusing the socket (HTTP/1.1 or keep-alive),
			// we'll need to use the same BufferedInputStream the whole time
	
			// snoop the response, and if it's a web page (200), then
			// we'll save the url as the "current page"
			IncomingResponse inResp = new IncomingResponse(bis);

//			updateHistory(request, reqState, inResp);
//			extractMAC(request, reqState, inResp);
	
			return inResp;
		} catch (IOException ex) {
			throw new PageException(ex);
		} catch (ParseException ex) {
			throw new PageException(ex);
		}
    }

	void updateHistory(HttpRequest request, RequestState reqState,
		IncomingResponse inResp) {
		try {
			// Don't log something on the history page unless the
			// browser actually displayed it (SC_OK) and it was a
			// page (HTML), not a gif or some other included piece.
			if (inResp.getCode()!=HttpServletResponse.SC_OK) {
				return;
			}
			String authHdr = request.getHeader(HTTP_AUTH_RESPONSE);
			String contentTypeHeader = inResp.getHeader(HttpHeader.ContentType);
			if (contentTypeHeader == null
				|| authHdr == null
				|| !contentTypeHeader.equalsIgnoreCase("text/html")
				|| !authHdr.startsWith(SNOWFLAKEPROOF)) {
				return;
			}

			// Object was an HTML page & authorized with Sf.
			PageHistory ph = new PageHistory();
			ph.url = reqState.getURL();;
			ph.sfProof = ProtectedServlet.extractProof(authHdr);
			if (ph.sfProof!=null) {
				ph.snowflakeStatus = PageHistory.SF_SUCCESS;
			}
			history.addHistory(ph);
		} catch (ParseException ex) {
			// don't bother updating history -- response was garbled
		}
	}

	void extractMAC(HttpRequest request, RequestState reqState,
		IncomingResponse inResp) {
		if (!useMacs) {
			return;
		}
		try {
			String encryptedMacStr = inResp.getHeader(ENCRYPTEDMAC);
			if (encryptedMacStr==null) {
				return;
			}

			// parse out the MAC
			SexpString sexpString = new SexpString(
				new PushbackInputStream(	
					new ByteArrayInputStream(encryptedMacStr.getBytes())
				));
			byte[] encryptedMac = sexpString.bytesContent();

			// decrypt the MAC
			SDSIPrivateKey privateKey = prover.getIdentityPrivateKey();
			String algorithm = "RSA";
				// should be able to say privateKey.getAlgorithm()
			Cipher cipher = Cipher.getInstance(algorithm);
			cipher.initDecrypt(privateKey);
			byte[] decryptedMac = cipher.crypt(encryptedMac);

			// This should be the same as paddedMac on the server side.
			ObjectHash macHash = KeyTools.hashObject(decryptedMac);

			// Create a proof step so that macHash => me; that way
			// whenever I want to use macHash as a shortcut, I just
			// say requestHash => macHash.
			prover.createAuth(macHash, prover.getIdentityPublicKey());

			// Associate the mac and its hash with the current URL.
			// If we were clever, we'd have the server tell us the "scope"
			// (say, URL prefix) within which the MAC will be understood;
			// for now, we use the document path. Too specific.
			pathToMac.put(reqState.getURL(), new MacGuy(decryptedMac, macHash));
		} catch (NoSuchAlgorithmException ex) {
			ex.printStackTrace();
		} catch (KeyException ex) {
			ex.printStackTrace();
		} catch (SexpException ex) {
			ex.printStackTrace();
		}
	}

	/**
	 * Verify that the returned document meets the authentication
	 * requirements we expect.
	 * 
	 * @todo Doesn't do anything if authentication
	 * info not present. Gets upset if it's present but
	 * unverifiable or otherwise bogus. A real app should treat missing
	 * info a lot like unverifiable info.
	 */
	void checkServerAuthentication(HttpRequest request, RequestState reqState,
		IncomingResponse inResp) {
		boolean installed = false;
		try {
			String docForServerNameProof = inResp.getHeader(DOCFORSERVERNAME);
			if (docForServerNameProof!=null) {
				SDSIObject so = KeyTools.parseString(docForServerNameProof);
				if (so instanceof Proof) {
					Proof po = (Proof) so;
					if (po.getSubject() instanceof ObjectHash) {
// TODO: do something useful with tags & proofs toward names
//						po.verify(prover.getIdentityPublicKey(), docHash,
//							Tag.getTagStar());

						// no point in having prover look at object hash,
						// since the only useful mapping immediately follows
						// proof 'po'. Maybe we'd discover some surprising
						// things about other people sending us the same
						// doc, but whoopee.
						po.verify();
						List nl = prover.getNames(po.getIssuer(), 1);
						if (nl.size()<1) {
							throw new InvalidProofException(
								"server not known by any name.");
						}
//						System.out.println("server known as "
//							+prover.getName(nl.get(0)));

						ObjectHash docHash = (ObjectHash) po.getSubject();
						inResp.attachDocumentVerifier(docHash);
						installed = true;
					}
				}
			}
		} catch (InvalidProofException ex) {
			// will pick this up as installed==false
			System.out.println(ex.toString());
		}
		if (installed == false) {
			System.out.println("no server authentication for document.");
		}
	}

	boolean satisfyIDRequest(IncomingResponse inResp, RequestState reqState)
		throws PageException {

		String authDemand = inResp.getHeader(HTTP_AUTH_CHALLENGE);
		if (!authDemand.startsWith(SNOWFLAKEPROOF)
			|| authDemand.indexOf(IDENTIFYCLIENT)<0) {
			return false;
		}

		reqState.trying(IDENTIFYCLIENT);
	
		// revise the request to mention my preferred identity
		SDSIPrincipal identity = getIdentity();
		if (identity==null) {
			throw new PageException("I have no identity.");
		}
		reqState.setIdentity(identity.toReadableString(2000));
		return true;
	}

	boolean satisfyAuthDemand(IncomingResponse inResp, RequestState reqState)
		throws PageException {
		try {
			String authDemand = inResp.getHeader(HTTP_AUTH_CHALLENGE);
			if (!authDemand.startsWith(SNOWFLAKEPROOF)) {
				return false;
			}
	
			SDSIPrincipal issuer=null;
			Tag tag=null;
			Subject subject = null;
				// null means subject is this request;
				// non-null means a particular quoted
				// principal used by the proxy
	
			if (authDemand.indexOf(AUTHORIZECLIENT)>=0) {
				reqState.trying(AUTHORIZECLIENT);
				subject = reqState.getRequestHash();
			} else if (authDemand.indexOf(AUTHORIZEPROXY)>=0) {
if (false) {
// jonh disables this, since we might go 'round twice to get talking
// to the MailServlet.
				reqState.trying(AUTHORIZEPROXY);
}
				// Proxy needs proof that PROXYPRINCIPAL (a quoting
				// principal) speaks for me.
				Quoting quoting = (Quoting)
					KeyTools.parseString(inResp.getHeader(PROXYPRINCIPAL));
				subject = quoting;
				issuer = getIdentity();

				// Fill in a psuedoprincipal with my identity
				if (quoting.getQuotee() instanceof PseudoPrincipal) {
					quoting = new Quoting(quoting.getQuoter(), issuer);
					// TODO: again, by 'issuer' here we mean 'me'
				}

				// check that quoting is something sensical
				if (!KeyTools.arePrincipalsEquivalent(
						(SDSIObject) quoting.getQuotee(), issuer)) {
					throw new PageException("Quotee is not me. Hmm.");
					// TODO: well, it doesn't HAVE to be me -- it can
					// be any principal I speak for, right?
				}

				// okay, then, we're now happy to sign off on the request.
				subject = quoting;
			} else {
				throw new
					PageException("I don't understand server's challenge");
			}
	
			if (issuer==null) {
				issuer = (SDSIPrincipal)
					KeyTools.parseString(inResp.getHeader(SERVICEISSUER));
			}
	
			tag = (Tag) KeyTools.parseString(inResp.getHeader(MINIMUMTAG));
			System.out.println("server wants proof rgdg tag "
				+tag.toReadableString(72));
	
			String auth = null;
			Proof proof = getAuthorizationFor(subject, issuer, tag);

			// found a proof; cache it for next time
			// TODO: don't cache this unless the auth actually _works_
			// Although right now, this is the mechanism by which we
			// pass the proof up to the TRY_IDENTICAL part of the
			// state machine that signs off on the individual request
			// itself.
			pathToAuth.put(reqState.getURL(), proof);
//			System.err.println("stashing proof under "+reqState.getURL());

//			System.out.println("=== Good for me, I built this proof:\n"+
//				prover.getProofName(proof, true));

			return true;
		} catch (SexpParseException ex) {
			throw new PageException(ex);
		}
	}

	/**
	 * Translate a Jetty HttpRequest into a bytestring for hashing.
	 * This method is used by servlet.PSHandler, too. Yuk; it should
	 * be factored into a Tools class or somewhere more reasonable.
	 * 
	 * @todo won't scale to POST requests...
	 */
	public static byte[] getRequestAsBytes(HttpRequest request) {
		ByteArrayOutputStream baos;
		try {
			baos = new ByteArrayOutputStream();
			// TODO URGENT: write the initial line out, too,
			// not just the headers!
			request.write(baos);
		} catch (IOException ex) {
			return null;
		}
		return baos.toByteArray();
	}

	/**
	 * Look up the first principal that I can speak for, and use that
	 * as my identity. Always returns a hash.
	 */
	Hash getIdentity() {
		SDSIPublicKey publicKey = prover.getIdentityPublicKey();
		try {
			return new Hash("md5", publicKey);
		} catch (NoSuchAlgorithmException ex) {
		}
		return null;
	}

	Proof getAuthorizationFor(Subject subject, SDSIPrincipal issuer,
		Tag authTag) {

		// ensure we've seen the latest set of certificates available
		prover.loadCache();
		Proof proof = prover.getProof(issuer, subject, authTag);
		if (proof != null) {
			return proof;
		}

		if (!prover.isFinal(issuer)) {
			// get a subproof that we control
			proof = prover.getProof(issuer, prover.getIdentityPublicKey(),
				authTag);
			if (proof == null) {
				return null;
			}
			// write up a proof from subject to me as issuer
			Proof subjectForMe = prover.createAuth(subject,
				prover.getIdentityPublicKey(), authTag,
				prover.getIdentityPublicKey());
			// concatenate them.
//			System.out.println("Merging "+prover.getName(proof));
//			System.out.println("    and "+prover.getName(subjectForMe));
			proof = new TwoStepProof(proof, subjectForMe);
		} else {
			// write up a proof from subject to (final) issuer
			// This case is only subtle if prover knows about more
			// private keys than the one reported by prover.getIdentity*().
			proof = prover.createAuth(subject, issuer, authTag,
				prover.getPublicKeyForPrincipal(issuer));
		}

		return proof;
	}

	class RequestState {
		static final int MAX_LOOPS = 3;
		int state = TRY_IDENTICAL;
		int loops = 0;
		HttpRequest request;
		String url = null;
		ObjectHash requestHash = null;
		boolean sentID = false;
		HashSet tried = new HashSet();

		RequestState(HttpRequest request) {
			this.request = request;
		}

		void setState(int state) { this.state = state; }

		int getState() { return state; }

		Proof getHint() {
			Proof proof = (Proof) pathToAuth.get(getURL());
//			if (proof!=null) {
//			System.out.println("=== And then pulled this out of my ... cache\n"+
//				prover.getProofName(proof, true));
//			}
			return proof;
		}

		MacGuy getMacHint() {
			return (MacGuy) pathToMac.get(getURL());
		}

		String getURL() {
			if (url==null) {
				String[] mup = parseRequestLine(request);
				url = mup[1];
			}
			return url;
		}

		byte[] getRequestBytes() {
			return getRequestAsBytes(request);
		}

		ObjectHash getRequestHash() {
			if (requestHash==null) {
				// This cached value can become stale if request is
				// modified; any code modifying the request must clear
				// the entry.
				requestHash = KeyTools.hashObject(getRequestBytes());
			}
			return requestHash;
		}

		// Ask the server to encrypt a MAC token we can use for
		// faster auth in the future.
		// TODO: 2000-char-long line is because the server (servlet)
		// can't always reconstruct the headers exactly as we send
		// them to do the hash. It would be a lot better if we were
		// to just fix Jetty HttpRequest/HttpResponse so that we
		// could do this right.
		void requestMac() {
			if (useMacs) {
				request.setHeader(REQUESTMAC,
					prover.getIdentityPublicKey().toReadableString(2000));
				requestHash = null;
			}
		}

		void clearMacRequest() {
			if (useMacs) {
				request.setHeader(REQUESTMAC, null);
				requestHash = null;
			}
		}

		void setAuth(Proof authToUse) {
			if (authToUse!=null) {
//				Timer t = new Timer();
//				request.setHeader(HTTP_AUTH_RESPONSE,
//					SNOWFLAKEPROOF+authToUse.toReadableString(72));
//				System.out.println("auth.toReadableString: "+t);
				// debugAuth(authToUse);

				// Put a 'hex' on the canonical representation -- it's
				// much faster.
				byte[] buf = Hex.bytesToHex(authToUse.getSrep().getCanonRep());
				request.setHeader(HTTP_AUTH_RESPONSE,
					SNOWFLAKEPROOF+(new String(buf)));

//				try {
//				OutputStream os = new FileOutputStream("/tmp/foo");
//				os.write(request.getHeader(HTTP_AUTH_RESPONSE).getBytes());
//				os.close();
//				} catch (Exception ex) {ex.printStackTrace(); }
				requestHash = null;
			}
		}

		void setIdentity(String id) {
			request.setHeader(CLIENTIDENTITY, id);
			requestHash = null;
			sentID = true;
		}

		void clearAuth() {
			request.setHeader(HTTP_AUTH_RESPONSE, null);
			requestHash = null;
		}

		void countLoops() throws PageException {
			loops++;
			if (loops>MAX_LOOPS) {
				throw new PageException("Too many ("+loops+") auth attempts.");
			}
		}

		void trying(Object which) throws PageException {
			if (tried.contains(which)) {
				throw new PageException("Already tried "+which);
			}
			tried.add(which);
		}
	}
}
