package servlet;

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

import com.mortbay.HTTP.*;

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

/**
 * A ProtectedServlet instantiates a new PSHandler object (actually a
 * subclass defined by a subclass of ProtectedServlet) to handle each
 * individual request.
 * Any state we store in the
 * ProtectedServlet object itself is subject to simultaneous access
 * by multiple threads.
 * So for each request, we whip together a PSHandler object to gather
 * per-request state in a convenient place.
 */
public class PSHandler
	implements SfHttpProtocol {

	ProtectedServlet parent;
	HttpServletRequest request;
	HttpServletResponse response;

	PSHandler(ProtectedServlet parent,
		HttpServletRequest request, HttpServletResponse response) {
		this.parent = parent;
		this.request = request;
		this.response = response;
	}

	/**
	 * A method that determines the issuer principal; that is, the
	 * principal that controls the requested resource.
	 */
	public SDSIPrincipal getRequiredIssuer() {
		return parent.prover.getIdentityHash();
	}

	/**
	 * Handle a single request.
	 * This method calls {@link #requestIsAuthorized} to check the authority
	 * of the incoming request. If the request is authorized, it calls
	 * {@link #servePage} to return the correct content. Otherwise, it calls
	 * {@link #demandAuth} to send an appropriate
	 * Snowflake/HTTP Authorization demand to the client.
	 *
	 * <p>The request itself and the response
	 * object were stored in this object's state when the constructor
	 * was called.
	 */
	public void doGet()
		throws ServletException, IOException {

//		timingexp.Timeline.zeroTimer();
//		timingexp.Timeline.timePoint("server start");
		StatusCodeException sce = null;

		try {
			if (checkExperimentReconfigure()) {
				return;
			}
			boolean requestIsAuth = requestIsAuthorized();
			if (requestIsAuth) {
				sendMAC();
				servePage();
			} else {
				demandAuth();
			}
		} catch (StatusCodeException ex) {
			sce = ex;
		} catch (Exception ex) {
			sce = new StatusCodeException(
				response.SC_INTERNAL_SERVER_ERROR,
				ex.toString());
			ex.printStackTrace();
		}
		if (sce!=null) {
			sce.sendError(response);
		}
//		timingexp.Timeline.timePoint("server done");
	}

	Tag getResourceTag() {
		iamabstract(); return null;
	}

	void iamabstract() {
		throw new RuntimeException("jdb is laaaame, and I am abstract");
	}

	/**
	 * Determine whether the request is correctly authenticated.
	 */
    boolean requestIsAuthorized()
		throws ServletException, IOException {
		try {
			String auth = request.getHeader(HTTP_AUTH_RESPONSE);
			if (auth==null
				|| !auth.startsWith(SNOWFLAKEPROOF)) {
				return false;
			}
			Assert.assert(auth!=null);

//			timingexp.Timeline.timePoint("parsing");
			// Parse out the authorization proof
			Proof proof = parent.extractProof(auth);
//			timingexp.Timeline.timePoint("parsed");

			// Use preverified results if available
			// Turned off because it actually makes life slower -- more
			// memory used, and we have to actually look for the substitutions,
			// and I guess verifying just isn't that bad after all.
			// A lot of these optimization/caching decisions are hard
			// when there are such oddly disproportionate slowdowns.
			// SDSI parsing accounts for a way bigger part of the time
			// than it should.
			proof = parent.proofCache.substitute(proof);

//			timingexp.Timeline.timePoint("subst");
			if (proof==null) {
				System.err.println("auth was:");
				System.err.println(auth);
				return false;
			}

			// System.err.println("proof: "+proof.toReadableString(72));

			// Now verify that the request has the hash mentioned
			// in the certificate.
			// Of course, we have to null out the Authorization header,
			// since the sender couldn't have computed on it.

			// TODO: future mechanisms could make the connection integral,
			// essentially doing the hashing for us. Handy. Then the
			// principal would be the connection, rather than the request.

			// TODO: This approach is Jetty-specific, because the Sun
			// Servlets interface doesn't expose a painless way to see the
			// original, unmangled request. I suppose we could standardize
			// some sort of smashing algorithm to canonicalize the headers
			// so they could be reconstructed, but holy cow, why bother?

			HttpRequest httpRequest = (HttpRequest) request;
			httpRequest.setHeader(HTTP_AUTH_RESPONSE, null);

//			// TODO: this code is reproduced in ProxyHandler.java
//			// OutputStream last = new NullOutputStream();
//			// OutputStream last = new FileOutputStream("/tmp/foo2");
//			// CountingFilterOutputStream cfos =
//			//	new CountingFilterOutputStream(last);
//			FileOutputStream objectLog = new FileOutputStream("/tmp/foo2");
//			DigestOutputStream dos = new DigestOutputStream(objectLog, md);
//			httpRequest.write(dos);
//			dos.close();
//			byte[] computedRequestDigest = md.digest();

			byte[] requestBytes = SfUserAgent.getRequestAsBytes(
				(HttpRequest) request);
// DEBUG
//	CopyStream.copyStream(new ByteArrayInputStream(requestBytes),
//		new FileOutputStream("/tmp/foo-server"));
//		Timer t = new Timer();
			MessageDigest md = MessageDigest.getInstance("md5");
			md.update(requestBytes);
			byte[] requestDigest = md.digest();
//		System.out.println("digest at server: "+t);

			Hash computedHash = new Hash("md5", requestDigest);
			ObjectHash computedObjectHash = new ObjectHash(computedHash);
//	System.out.println("obj hash = "+computedObjectHash.toReadableString(72));
//	System.out.println("proof subject = "+((SDSIObject)proof.getSubject()).toReadableString(72));
//			timingexp.Timeline.timePoint("request hashed");

			if (proof instanceof TwoStepProof
				&& ((TwoStepProof) proof).getChildProof(1)
					instanceof MacProof) {
				MacProof mp =
					(MacProof) ((TwoStepProof) proof).getChildProof(1);
//				System.out.println("preparing MacProof");
				ObjectHash macHash = (ObjectHash) mp.getIssuer();
				byte[] mac = (byte[]) parent.hashToMac.get(macHash);
				if (mac!=null) {
					mp.prepareVerify(requestBytes, mac);
//			timingexp.Timeline.timePoint("mac prepared");
				} else {
					System.out.println("mac sent, but no match");
				}
			}

			try {
				proof.verify(getRequiredIssuer(), computedObjectHash,
				getResourceTag());
//			timingexp.Timeline.timePoint("proof verified");
			} catch (InvalidProofException ex) {
				System.err.println("Proof is not valid:\n"+ex);
				// ex.printStackTrace();
				return false;
			}
//			timingexp.Timeline.timePoint("verified");

//			timingexp.Timeline.timePoint("authorized");
			return true;
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return false;
	}

	/**
	 * What proof is required? The client will need to know the 'tag'
	 * (abstract name for the resource as the owning principal knows
	 * it; perhaps independent of the server) and the owning principal
	 * (that the proof must reduce to).
	 */
	void demandAuth()
		throws ServletException, IOException {
		String hashStr = getRequiredIssuer().toReadableString(72);

		// indicate that an SF proof is required, and what it's
		// supposed to prove.
		response.setHeader(HTTP_AUTH_CHALLENGE,
			SNOWFLAKEPROOF+AUTHORIZECLIENT);

		// indicate the issuer that the client's request must speak for
		response.setHeader(SERVICEISSUER,
			getRequiredIssuer().toReadableString(72));

		// indicate the authorization tag -- the abstract name for the
		// resource.
		response.setHeader(MINIMUMTAG,
			getResourceTag().toReadableString(250));

		response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
			"<br>AuthType: "+request.getAuthType()+"\n"
			+"<br>Authorization hdr: "
				+request.getHeader(HTTP_AUTH_RESPONSE)+"\n"
			+"<p>Auth requested: "
				+"SnowflakeProof "+hashStr);
    }

	/**
	 * Override this method to actually serve up your servlet-specific data.
	 */
	public void servePage()
		throws ServletException, IOException {
		iamabstract();
	}

	String resourcePath = null;

	/**
	 * A handy helper method for servlets that use the path in the
	 * URL as part of the request specification. It returns the URL
	 * path, suitable for restriction using the <code>(* prefix )</code>
	 * tag.
	 */
	String getResourcePath() {
		if (resourcePath==null) {
			resourcePath = request.getPathInfo();
			if (resourcePath==null) {
				resourcePath = "";
			} else {
				// strip out any %xy or + encodings.
				try {
					resourcePath = URLDecoder.decode(resourcePath);
				} catch (Exception ex) {
					// thank you Sun for your brilliant choice of
					// exceptions to throw here... :vP
					// Pass the exception up in a form that allows
					// us to return the right Status Code.
					throw new StatusCodeException(response.SC_BAD_REQUEST,
						"URLDecoder couldn't decode path: "+resourcePath);
				}
			}
		}
		return resourcePath;
	}

	void sendMAC() {
		try {
			String mr = request.getHeader(REQUESTMAC);
			if (mr==null) {
				return;
			}
			// System.out.println("mac requested; signing");
			SDSIPublicKey pk;
			try {
				pk = (SDSIPublicKey) KeyTools.parseString(mr);
			} catch (ClassCastException ex) {
				return;
			}

			// Generate a random string to use to authenticate messages
			byte[] plainMac = new byte[32];
			parent.random.nextBytes(plainMac);

			// TODO
			// From Cryptix.provider.rsa.RawRSACipher
			// "Note that using raw RSA directly on application data is
			// potentially insecure; wherever possible a higher level
			// algorithm such as "RSA/PKCS#1" should be used. (Note:
			// RSA/PKCS#1 is not implemented in this version of
			// Cryptix.)"
			// System.out.println("pk.algorithm: "+pk.getAlgorithm());
			// String algorithm = pk.getAlgorithm();
			String algorithm = "RSA";
			Cipher cipher = Cipher.getInstance(algorithm);
			cipher.initEncrypt(pk);

			// I wanted padding, but I don't know how to get it out of
			// Cryptix. So we'll just pad the dumb thing ourselves.
			int blockSize = cipher.getInputBlockSize();
			byte[] paddedMac = new byte[blockSize];
			System.arraycopy(plainMac, 0, paddedMac, 0, plainMac.length);

			byte[] cryptedMac = cipher.crypt(paddedMac);
			SexpString ss = new SexpString(cryptedMac);

			// Create an identifier by which the client and I can refer
			// to this MAC without revealing it.
			ObjectHash macHash = KeyTools.hashObject(paddedMac);

			// Put the encrypted hash into a package to send to the client.
			// TODO: this will be huge with manual padding.
			SexpString sexpString = new SexpString(cryptedMac);
			String cryptedString = sexpString.toReadableString(0,2000,0);

			// TODO: we could store a proof that mac => client, but
			// it would be 'says'ed by me, and would require a little
			// more machinery to be convincing. So we let the client
			// sign off on that. This also lets the client decide
			// how far he's willing to trust this MAC trick.

			// Put an ENCRYPTEDMAC header into the outbound response to
			// carry the encrypted MAC message to the client.
			response.setHeader(ENCRYPTEDMAC, cryptedString);

			// map the hash to the secret mac sequence so we know what it
			// to use when the client mentions the hash.
			parent.hashToMac.put(macHash, paddedMac);
		} catch (KeyException ex) {
			System.err.println("Trying to construct MAC:");
			ex.printStackTrace();
		} catch (NoSuchAlgorithmException ex) {
			System.err.println("Trying to construct MAC:");
			ex.printStackTrace();
		}
	}

	boolean checkExperimentReconfigure() {
		System.out.println("wrong code running");
		return false;
	}
}
