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

/* --------------------------------------------------------------------- */
/** Proxy HttpHandler
 * This handler can service requests of the form:<PRE>
 * METHOD [http|ftp|file]:URL VERSION
 * </PRE>
 * Which are generated by browsers that are talking to a proxy.  This
 * handler can be used as a simple proxy or the basis of an advanced
 * proxy.
 * <h3>Notes</h3>
 * The handler must be installed in a handler stack starting with "http:"
 * or "ftp:".  For a HTTP proxy, the ParamHandler should also be in the
 * stack before the ProxyHandler, so that form content is read.
 */
public class ProxyHandler
	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;

	static final int HISTORY_SIZE = 10;
	public History getHistory() { return history; }

	PrincipalManager principalManager;

    /* ------------------------------------------------------------ */
    /** Constructor from properties.
     * Calls setProperties.
     * @param properties Configuration properties
     */
    public ProxyHandler(Properties properties)
    {
		prover = new Prover2(properties.getProperty("certDir"));
		principalManager = new PrincipalManager(prover, this.getHistory());
		history = new History(10);
		pathToAuth = new PrefixMap();
    }
    
    /* ------------------------------------------------------------ */
    /** Constructor. 
     */
    public ProxyHandler() {
		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)
         throws IOException {
		timingexp.Timeline.zeroTimer();
		timingexp.Timeline.timePoint("proxy: transaction initiated");
        String requestLine = request.getRequestLine();
		String url="";
		String protocol="";
        try{
            int s1=requestLine.indexOf(' ',0);
            int s2=requestLine.indexOf(' ',s1+1);
            url=requestLine.substring(s1+1,s2);
			protocol = requestLine.substring(s2+1);
			System.out.println("PRotocol from browser is "+protocol);
        } catch(Exception e) {
            Code.warning(e);
        }
        
        Code.debug("Proxy request "+url);
        if (url.startsWith("file:")) {
			// Don't handle file: requests -- gaping security
			// hole when answering requests 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(response,url,protocol);
        } else if (url.startsWith("ftp:")) {
            getFTP(response,url);
		}
		timingexp.Timeline.timePoint("proxy: transaction complete");
    }

    /* ---------------------------------------------------------------- */
    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 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(HttpResponse response, String urlStr, String protocol)
         throws IOException
    {
        Code.debug("get URL="+urlStr);
        HttpRequest request = response.getRequest();
        URL url=new URL(urlStr);

        int port = url.getPort() ;
        Socket socket= new Socket(url.getHost(),port<0?80:port);

        try {
            String newPath = new URI(url.getFile()).getPath();
            request.translateAddress(request.getResourcePath(),
                                     newPath,true);
            request.setHeader(HttpHeader.Connection,null);
            request.setHeader("Host",null);

			// 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.
			if (request.getHeader("Authorization") == null) {
				Proof proof = (Proof) pathToAuth.get(urlStr);
				System.err.println("Looked up  "+urlStr+" to: "+
					((proof==null) ? "null" : prover.getName(proof)));
				if (proof!=null) {
					String auth = proof.toReadableString(72);
					response.getRequest().setHeader("Authorization",
						SNOWFLAKEPROOF+auth);
				}
			}

           	// request.setVersion(request.HTTP_1_0);
           	request.setVersion(protocol);
            OutputStream os = socket.getOutputStream();
            request.write(os);
			os.flush();	// don't close() -- that closes the entire socket fd!
			timingexp.Timeline.timePoint("proxy: sent request");

			InputStream is[] = new InputStream[1];
				// need a "reference" to an InputStream so snoopResponse
				// can replace it.
			is[0] = socket.getInputStream();

			// snoop the response, and if it's a web page (200), then
			// save the url as the "current page"
			CapturedResponse cp = snoopResponse(is);
			timingexp.Timeline.timePoint("proxy: received response headers");
			if (cp!=null) {
				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.
				if (cp.getCode()==HttpServletResponse.SC_OK
					&& cp.getHeader("content-type")
						.equalsIgnoreCase("text/html")) {
					// was Snowflake required?
					String authHdr = request.getHeader("Authorization");
					ph.sfProof = ProtectedServlet.extractProof(authHdr);
					if (ph.sfProof!=null) {
						ph.snowflakeStatus = PageHistory.SF_SUCCESS;
					}
					history.addHistory(ph);
				}

				// Check for a needs-Authentication reply that we can
				// handle here in this Proxy
				// checkForRetry returns true if it handles the request on
				// its own; else we should pass the original response on.
				if (checkForRetry(request, response, cp, urlStr, protocol)) {
					is[0] = null;
				}
			} // else can't snoop the response -- just pass it on, I guess.

			if (is[0] !=null) {
            	Code.debug("waiting for forward reply...");
            	response.writeInputStream(is[0], -1, true);
			}
			// else retry already wrote the input stream out; this
			// stack frame is dead.
        }
        finally
        {
            socket.close();
        };
    }

	static final int BUFFER_SIZE = 16384;
		// a request with more headers than this will make me puke.

	CapturedResponse snoopResponse(InputStream[] is) {
		try {
			// we need to peek into the stream (to get the headers)
			// while being able to later rewind it and replay it in
			// full to the browser if we're not going to replace it
			// with a new one. So we use a BufferedInputStream with
			// a big buffer.
			// TODO: Replace with an 'infinite' buffer, or one that
			// at least throws an exception on read() so that we don't
			// overrun the buffer.
			BufferedInputStream bis = new BufferedInputStream(is[0],
				BUFFER_SIZE);
			bis.mark(BUFFER_SIZE);

			is[0] = bis;
				// replace caller's InputStream with our buffered one,
				// so caller will see the rewound stream.
	
			HttpInputStream his = new HttpInputStream(bis);
	
			// parse out the HTTP response line
			// Example: HTTP/1.1 200 OK
			String headerLine = his.readLine();
			if (headerLine==null
				|| !headerLine.startsWith("HTTP")) {
				// invalid response line
				bis.reset();
				return null;
			}
			String codeStr, descStr;
			try {
				int c0 = headerLine.indexOf(' ', 0);
				int c1 = headerLine.indexOf(' ', c0+1);
				codeStr = headerLine.substring(c0+1, c1);
				descStr = headerLine.substring(c1+1);
			} catch (IndexOutOfBoundsException ex) {
				// invalid response line
				bis.reset();
				return null;
			}
			int code = Integer.parseInt(codeStr);
	
			// now suck in all the headers
			CapturedResponse header =
				new CapturedResponse(headerLine, code, descStr);
			header.read(his);
	
			bis.reset();
			return header;
		} catch (IOException ex) {
			return null;
		}
	}

	/**
	 * Returns true if we have sent our own response, or false if
	 * the caller should go ahead and pass through the original
	 * InputStream.
	 */
	boolean checkForRetry(HttpRequest request,
		HttpResponse response, CapturedResponse cp,
		String urlStr, String protocol)
		throws IOException {

		// If the request already has Authorization information
		// in it, we can't help much. Else if the response code
		// is 401, we have a job to do!
		if (request.getHeader("Authorization")!=null) {
			if (cp.getCode()==HttpServletResponse.SC_UNAUTHORIZED) {
				// we tried to authorize, but still failed -- tell
				// the browser it's FORBIDDEN to prevent the browser
				// from popping up a login window.
				response.sendError(
					HttpServletResponse.SC_FORBIDDEN,
					cp.getDesc());
				return true;
			}
			// We tried to authorize, and I guess something interesting
			// happened. Let the browser see it!
			return false;
		}

		if (cp.getCode()!=HttpServletResponse.SC_UNAUTHORIZED) {
			// HTTP reply doesn't indicate that anything special needs to
			// be done.
			return false;
		}

		// Okay, we know we have an unathorized (401)
		// reply. Scan the headers to see if it's one
		// we can handle ('SnowflakeProof').
		if (!cp.getHeader("WWW-Authenticate").startsWith(SNOWFLAKEPROOF)) {
			// nope, some other auth method -- pass it through to browser
			return false;
		}

		// revise the request with new Auth info
		if (snowflakeAuthorize(cp, response, urlStr, protocol)) {
			// okay -- revision took effect
			return true;
		} else {
			// rather than returning the original reply,
			// let's change the result code, since we
			// know our dumb browser will do the wrong thing
			// (ask the user for BasicAuth info).
			response.sendError(
				HttpServletResponse.SC_FORBIDDEN,
				"Proxy cannot provide valid authorization.");
			return true;
		}
	}

	/**
	 * returns true if we successfully construct and send off a new
	 * request; else we return false to have the original reply
	 * returned to the browser.
	 * TODO: never return a 401 when it's for a SnowflakeProof --
	 * if we can't handle it, we should return some custom page. Stupid
	 * browsers (stupid enough to need a proxy for help with SfCerts)
	 * will ask the user for BasicAuth, which is confusing.
	 */
	boolean snowflakeAuthorize(CapturedResponse cp,
		HttpResponse response, String urlStr, String protocol)
		throws IOException {

		timingexp.Timeline.timePoint("proxy: snowflakeAuthorize");
		String authDemand = cp.getHeader("WWW-Authenticate");

		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) {
			subject = getRequestSubject(response.getRequest());
		} else if (authDemand.indexOf(AUTHORIZEPROXY)>=0) {
			// Proxy needs proof that PROXYPRINCIPAL (a quoting
			// principal) speaks for me.
			Quoting quoting = (Quoting)
				KeyTools.parseString(cp.getHeader(PROXYPRINCIPAL));
			// TODO: check that quoting is something sensical
			subject = quoting;
			issuer = getIdentity();
		} else if (authDemand.indexOf(IDENTIFYCLIENT)>=0) {
			SDSIPrincipal identity = getIdentity();
			if (identity==null) {
				return false; 	// TODO: handle this with an exception
			}

			// revise the request to mention my preferred identity
			response.getRequest().setHeader(CLIENTIDENTITY,
				identity.toReadableString(72));
			// process the revised request, which may loop back around
			// for more info of some other form again. Yikes.
   			getHTTP(response, urlStr, protocol);
			return true;
		} else {
			System.err.println("I don't understand server's challenge");
			return false;
		}

		if (issuer==null) {
			issuer = (SDSIPrincipal)
				KeyTools.parseString(cp.getHeader(SERVICEISSUER));
		}

		tag = (Tag) KeyTools.parseString(cp.getHeader(MINIMUMTAG));

		try {
			String auxFacts = cp.getHeader(AUXILLIARYFACTS);
			if (auxFacts!=null) {
				Proof p = (Proof) KeyTools.parseString(auxFacts);
				prover.digestProof(p);
			}
		} catch (Exception ex) {
			System.err.println("Couldn't grok auxilliary facts:");
			ex.printStackTrace();
		}

		Proof proof = getAuthorizationFor(subject, issuer, tag);
		String auth = proof.toReadableString(72);
		if (auth!=null) {
			response.getRequest().setHeader("Authorization",
				SNOWFLAKEPROOF+auth);
			
			pathToAuth.put(urlStr, proof);
			System.err.println("Associated "+urlStr+" to: "
				+prover.getName(proof));

			// process the revised request, which won't
			// recurse because the request now contains
			// an Authentication: header.
			timingexp.Timeline.timePoint("proxy: snowflakeAuthorize found proof, recursing");
   			getHTTP(response, urlStr, protocol);
			timingexp.Timeline.timePoint("proxy: snowflakeAuthorize recursion done");
			return true;
		}

		timingexp.Timeline.timePoint("proxy: snowflakeAuthorize failed");
		return false;
	}

	// Return the 'Subject' that represents a given HTTP request,
	// so the caller can construct a proof authorizing the request.
	Subject getRequestSubject(HttpRequest request) {
		try {
			// compute the digest of the outbound request, as
			// that is the subject we're supposed to be authorizing
			MessageDigest md = MessageDigest.getInstance("md5");
			OutputStream last = new NullOutputStream();
			CountingFilterOutputStream cfos =
				new CountingFilterOutputStream(last);
			DigestOutputStream dos = new DigestOutputStream(cfos, md);
			request.write(dos);
			dos.close();
			byte[] requestDigest = md.digest();
			Hash requestHash = new Hash("md5", requestDigest);
			ObjectHash subject = new ObjectHash(requestHash);
			return subject;
		} catch (NoSuchAlgorithmException ex) {
			ex.printStackTrace();
		} catch (IOException ex) {
			ex.printStackTrace();
		}
		return null;
	}

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

	Proof getAuthorizationFor(Subject subject, SDSIPrincipal issuer,
		Tag authTag) {
		System.err.println("6243: getAuthFor("
			+prover.getName(issuer)+" <= "
			+prover.getName(subject)+"  rgdg "+authTag+")");
		// ensure we've seen the latest set of certificates available
		prover.loadCache();

		// 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;
			}
		}

		// If subject is a quoting principal, we may just have to write
		// up the proof right here.
		Proof proof = prover.getProof(issuer, subject, authTag);
		if (proof != null) {
			return proof;
		}
		if (subject instanceof Quoting
			&& prover.isFinal(issuer)) {
			// TODO: issuer doesn't have to be final, we just have to
			// have a final subject that speaks for it.

			// 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;
			}
			// Okay, sign off on the requested delegation
			return prover.createAuth(quoting, issuer);
		}
		return null;
	}

    /* ---------------------------------------------------------------- */
    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());
    }

//	class CachedAuth {
//		Subject subject;
//		SDSIPrincipal issuer;
//		Tag tag;
//
//		CachedAuth(Subject subject, SDSIPrincipal issuer, Tag tag) {
//			this.issuer = issuer;
//			this.tag = tag;
//		}
//
//		public String toString() {
//			return "Issuer "+prover.getName(issuer)+", tag "+tag;
//		}
//	}
}
