package timingexp;

import java.net.*;
import java.io.*;
import java.util.*;
import jp.*;
import com.mortbay.HTTP.*;
import com.mortbay.Util.*;
import COM.claymoresystems.cert.*;
import COM.claymoresystems.ptls.*;
import COM.claymoresystems.sslg.*;
import Tools.*;

/**
 * A class of experiments that measure the relative speeds of various types of
 * HTTP requests.
 */
public class HttpExp
	extends Experiment {
	final int bufsize = 1024;
	byte[] buf = new byte[bufsize];

	public void runExperiment(Options opts) {
		try {
			int numberOfTimings = opts.getInt("numberOfTimings");
			int numberOfConnections = opts.getInt("numberOfConnections");
			int requestsPerConnection = opts.getInt("requestsPerConnection");

			// Create a factory for creating connections
			SocketFactory socketFactory;
			int port = -1;
			String protocol = null;
			if ("plain".equals(opts.get("socket"))) {
				socketFactory = new PlainSocketFactory();
				port = 80;
				protocol = "http";
			} else if ("SSL".equals(opts.get("socket"))) {
				socketFactory = new SSLSocketFactory(	
					opts.getBoolean("cacheContext"),
					opts.getBoolean("cacheSessions"));
				port = 443;
				// protocol = "https";
				protocol = "http";
					// java.net.URL doesn't understand https, and I don't
					// think it actually affects anything.
			} else {
				throw new RuntimeException("Invalid value for -socket "
					+opts.get("socket")+"; should be \"plain\" or \"SSL\"");
			}
			if (opts.get("port")!=null) {
				port = opts.getInt("port");
			}

			String uri = opts.get("uri");

			// Create the URL including the original hostname
			URL url = new URL(protocol, "shovel.cs.dartmouth.edu", port, uri);

			// figure out which streaminess of HTTP to use
			boolean use11;
			if ("1.1".equals(opts.get("protocol"))) {
				use11 = true;
			} else if ("1.0".equals(opts.get("protocol"))) {
				use11 = false;
			} else {
				throw new RuntimeException("Invalid value for -protocol "
					+opts.get("protocol"));
			}

			// Create a factory for testing connections
			TestOneConn toc;
			if (opts.getBoolean("useSnowflake")) {
				toc = new TestConnSf(url, opts);
			} else {
				if (use11) {
					toc = new TestConn11(socketFactory, url);
				} else {
					if (requestsPerConnection!=1) {
						throw new RuntimeException(
							"Invalid requestsPerConnection "
							+requestsPerConnection+" for HTTP/1.0; must be 1");
						// yeah, I know you can do keep-alive connections,
						// but I'm not.
					}
					toc = new TestConn10(socketFactory, url);
				}
			}

			for (int timing=0; timing<numberOfTimings; timing++) {
				socketFactory.reset();
				int conn;
				timeline.zeroTimer();
				Timer timer = new Timer();
				for (conn=0; conn<numberOfConnections; conn++) {
//					timeline.timePoint("Create socket");
					toc.testConn(requestsPerConnection);
//					timeline.timePoint("Connection complete");
				}
				double time = timer.wallTime();
				System.out.println(opts.get("experimentID")
					+" "+conn*requestsPerConnection
					+" "+time
					+" "+((int)(
						time*1000.0/((double) conn*requestsPerConnection)
						))
					);
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}

	abstract class TestOneConn {
		URL url;
		InetAddress inetAddress;
		int port;

		public HttpRequest getRequest() {
			return new HttpRequest(null, "GET", url.getFile());
		}

		public void writeRequest(OutputStream os)
			throws IOException {
			getRequest().write(os);
			os.flush();
		}

		public TestOneConn(URL url)
			throws UnknownHostException {
			this.url = url;
			this.inetAddress = InetAddress.getByName(url.getHost());
			this.port = url.getPort();
		}

		public abstract void testConn(int requestsPerConnection)
			throws IOException, java.net.UnknownHostException;
	}

	abstract class TestSocketConn extends TestOneConn {
		SocketFactory socketFactory;

		public TestSocketConn(SocketFactory socketFactory, URL url)
			throws UnknownHostException {
			super(url);
			this.socketFactory = socketFactory;

//			try {
//				ByteArrayOutputStream jimbob = new ByteArrayOutputStream();
//				getRequest().write(jimbob);
//				rawrequest = jimbob.toByteArray();
//				System.out.write(rawrequest);
//			} catch (IOException ex) {}
		}

		public void writeRequest(OutputStream os)
			throws IOException {
			BufferedOutputStream bos = new BufferedOutputStream(os);
			getRequest().write(bos);
			bos.flush();		// should flush os
		}

		IncomingResponse getResponse(InputStream is) {
			try {
				IncomingResponse inResp = new IncomingResponse(is, false);
				if (inResp.getCode()!=200) {
					throw new RuntimeException("GET Error: "
						+inResp.getResponseLine());
				}
				return inResp;
			} catch (ParseException ex) {
				throw new RuntimeException(ex.toString());
			}
		}

		public void testConn(int requestsPerConnection)
			throws IOException, java.net.UnknownHostException {
			Socket socket = socketFactory.createSocket(inetAddress, port);
			// timeline.timePoint("Socket created");
			testConn(socket, requestsPerConnection);
			// timeline.timePoint("test done");
			socket.close();
			// timeline.timePoint("connection closed");
		}

		public abstract void testConn(Socket socket, int requestsPerConnection)
			throws IOException, java.net.UnknownHostException;
	}

	// requestsPerConnection = 1 on 1.0 connections
	class TestConn10 extends TestSocketConn {
		public TestConn10(SocketFactory socketFactory, URL url)
			throws UnknownHostException {
			super(socketFactory, url);
		}
		public void testConn(Socket socket, int requestsPerConnection)
			throws IOException, java.net.UnknownHostException {
			writeRequest(socket.getOutputStream());

			IncomingResponse inResp = getResponse(
				new BufferedInputStream(socket.getInputStream()));
			suckSocketDry(inResp.getContentStream(), Integer.MAX_VALUE);
			inResp.close();
		}
	}

	class TestConn11 extends TestSocketConn {
		public HttpRequest getRequest() {
			HttpRequest request = super.getRequest();
			request.setVersion("HTTP/1.1");
			request.setHeader("Host", url.getHost());
			return request;
		}

		public TestConn11(SocketFactory socketFactory, URL url)
			throws UnknownHostException {
			super(socketFactory, url);
		}
		public void testConn(Socket socket, int requestsPerConnection)
			throws IOException, java.net.UnknownHostException {
			InputStream is = socket.getInputStream();
			OutputStream os = socket.getOutputStream();

			BufferedInputStream bis = new BufferedInputStream(is, 16384);
			for (int loops=0; loops<requestsPerConnection; loops++) {
				doOne11Request(bis, os);
			}
		}

		void doOne11Request(InputStream is, OutputStream os)
			throws IOException {
	
			writeRequest(os);
	
			IncomingResponse inResp = getResponse(is);
			int length = inResp.getIntHeader("content-length");
			if (length==-1) {
				System.err.println("No content-length; sucking");
				length = Integer.MAX_VALUE;
			}
			suckSocketDry(inResp.getContentStream(), length);
		}
	}

	// A class that uses the SfUserAgent to send a request
	class TestConnSf extends TestOneConn {
		HttpRequest request;
		SfUserAgent agent;
		boolean changeSequenceNumber;
		int sequenceNumber=0;
			// sequence number is used to prevent identical
			// object hashes on two requests

		public TestConnSf(URL url, Options opts)
			throws UnknownHostException {
			super(url);
			// What to fill in for server?
			Properties properties = new Properties();
			properties.put("certDir", "certs-jon");
			changeSequenceNumber = !opts.getBoolean("identicalRequests");
//			properties.put("preventOptimistic",
//				(!opts.getBoolean("cacheContext"))?"true":"false");
			properties.put("useMacs",
				opts.getBoolean("useMacs")?"true":"false");
			properties.put("authenticateServer",
				opts.getBoolean("authenticateServer")?"true":"false");
			agent = new SfUserAgent(properties);

			reconfigureServer(opts);
		}

		void reconfigureServer(Options opts) {
			// send signFiles and cacheSigns options
			HttpRequest httpRequest = new HttpRequest(null,
				"GET", "/files/");
			httpRequest.setHeader("X-Experiment-Control", "true");
			httpRequest.setHeader("X-SignFiles", opts.get("signFiles"));
			httpRequest.setHeader("X-CacheSigns", opts.get("cacheSigns"));
			httpRequest.setHeader("X-Debug", opts.get("serverDebug"));
			boolean success = false;
			try {
				IncomingResponse inResp = agent.getHTTP(inetAddress, port, url,
					httpRequest);
				if (inResp.getCode()==200) {
					success=true;
				}
				inResp.close();
			} catch (PageException ex) {
			} catch (IOException ex) {
			} catch (ParseException ex) {
			}
			if (!success) {
				throw new RuntimeException("reconfiguring server failed.");
			}
		}

		public HttpRequest getRequest() {
			HttpRequest request = super.getRequest();
			if (changeSequenceNumber) {
				sequenceNumber++;
			}
			request.setIntHeader("X-Sequence", sequenceNumber);
//			System.out.println(sequenceNumber);
			return request;
		}

		public void testConn(int requestsPerConnection)
			throws IOException, java.net.UnknownHostException {
			try {
//				timeline.zeroTimer();
//				timeline.timePoint("calling getHTTP");
//				System.err.println("Request:");
//				System.err.println(getRequest().toString());
				IncomingResponse inResp = agent.getHTTP(inetAddress, port, url,
					getRequest());
				try {
					if (inResp.getCode()!=200) {
						throw new RuntimeException("GET Error: "
							+inResp.getResponseLine());
					}
					// System.err.println("rc = "+inResp.getCode());
					// Tools.CopyStream.copyStream(inResp.getReplayStream(), System.out);
				} catch (ParseException ex) {
					throw new RuntimeException(ex.toString());
				}
//	try { System.out.println(inResp.getResponseLine());	} catch (ParseException ex) {}
//				timeline.timePoint("sucking socket");
				suckSocketDry(inResp.getContentStream(), Integer.MAX_VALUE);
				inResp.close();
//				timeline.timePoint("socket is dry and closed");
			} catch (PageException ex) {
				ex.printStackTrace();
				throw new IOException(ex.toString());
			}
		}
	}

	
	static abstract class SocketFactory {
		public abstract Socket createSocket(InetAddress inetAddr, int port)
			throws IOException;
		public void reset() { }
	}

	static class PlainSocketFactory extends SocketFactory {
		public Socket createSocket(InetAddress inetAddr, int port)
			throws IOException {
			Socket socket = new Socket(inetAddr, port);
			socket.setTcpNoDelay(true);
			return socket;
		}
	}

	static class SSLSocketFactory extends SocketFactory {
		SSLContext cachedContext = null;
		boolean cacheContext;
		boolean cacheSessions;
		SSLSocketFactory(boolean cacheContext, boolean cacheSessions) {
			this.cacheContext = cacheContext;
			this.cacheSessions = cacheSessions;
		}
		public void reset() {
			cachedContext = null;
		}
		public Socket createSocket(InetAddress inetAddr, int port)
			throws IOException {
			if ((!cacheContext)
				|| cachedContext==null) {
				// timeline.timePoint("Create context");
				SSLContext ctx = new SSLContext();
				SSLPolicyInt policy = new SSLPolicyInt();
	
				// TODO: will this eliminate some pk verifies that it shouldn't?
				policy.acceptUnverifiableCertificates(false);
	
				ctx.setPolicy(policy);
	
				// ctx.loadRootCertificates("/u/jonh/puretls-0.9b1/root.pem");
				ctx.loadRootCertificates("/usr/local/apache/conf/ssl.crt/snakeoil-ca-rsa.crt");
//				ctx.loadEAYKeyFile("/u/jonh/puretls-0.9b1/client.pem",
//					"password");
				ctx.loadEAYKeyFile("timingexp/client-rsa.key", "");
				ctx.seedRNG(null);	// "fakeseed"

				cachedContext = ctx;
				// timeline.timePoint("Context created");
			} else if (!cacheSessions) {
				cachedContext.clearSessionCache();
			}

			return new SSLSocket(cachedContext, inetAddr, port);
		}
	}
	
	void suckSocketDry(InputStream is, int limit)
		throws IOException {
		int total = 0;
		int rc;
		while (true) {
			rc = is.read(buf, 0, Math.min(buf.length, limit));
			if (rc<=0) {
				break;
			}
			// System.out.write(buf, 0, rc);
			total += rc;
			if (total>=limit) {
				break;
			}
		}
		// System.out.println("total vaccuum = "+total+" / "+limit);
	}

	public static Timeline timeline;
		// static so it can be prodded from SSL module

	HttpExp() {
		timeline = new Timeline();
	}

	static class HttpExpOptions extends Options {
		public HttpExpOptions() {}
		public HttpExpOptions(String[] args) {
			super(args);
		}
		public void defineOptions() {
		defineOption("numberOfTimings", "", "10");
		defineOption("numberOfConnections", "", "1000");
		defineOption("requestsPerConnection",
			"How many times to reuse an HTTP/1.1 connection", "1");
		defineOption("protocol", "Which connection type", "1.0");

		defineOption("useSnowflake", "Snowflake vs Http/SSL", "false");
		defineOption("useMacs", "use MAC shortcut for Snowflake", "true");
		defineOption("identicalRequests", "every request has same hash", "false");
		defineOption("authenticateServer", "authenticate server's documents, including server name", "true");
		defineOption("signFiles", "server will sign files", "true");
		defineOption("cacheSigns", "server will cache file signatures", "true");
		defineOption("serverDebug", "server will excrete debugging info", "false");

		defineOption("socket", "Socket factory (plain or SSL)", "plain");
		defineOption("port", "port number to connect to", null);
		defineOption("cacheContext",
			"Cache the SSL context within a timing", "true");
			// If false, every new connection will require a new
			// context (which means loading keying material and
			// possibly reverifying certs), as well as a new
			// session key negotiation.
		defineOption("cacheSessions",
			"Cache the SSL sessions within a timing", "true");
			// If false, every new connection will require a new
			// session key negotiation
		defineOption("experimentID",
			"a value printed on each timing output line", "unknown");
		defineOption("uri",
			"name of the resource to fetch from the server",
			"/timing-file-100.txt");
		}
	}

	public static void main(String[] args) {
		Options opts = new HttpExpOptions(args);
		HttpExp exp = new HttpExp();
		opts.dumpOptions(System.out);
		System.out.println("Key: experiment-id requests total-time ms-per-req");
		exp.runExperiment(opts);
	}

	/**
	 * Create a default Options object, which the GenerateTestCases
	 * harness will populate with the actual options.
	 */
	public Options optionsFactory() {
		return new HttpExpOptions();
	}
}
