2011/10/02

HTTP/HTTPS over SOCKS Proxy with Java org.apache.http 4.x

In my current Android project I struggled quite a bit to get HTTPS requests tunneled over the SOCKS proxy protocol working. This is my solution for the org.apache.http library 4.x:

Step 1 - Create a SOCKS Proxy Socket Factory



The org.apache.http library utilizes Factory classes for the instantiation of the Socket objects used by the http client. Fortunately, Java already provides an implementation of the SOCKS protocol which can be used in an extended PlainSocketFactory. The Proxy address can be passed to the Factory through HttpParams.

public class SocksPlainSocketFactory extends PlainSocketFactory {
public static final String SOCKS_PROXY_HOST = "socks.proxyHost";
public static final String SOCKS_PROXY_PORT = "socks.proxyPort";

@Override
public Socket createSocket(final HttpParams params) {
String proxyHost = (String) params.getParameter(SOCKS_PROXY_HOST);
int proxyPort = (Integer) params.getParameter(SOCKS_PROXY_PORT);

InetSocketAddress socksAddr = new InetSocketAddress(proxyHost, proxyPort);
Proxy proxy = new Proxy(Proxy.Type.SOCKS, socksAddr);
Socket socket = new Socket(proxy);

return socket;
}
}


Step 2 - Creating SSL over SOCKS Sockets



Http clients built with the org.apache.http library utilize a connection manager class to manage many connections. The code responsible for actually creating a connection is located in a connection operator class. To layer SSL connections over SOCKS I implemented such a connection operator which uses the above SocksPlainSocketFactory to create a SOCKS socket and then let it be opened by the scheme specific SocketFactory - in the case of https this results in a SSLSocket layered over SOCKS.

The setup of the http client:

DefaultHttpClient httpClient = null;
try {
if (proxyEnabled) {

Uri proxy = Uri.parse(proxyUrl);
String scheme = proxy.getScheme().toLowerCase();

// setup socks proxy
if (scheme.equalsIgnoreCase("socks")) {
Log.d(TAG, "Using proxy @ " + proxyUrl);
params.setParameter(SocksPlainSocketFactory.SOCKS_PROXY_HOST, proxy.getHost());
params.setParameter(SocksPlainSocketFactory.SOCKS_PROXY_PORT, proxy.getPort());

SingleClientConnManager cm = new SingleClientConnManager() {
@Override
protected ClientConnectionOperator createConnectionOperator(SchemeRegistry schreg) {
return new SocksProxyClientConnOperator(schreg);
}
};

httpClient = new DefaultHttpClient(cm, params);
httpClient = useTrustingTrustManager(httpClient);
} else {
Log.d(TAG, "Using no proxy. Don't support '" + scheme + "' proxy scheme.");
}
} else {
httpClient = new DefaultHttpClient(params);
Log.d(TAG, "Using no proxy.");
}
} catch (Exception e) {
Log.d(TAG, "Error: " + e.toString());
}


The extended connection operator:


public class SocksProxyClientConnOperator extends DefaultClientConnectionOperator {

public SocksProxyClientConnOperator(final SchemeRegistry schemes) {
super(schemes);
}

@Override
public void openConnection(final OperatedClientConnection conn, final HttpHost target,
final InetAddress local, final HttpContext context, final HttpParams params) throws IOException {
if (conn == null) {
throw new IllegalArgumentException("Connection may not be null");
}
if (target == null) {
throw new IllegalArgumentException("Target host may not be null");
}
if (params == null) {
throw new IllegalArgumentException("Parameters may not be null");
}
if (conn.isOpen()) {
throw new IllegalStateException("Connection must not be open");
}

SchemeSocketFactory socksPlainSocketFactory = new SocksPlainSocketFactory();

Scheme schm = schemeRegistry.getScheme(target.getSchemeName());
SchemeSocketFactory sf = schm.getSchemeSocketFactory();

InetAddress[] addresses = resolveHostname(target.getHostName());
int port = schm.resolvePort(target.getPort());

Socket sock = socksPlainSocketFactory.createSocket(params);
conn.opening(sock, target);

for (int i = 0; i < addresses.length; i++) {
InetAddress address = addresses[i];
boolean last = i == addresses.length - 1;
InetSocketAddress remoteAddress = new InetSocketAddress(address, port);
InetSocketAddress localAddress = null;
if (local != null) {
localAddress = new InetSocketAddress(local, 0);
}
try {
Socket connsock = sf.connectSocket(sock, remoteAddress, localAddress, params);
if (sock != connsock) {
sock = connsock;
conn.opening(sock, target);
}
prepareSocket(sock, context, params);
conn.openCompleted(sf.isSecure(sock), params);
break;
} catch (ConnectException ex) {
if (last) {
throw new HttpHostConnectException(target, ex);
}
} catch (ConnectTimeoutException ex) {
if (last) {
throw ex;
}
}
}
}
}


Alternative



An alternative solution would be to extend a SSLSocketFactory as SocksSSLSocketFactory and register both SocksSSLSocketFactory and the above SocksPlainSocketFactory in the SchemeRegistry of the http client.

2 comments: