UNPKG

forward-proxy

Version:

p2p http forward and socks proxy based on AppNet.io and Node.js

923 lines (787 loc) 64.3 kB
// Copyright (c) 2013 Tom Zhou<appnet.link@gmail.com> var eventEmitter = require('events').EventEmitter, util = require('util'), WEBPP = require('appnet.io'), SEP = WEBPP.SEP, vURL = WEBPP.vURL, URL = require('url'), NET = require('net'), UDT = require('udt'), httpps = require('httpps'), filter = require('./filter'); OS = require('os'); // for network interface check; // helpers function isLocalhost(host){ return ((host === 'localhost') || (host === '127.0.0.1') || (host === '0:0:0:0:0:0:0:1') || (host === '::1')); } function isLocalintf(host){ var intfs = OS.networkInterfaces(); var yes = false; for (var k in intfs) for (var kk in intfs[k]) if ((intfs[k])[kk].address && ((intfs[k])[kk].address === host)) yes = true; return yes; } // Debug level var Debug = 0; // Proxy class // a proxy will contain one appnet.io name-client // - options: user custom parameters, like {secmode: ..., usrkey: ..., domain: ..., endpoints: ..., turn: ...} // - options.secmode: ssl, enable ssl/https; acl, enable ssl/https,host-based ACL // - options.export: Forward-proxy's Export service vURL // - options.access_local: Enable local access on Export host, 1: enable, 0: disable, default disable it // - fn: callback to pass proxy informations var Proxy = module.exports = function(options, fn){ var self = this; if (!(this instanceof Proxy)) return new Proxy(options, fn); // super constructor eventEmitter.call(self); if (typeof options == 'function') { fn = options; options = {}; } // check arguments self.access_local = options.access_local || 0; // 0. // export proxy cache self.exportCache = {}; // 0.1 // fill dedicated export service vURL if (options && options.export) { self.exportCache[options.export] = {vurl: options.export}; } // 1. // create name client var nmcln = self.nmcln = new WEBPP({ usrinfo: { domain: (options && options.domain) || '51dese.com', usrkey: (options && options.usrkey) || ('forward-proxy@'+Date.now()) }, srvinfo: { timeout: 20, endpoints: (options && options.endpoints) || [ {ip: '51dese.com', port: 51686}, {ip: '51dese.com', port: 51868} ], turn: (options && options.turn) || [ {ip: '51dese.com', agent: 51866, proxy: 51688} ] }, // vURL mode: vpath-based vmode: vURL.URL_MODE_PATH, // secure mode secmode: (options && options.secmode === 'ssl') ? SEP.SEP_SEC_SSL : SEP.SEP_SEC_SSL_ACL_HOST, // ssl mode sslmode: (options && options.sslmode === 'both') ? SEP.SEP_SSL_AUTH_SRV_CLNT : SEP.SEP_SSL_AUTH_SRV_ONLY }); // 2. // check ready nmcln.once('ready', function(){ // 3. // export http proxy // TBD... admin portal page function exportHttpProxy(req, res){ res.writeHead(400); res.end('TBD... admin portal page'); console.error('TBD... admin portal page'); } // 3.1 // export http tunnel function exportHttpTunnel(req, socket, head){ // 1. // find next hop in case middle relay using turn-forward-to headers var middle = req.headers && req.headers['turn-forward-to']; if (middle) { var relays = middle.split(','); var nxstep = relays[0]; // 1.1 // break loops var loop = false; var mine = nmcln.vurl.match(vURL.regex_vboth); for (var idx = 0; idx < relays.length; idx ++) if (mine === (relays[idx]).match(vURL.regex_vboth)) { loop = true; break; } if (loop) { // stop on loop socket.end('stop on loop'); console.error('stop on loop:'+nmcln.vurl); return; } // 1.2 // check on vURL var vstrs, vurle; if (vstrs = nxstep.match(vURL.regex_vboth)) { vurle = vstrs[0]; // 2. // get peer info by vURL nmcln.getvURLInfo(vurle, function(err, routing){ // 2.1 // check error and authentication if (err || !routing) { // invalid vURL socket.end('invalid URL'); console.error('invalid URL:'+nxstep); } else { // 3. // check STUN alability nmcln.checkStunable(vurle, function(err, yes){ if (err) { // invalid vURL socket.end('invalid URL'); console.error('invalid URL:'+nxstep); } else { // over STUN if (yes) { // 5. // traverse STUN session to peer nmcln.trvsSTUN(vurle, function(err, stun){ if (err || !stun) { // STUN not availabe socket.end('STUN not available, please use TURN'); console.error('STUN not available:'+nxstep); } else { // get peer endpoint var dstip = stun.peerIP, dstport = stun.peerPort; // setup tunnel to target by make CONNECT request var roptions = { port: dstport, hostname: dstip, method: 'CONNECT', path: req.url, agent: false, // set user-specific feature,like maxim bandwidth,etc localAddress: { addr: nmcln.ipaddr, port: nmcln.port, opt: { mbw: options.mbw || null } } }; // set SSL related options if (nmcln.secmode && nmcln.secerts) { Object.keys(nmcln.secerts).forEach(function(k){ roptions[k] = nmcln.secerts[k]; }); } // set turn-forward-to header for middle relays if (relays.length > 1) { var nmiddle = []; for (var idx = 1; idx < relays.length; idx ++) nmiddle.push(relays[idx]); var going = nmiddle.join(','); roptions.headers = {}; roptions.headers['turn-forward-to'] = going; } var rreq = httpps.request(roptions); rreq.end(); if (Debug) console.log('tunnel proxy relay, connect to %s:%d', dstip, dstport); rreq.on('connect', function(rres, rsocket, rhead) { if (Debug) console.log('tunnel proxy relay, got connected'); socket.write('HTTP/1.1 200 Connection Established\r\n' + 'Proxy-agent: Node-Proxy\r\n' + '\r\n'); rsocket.pipe(socket); socket.pipe(rsocket); rsocket.on('error', function(e) { console.log("tunnel proxy relay, socket error: " + e); socket.end(); }); }); rreq.on('error', function(e) { console.log("tunnel proxy relay, CONNECT request error: " + e); socket.end(); }); } }); } else { // over TURN, not support for middle relays socket.end('not support turn for middle relays '+nxstep); console.error('not support turn for middle relays '+nxstep); } } }); } }); } else { // not reachable socket.end('not reachable'); console.error('not reachable:'+nxstep); } } else { // 2. // reach export var urls = URL.parse('http://'+req.url, true, true); var srvip = urls.hostname; var srvport = urls.port || 443; // check if access to export local host if ((self.access_local === 0) && (isLocalhost(srvip) || isLocalintf(srvip))) { console.log("http tunnel proxy to " + req.url + ", deny local access on export host"); socket.end(); return; } if (Debug) console.log('http tunnel proxy, connect to %s:%d', srvip, srvport); var srvSocket = NET.connect(srvport, srvip, function() { if (Debug) console.log('http tunnel proxy, got connected!'); ///srvSocket.write(head); socket.write('HTTP/1.1 200 Connection Established\r\n' + 'Proxy-agent: Node-Proxy\r\n' + '\r\n'); srvSocket.pipe(socket); socket.pipe(srvSocket); }); srvSocket.setNoDelay(true); srvSocket.on('error', function(e) { console.log("http tunnel proxy to " + req.url + ", socket error: " + e); socket.end(); }); } }; // 5. // import http proxy function importHttpProxy(req, res){ var vurle, vstrs, urle = req.url; if (Debug) console.log('proxy to '+urle+',headers:'+JSON.stringify(req.headers)); function resErr(err){ try { res.writeHead(500); res.end(err); } catch (e) { console.log('res.end exception '+e); } } // 0. // find next hop // 1. // match vURL pattern: // - vhost like http(s)://xxx.vurl.51dese.com // - vpath like http(s)://51dese.com"/vurl/xxx" if (vstrs = req.headers.host.match(vURL.regex_vhost)) { vurle = vstrs[0]; if (Debug) console.log('proxy for client with vhost:'+vurle); } else if (vstrs = urle.match(vURL.regex_vpath)) { vurle = vstrs[0]; // prune vpath in req.url req.url = req.url.replace(vurle, ''); if (Debug) console.log('proxy for client with vpath:'+vurle); } else if (vurle = self.findExport(req.headers.host, urle)) { if (Debug) console.log('use export proxy '+vurle); } else { // not reachable resErr('not reachable'); console.error('not reachable:'+urle); return; } if (Debug) console.log('tunnel proxy for client request.headers:'+JSON.stringify(req.headers)+ ',url:'+urle+',vurl:'+vurle); // 1.1 // !!! rewrite req.url to remove vToken parts // TBD ... vToken check req.url = req.url.replace(vURL.regex_vtoken, ''); // 2. // get peer info by vURL nmcln.getvURLInfo(vurle, function(err, routing){ // 2.1 // check error and authentication if (err || !routing) { // invalid vURL resErr('invalid URL'); console.error('invalid URL:'+urle); // clear export cache if (self.exportCache[vurle]) { self.exportCache[vurle] = null; } } else { // 3. // check STUN alability nmcln.checkStunable(vurle, function(err, yes){ if (err) { // invalid vURL resErr('invalid URL'); console.error('invalid URL:'+urle); // clear export cache if (self.exportCache[vurle]) { self.exportCache[vurle] = null; } } else { // over STUN if (yes) { // 5. // traverse STUN session to peer nmcln.trvsSTUN(vurle, function(err, stun){ if (err || !stun) { // STUN not availabe resErr('STUN not available, please use TURN'); console.error('STUN not available:'+urle); // clear export cache if (self.exportCache[vurle]) { self.exportCache[vurle] = null; } } else { // get peer endpoint var dstip = stun.peerIP, dstport = stun.peerPort; // 6. // setup tunnel to target by make CONNECT request var roptions = { port: dstport, hostname: dstip, method: 'CONNECT', path: (/(:\d+)$/gi).test(req.headers.host) ? req.headers.host : req.headers.host+':80', agent: false, // set user-specific feature,like maxim bandwidth,etc localAddress: { addr: nmcln.ipaddr, port: nmcln.port, opt: { mbw: options.mbw || null } } }; // set SSL related options if (nmcln.secmode && nmcln.secerts) { Object.keys(nmcln.secerts).forEach(function(k){ roptions[k] = nmcln.secerts[k]; }); } var rreq = httpps.request(roptions); rreq.end(); rreq.on('error', function(e) { console.log("tunnel proxy, CONNECT request error: " + e); resErr("tunnel proxy, CONNECT request error: " + e); }); if (Debug) console.log('tunnel proxy, connect to %s:%d', dstip, dstport); rreq.on('connect', function(rres, rsocket, rhead) { if (Debug) console.log('tunnel proxy, got connected'); rsocket.on('error', function(e) { console.log("tunnel proxy, socket error: " + e); resErr("tunnel proxy, socket error: " + e); }); if (Debug) console.log('req.headers: '+JSON.stringify(req.headers)); // request on tunnel connection var toptions = { method: req.method, path: req.url.match(/^(http:)/gi)? URL.parse(req.url).path : req.url, agent: false, // set headers headers: req.headers, // pass rsocket which's request on createConnection: function(port, host, options){ return rsocket } }; var treq = httpps.request(toptions, function(tres){ if (Debug) console.log('tunnel proxy, got response, headers:'+JSON.stringify(tres.headers)); try { // set headers Object.keys(tres.headers).forEach(function (key) { res.setHeader(key, tres.headers[key]); }); res.writeHead(tres.statusCode); tres.pipe(res); tres.on('error', function(e) { console.log("tunnel proxy, tunnel response error: " + e); resErr("tunnel proxy, tunnel response error: " + e); }); } catch (e) { console.log("tunnel proxy, tunnel response exception: " + e); } }); treq.on('error', function(e) { console.log("tunnel proxy, tunnel request error: " + e); resErr("tunnel proxy, tunnel request error: " + e); }); req.pipe(treq); req.on('error', resErr); req.on('aborted', function () { treq.abort(); }); if (req.trailers) { treq.end(); } req.on('close', function () { treq.abort(); }); }); } }); } else { // over TURN // 5. // traverse TURN session to peer // notes: TURN session will use vToken for authentication nmcln.trvsTURN(vurle, function(err, turn){ if (err || !turn) { // TURN not availabe resErr('TURN not available, please check TURN service setup'); console.error('TURN not available:'+urle); // clear export cache if (self.exportCache[vurle]) { self.exportCache[vurle] = null; } } else { // 6. // setup tunnel to target by make CONNECT request var roptions = { port: routing.turn.proxyport, hostname: routing.turn.ipaddr, method: 'CONNECT', path: (/(:\d+)$/gi).test(req.headers.host) ? req.headers.host : req.headers.host+':80', agent: false }; // set turn-forward-to header: destination name-client's full vURL string roptions.headers = {}; roptions.headers['turn-forward-to'] = vurle; // set SSL related options // TBD... /*if (nmcln.secmode && nmcln.secerts) { Object.keys(nmcln.secerts).forEach(function(k){ roptions[k] = nmcln.secerts[k]; }); }*/ var rreq = httpps.request(roptions); rreq.end(); rreq.on('error', function(e) { console.log("tunnel proxy over TURN, CONNECT request error: " + e); resErr("tunnel proxy over TURN, CONNECT request error: " + e); }); if (Debug) console.log('tunnel proxy over TURN, connect to %s:%d', routing.turn.ipaddr, routing.turn.proxyport); rreq.on('connect', function(rres, rsocket, rhead) { if (Debug) console.log('tunnel proxy over TURN, got connected'); rsocket.on('error', function(e) { console.log("tunnel proxy over TURN, socket error: " + e); resErr("tunnel proxy over TURN, socket error: " + e); }); // request on tunnel connection var toptions = { method: req.method, path: req.url.match(/^(http:)/gi)? URL.parse(req.url).path : req.url, agent: false, // set headers headers: req.headers, // pass rsocket which's request on createConnection: function(port, host, options){ return rsocket } }; var treq = httpps.request(toptions, function(tres){ if (Debug) console.log('tunnel proxy over TURN, got response, headers:'+JSON.stringify(tres.headers)); // set headers Object.keys(tres.headers).forEach(function (key) { res.setHeader(key, tres.headers[key]); }); try { res.writeHead(tres.statusCode); tres.pipe(res); tres.on('error', function(e) { console.log("tunnel proxy over TURN, tunnel response error: " + e); resErr("tunnel proxy, tunnel response error: " + e); }); } catch (e) { console.log("tunnel proxy over TURN, tunnel response exception: " + e); } }); treq.on('error', function(e) { console.log("tunnel proxy over TURN, tunnel request error: " + e); resErr("tunnel proxy over TURN, tunnel request error: " + e); }); req.pipe(treq); req.on('error', resErr); req.on('aborted', function () { treq.abort(); }); if (req.trailers) { treq.end(); } req.on('close', function () { treq.abort(); }); }); } }); } } }); } }); } // 5.1 // import http tunnel proxy based on CONNECT method function importHttpTunnel(req, socket, head) { var vurle, vstrs, urle = req.url; if (Debug) console.log('tunnel to '+urle); // 0. // find next hop // 1. // match vURL pattern: // - vhost like http(s)://xxx.vurl.51dese.com // - vpath like http(s)://51dese.com/vurl/xxx" if (vstrs = urle.match(vURL.regex_vhost)) { vurle = vstrs[0]; if (Debug) console.log('tunnel for client with vhost:'+vurle); } else if (vstrs = urle.match(vURL.regex_vpath)) { vurle = vstrs[0]; // prune vpath in req.url req.url = req.url.replace(vurle, ''); if (Debug) console.log('proxy for client with vpath:'+vurle); } else if (vurle = self.findExport(urle, urle)) { if (Debug) console.log('use export proxy '+vurle); } else { // not reachable socket.end('not reachable'); console.error('not reachable:'+urle); return; } if (Debug) console.log('tunnel proxy for client request.headers:'+JSON.stringify(req.headers)+ ',url:'+urle+',vurl:'+vurle); // 1.1 // !!! rewrite req.url to remove vToken parts // TBD ... vToken check req.url = req.url.replace(vURL.regex_vtoken, ''); // 2. // get peer info by vURL nmcln.getvURLInfo(vurle, function(err, routing){ // 2.1 // check error and authentication if (err || !routing) { // invalid vURL socket.end('invalid URL'); console.error('invalid URL:'+urle); // clear export cache if (self.exportCache[vurle]) { self.exportCache[vurle] = null; } } else { // 3. // check STUN alability nmcln.checkStunable(vurle, function(err, yes){ if (err) { // invalid vURL resErr('invalid URL'); console.error('invalid URL:'+urle); // clear export cache if (self.exportCache[vurle]) { self.exportCache[vurle] = null; } } else { // over STUN if (yes) { // 5. // traverse STUN session to peer nmcln.trvsSTUN(vurle, function(err, stun){ if (err || !stun) { // STUN not availabe socket.end('STUN not available, please use TURN'); console.error('STUN not available:'+urle); // clear export cache if (self.exportCache[vurle]) { self.exportCache[vurle] = null; } } else { // get peer endpoint var dstip = stun.peerIP, dstport = stun.peerPort; // 6. // if req.url is valid vURL, connect it directly, // otherwise do CONNECT tunnel over export vURL // notes: disable it to avoid middle-man attack if (urle.match(vurle)) { // 6.1 // connect it directly if (Debug) console.log('https proxy, httpp connect to %s:%d', dstip, dstport); // connection options var coptions = { port: dstport, host: dstip, // set user-specific feature,like maxim bandwidth,etc localAddress: { addr: nmcln.ipaddr, port: nmcln.port, opt: { mbw: options.mbw || null } } }; var srvSocket = UDT.connect(coptions, function() { if (Debug) console.log('https proxy, httpp connect, got connected!'); socket.write('HTTP/1.1 200 Connection Established\r\n' + 'Proxy-agent: Node-Proxy\r\n' + '\r\n'); srvSocket.pipe(socket); socket.pipe(srvSocket); }); srvSocket.on('error', function(e) { console.log("https proxy, httpp connect to " + req.url + ", socket error: " + e); socket.end(); }); } else { // 6.2 // setup tunnel to target by make CONNECT request var roptions = { port: dstport, hostname: dstip, method: 'CONNECT', path: req.url, agent: false, // set user-specific feature,like maxim bandwidth,etc localAddress: { addr: nmcln.ipaddr, port: nmcln.port, opt: { mbw: options.mbw || null } } }; // set SSL related options if (nmcln.secmode && nmcln.secerts) { Object.keys(nmcln.secerts).forEach(function(k){ roptions[k] = nmcln.secerts[k]; }); } var rreq = httpps.request(roptions); rreq.end(); if (Debug) console.log('tunnel proxy, connect to %s:%d', dstip, dstport); rreq.on('connect', function(rres, rsocket, rhead) { if (Debug) console.log('tunnel proxy, got connected'); socket.write('HTTP/1.1 200 Connection Established\r\n' + 'Proxy-agent: Node-Proxy\r\n' + '\r\n'); rsocket.pipe(socket); socket.pipe(rsocket); rsocket.on('error', function(e) { console.log("tunnel proxy, socket error: " + e); socket.end(); }); }); rreq.on('error', function(e) { console.log("tunnel proxy, CONNECT request error: " + e); socket.end(); }); } } }); } else { // over TURN // 5. // traverse TURN session to peer nmcln.trvsTURN(vurle, function(err, turn){ if (err || !turn) { // TURN not availabe resErr('TURN not available, please check TURN service setup'); console.error('TURN not available:'+urle); // clear export cache if (self.exportCache[vurle]) { self.exportCache[vurle] = null; } } else { // 6. // setup tunnel to target by make CONNECT request var roptions = { port: routing.turn.proxyport, hostname: routing.turn.ipaddr, method: 'CONNECT', path: req.url, agent: false }; // set turn-forward-to header: destination name-client's full vURL string roptions.headers = {}; roptions.headers['turn-forward-to'] = vurle; // set SSL related options /*if (nmcln.secmode && nmcln.secerts) { Object.keys(nmcln.secerts).forEach(function(k){ roptions[k] = nmcln.secerts[k]; }); }*/ var rreq = httpps.request(roptions); rreq.end(); if (Debug) console.log('tunnel proxy over TURN, connect to %s:%d', routing.turn.ipaddr, routing.turn.proxyport); rreq.on('connect', function(rres, rsocket, rhead) { if (Debug) console.log('tunnel proxy over TURN, got connected'); socket.write('HTTP/1.1 200 Connection Established\r\n' + 'Proxy-agent: Node-Proxy\r\n' + '\r\n'); rsocket.pipe(socket); socket.pipe(rsocket); rsocket.on('error', function(e) { console.log("tunnel proxy over TURN, socket error: " + e); socket.end(); }); }); rreq.on('error', function(e) { console.log("tunnel proxy over TURN, CONNECT request error: " + e); socket.end(); }); } }); } } }); } }); } // 5.2 // import socks proxy function importSocksProxy(socket, port, address, proxy_ready) { var vurle, vstrs, urle = address+':'+port; if (Debug) console.log('socks proxy to '+urle); // 1. // find next hop // TBD... if (vstrs = urle.match(vURL.regex_vhost)) { vurle = vstrs[0]; if (Debug) console.log('tunnel for client with vhost:'+vurle); } else if (vurle = self.findExport(urle, urle)) { if (Debug) console.log('use export proxy '+vurle); } else { // not reachable socket.end('not reachable'); console.error('not reachable:'+urle); return; } if (Debug) console.log('socks proxy for client'+ ',url:'+urle+',vurl:'+vurle); // 2. // get peer info by vURL nmcln.getvURLInfo(vurle, function(err, routing){ // 2.1 // check error and authentication if (err || !routing) { // invalid vURL socket.end('invalid URL'); console.error('invalid URL:'+urle); // clear export cache if (self.exportCache[vurle]) { self.exportCache[vurle] = null; } } else { // 3. // check STUN alability nmcln.checkStunable(vurle, function(err, yes){ if (err) { // invalid vURL resErr('invalid URL'); console.error('invalid URL:'+urle); // clear export cache if (self.exportCache[vurle]) { self.exportCache[vurle] = null; } } else { // over STUN if (yes) { // 5. // traverse STUN session to peer nmcln.trvsSTUN(vurle, function(err, stun){ if (err || !stun) { // STUN not availabe socket.end('STUN not available, please use TURN'); console.error('STUN not available:'+urle); // clear export cache if (self.exportCache[vurle]) { self.exportCache[vurle] = null; } } else { // get peer endp