peer-proxy
Version:
Proxy web service from peer
1,130 lines (927 loc) • 46.1 kB
JavaScript
// Copyright (c) 2013 Tom Zhou<appnet.link@gmail.com>
var WEBPP = require('appnet.io'),
SEP = WEBPP.SEP,
vURL = WEBPP.vURL,
httppProxy = require('httpp-proxy'),
URL = require('url'),
zlib = require('zlib'),
Buffer = require('buffer').Buffer,
Iconv = require('iconv-lite'),
Jschardet = require('jschardet'),
Connect = require('connect'),
trumpet = require('trumpet'),
NET = require('net'),
UDT = require('udt');
// authentication module
var httpauth = require('http-auth');
// security hash
// sipkey can be any user defined 4 integers
var siphash = require('siphash'),
sipkey = [0x66662222, 0x86868686, 0x66665555, 0x33339999]; // magic key
// Debug level
// 1: display error, proxy entry
// 2: display req/res headers/statusCode
var Debug = 0;
// Proxy class
// a proxy will contain one appnet.io name-client
// - websites: array of web site/service URL, like ['http://sohu.com', 'https://google.com']
// - fn: callback to pass proxy informations
// - options: user custom parameters, like {secmode: ..., usrkey: ..., mode: ..., domain: ..., endpoints: ..., turn: ...}
// - options.secmode: ssl, enable ssl/https; acl, enable ssl/https,host-based ACL
// - options.sslmode: srv, only verify server side cert; both, verify both server and client side cert
///
// - options.level: srv, only proxy url/link on server side; both, proxy url/link on both server and web site
// - options.auth: http basic-auth as username:password
var Proxy = module.exports = function(websites, fn, options){
var self = this;
if (!(this instanceof Proxy)) return new Proxy(websites, fn, options);
if (!Array.isArray(websites)) websites = [websites];
// check proxy level
options = options || {};
options.level = options.level || 'srv';
// check basic auth
var basicauth = false;
if (options && options.auth) {
var astr = options.auth.split(':');
basicauth = {username: astr && astr[0], password: astr && astr[1]};
}
// check upload
var fileupload = false;
// 0.
// proxy cache and URLs
self.proxyCache = {}; // http proxy for website
self.proxyURL = {}; // vURL for website
self.proxyChd = {}; // child link in websites
self.proxyPrn = {}; // parent link for websites
// 1.
// create name client
var nmcln = self.nmcln = new WEBPP({
usrinfo: {
domain: (options && options.domain) || '51dese.com',
usrkey: (options && options.usrkey) || ('peerproxy@'+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: vhost-based
vmode: vURL.URL_MODE_HOST,
// secure mode
secmode: (options && options.secmode === 'ssl') ? SEP.SEP_SEC_SSL :
SEP.SEP_SEC_SSL_ACL_HOST,
// ssl mode
sslmode: (options && options.sslmode === 'srv') ? SEP.SEP_SSL_AUTH_SRV_ONLY :
SEP.SEP_SSL_AUTH_SRV_CLNT
});
// 1.1
// check ready
nmcln.once('ready', function(){
if (Debug) console.log('name-client ready on vURL:'+nmcln.vurl);
// 2.
// setup proxy App
// notes: main URL entry without parent URL
for (var idx = 0; idx < websites.length; idx ++)
self.addURL(websites[idx], {level: options.level});
// 3.
// http proxy
// website vhost regex as p(16hex)p-
var vhostregex = /p([0-9]|[a-f]){16}p-/gi;
var vpathregex = /-p([0-9]|[a-f]){16}p/gi;
var proxyHttp = function(req, res, next){
var vstrs, vurle;
// 3.1
// check vURL
if ((self.nmcln.vmode === vURL.URL_MODE_HOST) &&
(vstrs = req.headers.host.match(vhostregex))) {
vurle = vstrs[0];
} else if ((self.nmcln.vmode === vURL.URL_MODE_PATH) &&
(vstrs = req.url.match(vpathregex))) {
vurle = vstrs[0];
} else {
// invalid vURL, nothing to do
if (Debug) console.error('http invalid vURL:'+JSON.stringify(req.headers));
next();
return;
}
// 3.2
// !!! rewrite req.url to remove vToken parts
// TBD ... vToken check
///req.url = req.url.replace(vURL.regex_vtoken, '');
if (vURL.regex_vtoken.test(req.url)) {
res.writeHead(301, {'location': req.url.replace(vURL.regex_vtoken, '')});
res.end();
return;
}
// 3.3
// proxy target
if (self.proxyCache[vurle]) {
self.proxyCache[vurle].proxyRequest(req, res);
} else {
// unknown vURL
res.writeHead(400);
res.end('unknown vURL');
if (Debug) console.error('unknown vURL:'+JSON.stringify(req.headers));
return;
}
};
// 4.
// create http proxy App
var appHttp = Connect();
// 4.1
// add third-party connect middle-ware
// 4.1.1
// add http basic-auth module
if (basicauth) {
var basic = httpauth.basic({
realm: "51dese.com"
}, function (username, password, callback) {
callback(username === basicauth.username &&
password === basicauth.password);
});
appHttp.use(httpauth.connect(basic));
}
// 4.1.2
// compress
// TBD...
///appHttp.use(Connect.compress());
// 4.1.3
// cache
///appHttp.use(Connect.staticCache({maxLength: 512*1024, maxObjects: 64}));
// ...
// 4.1.6
// query string parser
appHttp.use(Connect.query());
// 4.2
// portal page -> //////////////////////////////////////////////////////////
// 4.2.1
// static file service
appHttp.use('/vstatic', Connect.static(__dirname + '/public'));
// 4.2.2
// admin service, checking admin node like 1XX,2XX,3XX,etc
// notes: to enable admin mode append /vadmin[/:mode] in req.url
// 4.2.2.1
// - 1XX: retrieve all proxed website list
appHttp.use('/vadmin/1', function(req, res, next){
var htmlstr = '';
htmlstr += '<ol>';
for (var site in self.proxyURL) {
if (Debug) console.log('plink of '+site+': '+self.proxyPrn[site]);
///if (self.proxyPrn[site]) continue;
htmlstr += '<li>'+
'<a href='+
'"'+self.proxyURL[site]+'">'+
'>> '+site+
'</a>'+
'</li>';
}
htmlstr += '</ol>';
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(htmlstr);
});
// 4.2.2.2
// - 2XX: add single website to proxy by querystring, then return html
// like /vadmin/2/?url=xxx&purl=xxx
appHttp.use('/vadmin/2', function(req, res, next){
if (Debug) console.log('req.query:'+JSON.stringify(req.query));
if (req.query && req.query.url){
var vurl = self.addURL(req.query.url, {purl: req.query.purl, level: options.level});
if (vurl) {
res.writeHead(301, {'location': vurl});
res.end();
} else {
next('invalid param');
}
} else {
next('invalid param');
}
});
// 4.2.2.3
// - 3XX: add batch of website to proxy by querystring, then return json
// like /vadmin/3/?url=xxx,yyy,zzz&purl=xxx
appHttp.use('/vadmin/3', function(req, res, next){
if (Debug) console.log('req.query:'+JSON.stringify(req.query));
if (req.query && req.query.url){
var urls = req.query.url.split(',');
var vobj = {};
for (var idx in urls) {
var vurl = self.addURL(urls[idx], {purl: req.query.purl, level: options.level});
if (vurl) {
vobj[urls[idx]] = vurl;
}
}
res.writeHead(200, {'Content-Type': 'application/json'});
res.end(JSON.stringify(vobj));
} else {
next('invalid param');
}
});
// <- portal page //////////////////////////////////////////////////////////////
// 4.3
// add http proxy in App
appHttp.use(proxyHttp);
// 5.
// websocket proxy
var appWs = function(req, socket, head){
var vstrs, vurle;
// 5.1
// check vURL
if ((self.nmcln.vmode === vURL.URL_MODE_HOST) &&
(vstrs = req.headers.host.match(vhostregex))) {
vurle = vstrs[0];
} else if ((self.nmcln.vmode === vURL.URL_MODE_PATH) &&
(vstrs = req.url.match(vpathregex))) {
vurle = vstrs[0];
} else {
// invalid vURL, nothing to do
// MUST not close socket, which will break other upgrade listener
if (Debug) console.error('ws invalid URL:'+JSON.stringify(req.headers));
return;
}
// 5.2
// !!! rewrite req.url to remove vToken parts
// TBD ... vToken check
req.url = req.url.replace(vURL.regex_vtoken, '');
// 5.3
// proxy target
if (self.proxyCache[vurle]) {
self.proxyCache[vurle].proxyWebSocketRequest(req, socket, head);
} else {
// unknown vURL
socket.end('unknown vURL');
if (Debug) console.error('unknown vURL:'+JSON.stringify(req.headers));
return;
}
};
// 6.
// https tunnel proxy
// notes: the idea is see https request/websocket proxy as reverse proxy to destination http/https website,
// so, create connection to peer-proxy httpps server self.
var appTunnel = function(req, socket, head){
var roptions = {
port: nmcln.port,
host: nmcln.ipaddr,
localAddress: {
addr: nmcln.ipaddr
}
};
if (Debug) console.log('http tunnel proxy, connect to self %s:%d', nmcln.ipaddr, nmcln.port);
var srvSocket = UDT.connect(roptions, function() {
if (Debug) console.log('http tunnel proxy, got connected!');
///srvSocket.write(head);
socket.pipe(srvSocket);
socket.write('HTTP/1.1 200 Connection Established\r\n' +
'Proxy-agent: Node-Proxy\r\n' +
'\r\n');
srvSocket.pipe(socket);
});
srvSocket.on('error', function(e) {
console.log("http tunnel proxy, socket error: " + e);
socket.end();
});
};
// 7.
// hook App on name-client
nmcln.bsrv.srv.on('request', appHttp);
nmcln.bsrv.srv.on('upgrade', appWs);
nmcln.bsrv.srv.on('connect', appTunnel);
// 8.
// pass proxy URLs back
fn(null, self.proxyURL);
// 9.
// report peer-service
// like {vurl:x,cate:x,name:x,desc:x,tags:x,acls:x,accounting:x,meta:x}
for (var origin in self.proxyURL) {
nmcln.reportService({
vurl: self.proxyURL[origin],
cate: 'peer-proxy',
name: origin,
meta: {origin: origin}
});
}
// 9.1
// update peer-service: connetion loss, etc
// TBD...
});
// 1.2
// check error
nmcln.on('error', function(err){
console.log('name-client create failed:'+JSON.stringify(err));
fn(err);
});
};
// instance methods
// add URL proxy entry
// options: {purl: ...}
// - purl: parent or root URL
Proxy.prototype.addURL = function(oriUrl, options){
var sUrl = oriUrl.match(/^(http:\/\/)|^(https:\/\/)/gi)? oriUrl : 'http://'+oriUrl;
var self = this;
var urls = URL.parse(sUrl, true, true);
var host = urls.hostname;
var proto = urls.protocol || 'http:';
var port = urls.port || ((proto === 'wss:' || proto === 'https:')? 443 : 80);
var path = urls.path;
var urle = proto+'//'+urls.host; // proto+host striped path part of URL
// check urls
if (!host) {
console.log('invalid URL to proxy: '+oriUrl);
return null;
}
// check options
var purl = (options && options.purl) || '';
// check proxy level
var level = (options && options.level) || 'srv';
// vURL string format, like pxxxp. in vhost mode or /pxxxp in vpath mode
var vstrs = self.nmcln.vurl.split('//');
var wstrs = siphash.hash_hex(sipkey, urle);
var vurle = (self.nmcln.vmode === vURL.URL_MODE_HOST) ? 'p'+wstrs+'p-' : '-p'+wstrs+'p';
///console.log('add proxy url:'+oriUrl);
// 1.
// create proxy to target
if (!self.proxyCache[vurle]) {
self.proxyCache[vurle] = new httppProxy.HttpProxy({
changeOrigin: true,
enable: {xforward: false},
httpp: true,
https: self.nmcln.secmode,
/*forward: {
httpp: false,
host: host,
port: port,
https: (proto === 'wss:' || proto === 'https:')
},*/
target: {
httpp: false,
host: host,
port: port,
https: (proto === 'wss:' || proto === 'https:')
}
});
// hook urle,vurle,purl in httpProxy instance
self.proxyCache[vurle].urle = urle;
self.proxyCache[vurle].vurle = vurle;
// 1.1
// Handle request error
self.proxyCache[vurle].on('proxyError', function(err, req, res){
if (Debug) console.error(err+',proxy to '+urle);
// 1.1.1
// send error back
try {
res.writeHead(500, {'Content-Type': 'text/plain'});
if (req.method !== 'HEAD') {
if (process.env.NODE_ENV === 'production') {
res.write('Internal Server Error');
} else {
res.write('An error has occurred: ' + JSON.stringify(err));
}
}
res.end();
} catch (ex) {
console.error("res.end error: %s", ex.message) ;
}
// 1.1.2
// clear vURL entry
// notes: still keep it to avoid attack
///self.delURL(urle);
});
// 1.2
// Handle upgrade error
self.proxyCache[vurle].on('webSocketProxyError', function(err, req, socket, head){
if (Debug) console.error(err+',proxy to '+urle);
// send error back
try {
if (process.env.NODE_ENV === 'production') {
socket.write('Internal Server Error');
} else {
socket.write('An error has occurred: ' + JSON.stringify(err));
}
socket.end();
} catch (ex) {
console.error("socket.end error: %s", ex.message) ;
}
// clear vURL entry
// notes: still keep it to avoid attack
///self.delURL(urle);
});
// 2.
// Handle custom rewrite logics on response
self.proxyCache[vurle].on('proxyResponse', function(req, res, response){
var prxself = this;
if (Debug) console.log('\n\n\nProxy response,'+'req.url:'+req.url+',req.headers:'+JSON.stringify(req.headers)+
'\n\n,response.statusCode:'+response.statusCode+',response.headers:'+JSON.stringify(response.headers));
// 3.
// rewrite href from text/html,text/xml
// 2xx success
if ((response.statusCode >= 200 && response.statusCode < 300) &&
('content-type' in response.headers) &&
(response.headers['content-type'].match('text/html') ||
response.headers['content-type'].match('text/xml'))) {
if (Debug) console.log('Proxy 200 response,'+'response.headers:'+JSON.stringify(response.headers));
// 3.0
// rewrite Content-Location in response
if (response.headers['content-location']) {
response.headers['content-location'] = response.headers['content-location'].replace(vURL.regex_url, function(href){
var hrefvurl = self.addURL(href);
if (Debug) console.log('res.location of text/html:'+href+' -> '+hrefvurl);
return hrefvurl;
});
}
// 3.1
// intercept res.writeHead, res.write and res.end
// notes:
// - unzip and zip again
// - ...
var reshed = {};
var resbuf = [];
var ressiz = 0;
var resstr = '';
var _res_write = res.write, _res_end = res.end, _res_writeHead = res.writeHead;
var _decomp, _encomp, _codec;
// 3.1.1
// overwrite res.writeHead by cache statusCode
res.writeHead = function(statusCode, reasonPhrase, headers) {
reshed.statusCode = statusCode;
reshed.headers = {};
if (typeof reasonPhrase === 'object') {
reshed.headers = reasonPhrase;
} else if (typeof headers === 'object') {
reshed.headers = headers;
}
Object.keys(reshed.headers).forEach(function (key) {
res.setHeader(key, reshed.headers[key]);
});
};
// 3.2
// handle compressed text
if (('content-encoding' in response.headers) &&
(response.headers['content-encoding'].match('gzip') ||
response.headers['content-encoding'].match('deflate'))) {
if (Debug) console.log('Proxy ziped response,'+'response.headers:'+JSON.stringify(response.headers));
if (response.headers['content-encoding'].match('gzip')) {
_codec = 'gzip';
_decomp = zlib.createGunzip();
_encomp = zlib.createGzip();
} else {
_codec = 'deflate';
_decomp = zlib.createInflate();
_encomp = zlib.createDeflate();
}
if (Debug) console.log('\n\ngzip');
// 3.2.1
// override res.write and res.end
res.write = function(trunk){
return _decomp.write(trunk);
};
res.end = function(trunk){
_decomp.end(trunk);
};
// 3.3
// in case handle Node.js-not-supported charset
// - detect charset
// - decode content by charset
// - rewrite resstr
// - send rewrote resstr by charset
// - force response on utf-8 charset
// TBD...
_decomp.on('data', function(text) {
if (text) {
resbuf.push(text);
ressiz += text.length;
}
});
_decomp.on('end', function() {
// 3.3.1
// concat big buffer
var bigbuf = Buffer.concat(resbuf, ressiz);
// 3.3.2
// detect charset
var chardet = Jschardet.detect(bigbuf);
var charset = chardet.encoding;
if (Debug) console.log('charset:'+JSON.stringify(chardet));
// 3.3.3
// decode content by charset
resstr = Iconv.decode(bigbuf, charset);
if (Debug > 1) console.log('text response:'+resstr);
// 3.3.4
// rewrite text content
///console.log('before rewrite:'+JSON.stringify(resstr.match(vURL.regex_url)));
// 3.3.4.1
// rewrite href host part with trumpet
// TBD...
/*
var tr = trumpet();
tr.select('a', function(node) {
node.html(function (html) {
console.log(node.name + ': ' + html);
});
});
tr.end(resstr);
*/
// 3.3.4.1
// rewrite href host part in head and body of text/html
resstr = resstr.replace(/(<head(\s|\S)*\/head>)|(<body(\s|\S)*\/body>)/gi, function(hb){
if (Debug) console.log('head-body:'+hb);
// href rewrite
hb = hb.replace(vURL.regex_url, function(href){
// add proxy for child href
var hrefvurl = self.addURL(
href,
{purl: (req.connection.encrypted ? 'https://' : 'http://')+req.headers.host+req.url});
if (Debug) console.log('zipped text/html:'+href+' -> '+hrefvurl);
return hrefvurl;
});
// in case full proxy mode, embedded peer-proxy.js and jquery.js in head
if ((level === 'both') && (/<head(\s|\S)*\/head>/gi).test(hb)) {
if (!hb.match(/(jquery\.js)|(jquery\.min\.js)/gi)) {
hb = hb.replace(/<\/head>/gi, function(html){
var text = '<script type="text/javascript" src="/vstatic/jquery-2.0.3.min.js"></script>';
return text + html;
});
}
if (!hb.match(/(peer-proxy\.js)|(peer-proxy\.min\.js)/gi)) {
hb = hb.replace(/<\/head>/gi, function(html){
var text = '<script type="text/javascript" src="/vstatic/peer-proxy.js"></script>';
return text + html;
});
}
}
return hb;
});
// 3.3.4.1.1
// rewrite href host part in other case
if (!resstr.match(/(<head(\s|\S)*\/head>)|(<body(\s|\S)*\/body>)/gi)) {
if (Debug) console.log('other-body:'+resstr+',\nreq.url:'+req.url);
resstr = resstr.replace(vURL.regex_url, function(href){
// add proxy for child href
var hrefvurl = self.addURL(
href,
{purl: (req.connection.encrypted ? 'https://' : 'http://')+req.headers.host+req.url});
if (Debug) console.log('zipped text/html:'+href+' -> '+hrefvurl);
return hrefvurl;
});
}
// 3.3.4.2
// rewrite href path part in case vpath-based vURL mode
if (self.nmcln.vmode === vURL.URL_MODE_PATH) {
// rewrite href path part in head and body of text/html
resstr = resstr.replace(/(<head(\s|\S)*\/head>)|(<body(\s|\S)*\/body>)/gi, function(hb){
if (Debug) console.log('head-body:'+hb);
hb = hb.replace(vURL.regex_url, function(href){
if (Debug) console.log('href:'+href);
// skip on host-based rewrite
if (href.match(vURL.regex_url)) {
return href;
} else {
var vurlstrs = self.nmcln.vurl.split('/');
var vurlpath = '/';
for (var idx = 3; idx < vurlstrs.length; idx ++) {
vurlpath += vurlstrs[idx];
if (idx < (vurlstrs.length - 1)) vurlpath += '/';
}
vurlpath += prxself.vurle;
// req.url string
var requrls = URL.parse(req.url, true, true);
// like href="ruyier/"
var hrefstrs = href.split('"');
var hrefpath = hrefstrs[1];
// append vPath
return (hrefstrs[0]+'"'+
((hrefpath[0] === '/') ? vurlpath : (vurlpath + requrls.path + '/'))+
hrefstrs[1]+ '"');
}
});
// in case full proxy mode, embedded peer-proxy.js and jquery.js in head
if ((level === 'both') && (/<head(\s|\S)*\/head>/gi).test(hb)) {
if (!hb.match(/(jquery\.js)|(jquery\.min\.js)/gi)) {
hb = hb.replace(/<\/head>/gi, function(html){
var text = '<script type="text/javascript" src="/vstatic/jquery-2.0.3.min.js"></script>';
return text + html;
});
}
if (!hb.match(/(peer-proxy\.js)|(peer-proxy\.min\.js)/gi)) {
hb = hb.replace(/<\/head>/gi, function(html){
var text = '<script type="text/javascript" src="/vstatic/peer-proxy.js"></script>';
return text + html;
});
}
}
return hb;
});
// 3.3.4.2.1
// rewrite href host part in other case
// TBD ...
}
///console.log('after rewrite:'+JSON.stringify(resstr.match(vURL.regex_url)));
if (Debug > 1) console.log('overwrote text response:'+resstr);
// 3.3.5
// compress overwrote text and send out
if (_codec === 'gzip') {
var encbuf = Iconv.encode(resstr, charset);
// rewrite content-length
res.setHeader('content-length', encbuf.length);
res.writeHead = _res_writeHead;
res.writeHead(reshed.statusCode || 200);
zlib.gzip(encbuf, function(err, buffer) {
if (err) {
console.log(err+',deflate failed');
res.emit('error', err+',gzip failed');
} else {
res.write = _res_write;
res.end = _res_end;
res.end(buffer);
}
});
} else {
var encbuf = Iconv.encode(resstr, charset);
// rewrite content-length
res.setHeader('content-length', encbuf.length);
res.writeHead = _res_writeHead;
res.writeHead(reshed.statusCode || 200);
zlib.deflate(encbuf, function(err, buffer) {
if (!err) {
console.log(err+',deflate failed');
res.emit('error', err+',deflate failed');
} else {
res.write = _res_write;
res.end = _res_end;
res.end(buffer);
}
});
}
});
// 3.4
// decompress data
_decomp.on('drain', function(){
res.emit('drain');
});
} else {
if (Debug) console.log('\n\nnotzip');
// 3.5
// in case handle Node.js-not-supported charset
// - detect charset
// - decode content by charset
// - rewrite resstr
// - send rewrote by charset
// - force response on utf-8 charset
// TBD...
// 3.5.1
// override res.write and res.end
res.write = function(text){
if (text) {
resbuf.push(text);
ressiz += text.length;
}
return true;
};
res.end = function(text){
if (text) {
resbuf.push(text);
ressiz += text.length;
}
// 3.5.2
// concat big buffer
var bigbuf = Buffer.concat(resbuf, ressiz);
// 3.5.3
// detect charset
var chardet = Jschardet.detect(bigbuf);
var charset = chardet.encoding;
if (Debug) console.log('charset:'+JSON.stringify(chardet));
// 3.5.4
// decode content by charset
resstr = Iconv.decode(bigbuf, charset);
if (Debug > 1) console.log('text response:'+resstr);
// 3.5.5
// rewrite text content
///console.log('before rewrite:'+JSON.stringify(resstr.match(vURL.regex_url)));
// 3.5.5.1
// rewrite href host part with trumpet
// TBD...
/*
var tr = trumpet();
tr.select('a', function(node) {
node.html(function (html) {
console.log(node.name + ': ' + html);
});
});
tr.end(resstr);
*/
// 3.5.5.1
// rewrite href host part in head and body of text/html
resstr = resstr.replace(/(<head(\s|\S)*\/head>)|(<body(\s|\S)*\/body>)/gi, function(hb){
if (Debug) console.log('head-body:'+hb);
hb = hb.replace(vURL.regex_url, function(href){
// add proxy for child href
var hrefvurl = self.addURL(
href,
{purl: (req.connection.encrypted ? 'https://' : 'http://')+req.headers.host+req.url});
if (Debug) console.log('zipped text/html:'+href+' -> '+hrefvurl);
return hrefvurl;
});
// in case full proxy mode, embedded peer-proxy.js and jquery.js in head
if ((level === 'both') && (/<head(\s|\S)*\/head>/gi).test(hb)) {
if (!hb.match(/(jquery\.js)|(jquery\.min\.js)/gi)) {
hb = hb.replace(/<\/head>/gi, function(html){
var text = '<script type="text/javascript" src="/vstatic/jquery-2.0.3.min.js"></script>';
return text + html;
});
}
if (!hb.match(/(peer-proxy\.js)|(peer-proxy\.min\.js)/gi)) {
hb = hb.replace(/<\/head>/gi, function(html){
var text = '<script type="text/javascript" src="/vstatic/peer-proxy.js"></script>';
return text + html;
});
}
}
return hb;
});
// 3.5.5.1.1
// rewrite href host part in other case
if (!resstr.match(/(<head(\s|\S)*\/head>)|(<body(\s|\S)*\/body>)/gi)) {
if (Debug) console.log('other-body:'+resstr+',\nreq.url:'+req.url);
resstr = resstr.replace(vURL.regex_url, function(href){
// add proxy for child href
var hrefvurl = self.addURL(
href,
{purl: (req.connection.encrypted ? 'https://' : 'http://')+req.headers.host+req.url});
if (Debug) console.log('zipped text/html:'+href+' -> '+hrefvurl);
return hrefvurl;
});
}
// 3.5.5.2
// rewrite href path part in case vpath-based vURL mode
if (self.nmcln.vmode === vURL.URL_MODE_PATH) {
// rewrite href path part in head and body of text/html
resstr = resstr.replace(/(<head(\s|\S)*\/head>)|(<body(\s|\S)*\/body>)/gi, function(hb){
if (Debug) console.log('head-body:'+hb);
hb = hb.replace(vURL.regex_url, function(href){
if (Debug) console.log('href:'+href);
// skip on host-based rewrite
if (href.match(vURL.regex_url)) {
return href;
} else {
var vurlstrs = self.nmcln.vurl.split('/');
var vurlpath = '/';
for (var idx = 3; idx < vurlstrs.length; idx ++) {
vurlpath += vurlstrs[idx];
if (idx < (vurlstrs.length - 1)) vurlpath += '/';
}
vurlpath += prxself.vurle;
// req.url string
var requrls = URL.parse(req.url, true, true);
// like href="ruyier/"
var hrefstrs = href.split('"');
var hrefpath = hrefstrs[1];
// append vPath
return (hrefstrs[0]+'"'+
((hrefpath[0] === '/') ? vurlpath : (vurlpath + requrls.path + '/'))+
hrefstrs[1]+ '"');
}
});
// in case full proxy mode, embedded peer-proxy.js and jquery.js in head
if ((level === 'both') && (/<head(\s|\S)*\/head>/gi).test(hb)) {
if (!hb.match(/(jquery\.js)|(jquery\.min\.js)/gi)) {
hb = hb.replace(/<\/head>/gi, function(html){
var text = '<script type="text/javascript" src="/vstatic/jquery-2.0.3.min.js"></script>';
return text + html;
});
}
if (!hb.match(/(peer-proxy\.js)|(peer-proxy\.min\.js)/gi)) {
hb = hb.replace(/<\/head>/gi, function(html){
var text = '<script type="text/javascript" src="/vstatic/peer-proxy.js"></script>';
return text + html;
});
}
}
return hb;
});
// 3.5.5.2.1
// rewrite href host part in other case
// TBD ...
}
///console.log('after rewrite:'+JSON.stringify(resstr.match(vURL.regex_url)));
if (Debug > 1) console.log('overwrote text response:'+resstr);
// 3.6
// send overwrote text out
res.writeHead = _res_writeHead;
res.write = _res_write;
res.end = _res_end;
var encbuf = Iconv.encode(resstr, charset);
// rewrite content-length
res.setHeader('content-length', encbuf.length);
res.writeHead = _res_writeHead;
res.writeHead(reshed.statusCode || 200);
res.end(encbuf);
};
}
}
// 4.
// ...
// 5.
// redirection to another host
// notes: httpp-proxy internal logics has handled same host case
function isLocalhost(host){
return ((host === 'localhost') || (host === '127.0.0.1') ||
(host === '0:0:0:0:0:0:0:1') || (host === '::1'));
}
if (Debug) console.log('response.statusCode:'+response.statusCode);
// 3xx redirection
if ((response.statusCode >= 300 && response.statusCode < 400) &&
typeof response.headers.location !== 'undefined') {
if (Debug) console.log('Proxy redirection response,'+'response.headers:'+JSON.stringify(response.headers)+
',oriReqHost:'+prxself.oriReqHost);
var location = URL.parse(response.headers.location, true, true);
var oriReqUrl = URL.parse((prxself.source.https ? 'https://' : 'http://')+prxself.oriReqHost, true, true);
var sameHost, samePort, sameProto;
// handle default port
location.port = location.port || ((location.protocol === 'https:') ? 443 : 80);
oriReqUrl.port = oriReqUrl.port || ((oriReqUrl.protocol === 'https:') ? 443 : 80);
if (Debug) console.log('\n\noriReqUrl:'+JSON.stringify(oriReqUrl)+'\nlocation:'+JSON.stringify(location));
sameHost = ((location.hostname === oriReqUrl.hostname) ||
(isLocalhost(location.hostname) && isLocalhost(oriReqUrl.hostname)));
samePort = (location.port === oriReqUrl.port);
sameProto = (location.protocol === (prxself.source.https ? 'https:':'http:'));
// rewrite res.headers.location for redirection to the different target response
if (!(sameHost && samePort && sameProto)) {
// 5.1
// add proxy for redirection location
// like http://www.google.com/
var locvurl = self.addURL(response.headers.location);
if (Debug) console.log('redirection:'+response.headers.location+' -> '+locvurl);
// 5.2
// calculate replaced string
response.headers.location = locvurl;
}
}
// 6.
// rewrite domain of set-cookie from response.headers
// TBD... rewrite it with proxy url domain
var regexsc1 = new RegExp('domain=[a-z0-9-\.]+;', 'gi');
var regexsc2 = new RegExp('domain=[a-z0-9-\.]+', 'gi');
if (response.headers['set-cookie']) {
for (var idx = 0; idx < response.headers['set-cookie'].length; idx ++) {
if (response.headers['set-cookie'][idx].match(regexsc1)) {
response.headers['set-cookie'][idx] = response.headers['set-cookie'][idx].replace(regexsc1, '');
} else {
response.headers['set-cookie'][idx] = response.headers['set-cookie'][idx].replace(regexsc2, '');
}
}
if (Debug) console.log('\nRewrote set-cookie response,'+'response.headers:'+JSON.stringify(response.headers));
}
});
}
// 7.
// push to parent URL cache
if (!(oriUrl in self.proxyChd)) {
self.proxyChd[oriUrl] = [];
if (vURL.regex_url.test(purl) && (purl in self.proxyChd)) {
self.proxyChd[purl].push(oriUrl);
self.proxyPrn[oriUrl] = purl;
}
}
// 8.
// caulculate vhost-based vURL
return self.proxyURL[oriUrl] = (self.nmcln.vmode === vURL.URL_MODE_HOST) ?
vstrs[0]+'//'+vurle+vstrs[1]+path : vstrs[0]+'//'+vstrs[1]+vurle+path;
};
// delete URL proxy entry
Proxy.prototype.delURL = function(oriUrl){
var sUrl = oriUrl.match(/^(http:\/\/)|^(https:\/\/)/gi)? oriUrl : 'http://'+oriUrl;
var self = this;
var urls = URL.parse(sUrl, true, true);
var host = urls.hostname;
var proto = urls.protocol || 'http:';
var port = urls.port || ((proto === 'wss:' || proto === 'https:')? 443 : 80);
var path = urls.path;
var urle = proto+'//'+urls.host; // proto+host striped path part of URL
// check urls
if (!host) {
console.log('invalid URL to proxy: '+oriUrl);
return null;
}
// vURL string format
var vstrs = self.nmcln.vurl.split('//');
var wstrs = siphash.hash_hex(sipkey, urle);
var vurle = (self.nmcln.vmode === vURL.URL_MODE_HOST) ? 'p'+wstrs+'p-' : '-p'+wstrs+'p';
///console.log('del proxy url:'+oriUrl);
if (self.proxyCache[vurle]) self.proxyCache[vurle] = null;
if (self.proxyURL[oriUrl]) self.proxyURL[oriUrl] = null;
// clear children cache
if (self.proxyChd[oriUrl]) {
self.proxyChd[oriUrl].forEach(function(e){
self.delURL(e);
});
self.proxyChd[oriUrl] = null;
}
// clear parent cache
if (self.proxyPrn[oriUrl]) {
self.proxyPrn[oriUrl] = null;
}
return self;
};
// class methods