mirror of
https://github.com/avwo/whistle.git
synced 2026-10-19 07:01:44 +08:00
589 lines
14 KiB
JavaScript
589 lines
14 KiB
JavaScript
var os = require('os');
|
||
var path = require('path');
|
||
var fse = require('fs-extra2');
|
||
var fs = require('fs');
|
||
var iconv = require('iconv-lite');
|
||
var qs = require('querystring');
|
||
var Buffer = require('safe-buffer').Buffer;
|
||
var zlib = require('./zlib');
|
||
var pkgConf = require('../../package.json');
|
||
var isUtf8 = require('./is-utf8');
|
||
var PassThrough = require('stream').PassThrough;
|
||
|
||
var HEAD_RE = /^head$/i;
|
||
var HOME_DIR_RE = /^[~~]\//;
|
||
var REGISTRY_RE = /^--registry=https?:\/\/[^/?]/;
|
||
var ILLEGAL_TRAILERS = [
|
||
'host',
|
||
'transfer-encoding',
|
||
'content-length',
|
||
'cache-control',
|
||
'te',
|
||
'max-forwards',
|
||
'authorization',
|
||
'set-cookie',
|
||
'content-encoding',
|
||
'content-type',
|
||
'content-range',
|
||
'trailer',
|
||
'connection',
|
||
'upgrade',
|
||
'http2-settings',
|
||
'proxy-connection',
|
||
'transfer-encoding',
|
||
'keep-alive'
|
||
];
|
||
var REMOTE_URL_RE = /^\s*((?:git[+@]|github:)[^\s]+\/whistle\.[a-z\d_-]+(?:\.git)?|https?:\/\/[^\s]+)\s*$/i;
|
||
var WHISTLE_PLUGIN_RE = /^((?:@[\w-]+\/)?whistle\.[a-z\d_-]+)(?:\@([\w.^~*-]*))?$/;
|
||
var HTTP_RE = /^https?:\/\/[^/?]/;
|
||
var SEP_RE = /\s*[|,;\s]+\s*/;
|
||
var AUTH_RE = /^Basic /i;
|
||
var STREAM_OPTS = { highWaterMark: 1 };
|
||
|
||
exports.REMOTE_URL_RE = REMOTE_URL_RE;
|
||
exports.WHISTLE_PLUGIN_RE = WHISTLE_PLUGIN_RE;
|
||
exports.TEMP_PATH_RE = /^temp\/([\da-f]{64}|blank)(\.[\w.-]{1,20})?/;
|
||
|
||
exports.getPlugins = function(argv, isInstall) {
|
||
return argv.filter(function(name, i) {
|
||
if (WHISTLE_PLUGIN_RE.test(name)) {
|
||
return true;
|
||
}
|
||
if (argv[i - 1] === '--registry') {
|
||
return false;
|
||
}
|
||
return isInstall && REMOTE_URL_RE.test(name);
|
||
});
|
||
};
|
||
|
||
exports.getPluginsData = function(data) {
|
||
var cmd = data && data.cmd;
|
||
if (!cmd || typeof cmd !== 'string' || cmd.length > 2048) {
|
||
return;
|
||
}
|
||
var pkgs;
|
||
var registry;
|
||
cmd.trim().split(SEP_RE).forEach(function(name) {
|
||
name = name.trim();
|
||
if (WHISTLE_PLUGIN_RE.test(name)) {
|
||
pkgs = pkgs || [];
|
||
pkgs.push({
|
||
name: RegExp.$1,
|
||
version: RegExp.$2
|
||
});
|
||
} else if (!registry && REGISTRY_RE.test(name)) {
|
||
registry = name.substring(11, 1035);
|
||
}
|
||
});
|
||
return pkgs && {
|
||
registry: registry,
|
||
pkgs: pkgs
|
||
};
|
||
};
|
||
|
||
function isHead(req) {
|
||
return HEAD_RE.test(req.method);
|
||
}
|
||
|
||
exports.isHead = isHead;
|
||
|
||
|
||
exports.getUpdateUrl = function(conf) {
|
||
var url = conf.updateUrl;
|
||
if (url && REMOTE_URL_RE.test(url) && url.length < 2100) {
|
||
return url;
|
||
}
|
||
};
|
||
|
||
exports.noop = function () {};
|
||
|
||
function removeIPV6Prefix(ip) {
|
||
if (typeof ip != 'string') {
|
||
return '';
|
||
}
|
||
return ip.indexOf('::ffff:') === 0 ? ip.substring(7) : ip;
|
||
}
|
||
|
||
exports.removeIPV6Prefix = removeIPV6Prefix;
|
||
|
||
function hasBody(res, req) {
|
||
if (req && isHead(req)) {
|
||
return false;
|
||
}
|
||
var statusCode = res.statusCode;
|
||
return !(
|
||
statusCode == 204 ||
|
||
(statusCode >= 300 && statusCode < 400) ||
|
||
(100 <= statusCode && statusCode <= 199)
|
||
);
|
||
}
|
||
|
||
exports.hasBody = hasBody;
|
||
|
||
function isEmptyObject(a) {
|
||
if (a) {
|
||
for (var i in a) { // eslint-disable-line
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
exports.isEmptyObject = isEmptyObject;
|
||
|
||
exports.lowerCaseify = function (obj, rawNames) {
|
||
var result = {};
|
||
if (!obj) {
|
||
return result;
|
||
}
|
||
Object.keys(obj).forEach(function (name) {
|
||
var value = obj[name];
|
||
if (value !== undefined) {
|
||
var key = name.toLowerCase();
|
||
result[key] = Array.isArray(value) ? value : value + '';
|
||
if (rawNames) {
|
||
rawNames[key] = name;
|
||
}
|
||
}
|
||
});
|
||
return result;
|
||
};
|
||
|
||
exports.removeIllegalTrailers = function (headers) {
|
||
ILLEGAL_TRAILERS.forEach(function (key) {
|
||
delete headers[key];
|
||
});
|
||
};
|
||
|
||
exports.addTrailerNames = function (
|
||
res,
|
||
newTrailers,
|
||
rawNames,
|
||
delTrailers,
|
||
req
|
||
) {
|
||
if (!hasBody(res, req) || isEmptyObject(newTrailers)) {
|
||
return;
|
||
}
|
||
var headers = res.headers;
|
||
delete headers['content-length'];
|
||
delete headers['transfer-encoding'];
|
||
var nameMap = {};
|
||
var curTrailers = headers.trailer;
|
||
if (curTrailers) {
|
||
if (typeof curTrailers === 'string') {
|
||
nameMap[curTrailers.toLowerCase()] = curTrailers;
|
||
} else if (Array.isArray(curTrailers)) {
|
||
curTrailers.forEach(function (key) {
|
||
if (key && typeof key === 'string') {
|
||
nameMap[key.toLowerCase()] = key;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
Object.keys(newTrailers).forEach(function (key) {
|
||
var lkey = key.toLowerCase();
|
||
if (
|
||
(!delTrailers || !delTrailers[lkey]) &&
|
||
ILLEGAL_TRAILERS.indexOf(lkey) === -1
|
||
) {
|
||
nameMap[lkey] = key;
|
||
}
|
||
});
|
||
if (rawNames && !rawNames.trailer) {
|
||
rawNames.trailer = 'Trailer';
|
||
}
|
||
headers.trailer = Object.keys(nameMap).map(function (key) {
|
||
return nameMap[key];
|
||
});
|
||
};
|
||
|
||
exports.onResEnd = function (res, callback) {
|
||
var state = res._readableState || '';
|
||
if (state.endEmitted) {
|
||
return callback();
|
||
}
|
||
res.on('end', callback);
|
||
};
|
||
|
||
var UPGRADE_RE = /^\s*upgrade\s*$/i;
|
||
var WS_RE = /^\s*websocket\s*$/i;
|
||
var CONNECT_RE = /^\s*CONNECT\s*$/i;
|
||
var CONNECT_PROTOS = 'connect:,socket:,tunnel:,conn:,tls:,tcp:'.split(',');
|
||
|
||
exports.isUpgrade = function(options, headers) {
|
||
var p = options.protocol;
|
||
if (p === 'ws:' || p === 'wss:' || options.method === 'UPGRADE') {
|
||
return true;
|
||
}
|
||
return headers && UPGRADE_RE.test(headers.connection);
|
||
};
|
||
|
||
exports.isWebSocket = function(headers) {
|
||
return headers && WS_RE.test(headers.upgrade);
|
||
};
|
||
|
||
exports.isConnect = function(options) {
|
||
return (
|
||
CONNECT_RE.test(options.method) ||
|
||
CONNECT_PROTOS.indexOf(options.protocol) !== -1
|
||
);
|
||
};
|
||
|
||
var PROTOCOL_RE = /^[a-z0-9.-]+:\/\//i;
|
||
|
||
function hasProtocol(url) {
|
||
return PROTOCOL_RE.test(url);
|
||
}
|
||
|
||
function setProtocol(url, isHttps) {
|
||
return hasProtocol(url) ? url : (isHttps ? 'https://' : 'http://') + url;
|
||
}
|
||
|
||
function getProtocol(url) {
|
||
return hasProtocol(url) ? url.substring(0, url.indexOf('://') + 1) : null;
|
||
}
|
||
|
||
function removeProtocol(url, clear) {
|
||
return hasProtocol(url)
|
||
? url.substring(url.indexOf('://') + (clear ? 3 : 1))
|
||
: url;
|
||
}
|
||
|
||
function replaceProtocol(url, protocol) {
|
||
return (protocol || 'http:') + removeProtocol(url);
|
||
}
|
||
|
||
exports.hasProtocol = hasProtocol;
|
||
exports.setProtocol = setProtocol;
|
||
exports.getProtocol = getProtocol;
|
||
exports.removeProtocol = removeProtocol;
|
||
exports.replaceProtocol = replaceProtocol;
|
||
|
||
function getHomedir() {
|
||
//默认设置为`~`,防止Linux在开机启动时Node无法获取homedir
|
||
return (
|
||
(typeof os.homedir == 'function'
|
||
? os.homedir()
|
||
: process.env[process.platform == 'win32' ? 'USERPROFILE' : 'HOME']) ||
|
||
'~'
|
||
);
|
||
}
|
||
|
||
exports.getHomedir = getHomedir;
|
||
|
||
function getHomePath(dir) {
|
||
if (!dir || !HOME_DIR_RE.test(dir)) {
|
||
return dir;
|
||
}
|
||
return path.join(getHomedir(), '.' + dir.substring(1));
|
||
}
|
||
|
||
exports.getHomePath = getHomePath;
|
||
|
||
function getDefaultWhistlePath() {
|
||
return path.join(getHomedir(), '.WhistleAppData');
|
||
}
|
||
|
||
function getWhistlePath() {
|
||
return getHomePath(process.env.WHISTLE_PATH) || getDefaultWhistlePath();
|
||
}
|
||
|
||
exports.getDefaultWhistlePath = getDefaultWhistlePath;
|
||
exports.getWhistlePath = getWhistlePath;
|
||
|
||
function getLogFile(name) {
|
||
var whistlePath = getWhistlePath();
|
||
fse.ensureDirSync(whistlePath);
|
||
return path.join(whistlePath, pkgConf.name + (name ? '-' + name : '') + '.log');
|
||
}
|
||
|
||
exports.getLogFile = getLogFile;
|
||
|
||
function padLeft(n) {
|
||
return n > 9 ? n + '' : '0' + n;
|
||
}
|
||
|
||
function getDate() {
|
||
var date = new Date();
|
||
return date.getFullYear() + padLeft(date.getMonth() + 1) + padLeft(date.getDate());
|
||
}
|
||
|
||
exports.writeLogSync = function(msg) {
|
||
try {
|
||
fs.writeFileSync(getLogFile(), msg, { flag: 'a' });
|
||
} catch (e) {
|
||
msg += '\r\n' + e.stack;
|
||
try {
|
||
fs.writeFileSync(getLogFile('error'), msg, { flag: 'a' });
|
||
} catch (e) {
|
||
try {
|
||
fs.writeFileSync(getLogFile(getDate()), msg + '\r\n' + e.stack, { flag: 'a' });
|
||
} catch (e) {}
|
||
}
|
||
}
|
||
};
|
||
|
||
var SPEC_TAGS = {
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
'\'': ''',
|
||
'`': '`'
|
||
};
|
||
var SPEC_TAG_RE = /[&<>"'`]/g;
|
||
|
||
function transformTag(tag) {
|
||
return SPEC_TAGS[tag] || tag;
|
||
}
|
||
|
||
exports.encodeHtml = function(str) {
|
||
if (typeof str !== 'string') {
|
||
return str;
|
||
}
|
||
return str.replace(SPEC_TAG_RE, transformTag);
|
||
};
|
||
|
||
var SEP_RE_G = /^[^\n\r\S]*```+\s*$/mg;
|
||
|
||
exports.wrapRuleValue = function(key, value, size, policy) {
|
||
if (!key || value == null) {
|
||
return '';
|
||
}
|
||
if (!value || typeof value !== 'string') {
|
||
return '\n``` ' + key + '\n\n```\n';
|
||
}
|
||
var isOverSize = size > 0 && value.length > size;
|
||
if (isOverSize) {
|
||
if (policy === 'ignore') {
|
||
return '';
|
||
}
|
||
if (policy === 'empty') {
|
||
return '\n``` ' + key + '\n\n```\n';
|
||
}
|
||
value = value.substring(0, size);
|
||
}
|
||
var list = value.match(SEP_RE_G);
|
||
if (!list) {
|
||
return '\n``` ' + key + '\n' + value + '\n```\n';
|
||
}
|
||
list = list.map(function(item) {
|
||
return item.trim().length;
|
||
});
|
||
var count = Math.max.apply(null, list) + 1;
|
||
var sep = typeof '`'.repeat === 'function' ? '`'.repeat(count) : '```````````';
|
||
return '\n' + sep + ' ' + key + '\n' + value + '\n' + sep + '\n';
|
||
};
|
||
|
||
exports.isGroup = function(name) {
|
||
return name && name[0] === '\r';
|
||
};
|
||
|
||
var UTF8_RE = /^utf-?8$/i;
|
||
function bufferToString(buffer, encoding) {
|
||
if (typeof encoding === 'string') {
|
||
encoding = encoding.trim();
|
||
} else {
|
||
encoding = null;
|
||
}
|
||
if (Buffer.isBuffer(buffer) && !UTF8_RE.test(encoding) && !isUtf8(buffer)) {
|
||
try {
|
||
buffer = iconv.encode(buffer, encoding || 'GB18030');
|
||
} catch (e) {}
|
||
}
|
||
return String(buffer || '');
|
||
}
|
||
|
||
exports.bufferToString = bufferToString;
|
||
|
||
var URLENCODED_RE = /application\/x-www-form-urlencoded/i;
|
||
var POST_RE = /^post$/i;
|
||
|
||
function isUrlEncoded(req) {
|
||
var type = req.method && req.headers && req.headers['content-type'];
|
||
return type && POST_RE.test(req.method) && URLENCODED_RE.test(type);
|
||
}
|
||
|
||
exports.isUrlEncoded = isUrlEncoded;
|
||
|
||
function queryToString(query, req) {
|
||
if (!req || typeof query !== 'object' || !isUrlEncoded(req)) {
|
||
return query;
|
||
}
|
||
return qs.stringify(query);
|
||
}
|
||
|
||
exports.toBuffer = function(data, req) {
|
||
data = queryToString(data, req);
|
||
if (data == null || Buffer.isBuffer(data)) {
|
||
return data;
|
||
}
|
||
if (typeof data !== 'string') {
|
||
try {
|
||
data = JSON.stringify(data);
|
||
} catch (e) {
|
||
return;
|
||
}
|
||
}
|
||
return Buffer.from(data);
|
||
};
|
||
|
||
|
||
exports.readStream = function(stream, callback) {
|
||
var buffer = null;
|
||
stream.on('data', function(chunk) {
|
||
buffer = buffer ? Buffer.concat([buffer, chunk]) : chunk;
|
||
});
|
||
stream.once('end', function() {
|
||
var buf;
|
||
var text;
|
||
var json;
|
||
var err;
|
||
var jsonErr;
|
||
stream.zlib = zlib;
|
||
var encoding = stream.headers && stream.headers['content-encoding'];
|
||
var getBuffer = function(cb) {
|
||
if (err || buf !== undefined) {
|
||
return cb(err, buf);
|
||
}
|
||
zlib.unzip(encoding, buffer, function(e, ctn) {
|
||
err = e;
|
||
buf = ctn || null;
|
||
cb(err, buf);
|
||
});
|
||
};
|
||
var getText = function(cb, ecd) {
|
||
if (err || text != null) {
|
||
return cb(err, text);
|
||
}
|
||
getBuffer(function(e, ctn) {
|
||
err = e;
|
||
text = bufferToString(ctn, ecd);
|
||
cb(err, text);
|
||
});
|
||
};
|
||
stream.getBuffer = getBuffer;
|
||
stream.getText = getText;
|
||
stream.getJson = function(cb, ecd) {
|
||
getText(function() {
|
||
if (err || jsonErr || !text || json !== undefined) {
|
||
return cb(err || jsonErr, json);
|
||
}
|
||
try {
|
||
if (isUrlEncoded(stream)) {
|
||
json = qs.parse(text);
|
||
} else {
|
||
text = text.trim();
|
||
if (text[0] === '[' || text[0] === '{') {
|
||
json = JSON.parse(text) || null;
|
||
} else {
|
||
json = null;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
jsonErr = e;
|
||
}
|
||
cb(jsonErr, json);
|
||
}, ecd);
|
||
};
|
||
callback(buffer);
|
||
});
|
||
};
|
||
|
||
exports.readJsonSync = function(filepath) {
|
||
try {
|
||
return fse.readJsonSync(filepath);
|
||
} catch (e) {
|
||
try {
|
||
return fse.readJsonSync(filepath);
|
||
} catch (e) {}
|
||
}
|
||
};
|
||
|
||
exports.getRegistry = function(registry) {
|
||
if (!registry || !HTTP_RE.test(registry) || registry.length > 1024) {
|
||
return;
|
||
}
|
||
return registry;
|
||
};
|
||
|
||
function copyFile(src, dest, callback, retry) {
|
||
var execCb = function (e) {
|
||
if (e && !retry) {
|
||
copyFile(src, dest, callback, true);
|
||
} else {
|
||
callback(e);
|
||
}
|
||
};
|
||
if (typeof fs.copyFile === 'function') {
|
||
fs.copyFile(src, dest, execCb);
|
||
} else {
|
||
fse.copy(src, dest, execCb);
|
||
}
|
||
}
|
||
|
||
exports.copyFile = copyFile;
|
||
|
||
exports.getHostPort = function(str) {
|
||
if (!str || (typeof str !== 'string' && typeof str !== 'number')) {
|
||
return;
|
||
}
|
||
if (/^(?:([\w.-]+):)?([1-9]\d{0,4})$/.test(str) || /^\[([\w.:]+)\]:([1-9]\d{0,4})$/.test(str)) {
|
||
return {
|
||
host: RegExp.$1,
|
||
port: parseInt(RegExp.$2, 10)
|
||
};
|
||
}
|
||
};
|
||
|
||
exports.onSocketEnd = function(socket, callback) {
|
||
var execCallback = function (err) {
|
||
socket._hasError = true;
|
||
if (callback) {
|
||
callback(err);
|
||
callback = null;
|
||
}
|
||
};
|
||
if (socket.aborted || socket.destroyed || socket._hasError) {
|
||
return execCallback();
|
||
}
|
||
socket.on('error', execCallback);
|
||
socket.once('close', execCallback);
|
||
socket.once('end', execCallback);
|
||
socket.once('timeout', execCallback);
|
||
};
|
||
|
||
exports.parseAuth = function(auth) {
|
||
var result = {};
|
||
if (!auth) {
|
||
return result;
|
||
}
|
||
auth = AUTH_RE.test(auth) ? auth.substring(6).trim() : auth;
|
||
result.auth = auth;
|
||
auth = Buffer.from(auth, 'base64').toString();
|
||
var index = auth.indexOf(':');
|
||
if (index === -1) {
|
||
result.name = auth;
|
||
result.pass = '';
|
||
} else {
|
||
result.name = auth.substring(0, index);
|
||
result.pass = auth.substring(index + 1);
|
||
}
|
||
return result;
|
||
};
|
||
|
||
exports.formatHost = function(host) {
|
||
if (host[0] === '[') {
|
||
var end = host.length - 1;
|
||
if (host[end] === ']') {
|
||
host = host.substring(1, end);
|
||
}
|
||
}
|
||
return host;
|
||
};
|
||
|
||
exports.createTransform = function() {
|
||
return new PassThrough(STREAM_OPTS);
|
||
};
|