package jp;

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

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

/**
 * This class implements the user interface to the SfUserAgent. It is
 * a servlet that managing keys, delegations, and name bindings via the
 * web browser. 
 *
 * @author jonh@cs.dartmouth.edu
 */
public class PrincipalManager
	extends HttpServlet {

	Prover2 prover;
	History history;

	/**
	 * A principal manager is instantiated by the SfUserAgent, and
	 * given references to the SfUserAgent's prover and page view
	 * history.
	 */
	public PrincipalManager(Prover2 prover, History history) {
		this.prover = prover;
		this.history = history;
	}

	/**
	 * The standard servlet initialization method.
	 */
	public static void initialize() {
	}

	/**
	 * A request directed at the PrincipalManager is delivered here by
	 * the servlet mechanism.
	 */
	public void doGet(HttpServletRequest request,
		HttpServletResponse response)
		throws ServletException, IOException {
		(new PMHandler(request, response)).doGet();
	}

	interface ObjectFunc {
		public String func(Object arg);
	}
	
	/**
	 * A PMHandler object is created per-request, to conveniently
	 * encapsulate the state associated with an individual request.
	 */
	class PMHandler {
		HttpServletRequest request;
		HttpServletResponse response;
	
		PMHandler(HttpServletRequest request,
			HttpServletResponse response) {
			this.request = request;
			this.response = response;
		}
	
		public void doGet()
			throws ServletException, IOException {

			try {
				prover.loadCache();
		
				String page = request.getParameter("page");
				if (page==null) {
					page = "topPage";
				}
				try {
					if (page.equals("topPage")) {
						topPage();
					} else if (page.equals("createDef")) {
						createDef();
					} else if (page.equals("importObject")) {
						importObject();
					} else if (page.equals("submitImport")) {
						submitImport();
					} else if (page.equals("createAuth")) {
						createAuth();
					} else if (page.equals("submitDef")) {
						submitDef();
					} else if (page.equals("submitAuth")) {
						submitAuth();
					} else if (page.equals("delegatePage")) {
						delegatePage();
					} else if (page.equals("submitDelegatePage")) {
						submitDelegatePage();
					} else if (page.equals("details")) {
						details();
					} else if (page.equals("authorizedLink")) {
						authorizedLink();
					} else if (page.equals("newKey")) {
						newKey();
					} else {
						errorPage("Unknown page: "+page);
					}
				} catch (Exception ex) {
					throw new PageException(ex);
				}
			} catch (PageException ex) {
				ex.sendResponseTo(response);
			}
		}

		/**
		 * create a new StateRef object based on the current request,
		 * on which the caller can tweak the parameters for building
		 * links or tables to pass to the next page.
		 */
		StateRef newRef() {
			return new StateRef(request);
		}
	
		public void errorPage(Exception ex)
			throws PageException {
			throw new PageException(ex);
		}

		public void errorPage(String error)
			throws PageException {
			throw new PageException(error);
		}

		SDSIObject getSDSIParameter(String param) {
			String sexpStr = request.getParameter(param);
			SDSIObject sdsiObject = KeyTools.parseString(sexpStr);
			return sdsiObject;
		}

		public void newKey()
			throws ServletException, IOException, NoSuchAlgorithmException {

			SDSIKeyPair pair = Tool.generateKeyPair();
			prover.introduceObject(pair, true);
				// TODO: handle persistence for the right objects

			// internally "redirect" to main page (showing new key)
			// TODO: tweak this redirect to repair the URL on the client.
			// It would be great to do that in the headers, rather than
			// with a redirect (like I do in the FAQ-O-Matic), which slows
			// things down.
			this$0.doGet(newRef().deleteAll()
				.change("page", "topPage").asRequest(),
				response);
		}

		public void details()
			throws ServletException, IOException {

			SDSIObject subjectHash = getSDSIParameter("subject");
			SDSIPublicKey subjectPub =
				prover.getPublicKeyForHash((Hash) subjectHash);
			
			Writer w = response.getWriter();
			w.write("<h3>"+prover.getName(subjectPub)+"</h3>\n");
			w.write("<p>Public Key S-Expression:\n<pre>"
				+subjectPub.toReadableString(80)
				+"</pre>\n");

			try {
				w.write("<p>MD5 Hash:\n<pre>"
					+(new Hash("md5", subjectPub).toReadableString(72))
					+"</pre>\n");
			} catch (NoSuchAlgorithmException ex) {}

			java.util.List list = prover.getNames(subjectPub, -1);
			if (list.size()>0) {
				w.write("<p>Secure names for this principal:<ul>");
				Iterator iter = list.iterator();
				while (iter.hasNext()) {
					NameNode nn = (NameNode) iter.next();
					w.write("<li>"+prover.getName(nn.getPrincipal())
						+":"+nn+"\n");
				}
				w.write("</ul>\n");
			}

			addReturn(w);
		}

		public void submitDef()
			throws ServletException, IOException {

			SDSIObject subjectHash = getSDSIParameter("subject");
			SDSIObject issuerHash = getSDSIParameter("issuer");
			SDSIPublicKey issuerPub =
				prover.getPublicKeyForHash((Hash) issuerHash);
			SDSIPrivateKey issuerPriv =
				prover.getPrivateKeyForPublic(issuerPub);
			String name = request.getParameter("name");
			String validityDaysStr = request.getParameter("validityDays");
			int validityDays = Integer.parseInt(validityDaysStr);

			SignedCertificate sc;
			try {
				sc = Tool.generateDefCertificate(
					issuerPub, issuerPriv, name, (Subject) subjectHash,
					validityDays);
			} catch (Exception ex) {
				throw new RuntimeException(ex.toString());
			}

			prover.introduceObject(sc, true);

			this$0.doGet(newRef().deleteAll()
				.change("page", "topPage").asRequest(),
				response);
		}

		public void submitAuth()
			throws ServletException, IOException {

			SDSIObject subjectHash = getSDSIParameter("subject");
			SDSIObject issuerHash = getSDSIParameter("issuer");
			SDSIPublicKey issuerPub =
				prover.getPublicKeyForHash((Hash) issuerHash);
			SDSIPrivateKey issuerPriv =
				prover.getPrivateKeyForPublic(issuerPub);

			String tagStr = request.getParameter("tag");

			String propagateStr = request.getParameter("propagate");
			boolean propagate = propagateStr.equalsIgnoreCase("true");

			String validityDaysStr = request.getParameter("validityDays");
			int validityDays = Integer.parseInt(validityDaysStr);

			SignedCertificate sc;
			try {
				sc = Tool.generateAuthCertificate(
					issuerPub, issuerPriv, (Subject) subjectHash,
					tagStr, propagate, validityDays);
			} catch (Exception ex) {
				throw new RuntimeException(ex.toString());
			}

			prover.introduceObject(sc);

			// prover no longer mangles around in the filesystem. In the
			// meantime, hack out the output so we can get to it.
			Writer os =
				new OutputStreamWriter(new FileOutputStream("/tmp/foo"));
			os.write(sc.toReadableString(72));
			os.close();

			this$0.doGet(newRef().deleteAll()
				.change("page", "topPage").asRequest(),
				response);
		}
			
		public void createDef()
			throws ServletException, IOException {

			TableForm form = newRef().change("page", "submitDef").asForm();

			SDSIObject subjectHash = getSDSIParameter("subject");
			String subjectName = prover.getName(subjectHash);
			form.addText("Subject", subjectName
				+" ("+subjectHash.toReadableString(72)+")");

			// Which issuer?
			Set l = prover.getPrincipalsByType(SDSIKeyPair.class);
			Select select = form.addSelect("issuer", "Issuer", false,
				Math.max(Math.min(l.size(), 4), 1));
			Iterator iter = l.iterator();
			boolean first = true;
			while (iter.hasNext()) {
				SDSIKeyPair skp = (SDSIKeyPair) iter.next();
				Hash pkHash;
				try {
					SDSIPublicKey publicKey = skp.getPublicKey();
					pkHash = new Hash("md5", publicKey);
				} catch (NoSuchAlgorithmException ex) {
					throw new RuntimeException(ex.toString());
				}
				select.add(prover.getName(skp), first,
					pkHash.toReadableString(72));
				first = false;
			}

			form.addTextField("name", "Name", 30, "");

			form.addTextField("validityDays", "Validity Interval (days)",
				5, "10");

			form.addButton("submit", "Make Name Certificate");
			form.method("GET");

			Writer w = response.getWriter();
			form.write(w);
			addReturn(w);
		}

		public void importObject()
			throws ServletException, IOException {

			TableForm form = newRef().change("page", "submitImport").asForm();
			form.addTextArea("object", "Paste SDSI object:", 60, 15, "");
			form.addButton("submit", "Import Object");
			form.method("GET");

			Writer w = response.getWriter();
			form.write(w);
			addReturn(w);
		}

		public void submitImport()
			throws ServletException, IOException {

			String objectStr = request.getParameter("object");
			SDSIObject theObject = KeyTools.parseString(objectStr);
			if (theObject!=null) {
				String type = "unknown";
				if (theObject.getSrep() instanceof SexpList) {
					type = ((SexpList) theObject.getSrep()).getType();
				}
				prover.introduceObject(theObject);
			}

			this$0.doGet(newRef().deleteAll()
				.change("page", "topPage").asRequest(),
				response);
		}

		public void createAuth()
			throws ServletException, IOException {

			TableForm form = newRef().change("page", "submitAuth").asForm();

			// TODO: let user specify how indirect subject is
			// -- by key? (absolute)
			// -- by one of n names? (indirect)
			SDSIObject subjectHash = getSDSIParameter("subject");
			String subjectName = prover.getName(subjectHash);
			form.addText("Subject", subjectName
				+" ("+subjectHash.toReadableString(72)+")");

			// Which issuer?
			Set l = prover.getPrincipalsByType(SDSIKeyPair.class);
			Select select = form.addSelect("issuer", "Issuer", false,
				Math.max(Math.min(l.size(), 4), 1));
			Iterator iter = l.iterator();
			boolean first = true;
			while (iter.hasNext()) {
				SDSIKeyPair skp = (SDSIKeyPair) iter.next();
				Hash pkHash;
				try {
					SDSIPublicKey publicKey = skp.getPublicKey();
					pkHash = new Hash("md5", publicKey);
				} catch (NoSuchAlgorithmException ex) {
					throw new RuntimeException(ex.toString());
				}
				select.add(prover.getName(skp), first,
					pkHash.toReadableString(72));
				first = false;
			}

			form.addTextField("tag", "Tag", 30, "");

			form.addCheckbox("propagate", "Propagate Delegation", true);

			form.addTextField("validityDays", "Validity Interval (days)",
				5, "10");

			form.addButton("submit", "Make Auth Certificate");
			form.method("GET");

			Writer w = response.getWriter();
			form.write(w);
			addReturn(w);
		}

		public void delegatePage()
			throws ServletException, IOException, PageException {

			// LEFT OFF HERE!
			// have user select subject.
			// 
			// Extract the relevant part of the proof,
			// put together a certificate, package it all up,
			// make an <a href=...> link.

			String targetUrl = request.getParameter("targetUrl");
			PageHistory ph = history.findPage(targetUrl);
			if (ph==null) {
				errorPage("I don't seem to have "
					+"<a href=\""+targetUrl+"\">"+targetUrl+"</a> "
					+"in my history anymore. Click it and it'll go to "
					+"the top of the list, I guess.");
			}

			TableForm form = newRef().change("page", "submitDelegatePage")
				.asForm();

			form.addText("Delegating page", targetUrl);

			Set l = prover.getPrincipalsByType(Subject.class);
			Select select = form.addSelect("subject", "Subject", false,
				Math.max(Math.min(l.size(), 4), 1));
			Iterator iter = l.iterator();
			boolean first = true;
			while (iter.hasNext()) {
				Subject subject = (Subject) iter.next();
				Hash subjectHash;
				if (subject instanceof Hash) {
					subjectHash = (Hash) subject;
				} else {
					try {
						subjectHash = new Hash("md5", (SDSIObject) subject);
					} catch (NoSuchAlgorithmException ex) {
						continue;
					} catch (ClassCastException ex) {
						continue;
					}
				}
				select.add(prover.getName(subject), first,
					subjectHash.toReadableString(600));
				first = false;
			}

			// TODO: install a limiting tag appropriate to the URL.

			form.addCheckbox("propagate", "Propagate Delegation", true);

			form.addTextField("validityDays", "Validity Interval (days)",
				5, "10");

			form.addButton("submit", "Delegate Page");
			form.method("GET");

			Writer w = response.getWriter();
			form.write(w);
			addReturn(w);
		}

		public void submitDelegatePage()
			throws ServletException, IOException, PageException {

			String targetUrl = request.getParameter("targetUrl");
			PageHistory ph = history.findPage(targetUrl);
			if (ph==null) {
				errorPage("I don't seem to have "
					+"<a href=\""+targetUrl+"\">"+targetUrl+"</a> "
					+"in my history anymore. Click it and it'll go to "
					+"the top of the list, I guess.");
			}

			SDSIObject subjectHash = getSDSIParameter("subject");

			String validityDaysStr = request.getParameter("validityDays");
			int validityDays = Integer.parseInt(validityDaysStr);

			String propagateStr = request.getParameter("propagate");
			boolean propagate = propagateStr.equalsIgnoreCase("true");

			// TODO: make a reasonable tag
			String tagStr = "(tag *)";

		/* this is debug code to figure out what the
		 * standard proof looks like

			Writer w = response.getWriter();
			Iterator iter = ph.sfProof.preorderIssuers().iterator();
			while (iter.hasNext()) {
				SDSIPrincipal principal = (SDSIPrincipal) iter.next();
				w.write("<p>Issuer: "+principal+"\n");
				w.write("<br>aka: "+prover.getName(principal)+"\n");
			}
			addReturn(w);
		 */
			// figure out who we are that got us into that page, and
			// have that key delegate its authority to the subject.
			//
			// The subject of the proof is going to be the hash of the
			// HTTP request that we sent to the server.
			// The last 'Prooflet' in the chain delegates to the HTTP
			// request (the subject) from a key we control (the issuer).
			// So build an auth certificate based on that last issuer.
			java.util.List proofIssuers = ph.sfProof.preorderIssuers();
			SDSIPublicKey issuerPub = (SDSIPublicKey) proofIssuers
				.get(proofIssuers.size()-1);
			SDSIPrivateKey issuerPriv =
				prover.getPrivateKeyForPublic(issuerPub);

			// put together the auth certificate
			SignedCertificate sc=null;
			try {
				sc = Tool.generateAuthCertificate(
					issuerPub, issuerPriv, (Subject) subjectHash,
					tagStr, propagate, validityDays);
			} catch (Exception ex) {
				throw new RuntimeException(ex.toString());
			}
			prover.introduceObject(sc);

			// now package the whole thing as a link the recipient can
			// click.

			Writer w = response.getWriter();
			// assume their security.localhost is named the same as ours
			// (silly assumption.)
			w.write("E-mail this page to the recipient.");
			w.write("<p>"
				+newRef().deleteAll().change("page", "authorizedLink")
				.change("proof", ph.sfProof.toReadableString(72))
				.change("cert", sc.toReadableString(72))
				.change("target", targetUrl)
				.asLink("Authorization-carrying link to "+ph.title)
				);

			addReturn(w);

		/*
			this$0.doGet(newRef().deleteAll()
				.change("page", "topPage").asRequest(),
				response);
		*/
		}

		public void authorizedLink()
			throws ServletException, IOException {

			Proof proof = (Proof) getSDSIParameter("proof");
			SDSIObject certificate = getSDSIParameter("cert");
			String target = request.getParameter("target");

			prover.digestProof(proof);
			prover.introduceObject(certificate);
			// redirect the user to the target URL
			response.sendRedirect(target);
			System.out.println("redirected browser to: "+target);
		}

		void addReturn(Writer w)
			throws IOException {
			w.write("\n<hr>"
				+newRef().deleteAll().asLink("Return to main page")
				+"\n");
		}
	
		public void topPage()
			throws ServletException, IOException {
	
			response.setContentType("text/html");
			Writer w = response.getWriter();
			w.write("<title>Snowflake Security Management Page</title>\n");
			w.write("<h3>Snowflake Security Management Page</h3>\n");
	
			w.write("Principals: (principals with the private "
				+"key stored here are in <b>bold</b>)\n");
			w.write("<ul>\n");
			Set pset = prover.getPrincipalsByType(SDSIPrincipal.class);
			Set hashes = prover.getPrincipalsByType(Hash.class);
			pset.removeAll(hashes);
			addList(pset, w, new ObjectFunc() {
				public String func(Object arg) {
					String result = "";
					Hash hash;
					if (arg instanceof Hash) {
						hash = (Hash) arg;
					} else {
						try {
							hash = new Hash("md5", (SDSIPrincipal) arg);
						} catch (NoSuchAlgorithmException ex) {
							throw new RuntimeException(ex.toString());
						}
					}
					String hashStr = hash.toReadableString(72);
					result += " "+newRef().change("page", "createDef")
						.change("subject", hashStr)
						.asLink("define name");
					result += " "+newRef().change("page", "createAuth")
						.change("subject", hashStr)
						.asLink("delegate");
					result += " "+newRef().change("page", "details")
						.change("subject", hashStr)
						.asLink("details");
					return result;
				}
			});
			w.write("<P>"+newRef().change("page", "newKey")
				.asLink("New Private Key"));
			w.write("</ul>\n");

			w.write("History of pages requiring Snowflake authorization:");
			w.write("<ul>\n");
			Iterator iter = history.iterator();
			int count=0;
			while (iter.hasNext()) {
				PageHistory ph = (PageHistory) iter.next();
				if (ph==null) {
					break;
				}
				if (count==0) {
					w.write("<li>Current page: ");
				} else {
					w.write("<li>History["+count+"]: ");
				}
				w.write("<a href=\""+ph.url+"\">"+ph.url+"</a>\n");
				if (ph.snowflakeStatus == PageHistory.SF_SUCCESS) {
					w.write(newRef().change("page", "delegatePage")
						.change("targetUrl", ph.url)
						.asLink("Delegate authorization for this page"));
				}
				count++;
			}
			w.write("</ul>\n");
	
			w.write("Proofs:<ul>\n");
			Set proofSet = prover.getProofs();
			addList(proofSet, w, null);
			w.write("</ul>\n");

			w.write("Other principally things:<ul>\n");
			Set cset = prover.getPrincipals();
			cset.removeAll(pset);
			addList(cset, w, null);
			w.write("</ul>\n");
		}
	
		void addList(Set s, Writer w, ObjectFunc f)
			throws IOException {
			Iterator iter = s.iterator();
			while (iter.hasNext()) {
				Object o = iter.next();
				String name;
				if (o instanceof Proof) {
					name = prover.getProofName((Proof) o, true);
				} else {
					name = prover.getName(o);
				}
				String desc =
					(prover.getSecureName(o)==null
						? " (mnemonic)"
						: " (secure)")
					+"<br>(class "+o.getClass()+")\n";
				if (f!=null) {
					desc += "<br>"+f.func(o)+"\n";
				}
				w.write("<li> ");
				boolean bold = false;
				try {
					bold = prover.isFinal((SDSIObject) o);
				} catch (ClassCastException ex) {}
				if (bold) { w.write("<b>"); }
				w.write(name);
				if (bold) { w.write("</b>"); }
				w.write(desc+"\n");
			}
		}

		/**
		 * Inner class of PMHandler.
		 * an object that records the desired differences between the
		 * current (request) page and the desired destination page, then
		 * generates the appropriate URL.
		 */
	} /* end class PMHandler */
}

/**
 * The errorPage() methods throw this exception to unravel the call
 * stack upon error, so that callers to errorPage() don't have to worry
 * about making all the right returns on their own. (That's sort of the
 * point of an exception, isn't it?) It gets caught in doGet, which
 * then absorbs it (instead of calling errorPage() again to display the
 * exception).
 */
class ErrorPageException
	extends RuntimeException {
}
