whistle
Version:
HTTP, HTTP2, HTTPS, Websocket debugging proxy
717 lines (669 loc) • 20 kB
JavaScript
var rules = require('./index');
var util = require('../util');
var config = require('../config');
var Storage = require('./storage');
var httpMgr = require('../util/http-mgr');
var INTERVAL = 1000 * 60 * 60 * 2;
var rulesStorage = new Storage(
config.rulesDir,
{ Default: true },
config.disableWebUI
);
var valuesStorage = new Storage(config.valuesDir, null, config.disableWebUI);
var propertiesStorage = new Storage(config.propertiesDir, { composerHistory: true }, config.disableWebUI);
var LINE_END_RE = /\n|\r\n|\r/g;
var SPACE_RE = /\s/;
var MAX_COUNT_BY_IMPORT = 100;
var inlineValues = {};
var proxy, pluginMgr;
var serviceRules;
var mockRules;
var dnsOrder = propertiesStorage.getProperty('dnsOrder');
config.ipv6Only = config.ipv6Only || !!propertiesStorage.getProperty('ipv6Only');
if (!config.dnsOrder && !config.setDefaultResultOrder(dnsOrder)) {
config.dnsOrder = config.defaultDnsOrder;
}
function limitValueLen(name, len) {
var value = propertiesStorage.getProperty(name);
if (typeof value !== 'string') {
propertiesStorage.setProperty(name, name);
} else if (value.length > len) {
propertiesStorage.setProperty(name, value.substring(0, len));
}
}
limitValueLen('Custom1', 16);
limitValueLen('Custom2', 16);
/**
* rules
*/
function handleInlineValues(text, file) {
return util.resolveInlineValues(text, inlineValues, file);
}
function reverseRules(text, file) {
if (!text) {
return '';
}
text = handleInlineValues(text, file);
text = text.split(LINE_END_RE).reverse();
return text.join('\n');
}
if (config.networkMode && !config.shadowRulesMode) {
exports.parse = util.noop;
} else {
httpMgr.addChangeListener(function() {
var disableRules =
!config.notAllowedDisableRules &&
propertiesStorage.getProperty('disabledAllRules');
var shadowRules =
disableRules && config.allowDisableShadowRules ? null : config.shadowRules;
var backRulesFirst =
!config.disabledBackOption &&
propertiesStorage.getProperty('backRulesFirst') === true;
disableRules = disableRules || config.pluginsMode || config.shadowRulesMode;
var defaultRules = disableRules || defaultRulesIsDisabled() ? null : getDefaultRules();
var rulesList = [];
var resolveRemoteRules = util.getRemoteRulesResolver(inlineValues);
var addRules = function(text, name, type) {
if (text) {
var file = (type || 'File: ') + name;
if (backRulesFirst && !type) {
text = resolveRemoteRules(reverseRules(text, file), file);
rulesList.unshift({
resolved: true,
file: file,
text: text
});
} else {
text = resolveRemoteRules(handleInlineValues(text, file), file);
rulesList.push({
resolved: true,
file: file,
text: text
});
}
}
};
if (!disableRules && !config.multiEnv) {
getAllRulesFile().forEach(function (file) {
if (file.selected && !util.isGroup(file.name)) {
addRules(file.data, file.name);
}
});
}
addRules(defaultRules, 'Default');
addRules(mockRules, '', 'Mock Rules');
addRules(serviceRules, '', 'Service Rules');
addRules(shadowRules, '', 'Shadow Rules');
rules._rawRulesText = rulesList.map(function(item) {
return item.text;
}).join('\r\n');
pluginMgr.emit('updateRules', true);
rules.parse(rulesList, null, inlineValues);
inlineValues = {};
});
}
function parseRules() {
return httpMgr.triggerChange();
}
exports.parseRules = parseRules;
function setDefaultRules(data) {
data = typeof data != 'string' ? '' : data;
var oldData = rulesStorage.getProperty('defalutRules') || '';
rulesStorage.setProperty('defalutRules', data);
parseRules();
return data !== oldData;
}
function getDefaultRules() {
return rulesStorage.getProperty('defalutRules');
}
function disableDefaultRules() {
rulesStorage.setProperty('disabledDefalutRules', true);
parseRules();
proxy.emit('rulesDataChange', 'disabledDefalutRules', true);
}
function enableDefaultRules() {
rulesStorage.setProperty('disabledDefalutRules', false);
parseRules();
proxy.emit('rulesDataChange', 'disabledDefalutRules', false);
}
function defaultRulesIsDisabled() {
return rulesStorage.getProperty('disabledDefalutRules');
}
function selectRulesFile(file) {
if (!rulesStorage.existsFile(file) || config.multiEnv) {
return;
}
var isGroup = util.isGroup(file);
var selectedList = isGroup || allowMultipleChoice() ? getSelectedRulesList() : [];
if (!isGroup && selectedList.indexOf(file) == -1) {
selectedList.push(file);
rulesStorage.setProperty('selectedList', selectedList);
}
parseRules();
proxy.emit('rulesDataChange', 'selectedList', selectedList);
return selectedList;
}
function unselectRulesFile(file, force, all) {
if (!force && config.multiEnv) {
return;
}
var selectedList = getSelectedRulesList();
var hasChanged;
all = all || [file];
all.forEach(function(name) {
var index = selectedList.indexOf(name);
if (index != -1) {
selectedList.splice(index, 1);
hasChanged = true;
}
});
if (hasChanged) {
rulesStorage.setProperty('selectedList', selectedList);
parseRules();
proxy.emit('rulesDataChange', 'selectedList', selectedList);
}
return selectedList;
}
function allowMultipleChoice() {
return (
!config.disabledMultipleOption &&
propertiesStorage.getProperty('allowMultipleChoice')
);
}
function clearSelection() {
rulesStorage.setProperty('selectedList', []);
proxy.emit('rulesDataChange', 'selectedList', []);
parseRules();
}
function getSelectedRulesList() {
if (config.multiEnv) {
return [];
}
var selectedList = rulesStorage.getProperty('selectedList');
if (!Array.isArray(selectedList)) {
selectedList = [];
rulesStorage.setProperty('selectedList', selectedList);
}
return selectedList;
}
function removeRulesFile(file, all) {
if (all) {
if (rulesStorage.removeGroup(file)) {
unselectRulesFile(file, true, all);
return true;
}
return false;
}
unselectRulesFile(file, true);
return rulesStorage.removeFile(file);
}
function renameRulesFile(file, newFile) {
if (!rulesStorage.renameFile(file, newFile)) {
return;
}
var selectedList = getSelectedRulesList();
var index = selectedList.indexOf(file);
if (index != -1) {
selectedList[index] = newFile;
rulesStorage.setProperty('selectedList', selectedList);
}
return true;
}
function addRulesFile(file, data) {
return rulesStorage.writeFile(file, data);
}
function getAllRulesFile() {
var list = rulesStorage.getFileList();
var selectedList = getSelectedRulesList();
list.forEach(function (file) {
file.selected = !util.isGroup(file.name) && selectedList.indexOf(file.name) != -1;
});
return list;
}
function getFirstGroup(storage) {
var list = storage.getFileList();
for (var i = 0, len = list.length; i < len; i++) {
if (util.isGroup(list[i].name)) {
return list[i];
}
}
}
function resetRulesIfResort(fromName, toName) {
var selectedList = getSelectedRulesList();
if (
selectedList.indexOf(fromName) == -1 &&
selectedList.indexOf(toName) == -1
) {
return;
}
parseRules();
}
function moveRulesTo(fromName, toName, clientId, group, toTop) {
if (rulesStorage.moveTo(fromName, toName, group, toTop)) {
resetRulesIfResort(fromName, toName);
config.setModified(clientId, true);
proxy.emit('rulesDataChange', 'move', fromName, toName);
return true;
}
}
exports.rules = {
getConfig: function() {
var list = [
{
name: 'Default',
selected: !defaultRulesIsDisabled()
}
];
var selectedList = getSelectedRulesList();
rulesStorage.getFileList().forEach(function (file) {
if (!util.isGroup(file.name)) {
list.push({ name: file.name, selected: selectedList.indexOf(file.name) !== -1 });
}
});
return {
pluginsDisabled: propertiesStorage.getProperty('disabledAllPlugins'),
disabled: !config.notAllowedDisableRules && propertiesStorage.getProperty('disabledAllRules'),
list: list
};
},
disableAllRules: function(disabled) {
propertiesStorage.setProperty('disabledAllRules', disabled);
parseRules();
proxy.emit('rulesDataChange', 'disabledAllRules', disabled);
},
getRawRulesText: function() {
return rules._rawRulesText || '';
},
getEnabledRules: function() {
return rules.getEnabledRules();
},
getMFlag: function() {
return rules.getMFlag();
},
moveGroupToTop: function(groupName, clientId) {
var result = rulesStorage.moveGroupToTop(groupName);
if (result) {
config.setModified(clientId, true);
proxy.emit('rulesDataChange', 'moveGroupToTop');
}
return result;
},
exists: function(name) {
return rulesStorage.existsFile(name);
},
recycleBin: rulesStorage.recycleBin,
enableBackRulesFirst: function (backRulesFirst) {
var curFlag = propertiesStorage.getProperty('backRulesFirst') === true;
if (curFlag !== backRulesFirst) {
propertiesStorage.setProperty('backRulesFirst', backRulesFirst);
parseRules();
proxy.emit('rulesDataChange', 'backRulesFirst', backRulesFirst);
}
},
getAllData: function() {
var list = getAllRulesFile();
list.unshift({
name: 'Default',
data: getDefaultRules(),
selected: !defaultRulesIsDisabled()
});
return {
backRulesFirst: propertiesStorage.getProperty('backRulesFirst') === true,
allowMultipleChoice: !!propertiesStorage.getProperty('allowMultipleChoice'),
list: list
};
},
moveTo: moveRulesTo,
moveToTop: function (name, clientId) {
var first = name && getAllRulesFile()[0];
first && moveRulesTo(name, first.name, clientId, null, true);
},
moveToGroup: function(name, groupName, isTop) {
if (rulesStorage.moveToGroup(name, groupName, isTop)) {
proxy.emit('rulesDataChange', 'moveToGroup', name, groupName, isTop);
}
},
get: function (file) {
return rulesStorage.readFile(file);
},
remove: function (file, clientId, all) {
if (removeRulesFile(file, all)) {
config.setModified(clientId, true);
proxy.emit('rulesDataChange', 'remove', file);
}
},
getFirstGroup: function() {
return getFirstGroup(rulesStorage);
},
add: function (file, data, clientId) {
if (file === 'Default') {
return;
}
var result = addRulesFile(file, data);
if (result) {
config.setModified(clientId, true);
proxy.emit('rulesDataChange', 'add', file);
}
return result;
},
rename: function (file, newFile, clientId) {
if (renameRulesFile(file, newFile)) {
config.setModified(clientId, true);
proxy.emit('rulesDataChange', 'rename', file, newFile);
}
},
select: selectRulesFile,
unselect: unselectRulesFile,
list: getAllRulesFile,
getDefault: getDefaultRules,
setDefault: function (value, clientId) {
if (setDefaultRules(value)) {
config.setModified(clientId, true);
proxy.emit('rulesDataChange', 'add', 'Default');
}
},
enableDefault: enableDefaultRules,
disableDefault: disableDefaultRules,
defaultRulesIsDisabled: defaultRulesIsDisabled,
parseRules: parseRules,
clearSelection: clearSelection,
getSelectedList: getSelectedRulesList
};
/**
* values
*/
function addValuesFile(file, data) {
return valuesStorage.writeFile(file, data);
}
exports.removeBatch = function(obj, body) {
var list = body.list;
var name = body.name;
if (!Array.isArray(list) || !list.length) {
list = [name];
}
list.forEach(function(item) {
obj.remove(item, body.clientId);
});
};
exports.values = {
recycleBin: valuesStorage.recycleBin,
exists: function(name) {
return valuesStorage.existsFile(name);
},
moveTo: function (fromName, toName, clientId, group, toTop) {
if (valuesStorage.moveTo(fromName, toName, group, toTop)) {
config.setModified(clientId);
proxy.emit('valuesDataChange', 'move', fromName, toName);
return true;
}
},
moveToGroup: function(name, groupName, isTop) {
if (valuesStorage.moveToGroup(name, groupName, isTop)) {
proxy.emit('valuesDataChange', 'moveToGroup', name, groupName, isTop);
}
},
getFirstGroup: function() {
return getFirstGroup(valuesStorage);
},
add: function (file, data, clientId) {
var result = addValuesFile(file, data);
if (result) {
config.setModified(clientId);
proxy.emit('valuesDataChange', 'add', file);
}
return result;
},
get: function (file) {
return valuesStorage.readFile(file);
},
remove: function remove(file, clientId, all) {
if (all ? valuesStorage.removeGroup(file) : valuesStorage.removeFile(file)) {
config.setModified(clientId);
proxy.emit('valuesDataChange', 'remove', file);
}
},
rename: function (file, newFile, clientId) {
if (valuesStorage.renameFile(file, newFile)) {
config.setModified(clientId);
proxy.emit('valuesDataChange', 'rename', file, newFile);
}
},
list: function list() {
return valuesStorage.getFileList();
}
};
setTimeout(function getWhistleVersion() {
util.getLatestVersion('https://registry.npmjs.org/whistle', function (ver) {
ver && propertiesStorage.writeFile('latestVersion', ver);
setTimeout(getWhistleVersion, INTERVAL);
});
if (config.checkUpdateClient) {
util.getLatestVersion('https://raw.githubusercontent.com/avwo/whistle-client/main/package.json', function (ver) {
ver && propertiesStorage.writeFile('latestClientVersion', ver);
}, 'version' );
}
}, 1000); //等待package的信息配置更新完成
function setEnableCapture(enable) {
config.isEnableCapture = enable;
propertiesStorage.setProperty('interceptHttpsConnects', enable);
}
if (config.persistentCapture && config.isEnableCapture) {
setEnableCapture(true);
}
exports.setDisabledCertFile = function(filename, disabled) {
var len = util.isString(filename) ? filename.length : 0;
if (!len || len > 100) {
return;
}
var list = propertiesStorage.getProperty('disabledCertFiles');
if (!Array.isArray(list)) {
list = [];
}
var index = list.indexOf(filename);
if (disabled) {
if (index !== -1) {
list.splice(index, 1);
}
list.push(filename);
if (list.length > 256) {
list = list.slice(-256);
}
propertiesStorage.setProperty('disabledCertFiles', list);
} else if (index !== -1) {
list.splice(index, 1);
propertiesStorage.setProperty('disabledCertFiles', list);
}
};
exports.isDisabledCertFile = function(filename) {
var list = propertiesStorage.getProperty('disabledCertFiles');
return Array.isArray(list) && list.indexOf(filename) !== -1;
};
exports.properties = {
setIPv6Only: function(checked) {
checked = !!checked;
propertiesStorage.setProperty('ipv6Only', checked);
config.ipv6Only = checked;
},
setDnsOrder: function(order) {
if (config.setDefaultResultOrder(order)) {
order = +order;
config.dnsOrder = order;
propertiesStorage.setProperty('dnsOrder', order);
}
},
getLatestVersion: function (name) {
var version = propertiesStorage.readFile(name || 'latestVersion');
return typeof version === 'string' && version.length < 60 ? version : '';
},
isEnableCapture: function () {
if (config.multiEnv || config.notAllowedEnableHTTPS) {
return false;
}
if (config.isEnableCapture != null) {
return config.isEnableCapture;
}
return !!propertiesStorage.getProperty('interceptHttpsConnects');
},
setEnableCapture: setEnableCapture,
isEnableHttp2: function () {
if (config.isEnableHttp2 != null) {
return config.isEnableHttp2;
}
return propertiesStorage.getProperty('enableHttp2') !== false;
},
setEnableHttp2: function (enable) {
config.isEnableHttp2 = enable;
propertiesStorage.setProperty('enableHttp2', enable);
},
set: function (name, value) {
typeof name == 'string'
? propertiesStorage.setProperty(name, value)
: propertiesStorage.setProperties(name);
},
remove: function (name) {
propertiesStorage.removeProperty(name);
},
get: function (name) {
return propertiesStorage.getProperty(name);
}
};
function getRules(rules) {
if (Array.isArray(rules)) {
return rules.join('\n');
}
if (typeof rules === 'string') {
return rules;
}
}
function getKeys(obj) {
var list = obj[''];
var keys = Object.keys(obj);
list = list && (Array.isArray(list) ? list : Array.isArray(list.list) ? list.list : null);
if (!list) {
return keys;
}
delete obj[''];
var result = [];
var count = 0;
list = list.concat(keys);
for (var i = 0, len = list.length; i < len; i++) {
var name = list[i];
if (util.isString(name) && (result.indexOf(name) === -1)) {
if (++count > MAX_COUNT_BY_IMPORT) {
return result;
}
result.push(name);
}
}
return result;
}
exports.addRules = function (rules, replace, clientId) {
if (rules == null) {
return;
}
replace = replace !== false;
var hasChanged;
if (Array.isArray(rules) || typeof rules == 'string') {
if (replace !== false || !getDefaultRules()) {
hasChanged = setDefaultRules(getRules(rules));
}
} else {
getKeys(rules).forEach(function (name) {
var item = name ? rules[name] : null;
if (Array.isArray(item) || typeof item === 'string') {
item = { rules: item };
}
if (item) {
item.rules = getRules(item.rules);
if (typeof item.replace !== 'boolean') {
item.replace = replace;
}
if (name === 'Default') {
if (
typeof item.rules === 'string' &&
(item.replace !== false || !getDefaultRules())
) {
if (setDefaultRules(item.rules)) {
hasChanged = true;
}
}
if (item.enable) {
enableDefaultRules();
} else if (item.enable === false) {
disableDefaultRules();
}
} else {
if (
typeof item.rules === 'string' &&
(item.replace !== false || !rulesStorage.existsFile(name))
) {
if (addRulesFile(name, item.rules)) {
hasChanged = true;
}
}
if (item.enable) {
selectRulesFile(name);
} else if (item.enable === false) {
unselectRulesFile(name);
}
}
}
});
}
if (hasChanged) {
config.setModified(clientId, true);
proxy.emit('rulesDataChange', 'addRules');
}
};
exports.addValues = function (values, replace, clientId) {
if (values == null || Array.isArray(values)) {
return;
}
replace = replace !== false;
var hasChanged;
getKeys(values).forEach(function (name) {
var isGroup = name[0] === '\r';
name = name.trim();
if (!name || SPACE_RE.test(name)) {
return;
}
if (isGroup) {
name = '\r' + name;
}
if (!replace && valuesStorage.existsFile(name)) {
return;
}
var value = name ? values[name] : null;
if (value == null) {
return;
}
if (typeof value !== 'string') {
value = JSON.stringify(value, null, ' ') || '';
}
if (addValuesFile(name, value)) {
hasChanged = true;
}
});
if (hasChanged) {
config.setModified(clientId);
proxy.emit('valuesDataChange', 'addValues');
}
};
exports.setup = function (p) {
proxy = p;
};
exports.setServiceRules = function(rulesText) {
if (typeof rulesText === 'string') {
serviceRules = rulesText;
parseRules();
}
};
exports.setMockRules = function(rulesText) {
if (typeof rulesText === 'string') {
mockRules = rulesText;
parseRules();
}
};
exports.setPluginMgr = function(p) {
pluginMgr = p;
};