0

I'm trying to create an HTTP/S MitM forwarding proxy using Node.js.

The way I'm tackling this project is by reusing the solution found in ./lib/proxy.js file of the NPM Proxy Cache project created by @runk after he raised the issue on the Node HTTP Proxy project issue tracker.

My Proxy() class looks like this:

var request = require('request')
  , https = require('https')
  , http = require('http')
  , net = require('net')
  , url = require('url')
  , os = require('os')
  , fs = require('fs');

var SOCKET_PATH = os.tmpdir() + 'mitm.sock';
console.log('[SOCKET PATH] ' + SOCKET_PATH);

function Proxy (config) {
    config = config || {};

    if(fs.existsSync(SOCKET_PATH)) {
        fs.unlinkSync(SOCKET_PATH);
    }

    var options = {
        key: fs.readFileSync('./certs/dummy.key', 'utf8'),
        cert: fs.readFileSync('./certs/dummy.crt', 'utf8')
    };

    // HTTPS Server
    https.createServer(options, this.handler).listen(config.port + 1, this.hostname, function (e) {
        if(e) {
            console.log('[HTTPS] Server listen() error !');
            throw e;
        }
    });

    // HTTP Server
    var server = http.createServer(this.handler);
    server.listen(config.port, this.hostname, function (e) {
        if(e) {
            console.log('[HTTP] Server listen() error !');
            throw e;
        }
    });

    // Intercept CONNECT requests for HTTPS handshake
    server.addListener('connect', this.httpsHandler);
}

Proxy.prototype.handler = function (req, res) {
    var schema = !!req.client.pair ? 'https' : 'http'
      , path = url.parse(req.url).path;

    var dest = schema + '://' + req.headers['host'] + path;

    console.log('(1) - [' + schema.toUpperCase() + '] ' + req.method + ' ' + req.url);

    var params = {
        rejectUnauthorized: false,
        url: dest
    };

    if(req.method.toUpperCase() !== 'GET') {
        return console.log('[HTTP] Request is not HTTP GET.');
    }

    var onResponse = function (e, response) {
        if(e == null && response.statusCode === 200) {
            return r.pipe(res);
        }

        var body = 'Status ' + response.statusCode + ' returned';
        if(e) {
            body = e.toString();
        }

        res.end(body);
    };

    var r = request(params);
    r.on('response', onResponse.bind(null, null));
    r.on('error', onResponse.bind(null));
};

Proxy.prototype.httpsHandler = function (request, socketRequest, bodyHead) {
    var httpVersion = request['httpVersion']
      , url = request['url'];

    console.log('(2) - [HTTPS] ' + request['method'] + ' ' + request['url']);

    var proxySocket = new net.Socket();

    // ProxySocket event handlers
    proxySocket.connect(SOCKET_PATH, function () {
        proxySocket.write(bodyHead);
        proxySocket.write('HTTP/' + httpVersion + ' 200 Connection established\r\n\r\n');
    });

    proxySocket.on('data', function (chunk) {
        console.log('ProxySocket - "data"');
        socketRequest.write(chunk);
    });

    proxySocket.on('end', function () {
        console.log('ProxySocket - "end"');
        socketRequest.end();
    });

    proxySocket.on('error', function (e) {
        console.log('ProxySocket - "error"');
        console.log(e);
        console.log(e.stack);
        socketRequest.write('HTTP/' + httpVersion + ' 500 Connection error\r\n\r\n');
        socketRequest.end();
    });

    // SocketRequest event handlers
    socketRequest.on('data', function (chunk) {
        console.log('SocketRequest - "data"');
        proxySocket.write(chunk);
    });

    socketRequest.on('end', function () {
        console.log('SocketRequest - "end"');
        proxySocket.end();
    });

    socketRequest.on('error', function (e) {
        console.log('socketRequest - "error"');
        console.log(e);
        console.log(e.stack);
        proxySocket.end();
    });

};

module.exports = Proxy;

And my Index.js file that start my program looks like this:

var Proxy = require('./lib/proxy');

var proxy = new Proxy({
    hostname: '127.0.0.1',
    port: 8000
});

Here's my directory / file structure this:

/my_project
    /certs
        dummy.crt // Copied from the NPM Proxy Cache project
        dummy.csr // Copied from the NPM Proxy Cache project
        dummy.key // Copied from the NPM Proxy Cache project
    /lib
        proxy.js
    index.js

I'm testing my program by setting (in Mac OSX Maverick) an HTTP and HTTPS proxy as IP address 127.0.0.1 and port 8000.

When browsing an HTTP only website everything works fine, but if I browse an HTTPS website I get the following error:

{[Error: connect ENOENT] code: 'ENOENT', errno: 'ENOENT', syscall: 'connect'}
Error: connect ENOENT
    at errnoException (net.js:904:11)
    at Object.afterConnect [as oncomplete] (net.js:895:19)
  • Any ideas from where this issue could come from and how to fix this ?

Thank you very much in advance !

(If you want to test my code, the NPM module request is the only dependency needed to run the code.)

EDIT: The certs can be downloaded from here : https://github.com/runk/npm-proxy-cache/tree/master/cert.

Hrqls
  • 2,944
  • 4
  • 34
  • 54
m_vdbeek
  • 3,704
  • 7
  • 46
  • 77
  • Is it all the stacktrace you have? Have you tried in *nix environnement using VirtualBox? – Vinz243 Jun 03 '14 at 16:17
  • ALso, I'd like to test it on Windows but I how to get the certs? – Vinz243 Jun 03 '14 at 16:25
  • @Vinz243, you can download the certs from here : https://github.com/runk/npm-proxy-cache/tree/master/cert. My tests are on a Mac OSX so technically it is a *nix environment but I will try on a VirtualBox. – m_vdbeek Jun 04 '14 at 06:23

1 Answers1

2

I'm an author of npm-proxy-cache. In fact I've created another project called thin https://www.npmjs.org/package/thin and I hope in future the npm proxy cache thing will utilize it. Despite the fact that it's still very rough it's usable and it does what you need.

E.g.

proxy code

var Thin = require('thin')

var proxy = new Thin;

// `req` and `res` params are `http.ClientRequest` and `http.ServerResponse` accordingly
// be sure to check http://nodejs.org/api/http.html for more details
proxy.use(function(req, res, next) {
  console.log('Proxying:', req.url);
  next();
});

// you can add different layers of "middleware" similar to "connect", 
// but with few exclusions
proxy.use(function(req, res, next) {
  if (req.url === '/foobar')
    return res.end('intercepted');
  next();
});

proxy.listen(8081, 'localhost', function(err) {
  // .. error handling code ..
});

server code

var express = require('express'); // v3.4
var app = express();

app.use(express.urlencoded({limit: '10mb'}));

app.get('/test', function(req, res){
  console.log(req.protocol, 'get req.query', req.query);
  res.end('get: hello world');
});

app.post('/test', function(req, res) {
  console.log(req.protocol, 'post req.query', req.query);
  console.log(req.protocol, 'post req.body', req.body);
  res.end('post: hello world');
});

app.listen(3000);


var fs = require('fs');
var https = require('https');

https.createServer({
  key: fs.readFileSync('./cert/dummy.key'), // your mitm server keys
  cert: fs.readFileSync('./cert/dummy.crt')
}, app).listen(3001);

You need to start proxy and server in two terminal sessions, then

curl -d "foo=baz" -k -x https://localhost:8081 https://localhost:3001/test?foo=bar
curl -d "foo=baz" -x http://localhost:8081 http://localhost:3000/test?foo=bar

After that you should be able to see following output from the server

https post req.query { foo: 'bar' }
https post req.body { foo: 'baz' }
http post req.query { foo: 'bar' }
http post req.body { foo: 'baz' }

Small example for interceptor

curl -d "foo=baz" -k -x https://localhost:8081 https://localhost:3001/foobar

It should return intercepted

Hope that helps :)

deadrunk
  • 13,861
  • 4
  • 29
  • 29