whistle
Version:
HTTP, HTTP2, HTTPS, Websocket debugging proxy
623 lines (594 loc) • 18.4 kB
JavaScript
require('../util/patch');
var express = require('express');
var fork = require('pfork').fork;
var bodyParser = require('body-parser');
var multer = require('multer2');
var fs = require('fs');
var fse = require('fs-extra2');
var path = require('path');
var gzip = require('zlib').gzip;
var util = require('./util');
var extractSaz = require('./extract-saz');
var generateSaz = require('./generate-saz');
var getServer = require('hagent').getServer;
var dataCenter = require('./data-center');
var composer = require('./composer');
var handleComposeData = require('./compose-data');
var Limiter = require('async-limiter');
var common = require('../util/common');
var logger = require('./console-log');
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1';
var saveData = dataCenter.saveData;
var shareData = dataCenter.shareData;
var requestData = dataCenter.requestData;
var forwardRequest = dataCenter.forwardRequest;
var INSTALL_SCRIPT = path.join(__dirname, 'install.js');
var installChild;
var LOG_RE = /^(\d+)\r(fatal|error|warn|info|debug|log)\r([^\s]*)\r/;
var TEMP_PLUGINS_PATH = path.join(common.getWhistlePath(), '.temp_plugins');
var SESSIONS_FILE_RE = /\.(txt|json|saz)$/i;
var limiter = new Limiter({ concurrency: 10 });
var MAX_PLUGINS = 10;
var TEMP_FILES_PATH;
var SAVED_SESSIONS_PATH;
var LIMIT_SIZE = 1024 * 1024 * 128;
var MAX_TEMP_SIZE = 1024 * 1024 * 12;
var MAX_PLUGIN_SIZE = MAX_TEMP_SIZE;
var MAX_TEMP_PLUGINS_AGE = 1000 * 60 * 10; // 10 minutes
var CR = '\r';
var jsonParser = bodyParser.json({ limit: LIMIT_SIZE });
var PLUGIN_SEP = /\s*,\s*/;
var config;
var curWhistleId;
var hasWhistleToken;
var whistleIdFile;
var storage = multer.memoryStorage();
var INVALID_NAME_RE = /[\u001e\u001f\u200e\u200f\u200d\u200c\u202a\u202d\u202e\u202c\u206e\u206f\u206b\u206a\u206d\u206c'<>:"\\/|?*]+/g;
var SPACE_RE = /\s+/g;
var upload = multer({
storage: storage,
fieldSize: LIMIT_SIZE
});
var promises = {};
var hasTempPluginsDir;
var ensureTempPluginsDir = function() {
if (!hasTempPluginsDir) {
try {
fse.ensureDirSync(TEMP_PLUGINS_PATH);
hasTempPluginsDir = true;
} catch (e) {}
}
};
ensureTempPluginsDir();
function parseLog(log) {
if (!log || typeof log !== 'string') {
return;
}
var match = LOG_RE.exec(log);
return match && {
t: match[1],
level: match[2],
id: match[3],
text: log.substring(match[0].length)
};
}
process.on('data', function(data) {
if (data) {
if (data.type === 'whistleId') {
curWhistleId = data.whistleId;
hasWhistleToken = data.hasWhistleToken;
} else {
saveData(data);
}
}
});
function getFilename(filename, time, count) {
filename = common.isString(filename) ? filename.replace(INVALID_NAME_RE, '').replace(SPACE_RE, ' ').substring(0, 64).trim() : '';
time = time || Date.now();
return common.getMonth(time) + '/' + filename + '_' + count + '_' + time;
}
function saveSessions(data, cb) {
var count = Array.isArray(data.sessions) && data.sessions.length;
if (!count) {
return cb();
}
limiter.push(function (done) {
gzip(JSON.stringify(data.sessions), function(err, buf) {
done();
if (err) {
return cb(err);
}
var filename = getFilename(data.filename, 0, count);
util.writeFile(path.join(SAVED_SESSIONS_PATH, filename), buf, function(e) {
!e && saveData({ type: 'savedData', filename: filename, buffer: buf } );
cb(e);
});
});
});
}
function getContent(options) {
var value = options.value;
if (value && typeof value === 'string') {
return value;
}
var base64 = options.base64;
if (base64) {
try {
return Buffer.from(base64, 'base64');
} catch(e) {}
}
return '';
}
function getFile(filename, cb) {
common.getStat(filename, function(err, stat) {
if (err ? err.code === 'ENOENT' : !stat.isFile()) {
return cb();
}
if (err) {
return cb(err.message || 'Error');
}
if (stat.size > MAX_TEMP_SIZE) {
return cb('File is too large to load');
}
fs.readFile(filename, function(e, data) {
if (e) {
return cb(e.message || 'Error');
}
cb(null, data);
});
});
}
function getTempFiles(list, cb) {
var len = list.length;
if (!len) {
return cb(list);
}
if (len > 11) {
len = 11;
list = list.slice(0, 11);
}
var result = [];
var execCb = function() {
--len;
if (len === 0) {
cb(result);
}
};
list = list.forEach(function(filename) {
if (!common.isTempFile(filename)) {
return execCb();
}
filename = path.join(TEMP_FILES_PATH, filename);
getFile(filename, function(err, data) {
if (data) {
result.push(data.toString('base64'));
}
execCb();
});
});
}
function handleError(clientId, e) {
if (clientId) {
e = (typeof e === 'string' ? e : e && e.message) || 'Install failed';
process.sendData({ type: 'error', error: e.substring(0, 512), clientId: clientId });
}
}
function writeTempPlugin(plugin, data, cb) {
if (!data) {
return cb();
}
plugin = plugin.split('/').pop();
var tempPath = path.join(TEMP_PLUGINS_PATH, plugin);
ensureTempPluginsDir();
util.writeFile(tempPath, data, function(e) {
cb(e, e ? null : tempPath);
});
}
function cleanTempPlugins() {
fs.readdir(TEMP_PLUGINS_PATH, function(err, files) {
setTimeout(cleanTempPlugins, MAX_TEMP_PLUGINS_AGE);
if (err) {
return;
}
var now = Date.now();
files.forEach(function(file) {
if (!common.TGZ_FILE_NAME_RE.test(file)) {
return;
}
fs.stat(path.join(TEMP_PLUGINS_PATH, file), function(err, stats) {
if (err) {
return;
}
if (Math.abs(now - stats.mtime.getTime()) < MAX_TEMP_PLUGINS_AGE) {
return;
}
fs.unlink(path.join(TEMP_PLUGINS_PATH, file), common.noop);
});
});
});
}
cleanTempPlugins();
function loadTgzPlugins(clientId, tgzPlugins, cb) {
var len = tgzPlugins.length;
if (!len) {
return cb();
}
var result = [];
var execCb = function() {
if (--len === 0) {
cb(result);
}
};
tgzPlugins.forEach(function(plugin) {
var p = promises[plugin];
if (p) {
// 同一 plugin 避免重复请求
return p.then(execCb);
}
promises[plugin] = new Promise(function(resolve) {
requestData({
url: '/service/cgi/plugin?name=' + encodeURIComponent(plugin),
maxLength: MAX_PLUGIN_SIZE,
responseType: 'buffer',
strictMode: true
}, function(err, data) {
writeTempPlugin(plugin, data, function(e, file) {
file && result.push('file:' + file);
execCb();
err = err || e;
err && handleError(clientId, err.message || 'Install plugin \'' + plugin + '\' failed');
delete promises[plugin];
resolve();
});
});
});
});
}
function execInstallPlugins(argv, clientId) {
if (installChild) {
return installChild.sendData({ type: 'installPlugins', argv: argv, clientId: clientId });
}
fork({
script: INSTALL_SCRIPT,
debugMode: config.debugMode
}, function (err, _, child) {
if (err) {
return handleError(clientId, err);
}
installChild = child;
child.once('close', function () {
installChild = null;
});
child.on('data', function (data) {
if (data && data.type === 'error') {
return handleError(data.clientId, data.data);
}
});
installChild.sendData({ type: 'installPlugins', argv: argv, clientId: clientId });
});
}
function installPlugins(data, clientId) {
var argv = data.pkgs.map(function(item) {
return item.name + (item.version ? '@' + item.version : '');
});
if (data.whistleDir) {
argv.push('--dir=' + data.whistleDir);
}
if (data.registry) {
argv.push('--registry=' + data.registry);
}
execInstallPlugins(argv, clientId);
}
function sessionsHandler() {
var app = express();
app.use(function (req, res, next) {
req.on('error', abort);
res.on('error', abort);
function abort() {
res.destroy();
}
next();
});
app.get('/service/*', forwardRequest);
app.post('/cgi-bin/service/save', jsonParser, function(req, res) {
if (util.isShareType(req.body.type)) {
shareData(req.body, function(err, data) {
res.json(err ? {ec: 2, data, em: err.message || 'Error'} : data);
});
return;
}
saveData(req.body);
res.json({ec: 0});
});
app.post('/cgi-bin/service/login', jsonParser, function(req, res) {
requestData({
method: 'POST',
url: '/service/cgi/login',
strictMode: true,
headers: {
'content-type': 'application/json'
},
body: req.body
}, function(err, data) {
if (err) {
return res.json({ec: 2, em: err.message || 'Error'});
}
config.whistleId = data.whistleId;
config.whistleToken = data.token;
process.sendData({ type: 'whistleIdChange', whistleId: data.whistleId, hasWhistleToken: !!data.token });
res.json({ec: 0});
try {
var text = common.encryptAES(data.whistleId + CR + data.token + CR + Date.now(), config.clientId, config.clientIdNo);
fs.writeFile(whistleIdFile, text, function(e) {
if (e) {
fs.writeFile(whistleIdFile, text, common.noop);
}
});
} catch (e) {}
});
});
app.post('/cgi-bin/plugins/install', bodyParser.urlencoded({ extended: true, limit: '1mb'}), function(req, res) {
var plugins = req.body.plugins;
var clientId = req.body.clientId;
plugins = common.isString(plugins) ? plugins.trim().split(PLUGIN_SEP) : null;
plugins = plugins && common.getPlugins(plugins, true);
var count = plugins ? plugins.length : 0;
if (count) {
if (count > MAX_PLUGINS) {
plugins = plugins.slice(0, MAX_PLUGINS);
count = MAX_PLUGINS;
}
var curTgzPlugins = [];
plugins = plugins.filter(function(name) {
if (!name.indexOf('file:')) {
curTgzPlugins.push(name.substring(5));
return false;
}
return true;
});
loadTgzPlugins(clientId, curTgzPlugins, function(tgzPlugins) {
plugins = plugins.concat(tgzPlugins);
if (!plugins.length) {
return;
}
var registry = common.getRegistry(req.body.registry);
if (config.hasInstaller) {
var pkgs = plugins.map(function(name) {
if (common.WHISTLE_PLUGIN_RE.test(name)) {
return {
name: RegExp.$1,
version: RegExp.$2
};
}
return { name: name };
});
process.sendData({ type: 'installPlugins', data: {
registry: registry,
pkgs: pkgs
} });
} else {
registry && plugins.push('--registry=' + registry);
execInstallPlugins(plugins, clientId);
}
});
return res.json({ ec: 0, count: count });
}
var data = common.parsePlugins(req.body);
if (data) {
if (config.hasInstaller) {
process.sendData({ type: 'installPlugins', data: data });
} else {
installPlugins(data, clientId);
}
}
res.json({ ec: 0, count: data ? data.pkgs.length : 0 });
});
app.post('/cgi-bin/service/logout', function(req, res) {
config.whistleId = undefined;
config.whistleToken = undefined;
process.sendData({ type: 'whistleIdChange' });
res.json({ec: 0});
});
app.post('/cgi-bin/saved/save', jsonParser, function(req, res) {
saveSessions(req.body, function(err) {
res.json({ec: err ? 2 : 0, em: err ? err.message || 'Error' : undefined});
});
});
app.post('/cgi-bin/composer', jsonParser, composer.handleRequest);
app.get('/cgi-bin/compose-data', handleComposeData);
'';
app.get('/cgi-bin/saved/list', function(req, res) {
util.getSavedList(SAVED_SESSIONS_PATH, function(err, list) {
if (err) {
return res.json({ ec: 2, em: err.message } );
}
common.sendGzip(req, res, { ec: 0, list: list });
});
});
app.post('/cgi-bin/saved/remove', jsonParser, function(req, res) {
var filename = getFilename(req.body.filename, +req.body.time, +req.body.count);
fs.unlink(path.join(SAVED_SESSIONS_PATH, filename), function(err) {
dataCenter.removeSavedData(filename.substring(filename.indexOf('/') + 1));
if (err && err.code !== 'ENOENT') {
return res.json({ ec: 2, em: err.message });
}
res.json({ ec: 0 });
});
});
app.get('/cgi-bin/saved/sessions', function(req, res) {
var filename = getFilename(req.query.filename, +req.query.time, +req.query.count);
res.writeHead(200, {
'Content-Type': 'application/json',
'Content-Encoding': 'gzip'
});
fs.createReadStream(path.join(SAVED_SESSIONS_PATH, filename))
.on('error', function(err) {
res.emit('error', err);
})
.pipe(res);
});
app.get('/cgi-bin/temp/get', function(req, res) {
var files = req.query.files;
if (files && typeof files === 'string') {
return getTempFiles(files.split(','), function(list) {
common.sendGzip(req, res, { ec: 0, list: list });
});
}
var filename = req.query.filename;
if (common.isTempFile(filename)) {
filename = path.join(TEMP_FILES_PATH, filename);
}
getFile(filename, function(em, data) {
if (em) {
return res.json({ ec: 2, em: em });
}
common.sendGzip(req, res, { ec: 0, value: data && data + '' });
});
});
app.get('/cgi-bin/history', composer.getHistory);
app.use('/cgi-bin/temp/create',
jsonParser,
function(req, res) {
util.writeTempFile(TEMP_FILES_PATH, getContent(req.body), function(e, filename) {
if (e) {
return res.json({ ec: 2, em: e.message });
}
res.json({ ec: 0, filepath: 'temp/' + filename });
});
}
);
app.use(
'/cgi-bin/sessions/import',
upload.single('importSessions'),
function (req, res) {
var file = req.file;
var suffix;
if (file && SESSIONS_FILE_RE.test(file.originalname)) {
suffix = RegExp.$1.toLowerCase();
}
if (!suffix || !Buffer.isBuffer(file.buffer)) {
return res.json([]);
}
if (suffix !== 'saz') {
var sessions = util.parseJSON(file.buffer + '');
return common.sendGzip(req, res, Array.isArray(sessions) ? sessions : []);
}
try {
extractSaz(file.buffer, function (data) {
common.sendGzip(req, res, data);
});
} catch (e) {
common.sendRes(req, 500, e.stack);
}
}
);
app.use('/cgi-bin/log/set', function(req, _, next) {
req.headers['content-type'] = 'application/json';
next();
});
app.use(bodyParser.urlencoded({ extended: true, limit: LIMIT_SIZE }));
app.use(jsonParser);
app.use('/cgi-bin/sessions/export', function (req, res) {
var body = req.body;
var type = body.exportFileType;
var download = function(content) {
res.attachment(util.getFilename(type, body.exportFilename)).send(content || body.sessions);
};
if (type !== 'Fiddler') {
return download();
}
generateSaz(body, function(e, body) {
if (e) {
return common.sendRes(res, 500, e.stack);
}
download(body);
});
});
app.use('/cgi-bin/log/set', function(req, res) {
res.setHeader('content-type', 'image/png');
if (req.method === 'POST') {
var list = req.body.list;
if (Array.isArray(list)) {
var len = Math.min(list.length, 20);
for (var i = 0; i < len; i++) {
logger.addLog(parseLog(list[i]));
}
}
} else {
logger.addLog(req.query);
}
res.end();
});
app.use('/cgi-bin/log/get', function(req, res) {
var query = req.query;
var startLogTime = query.startLogTime;
var init = startLogTime == -4;
var stopRecordConsole = startLogTime == -3;
var curLogId = logger.getLatestId();
common.sendGzip(req, res, {
ec: 0,
log: init || stopRecordConsole ? [] : logger.getLogs(startLogTime, query.count, query.logId),
curLogId: stopRecordConsole ? undefined : curLogId,
lastLogId: init ? (curLogId || -2) : (stopRecordConsole ? curLogId : undefined)
});
});
return app;
}
function updateWhistleId() {
if (config && (config.whistleId !== curWhistleId || !config.whistleToken !== !hasWhistleToken)) {
process.sendData({ type: 'whistleIdChange', whistleId: config.whistleId, hasWhistleToken: !!config.whistleToken });
}
}
var preList;
(function updateSystyemInfo() {
var curList = [];
var isChanged = !preList;
common.walkInterfaces(function (info) {
var addr = info.address.toLowerCase();
curList.push(addr);
isChanged = isChanged || preList.indexOf(addr) === -1;
});
isChanged = isChanged || curList.length !== preList.length;
preList = curList;
if (isChanged && process.sendData) {
process.sendData({ type: 'w2NetworkInterfacesChange' });
}
updateWhistleId();
setTimeout(updateSystyemInfo, 2000);
})();
function checkLogin(whistleId, token) {
if (!whistleId || !token) {
return;
}
// config.whistleId = whistleId;
setTimeout(function() {
// config.whistleToken = token;
updateWhistleId();
}, 2000);
}
function readWhistleLoginFile(file) {
try {
var text = common.decryptAES(common.readFileTextSync(file).trim(), config.clientId, config.clientIdNo).split(CR);
return text.length === 3 && text;
} catch (e) {}
}
function checkWhistleId(cb) {
var text = config.disableWebUI ? null
: readWhistleLoginFile(whistleIdFile) || (config.storage && readWhistleLoginFile(path.join(config.baseDir, '.whistle_id')));
text && checkLogin(text[0], text[1]);
cb();
}
module.exports = function (options, callback) {
config = options;
whistleIdFile = path.join(options.baseDir, options.storage || '', '.whistle_id');
TEMP_FILES_PATH = options.TEMP_FILES_PATH;
SAVED_SESSIONS_PATH = options.SAVED_SESSIONS_PATH;
dataCenter.setup(options);
composer.setup(options);
checkWhistleId(function() {
getServer(function (server, port) {
server.on('request', sessionsHandler());
callback(null, { port: port, whistleId: config.whistleId });
});
});
};