/* * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ import com.sun.net.httpserver.BasicAuthenticator; import com.sun.net.httpserver.Filter; import com.sun.net.httpserver.Headers; 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.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Writer; import java.math.BigInteger; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.MalformedURLException; import java.net.ServerSocket; import java.net.Socket; import java.net.URL; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.util.Arrays; import java.util.Base64; import java.util.List; import java.util.Objects; import java.util.Random; import java.util.stream.Collectors; import javax.net.ssl.SSLContext; import sun.net.www.HeaderParser; /** * A simple HTTP server that supports Digest authentication. * By default this server will echo back whatever is present * in the request body. * @author danielfuchs */ public class HTTPTestServer extends HTTPTest { final HttpServer serverImpl; // this server endpoint final HTTPTestServer redirect; // the target server where to redirect 3xx final HttpHandler delegate; // unused private HTTPTestServer(HttpServer server, HTTPTestServer target, HttpHandler delegate) { this.serverImpl = server; this.redirect = target; this.delegate = delegate; } public static void main(String[] args) throws IOException { HTTPTestServer server = create(HTTPTest.DEFAULT_PROTOCOL_TYPE, HTTPTest.DEFAULT_HTTP_AUTH_TYPE, HTTPTest.AUTHENTICATOR, HTTPTest.DEFAULT_SCHEME_TYPE); try { System.out.println("Server created at " + server.getAddress()); System.out.println("Strike <Return> to exit"); System.in.read(); } finally { System.out.println("stopping server"); server.stop(); } } private static String toString(Headers headers) { return headers.entrySet().stream() .map((e) -> e.getKey() + ": " + e.getValue()) .collect(Collectors.joining("\n")); } public static HTTPTestServer create(HttpProtocolType protocol, HttpAuthType authType, HttpTestAuthenticator auth, HttpSchemeType schemeType) throws IOException { return create(protocol, authType, auth, schemeType, null); } public static HTTPTestServer create(HttpProtocolType protocol, HttpAuthType authType, HttpTestAuthenticator auth, HttpSchemeType schemeType, HttpHandler delegate) throws IOException { Objects.requireNonNull(authType); Objects.requireNonNull(auth); switch(authType) { // A server that performs Server Digest authentication. case SERVER: return createServer(protocol, authType, auth, schemeType, delegate, "/"); // A server that pretends to be a Proxy and performs // Proxy Digest authentication. If protocol is HTTPS, // then this will create a HttpsProxyTunnel that will // handle the CONNECT request for tunneling. case PROXY: return createProxy(protocol, authType, auth, schemeType, delegate, "/"); // A server that sends 307 redirect to a server that performs // Digest authentication. // Note: 301 doesn't work here because it transforms POST into GET. case SERVER307: return createServerAndRedirect(protocol, HttpAuthType.SERVER, auth, schemeType, delegate, 307); // A server that sends 305 redirect to a proxy that performs // Digest authentication. case PROXY305: return createServerAndRedirect(protocol, HttpAuthType.PROXY, auth, schemeType, delegate, 305); default: throw new InternalError("Unknown server type: " + authType); } } static HttpServer createHttpServer(HttpProtocolType protocol) throws IOException { switch (protocol) { case HTTP: return HttpServer.create(); case HTTPS: return configure(HttpsServer.create()); default: throw new InternalError("Unsupported protocol " + protocol); } } static HttpsServer configure(HttpsServer server) throws IOException { try { SSLContext ctx = SSLContext.getDefault(); server.setHttpsConfigurator(new Configurator(ctx)); } catch (NoSuchAlgorithmException ex) { throw new IOException(ex); } return server; } static void setContextAuthenticator(HttpContext ctxt, HttpTestAuthenticator auth) { final String realm = auth.getRealm(); com.sun.net.httpserver.Authenticator authenticator = new BasicAuthenticator(realm) { @Override public boolean checkCredentials(String username, String pwd) { return auth.getUserName().equals(username) && new String(auth.getPassword(username)).equals(pwd); } }; ctxt.setAuthenticator(authenticator); } public static HTTPTestServer createServer(HttpProtocolType protocol, HttpAuthType authType, HttpTestAuthenticator auth, HttpSchemeType schemeType, HttpHandler delegate, String path) throws IOException { Objects.requireNonNull(authType); Objects.requireNonNull(auth); HttpServer impl = createHttpServer(protocol); final HTTPTestServer server = new HTTPTestServer(impl, null, delegate); final HttpHandler hh = server.createHandler(schemeType, auth, authType); HttpContext ctxt = impl.createContext(path, hh); server.configureAuthentication(ctxt, schemeType, auth, authType); impl.bind(new InetSocketAddress("127.0.0.1", 0), 0); impl.start(); return server; } public static HTTPTestServer createProxy(HttpProtocolType protocol, HttpAuthType authType, HttpTestAuthenticator auth, HttpSchemeType schemeType, HttpHandler delegate, String path) throws IOException { Objects.requireNonNull(authType); Objects.requireNonNull(auth); HttpServer impl = createHttpServer(protocol); final HTTPTestServer server = protocol == HttpProtocolType.HTTPS ? new HttpsProxyTunnel(impl, null, delegate) : new HTTPTestServer(impl, null, delegate); final HttpHandler hh = server.createHandler(schemeType, auth, authType); HttpContext ctxt = impl.createContext(path, hh); server.configureAuthentication(ctxt, schemeType, auth, authType); impl.bind(new InetSocketAddress("127.0.0.1", 0), 0); impl.start(); return server; } public static HTTPTestServer createServerAndRedirect( HttpProtocolType protocol, HttpAuthType targetAuthType, HttpTestAuthenticator auth, HttpSchemeType schemeType, HttpHandler targetDelegate, int code300) throws IOException { Objects.requireNonNull(targetAuthType); Objects.requireNonNull(auth); // The connection between client and proxy can only // be a plain connection: SSL connection to proxy // is not supported by our client connection. HttpProtocolType targetProtocol = targetAuthType == HttpAuthType.PROXY ? HttpProtocolType.HTTP : protocol; HTTPTestServer redirectTarget = (targetAuthType == HttpAuthType.PROXY) ? createProxy(protocol, targetAuthType, auth, schemeType, targetDelegate, "/") : createServer(targetProtocol, targetAuthType, auth, schemeType, targetDelegate, "/"); HttpServer impl = createHttpServer(protocol); final HTTPTestServer redirectingServer = new HTTPTestServer(impl, redirectTarget, null); InetSocketAddress redirectAddr = redirectTarget.getAddress(); URL locationURL = url(targetProtocol, redirectAddr, "/"); final HttpHandler hh = redirectingServer.create300Handler(locationURL, HttpAuthType.SERVER, code300); impl.createContext("/", hh); impl.bind(new InetSocketAddress("127.0.0.1", 0), 0); impl.start(); return redirectingServer; } public InetSocketAddress getAddress() { return serverImpl.getAddress(); } public void stop() { serverImpl.stop(0); if (redirect != null) { redirect.stop(); } } protected void writeResponse(HttpExchange he) throws IOException { if (delegate == null) { he.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0); he.getResponseBody().write(he.getRequestBody().readAllBytes()); } else { delegate.handle(he); } } private HttpHandler createHandler(HttpSchemeType schemeType, HttpTestAuthenticator auth, HttpAuthType authType) { return new HttpNoAuthHandler(authType); } private void configureAuthentication(HttpContext ctxt, HttpSchemeType schemeType, HttpTestAuthenticator auth, HttpAuthType authType) { switch(schemeType) { case DIGEST: // DIGEST authentication is handled by the handler. ctxt.getFilters().add(new HttpDigestFilter(auth, authType)); break; case BASIC: // BASIC authentication is handled by the filter. ctxt.getFilters().add(new HttpBasicFilter(auth, authType)); break; case BASICSERVER: switch(authType) { case PROXY: case PROXY305: // HttpServer can't support Proxy-type authentication // => we do as if BASIC had been specified, and we will // handle authentication in the handler. ctxt.getFilters().add(new HttpBasicFilter(auth, authType)); break; case SERVER: case SERVER307: // Basic authentication is handled by HttpServer // directly => the filter should not perform // authentication again. setContextAuthenticator(ctxt, auth); ctxt.getFilters().add(new HttpNoAuthFilter(authType)); break; default: throw new InternalError("Invalid combination scheme=" + schemeType + " authType=" + authType); } case NONE: // No authentication at all. ctxt.getFilters().add(new HttpNoAuthFilter(authType)); break; default: throw new InternalError("No such scheme: " + schemeType); } } private HttpHandler create300Handler(URL proxyURL, HttpAuthType type, int code300) throws MalformedURLException { return new Http3xxHandler(proxyURL, type, code300); } // Abstract HTTP filter class. private abstract static class AbstractHttpFilter extends Filter { final HttpAuthType authType; final String type; public AbstractHttpFilter(HttpAuthType authType, String type) { this.authType = authType; this.type = type; } String getLocation() { return "Location"; } String getAuthenticate() { return authType == HttpAuthType.PROXY ? "Proxy-Authenticate" : "WWW-Authenticate"; } String getAuthorization() { return authType == HttpAuthType.PROXY ? "Proxy-Authorization" : "Authorization"; } int getUnauthorizedCode() { return authType == HttpAuthType.PROXY ? HttpURLConnection.HTTP_PROXY_AUTH : HttpURLConnection.HTTP_UNAUTHORIZED; } String getKeepAlive() { return "keep-alive"; } String getConnection() { return authType == HttpAuthType.PROXY ? "Proxy-Connection" : "Connection"; } protected abstract boolean isAuthentified(HttpExchange he) throws IOException; protected abstract void requestAuthentication(HttpExchange he) throws IOException; protected void accept(HttpExchange he, Chain chain) throws IOException { chain.doFilter(he); } @Override public String description() { return "Filter for " + type; } @Override public void doFilter(HttpExchange he, Chain chain) throws IOException { try { System.out.println(type + ": Got " + he.getRequestMethod() + ": " + he.getRequestURI() + "\n" + HTTPTestServer.toString(he.getRequestHeaders())); if (!isAuthentified(he)) { try { requestAuthentication(he); he.sendResponseHeaders(getUnauthorizedCode(), 0); System.out.println(type + ": Sent back " + getUnauthorizedCode()); } finally { he.close(); } } else { accept(he, chain); } } catch (RuntimeException | Error | IOException t) { System.err.println(type + ": Unexpected exception while handling request: " + t); t.printStackTrace(System.err); he.close(); throw t; } } } private final static class DigestResponse { final String realm; final String username; final String nonce; final String cnonce; final String nc; final String uri; final String algorithm; final String response; final String qop; final String opaque; public DigestResponse(String realm, String username, String nonce, String cnonce, String nc, String uri, String algorithm, String qop, String opaque, String response) { this.realm = realm; this.username = username; this.nonce = nonce; this.cnonce = cnonce; this.nc = nc; this.uri = uri; this.algorithm = algorithm; this.qop = qop; this.opaque = opaque; this.response = response; } String getAlgorithm(String defval) { return algorithm == null ? defval : algorithm; } String getQoP(String defval) { return qop == null ? defval : qop; } // Code stolen from DigestAuthentication: private static final char charArray[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; private static String encode(String src, char[] passwd, MessageDigest md) { try { md.update(src.getBytes("ISO-8859-1")); } catch (java.io.UnsupportedEncodingException uee) { assert false; } if (passwd != null) { byte[] passwdBytes = new byte[passwd.length]; for (int i=0; i<passwd.length; i++) passwdBytes[i] = (byte)passwd[i]; md.update(passwdBytes); Arrays.fill(passwdBytes, (byte)0x00); } byte[] digest = md.digest(); StringBuilder res = new StringBuilder(digest.length * 2); for (int i = 0; i < digest.length; i++) { int hashchar = ((digest[i] >>> 4) & 0xf); res.append(charArray[hashchar]); hashchar = (digest[i] & 0xf); res.append(charArray[hashchar]); } return res.toString(); } public static String computeDigest(boolean isRequest, String reqMethod, char[] password, DigestResponse params) throws NoSuchAlgorithmException { String A1, HashA1; String algorithm = params.getAlgorithm("MD5"); boolean md5sess = algorithm.equalsIgnoreCase ("MD5-sess"); MessageDigest md = MessageDigest.getInstance(md5sess?"MD5":algorithm); if (params.username == null) { throw new IllegalArgumentException("missing username"); } if (params.realm == null) { throw new IllegalArgumentException("missing realm"); } if (params.uri == null) { throw new IllegalArgumentException("missing uri"); } if (params.nonce == null) { throw new IllegalArgumentException("missing nonce"); } A1 = params.username + ":" + params.realm + ":"; HashA1 = encode(A1, password, md); String A2; if (isRequest) { A2 = reqMethod + ":" + params.uri; } else { A2 = ":" + params.uri; } String HashA2 = encode(A2, null, md); String combo, finalHash; if ("auth".equals(params.qop)) { /* RRC2617 when qop=auth */ if (params.cnonce == null) { throw new IllegalArgumentException("missing nonce"); } if (params.nc == null) { throw new IllegalArgumentException("missing nonce"); } combo = HashA1+ ":" + params.nonce + ":" + params.nc + ":" + params.cnonce + ":auth:" +HashA2; } else { /* for compatibility with RFC2069 */ combo = HashA1 + ":" + params.nonce + ":" + HashA2; } finalHash = encode(combo, null, md); return finalHash; } public static DigestResponse create(String raw) { String username, realm, nonce, nc, uri, response, cnonce, algorithm, qop, opaque; HeaderParser parser = new HeaderParser(raw); username = parser.findValue("username"); realm = parser.findValue("realm"); nonce = parser.findValue("nonce"); nc = parser.findValue("nc"); uri = parser.findValue("uri"); cnonce = parser.findValue("cnonce"); response = parser.findValue("response"); algorithm = parser.findValue("algorithm"); qop = parser.findValue("qop"); opaque = parser.findValue("opaque"); return new DigestResponse(realm, username, nonce, cnonce, nc, uri, algorithm, qop, opaque, response); } } private class HttpNoAuthFilter extends AbstractHttpFilter { public HttpNoAuthFilter(HttpAuthType authType) { super(authType, authType == HttpAuthType.SERVER ? "NoAuth Server" : "NoAuth Proxy"); } @Override protected boolean isAuthentified(HttpExchange he) throws IOException { return true; } @Override protected void requestAuthentication(HttpExchange he) throws IOException { throw new InternalError("Should not com here"); } @Override public String description() { return "Passthrough Filter"; } } // An HTTP Filter that performs Basic authentication private class HttpBasicFilter extends AbstractHttpFilter { private final HttpTestAuthenticator auth; public HttpBasicFilter(HttpTestAuthenticator auth, HttpAuthType authType) { super(authType, authType == HttpAuthType.SERVER ? "Basic Server" : "Basic Proxy"); this.auth = auth; } @Override protected void requestAuthentication(HttpExchange he) throws IOException { he.getResponseHeaders().add(getAuthenticate(), "Basic realm=\"" + auth.getRealm() + "\""); System.out.println(type + ": Requesting Basic Authentication " + he.getResponseHeaders().getFirst(getAuthenticate())); } @Override protected boolean isAuthentified(HttpExchange he) { if (he.getRequestHeaders().containsKey(getAuthorization())) { List<String> authorization = he.getRequestHeaders().get(getAuthorization()); for (String a : authorization) { System.out.println(type + ": processing " + a); int sp = a.indexOf(' '); if (sp < 0) return false; String scheme = a.substring(0, sp); if (!"Basic".equalsIgnoreCase(scheme)) { System.out.println(type + ": Unsupported scheme '" + scheme +"'"); return false; } if (a.length() <= sp+1) { System.out.println(type + ": value too short for '" + scheme +"'"); return false; } a = a.substring(sp+1); return validate(a); } return false; } return false; } boolean validate(String a) { byte[] b = Base64.getDecoder().decode(a); String userpass = new String (b); int colon = userpass.indexOf (':'); String uname = userpass.substring (0, colon); String pass = userpass.substring (colon+1); return auth.getUserName().equals(uname) && new String(auth.getPassword(uname)).equals(pass); } @Override public String description() { return "Filter for " + type; } } // An HTTP Filter that performs Digest authentication private class HttpDigestFilter extends AbstractHttpFilter { // This is a very basic DIGEST - used only for the purpose of testing // the client implementation. Therefore we can get away with never // updating the server nonce as it makes the implementation of the // server side digest simpler. private final HttpTestAuthenticator auth; private final byte[] nonce; private final String ns; public HttpDigestFilter(HttpTestAuthenticator auth, HttpAuthType authType) { super(authType, authType == HttpAuthType.SERVER ? "Digest Server" : "Digest Proxy"); this.auth = auth; nonce = new byte[16]; new Random(Instant.now().toEpochMilli()).nextBytes(nonce); ns = new BigInteger(1, nonce).toString(16); } @Override protected void requestAuthentication(HttpExchange he) throws IOException { he.getResponseHeaders().add(getAuthenticate(), "Digest realm=\"" + auth.getRealm() + "\"," + "\r\n qop=\"auth\"," + "\r\n nonce=\"" + ns +"\""); System.out.println(type + ": Requesting Digest Authentication " + he.getResponseHeaders().getFirst(getAuthenticate())); } @Override protected boolean isAuthentified(HttpExchange he) { if (he.getRequestHeaders().containsKey(getAuthorization())) { List<String> authorization = he.getRequestHeaders().get(getAuthorization()); for (String a : authorization) { System.out.println(type + ": processing " + a); int sp = a.indexOf(' '); if (sp < 0) return false; String scheme = a.substring(0, sp); if (!"Digest".equalsIgnoreCase(scheme)) { System.out.println(type + ": Unsupported scheme '" + scheme +"'"); return false; } if (a.length() <= sp+1) { System.out.println(type + ": value too short for '" + scheme +"'"); return false; } a = a.substring(sp+1); DigestResponse dgr = DigestResponse.create(a); return validate(he.getRequestMethod(), dgr); } return false; } return false; } boolean validate(String reqMethod, DigestResponse dg) { if (!"MD5".equalsIgnoreCase(dg.getAlgorithm("MD5"))) { System.out.println(type + ": Unsupported algorithm " + dg.algorithm); return false; } if (!"auth".equalsIgnoreCase(dg.getQoP("auth"))) { System.out.println(type + ": Unsupported qop " + dg.qop); return false; } try { if (!dg.nonce.equals(ns)) { System.out.println(type + ": bad nonce returned by client: " + nonce + " expected " + ns); return false; } if (dg.response == null) { System.out.println(type + ": missing digest response."); return false; } char[] pa = auth.getPassword(dg.username); return verify(reqMethod, dg, pa); } catch(IllegalArgumentException | SecurityException | NoSuchAlgorithmException e) { System.out.println(type + ": " + e.getMessage()); return false; } } boolean verify(String reqMethod, DigestResponse dg, char[] pw) throws NoSuchAlgorithmException { String response = DigestResponse.computeDigest(true, reqMethod, pw, dg); if (!dg.response.equals(response)) { System.out.println(type + ": bad response returned by client: " + dg.response + " expected " + response); return false; } else { System.out.println(type + ": verified response " + response); } return true; } @Override public String description() { return "Filter for DIGEST authentication"; } } // Abstract HTTP handler class. private abstract static class AbstractHttpHandler implements HttpHandler { final HttpAuthType authType; final String type; public AbstractHttpHandler(HttpAuthType authType, String type) { this.authType = authType; this.type = type; } String getLocation() { return "Location"; } @Override public void handle(HttpExchange he) throws IOException { try { sendResponse(he); } catch (RuntimeException | Error | IOException t) { System.err.println(type + ": Unexpected exception while handling request: " + t); t.printStackTrace(System.err); throw t; } finally { he.close(); } } protected abstract void sendResponse(HttpExchange he) throws IOException; } private class HttpNoAuthHandler extends AbstractHttpHandler { public HttpNoAuthHandler(HttpAuthType authType) { super(authType, authType == HttpAuthType.SERVER ? "NoAuth Server" : "NoAuth Proxy"); } @Override protected void sendResponse(HttpExchange he) throws IOException { HTTPTestServer.this.writeResponse(he); } } // A dummy HTTP Handler that redirects all incoming requests // by sending a back 3xx response code (301, 305, 307 etc..) private class Http3xxHandler extends AbstractHttpHandler { private final URL redirectTargetURL; private final int code3XX; public Http3xxHandler(URL proxyURL, HttpAuthType authType, int code300) { super(authType, "Server" + code300); this.redirectTargetURL = proxyURL; this.code3XX = code300; } int get3XX() { return code3XX; } @Override public void sendResponse(HttpExchange he) throws IOException { System.out.println(type + ": Got " + he.getRequestMethod() + ": " + he.getRequestURI() + "\n" + HTTPTestServer.toString(he.getRequestHeaders())); System.out.println(type + ": Redirecting to " + (authType == HttpAuthType.PROXY305 ? "proxy" : "server")); he.getResponseHeaders().add(getLocation(), redirectTargetURL.toExternalForm().toString()); he.sendResponseHeaders(get3XX(), 0); System.out.println(type + ": Sent back " + get3XX() + " " + getLocation() + ": " + redirectTargetURL.toExternalForm().toString()); } } static class Configurator extends HttpsConfigurator { public Configurator(SSLContext ctx) { super(ctx); } @Override public void configure (HttpsParameters params) { params.setSSLParameters (getSSLContext().getSupportedSSLParameters()); } } // This is a bit hacky: HttpsProxyTunnel is an HTTPTestServer hidden // behind a fake proxy that only understands CONNECT requests. // The fake proxy is just a server socket that intercept the // CONNECT and then redirect streams to the real server. static class HttpsProxyTunnel extends HTTPTestServer implements Runnable { final ServerSocket ss; public HttpsProxyTunnel(HttpServer server, HTTPTestServer target, HttpHandler delegate) throws IOException { super(server, target, delegate); System.out.flush(); System.err.println("WARNING: HttpsProxyTunnel is an experimental test class"); ss = new ServerSocket(0, 0, InetAddress.getByName("127.0.0.1")); start(); } final void start() throws IOException { Thread t = new Thread(this, "ProxyThread"); t.setDaemon(true); t.start(); } @Override public void stop() { super.stop(); try { ss.close(); } catch (IOException ex) { if (DEBUG) ex.printStackTrace(System.out); } } // 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); } } }; } @Override 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(); } @Override public void run() { 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 { // close the previous connection 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( clientConnection.getOutputStream(), "UTF-8"); PrintWriter pw = new PrintWriter(w); System.out.println("Tunnel: Reading request line"); String requestLine = readLine(ccis); System.out.println("Tunnel: Request 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))); } 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. If it does let our serverImpl // deal with it. 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. t1.join(); t2.join(); } } catch (Throwable ex) { try { ss.close(); } catch (IOException ex1) { ex.addSuppressed(ex1); } ex.printStackTrace(System.err); } } } }