/**
 * This handler is called to manage requests on the proxy port
 *
 * 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: 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!)
 * 
 * Based on com.mortbay.HTTP.*.ProxyHandler
 */

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.*;

public class SfUserAgent
	extends NullHandler
	implements SfHttpProtocol {
	/**
	 * 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;
	public History getHistory() { return history; }

	PrincipalManager principalManager;

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

    /* ------------------------------------------------------------ */
    /** Constructor from properties.
     * Calls setProperties.
     * @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 ums = properties.getProperty("useMacs");
		if (ums!=null) {
			useMacs = ums.startsWith("t");
		}
    }
    
    /* ------------------------------------------------------------ */
    /** Constructor. 
     */
    public SfUserAgent() {
		this(new Properties());
	}

    /* ------------------------------------------------------------ */
    /** Configure from properties.
     * No configuration parameters for this handler
     * @param properties configuration.
     */
    public void setProperties(Properties properties)
    { }
    
    /* ----------------------------------------------------------------- */
    /** Handle proxy requests.
     */
    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());
    }
    
	// 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 (not proxies).
	 */
    public IncomingResponse getHTTP(InetAddress inetAddress, int port,
		URL url, HttpRequest request)
         throws IOException, PageException
    {
		request.setHeader(HttpHeader.Connection,null);
		request.setHeader("Host",null);
       	// request.setVersion(request.HTTP_1_0);
      	request.setVersion("HTTP/1.0");
			// These tweaks have something to do with the fact that
			// I don't understand HTTP/1.1, but that the browser's
			// request may have been 1.1. I think.
//		timingexp.Timeline.timePoint("SUA set up request");

		// 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.
		// Note we have to do this before optimisticAuthorize,
		// or we would be modifying the request after taking its hash.
		if (useMacs) {
			request.setHeader(REQUESTMAC,
				getIdentityPublicKey().toReadableString(2000));
		}

		// Does something if the request looks familiar, else
		// does nothing.
//		if (!preventOptimistic) {
			request = optimisticAuthorize(request, url.toString());
//		}
//		timingexp.Timeline.timePoint("SUA opt auth");

		IncomingResponse inResp = sendRequest(request, inetAddress, port, url);
			// Send the request out the first time.
//		timingexp.Timeline.timePoint("SUA sent request");
		Set tried = new HashSet();
		while (true) {
			if (snowflakeAuthorize(request, inResp, url, tried)) {
//		timingexp.Timeline.timePoint("SUA snowflakeauth");
				// we were able to improve the request with more auth
				// info. Loop around and try again.
//				System.out.println("looping around");
				inResp.close();		// close up the old connection
				inResp = sendRequest(request, inetAddress, port, url);
//		timingexp.Timeline.timePoint("SUA sent request again");
			} else {
				// same ol' same ol' -- sending the request again wouldn't
				// help it.
				break;
			}
		}
//		timingexp.Timeline.timePoint("SUA retry loop done");
		// TODO: explicitly close old response(s), to avoid having
		// an open socket connection linger around needlessly.

		// TODO: if response ends up 401 SfAuth, then remap it to
		// 403 FORBIDDEN to keep browser from getting confused.
		if (serverDemandsSfAuth(inResp)) {
			throw new PageException(HttpServletResponse.SC_FORBIDDEN,
				"Proxy cannot provide valid authorization.");
		} else {
			// Forward the response back to the client.
//		timingexp.Timeline.timePoint("SUA returning response");
			return inResp;
		}
	}

	IncomingResponse sendRequest(HttpRequest request,
		InetAddress inetAddress, int port, URL url)
		throws PageException {
		try {
	        Socket socket= new Socket(inetAddress, port);
			socket.setTcpNoDelay(true);
//		timingexp.Timeline.timePoint("SUA opened sock");
	
			// 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!
//			timingexp.Timeline.timePoint("SfUserAgent: sent request");
//		timingexp.Timeline.timePoint("SUA wrote request");
	
			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
//		timingexp.Timeline.timePoint("SUA got InputStream");
	
			// snoop the response, and if it's a web page (200), then
			// save the url as the "current page"
			IncomingResponse inResp = new IncomingResponse(bis);
//		timingexp.Timeline.timePoint("SUA parsed IncomingResponse");

//			timingexp.Timeline.timePoint("proxy: received response headers");
	
			updateHistory(request, inResp, url.toString());
			extractMAC(request, inResp, url.toString());
//		timingexp.Timeline.timePoint("SUA updated history");
	
			return inResp;
		} catch (IOException ex) {
			throw new PageException(ex);
		} catch (ParseException ex) {
			throw new PageException(ex);
		}
    }

	void extractMAC(HttpRequest request, IncomingResponse inResp,
		String urlStr) {
		try {
			String encryptedMacStr = inResp.getHeader(ENCRYPTEDMAC);
			if (encryptedMacStr==null) {
				return;
			}
			SexpString sexpString = new SexpString(
				new PushbackInputStream(	
					new ByteArrayInputStream(encryptedMacStr.getBytes())
				));
			byte[] encryptedMac = sexpString.bytesContent();

			SDSIPrivateKey privateKey = 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, getIdentityPublicKey());

			pathToMac.put(urlStr, new MacGuy(decryptedMac, macHash));
		} catch (NoSuchAlgorithmException ex) {
			ex.printStackTrace();
		} catch (KeyException ex) {
			ex.printStackTrace();
		} catch (SexpException ex) {
			ex.printStackTrace();
		}
	}

	void updateHistory(HttpRequest request, IncomingResponse inResp,
		String urlStr) {
		try {
			PageHistory ph = new PageHistory();
			ph.url = urlStr;
			// 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.
			String contentTypeHeader = inResp.getHeader("content-type");
			String authHdr = request.getHeader(HTTP_AUTH_RESPONSE);
			if (inResp.getCode()==HttpServletResponse.SC_OK &&
				contentTypeHeader!=null &&
				contentTypeHeader.equalsIgnoreCase("text/html") &&
				authHdr != null &&
				authHdr.startsWith(SNOWFLAKEPROOF)) {
				// Object was an HTML page & authorized with Sf.
				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
		}
	}

	/**
	 * If this is a first attempt for this request, see if we can
	 * preemptively offer valid authorization based on that provided
	 * for a similar previous request. If we succeed, we save the
	 * roundtrip of the server challenging us.
	 */
	HttpRequest optimisticAuthorize(HttpRequest request, String urlStr) {
		Proof cachedProof = (Proof) pathToAuth.get(urlStr);
//		System.err.println("Looked up  "+urlStr+" to: "+
//			((cachedProof==null) ? "null" : prover.getName(cachedProof)));
		if (cachedProof!=null) {
			if (cachedProof.getSubject() instanceof ObjectHash) {
				// It was a proof about a specific request --
				// we'll take the hint that we need auth,
				// but not the proof itself.

				// We should try to use a MacGuy if we can.
				MacGuy mg = null;
				if (useMacs) {
					mg = (MacGuy) pathToMac.get(urlStr);
				}
				if (mg!=null) {
					cachedProof = getAuthorizationFor(mg.macHash,
						cachedProof.getIssuer(), cachedProof.getTag(),
						request);
					
					// Delete the REQUESTMAC header, since (duh) we have
					// one we can use. The whole point is to eliminate
					// public key operations.
					request.setHeader(REQUESTMAC, null);
							
					// now we only need to hash up the request, and
					// we're ready to go!
					byte[] requestBytes = getRequestAsBytes(request);

					ObjectHash reqHash = KeyTools.hashObject(requestBytes);
					MacProof mp = new MacProof(requestBytes, reqHash,
						mg.mac, mg.macHash);

					Proof completeProof = new TwoStepProof(cachedProof, mp);
						// don't even bother digesting this stupid proof;
						// it's very unlikely to be useful again (unless
						// we re-send the identical request).
					
					// pretend this is the one we had in mind all along.
					cachedProof = completeProof;
				} else {
					// write a direct proof that doesn't exploit the MAC
					// principal.
					// TODO: This won't work for issuers that aren't me!
					// getAuthorizationFor() needs to be fixed to get as much
					// of the proof as it can (me => issuer), and write just that
					// last bit (obj hash => me).
					// getAuthorizationFor() will automatically drop in the
					// new request's object hash in place of the old subject.
					cachedProof = getAuthorizationFor(cachedProof.getSubject(),
						cachedProof.getIssuer(), cachedProof.getTag(), request);
				}
			}
			String auth = cachedProof.toReadableString(72);
			request.setHeader(HTTP_AUTH_RESPONSE, SNOWFLAKEPROOF+auth);
		}
		return request;
	}

	boolean serverDemandsSfAuth(IncomingResponse inResp) {
		String authDemand = inResp.getHeader(HTTP_AUTH_CHALLENGE);
		if (authDemand==null) {
			return false;
		}
		return authDemand.startsWith(SNOWFLAKEPROOF);
	}

	/**
	 * Tweak a request to include Snowflake authorization information,
	 * based on the requirements in an actual 401 response from a server.
	 */
	boolean snowflakeAuthorize(HttpRequest request, IncomingResponse inResp,
		URL url, Set tried)
		throws PageException {
		try {
//			timingexp.Timeline.timePoint("proxy: snowflakeAuthorize");
	
			if (inResp.getCode()!=HttpServletResponse.SC_UNAUTHORIZED) {
				// HTTP reply doesn't indicate that anything special needs to
				// be (can be) done.
				return false;
			}
	
			String authDemand = inResp.getHeader(HTTP_AUTH_CHALLENGE);
	
			if (!inResp.getHeader(HTTP_AUTH_CHALLENGE)
					.startsWith(SNOWFLAKEPROOF)) {
				// nope, some other auth method -- pass it through to browser
				return false;
			}
	
			if (authDemand.indexOf(IDENTIFYCLIENT)>=0) {
				SDSIPrincipal identity = getIdentity();
				if (identity==null) {
					throw new PageException("I have no identity.");
				}
	
				// revise the request to mention my preferred identity
				request.setHeader(CLIENTIDENTITY, identity.toReadableString(72));
	
				// process the revised request, which may loop back around
				// for more info of some other form again. Yikes.
				// TODO: for this to work, need to tell getHTTP that we could
				// use another round of auth.
				return true;
			}
	
			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) {
				if (tried.contains(AUTHORIZECLIENT)) {
					return false;
				}
				tried.add(AUTHORIZECLIENT);
				subject = getRequestSubject(request);
			} else if (authDemand.indexOf(AUTHORIZEPROXY)>=0) {
				if (tried.contains(AUTHORIZEPROXY)) {
					return false;
				}
				tried.add(AUTHORIZEPROXY);
				// Proxy needs proof that PROXYPRINCIPAL (a quoting
				// principal) speaks for me.
				Quoting quoting = (Quoting)
					KeyTools.parseString(inResp.getHeader(PROXYPRINCIPAL));
				// TODO: check that quoting is something sensical
				subject = quoting;
				issuer = getIdentity();
			} 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));
	
			String auth = null;
			Proof proof = getAuthorizationFor(subject, issuer, tag, request);
			if (proof!=null) {
				auth = proof.toReadableString(72);
			}
			if (auth!=null) {
				request.setHeader(HTTP_AUTH_RESPONSE, SNOWFLAKEPROOF+auth);
				
				// found a proof; cache it for next time
				// TODO: don't cache this unless the auth actually _works_
				pathToAuth.put(url.toString(), proof);
//				System.err.println("Associated "+url.toString()+" to: "
//					+prover.getName(proof));
			
				return true;
			}
	
//			timingexp.Timeline.timePoint("proxy: snowflakeAuthorize failed");
		} catch (ParseException ex) {
			throw new PageException(ex);
		}
		return false;
	}

	// TODO: won't scale to POST-y 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();
	}

	// Return the 'Subject' that represents a given HTTP request,
	// so the caller can construct a proof authorizing the request.
	Subject getRequestSubject(HttpRequest request) {
		try {
			// We will be replacing the Authorization header, so we
			// certainly don't want to compute the hash over that header.
			request.setHeader(HTTP_AUTH_RESPONSE, null);

			// compute the digest of the outbound request, as
			// that is the subject we're supposed to be authorizing
//			OutputStream last = new NullOutputStream();
//			CountingFilterOutputStream cfos =
//				new CountingFilterOutputStream(last);
//			FileOutputStream objectLog = new FileOutputStream("/tmp/foo3");
//			DigestOutputStream dos = new DigestOutputStream(objectLog, md);
//			request.write(dos);
//			dos.close();
//	Timer t = new Timer();
			MessageDigest md = MessageDigest.getInstance("md5");
			md.update(getRequestAsBytes(request));
			byte[] requestDigest = md.digest();
//	System.out.println("digest at client: "+t);

			Hash requestHash = new Hash("md5", requestDigest);
			ObjectHash subject = new ObjectHash(requestHash);
			return subject;
		} catch (NoSuchAlgorithmException ex) {
			ex.printStackTrace();
		}
		return null;
	}

	SDSIPublicKey getIdentityPublicKey() {
		prover.loadCache();
		Set finalPrincipals = prover.getFinalPrincipals();
		if (finalPrincipals.size()==0) {
			System.err.println("Hey, no identity! Gasp.");
			return null;
		}
		SDSIPrincipal principal = 
			(SDSIPrincipal) finalPrincipals.iterator().next();
		return prover.getPublicKeyForPrincipal(principal);
	}

	SDSIPrivateKey getIdentityPrivateKey() {
		return prover.getPrivateKeyForPublic(getIdentityPublicKey());
	}

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

	Proof getAuthorizationFor(Subject subject, SDSIPrincipal issuer,
		Tag authTag, HttpRequest request) {
		return getAuthorizationFor(subject, issuer, authTag, request, true);
	}

	Proof getAuthorizationFor(Subject subject, SDSIPrincipal issuer,
		Tag authTag, HttpRequest request, boolean construct) {
//		System.err.println("6243: getAuthFor("
//			+prover.getName(subject)+" => "
//			+prover.getName(issuer)
//			+"  rgdg "+authTag+")");

		// Replace a stand-in "PseudoPrincipal" with my own identity
		if (subject instanceof Quoting
			&& ((Quoting) subject).getQuotee() instanceof PseudoPrincipal) {
			try {
				subject = new Quoting(((Quoting) subject).getQuoter(), issuer);
			} catch (SexpParseException ex) {
				ex.printStackTrace();
				return null;
			}
		}

		// ensure we've seen the latest set of certificates available
		prover.loadCache();
		Proof proof = prover.getProof(issuer, subject, authTag);
		if (proof != null
			|| !construct) {
			return proof;
		}
		// If subject is a quoting principal, we may just have to write
		// up the proof right here. Or maybe it's our request hash.
		if (prover.isFinal(issuer)) {
			// TODO: issuer doesn't have to be final, we just have to
			// have a final subject that speaks for it.

			if (subject instanceof Quoting) {
				// Does the subject make sense? It should be the server we're
				// talking to, quoting me.
				// TODO: this only checks the "quoting me" part.
				Quoting quoting = (Quoting) subject;
				if (!KeyTools.arePrincipalsEquivalent(
						(SDSIObject) quoting.getQuotee(), issuer)) {
					System.err.println("Quotee is not me. Hmm.");
					return null;
				}
			} else if (subject instanceof ObjectHash) {
				// ensure this is actually the request we're sending that
				// we'll be signing.
				subject = getRequestSubject(request);
			} else {
				System.err.println("Unexpected subject type");
				return null;
			}
			// Okay, sign off on the requested delegation
			return prover.createAuth(subject, issuer);
		}
		return null;
	}

	/**
	 * Oh, that OTHER 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());
    }
}
