import java.io.BufferedReader; 
import java.io.File; 
import java.io.FileInputStream; 
import java.io.IOException; 
import java.io.InputStream; 
import java.io.InputStreamReader; 
import java.io.OutputStream; 
import java.io.OutputStreamWriter; 
import java.io.PrintWriter; 
import java.io.Writer; 
import java.net.InetAddress; 
import java.net.InetSocketAddress; 
import java.net.MalformedURLException; 
import java.net.ProtocolException; 
import java.net.Proxy; 
import java.net.ServerSocket; 
import java.net.Socket; 
import java.net.URI; 
import java.net.URISyntaxException; 
import java.nio.charset.StandardCharsets; 
import java.security.KeyManagementException; 
import java.security.KeyStore; 
import java.security.KeyStoreException; 
import java.security.NoSuchAlgorithmException; 
import java.security.NoSuchProviderException; 
import java.security.UnrecoverableKeyException; 
import java.security.cert.CertificateException; 
import java.util.HashMap; 
import java.util.StringTokenizer; 

import javax.net.ssl.HostnameVerifier; 
import javax.net.ssl.HttpsURLConnection; 
import javax.net.ssl.KeyManagerFactory; 
import javax.net.ssl.SSLContext; 
import javax.net.ssl.SSLSession; 
import javax.net.ssl.TrustManagerFactory; 

import com.sun.net.httpserver.HttpContext; 
import com.sun.net.httpserver.HttpExchange; 
import com.sun.net.httpserver.HttpHandler; 
import com.sun.net.httpserver.HttpServer; 
import com.sun.net.httpserver.HttpsConfigurator; 
import com.sun.net.httpserver.HttpsParameters; 
import com.sun.net.httpserver.HttpsServer; 

import java.security.cert.X509Certificate; 

import sun.security.tools.keytool.CertAndKeyGen; 
import sun.security.x509.X500Name; 

public class JDK8209178 {
	static { 

		// System.setProperty("javax.net.debug","all"); // No ... both client and server are logging ... 

		try { 
			HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { 
				public boolean verify(String hostname, SSLSession session) { 
					return true; 
				} 
			}); 
			SSLContext.setDefault(new TestSSLContext().get()); 
		} catch (Exception ex) { 
			throw new ExceptionInInitializerError(ex); 
		} 
	} 

	static final String RESPONSE = "<html><body><p>Hello World!</body></html>"; 
	static final String PATH = "/foo/"; 

	static HttpServer createHttpsServer() throws IOException, NoSuchAlgorithmException { 
		HttpsServer server = com.sun.net.httpserver.HttpsServer.create(); 
		HttpContext context = server.createContext(PATH); 
		context.setHandler(new HttpHandler() { 

			boolean simulateError = true; 

			@Override 
			public void handle(HttpExchange he) throws IOException { 

				System.out.printf("%s - received request on : %s%n",Thread.currentThread().getName(),he.getRequestURI()); 
				System.out.printf("%s - received request headers : %s%n",Thread.currentThread().getName(),new HashMap(he.getRequestHeaders())); 

				InputStream requestBody = he.getRequestBody(); 
				String body = JDK8209178.toString(requestBody); 
				System.out.printf("%s - received request body : %s%n",Thread.currentThread().getName(),body); 

				if(simulateError) { 
					simulateError = false; 

					System.out.printf("%s - closing connection unexpectedly ... %n",Thread.currentThread().getName(),he.getRequestHeaders()); 

					he.close(); // try not to respond anything the first time ... 
					return; 
				} 

				he.getResponseHeaders().add("encoding", "UTF-8"); 
				he.sendResponseHeaders(200, RESPONSE.length()); 
				he.getResponseBody().write(RESPONSE.getBytes(StandardCharsets.UTF_8)); 
				he.close(); 
			} 
		}); 

		server.setHttpsConfigurator(new Configurator(SSLContext.getDefault())); 
		server.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0); 
		return server; 
	} 

	public static void main(String[] args) 
			throws IOException, 
			URISyntaxException, 
			NoSuchAlgorithmException, 
			InterruptedException 
	{ 
		HttpServer server = createHttpsServer(); 
		server.start(); 
		try { 
			new JDK8209178().test(server); 

		} finally { 
			server.stop(0); 
			System.out.println("Server stopped"); 
		} 
	} 

	public void test(HttpServer server /*, HttpClient.Version version*/) 
			throws IOException, 
			URISyntaxException, 
			NoSuchAlgorithmException, 
			InterruptedException 
	{ 
		System.out.println("Server is: " + server.getAddress().toString()); 
		System.out.println("Verifying communication with server"); 
		URI uri = new URI("https:/" + server.getAddress().toString() + PATH + "x"); 

		TunnelingProxy proxy = new TunnelingProxy(server); 
		proxy.start(); 
		try { 
			System.out.println("Proxy started"); 
			Proxy p = new Proxy(Proxy.Type.HTTP, 
					InetSocketAddress.createUnresolved("localhost", proxy.getAddress().getPort())); 
			System.out.println("Verifying communication with proxy"); 


			callHttpsServerThroughProxy(uri, p); 

		} finally { 
			System.out.println("Stopping proxy"); 
			proxy.stop(); 
			System.out.println("Proxy stopped"); 
		} 
	} 

	private void callHttpsServerThroughProxy(URI uri, Proxy p) 
			throws IOException, MalformedURLException, ProtocolException { 
		HttpsURLConnection urlConnection = (HttpsURLConnection) uri.toURL().openConnection(p); 

		urlConnection.setConnectTimeout(1000); 
		urlConnection.setReadTimeout(3000); 
		urlConnection.setDoInput(true); 
		urlConnection.setDoOutput(true); 
		urlConnection.setRequestMethod("POST"); 
		urlConnection.setUseCaches(false); 

		urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); 
		urlConnection.setRequestProperty("charset", "utf-8"); 
		urlConnection.setRequestProperty("Connection", "keep-alive"); 

		String urlParameters = "param1=a&param2=b&param3=c"; 
		byte[] postData = urlParameters.getBytes(StandardCharsets.UTF_8); 

		OutputStream outputStream = urlConnection.getOutputStream(); 
		outputStream.write(postData); 
		outputStream.close(); 

		int responseCode = urlConnection.getResponseCode(); 
		System.out.printf(" ResponseCode : %s%n", responseCode); 

		String output; 
		InputStream inputStream= (responseCode < 400) ? urlConnection.getInputStream() : urlConnection.getErrorStream(); 

		output = toString(inputStream); 
		inputStream.close(); 

		System.out.printf(" Output from server : %s%n", output); 

		if (responseCode == 200) { 
			// OK ! 

		} else { 
			throw new RuntimeException("Bad response Code : " + responseCode); 
		} 
	} 

	static class TunnelingProxy { 
		final Thread accept; 
		final ServerSocket ss; 
		final boolean DEBUG = false; 
		final HttpServer serverImpl; 
		TunnelingProxy(HttpServer serverImpl) throws IOException { 
			this.serverImpl = serverImpl; 
			ss = new ServerSocket(); 
			accept = new Thread(this::accept); 
		} 

		void start() throws IOException { 
			ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); 
			accept.start(); 
		} 

		// Pipe the input stream to the output stream. 
		private synchronized Thread pipe(InputStream is, OutputStream os, char tag) { 
			return new Thread("TunnelPipe("+tag+")") { 
				@Override 
				public void run() { 
					try { 
						try { 
							int c; 
							while ((c = is.read()) != -1) { 
								os.write(c); 
								os.flush(); 
								// if DEBUG prints a + or a - for each transferred 
								// character. 
								if (DEBUG) System.out.print(tag); 
							} 
							is.close(); 
						} finally { 
							os.close(); 
						} 
					} catch (IOException ex) { 
						if (DEBUG) ex.printStackTrace(System.out); 
					} 
				} 
			}; 
		} 

		public InetSocketAddress getAddress() { 
			return new InetSocketAddress(ss.getInetAddress(), ss.getLocalPort()); 
		} 

		// This is a bit shaky. It doesn't handle continuation 
		// lines, but our client shouldn't send any. 
		// Read a line from the input stream, swallowing the final 
		// \r\n sequence. Stops at the first \n, doesn't complain 
		// if it wasn't preceded by '\r'. 
		// 
		String readLine(InputStream r) throws IOException { 
			StringBuilder b = new StringBuilder(); 
			int c; 
			while ((c = r.read()) != -1) { 
				if (c == '\n') break; 
				b.appendCodePoint(c); 
			} 
			if (b.codePointAt(b.length() -1) == '\r') { 
				b.delete(b.length() -1, b.length()); 
			} 
			return b.toString(); 
		} 

		public void accept() { 
			Socket clientConnection = null; 
			try { 
				while (true) { 
					System.out.println("Tunnel: Waiting for client"); 
					Socket previous = clientConnection; 
					try { 
						clientConnection = ss.accept(); 
					} catch (IOException io) { 
						if (DEBUG) io.printStackTrace(System.out); 
						break; 
					} finally { 
						// we have only 1 client at a time, so it is safe 
						// to close the previous connection here 
						if (previous != null) previous.close(); 
					} 
					System.out.println("Tunnel: Client accepted"); 
					Socket targetConnection = null; 
					InputStream ccis = clientConnection.getInputStream(); 
					OutputStream ccos = clientConnection.getOutputStream(); 
					Writer w = new OutputStreamWriter(ccos, "UTF-8"); 
					PrintWriter pw = new PrintWriter(w); 
					System.out.println("Tunnel: Reading request line"); 
					String requestLine = readLine(ccis); 
					System.out.println("Tunnel: Request status line: " + requestLine); 
					if (requestLine.startsWith("CONNECT ")) { 
						// We should probably check that the next word following 
						// CONNECT is the host:port of our HTTPS serverImpl. 
						// Some improvement for a followup! 

						// Read all headers until we find the empty line that 
						// signals the end of all headers. 
						while(!requestLine.equals("")) { 
							System.out.println("Tunnel: Reading header: " 
									+ (requestLine = readLine(ccis))); 
						} 

						// Open target connection 
						targetConnection = new Socket( 
								serverImpl.getAddress().getAddress(), 
								serverImpl.getAddress().getPort()); 

						// Then send the 200 OK response to the client 
						System.out.println("Tunnel: Sending " 
								+ "HTTP/1.1 200 OK\r\n\r\n"); 
						pw.print("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); 
						pw.flush(); 
					} else { 
						// This should not happen. 
						throw new IOException("Tunnel: Unexpected status line: " 
								+ requestLine); 
					} 

					// Pipe the input stream of the client connection to the 
					// output stream of the target connection and conversely. 
					// Now the client and target will just talk to each other. 
					System.out.println("Tunnel: Starting tunnel pipes"); 
					Thread t1 = pipe(ccis, targetConnection.getOutputStream(), '+'); 
					Thread t2 = pipe(targetConnection.getInputStream(), ccos, '-'); 
					t1.start(); 
					t2.start(); 

					// We have only 1 client... wait until it has finished before 
					// accepting a new connection request. 
					// System.out.println("Tunnel: Waiting for pipes to close"); 
					// t1.join(); 
					// t2.join(); 
					System.out.println("Tunnel: Done - waiting for next client"); 
				} 
			} catch (Throwable ex) { 
				try { 
					ss.close(); 
				} catch (IOException ex1) { 
					ex.addSuppressed(ex1); 
				} 
				ex.printStackTrace(System.err); 
			} 
		} 

		void stop() throws IOException { 
			ss.close(); 
		} 

	} 

	static class Configurator extends HttpsConfigurator { 
		public Configurator(SSLContext ctx) { 
			super(ctx); 
		} 

		@Override 
		public void configure (HttpsParameters params) { 
			params.setSSLParameters (getSSLContext().getSupportedSSLParameters()); 
		} 
	} 


 static class TestSSLContext { 

		SSLContext ssl; 

		public TestSSLContext() throws Exception { 
			init(); 
		} 

		private void init() throws Exception { 

			CertAndKeyGen keyGen=new CertAndKeyGen("RSA","SHA1WithRSA",null); 
			keyGen.generate(1024); 

			//Generate self signed certificate 
			X509Certificate[] chain=new X509Certificate[1]; 
			chain[0]=keyGen.getSelfCertificate(new X500Name("CN=ROOT"), (long)365*24*3600); 

			char[] passphrase = "passphrase".toCharArray(); 

			KeyStore ks = KeyStore.getInstance("JKS"); 
			ks.load(null, passphrase); // must be "initialized" ... 

			ks.setKeyEntry("server", keyGen.getPrivateKey(), passphrase, chain); 


			KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); 
			kmf.init(ks, passphrase); 

			TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); 
			tmf.init(ks); 

			ssl = SSLContext.getInstance("TLS"); 
			ssl.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); 

		} 

		public SSLContext get() { 
			return ssl; 
		} 
	} 
	// ############################################################################################### 

	private static String toString(InputStream inputStream) throws IOException { 
		StringBuilder sb = new StringBuilder(); 
		BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); 
		int i = bufferedReader.read(); 
		while (i != -1) { 
			sb.append((char) i); 
			i = bufferedReader.read(); 
		} 
		bufferedReader.close(); 
		return sb.toString(); 
	} 
}
