之前尝试用 nodejs 写过一个简单的 http 代理服务器,着重处理了 https 的代理资源请求,但一直不是很完善,今天通过学习 SNI 总算比较好的解决了这个问题。
问题
目前不少代理服务器对于 http 支持都已经非常好了,通过建立 http server 接收到请求后再请求真正的地址,结合 pipe 核心代码也不多。而对于 https 请求则需要有独立的处理逻辑。
简单处理
由于当通过 http 代理服务器访问 https 资源时会使用 http 隧道的方式,需要监听 http server 的 connect 事件,之后请求 https 服务器,在 https 代理服务器中可以访问到用户的原始请求信息,根据该信息再进行资源替换等操作,代码示例如下:
httpServer = createHttpServer(); httpsServer = createHttpsServer({ key: key, cert: cert }, 8888, function (req) { if (needProxy(req)) { processProxy(req, res); } }); httpServer.on('connect', function (req, socket) { socket.pipe(net.connect(8888)).pipe(socket); });
遇到的问题就是由于 https server 是用自己的证书建立的,返回内容后浏览器会显示警告界面通知用户当前服务器返回的证书信息和请求的网址不一致,不能像 fiddler 等软件一样直接显示内容。
动态生成证书
多 server 方案
根据浏览器的提示,可以根据请求网址动态生成对应的证书来解决这个问题,首先生成自签名的代理服务器证书,代理服务器根据请求网址的不同,调用 openssl(推荐工具模块 pem) 以及自身的证书来动态签署包含请求网址信息的证书,进而建立对应的 https server 来处理请求,代码示例如下:
httpServer.on('connect', function (req, socket) { generateCert(req.host, function () { createHttpsServer(function (port) { socket.pipe(net.connect(8888)).pipe(socket); }); }); });
这个方案基本上就是类似中间人攻击,浏览器自然也有解决方案,当请求后会返回证书无效的警告页面,可以通过把生成的代理服务器证书加入到浏览器受信任的根证书来解决。
另一个问题是会根据每个 https 请求的域名来建立一个动态的 https server,每个 https server 都会消耗代理服务器端的一个端口,而端口数则是有上限的(65535),虽然这个上限很不容易达到。
SNI 方案
类似问题其实很早就提出了,即在虚拟主机问题:如何能够像 http 一样在一台机器(一个 ip 地址)上部署多个不同 https 服务,解决这个问题的方案为 SNI,即证书可以根据请求域名动态提供,在请求到来时提供即可,nodejs 提供了两个 api
SNICallback 回调以及 addContext 方法。
那么就不需要根据不同的证书创建多个 https server,只需要创建一个 server 即可,只不过这个 server 会跟据请求域名动态得取不同的证书信息,代码示例如下:
function generatePKI(host) { var defer = Q.defer(); pkiPromises[host] = defer.promise; generateCert(host, function (option) { httpsServer.addContext(host, option); defer.resolve(); }); } httpServer.on('connect', function (req, socket) { if (req.url.match(/:443$/)) { var host = req.url.substring(0, req.url.length - 4); var promise; if (promise = pkiPromises[host]) {} else { generatePKI(host); promise = pkiPromises[host] } promise.then(function () { var mediator = net.connect(httpsPort); mediator.on('connect', function () { socket.write("HTTP/1.1 200 Connection established\r\n\r\n"); }); socket.pipe(mediator).pipe(socket); }); } });
在请求到来时通过通过动态生成证书并调用 addContext 来关联域名和证书信息,
另外通过 promise 解决了并发请求的排队问题,在请求到来时,如果证书还没生成,就排队到 promise 上,生成证书后当前的排队请求以及后来的新请求就可以立即处理了。
缺点则是 windows xp 上的 ie 不支持 SNI,此方案无效,不过 xp 已不被微软支持,这个问题应该可以忽略了