package servlet;

import proof.*;
import sdsi.*;
import sdsi.sexp.*;
import Tools.*;
import ssh.*;
import rmi.*;

import relational.*;
import relational.email.*;

import com.mortbay.HTTP.*;

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

/**
 * MailServlet serves up email messages stored in a {@link relational.Database}
 * according to the schema in {@link relational.email}. Messages and queries
 * are mapped into Snowflake restriction sets (SPKI tags) and protected
 * with delegated authority.<p>
 *
 * This is the Gateway example from the thesis. It quotes the client program
 * to ensure that the server is making access-control decisions, even if
 * the gateway holds delegated authority over databases belonging to
 * multiple users.
 *
 * @author jonh@cs.dartmouth.edu
 */

public class MailServlet extends ProtectedServlet
{ 
	Database database;

	static final boolean clientIdentityRoundTrip = false;
		// an old version of the code required a round-trip to the
		// client to find out who (what principal) the client wants to
		// identify himself as; that's silly. When this flag is false,
		// we just send the auth demand with a "hole" in it
		// (a PseudoPrincipal), where the client is expected to fill in
		// the correct identity if he so chooses.

	SSHContext myContext;
	static Log log = new Log();
	static {
		log.addLevel("default");
	}

	/**
	 * Standard servlet initialization from parameters in a
	 * configuration object. This servlet grabs its identity from a
	 * pregenerated public key in <code>certs-sharedserver/</code>; a
	 * production implementation should generate (and cache) its own
	 * public keys or other form of principal identification.<p>
	 *
	 * Then it creates a Prover to cache delegations and handle
	 * demands of authority from the Database object.
	 */
	public void init(ServletConfig config) {
		try {
			String cheaterPriv = "certs-sharedserver/id.private";
			String cheaterPub = "certs-sharedserver/id.public";
			SDSIRSAPrivateKey priv =
				(SDSIRSAPrivateKey) KeyTools.processFilename(cheaterPriv);
			SDSIRSAPublicKey pub =
				(SDSIRSAPublicKey) KeyTools.processFilename(cheaterPub);
			myContext = new SSHContext(ssh.RSA.RSAKey.fromRSAPrivateKey(priv),
				ssh.RSA.RSAKey.fromRSAPublicKey(pub));
			log.log("cheated by loading identity from file "+cheaterPriv);
		} catch (Exception ex) {
			log.log("creating a keypair for this service."
				+" (cheating failed with "+ex.toString()+")");
			myContext = SSHContext.newKeys();
			log.log("done creating keypair.");
		}
		try {
			prover = new Prover2("certs-ms");

			SDSIPublicKey serverPublicKey = myContext.getPublicKey();
			Hash serverPublicHash = new Hash("md5", serverPublicKey);

			// Make sure the prover system knows about the outgoing
			// channel keys, since we can sign with them, and they're
			// the ones the client's proof will define as the subject.
			SDSIPrivateKey serverPrivateKey = myContext.getPrivateKey();
			SDSIKeyPair skp = new SDSIKeyPair(serverPrivateKey,
				serverPublicKey);
			prover.introducePrincipal(skp);
				// That should make the skp the prover's idea of my identity.
			Assert.assert(prover.getIdentityPublicKey()
				.equals(serverPublicKey));
		} catch (NoSuchAlgorithmException ex) {
			ex.printStackTrace();
		}
	}

	/**
	 * doGet() handles a single request from a client. It binds the
	 * client's thread to an SSHContext associated with the gateway's
	 * identity, and instantiates an inner class to handle the
	 * individual request.
	 */
    public void doGet(HttpServletRequest request,
		HttpServletResponse response)
		throws ServletException, IOException {

		// Timer t = new Timer();
//		timingexp.Timeline.timePoint("gateway: request received");
		try {
			// set up outgoing SSH context for this thread.
			SSHContext.contextByThread.set(myContext);
			(new Handler(request,response)).doGet();
		} finally {
			// don't let other users of this thread borrow my context.
			SSHContext.contextByThread.set(null);
		}
//		timingexp.Timeline.timePoint("gateway: request complete");
		// System.err.println("Total request time at server: "+t);
	}

	/**
	 * Boilerplate. This inner-class stuff is getting clumsy.
	 */
	PSHandler getHandler(HttpServletRequest request,
		HttpServletResponse response) {
		return new Handler(request, response);
	}

	class Handler extends PSHandler {
		Tag serverTag = null;
			// The restriction tag that the server asked for.

		Handler(HttpServletRequest request, HttpServletResponse response) {
			super(this$0, request, response);
		}

		public void doGet()
			throws ServletException, IOException {
			if (clientIdentityRoundTrip) {
				String clientIdentity = request.getHeader(CLIENTIDENTITY);
				if (clientIdentity == null) {
//					timingexp.Timeline.timePoint(
//						"gateway: demanding identity");
					demandIdentity();
//					timingexp.Timeline.timePoint(
//						"gateway: demanding identity done");
					return;
				} else {
					SDSIPrincipal client = (SDSIPrincipal)
						KeyTools.parseString(clientIdentity);
//					timingexp.Timeline.timePoint("gateway: "
//						+"got identity: "+prover.getName(client));
				}
			}
			// now super.doGet() would try to ensure that enclosed proof
			// authorized the incoming request. We don't care about
			// that; we just want to use the proof to authorize
			// our outgoing request to the upstream server.
//			timingexp.Timeline.timePoint("gateway: serve page");
			servePage();
//			timingexp.Timeline.timePoint("gateway: serve page done");
		}

		void demandAuth()
			throws ServletException, IOException {
			// what proof is required? -- 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).
			String hashStr = getRequiredIssuer().toReadableString(72);
	
			// indicate that an SF proof is required, and what it's
			// supposed to prove.
			response.setHeader("WWW-Authenticate",
				SNOWFLAKEPROOF+AUTHORIZEPROXY);

			// indicate the issuer that the client's request must speak for
			response.setHeader(PROXYPRINCIPAL,
				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("Authorization")+"\n"
				+"<p>Auth requested: "
					+"SnowflakeProof "+hashStr);
	    }

		void demandIdentity()
			throws ServletException, IOException {
			System.err.println("demandIdentity(): "+IDENTIFYCLIENT);
			response.setHeader("WWW-Authenticate",
				SNOWFLAKEPROOF+IDENTIFYCLIENT);
			response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
		}

		/**
		 * This method creates the ``G | ?'' quoting principal (where
		 * the ? is a {@link sdsi.PseudoPrincipal} to send to the
		 * client in a PROXYPRINCIPAL challenge. This is how the
		 * gateway tells the client to whom the client needs to
		 * delegate to have its request processed.
		 */
		public SDSIPrincipal getRequiredIssuer() {
			Quoting q = null;
			try {
				SDSIPrincipal client;
				if (clientIdentityRoundTrip) {
					String clientIdentity = request.getHeader(CLIENTIDENTITY);
					if (clientIdentity==null) {
						return null;
					}
					client = (SDSIPrincipal)
						KeyTools.parseString(clientIdentity);
				} else {
					client = new PseudoPrincipal("Your Identity Here");
				}
				q = new Quoting(prover.getIdentityPublicKey(), client);
//				timingexp.Timeline.timePoint("gateway: required issuer: "
//					+prover.getName(q));
//				timingexp.Timeline.timePoint("gateway: spk is a "
//					+prover.getIdentityPublicKey().getClass());
			} catch (SexpParseException ex) {
				ex.printStackTrace();
			}
			return q;
		}

		String getPage() {
			String page = getResourcePath();
			if (page.endsWith("/")) {
				page = page.substring(0,page.length()-1);
			}
			if (page.equals("")) {
				return "index";
			} else {
				return page;
			}
		}

		String getMessageId() {
			String messageId = request.getParameter("messageId");
			if (messageId==null) {
				return "-1";
			} else {
				return messageId;
			}
		}

		Tag getResourceTag() {
			SexpList messageSexp =
				new SexpList().append("messageId").append(getMessageId());
			SexpList mailSexp =
				new SexpList().append("mail").append(messageSexp);
			SexpList tagSexp =
				new SexpList().append("tag").append(mailSexp);
			try {
				Tag gatewayLevelTag = new Tag(tagSexp);
				Tag unionTag = gatewayLevelTag;
				if (serverTag!=null) {
					// also ask client to meet server's requirements
					unionTag = unionTag.union(serverTag);
				}
				return unionTag;
			} catch (SexpParseException ex) {
				ex.printStackTrace();
				return null;
			}
		}

		public void servePage()
			throws ServletException, IOException {
			// incorporate any proof from client into Prover to
			// use with outgoing upstream request
			String auth = request.getHeader("Authorization");
			Proof proof = null;
			InvokeHack.setCurrentProver(prover);
			if (auth!=null
				&& auth.startsWith(SNOWFLAKEPROOF)) {
				proof = extractProof(auth);
				if (proof!=null) {
//					timingexp.Timeline.timePoint("gateway: digesting "
//						+prover.getName(proof));
					prover.digestProof(proof);
//					timingexp.Timeline.timePoint("gateway: done digesting");
				} else {
					System.err.println("Couldn't grok proof: "+auth);
					demandAuth();
					return;
				}
			} else {
				System.err.println("No delegated auth provided.");
//				timingexp.Timeline.timePoint("gateway: no auth provided");
				demandAuth();
				return;
			}
//			timingexp.Timeline.timePoint("gateway: auth grokked");

			// State that the outgoing connection made by this
			// thread ("Ch") speaks for the subject of the auth proof.
			// Subject of the auth proof should be a Quoting, M|C,
			// M is me the multiplexed server, and C is the client.
			SSHContext context = SSHContext.getDefault();
			SDSIPrincipal ch = context.getSDSIRSAPublicKey();

//			System.out.println("Expected quoting as subject, got:\n"
//				+prover.getProofName(proof, true));
//			System.out.println(proof.toReadableString(72));

			Quoting q = (Quoting) proof.getSubject();
			if (!prover.isFinal((SDSIObject) q.getQuoter())) {
				prover.dt.dumpObjects();
				System.err.println("quoter (me?): "+prover.dt.getDebugName(q.getQuoter()));
				Set set = prover.getFinalPrincipals();
				System.err.println("I have "+set.size()+" final principals:");
				Iterator iter = set.iterator();
				while (iter.hasNext()) {
					Subject subject = (Subject) iter.next();
					System.err.println("final guy: "+subject.toString());
				}
				throw new ServletException(
					"Hey, this proof doesn't help me at all.");
			}
			Proof outProof = prover.getProof(q, ch, Tag.getTagStar());
			if (outProof==null) {
//				timingexp.Timeline.timePoint("gateway: "
//					+"construct   quoting proof");

				// Make the statement "M says Ch => M|C".
				Validity v = new Validity();
				v.updateAfter(new Date(System.currentTimeMillis()+30000L));
				Auth authCert = new Auth(q, ch, Tag.getTagStar(),
					true, v);

				SDSIPublicKey myPublicKey =
					prover.getPublicKeyForPrincipal((SDSIObject) q.getQuoter());
				SDSIPrivateKey myPrivateKey =
					prover.getPrivateKeyForPublic(myPublicKey);
				SDSISignature ss;
				try {
					ss = new SDSISignature(authCert, myPrivateKey, myPublicKey);
				} catch (SignatureException ex) {
					throw new ServletException(ex.toString());
				}
				SignedCertificate sc = new SignedCertificate(authCert, ss);
				outProof = new SignedCertificateProof(sc, null);
					// TODO: remove "handoff" part of SignedCertificateProof
//				timingexp.Timeline.timePoint("gateway: "
//					+"constructed quoting proof: "+prover.getName(outProof));

//				timingexp.Timeline.timePoint("gateway: "
//					+"digesting quoting proof");
				prover.digestProof(outProof);
//				timingexp.Timeline.timePoint("gateway: "
//					+"digested  quoting proof");
			}
//	System.out.println("outProof = "+prover.getProofName(outProof,true));

			try {
				if (getPage().equals("index")) {
					serveIndex();
				} else if (getPage().equals("message")) {
					serveMessage();
				} else {
					response.sendError(HttpServletResponse.SC_BAD_REQUEST,
						"No such page "+getPage());
				}
			} catch (SfNeedAuthorizationException ex) {
				System.err.println("Insufficient auth for server.");
				System.err.println("subject: "
					+prover.getName(ex.getSubject()));
				System.err.println("issuer: "
					+prover.getName(ex.getIssuer()));
				System.err.println("tag: "
					+ex.getTag().toReadableString(72));
				serverTag = ex.getTag();
				demandAuth();
				return;
				// throw ex;
			}
		}

		void connectToDatabase() {
			if (database!=null) {
				return;
			}
			try {
				InetAddress thisHost = InetAddress.getLocalHost();
				database = (Database)
					Naming.lookup("//"+thisHost.getHostName()+"/RMIDatabase");
			} catch (Exception ex) {
				ex.printStackTrace();
				throw new StatusCodeException(response.SC_INTERNAL_SERVER_ERROR,
					ex.toString());
			}
		}

		void serveIndex()
			throws ServletException, IOException, SfNeedAuthorizationException {

//			timingexp.Timeline.timePoint("gateway: connecting to database");
			connectToDatabase();
//			timingexp.Timeline.timePoint("gateway: connected  to database");

			Vector bv;
//			timingexp.Timeline.timePoint("gateway: calling select");
			bv = InternalDatabase.select(database, Message.class,
				new WhereAlways());
//			timingexp.Timeline.timePoint("gateway: calling select done");

			Writer w = response.getWriter();
			w.write(bv.size()+" messages\n");
			Iterator iter = bv.iterator();
			while (iter.hasNext()) {
				Message m = (Message) iter.next();
				w.write("<li><a href=\"message/?messageId="
					+m.primaryKey.toString()+"\">"
					+m.primaryKey.toString()+"</a>\n");
			}
		}

		void serveMessage()
			throws ServletException, IOException, SfNeedAuthorizationException {

			connectToDatabase();
	
			Integer msgId = Integer.valueOf(getMessageId());
//			System.out.println("msgId="+msgId);
			Vector bv = InternalDatabase.select(database, Body.class,
				Where.equals(Body.f_msg, msgId));
			String bodyString;
			if (bv.size()>=1) {
				bodyString = ((Body) bv.elementAt(0)).body;
			} else {
				bodyString = "[no body]";
			}

			StringBuffer headers = new StringBuffer();
			Vector hv = InternalDatabase.select(database, Header.class,
				Where.equals(Header.f_msg, msgId));

			// priority mode: sort headers by priority, and
			// TODO: highlight the first couple headers.
			Collections.sort(hv, HeaderPriorityComparator.getComp());

			for (int i=0; i<hv.size(); i++) {
				Header h = (Header) hv.elementAt(i);
				// headers.append("<"+h.order+">");
				headers.append(h.name);
				headers.append(": ");
				headers.append(h.value);
				headers.append('\n');
			}
	
			Writer w = response.getWriter();
			w.write("<b><pre>"+headers.toString()+"</pre></b>\n");
			w.write("<pre>"+bodyString+"</pre>\n");
		}
	}
}
