公司內的網絡有點搞笑,需要配置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上去的喲!