├── .gitignore ├── History.md ├── README.md ├── boot.js ├── debug.js ├── dns.js ├── gfw.js ├── http.js ├── list ├── hosts.whitelist ├── pac.blacklist └── pac.whitelist ├── package.json ├── proto ├── direct.js ├── http.js ├── index.js ├── shadow.js └── socks5.js ├── server.js ├── shadow.js ├── socks5.js ├── tmpl ├── pac.tmpl └── server.tmpl └── wpad.js /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | node_modules 4 | 5 | lib-cov 6 | *.seed 7 | *.log 8 | *.csv 9 | *.dat 10 | *.out 11 | *.pid 12 | *.gz 13 | 14 | pids 15 | logs 16 | results 17 | 18 | npm-debug.log 19 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | ##0.0.3 / 2012-01-23 2 | * added ip level block detect and feedback 3 | * fix minor logic fails 4 | 5 | ##0.0.2 / 2012-01-10 6 | * fixed #1,#2,#3,#4,#5 7 | * dynamic conn reset check 8 | * use tcp on dns for clean resolve 9 | * added black/white list check 10 | * use all-proxy strategy enables proxy retry 11 | 12 | ##0.0.1 / 2013-01-08 13 | * Initial release 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pobi Project 2 | 3 | Diagram 4 | ------- 5 | ``` 6 | +---------------LOCAL----------------+ +--WORKER--+ 7 | | | | | 8 | +-------+ | +---+ +----+ +-----+ +------+ | +---+ | +------+ | 9 | |Browser| --DNS--> |DNS| |WPAD| |HTTP | |SOCKS5| | |GFW| | |SERVER| | 10 | | | <------- | | | | |PROXY| |PROXY | | | | | | | | 11 | |chrome | | +---+ | | | | | | | | | | | | | 12 | |safari | --WPAD PAC----> | | | | | | | | | | | | | 13 | |firefox| <-------------- | | | | | | | | | | | | | 14 | |opera | | +----+ | | | | | | | | | | | 15 | |ie | --HTTP PROXY----------> | | -> | | --ENCODED-> | | | 16 | |... | <---------------------- | | <- | | <-ENCODED-- | | | 17 | +-------+ | +-----+ | | | | | | | | | 18 | | | | | | | | | | | 19 | +-------+ | | | | | | | | | | 20 | |Tools | --SOCKS5 PROXY-------------------> | | --ENCODED-> | | | 21 | |curl | <--------------------------------> | | <-ENCODED-- | | | 22 | +-------+ | +------+ | +---+ | +------+ | 23 | +------------------------------------+ +----------+ 24 | ``` 25 | 26 | Install & Run & Config 27 | ---------------------- 28 | 29 | ### Install node.js (http://nodejs.org/download/) 30 | 31 | ### Install pobi (Linux/MacOSX/Windows) 32 | 33 | ``` 34 | npm -g install https://github.com/jackyz/pobi/tarball/master 35 | ``` 36 | 37 | ### Run (Linux/MacOSX) 38 | ``` 39 | ## Local DNS is 192.168.1.1 using shadowsocks secure link 40 | DEBUG=* sudo npm -g start pobi --lodns=udp://192.168.1.1:53 --worker=shadow://pass@1.2.3.4:9876 41 | ## Local DNS is 192.168.1.1 using socks5 secure link 42 | DEBUG=* sudo npm -g start pobi --lodns=udp://192.168.1.1:53 --worker=socks5://127.0.0.1:1070 43 | ``` 44 | 45 | ### Run (Windows) 46 | ``` 47 | ## Local DNS is 192.168.1.1 using shadowsocks secure link 48 | set DEBUG=* && npm -g start pobi --lodns=udp://192.168.1.1:53 --worker=shadow://pass@1.2.3.4:9876 49 | ## Local DNS is 192.168.1.1 using socks5 secure link 50 | set DEBUG=* && npm -g start pobi --lodns=udp://192.168.1.1:53 --worker=socks5://127.0.0.1:1070 51 | ``` 52 | 53 | ### Config 54 | 55 | Point your DNS to ip address that running pobi project. You can set on your own NIC(works for you only) or on your router(works for your local network). 56 | 57 | Enable browser's 'Auto Proxy Configure'(works for macosx/linux/ios), or set 'Auto Proxy Configure url' as http://wpad/wpad.dat (works for windows). 58 | 59 | * In IE: `Tool` - `Internet Options` - `Connection` - `Lan` - `Use AutoConfig` - Address:`http://wpad/wpad.dat` 60 | * In Safari: `Preference` - `Advanced` - `Proxies` - `Auto Proxy Discovery` 61 | * In Firefox: `Preference` - `Advanced` - `Network` - `Settings` - `Auto-detect proxy setting for this network` 62 | 63 | You have done, Enjoy. 64 | 65 | Upgrade 66 | ------- 67 | 68 | Of course we need upgrade, you can do it easy. 69 | ``` 70 | npm -g remove pobi 71 | npm -g install https://github.com/jackyz/pobi/tarball/master 72 | ``` 73 | 74 | Thanks 75 | ------ 76 | 77 | * Of course, __The Party__, __The Country__ and __The G.F.W.__ must be first ;) 78 | * XiXiang project: http://code.google.com/p/scholarzhang 79 | * AutoProxyGFWList: http://code.google.com/p/autoproxy-gfwlist 80 | * AutoProxy2Pac: http://autoproxy2pac.appspot.com 81 | * GFWWhiteList: https://github.com/n0wa11/gfw_whitelist 82 | * Inspired By Clowwindy's ShadowSocks: https://github.com/clowwindy/shadowsocks-nodejs 83 | * Name was Inspired by Liu Cixin's science fiction `The Three Body Trilogy` (aka. `SanTi`) part II 84 | -------------------------------------------------------------------------------- /boot.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var url = require('url'); 3 | 4 | var timeout = 2000; // 2' 5 | 6 | function encode(str){ 7 | // todo return "AB + AB XOR BASE64(PATH)" 8 | return str; 9 | } 10 | 11 | function load(surl, cb){ 12 | var options = url.parse(surl); 13 | var req = http.request({ 14 | hostname: options.hostname, 15 | port: 80, 16 | method:'GET', 17 | path: '/', 18 | agent: false, 19 | headers: { 20 | 'Content-Type':'image/jpeg', 21 | 'Cookie': encode(url.path) 22 | } 23 | }, function(res){ 24 | if (res.statusCode != 200) return cb(res.statusCode); 25 | var buf = [], len = 0; 26 | res.on('data', function(d){ 27 | buf.push(d); 28 | len += d.length; 29 | }); 30 | res.on('end', function(){ 31 | var c = Buffer.concat(buf, len).toString(); 32 | cb(null, c); 33 | }); 34 | }); 35 | req.on('error', function(e){ 36 | cb(e); 37 | }); 38 | // req.setTimeout(timeout); 39 | req.setTimeout(timeout, function(){ 40 | req.abort(); 41 | // cb('ETIMEOUT'); 42 | }); 43 | req.end(); 44 | } 45 | 46 | function try_upstreams(upstreams, system, token, cb){ 47 | if (upstreams.length == 0) { 48 | cb("all_upstreams_failed"); 49 | } else { 50 | var upstream = upstreams[0]; 51 | var url = 'http://'+upstream+'/'+system+'?token='+token; 52 | console.log('try upstream %s ...', upstream); 53 | load(url, function(e,c){ 54 | if (e) { 55 | console.log('upstream %s failed.', upstream); 56 | upstreams.shift(); 57 | try_upstreams(upstreams, system, token, cb); 58 | } else { 59 | console.log('upstream %s hit.', upstream); 60 | cb(null, c); 61 | } 62 | }); 63 | } 64 | } 65 | 66 | function get_local(){ 67 | // todo get ip address of local machine 68 | return '0.0.0.0'; 69 | } 70 | 71 | var app = null; 72 | 73 | function start(){ 74 | // align params 75 | var args = process.argv.slice(0); // clone level 1 76 | args.shift(); args.shift(); 77 | var system = args.shift(); 78 | var s = args.shift().split('@'); 79 | var token = s[0]; 80 | var host = s[1] || get_local(); 81 | var upstreams = args; 82 | // try upstream 83 | try_upstreams(upstreams, system, token, function(e,c){ 84 | if(e) { 85 | console.error('all upstreams fails. reactive please.'); 86 | process.exit(1); 87 | } 88 | console.log("start...\n%s", c); 89 | // app = eval('((function(){'+c+'})())'); 90 | // app.start(host, function(){ 91 | // console.log('started'); 92 | // }); 93 | }); 94 | // restart automatically to prevent too long connection 95 | var hour = 3600000; // 1 hour 96 | setInterval(restart, (system == 'worker') ? hour * 24 : hour); 97 | } 98 | 99 | function restart(){ 100 | console.log("shutdown..."); 101 | app.stop(function(){ 102 | start(); 103 | }); 104 | } 105 | 106 | if(!module.parent){ 107 | start(); 108 | } 109 | 110 | // node boot system token@host up1 up2 up3 111 | 112 | // * starts local(dns/wpad/http/socks5) on 192.168.1.3 for share 113 | // node boot local 12345@192.168.1.3 1.2.3.4 5.6.7.8 114 | 115 | // * starts worker on 202.106.107.108 for locals 116 | // node boot worker 12345@202.106.107.108 1.2.3.4 5.6.7.8 117 | 118 | // * starts helper on 202.106.107.108 for workers 119 | // node boot helper 12345@202.106.107.108 1.2.3.4 5.6.7.8 120 | -------------------------------------------------------------------------------- /debug.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var tty = require('tty'); 7 | 8 | /** 9 | * Expose `debug()` as the module. 10 | */ 11 | 12 | module.exports = debug; 13 | 14 | /** 15 | * Enabled debuggers. 16 | */ 17 | 18 | var names = [] 19 | , skips = []; 20 | 21 | (process.env.DEBUG || '') 22 | .split(/[\s,]+/) 23 | .forEach(function(name){ 24 | name = name.replace('*', '.*?'); 25 | if (name[0] === '-') { 26 | skips.push(new RegExp('^' + name.substr(1) + '$')); 27 | } else { 28 | names.push(new RegExp('^' + name + '$')); 29 | } 30 | }); 31 | 32 | /** 33 | * Colors. 34 | */ 35 | 36 | var colors = [6, 2, 3, 4, 5, 1]; 37 | 38 | /** 39 | * Previous debug() call. 40 | */ 41 | 42 | var prev = {}; 43 | 44 | /** 45 | * Previously assigned color. 46 | */ 47 | 48 | var prevColor = 0; 49 | 50 | /** 51 | * Is stdout a TTY? Colored output is disabled when `true`. 52 | */ 53 | 54 | var isatty = tty.isatty(2); 55 | 56 | /** 57 | * Select a color. 58 | * 59 | * @return {Number} 60 | * @api private 61 | */ 62 | 63 | function color() { 64 | return colors[prevColor++ % colors.length]; 65 | } 66 | 67 | /** 68 | * Humanize the given `ms`. 69 | * 70 | * @param {Number} m 71 | * @return {String} 72 | * @api private 73 | */ 74 | 75 | function humanize(ms) { 76 | var sec = 1000 77 | , min = 60 * 1000 78 | , hour = 60 * min; 79 | 80 | if (ms >= hour) return (ms / hour).toFixed(1) + 'h'; 81 | if (ms >= min) return (ms / min).toFixed(1) + 'm'; 82 | if (ms >= sec) return (ms / sec | 0) + 's'; 83 | return ms + 'ms'; 84 | } 85 | 86 | /** 87 | * Create a debugger with the given `name`. 88 | * 89 | * @param {String} name 90 | * @return {Type} 91 | * @api public 92 | */ 93 | 94 | function debug(name) { 95 | function disabled(){} 96 | disabled.enabled = false; 97 | 98 | var match = skips.some(function(re){ 99 | return re.test(name); 100 | }); 101 | 102 | if (match) return disabled; 103 | 104 | match = names.some(function(re){ 105 | return re.test(name); 106 | }); 107 | 108 | if (!match) return disabled; 109 | var c = color(); 110 | 111 | function colored(fmt) { 112 | var curr = new Date; 113 | var ms = curr - (prev[name] || curr); 114 | prev[name] = curr; 115 | 116 | fmt = ' \033[9' + c + 'm' + name + ' ' 117 | + '\033[3' + c + 'm\033[90m' 118 | + fmt + '\033[3' + c + 'm' 119 | + ' +' + humanize(ms) + '\033[0m'; 120 | 121 | console.error.apply(this, arguments); 122 | } 123 | 124 | function plain(fmt) { 125 | fmt = new Date().toUTCString() 126 | + ' ' + name + ' ' + fmt; 127 | console.error.apply(this, arguments); 128 | } 129 | 130 | colored.enabled = plain.enabled = true; 131 | 132 | return isatty 133 | ? colored 134 | : plain; 135 | } 136 | -------------------------------------------------------------------------------- /dns.js: -------------------------------------------------------------------------------- 1 | var url = require('url') 2 | , ndns = require('native-dns') 3 | , gfw = require('./gfw') 4 | , debug = require('./debug')('DNS'); 5 | 6 | // ---- 7 | 8 | var wpad_domain = 'wpad'; 9 | 10 | var TIMEOUT = 2000; // 2 second 11 | 12 | // ---- 13 | 14 | function serve_wpad(req, res){ 15 | var self = this; 16 | var dn = req.question[0].name; 17 | res.answer.push(ndns.A({ 18 | name:wpad_domain, address:self.wpad, ttl:600 19 | })); 20 | res.send(); 21 | debug("%s: Query [WPAD] %j -> %j", req.ip, dn, [self.wpad]); 22 | } 23 | 24 | // if not name resolve, use lodns direct 25 | function proxy_query(req, res){ 26 | var self = this; 27 | var q = req.question[0]; 28 | var dn = q.name; 29 | var color = 'white'; 30 | _query.call(self, color, req, function(e,r){ 31 | if (e){ 32 | debug("%s: Query [%s] %j FAIL %s", req.ip, color, dn, e.code); 33 | } else { 34 | res.answer = r; 35 | } 36 | res.send(); 37 | }); 38 | } 39 | 40 | function serve_query(req, res){ 41 | var self = this; 42 | var q = req.question[0]; 43 | var dn = q.name; 44 | var color = (q.type==1 && q.class==1) ? gfw.identifyDomain(dn) : 'white'; 45 | _serve_query.call(self, color, req, res); 46 | } 47 | 48 | function _serve_query(color, req, res){ 49 | var self = this; 50 | var q = req.question[0]; 51 | var dn = q.name; 52 | _query.call(self, color, req, function(e,r){ 53 | if (e) { 54 | if ((color == 'gray' || color == 'white') && e.code == 'ETIMEOUT') { 55 | debug("%s: Query [%s] %j TIMEOUT RETRY", req.ip, color, dn); 56 | req.retry++; 57 | _serve_query.call(self, 'black', req, res); 58 | } else { 59 | gfw.identifyDomain(dn, 'fail'); 60 | debug("%s: Query [%s] %j FAIL %s", req.ip, color, dn, e.code); 61 | res.send(); 62 | } 63 | } else { 64 | var r2 = filterFails(dn, r); 65 | if (r2.length){ 66 | debug("%s: Query [%s] %j OK", req.ip, color, dn); 67 | // retry connect ok, confirm the color 68 | gfw.identifyDomain(dn, (color != 'black') ? 'white' : 'black'); 69 | res.answer = r; 70 | res.send(); 71 | } else if ((color == 'gray' || color == 'white')) { 72 | // try black 73 | debug("%s: Query [%s] %j GFWED RETRY", req.ip, color, dn); 74 | req.retry++; 75 | _serve_query.call(self, 'black', req, res); 76 | } else { 77 | // it's too hard, need more carefully 78 | // gfw.identifyDomain(dn, 'fail'); 79 | debug("%s: Query [%s] %j GFWED FAIL", req.ip, color, dn); 80 | res.send(); 81 | } 82 | } 83 | }); 84 | } 85 | 86 | function _query(color, req, callback){ 87 | if (color == 'fail') return callback(null, []); 88 | var self = this; 89 | var q = req.question[0]; 90 | var dn = q.name; 91 | var uerr = null; 92 | var result = []; 93 | var ureq = ndns.Request({ 94 | question: q, 95 | server: (color == 'black') ? self.upstream : self.direct, 96 | timeout: TIMEOUT, 97 | cache: false 98 | }); 99 | function qEnd(e){ 100 | if (uerr) return; else uerr = e; // do not process error again 101 | if(!e) { 102 | callback(null, result); 103 | } else { 104 | callback(e, result); 105 | } 106 | } 107 | ureq.on('timeout', function(){ 108 | var e = new Error('timeout'); 109 | e.code = 'ETIMEOUT'; 110 | qEnd(e); 111 | }); 112 | ureq.on('error', function(e){ 113 | qEnd(e); 114 | }); 115 | ureq.on('message', function (e, r) { 116 | r.answer.forEach(function (a) { result.push(a); }); 117 | }); 118 | ureq.on('end', function () { 119 | qEnd(); 120 | }); 121 | ureq.send(); 122 | } 123 | 124 | function filterFails(domain, answers){ 125 | var r2 = []; 126 | for (var i=0; i", 6 | "dependencies": { 7 | "native-dns": ">= 0.4.0" 8 | }, 9 | "devDependencies": {}, 10 | "main": "index", 11 | "bin": {}, 12 | "scripts": {}, 13 | "engines": { 14 | "node": ">=0.8.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /proto/direct.js: -------------------------------------------------------------------------------- 1 | var net = require('net') 2 | , debug = require('../debug')('PROTO:DIRECT'); 3 | 4 | // ---- exports 5 | 6 | exports.init = function(options){ 7 | var socks = new net.Socket(); 8 | return socks; 9 | } 10 | -------------------------------------------------------------------------------- /proto/http.js: -------------------------------------------------------------------------------- 1 | var net = require('net') 2 | , http = require('http') 3 | , util = require('util') 4 | , stream = require('stream') 5 | , debug = require('../debug')('PROTO:HTTP'); 6 | 7 | var _host = config('proto', 'http', 'host') || '127.0.0.1'; 8 | var _port = config('proto', 'http', 'port') || 7070; 9 | 10 | // ---- http client interface 11 | 12 | function createConnection(){ // port,host,options 13 | var options = {}; 14 | 15 | if (typeof arguments[0] === 'object') { 16 | options = arguments[0]; 17 | } else if (typeof arguments[1] === 'object') { 18 | options = arguments[1]; 19 | options.port = arguments[0]; 20 | } else if (typeof arguments[2] === 'object') { 21 | options = arguments[2]; 22 | options.port = arguments[0]; 23 | options.host = arguments[1]; 24 | } else { 25 | if (typeof arguments[0] === 'number') { 26 | options.port = arguments[0]; 27 | } 28 | if (typeof arguments[1] === 'string') { 29 | options.host = arguments[1]; 30 | } 31 | } 32 | var socks = new HttpClientSocket(_host, _port); 33 | return socks.connect(options.port, options.host); 34 | }; 35 | 36 | exports.createConnection = createConnection; 37 | 38 | // ---- http client implement 39 | 40 | function HttpClientSocket(socks_host, socks_port) { 41 | stream.Stream.call(this); 42 | 43 | this.socket = null; // not init yet 44 | this.socks_host = socks_host; 45 | this.socks_port = socks_port; 46 | } 47 | util.inherits(HttpClientSocket, stream.Stream); 48 | 49 | HttpClientSocket.prototype.setTimeout = function(msecs, callback) { 50 | this.socket.setTimeout(msecs, callback); 51 | }; 52 | 53 | HttpClientSocket.prototype.setNoDelay = function() { 54 | this.socket.setNoDelay(); 55 | }; 56 | 57 | HttpClientSocket.prototype.setKeepAlive = function(setting, msecs) { 58 | this.socket.setKeepAlive(setting, msecs); 59 | }; 60 | 61 | HttpClientSocket.prototype.address = function() { 62 | return this.socket.address(); 63 | }; 64 | 65 | HttpClientSocket.prototype.pause = function() { 66 | this.socket.pause(); 67 | }; 68 | 69 | HttpClientSocket.prototype.resume = function() { 70 | this.socket.resume(); 71 | }; 72 | 73 | HttpClientSocket.prototype.end = function(data, encoding) { 74 | return this.socket.end(data, encoding); 75 | }; 76 | 77 | HttpClientSocket.prototype.destroy = function(exception) { 78 | this.socket.destroy(exception); 79 | }; 80 | 81 | HttpClientSocket.prototype.destroySoon = function() { 82 | this.socket.destroySoon(); 83 | this.writable = false; // node's http library asserts writable to be false after destroySoon 84 | }; 85 | 86 | HttpClientSocket.prototype.setEncoding = function(encoding) { 87 | this.socket.setEncoding(encoding); 88 | }; 89 | 90 | HttpClientSocket.prototype.write = function(data, arg1, arg2) { 91 | return this.socket.write(data, arg1, arg2); 92 | }; 93 | 94 | HttpClientSocket.prototype.connect = function(port, host) { 95 | var self = this; 96 | var req = http.request({ 97 | port: self._port, 98 | hostname: self._host, 99 | method: 'CONNECT', 100 | path: host+':'+port 101 | }); 102 | req.end(); 103 | req.on('connect', function(res, socket, head){ 104 | self.socket = socket; 105 | self.establish_connection(); 106 | }); 107 | req.on('timeout', function(){ 108 | self.emit('timeout'); 109 | }); 110 | req.on('error', function(e){ 111 | self.emit('error',e); 112 | }); 113 | return self; 114 | }; 115 | 116 | HttpClientSocket.prototype.establish_connection = function() { 117 | var self = this; 118 | 119 | self.socket.on('data', function(data) { 120 | self.emit('data', data); 121 | }); 122 | self.socket.on('close', function(had_error) { 123 | self.emit('close', had_error); 124 | }); 125 | self.socket.on('end', function() { 126 | self.emit('end'); 127 | }); 128 | self.socket.on('error', function(error) { 129 | self.emit('error', error); 130 | }); 131 | 132 | self.socket._httpMessage = self._httpMessage; 133 | self.socket.parser = self.parser; 134 | self.socket.ondata = self.ondata; 135 | self.writable = true; 136 | self.emit('connect'); 137 | }; 138 | -------------------------------------------------------------------------------- /proto/index.js: -------------------------------------------------------------------------------- 1 | var url = require('url') 2 | , util = require('util') 3 | , http = require('http') 4 | , debug = require('../debug')('PROTO'); 5 | 6 | // ---- upstream socket 7 | 8 | var upstreams = {}; 9 | 10 | module.exports = function(config){ 11 | var protocol = config.split(':')[0] || 'direct'; 12 | var u = upstreams[config]; 13 | if (u === undefined) { 14 | // debug('init upstream %j', config); 15 | // ---- createConnection 16 | function createConnection(){ 17 | // debug("init"); 18 | var sock = require('./'+protocol).init(config); 19 | // debug("init"); 20 | var options = {}; 21 | if (typeof arguments[0] === 'object') { 22 | options = arguments[0]; 23 | } else if (typeof arguments[1] === 'object') { 24 | options = arguments[1]; 25 | options.port = arguments[0]; 26 | } else if (typeof arguments[2] === 'object') { 27 | options = arguments[2]; 28 | options.port = arguments[0]; 29 | options.host = arguments[1]; 30 | } else { 31 | if (typeof (arguments[0] - 0) === 'number') { 32 | options.port = (arguments[0] - 0); 33 | } 34 | if (typeof arguments[1] === 'string') { 35 | options.host = arguments[1]; 36 | } 37 | } 38 | // debug("CONNECT %j %s:%s", arguments, options.host, options.port); 39 | return sock.connect(options.port, options.host); 40 | } 41 | // ---- agent 42 | function Agent(options) { 43 | http.Agent.call(this, options); 44 | this.createConnection = createConnection; 45 | } 46 | util.inherits(Agent, http.Agent); 47 | // Agent.prototype.maxSockets = 32; 48 | // var agent = new Agent({maxSockets:32}); 49 | var agent = new Agent(); 50 | agent.maxSockets = 32; 51 | // ---- exports 52 | u = { 53 | config: config, 54 | createConnection: createConnection, 55 | agent: agent 56 | }; 57 | upstreams[config] = u; 58 | } 59 | return u; 60 | } 61 | -------------------------------------------------------------------------------- /proto/shadow.js: -------------------------------------------------------------------------------- 1 | var net = require('net') 2 | , url = require('url') 3 | , util = require('util') 4 | , stream = require('stream') 5 | , crypto = require('crypto') 6 | , socks5 = require('./socks5') 7 | , debug = require('../debug')('PROTO:SHADOW'); 8 | 9 | // ---- exports 10 | 11 | exports.init = function(options){ 12 | var o = url.parse(options); 13 | var host = o.hostname || '127.0.0.1'; 14 | var port = o.port || 7070; 15 | var pass = o.auth || 'cool'; 16 | var socks = new ShadowSocks(host, port, pass); 17 | return socks; 18 | } 19 | 20 | exports.encodeAddress = socks5.encodeAddress; 21 | exports.decodeAddress = socks5.decodeAddress; 22 | 23 | exports.encode = encode; 24 | exports.decode = decode; 25 | 26 | // ---- shadow encode decode 27 | 28 | var Max = Math.pow(2,32); 29 | 30 | function merge_sort(array, comp){ 31 | 32 | function merge(left, right) { 33 | var result = new Array(); 34 | while ((left.length > 0) && (right.length > 0)) { 35 | if (comp(left[0], right[0]) <= 0) 36 | result.push(left.shift()); 37 | else 38 | result.push(right.shift()); 39 | } 40 | while (left.length > 0) result.push(left.shift()); 41 | while (right.length > 0) result.push(right.shift()); 42 | return result; 43 | } 44 | 45 | if (array.length < 2) return array; 46 | var middle = Math.ceil(array.length / 2); 47 | return merge( 48 | merge_sort(array.slice(0, middle), comp), 49 | merge_sort(array.slice(middle), comp) 50 | ); 51 | } 52 | 53 | function genTable(key){ // really slow, need cache 54 | var md5 = crypto.createHash('md5'); 55 | md5.update(key); 56 | var hash = new Buffer(md5.digest(), 'binary'); 57 | var al = hash.readUInt32LE(0); 58 | var ah = hash.readUInt32LE(4); 59 | var en_table = new Array(256); 60 | var de_table = new Array(256); 61 | for(var i=0; i<256; i++){ 62 | en_table[i] = i; 63 | } 64 | for(var i=1; i<1024; i++){ 65 | en_table = merge_sort(en_table, function(x,y){ 66 | return ((ah % (x + i)) * Max + al) % (x + i) - ((ah % (y + i)) * Max + al) % (y + i); 67 | }); 68 | } 69 | for(var i=0; i<256; i++){ 70 | de_table[en_table[i]] = i; 71 | } 72 | return [en_table, de_table]; 73 | } 74 | 75 | var _tables = {}; 76 | 77 | function getTable(key){ 78 | var t = _tables[key]; 79 | if (!t){ 80 | t = genTable(key); 81 | _tables[key] = t; 82 | } 83 | return t; 84 | } 85 | 86 | function mapTable(table, buf){ 87 | var buf1 = Buffer.isBuffer(buf) ? buf : new Buffer(buf); 88 | var buf2 = new Buffer(buf1.length); 89 | for(var i=0; i> 8 ); 227 | buffer.push( p & 0xff ); 228 | } 229 | 230 | var host = options.host; 231 | var port = options.port; 232 | var buffer = []; 233 | switch(net.isIP(host)) { 234 | case 0: 235 | buffer.push(0x03); 236 | parseDomainName(host, buffer); 237 | break; 238 | case 4: 239 | buffer.push(0x01); 240 | parseIPv4(host, buffer); 241 | break; 242 | case 6: 243 | buffer.push(0x04); 244 | parseIPv6(host, buffer); 245 | break; 246 | } 247 | parsePort(port, buffer); 248 | // debug("encodeAddress(%j):%j", options, new Buffer(buffer).toString('hex')); 249 | return buffer; 250 | } 251 | 252 | // buffer : the Buffer or Array 253 | // offset : the offset of address data. 3 for socks5 254 | // return : {host:ip, port:int, length:int} 255 | function decodeAddress(buffer, offset){ 256 | var host = ""; 257 | var host_len = 0; 258 | if (buffer[offset] == 0x01) { // ip v4 259 | host = util.format('%s.%s.%s.%s', buffer[offset+1], buffer[offset+2], buffer[offset+3], buffer[offset+4]); 260 | host_len = 4; 261 | } else if (buffer[offset] == 0x03) { // dns 262 | host = buffer.toString('utf8', offset+2, offset+2+buffer[offset+1]); 263 | host_len = buffer[offset+1]+1; 264 | } else if (buffer[offset] == 0x04) { // ip v6 265 | host = buffer.slice(buffer[offset+1], buffer[offset+1+16]); 266 | host_len = 16; 267 | } 268 | var portIndex = offset + 1 + host_len; 269 | var port = (buffer[portIndex] << 8) + buffer[portIndex+1]; 270 | var length = portIndex + 2; 271 | var result = {host:host, port:port, length:length}; 272 | // debug("decodeAddress(%s,%s):%j", buffer.toString('hex'), offset, result); 273 | return result; 274 | } 275 | 276 | function get_error_message(code) { 277 | switch(code) { 278 | case 1: 279 | return 'General SOCKS server failure'; 280 | case 2: 281 | return 'Connection not allowed by ruleset'; 282 | case 3: 283 | return 'Network unreachable'; 284 | case 4: 285 | return 'Host unreachable'; 286 | case 5: 287 | return 'Connection refused'; 288 | case 6: 289 | return 'TTL expired'; 290 | case 7: 291 | return 'Command not supported'; 292 | case 8: 293 | return 'Address type not supported'; 294 | default: 295 | return 'Unknown status code ' + code; 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | , path = require('path') 3 | , net = require('net') 4 | , d = require('domain').create() 5 | , debug = require('./debug')('APP') 6 | 7 | // ---- 8 | 9 | function getLocalIP(callback) { 10 | var socket = net.createConnection(53, '8.8.8.8'); 11 | socket.on('connect', function() { 12 | callback(undefined, socket.address().address); 13 | socket.destroy(); 14 | }); 15 | socket.on('error', function(e) { 16 | callback(e, 'error'); 17 | }); 18 | } 19 | 20 | var template = "/tmpl/server.tmpl"; 21 | 22 | function getConfig(ctx, callback) { 23 | try { 24 | var t = fs.readFileSync(path.dirname(__filename)+template, 'utf8'); 25 | var s = tmpl(t, ctx); 26 | var j = JSON.parse(s); 27 | callback(undefined, j); 28 | } catch(x) { 29 | callback(x); 30 | } 31 | } 32 | 33 | // ---- begin inline underscore template function 34 | 35 | // By default, Underscore uses ERB-style template delimiters, change the 36 | // following template settings to use alternative delimiters. 37 | var templateSettings = { 38 | evaluate : /<%([\s\S]+?)%>/g, 39 | interpolate : /<%=([\s\S]+?)%>/g, 40 | escape : /<%-([\s\S]+?)%>/g 41 | }; 42 | 43 | // When customizing `templateSettings`, if you don't want to define an 44 | // interpolation, evaluation or escaping regex, we need one that is 45 | // guaranteed not to match. 46 | var noMatch = /(.)^/; 47 | 48 | // Certain characters need to be escaped so that they can be put into a 49 | // string literal. 50 | var escapes = { 51 | "'": "'", 52 | '\\': '\\', 53 | '\r': 'r', 54 | '\n': 'n', 55 | '\t': 't', 56 | '\u2028': 'u2028', 57 | '\u2029': 'u2029' 58 | }; 59 | 60 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; 61 | 62 | // JavaScript micro-templating, similar to John Resig's implementation. 63 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 64 | // and correctly escapes quotes within interpolated code. 65 | var tmpl = function(text, data, settings) { 66 | settings = settings || templateSettings; 67 | 68 | // Combine delimiters into one regular expression via alternation. 69 | var matcher = new RegExp([ 70 | (settings.escape || noMatch).source, 71 | (settings.interpolate || noMatch).source, 72 | (settings.evaluate || noMatch).source 73 | ].join('|') + '|$', 'g'); 74 | 75 | // Compile the template source, escaping string literals appropriately. 76 | var index = 0; 77 | var source = "__p+='"; 78 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { 79 | source += text.slice(index, offset) 80 | .replace(escaper, function(match) { return '\\' + escapes[match]; }); 81 | 82 | if (escape) { 83 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 84 | } 85 | if (interpolate) { 86 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 87 | } 88 | if (evaluate) { 89 | source += "';\n" + evaluate + "\n__p+='"; 90 | } 91 | index = offset + match.length; 92 | return match; 93 | }); 94 | source += "';\n"; 95 | 96 | // If a variable is not specified, place data values in local scope. 97 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 98 | 99 | source = "var __t,__p='',__j=Array.prototype.join," + 100 | "print=function(){__p+=__j.call(arguments,'');};\n" + 101 | source + "return __p;\n"; 102 | 103 | try { 104 | var render = new Function(settings.variable || 'obj', source); 105 | } catch (e) { 106 | e.source = source; 107 | throw e; 108 | } 109 | 110 | if (data) return render(data); 111 | var template = function(data) { 112 | return render.call(this, data); 113 | }; 114 | 115 | // Provide the compiled function source as a convenience for precompilation. 116 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; 117 | 118 | return template; 119 | }; 120 | 121 | // ---- end inline underscore template function 122 | 123 | // ---- exports 124 | 125 | exports.tmpl = tmpl; 126 | 127 | // ---- main entry 128 | 129 | /* 130 | // node http leaks socket, bug 3536 131 | process.on('uncaughtException', function(e){ 132 | debug('UNCAUGHTEXCEPTION', e); 133 | }); 134 | */ 135 | 136 | // command 137 | // ** on local as a LOCAL (worker ip: 1.2.3.4) 138 | 139 | // start local app for shadowsocks / socks5 / vpn 140 | // npm -g start --lodns=udp://192.168.1.1:53 --worker=shadow://pas@1.2.3.4:5678 141 | // npm -g start --lodns=udp://192.168.1.1:53 --worker=socks5://192.168.1.3:7070 142 | // npm -g start --lodns=udp://192.168.1.1:53 --worker=vpn://192.168.5.1 143 | 144 | // ** on remote as a WORKER (self ip: 1.2.3.4) 145 | 146 | // npm -g start --app=worker --shadow=shadow://pass@0.0.0.0:5678 147 | // npm -g start --app=worker --socks5=socks5://0.0.0.0:7890 148 | // npm -g start --app=worker --vpn=vpn://192.168.5.2 149 | 150 | if (!module.parent) { 151 | 152 | var app = process.env.npm_config_app || 'local'; 153 | var lodns = process.env.npm_config_lodns; // || 'udp://192.168.1.1:53'; 154 | 155 | if ((lodns == undefined) && (app == 'local')) { 156 | console.log('It\'s not a bug, but local DNS is needed.'); 157 | console.log(' example: sudo npm start --lodns=udp://192.168.1.1:53'); 158 | console.log('Will run in remote DNS only, it\'s very slow.'); 159 | // console.log(''); 160 | // process.exit(0); 161 | } 162 | 163 | getLocalIP(function (error, localip) { 164 | if (error) return console.log('Not Online? error:', error); 165 | var ip = localip || '127.0.0.1'; 166 | var ctx = { 167 | local: { 168 | ip: ip, 169 | lodns: lodns || 'udp://8.8.8.8:53', 170 | worker: process.env.npm_config_worker || 'shadow://cool@'+ip+':1027' 171 | }, 172 | worker: { 173 | socks5: process.env.npm_config_socks5 || 'socks5://cool@'+ip+':1026', 174 | shadow: process.env.npm_config_shadow || 'shadow://cool@'+ip+':1027' 175 | } 176 | }; 177 | getConfig(ctx, function(error, conf){ 178 | if (error) return console.log('Config fail. error:', error); 179 | debug('starting %s on %s', app, localip); 180 | // the config parser 181 | function config(){ 182 | // debug("%j",conf); 183 | var args = Array.prototype.slice.call(arguments); 184 | var val = conf; 185 | for (var i=0; i %s:%s %s END OK", sock.remoteAddress, uhost, uport, us); 35 | } else { 36 | debug("%s -> %s:%s %s END FAIL %j", sock.remoteAddress, uhost, uport, us, e.code); 37 | } 38 | } 39 | function timeout(){ 40 | var e = new Error('timeout'); 41 | e.code = 'ETIMEOUT'; 42 | endup(e); 43 | } 44 | 45 | function onData(d){ 46 | if (stage == 0){ 47 | connect(d); 48 | } else if (stage == 1) { 49 | wait(d); 50 | } else if (stage == 2) { 51 | // connected , noop 52 | } else { 53 | var e = new Error('unknow stat'); 54 | e.code = 'UNKNOWN_USTAT'; 55 | debug(e); 56 | } 57 | } 58 | 59 | function connect(en){ 60 | stage = 1; // next stage is wait 61 | 62 | var d = shadow.decode(self.pass, en); 63 | // debug("READ %s", d.toString('hex')); 64 | var address = shadow.decodeAddress(d,0); 65 | uhost = address.host; 66 | uport = address.port; 67 | if(address.length < d.length) ubuff.push(d.slice(address.length)); 68 | 69 | // debug("%s -> %s:%s CON ING", sock.remoteAddress, uhost, uport); 70 | 71 | // sock.pause(); // hold data first, due not connected 72 | 73 | usock = self.upstream.createConnection(uport, uhost); 74 | usock.on('error', endup); 75 | usock.on('end', endup); 76 | usock.on('timeout', timeout); 77 | usock.setTimeout(CONTIMEOUT); 78 | 79 | usock.on('connect', function(){ 80 | uest = true; // est 81 | 82 | // debug("%s -> %s:%s EST BEGIN", sock.remoteAddress, uhost, uport); 83 | 84 | usock.setTimeout(ESTTIMEOUT); 85 | usock.setNoDelay(true); 86 | 87 | // flush the buff if any 88 | while(ubuff.length){ 89 | var d = ubuff.shift(); 90 | var r = usock.write(d); 91 | // debug('-> %s %s', d.toString('utf8'), r); 92 | } 93 | 94 | stage = 2; // next stage is noop 95 | // sock.resume(); 96 | // ** sock.pipe(usock); 97 | sock.on('data', function(en){ 98 | var d = shadow.decode(self.pass, en); 99 | var r = usock.write(d); 100 | if (!r) usock.pause(); 101 | // debug('-> %s %s', d.toString('utf8'), r); 102 | }); 103 | sock.on('end', function(){ usock.end(); }); 104 | sock.on('drain', function(){ usock.resume(); }); 105 | 106 | // ** usock.pipe(sock); 107 | usock.on('data', function(d){ 108 | var en = shadow.encode(self.pass, d); 109 | var r = sock.write(en); 110 | if(!r) usock.pause(); 111 | // debug('<- %s %s', d.toString('utf8'), r); 112 | }); 113 | usock.on('end', function(){ sock.end(); }); 114 | usock.on('drain', function(){ sock.resume(); }); 115 | /* 116 | usock.on('drain', function(){ 117 | sock.resume(); 118 | var d = ubuff.shift(); 119 | if (!d) { 120 | sock.resume(); 121 | } else { 122 | debug('-> %s', d.toString('utf8')); 123 | if (!usock.write(d)) sock.pause(); 124 | } 125 | }); 126 | usock.emit('drain'); 127 | */ 128 | }); 129 | } 130 | 131 | function wait(en){ 132 | // debug('%s WAIT CON', sock.remoteAddress); 133 | var d = shadow.decode(self.pass, en); 134 | // debug("READ %s", d.toString('hex')); 135 | ubuff.push(d); 136 | } 137 | 138 | sock.on('data', onData); 139 | sock.on('error', endup); 140 | sock.on('end', endup); 141 | sock.setTimeout(ESTTIMEOUT, timeout); 142 | sock.setNoDelay(true); 143 | } 144 | 145 | // ---- 146 | 147 | var server = null; 148 | 149 | function start(config){ 150 | var onListening = function(){ 151 | debug("listening on %j [%s] via %j", this.address(), this.pass, this.upstream.config); 152 | }; 153 | var onConnection = function(sock){ 154 | // debug("%s connect", sock.remoteAddress); 155 | serve.call(this, sock); 156 | }; 157 | var onClose = function(){ 158 | debug("closed %j", this.address()); 159 | }; 160 | var onError = function(err){ 161 | debug("error %j", err); 162 | }; 163 | 164 | // init 165 | server = net.createServer(); 166 | server.on('listening', onListening); 167 | server.on('connection', onConnection); 168 | server.on('close', onClose); 169 | server.on('error', onError); 170 | 171 | server.upstream = proto(config.upstream); 172 | 173 | var o = url.parse(config.listen); 174 | server.pass = o.auth || 'cool'; 175 | var host = o.hostname || '0.0.0.0'; 176 | var port = o.port || 1070; 177 | // 178 | server.listen(port, host); 179 | } 180 | exports.start = start; 181 | 182 | function stop(){ 183 | server.close(); 184 | } 185 | exports.stop = stop; 186 | -------------------------------------------------------------------------------- /socks5.js: -------------------------------------------------------------------------------- 1 | var url = require('url') 2 | , net = require('net') 3 | , debug = require('./debug')('SOCKS5') 4 | , proto = require('./proto') 5 | , socks5 = require('./proto/socks5'); 6 | 7 | // ---- timeout 8 | 9 | var CONTIMEOUT = 2000; // 2 second 10 | var ESTTIMEOUT = 4000; // 4 second 11 | 12 | // ---- 13 | 14 | function serve(sock){ 15 | 16 | // debug("connections:%s", self.connections); 17 | 18 | var self = this; 19 | var uhost = null; 20 | var uport = null; 21 | var usock = null; 22 | var uest = false; 23 | var uend = null; 24 | var stage = 0; 25 | var ubuff = []; 26 | 27 | function endup(e){ 28 | if (uend) return; else uend = e || true; // do not process error again 29 | try { usock.destroy(); } catch(x){ } 30 | // TODO retry other link 31 | if (uest) { try { sock.destroy(); } catch(x){ } } 32 | var us = uest ? 'EST' : 'CON'; 33 | if (!e) { 34 | // debug("%s -> %s:%s %s END OK",sock.remoteAddress, uhost, uport, us); 35 | } else { 36 | debug("%s -> %s:%s %s END FAIL %j", sock.remoteAddress, uhost, uport, us, e.code); 37 | } 38 | } 39 | function timeout(){ 40 | var e = new Error(); 41 | e.code = 'ETIMEOUT'; 42 | endup(e); 43 | } 44 | 45 | function onData(d){ 46 | if (stage == 0){ 47 | handshake(d); 48 | } else if (stage == 1) { 49 | command(d); 50 | } else if (stage == 2) { 51 | await(d); 52 | } else if (stage == 3) { 53 | // connected , noop 54 | } else { 55 | var e = new Error('unknow stat'); 56 | e.code = 'UNKNOWN_USTAT'; 57 | debug(e); 58 | } 59 | } 60 | 61 | function handshake(d){ 62 | // debug('%s HANDSHAKE', sock.remoteAddress); 63 | stage = 1; // next stage is command 64 | // todo check v5 65 | // todo auth 66 | sock.write(new Buffer([0x05, 0x00])); // socks5 noauth 67 | } 68 | 69 | function command(d){ 70 | // debug('%s COMMAND', sock.remoteAddress); 71 | // todo check v5 72 | var cmd = d[1]; 73 | if (cmd == 0x01) { // connect 74 | connect(d); 75 | //} else if (cmd == 0x02) { // bind 76 | //} else if (cmd == 0x03) { // udp associate 77 | } else { // unsupport 78 | sock.end(new Buffer([0x05,0x07,0x00,0x01])); 79 | var e = new Error('UNSUPPORT_CMD'); 80 | e.code = 'EUNKNOWN_CMD'; 81 | endup(e); 82 | } 83 | } 84 | 85 | function await(d){ 86 | // debug('%s AWAIT', sock.remoteAddress); 87 | ubuff.push(d); 88 | } 89 | 90 | function connect(d){ 91 | stage = 2; // next stage is await 92 | 93 | var address = socks5.decodeAddress(d,3); 94 | uhost = address.host; 95 | uport = address.port; 96 | if(address.length < d.length) ubuff.push(d.slice(address.length)); 97 | 98 | // debug("%s -> %s:%s CON ING", sock.remoteAddress, uhost, uport) 99 | 100 | usock = self.upstream.createConnection(uport, uhost); 101 | usock.on('error', endup); 102 | usock.on('end', endup); 103 | usock.setTimeout(CONTIMEOUT, timeout); 104 | 105 | usock.on('connect', function(){ 106 | uest = true; // est 107 | 108 | // debug("%s -> %s:%s EST BEGIN", sock.remoteAddress, uhost, uport); 109 | 110 | usock.setTimeout(ESTTIMEOUT, timeout); 111 | usock.setNoDelay(true); 112 | 113 | // flush the buff if any 114 | while(ubuff.length){ 115 | var da = ubuff.shift(); 116 | var r = usock.write(da); 117 | // debug('-> %s %s', da.toString('utf8'), r); 118 | } 119 | 120 | stage = 3; // next stage is noop 121 | sock.pipe(usock); 122 | var resp = new Buffer(d.length); 123 | d.copy(resp); 124 | resp[0] = 0x05; 125 | resp[1] = 0x00; 126 | resp[2] = 0x00; 127 | sock.write(resp); 128 | usock.pipe(sock); 129 | }); 130 | } 131 | 132 | sock.on('data', onData); 133 | sock.on('error', endup); 134 | sock.on('end', endup); 135 | sock.setTimeout(ESTTIMEOUT, timeout); 136 | sock.setNoDelay(true); 137 | } 138 | 139 | // ---- 140 | 141 | var server = null; 142 | 143 | function start(config){ 144 | var onListening = function(){ 145 | debug("listening on %s:%s", 146 | this.address().address, this.address().port); 147 | debug(" --upstream=%s", this.upstream.config); 148 | }; 149 | var onConnection = function(sock){ 150 | // debug("%s connect", sock.remoteAddress); 151 | serve.call(this, sock); 152 | }; 153 | var onClose = function(){ 154 | debug("closed %j", this.address()); 155 | }; 156 | var onError = function(err){ 157 | debug("error %j", err); 158 | }; 159 | 160 | // init 161 | server = net.createServer(); 162 | server.on('listening', onListening); 163 | server.on('connection', onConnection); 164 | server.on('close', onClose); 165 | server.on('error', onError); 166 | 167 | server.upstream = proto(config.upstream); 168 | 169 | var o = url.parse(config.listen); 170 | var host = o.hostname || '0.0.0.0'; 171 | var port = o.port || 7070; 172 | // 173 | server.listen(port, host); 174 | } 175 | exports.start = start; 176 | 177 | function stop(){ 178 | server.close(); 179 | } 180 | exports.stop = stop; 181 | -------------------------------------------------------------------------------- /tmpl/pac.tmpl: -------------------------------------------------------------------------------- 1 | /* 2 | * the proxy all strategy 3 | * let http proxy make decision of use or not use a proxy 4 | */ 5 | function FindProxyForURL(url, host) { 6 | return "<%=proxy%>"; 7 | } 8 | -------------------------------------------------------------------------------- /tmpl/server.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "local": { 3 | "gfw": { 4 | }, 5 | "dns": { 6 | "listen": "udp://0.0.0.0:53", 7 | "wpad": "<%=local.ip%>", 8 | "direct": "<%=local.lodns%>", 9 | "upstream": "tcp://8.8.8.8:53" 10 | }, 11 | "wpad": { 12 | "listen": "http://0.0.0.0:80", 13 | "proxy": "PROXY <%=local.ip%>:8080" 14 | }, 15 | "http": { 16 | "listen": "http://0.0.0.0:8080", 17 | "upstream": "socks5://127.0.0.1:7070" 18 | }, 19 | "socks5": { 20 | "listen": "tcp://0.0.0.0:7070", 21 | "upstream": "<%=local.worker%>" 22 | } 23 | }, 24 | "worker": { 25 | "shadow": { 26 | "listen": "<%=worker.shadow%>", 27 | "upstream": "direct://" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /wpad.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | , path = require('path') 3 | , url = require('url') 4 | , http = require('http') 5 | , gfw = require('./gfw') 6 | , server = require('./server') 7 | , debug = require('./debug')('WPAD'); 8 | 9 | // ---- 10 | 11 | var pacTmpl = '/tmpl/pac.tmpl'; // all-by-proxy pac template 12 | 13 | var wpad_path = '/wpad.da'; 14 | 15 | // ---- 16 | 17 | function serveWpad(req, res, cb){ 18 | var self = this; 19 | if (req.method == 'GET' && wpad_path == req.url.substr(0, wpad_path.length)){ 20 | res.writeHead(200, { 21 | 'Content-Type': 'application/x-ns-proxy-autoconfig; charset=UTF-8', 22 | 'Cache-Control': 'no-cache' 23 | }); 24 | res.write(self.pac); 25 | res.end(); 26 | debug('%s : %s %s ok', req.ip, req.method, req.url); 27 | cb(); 28 | } else if (req.method == 'GET' && req.url.substr(0,4) == '/gfw'){ 29 | var o = url.parse(req.url.substr(4), true); 30 | var code = 200; 31 | var data = ''; 32 | switch (o.pathname) { 33 | case '/identifyDomain' : 34 | var d = o.query.domain || undefined; 35 | var v = o.query.val || undefined; 36 | data = gfw.identifyDomain(d,v); 37 | break; 38 | case '/identifyIp' : 39 | var d = o.query.domain || undefined; 40 | var i = o.query.ip || undefined; 41 | var v = o.query.val || undefined; 42 | data = gfw.identifyIp(d,i,v); 43 | break; 44 | case '/identifyUrl' : 45 | var d = o.query.domain || undefined; 46 | var u = o.query.url || undefined; 47 | var v = o.query.val || undefined; 48 | data = gfw.identifyUrl(d,u,v); 49 | break; 50 | default : 51 | code = 404; 52 | } 53 | res.writeHead(code); 54 | res.end(data); 55 | cb(); 56 | } else { 57 | res.writeHead(404); 58 | res.end(); 59 | cb(404); 60 | } 61 | } 62 | 63 | // ---- 64 | 65 | var wpad = null; 66 | 67 | function start(config){ 68 | var onListening = function(){ 69 | debug("listening on %s:%s", 70 | this.address().address, this.address().port); 71 | }; 72 | var onRequest = function(req, res){ 73 | req.ip = req.connection.remoteAddress; 74 | serveWpad.call(this, req, res, function(e){ 75 | if (e) debug('%s : %s %s fail %j', req.ip, req.method, req.url, e); 76 | }); 77 | }; 78 | var onClose = function(){ 79 | debug("closed %j", this.address()); 80 | }; 81 | var onError = function(err){ 82 | debug("error %j", err); 83 | }; 84 | 85 | // init 86 | var proxy = config.proxy || "DIRECT"; 87 | var pac = server.tmpl(fs.readFileSync(path.dirname(__filename)+pacTmpl, 'utf8'), {proxy:proxy}); 88 | 89 | wpad = http.createServer(); 90 | wpad.on('listening', onListening); 91 | wpad.on('request', onRequest); 92 | wpad.on('close', onClose); 93 | wpad.on('error', onError); 94 | 95 | wpad.pac = pac; 96 | 97 | var o = url.parse(config.listen); 98 | var host = o.hostname || '0.0.0.0'; 99 | var port = o.port || 80; // wpad must on 80 100 | // 101 | wpad.listen(port, host); 102 | } 103 | exports.start = start; 104 | 105 | function stop(){ 106 | wpad.close(); 107 | } 108 | exports.stop = stop; 109 | --------------------------------------------------------------------------------