公司内的网络有点搞笑,需要配置pac脚本才能访问外网,但是除了浏览器之外的软件,大部分都不支持pac脚本的代理,也就一部分软件支持使用IE的代理,幸免遇难。
平时也就忍了,前段时间想装个archlinux的虚拟机,发现连pacman都用不了,google了很久也没有找到一个代理软件支持pac脚本的,于是乎想到用nodejs写一个,因为pac脚本本身是js的,所以实现起来应该比较方便。
首先认识一下pac脚本吧:http://en.wikipedia.org/wiki/Proxy_auto-config
看来真正的代理服务器我们不用去实现了,我们仅需要实现一个能够根据用户请求的网址交给pac脚本去计算代理服务器地址和端口,然后建立隧道(Tunnel)就可以了。
用nodejs建立隧道的代码很简单
net.createServer(function (clientSocket){ clientSocket.once('data', function (firstChunk){ // 解析http协议头, 分析出请求的url var url = /[A-Z]+\s+([^\s]+)\s+HTTP/.exec(firstChunk)[1]; if (url.indexOf('//') === -1) { // https协议交给pac脚本会得到错误的端口. url = 'http://' + url; } // 这个异步调用是在使用pac脚本计算应该使用哪个代理. getProxyHostAndPort(url, function (hostAndPort){ var serverSocket = net.connect(hostAndPort.port, hostAndPort.host, function() { clientSocket.pipe(serverSocket); serverSocket.write(firstChunk); serverSocket.pipe(clientSocket); serverSocket.on('end', function() { clientSocket.end(); }); }); }); }); }).listen(8088);
请注意:为了支持https,所以直接用net.createServer而不是http.createServer。
现在就只剩下解析pac脚本了,首先得拿到pac脚本的文件内容,这点用nodejs的http.get就可以了,这里就不介绍了。
我们假设已经拿到了pac脚本的代码,存在变量pacCode里,现在是时候把pac脚本里定义的FindProxyForURL函数导入到上下文了。
要用eval吗?等等,看看wiki里面的例子,FindProxyForURL里用到了两个未定义的函数shExpMatch还有isInNet,这两个函数我们得提供给pac脚本,不然在调用FindProxyForURL的时候会报错的.
我们先根据他们的用法猜猜他们是干嘛用的:shExpMatch是做shellExp的匹配测试,而isInNet是计算一个ip是否在一个网段(用掩码表示)内。
那就实现这两个函数
var shExpMatch = function (){ var _map = { '.': '\\.', '*': '.*?', '?': '.' }; var _rep = function (m){ return _map[m] }; return function (text, exp){ return new RegExp(exp.replace(/\.|\*|\?/g, _rep)).test(text); }; }(); var isInNet = function (){ function convert_addr(ipchars) { var bytes = ipchars.split('.'); return ((bytes[0] & 0xff) << 24) | ((bytes[1] & 0xff) << 16) | ((bytes[2] & 0xff) << 8) | (bytes[3] & 0xff); } return function (ipaddr, pattern, maskstr) { var match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(ipaddr); if (match[1] > 255 || match[2] > 255 || match[3] > 255 || match[4] > 255) { return false; // not an IP address } var host = convert_addr(ipaddr); var pat = convert_addr(pattern); var mask = convert_addr(maskstr); return ((host & mask) == (pat & mask)); }; }();
现在可以继续了
完成getProxyHostAndPort这个函数(我们采用new Function的方式避免使用eval,使用eval对js压缩工具不友好)
var fnPac = new Function('shExpMatch', 'isInNet', pacCode + ';return FindProxyForURL;')(shExpMatch, isInNet); function getProxyHostAndPort(url, callback){ var hostAndPort = parseHostAndPort(url); var str = fnPac(url, hostAndPort.host); var p = str.split(/\s*;\s*/g)[0]; if (p.indexOf('PROXY') !== -1) { var m = /PROXY\s*([^:]+)(?::(\d+))?/.exec(p); callback({ host: m[1], port: Number(m[2]) || 8080 }); } else { callback(hostAndPort); } }
基本上可以宣告结束了,不过让我郁闷的是pac脚本里还支持一个函数叫dnsResolve,而且公司里的pac脚本也用到了。
这个函数是个同步的,但是nodejs里提供的dns.resolve接口是异步的,这咋整?
解决的办法比较恶心,大家有兴趣的话就看源代码吧:
https://github.com/hackwaly/http-proxy-pac/blob/master/proxy.js
顺带说一句,这个源代码也是通过这个代理用git给push上去的哟!