whistle
Version:
HTTP, HTTP2, HTTPS, Websocket debugging proxy
452 lines (428 loc) • 11.1 kB
JavaScript
var http = require('http');
var https = require('https');
var fs = require('fs');
var extend = require('extend');
var fileMgr = require('./file-mgr');
var logger = require('./logger');
var parseUrl = require('./parse-url');
var zlib = require('./zlib');
var common = require('./common');
var cache = {};
var listeners = [];
var newUrls;
var TIMEOUT = 16000;
var MAX_RULES_LEN = 1024 * 72;
var MAX_FILE_LEN = 1024 * 256;
var MAX_INTERVAL = 1000 * 30;
var MIN_INTERVAL = 1000 * 10;
var TIMEOUT_ERR = new Error('Timeout');
var EXCEED = 'EXCEED';
var OPTIONS = { encoding: 'utf8' };
var queue = [];
var queueTimer;
var FILE_RE = /^(?:[a-z]:[\\/]|[~~]?\/)/i;
var GZIP_RE = /gzip/i;
var pendingList = process.whistleStarted ? null : [];
var pluginMgr;
process.once('whistleStarted', function () {
if (pendingList) {
pendingList.forEach(function (item) {
add(item[0], item[1], item[2]);
});
pendingList = null;
}
});
function getInterval(time, isLocal) {
var len = Object.keys(cache).length || 1;
var interval = isLocal
? 5000
: Math.max(MIN_INTERVAL, Math.ceil(MAX_INTERVAL / len));
var minTime = interval - (time > 0 ? time : 0);
return Math.max(minTime, 1000);
}
function triggerChange(data, body) {
if (data) {
body = (body && body.trim()) || '';
if (data.body === body) {
return;
}
data.body = body;
}
if (newUrls) {
return;
}
newUrls = {};
listeners.forEach(function (l) {
l();
});
Object.keys(newUrls).forEach(function (url) {
newUrls[url] = cache[url];
});
cache = newUrls;
newUrls = null;
}
function parseOptions(options) {
if (typeof options === 'string') {
options = parseUrl(options);
} else {
var fullUrl = options.url || options.uri;
if (fullUrl && typeof fullUrl === 'string') {
options = extend(options, parseUrl(fullUrl));
}
}
var maxLength = options.maxLength;
if (!(maxLength > 0)) {
options.maxLength = 0;
}
options.agent = false;
if (options.rejectUnauthorized !== true) {
options.rejectUnauthorized = false;
}
if (options.headers && options.headers.trailer) {
delete options.headers.trailer;
}
return options;
}
function toString(obj) {
if (obj == null) {
return;
}
if (Buffer.isBuffer(obj)) {
return obj;
}
if (typeof obj === 'object') {
return JSON.stringify(obj) || '';
}
return obj + '';
}
var NOT_PLUGIN_ERR = new Error('Error: not found');
var NOT_UI_SERVER_ERR = new Error('Error: not implemented uiServer');
NOT_PLUGIN_ERR.code = 404;
NOT_UI_SERVER_ERR.code = 501;
function loadPlugin(options, callback) {
var name = options.pluginName;
if (!name) {
return callback();
}
pluginMgr.loadPluginByName(name, function (err, ports) {
if (err || !ports || !ports.uiPort) {
return callback(err || (ports ? NOT_UI_SERVER_ERR : NOT_PLUGIN_ERR));
}
options.url =
'http://127.0.0.1:' + ports.uiPort + options.url.substring(name.length);
callback();
});
}
function sendReq(options, callback) {
var conf = options.whistleConfig;
var httpModule = http;
var isHttps = options.protocol === 'https:';
if (conf && options.headers) {
var headers = options.headers;
if (isHttps) {
headers['x-whistle-https-request'] = 1;
}
headers.host = headers.host || options.host;
options.protocol = null;
options.hostname = null;
options.agent = false;
options.host = conf.host;
options.port = conf.port;
} else if (isHttps) {
httpModule = https;
}
var timer, client;
var handleCallback = function (err, res) {
clearTimeout(timer);
if (httpModule) {
httpModule = null;
callback(err, res);
}
err && client && client.destroy();
};
timer = setTimeout(function () {
handleCallback(TIMEOUT_ERR);
}, TIMEOUT);
try {
client = httpModule.request(options, function (res) {
res.on('error', handleCallback);
handleCallback(null, res);
});
client.on('error', handleCallback);
var body = options.body;
if (body && typeof body.pipe === 'function') {
body.once('error', function () {
client.destroy();
});
body.pipe(client);
} else {
client.end(toString(body));
}
return client;
} catch (e) {
handleCallback(e);
}
}
function gunzip(err, res, body, callback) {
if (!err && body && res && GZIP_RE.test(res.headers['content-encoding'])) {
zlib.gunzip(body, callback);
} else {
callback(err, body);
}
}
function request(options, callback) {
loadPlugin(options, function (err) {
if (err) {
return callback(err, '', '');
}
options = parseOptions(options);
var done, client, timer;
var type = options.responseType;
var strictMode = options.strictMode;
var handleCallback = function (err, res, body) {
if (!done) {
done = true;
gunzip(err, res, body, function(e, data) {
data = e ? '' : (options.needRawData || type === 'buffer' ? data : data + '');
if (type === 'json') {
if (!data) {
e = e || new Error('Invalid JSON');
} else {
try {
data = JSON.parse(data);
} catch (err) {
data = null;
e = err;
}
}
}
callback(e, data, res || '');
});
}
clearTimeout(timer);
err && client && client.destroy();
};
var addTimeout = function () {
clearTimeout(timer);
timer = setTimeout(function () {
handleCallback(TIMEOUT_ERR);
}, TIMEOUT);
};
var maxLength = options.maxLength;
var handleResponse = function(res) {
if (strictMode && res && res.statusCode != 200) {
return handleCallback(new Error('Response code ' + res.statusCode), res);
}
if (type === 'stream') {
clearTimeout(timer);
return callback(null, res, res);
}
addTimeout();
var body = '';
res.on('data', function (data) {
body = body ? Buffer.concat([body, data]) : data;
addTimeout();
if (maxLength && body.length > maxLength) {
var err;
if (!options.ignoreExceedError) {
err = new Error('The response body exceeded length limit');
err.code = EXCEED;
}
handleCallback(err, res, body);
}
});
res.on('end', function() {
handleCallback(null, res, body);
});
};
client = sendReq(options, function (err, res) {
if (err) {
handleCallback(err);
} else {
handleResponse(res);
}
});
});
}
exports.request = request;
function readFile(url, callback) {
var data;
var now = Date.now();
var execCallback = function () {
callback(url, Date.now() - now);
};
var filePath = fileMgr.convertSlash(url);
common.getStat(filePath, function (err, stat) {
data = cache[url];
if (!data) {
return execCallback();
}
if (err) {
if (err.code === 'ENOENT') {
err = null;
} else {
logger.error(url, err.message);
}
triggerChange(data);
data.mtime = null;
return execCallback();
}
if (!stat.isFile()) {
triggerChange(data);
data.mtime = null;
return execCallback();
}
var time = stat.mtime.getTime();
if (time === data.mtime) {
return execCallback();
}
var stream = fs.createReadStream(filePath, OPTIONS);
var done;
var body = '';
var listener = function (err) {
if (done) {
return;
}
execCallback();
if (err && err.code !== 'ENOENT') {
return;
}
done = true;
data.mtime = time;
stream.close();
triggerChange(data, body);
};
stream.on('data', function (text) {
if (done) {
return;
}
body += text;
if (body.length > MAX_FILE_LEN) {
listener();
}
});
stream.on('error', listener);
stream.on('end', listener);
});
}
function addQueue(url, consumeTime) {
if (cache[url] && queue.indexOf(url) === -1) {
queue.push(url);
}
var data;
while (!queueTimer && !data) {
url = queue.shift();
if (!url) {
return;
}
data = cache[url];
if (data) {
queueTimer = setTimeout(function () {
queueTimer = null;
updateBody(url, addQueue);
}, getInterval(consumeTime, data.isLocalUrl || data.isLocalPath));
return;
}
}
}
function updateBody(url, callback, init) {
var data = cache[url];
if (!data) {
return callback && callback();
}
if (data.isLocalPath) {
return readFile(url, addQueue);
}
var now = Date.now();
var options = {
url: url,
pluginName: data.pluginName,
maxLength: MAX_RULES_LEN,
ignoreExceedError: true
};
if (data.headers) {
options.headers = data.headers;
}
request(options, function (err, body, res) {
data = cache[url];
callback && callback(url, Date.now() - now);
if (!data) {
return;
}
var code = res.statusCode;
var isRedirect = code == 301 || code == 302 || code == 303 || code == 307 || code == 308;
var notFound = err
? err.code === 'ENOTFOUND' || err.code === 'ECONNREFUSED'
: code != 200 && code != 204;
err && logger.error('[Load Rules]', url, err.message || err);
if (notFound) {
data._retry = data._retry || 0;
if (isRedirect || data._retry > 2) {
if (!err) {
var msg = code;
if (isRedirect) {
var loc = res.headers.location;
msg += loc ? ' redirect to ' + loc : '';
}
logger.warn('[Load Rules]', url, 'status', msg);
}
data._retry = -6;
err = body = '';
notFound = false;
}
++data._retry;
} else {
data._retry = 0;
}
if (notFound || err) {
if (init) {
updateBody(url);
return;
}
}
addQueue(url);
if (notFound || err) {
return;
}
triggerChange(data, body);
});
return true;
}
exports.addChangeListener = function (l) {
listeners.push(l);
};
function add(url, headers, pluginName) {
var data = cache[url];
if (!data) {
cache[url] = data = {
body: '',
pluginName: pluginName,
isLocalUrl: pluginName || url.indexOf('http://127.0.0.1:') === 0,
isLocalPath: FILE_RE.test(url),
headers: headers
};
updateBody(url, null, true);
}
if (newUrls) {
newUrls[url] = 1;
}
return data.body;
}
exports.add = function (url, headers, pluginName) {
if (pendingList && headers && headers['x-whistle-internal-id']) {
pendingList.push([url, headers, pluginName]);
return '';
}
return add(url, headers, pluginName);
};
exports.forceUpdate = function (root) {
Object.keys(cache).forEach(function (url) {
if (url.indexOf(root) === 0) {
updateBody(url);
}
});
};
exports.triggerChange = triggerChange;
exports.setPluginMgr = function (mgr) {
pluginMgr = mgr;
};