Coder Thoughts on software, technology and programming.

Piotr Mionskowski

An HTTPS system proxy with node

03 January 2014 http, node.js, https, and proxy

Creating a simple http proxy in node.js is super easy thanks to an excellent module – http-proxy. With only the following code you’ll have a proxy that can be used as system or browser level proxy:

var httpProxy = require('http-proxy');
var proxyServer = httpProxy.createServer(function (req,res,proxy) {
  var hostNameHeader = req.headers.host,
  hostAndPort = hostNameHeader.split(':'),
  host = hostAndPort[0],
  port = parseInt(hostAndPort[1]) || 80;
  proxy.proxyRequest(req,res, {
    host: host,
    port: port
  });
});
proxyServer.listen(8888);

Adding support for HTTPS

The first thing we will need is a certificate that will be used by TLS implementation for encryption. A server has access to certificate private and public key thus it is able to get a clear text from a message encrypted with public key. It’s a common knowledge that asymmetric encryption is more expensive in terms of CPU cycles than its symmetric counterpart. That’s way when using HTTPS connection asymmetric encryption is only used during initial handshake to exchange a session key in a secure manner between client and server. This session key is then used by both client and server for traffic encryption using symmetric algorithm.

Naturally to get or rather to buy a proper certificate one would have to be verified by a valid certification authority. Fortunately for the sake of a demo we can generate a self signed certificate. That’s really easy, a nice description is available on heroku help pages:

openssl genrsa -des3 -passout pass:x -out proxy-mirror.pass.key 2048
echo "Generated proxy-mirror.pass.key"

openssl rsa -passin pass:x -in proxy-mirror.pass.key -out proxy-mirror.key
rm proxy-mirror.pass.key
echo "Generated proxy-mirror.key"

openssl req -new -batch -key proxy-mirror.key -out proxy-mirror.csr -subj /CN=proxy-mirror/emailAddress=[email protected]/OU=proxy-mirror/C=PL/O=proxy-mirror
echo "Generated proxy-mirror.csr"

openssl x509 -req -days 365 -in proxy-mirror.csr -signkey proxy-mirror.key -out proxy-mirror.crt
echo "Generated proxy-mirror.crt"

http-proxy support for https

http-proxy module supports various https configuration for example passing traffic from https to http and vice versa. Unfortunately I couldn’t find a way to get it working without preconfiguring target host and port – which was a problem while I was implementing proxy-mirror. Here is what curlwill print out when trying to use such proxy:

curl -vk --proxy https://localhost:8888/ https://pl-pl.facebook.com/
* timeout on name lookup is not supported
* About to connect() to proxy localhost port 8888 (#0)
* Trying 127.0.0.1...
* connected
* Connected to localhost (127.0.0.1) port 8888 (#0)
* Establish HTTP proxy tunnel to pl-pl.facebook.com:443
> CONNECT pl-pl.facebook.com:443 HTTP/1.1
> Host: pl-pl.facebook.com:443
> User-Agent: curl/7.26.0
> Proxy-Connection: Keep-Alive
>

I suspect the problem is inherently related to the way http-proxy uses nodejs core http(s) modules – I might be wrong here though. The workaround I’ve used in proxy-mirror was to listen to CONNECT event on http server, establish socket connection to a fake https server listening on different port. When the https connection is established the fake https server handler proxies requests further. The advantage of this approach is that we can use the same proxy address for both http and https. Here is the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
var httpProxy = require('http-proxy'),
  fs = require('fs'),
  https = require('https'),
  net = require('net'),
  httpsOptions = {
    key: fs.readFileSync('proxy-mirror.key', 'utf8'),
    cert: fs.readFileSync('proxy-mirror.crt', 'utf8')
  };

var proxyServer = httpProxy.createServer(function (req, res, proxy) {
  console.log('will proxy request', req.url);
  var hostNameHeader = req.headers.host,
  hostAndPort = hostNameHeader.split(':'),
  host = hostAndPort[0],
  port = parseInt(hostAndPort[1]) || 80;
  proxy.proxyRequest(req, res, {
    host: host,
    port: port
  });
});

proxyServer.addListener('connect', function (request, socketRequest, bodyhead) {
  var srvSocket = net.connect(8889, 'localhost', function () {
    socketRequest.write('HTTP/1.1 200 Connection Established\r\n\r\n');
    srvSocket.write(bodyhead);
    srvSocket.pipe(socketRequest);
    socketRequest.pipe(srvSocket);
  });
});

var fakeHttps = https.createServer(httpsOptions, function (req, res) {
  var hostNameHeader = req.headers.host,
  hostAndPort = hostNameHeader.split(':'),
  host = hostAndPort[0],
  port = parseInt(hostAndPort[1]) || 443;

  proxyServer.proxy.proxyRequest(req, res, {
    host: host,
    port: port,
    changeOrigin: true,
    target: {
      https: true
    }
  });
});

proxyServer.listen(8888);
fakeHttps.listen(8889);

HTML5 WebSocket support

The above code has still problems handling WebSockets. This is because browsers, according to the spec, change the way they establish initial connection when they detect that they are behind an http proxy. As you can read on wikipedia more existing http proxy implementation suffer from this.  I still haven’t figured out how to handle this scenario elegantly with http-proxy and node.js, when I do I will post my findings here.