使用node.js搭建HTTPS代理服务器

46

我正在开发一个基于 node.js 的代理服务器应用程序,希望它支持作为服务器的 HTTPHTTPS(SSL) 协议。

目前我正在使用 node-http-proxy 来实现:

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);

我将我的浏览器设置为使用 localhost:8000HTTP 代理,它可以正常工作。我也想捕获 HTTPS 请求(即将我的浏览器设置为同时使用 localhost:8000 作为 HTTPS 代理,并在我的应用程序中捕获请求)。请问如何实现呢?
PS:
如果我订阅了 httpProxy 服务器对象的 upgrade 事件,我就能获取到请求,但是我不知道如何转发请求并将响应发送给客户端。
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
});

非常感谢您的帮助。


我需要的是:https://dev59.com/PWIj5IYBdhLWcg3wWj-d#63602976 - Ryan
4个回答

56

针对这个问题,几乎没有解决方案可言,而且支持在一台服务器上同时使用两种协议的文档资料十分匮乏。关键是要理解客户端代理配置可能会将https请求发送到http代理服务器,如果在Firefox中指定HTTP代理并勾选“所有协议都使用相同的代理”,这一点就更为明显了。

您可以通过监听“connect”事件来处理发送到HTTP服务器的https连接。请注意,在connect事件中,您只能访问套接字和bodyhead对象,无法访问响应对象。通过该套接字发送的数据将在作为代理服务器的您处保持加密状态。

采用这种解决方案,您不必自己创建证书,并且不会出现证书冲突。流量仅仅被代理,而不是被截获并用不同的证书重新编写。

//  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();
  });

});

我该如何添加用户和密码认证,以便只转发来自我的动态IP地址的请求? - user3788941
7
@y3sh 你应该写一个npm包。 - Azevedo
为什么“connect”监听器只对https触发?根据文档,connect是http事件之一,所以我不太确定如何将协议分开处理。 - idij
当连接事件收到请求时,我如何将我的请求转发到另一个代理服务器?我尝试用proxyPort和proxyIP替换port和hostdomain,但没有起作用。 - Point Networks
1
我使用这段代码编写了一个 NPM 包 (@ucipass/proxy)。请参见 https://github.com/ucipass/proxy - ucipass
显示剩余5条评论

20

这里是我无需依赖的解决方案(纯NodeJS系统库):

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()
  }
})

我使用Firefox代理设置测试了这段代码(它甚至要求输入用户名和密码!)。我在代码中输入了运行此代码的机器的IP地址和9191端口。我还设置了“将此代理服务器用于所有协议”。我在本地和VPS上运行了此代码 - 在两种情况下都能正常工作!

您可以使用curl测试您的NodeJS代理:

curl -x http://username:password@127.0.0.1:9191 https://www.google.com/

1
你是什么意思?你想知道目标查询参数还是什么? - Alexey Volodko
3
仅适用于HTTP。对于HTTPS不可能,因为它是“加密科学”!在这种情况下,您的NodeJS代理将成为“中间人”。而“加密科学”主要用于防御浏览器和服务器连接中任何“中间人”攻击... - Alexey Volodko
这太棒了,Алексей,我可以问一下你是在哪里学习如何编写代理的吗?我对将其扩展为正向代理很感兴趣。 - JasonS
我只是想在VPS上拥有自己的NodeJS代理服务器。所以我阅读了一些互联网文章,发现在“纯”的NodeJS中没有这样的解决方案。这段代码基于我六年的前端开发经验 - 这就是全部,我不是很了解NodeJS)) - Alexey Volodko
pipe 选项中为什么要设置 {end: false} - Qwertiy
显示剩余5条评论

9
我用 http-proxy 模块创造了一个 http/https 代理:https://gist.github.com/ncthis/6863947 现在的代码如下:
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);
  });

1
链接到gist已经过期。我已将其更新为https://gist.github.com/ncthis/6863947。 - david
1
@david 又过期了 - fidev
1
httpProxy.HttpProxy is not a function - Aero Wang
你如何添加 SSL 证书? - user1034912

2

node-http-proxy文档中包含了示例。 在https://github.com/nodejitsu/node-http-proxy查找“从HTTPS代理到HTTPS”的内容。在每个浏览器中,配置过程略有不同。 一些浏览器可以使用您的代理设置来处理所有协议; 其他浏览器需要单独配置SSL代理。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接