More‎ > ‎

How to Tunnel HTTP-Protocol with a Simple Java Proxy Server through a Firewall?

BY MARKUS SPRUNCK


Firewalls block - besides some other patterns - web addresses which are supposed to be dangerous, suspicious and/or non-productive. Quite often this is a good idea, but sometimes it is just necessary to tunnel a firewall to get access to what is needed. 

In principle you can do two things (i) change the firewall rule or (ii) use an proxy adapter to tunnel your requests through the firewall and process the blocked request somewhere else. The following article describes the second approach. A small Java example which demonstrates: How to tunnel HTTP protocol through a firewall? 

Basic Requirements

The implementation has to support the following requirements:
  • Access to blocked web sites with HTTP GET and HEAD (e.g. http://www.youtube.com)
  • Act as a local proxy server for web browsers (e.g. Chrome, Firefox)
  • Secured Connection between client and server components
  • Insecure HTTP Requests Types should not be supported i.e. CONNECT, POST and PUT

System Architecture

In Figure 1 you see the principle technical architecture. The application is divided into two components. A client side proxy server called HttpProxyAdapterClient and a server component called HttpProxyAdapterServer

Figure 1: System Architecture 

Three steps are needed:
  1. Web browser connects to the HttpProxyAdapterClient which packed the HTTP into a message to the remote HttpProxyAdapterServer

  2. HttpProxyAdapterServer forwards the HTTP request and returns the result to the HttpProxyAdapterClient

  3. HttpProxyAdapterClient sends the result back to the web browser.

Implementation of HttpProxyAdapterClient

The class HttpProxyAdapterClient.java contains the main method to start the proxy adapter client. You see between line 114 and 116 that a new socket is started in a separate thread each time the web browser sends a HTTP request to the defined port at the localhost. 

HttpProxyAdapterClient.java (lines 102-122):

102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
public static void main(final String[] args) {

	readPropertyFile();

	if (getLocalProxyPort() != -1) {

		LOGGER.info("SWEC-PROXY-ADAPTER is ready at localhost:" + getLocalProxyPort());

		try {
			httpServerSocket = new ServerSocket(getLocalProxyPort());
			final boolean listening = true;
			while (listening) {
				final Socket httpSocket = httpServerSocket.accept();
				final SocketThread httpThread = new SocketThread(httpSocket);
				httpThread.start();
			}
		} catch (final IOException e) {
			LOGGER.log(Level.SEVERE, "could not listen port " + getLocalProxyPort());
		}
	}
}

The class SocketThread.java is the working thread of the socket connection. When you implement your own service on Google App Engine you have to change the URL in line 55 to your own application name.

Logging is done in the method loggingOfResult() because several thread write to console in parallel and this ensures that all output of a thread is written in one message. 

SocketThread.java (lines 54-94):

 54
 55

 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
// TODO: Change URL to your Google App Engine application
private static final String REMOTE_PROXY_SERVER_HTTPS_URL = 
        "https://http-proxy-adapter-server.appspot.com/resources/server";

/**
 * Ensures that messages from concurrent threads are logged in a block
 */
private final StringBuilder message = new StringBuilder();

/**
 * Connection to the local web client
 */
private final Socket socket;

/**
 * Helper to parse the HTTP request of the local web client
 */
private RequestHeader requestHeader;

/**
 * Connection to remote server
 */
private HttpsURLConnection remoteConnection;

public SocketThread(final Socket socket) {
	checkNotNull(socket);
	this.socket = socket;
}

@Override
public void run() {
	try {
		readRequestFormLocalClient();
		forwardLocalRequestToRemoteServer();
		sendResponseToLocalClient();
		loggingOfResult();
	} catch (final IOException | IllegalStateException e) {
		LOGGER.log(Level.WARNING, e.getMessage());
	} finally {
		closeSocket();
	}
}

The file settings.ini has all parameters for the HttpProxyAdapterClient. You may select a different port number for the proxy server (don't forget to set the port in your web browser).

Settings.ini.java (lines 1-9):

1
2
3
4
5
6
7
8
9
# settings for http-proxy-adapter-client
local.http.proxyPort=2443

# settings for corporate and/or private proxy server
https.proxySet=false
https.proxyUserName=<your windows domain>\\<your windows user ide>
https.proxyUserPwd=<your windows user password>
https.proxyPort=<port number>
https.proxyHost=<domain name>

In the case you have to use a additional proxy in your network to access the internet, you should change the property https.proxySet to the value true and complete the other proxy setting. If you have to use a domain name with your user name, please use two backslashes.

Implementation of HttpProxyAdapterServer

The server is quite simple implemented. First the existence of a correct secret token is tested. If the client don't uses the same token as the server expects, the request will be rejected as unauthorized (see line 58). 

Service.java (lines 52-112):

 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84

 85
 86
 87
 88
 89

 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
@POST
@Consumes("text/plain")
public Response forwardRequestService(final String httpMessage) {
	try {

		// If the caller don't knows the token access is not allowed
		if (!httpMessage.contains(SecretToken.TOKEN_VALUE)) {
			return Response.status(Status.UNAUTHORIZED).build();
		}

		// Everything except GET & HEAD is not allowed
		final RequestHeader requestHeader = new RequestHeader(httpMessage);
		if (!"GET".equals(requestHeader.getType()) 
                        && !"HEAD".equals(requestHeader.getType())) {
			return Response.status(HttpURLConnection.HTTP_NOT_IMPLEMENTED).build();
		}

		final URL url = new URL(requestHeader.getUrl());
		final HttpURLConnection connectionHttp = (HttpURLConnection) url.openConnection();
		connectionHttp.setRequestMethod(requestHeader.getType());
		for (final String key : requestHeader.getParameter().keySet()) {
			if (isNotSecretToken(key)) {
				String value = requestHeader.getParameter().get(key);
				connectionHttp.addRequestProperty(key, value);
			}
		}
	
        	// Send response back to client
		final ByteArrayOutputStream baos = new ByteArrayOutputStream();
		final DataInputStream dis = new DataInputStream(connectionHttp.getInputStream());

		// Add response header
		final StringBuilder sb = new StringBuilder();
		sb.append(requestHeader.getHttpVersion() + " " 
                                + connectionHttp.getResponseCode() + " "
				+ connectionHttp.getResponseMessage() + "\r\n");
		final Map<String, List<String>> map = connectionHttp.getHeaderFields();
		for (final Map.Entry<String, List<String>> entry : map.entrySet()) {
			final String key = entry.getKey();
			sb.append(key + " : " + entry.getValue().toString().replace("[", "")
                                .replace("]", "").replace(",", " ")
					+ "\r\n");
		}
		sb.append("\r\n");

		// Add response content
		baos.write(sb.toString().getBytes(), 0, sb.toString().getBytes().length);
		final byte[] data = new byte[(int) Short.MAX_VALUE];
		int index = dis.read(data, 0, (int) Short.MAX_VALUE);
		while (index != -1) {
			baos.write(data, 0, index);
			index = dis.read(data, 0, (int) Short.MAX_VALUE);
		}
		final byte[] result = baos.toByteArray();
		return Response.ok(result).build();
	
        } catch (final MalformedURLException e) {
		System.out.print("MalformedURLException " + e.getMessage());
		return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build();
	} catch (final IOException e) {
		System.out.print("IOException " + e.getMessage());
		return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build();
	}
}

After the first test the server parses the HTTPS POST request from the HttpProxyAdapterClient and creates a HTTP GET or HEAD request. All other request will be rejected with the standard HTTP Code 501 (not implemented)

In line 89 it would be possible to filter single entries of the HTTP Header from the response. The same filter is possible in line 72 for the request header. Maybe you like to ensure that some header entries from your web browser are not sent to the server or in some cases it makes sense to filter the response from the web server. 

Development Environment

The code for server and client is relative small. The HttpProxyAdapterClient has 205 Lines of Code and HttpProxyAdapterServer has 185 Lines of Code. 

Notice that the Server is implemented in Java 7 SDK (the supported Java version of Google App Engine) and the client uses Java 8 JDK. Eclipse Java EE IDE for Web Developers (Version Luna Build id: 20140612-0600) has been used in combination with Google App Engine SDK (Version 1.9.15 - November 3, 2014) plugin.

 
Figure 2: Structure of Eclipse Projects

Just fork the eclipse project on GitHub. The Guava-Lib (guava-18.0.jar) is used by the client to handle exceptions and check preconditions. On the Google App Engine server the Jersey-Lib (jersey-bundle-1.18.1.jar) and Jettison-Lib (jettison-1.3.3.jar) are used to implement the service as simple REST interface. 

Tested Environments

  • Windows 7 - Chrome (version 38)
  • Windows 7 - Firefox (version 33.1)

Known Issues

  • No support of security: 
    The solution serves as a proof of concept and is not recommended for system critical applications. Be aware that it supports just simple hypertext transfer protocol without any security. Because of this reason the use of GET and HEAD function of HTTP protocol is supported. 

  • Firefox can’t handle zipped response:
    If you like to use Firefox, open page about:config and delete the default value (gzip, deflate) of option network.http.accept-encoding