whistle
Version:
HTTP, HTTP2, HTTPS, Websocket debugging proxy
2,004 lines (1,802 loc) • 49 kB
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 zlib = require('zlib');
var net = require('net');
var http = require('http');
var crypto = require('crypto');
var json5 = require('json5');
var extend = require('extend');
var tls = require('tls');
var https = require('https');
var zlibx = require('./zlib');
var pkgConf = require('../../package.json');
var isUtf8 = require('./is-utf8');
var PassThrough = require('stream').PassThrough;
var parseUrl = require('./parse-url');
var ALGORITHM = 'aes-256-cbc';
var gzip = zlib.gzip;
var LOCALHOST = '127.0.0.1';
var CONN_TIMEOUT = 30000;
var LINE_END_RE = /\r\n|\n|\r/;
var HEAD_RE = /^head$/i;
var HOME_DIR_RE = /^[~~]\//;
var REGISTRY_RE = /^--registry=https?:\/\/[^/?]/;
var UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
var RSLASH_RE = /\\/g;
var DIR_RE = /^--dir=(.+)$/;
var UTF8_OPTIONS = { encoding: 'utf8' };
var GZIP_THRESHOLD = 512;
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 TGZ_WHISTLE_PLUGIN_RE = /(?:^(?:file:)?|[\\/])(?:[\w.~-]+-)?whistle\.[a-z\d_-]+-\d+\.\d+\.\d+[\w.-]*\.(?:tgz|tar(?:\.gz)?)$/;
var HTTP_RE = /^https?:\/\/[^/?]/;
var SEP_RE = /\s*[|,;\s]+\s*/;
var RAW_CRLF_RE = /\\n|\\r/g;
var AUTH_RE = /^Basic /i;
var STREAM_OPTS = { highWaterMark: 1 };
var REAL_HOST_HEADER = 'x-whistle-real-host';
var supportsBr = zlib.createBrotliDecompress && zlib.createBrotliCompress;
var TIMEOUT_ERR = new Error('Timeout');
var noop = function () {};
var PATH_SEP_RE = /[\\/]/;
var CUR_PATH_RE = /^\.[\\/]/;
var CLIENT_PORT_HEADER = 'x-whistle-client-port';
var ALLOW_ACK = 'x-whistle-allow-tunnel-ack';
var PLUS_RE = /\+/g;
var TOKEN = '\r\u0000\n\u0003\r';
var PROP_SEP_RE = /(\\*)([|&]|\\[stnrfv])/g;
var TEMP_FILE_RE = /^[\da-f]{64}$/;
exports.isTempFile = function(str) {
return TEMP_FILE_RE.test(str);
};
var PROPS_CHAR = {
s: ' ',
t: '\t',
n: '\n',
r: '\r',
f: '\f',
v: '\v'
};
function replaceToken(s, val) {
return s.split(TOKEN).join(val);
}
exports.parseProps = function(str) {
return str.replace(PROP_SEP_RE, function(_, slash, val) {
var isChar = val.length > 1;
if (isChar) {
slash += '/';
val = val.substring(1);
} else if (!slash) {
return TOKEN;
}
var len = slash.length;
slash = slash.substring(0, Math.floor(len / 2));
if (isChar) {
return slash + (len % 2 ? PROPS_CHAR[val] : val);
}
return slash + (len % 2 ? val : TOKEN);
}).split(TOKEN);
};
var decoder = {
decodeURIComponent: function (s) {
s = replaceToken(s, '+');
return qs.unescape(s);
}
};
var rawDecoder = {
decodeURIComponent: function (s) {
return replaceToken(s, '+');
}
};
var rawDecoder2 = {
decodeURIComponent: function (s) {
return s;
}
};
exports.parseQuery = function (str, sep, eq, escape) {
try {
if (str.indexOf('+') === -1 || str.indexOf(TOKEN) !== -1) {
return qs.parse(str, sep, eq, escape ? rawDecoder2 : undefined);
}
str = str.replace(PLUS_RE, TOKEN);
return qs.parse(str, sep, eq, escape ? rawDecoder : decoder);
} catch (e) {}
return '';
};
exports.ALLOW_ACK = ALLOW_ACK;
exports.CONN_TIMEOUT = CONN_TIMEOUT;
exports.TGZ_FILE_NAME_RE = /^(?:[\w.~-]+-)?whistle\.[a-z\d_-]+-\d+\.\d+\.\d+[\w.-]*\.(?:tgz|tar(?:\.gz)?)$/;
exports.parseUrl = parseUrl;
exports.supportsBr = supportsBr;
exports.SERVICE_HOST = 'admin.weso.pro';
exports.REMOTE_URL_RE = REMOTE_URL_RE;
exports.WHISTLE_PLUGIN_RE = WHISTLE_PLUGIN_RE;
exports.TIMEOUT_ERR = TIMEOUT_ERR;
exports.TEMP_PATH_RE = /^temp\/([\da-f]{64}|blank)(\.[\w.-]{1,20})?/;
exports.REAL_HOST_HEADER = REAL_HOST_HEADER;
exports.CLIENT_PORT_HEADER = CLIENT_PORT_HEADER;
exports.ALPN_PROTOCOL_HEADER = 'x-whistle-alpn-protocol';
exports.CLIENT_ID_HEADER = 'x-whistle-client-id';
exports.CLIENT_IP_HEADER = 'x-forwarded-for';
exports.HTTPS_FIELD = 'x-whistle-https-request';
exports.ACK_HEADER = 'x-whistle-request-tunnel-ack';
exports.ORIGIN_HOST_HEADER = 'x-whistle-origin-host';
exports.AUTH_URL = 'x-auth-html-url';
exports.AUTH_STATUS = 'x-auth-status';
exports.formatPathSep = function (str) {
return str.replace(RSLASH_RE, '/');
};
function getTgzUrl(url, autoComplete) {
if (!TGZ_WHISTLE_PLUGIN_RE.test(url)) {
return;
}
if (HTTP_RE.test(url)) {
return url;
}
var index = url.indexOf(':');
if (index !== -1 && url.substring(0, index) === 'file') {
url = url.substring(index + 1);
}
if (autoComplete && (!PATH_SEP_RE.test(url) || CUR_PATH_RE.test(url))) {
url = path.join(process.cwd(), url);
}
return 'file:' + url;
}
function getPureUrl(url) {
var index = url.indexOf('?');
url = index === -1 ? url : url.substring(0, index);
index = url.indexOf('#');
return index === -1 ? url : url.substring(0, index);
}
exports.getPureUrl = getPureUrl;
exports.existsUpPath = function (str) {
return UP_PATH_REGEXP.test(str);
};
function readFileTextSync(file, safe, opts, retry) {
try {
return fs.readFileSync(file, opts);
} catch (e) {}
if (retry === true) {
return;
}
return safe ? readFileTextSync(file, opts, safe, true) :
fs.readFileSync(file, opts);
}
exports.readFileTextSync = function(file, safe) {
return readFileTextSync(file, safe !== false, UTF8_OPTIONS);
};
exports.readFileBufferSync = function(file, safe) {
return readFileTextSync(file, safe !== false);
};
exports.getPlugins = function(argv, isInstall, restArgv) {
var result = [];
argv.forEach(function(name, i) {
if (WHISTLE_PLUGIN_RE.test(name)) {
return result.push(name);
}
if (!isInstall || argv[i - 1] === '--registry') {
restArgv && restArgv.push(name);
return;
}
if (REMOTE_URL_RE.test(name)) {
return result.push(name);
}
var tgzUrl = getTgzUrl(name, restArgv);
if (tgzUrl) {
result.push(tgzUrl);
} else {
restArgv && restArgv.push(name);
}
});
return result;
};
exports.isPluginAddr = function(name) {
return REMOTE_URL_RE.test(name) || TGZ_WHISTLE_PLUGIN_RE.test(name);
};
exports.parsePlugins = function(data) {
var cmd = typeof data === 'string' ? data : (data && data.cmd);
if (!isString(cmd) || cmd.length > 2048) {
return;
}
var pkgs;
var registry;
var dir;
var isDir;
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);
} else if (!dir) {
if (DIR_RE.test(name)) {
dir = RegExp.$1.trim();
} else if (isDir) {
dir = name;
} else if (name === '--dir') {
isDir = true;
}
}
});
return pkgs && {
registry: registry,
whistleDir: dir,
pkgs: pkgs
};
};
function readJson(filePath, callback) {
fse.readJson(filePath, function (err, json) {
if (!err || err.code === 'ENOENT') {
return callback(err, json);
}
fse.readJson(filePath, callback);
});
}
exports.readJson = readJson;
var PKG_NAME_RE = /[/\\]package\.json$/;
exports.getPeerPlugins = function (pkgs, root, cb) {
var peerPlugins = [];
var curPlugins = {};
var len = pkgs.length;
if (typeof pkgs === 'string') {
pkgs = [{ name: pkgs }];
} else if (!Array.isArray(pkgs)) {
pkgs = [pkgs];
}
if (typeof root === 'function') {
cb = root;
root = null;
}
pkgs.forEach(function(pkg) {
curPlugins[pkg.name] = 1;
});
var index = len;
for (var i = 0; i < len; i++) {
var pkg = pkgs[i];
var pkgFile = pkg.root || root;
if (!PKG_NAME_RE.test(pkgFile)) {
pkgFile = path.join(pkgFile, 'node_modules', pkgs[i].name, 'package.json');
}
readJson(pkgFile, function (err, pkg) {
pkg = !err && pkg && pkg.whistleConfig;
if (pkg) {
var list = pkg.peerPluginList || pkg.peerPlugins;
if (Array.isArray(list) && list.length < 16) {
list.forEach(function (pkgName) {
pkgName = typeof pkgName === 'string' ? pkgName.trim() : null;
if (WHISTLE_PLUGIN_RE.test(pkgName)) {
pkgName = RegExp.$1;
var version = RegExp.$2;
if (!curPlugins[pkgName]) {
curPlugins[pkgName] = 1;
peerPlugins.push({
name: pkgName,
version
});
}
}
});
}
}
if (--index <= 0) {
cb(peerPlugins);
}
});
}
};
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 = noop;
function removeIPV6Prefix(ip) {
if (!isString(ip)) {
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) {
return !a || !Object.keys(a).length;
}
exports.isEmptyObject = isEmptyObject;
function lowerCaseify(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.lowerCaseify = lowerCaseify;
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 (isString(key)) {
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 NO_PROTO_RE = /[^a-zA-Z0-9.-]/;
function hasProtocol(url) {
var index = isString(url) ? url.indexOf('://') : -1;
if (index <= 0) {
return false;
}
return !NO_PROTO_RE.test(url.substring(0, index));
}
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);
}
function formatUrl(pattern) {
var queryString = '';
var queryIndex = pattern.indexOf('?');
if (queryIndex != -1) {
queryString = pattern.substring(queryIndex);
pattern = pattern.substring(0, queryIndex);
}
var index = pattern.indexOf('://');
index = pattern.indexOf('/', index == -1 ? 0 : index + 3);
return (index == -1 ? pattern + '/' : pattern) + queryString;
}
exports.hasProtocol = hasProtocol;
exports.setProtocol = setProtocol;
exports.getProtocol = getProtocol;
exports.removeProtocol = removeProtocol;
exports.replaceProtocol = replaceProtocol;
exports.formatUrl = formatUrl;
var QUERY_RE = /\/[^/]*(?:\?.*)?$/;
exports.getAbsUrl = function(url, fullUrl) {
if (HTTP_RE.test(url)) {
return formatUrl(url);
}
if (url[0] === '/') {
var index = fullUrl.indexOf('/', 8);
url = fullUrl.substring(0, index) + url;
return formatUrl(url);
}
url = fullUrl.replace(QUERY_RE, '') + '/' + url;
return formatUrl(url);
};
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, len) {
n = n + '';
var rest = (len || 2) - n.length;
return rest <= 0 ? n : '0'.repeat(rest) + n;
}
exports.padLeft = padLeft;
exports.getMonth = function(time) {
var date = new Date(time || Date.now());
return date.getFullYear() + padLeft(date.getMonth() + 1);
};
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) {
return isString(str) ? str.replace(SPEC_TAG_RE, transformTag) : str;
};
var SEP_RE_G = /^[^\n\r\S]*```+\s*$/mg;
exports.wrapRuleValue = function(key, value, size, policy) {
if (!key || value == null) {
return '';
}
if (!isString(value)) {
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 readStream(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 = zlibx;
var encoding = stream.headers && stream.headers['content-encoding'];
var getBuffer = function(cb) {
if (err || buf !== undefined) {
return cb(err, buf);
}
zlibx.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);
};
stream._readRawBuffer_ = buffer;
callback(buffer);
});
}
exports.readStream = readStream;
exports.wrapReadStream = function(stream) {
var callbacks = [];
var rawBuffer;
var getRawBuffer = function(callback) {
if (rawBuffer !== undefined || !callbacks) {
return callback(rawBuffer);
}
callbacks.push(callback);
callbacks.length === 1 && readStream(stream, function(buffer) {
rawBuffer = buffer;
callbacks.forEach(function(cb) {
cb(buffer);
});
callbacks = null;
});
};
stream.zlib = zlibx;
stream.getBuffer = function(callback) {
getRawBuffer(function() {
stream.getBuffer(callback);
});
};
stream.getText = function(callback) {
getRawBuffer(function() {
stream.getText(callback);
});
};
stream.getJson = function(callback) {
getRawBuffer(function() {
stream.getJson(callback);
});
};
stream.getRawBuffer = getRawBuffer;
return stream;
};
exports.readJsonSync = function(filepath) {
try {
return fse.readJsonSync(filepath);
} catch (e) {
if (e.code === 'ENOENT') {
return;
}
}
try {
return fse.readJsonSync(filepath);
} catch (e) {}
};
function isUrl(url) {
return HTTP_RE.test(url);
}
exports.isUrl = isUrl;
exports.getRegistry = function(registry) {
if (!registry || !isUrl(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 = parseUrl.formatHost;
exports.createTransform = function() {
return new PassThrough(STREAM_OPTS);
};
function trimUrl(url) {
if (!url) {
return url;
}
var index = url.indexOf('://');
if (index === -1) {
return url.trim();
}
index += 3;
var protocol = url.substring(0, index);
return protocol + url.substring(index).trim();
}
exports.trimUrl = trimUrl;
function getRuleValue(rule) {
return rule.value || rule.path;
}
exports.getRuleValue = getRuleValue;
function getMatcher(rule) {
return trimUrl(rule && (getRuleValue(rule) || rule.matcher));
}
exports.getMatcher = getMatcher;
function getMatcherValue(rule) {
rule = getMatcher(rule);
return rule && removeProtocol(rule, true);
}
exports.getMatcherValue = getMatcherValue;
var SPEC_CHAR_RE = /[|\\{}()[\]^$+?.]/g;
var SPEC_STAR_RE = /[|\\{}()[\]^$+?*.]/g;
exports.escapeRegExp = function (str, withStar) {
if (!str) {
return '';
}
return str.replace(withStar ? SPEC_STAR_RE : SPEC_CHAR_RE, '\\$&');
};
function joinIpPort(ip, port) {
if (!port) {
return ip;
}
if (net.isIPv6(ip)) {
ip = '[' + ip + ']';
}
return ip + ':' + port;
}
exports.joinIpPort = joinIpPort;
var TUNNEL_HOST_RE = /^[^:\/]+\.[^:\/]+:\d+$/;
var TUNNEL_IPV6_HOST_RE = /^\[[^\/]+\]:\d+$/;
exports.isTunnelHost = function(host) {
return host && (TUNNEL_HOST_RE.test(host) || TUNNEL_IPV6_HOST_RE.test(host));
};
var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER + '';
var MIN_SAFE_INTEGER = Math.abs(Number.MIN_SAFE_INTEGER) + '';
var DIG_RE = /^([+-]?)([1-9]\d{0,15})$/;
var NUM_RE = /^(?:0|[1-9]\d*)$/;
var ARR_RE = /\[(0|[1-8]\d{0,15}|9\d{0,14})\]$/;
var DOT_RE = /(\\+)\./g;
function isString(str) {
return str && typeof str === 'string';
}
exports.isString = isString;
function isSafeNumStr(str) {
if (str == '0') {
return true;
}
if (!DIG_RE.test(str)) {
return false;
}
var sign = RegExp.$1 === '-';
var num = RegExp.$2;
if (num.length < 16) {
return true;
}
return num <= (sign ? MIN_SAFE_INTEGER : MAX_SAFE_INTEGER);
}
exports.isSafeNumStr = isSafeNumStr;
function deleteProp(obj, key) {
try {
if (!Array.isArray(obj)) {
delete obj[key];
return;
}
if (NUM_RE.test(key)) {
obj.splice(key, 1);
}
} catch (e) {}
}
exports.deleteProp = deleteProp;
// "key"
// [n]
// [key]
// k.[n]."k[n]".k[n]
function parseKey(key) {
if (!key) {
return key;
}
var first = key[0];
var len = key.length - 1;
var last = key[len];
if (first === '"' && last === '"') {
return key.substring(1, len);
}
var result = ARR_RE.exec(key);
if (!result) {
return key;
}
var list = [];
while(result) {
key = key.slice(0, -result[0].length);
list.unshift(+result[1]);
result = ARR_RE.exec(key);
}
if (key) {
list.unshift(key);
}
return list;
}
function parseKeys(key) {
if (!isString(key)) {
return key;
}
key = key.trim();
if (key.indexOf('.') === -1) {
return parseKey(key);
}
key = key.replace(DOT_RE, function (_, slash) {
var len = slash.length;
slash = slash.substring(0, Math.floor(len / 2));
return slash + (len % 2 ? TOKEN : '.');
});
if (key.indexOf('.') === -1) {
return parseKey(replaceToken(key, '.'));
}
var result = [];
key.split('.').forEach(function(k) {
k = parseKey(replaceToken(k.trim(), '.'));
if (Array.isArray(k)) {
result = result.concat(k);
} else {
result.push(k);
}
});
return result;
}
exports.deleteProps = function(obj, key) {
key = parseKeys(key);
if (!Array.isArray(key)) {
return deleteProp(obj, key);
}
var len = key.length;
if (!len) {
return;
}
if (len === 1) {
return deleteProp(obj, key[0]);
}
--len;
for (var i = 0; i <= len; i++) {
if (!obj) {
return;
}
var k = key[i];
if (i === len) {
return deleteProp(obj, k);
}
obj = obj[k];
}
};
function replaceCrLf(char) {
return char === '\\r' ? '\r' : '\n';
}
function parseLine(line) {
var index = line.indexOf(': ');
if (index === -1) {
index = line.indexOf(':');
if (index === -1) {
index = line.indexOf('=');
}
}
var name, value;
if (index != -1) {
name = line.substring(0, index).trim();
value = line.substring(index + 1).trim();
if (value) {
var fv = value[0];
var lv = value[value.length - 1];
if (fv === lv) {
if (fv === '"' || fv === '\'' || fv === '`') {
value = value.slice(1, -1);
if (value && fv === '`') {
value = value.replace(RAW_CRLF_RE, replaceCrLf);
}
}
} else if (isSafeNumStr(value)) {
value = parseInt(value, 10);
}
}
} else {
name = line.trim();
value = '';
}
return {
name: name,
value: value
};
}
function isNum(n) {
return typeof n === 'number';
}
exports.parsePlainText = function (text, resolveKeys) {
var result;
text.split(LINE_END_RE).forEach(function(line) {
if (!(line = line.trim())) {
return;
}
var obj = parseLine(line);
var name = obj.name;
if (resolveKeys) {
name = parseKeys(obj.name);
}
var value = obj.value;
var isKey = !Array.isArray(name);
if (isKey || name.length <= 1) {
if (isKey) {
result = result || {};
} else {
name = name[0];
result = result || [];
}
result[name] = value;
return;
}
result = result || (isNum(name[0]) ? [] : {});
obj = result;
var lastIndex = name.length - 1;
name.forEach(function(key, i) {
if (i === lastIndex) {
obj[key] = value;
return;
}
var next = obj[key];
if (!next || typeof next !== 'object') {
next = isNum(name[i + 1]) ? [] : {};
}
obj[key] = next;
obj = next;
});
});
return result || {};
};
exports.isWhistleName = function(name) {
return /^[\w.-]{1,30}$/.test(name);
};
var HTTP_PORT_RE = /:80$/;
var HTTPS_PORT_RE = /:443$/;
function removeDefaultPort(host, isHttps) {
return host && host.replace(isHttps ? HTTPS_PORT_RE : HTTP_PORT_RE, '');
}
function getFullUrl(req) {
var headers = req.headers;
var host = headers[REAL_HOST_HEADER];
if (hasProtocol(req.url)) {
var options = parseUrl(req.url);
if (
options.protocol === 'https:' ||
(req.isWs && options.protocol === 'wss:')
) {
req.isHttps = true;
}
req.url = options.path;
if (options.host) {
headers.host = options.host;
}
} else {
req.url = req.url || '/';
if (req.url[0] !== '/') {
req.url = '/' + req.url;
}
}
if (host) {
delete headers[REAL_HOST_HEADER];
}
if (!isString(host)) {
host = headers.host;
if (typeof host !== 'string') {
host = headers.host = '';
}
} else if (headers.host !== host) {
if (isString(headers.host)) {
req._fwdHost = headers.host;
}
headers.host = host;
}
host = removeDefaultPort(host, req.isHttps);
return (req.isWs ? 'ws' : 'http') + (req.isHttps ? 's' : '') + '://' + host + req.url;
}
exports.getFullUrl = getFullUrl;
function getReqOptions(req, port, host) {
var options = parseUrl(getFullUrl(req));
options.headers = req.headers;
options.method = req.method;
options.agent = false;
options.protocol = null;
options.host = host || LOCALHOST;
if (port > 0) {
options.port = port;
}
options.hostname = null;
return options;
}
exports.getReqOptions = getReqOptions;
exports.forwardRequest = function(req, res, port, host) {
var options = getReqOptions(req, port, host);
var destroyed;
var abort = function () {
if (!destroyed) {
destroyed = true;
client.destroy();
}
};
var client = http.request(options, function (_res) {
res.writeHead(_res.statusCode, _res.headers);
_res.pipe(res);
});
req.on('error', abort);
res.on('error', abort);
res.once('close', abort);
client.on('error', function (err) {
abort();
res.emit('error', err);
});
req.pipe(client);
return client;
};
exports.setInternalOptions = function (options, config, isInternal) {
if ((options.url || options.uri) && !options.pluginName) {
options.headers = options.headers || {};
options.headers[config.PROXY_ID_HEADER] = isInternal ? 'internal' : 'internalx';
options.whistleConfig = {
host: config.host || LOCALHOST,
port: config.port
};
}
return options;
};
exports.createHash = function (str) {
var shasum = crypto.createHash('sha1');
shasum.update(str || '');
return shasum.digest('hex');
};
var VER_LEN = 3;
function compareVer(n1, n2, index) {
n1 = parseInt(n1, 10) || 0;
n2 = parseInt(n2, 10) || 0;
if (n1 === n2) {
return 0;
}
return n1 > n2 ? VER_LEN - index : index - VER_LEN;
}
exports.compareVersion = function(v1, v2) {
if (v1 === v2 || !v1 || typeof v1 !== 'string') {
return 0;
}
if (!v2 || typeof v2 !== 'string') {
return 3;
}
v1 = v1.split('.');
v2 = v2.split('.');
for (var i = 0; i < 3; i++) {
var flag = compareVer(v1[i], v2[i], i);
if (flag) {
return Math.max(flag, 0);
}
}
v1 = v1[2];
v2 = v2[2];
if (!v1 || !v2) {
return 0;
}
var i1 = v1.indexOf('-');
var i2 = v2.indexOf('-');
var test1 = i1 === -1 ? '' : v1.substring(i1);
var test2 = i2 === -1 ? '' : v2.substring(i2);
if (test1 === test2) {
return 0;
}
if (!test1 || !test2) {
return test1 ? 0 : 1;
}
return test1 > test2 ? 1 : 0;
};
exports.upperFirst = function(str) {
return isString(str) ? str[0].toUpperCase() + str.substring(1) : '';
};
exports.readWhistleRc = function(options) {
options = options || {};
if (options.rcPath === 'none') {
return;
}
var rcPath = options.rcPath || path.join(getHomedir(), '.whistlerc');
var rcText = (readFileTextSync(rcPath, true, UTF8_OPTIONS) || '').trim();
if (!rcText) {
return;
}
var storage = options.storage ? options.storage + '.' : '';
var wildcard = '*.';
var result;
rcText.split(LINE_END_RE).forEach(function(line) {
if (!(line = line.trim()) || line[0] === '#') {
return;
}
var index = line.indexOf(': ');
if (index === -1) {
index = line.indexOf('=');
}
if (index != -1) {
var name = line.substring(0, index).trim();
var value = name && line.substring(index + 1).trim();
if (value) {
if (name.indexOf(storage) !== 0) {
if (name.indexOf(wildcard) !== 0) {
return;
}
name = name.substring(wildcard.length);
} else {
name = name.substring(storage.length);
}
if (value[0] === '"' && value[value.length - 1] === '"') {
value = value.slice(1, -1);
}
result = result || {};
result[name] = value;
}
}
});
return result;
};
var GZIP_RE = /\bgzip\b/i;
var canGzip = function (req) {
return GZIP_RE.test(req.headers['accept-encoding']);
};
function sendGzipData(res, headers, buffer) {
if (headers) {
headers['Content-Encoding'] = 'gzip';
headers['Content-Length'] = buffer.length;
res.writeHead(200, headers);
res.end(buffer);
} else {
res.setHeader('Content-Encoding', 'gzip');
res.setHeader('Content-Length', buffer.length);
res.send(buffer);
}
}
function sendRes(res, status, text) {
res.status(status);
if (text && text[0] === '<') {
res.send(text);
} else {
res.type('text').end(text);
}
}
exports.sendRes = sendRes;
exports.sendGzip = function (req, res, data) {
if (!canGzip(req)) {
return res.json(data);
}
var str = JSON.stringify(data);
if (str.length < GZIP_THRESHOLD) {
return res.json(data);
}
gzip(str, function(err, result) {
if (err) {
try {
res.json(data);
} catch (e) {
sendRes(res, 500, 'Internal Server Error');
}
return;
}
sendGzipData(res, {
'Content-Type': 'application/json; charset=utf-8'
}, result);
});
};
exports.sendGzipText = function(req, res, headers, text, gzipText) {
if (!text || !canGzip(req) || (!gzipText && text.length < GZIP_THRESHOLD)) {
headers && res.writeHead(200, headers);
return headers ? res.end(text) : res.send(text);
}
if (gzipText) {
return sendGzipData(res, headers, gzipText);
}
gzip(text, function(err, result) {
if (err) {
headers && res.writeHead(200, headers);
return headers ? res.end(text) : res.send(text);
}
sendGzipData(res, headers, result);
});
};
exports.checkHistory = function(data) {
if (
typeof data.url === 'string' &&
typeof data.method === 'string' &&
typeof data.headers === 'string'
) {
data.url = data.url.trim();
data.headers = data.headers.trim();
data.method = data.method.trim();
if (!data.body) {
data.body = '';
return true;
}
return typeof data.body === 'string';
}
};
var TLSV2_CIPHERS = ['DHE-RSA-AES256-GCM-SHA384',
'DHE-RSA-AES128-GCM-SHA256',
'AES256-GCM-SHA384',
'ECDHE-ECDSA-AES256-GCM-SHA384'].join(':');
var TLS_OPTIONS = { ciphers: TLSV2_CIPHERS };
function getTlsOptions(rules) {
var cipher = rules && rules.cipher;
var opts = cipher && cipher.__tlsOptions;
opts = opts && opts._opts;
if (!opts) {
return TLS_OPTIONS;
}
if (!opts.ciphers) {
opts.ciphers = TLSV2_CIPHERS;
}
return opts;
}
exports.getTlsOptions = getTlsOptions;
function toLowerCase(str) {
return typeof str == 'string' ? str.trim().toLowerCase() : str;
}
exports.toLowerCase = toLowerCase;
function getContentEncoding(headers) {
var encoding = toLowerCase(
(headers && headers['content-encoding']) || headers
);
return encoding === 'gzip' || (supportsBr && encoding === 'br') ||
encoding === 'deflate' ? encoding : null;
}
exports.getContentEncoding = getContentEncoding;
function getUnzipStream(headers) {
switch (getContentEncoding(headers)) {
case 'gzip':
return zlib.createGunzip();
case 'br':
return supportsBr && zlib.createBrotliDecompress();
case 'deflate':
return zlib.createInflate();
}
}
exports.getUnzipStream = getUnzipStream;
var G_NON_LATIN1_RE = /\s|[^\x00-\xFF]/gu;
function safeEncodeURIComponent(ch) {
try {
return encodeURIComponent(ch);
} catch (e) {}
return ch;
}
exports.safeEncodeURIComponent = safeEncodeURIComponent;
/**
* 解析一些字符时,encodeURIComponent可能会抛异常,对这种字符不做任何处理
* see: http://stackoverflow.com/questions/16868415/encodeuricomponent-throws-an-exception
* @param ch
* @returns
*/
exports.encodeNonLatin1Char = function (str) {
if (!isString(str)) {
return '';
}
return str.replace(G_NON_LATIN1_RE, safeEncodeURIComponent);
};
function toUpperCase(str) {
return typeof str == 'string' ? str.trim().toUpperCase() : str;
}
exports.toUpperCase = toUpperCase;
var CHARSET_RE = /charset=([\w-]+)/i;
function getCharset(str) {
var charset;
if (CHARSET_RE.test(str)) {
charset = RegExp.$1;
if (!iconv.encodingExists(charset)) {
return;
}
}
return charset;
}
exports.getCharset = getCharset;
function toBuffer(buf, charset) {
if (buf == null || Buffer.isBuffer(buf)) {
return buf;
}
if (typeof buf === 'object') {
try {
buf = JSON.stringify(buf);
} catch (e) {}
} else {
buf = String(buf);
}
if (!buf) {
return;
}
if (charset && typeof charset === 'string' && !UTF8_RE.test(charset)) {
try {
charset = charset.toLowerCase();
if (charset === 'base64') {
return Buffer.from(buf, 'base64');
}
return iconv.encode(buf, charset);
} catch (e) {}
}
return Buffer.from(buf);
}
exports.toBuffer = toBuffer;
function hasRequestBody(req) {
req = typeof req == 'string' ? req : req.method;
if (typeof req != 'string') {
return false;
}
req = req.toUpperCase();
return !(
req === 'GET' ||
req === 'HEAD' ||
req === 'OPTIONS' ||
req === 'CONNECT'
);
}
exports.hasRequestBody = hasRequestBody;
function getMethod(method) {
if (typeof method !== 'string') {
return 'GET';
}
return method.trim().toUpperCase() || 'GET';
}
exports.getMethod = getMethod;
function evalJson(str) {
try {
return json5.parse(str);
} catch (e) {}
}
exports.parseRawJson = evalJson;
exports.isJson = function (str) {
str = str && typeof str === 'string' && str.trim();
if (!str) {
return false;
}
if (str === '{}') {
return true;
}
var first = str[0];
var last = str[str.length - 1];
if ((first !== '{' || last !== '}' || str.indexOf(':') === -1) && (first !== '[' || last !== ']')) {
return false;
}
return !!evalJson(str);
};
var CRLF_RE = /\r\n|\r|\n/g;
function parseHeaders(headers, rawNames) {
if (typeof headers == 'string') {
headers = headers.split(CRLF_RE);
}
var _headers = {};
headers.forEach(function (line) {
var index = line.indexOf(':');
var value;
if (index != -1) {
value = line.substring(index + 1).trim();
var rawName = line.substring(0, index).trim();
var name = rawName.toLowerCase();
var list = _headers[name];
if (rawNames) {
rawNames[name] = rawName;
}
if (list) {
if (!Array.isArray(list)) {
_headers[name] = list = [list];
}
list.push(value);
} else {
_headers[name] = value;
}
}
});
return lowerCaseify(_headers);
}
exports.parseHeaders = parseHeaders;
function isCiphersError(e) {
var c = e.code;
return (
c === 'EPROTO' || c === 'ERR_SSL_BAD_ECPOINT' || c === 'ERR_SSL_VERSION_OR_CIPHER_MISMATCH' ||
String(e.message).indexOf('disconnected before secure TLS connection was established') !== -1
);
}
exports.isCiphersError = isCiphersError;
exports.connect = function (options, callback) {
var socket, timer, done, retry;
var execCallback = function (err) {
clearTimeout(timer);
timer = null;
if (!done) {
done = true;
err ? callback(err) : callback(null, socket);
}
};
var handleConnect = function () {
execCallback();
};
var handleError = function (err) {
if (done) {
return;
}
socket.removeAllListeners();
socket.on('error', noop);
socket.destroy(err);
clearTimeout(timer);
if (retry) {
return execCallback(err);
}
retry = true;
timer = setTimeout(handleTimeout, 12000);
try {
if (options.ALPNProtocols && err && isCiphersError(err)) {
extend(options, getTlsOptions(options._rules));
}
socket = sockMgr.connect(options, handleConnect);
} catch (e) {
return execCallback(e);
}
socket.on('error', handleError);
socket.on('close', function (err) {
!done && execCallback(err || new Error('closed'));
});
};
var handleTimeout = function () {
handleError(TIMEOUT_ERR);
};
var sockMgr = options.ALPNProtocols ? tls : net;
timer = setTimeout(handleTimeout, 6000);
try {
socket = sockMgr.connect(options, handleConnect);
} catch (e) {
return execCallback(e);
}
socket.on('error', handleError);
};
function getRemoteAddr(req) {
try {
var socket = req.socket || req;
if (!socket._remoteAddr) {
var ip = removeIPV6Prefix(socket.remoteAddress) || LOCALHOST;
socket._remoteAddr = ip;
}
return socket._remoteAddr;
} catch (e) {}
return LOCALHOST;
}
exports.getRemoteAddr = getRemoteAddr;
function getRemotePort(req) {
try {
var socket = req.socket || req;
if (socket._remotePort == null) {
var port = socket.remotePort;
socket._remotePort = port > 0 ? port : '0';
}
return socket._remotePort;
} catch (e) {}
return '0';
}
exports.getRemotePort = getRemotePort;
exports.getClientPort = function (req, config) {
var headers = req.headers || {};
var port = headers[CLIENT_PORT_HEADER];
if (port > 0) {
return port;
}
return getRemotePort(req, config);
};
function isLocalIp(ip) {
if (!isString(ip)) {
return true;
}
return ip.length < 7 || ip === LOCALHOST;
}
exports.isLocalIp = isLocalIp;
function setRawHeader(headers, name, value) {
var keys = Object.keys(headers);
for (var i = 0, len = keys.length; i < len; i++) {
var key = keys[i];
if (key.toLowerCase() === name) {
headers[key] = value;
return;
}
}
headers[name] = value;
}
exports.setRawHeader = setRawHeader;
function getTunnelPath(headers) {
var host = headers.host || headers.Host;
if (host) {
return host;
}
var keys = Object.keys(headers);
for (var i = 0, len = keys.length; i < len; i++) {
var key = keys[i];
if (key.toLowerCase() === 'host') {
return headers[key];
}
}
}
exports.connectInner = function (options, cb, config) {
var headers = options.headers || {};
var proxyOptions = {
method: 'CONNECT',
agent: false,
proxyTunnelPath: options.proxyTunnelPath,
enableIntercept: options.enableIntercept,
path: getTunnelPath(headers),
host: options.proxyHost,
port: options.proxyPort,
rejectUnauthorized: config.rejectUnauthorized,
headers: headers
};
var httpModule = http;
if (options.proxyServername) {
proxyOptions.servername = options.proxyServername;
httpModule = https;
}
var auth = options.proxyAuth || options.auth;
if (auth) {
auth = Buffer.isBuffer(auth) ? auth : Buffer.from(auth + '');
auth = 'Basic ' + auth.toString('base64');
setRawHeader(headers, 'proxy-authorization', auth);
}
var timer = setTimeout(function () {
if (req) {
req.emit('error', TIMEOUT_ERR);
req.destroy();
req.socket && req.socket.destroy();
}
}, CONN_TIMEOUT);
var req = httpModule.request(proxyOptions);
req.on('error', noop);
req
.on('connect', function (res, socket) {
clearTimeout(timer);
socket.on('error', noop);
if (res.statusCode !== 200) {
var err = new Error(
'Tunneling socket could not be established, statusCode=' +
res.statusCode
);
err.statusCode = res.statusCode;
socket.destroy();
process.nextTick(function () {
req.emit('error', err);
});
return;
}
if (res.headers[ALLOW_ACK]) {
socket.write('1');
}
cb(socket, res);
})
.end();
return req;
};
// AES 加密
exports.encryptAES = function (text, secretKey, salt) {
var key = crypto.scryptSync(secretKey, salt || '5b6af7b9884e1165', 32);
var iv = crypto.randomBytes(16);
var cipher = crypto.createCipheriv(ALGORITHM, key, iv);
var encrypted = cipher.update(text, 'utf8', 'hex');
return iv.toString('hex') + '\n' + encrypted + cipher.final('hex');
};
// AES 解密
exports.decryptAES = function (text, secretKey, salt) {
text = text.split('\n');
if (text.length !== 2) {
throw new Error('Invalid encrypted text');
}
var key = crypto.scryptSync(secretKey, salt || '5b6af7b9884e1165', 32);
var iv = Buffer.from(text[0], 'hex');
var decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
var decrypted = decipher.update(text[1], 'hex', 'utf8');
return decrypted + decipher.final('utf8');
};
function getStat(filepath, callback) {
fs.stat(filepath, function (err, stat) {
if (stat) {
return callback(err, stat);
}
if (err && err.code === 'ENOENT') {
return callback(err);
}
fs.stat(filepath, callback);
});
}
exports.getStat = getStat;
exports.getStatSync = function (filepath) {
try {
return fs.statSync(filepath);
} catch (e) {
if (e.code === 'ENOENT') {
return;
}
}
try {
return fs.statSync(filepath);
} catch (e) {}
};
exports.walkInterfaces = function (callback) {
var interfaces = os.networkInterfaces();
Object.keys(interfaces).forEach(function (name) {
var list = interfaces[name];
Array.isArray(list) && list.forEach(function (iface) {
callback(iface, name);
});
});
};
var DATA_HEADER = 'x-whistle-session-info-5b6a_f7b9-88xe_1165_';
var FLAGS = ['_existsCustomCert', '_enableCapture', '_isUpgrade', '_noDecompress',
'_isUIRequest', 'fromTunnel', 'fromComposer', 'isPluginReq'];
var RAW_KEYS = ['_sniType', 'clientIp', 'clientPort', '_remoteAddr', '_remotePort', 'reqId'];
var ENCODE_KEYS = ['sniRuleValue', 'hostIp', '_pipeValue', '_hostValue', '_proxyValue',
'_pacValue', 'customParser', 'globalValue', 'serverName', 'commonName', 'fullUrl',
'method', '_statusCode', '_relativeUrl', '_ruleUrl', '_finalUrl', '_pluginVarsValue',
'_globalPluginVarsValue', '_ruleValue', '_extraUrl', '_ruleProtocol', '_rawPattern', 'hasCertCache'];
var RULES_KEYS = ['host', 'proxy', 'pac'];
var ALL_KEYS = FLAGS.concat(RAW_KEYS).concat(ENCODE_KEYS);
var DECODE_KEYS = ENCODE_KEYS.concat(RULES_KEYS);
var LAST_FLAGS_INDEX = FLAGS.length;
var LAST_RAW_INDEX = LAST_FLAGS_INDEX + RAW_KEYS.length;
var LAST_ENCODE_INDEX = LAST_RAW_INDEX + ENCODE_KEYS.length;
function getValue(rule) {
return rule && joinIpPort(getMatcherValue(rule) || '', rule.port);
}
exports.setSessionInfo = function(req, headers) {
var result = [];
FLAGS.forEach(function(flag, index) {
if (req[flag]) {
result[index] = 1;
}
});
RAW_KEYS.forEach(function(key, index) {
var value = req[key];
if (value != null) {
result[LAST_FLAGS_INDEX + index] = value;
}
});
ENCODE_KEYS.forEach(function(key, index) {
var value = req[key];
if (value) {
result[LAST_RAW_INDEX + index] = safeEncodeURIComponent(value);
}
});
var rules = req.rules || '';
RULES_KEYS.forEach(function(key, index) {
var value = getValue(rules[key]);
if (value) {
result[LAST_ENCODE_INDEX + index] = safeEncodeURIComponent(value);
}
});
result = result.join();
if (result) {
headers[DATA_HEADER] = result;
} else {
delete headers[DATA_HEADER];
}
return headers;
};
exports.getSessionInfo = function(headers) {
var data = headers[DATA_HEADER];
delete headers[DATA_HEADER];
if (!isString(data)) {
return;
}
var result = {};
var rawResult = {};
data = data.split(',');
ALL_KEYS.forEach(function(flag, index) {
result[flag] = data[index] || '';
});
DECODE_KEYS.forEach(function(key, index) {
var value = data[LAST_RAW_INDEX + index];
if (value) {
rawResult[key] = value;
try {
result[key] = decodeURIComponent(value);
} catch (e) {
result[key] = value;
}
}
});
result._ = rawResult;
return result;
};