Files
2022-06-07 20:47:54 +08:00

404 lines
9.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 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 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 (typeof obj === 'object') {
return JSON.stringify(obj);
}
if (Buffer.isBuffer(obj)) {
return 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 httpModule = options.protocol === 'https:' ? https : http;
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(new Error('Timeout'));
}, TIMEOUT);
try {
client = httpModule.request(options, function (res) {
res.on('error', handleCallback);
handleCallback(null, res);
});
client.on('error', handleCallback);
client.end(toString(options.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 handleCallback = function (err, res, body) {
if (!done) {
done = true;
gunzip(err, res, body, function(e, data) {
data = e ? '' : (options.needRawData ? data : data + '');
callback(e, data, res || '');
});
}
clearTimeout(timer);
err && client && client.destroy();
};
var addTimeout = function () {
clearTimeout(timer);
timer = setTimeout(function () {
handleCallback(new Error('Timeout'));
}, TIMEOUT);
};
var maxLength = options.maxLength;
var handleResponse = function(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);
fs.stat(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;
};