46

I am developing a node.js proxy server application and I want it to support HTTP and HTTPS(SSL) protocols (as server).

I'm currently using node-http-proxy like this:

const httpProxy = require('http-proxy'),
      http = require('http');

var server = httpProxy.createServer(9000, 'localhost', function(req, res, proxy) {
    console.log(req.url);
    proxy.proxyRequest(req, res);
});

http.createServer(function(req, res) {
    res.end('hello!');
}).listen(9000);

server.listen(8000);

I setup my browser to use HTTP proxy on localhost:8000 and it works. I also want to catch HTTPS requests (ie. setup my browser to use localhost:8000 as HTTPS proxy as well and catch the requests in my application). Could you please help me how can I do that?

PS:

If I subscribe to upgrade event of httpProxy server object I can get the requests but I don't know how to forward the request and send response to client:

server.on('upgrade', function(req, socket, head) {
    console.log(req.url);
    // I don't know how to forward the request and send the response to client
});

Any helps would be appreciated.

user1051478
  • 461
  • 1
  • 5
  • 4

4 Answers4

55

Solutions barely exist for this, and the documentation is poor at best for supporting both on one server. The trick here is to understand that client proxy configurations may send https requests to an http proxy server. This is true for Firefox if you specify an HTTP proxy and then check "same for all protocols".

You can handle https connections sent to an HTTP server by listening for the "connect" event. Note that you won't have access to the response object on the connect event, only the socket and bodyhead. Data sent over this socket will remain encrypted to you as the proxy server.

In this solution, you don't have to make your own certificates, and you won't have certificate conflicts as a result. The traffic is simply proxied, not intercepted and rewritten with different certificates.

//  Install npm dependencies first
//  npm init
//  npm install --save url@0.10.3
//  npm install --save http-proxy@1.11.1

var httpProxy = require("http-proxy");
var http = require("http");
var url = require("url");
var net = require('net');

var server = http.createServer(function (req, res) {
  var urlObj = url.parse(req.url);
  var target = urlObj.protocol + "//" + urlObj.host;

  console.log("Proxy HTTP request for:", target);

  var proxy = httpProxy.createProxyServer({});
  proxy.on("error", function (err, req, res) {
    console.log("proxy error", err);
    res.end();
  });

  proxy.web(req, res, {target: target});
}).listen(8080);  //this is the port your clients will connect to

var regex_hostport = /^([^:]+)(:([0-9]+))?$/;

var getHostPortFromString = function (hostString, defaultPort) {
  var host = hostString;
  var port = defaultPort;

  var result = regex_hostport.exec(hostString);
  if (result != null) {
    host = result[1];
    if (result[2] != null) {
      port = result[3];
    }
  }

  return ( [host, port] );
};

server.addListener('connect', function (req, socket, bodyhead) {
  var hostPort = getHostPortFromString(req.url, 443);
  var hostDomain = hostPort[0];
  var port = parseInt(hostPort[1]);
  console.log("Proxying HTTPS request for:", hostDomain, port);

  var proxySocket = new net.Socket();
  proxySocket.connect(port, hostDomain, function () {
      proxySocket.write(bodyhead);
      socket.write("HTTP/" + req.httpVersion + " 200 Connection established\r\n\r\n");
    }
  );

  proxySocket.on('data', function (chunk) {
    socket.write(chunk);
  });

  proxySocket.on('end', function () {
    socket.end();
  });

  proxySocket.on('error', function () {
    socket.write("HTTP/" + req.httpVersion + " 500 Connection error\r\n\r\n");
    socket.end();
  });

  socket.on('data', function (chunk) {
    proxySocket.write(chunk);
  });

  socket.on('end', function () {
    proxySocket.end();
  });

  socket.on('error', function () {
    proxySocket.end();
  });

});
Josh Hibschman
  • 3,148
  • 1
  • 25
  • 27
  • Love your example. I was wondering how you could augment it to also handle websocket upgrade connections – lzc Jan 30 '17 at 02:55
  • @y3sh would you please check my question here http://stackoverflow.com/questions/43908640/node-js-http-proxy-with-https? I think that your example can help me, but I don't know how to implement it.. – Valip May 11 '17 at 07:36
  • How can i add user and password authentication to this so that I only forward requests from me and my dynamic IP address? –  Aug 22 '17 at 08:10
  • 7
    @y3sh you should write an npm package – Azevedo Mar 13 '18 at 17:55
  • why does the 'connect' listener only get triggered for https? per the docs, connect is one of the events on http, so I'm not really sure how this works to divide the protocols. – idij Sep 05 '18 at 11:36
  • How would some change the response data? In socket.on('data') or in proxySocket.on('data')?? – puppeteer701 Nov 02 '18 at 10:21
  • how can I forward my request to another proxy server when the request is received on connect event? I tried replacing port and hostdomain with proxyPort and proxyIP but it did not work – Point Networks Mar 21 '19 at 11:13
  • @idij `connect` (http `CONNECT` method) is sent by the proxy clients only when they need to access an **https site**. In such a case the proxy will provide them a proxified **TCP socket** through which they can proceed to the ssl handshake. On the other hand, requesting an **http site** from proxy is the **same as a normal request**, but the full URL is passed to the method. – Marinos An Sep 11 '19 at 15:56
  • 1
    I wrote an NPM package (@ucipass/proxy) using this code. See https://github.com/ucipass/proxy – ucipass Apr 18 '20 at 18:41
  • this traffic can be easily intercepted, which is probably not a good solution for traversing chinese fire wall.. – MartianMartian Apr 01 '21 at 06:45
20

Here is my NO-dependencies solution (pure NodeJS system libraries):

const http = require('http')
const port = process.env.PORT || 9191
const net = require('net')
const url = require('url')

const requestHandler = (req, res) => { // discard all request to proxy server except HTTP/1.1 CONNECT method
  res.writeHead(405, {'Content-Type': 'text/plain'})
  res.end('Method not allowed')
}

const server = http.createServer(requestHandler)

const listener = server.listen(port, (err) => {
  if (err) {
    return console.error(err)
  }
  const info = listener.address()
  console.log(`Server is listening on address ${info.address} port ${info.port}`)
})

server.on('connect', (req, clientSocket, head) => { // listen only for HTTP/1.1 CONNECT method
  console.log(clientSocket.remoteAddress, clientSocket.remotePort, req.method, req.url)
  if (!req.headers['proxy-authorization']) { // here you can add check for any username/password, I just check that this header must exist!
    clientSocket.write([
      'HTTP/1.1 407 Proxy Authentication Required',
      'Proxy-Authenticate: Basic realm="proxy"',
      'Proxy-Connection: close',
    ].join('\r\n'))
    clientSocket.end('\r\n\r\n')  // empty body
    return
  }
  const {port, hostname} = url.parse(`//${req.url}`, false, true) // extract destination host and port from CONNECT request
  if (hostname && port) {
    const serverErrorHandler = (err) => {
      console.error(err.message)
      if (clientSocket) {
        clientSocket.end(`HTTP/1.1 500 ${err.message}\r\n`)
      }
    }
    const serverEndHandler = () => {
      if (clientSocket) {
        clientSocket.end(`HTTP/1.1 500 External Server End\r\n`)
      }
    }
    const serverSocket = net.connect(port, hostname) // connect to destination host and port
    const clientErrorHandler = (err) => {
      console.error(err.message)
      if (serverSocket) {
        serverSocket.end()
      }
    }
    const clientEndHandler = () => {
      if (serverSocket) {
        serverSocket.end()
      }
    }
    clientSocket.on('error', clientErrorHandler)
    clientSocket.on('end', clientEndHandler)
    serverSocket.on('error', serverErrorHandler)
    serverSocket.on('end', serverEndHandler)
    serverSocket.on('connect', () => {
      clientSocket.write([
        'HTTP/1.1 200 Connection Established',
        'Proxy-agent: Node-VPN',
      ].join('\r\n'))
      clientSocket.write('\r\n\r\n') // empty body
      // "blindly" (for performance) pipe client socket and destination socket between each other
      serverSocket.pipe(clientSocket, {end: false})
      clientSocket.pipe(serverSocket, {end: false})
    })
  } else {
    clientSocket.end('HTTP/1.1 400 Bad Request\r\n')
    clientSocket.destroy()
  }
})

I tested this code with Firefox Proxy Settings (it even asks for username and password!). I entered IP address of machine where this code is runned and 9191 port as you can see in the code. I also set "Use this proxy server for all protocols". I run this code locally and on VPS - in both cases works!

You can test your NodeJS proxy with curl:

curl -x http://username:password@127.0.0.1:9191 https://www.google.com/
Alexey Vol
  • 1,723
  • 1
  • 16
  • 20
  • With this you can know some information like hostname and port of the final url, but not things like complete url (with search path) and request method. Any idea of how to get that info? – ajimix Oct 08 '18 at 16:05
  • 1
    What do you mean? You want to know destination query params or what? – Alexey Vol Oct 09 '18 at 12:05
  • Is it possible to read the response data from the server? I have tried to listen on the data event by the socketServer but the result is not readable. I just get something like this: buړp������3& – csskevin Nov 29 '18 at 16:20
  • 3
    It is possible only for HTTP. For HTTPS it is impossible, because it is "Encryption science"! In this case your NodeJS proxy will be as "Man in the middle". And "Encryption science" is mainly for defence browser-server connection against any "Men in the middle"... – Alexey Vol Nov 30 '18 at 18:03
  • this is great Алексей, can I ask how/where you learned how to code a proxy? I'm interested in extending this to do a forward proxy. – JasonS Jan 19 '19 at 03:17
  • I just wanted to have my own NodeJS proxy server on VPS. So I learned some articles in internet and I found out that there is no such solution on "pure" NodeJS. This code is based on my 6 years Front-End dev. experience - that is all, I don't know NodeJS well )) – Alexey Vol Jan 19 '19 at 08:27
  • Why do you set `{end: false}` in `pipe` options? – Qwertiy Feb 15 '19 at 23:48
  • You can omit this, I copied this line from another project. In another project it was intentional to not close opened socket. – Alexey Vol Feb 17 '19 at 09:28
  • saved my day, bro – Gena Moroz Mar 18 '19 at 14:05
  • Yes @AlexeyVolodko Want query params, atleast the whole url with paths. How to get that? Should we used http2? – Rahul Kahale Sep 05 '22 at 17:40
9

I have created a http/https proxy with the aid of the http-proxy module: https://gist.github.com/ncthis/6863947

Code as of now:

var fs = require('fs'),
  http = require('http'),
  https = require('https'),
  httpProxy = require('http-proxy');

var isHttps = true; // do you want a https proxy?

var options = {
  https: {
    key: fs.readFileSync('key.pem'),
    cert: fs.readFileSync('key-cert.pem')
  }
};

// this is the target server
var proxy = new httpProxy.HttpProxy({
  target: {
    host: '127.0.0.1',
    port: 8080
  }
});

if (isHttps)
  https.createServer(options.https, function(req, res) {
    console.log('Proxying https request at %s', new Date());
    proxy.proxyRequest(req, res);
  }).listen(443, function(err) {
    if (err)
      console.log('Error serving https proxy request: %s', req);

    console.log('Created https proxy. Forwarding requests from %s to %s:%s', '443', proxy.target.host, proxy.target.port);
  });
else
  http.createServer(options.https, function(req, res) {
    console.log('Proxying http request at %s', new Date());
    console.log(req);
    proxy.proxyRequest(req, res);
  }).listen(80, function(err) {
    if (err)
      console.log('Error serving http proxy request: %s', req);

    console.log('Created http proxy. Forwarding requests from %s to %s:%s', '80', proxy.target.host, proxy.target.port);
  });
david
  • 2,529
  • 1
  • 34
  • 50
ncabral
  • 2,512
  • 1
  • 19
  • 20
2

The node-http-proxy docs contain examples of this. Look for "Proxying to HTTPS from HTTPS" at https://github.com/nodejitsu/node-http-proxy The configuration process is slightly different in every browser. Some have the option to use your proxy settings for all protocols; some you need to configure the SSL proxy separately.