allproxy
Version:
AllProxy: MITM HTTP Debugging Tool.
616 lines • 27.8 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.socketIoManager = exports.BATCH_SIZE = void 0;
const socket_io_1 = __importDefault(require("socket.io"));
const TcpProxy_1 = __importDefault(require("./TcpProxy"));
const LogProxy_1 = __importDefault(require("./LogProxy"));
const fs_1 = __importDefault(require("fs"));
const ProxyConfig_1 = require("../../common/ProxyConfig");
const net_1 = __importDefault(require("net"));
const Ping_1 = __importDefault(require("./Ping"));
const Resend_1 = __importDefault(require("./Resend"));
const GrpcProxy_1 = __importDefault(require("./GrpcProxy"));
const Paths_1 = __importDefault(require("./Paths"));
const Global_1 = __importDefault(require("./Global"));
const ConsoleLog_1 = __importDefault(require("./ConsoleLog"));
const BrowserLauncher_1 = __importDefault(require("./BrowserLauncher"));
const APFileSystem_1 = __importDefault(require("./APFileSystem"));
const app_1 = require("../../app");
const child_process_1 = require("child_process");
const path_1 = __importDefault(require("path"));
const FileLineMatcher_1 = __importDefault(require("./FileLineMatcher"));
const { rgPath } = require('@vscode/ripgrep');
const jqPath = './node_modules/node-jq/bin/jq';
const USE_HTTP2 = true;
const CONFIG_JSON = Paths_1.default.configJson();
const CACHE_SOCKET_ID = 'cache';
exports.BATCH_SIZE = 100; // windows size - maximum outstanding messages
const MAX_OUT = 2; // two message batches
let breakpointQueue = [];
class SocketIoInfo {
constructor(socket, configs) {
this.socket = undefined;
this.configs = [];
this.breakpointEnabled = false;
this.seqNum = 0;
this.messagesOut = 0;
this.queuedMessages = [];
this.socket = socket;
this.configs = configs;
}
}
;
class SocketIoManager {
constructor() {
this.socketIoMap = new Map();
this.resolveQueue = [];
this.activateConfig(this.getConfig());
exports.socketIoManager = this;
}
clientEndedSocket() {
for (const socketId in this.socketIoMap) {
this.closeAnyServersWithSocket(socketId);
this.socketIoMap.delete(socketId);
}
}
defaultConfig() {
// proxy all http by default
return [
{
isSecure: false,
protocol: 'browser:',
path: '/',
hostname: '',
port: 0,
recording: true,
hostReachable: true,
comment: ''
}
];
}
getConfig() {
const configs = fs_1.default.existsSync(CONFIG_JSON)
? JSON.parse(fs_1.default.readFileSync(CONFIG_JSON).toString()).configs
: this.defaultConfig();
let modified = false;
for (const config of configs) {
if (config.protocol === 'proxy:') {
config.protocol = 'browser:';
modified = true;
}
}
modified && this.saveConfig(configs);
return configs;
}
updateHostReachable() {
return new Promise((resolve) => {
const configs = this.getConfig();
this.resolveQueue.push(resolve);
if (this.resolveQueue.length > 1)
return;
let count = 0;
const done = () => {
if (++count === configs.length) {
// const queueCount = this.resolveQueue.length
let func;
// eslint-disable-next-line no-cond-assign
while (func = this.resolveQueue.pop()) {
func(configs);
}
}
};
configs.forEach(config => {
if (config.protocol === 'browser:' || config.protocol === 'log:') {
config.hostReachable = true;
done();
}
else {
config.hostReachable = false;
setTimeout(() => __awaiter(this, void 0, void 0, function* () {
const pingSuccessful = yield Ping_1.default.host(config.hostname);
if (!pingSuccessful) {
done();
}
else {
const socket = net_1.default.connect(config.port, config.hostname, () => {
config.hostReachable = true;
socket.end();
done();
});
socket.on('error', (_err) => {
done();
socket.end();
});
}
}));
}
});
});
}
saveConfig(proxyConfigs) {
// Cache the config, to configure the proxy on the next start up prior
// to receiving the config from the browser.
fs_1.default.writeFileSync(CONFIG_JSON, JSON.stringify({ configs: proxyConfigs }, null, 2));
}
addHttpServer(httpServer) {
ConsoleLog_1.default.debug('SocketIoManager add Server');
const server = new socket_io_1.default.Server(httpServer);
server.on('connection', (socket) => this._socketConnection(socket));
}
_socketConnection(socket) {
return __awaiter(this, void 0, void 0, function* () {
ConsoleLog_1.default.debug('SocketIoManager on connection');
const config = this.getConfig();
socket.emit('port config', Global_1.default.portConfig); // send port config to browser
socket.emit('proxy config', config); // send config to browser
socket.on('ping', (pingReply) => {
pingReply();
});
socket.on('ostype', (os, urlPath, ipInfo) => {
if (ipInfo) {
if (process.env.FILE_SYSTEM_LOG === '1') {
console.log(urlPath);
console.log(os);
// {
// ipAddress: '64.118.12.153',
// continentCode: 'NA',
// continentName: 'North America',
// countryCode: 'US',
// countryName: 'United States',
// stateProvCode: 'MN',
// stateProv: 'Minnesota',
// city: 'Underwood'
// }
ipInfo.date = getDateNow();
ipInfo.os = os;
ipInfo.app = urlPath;
console.log(JSON.stringify(ipInfo));
socket.handshake.url = urlPath;
socket.handshake.address = ipInfo.ipAddress;
}
}
(0, app_1.setOsBinaries)(os);
});
socket.on('get install type', (callback) => {
var type = 'Electron';
let headless = process.env.HEADLESS;
if (headless) {
if (process.env.npm_command === 'start') {
type = 'GitHub';
}
else {
type = 'NPM';
}
}
callback(type);
});
socket.on('proxy config', (proxyConfigs) => {
ConsoleLog_1.default.info(`${Paths_1.default.configJson()}:\n`, proxyConfigs);
this.saveConfig(proxyConfigs);
// Make sure all matching connection based servers are closed.
for (const proxyConfig of proxyConfigs) {
if (proxyConfig._server) {
this.closeAnyServerWithPort(proxyConfig.port);
}
}
this.activateConfig(proxyConfigs, socket);
// this.updateHostReachable();
});
socket.on('resend', (forwardProxy, method, url, message, body) => {
(0, Resend_1.default)(forwardProxy, method, url, message, body);
});
socket.on('breakpoint', (enable) => {
const socketIoInfo = this.socketIoMap.get(socket.id);
if (socketIoInfo) {
//ConsoleLog.info('breakpoint', enable);
socketIoInfo.breakpointEnabled = enable;
}
});
socket.on('detect browsers', (callback) => {
// Running in docker container
if (Global_1.default.inDockerContainer) {
callback([]);
}
else {
BrowserLauncher_1.default.detect()
.then((browsers) => {
callback(browsers);
})
.catch(e => {
console.log('Error detecting browsers:', e);
callback([]);
});
}
});
socket.on('launch browser', (browser) => {
BrowserLauncher_1.default.launch(browser);
});
socket.on('is file in downloads', (fileName, callback) => {
callback(fs_1.default.existsSync(process.env.HOME + '/Downloads/' + fileName));
});
socket.on('read file', (fileName, operator, filters, maxLines, callback) => __awaiter(this, void 0, void 0, function* () {
const filePath = "'" + process.env.HOME + "" + path_1.default.sep + 'Downloads' + path_1.default.sep + fileName + "'";
const rg = rgPath;
let cmd = '';
if (filters.length === 0) {
cmd = rg + ' -F -m ' + maxLines + ' "" ' + filePath;
}
else {
if (operator === 'and') {
for (let i = 0; i < filters.length; ++i) {
const filter = `'${filters[i]}'`;
if (i === filters.length - 1) {
cmd += i === 0
? rg + ' -F -m ' + maxLines + ' ' + filter + ' ' + filePath :
' | ' + rg + ' -F -m ' + maxLines + ' ' + filter;
}
else {
cmd += i === 0
? rg + ' -F ' + filter + ' ' + filePath
: ' | ' + rg + ' -F ' + filter;
}
}
}
else {
cmd += rg + ' -m ' + maxLines + " -e '" + filters.join('|') + "' " + filePath;
//console.log(cmd);
}
}
exports.socketIoManager.emitStatusToBrowser(socket, 'Executing ripgrep: ' + cmd);
const result = yield ripgrep(cmd, filters, socket);
const result2 = result.join();
callback(result2.toString().split('\n'));
}));
socket.on('sort file', (fileName, callback) => __awaiter(this, void 0, void 0, function* () {
const downloads = process.env.HOME + "" + path_1.default.sep + 'Downloads';
const filePath = downloads + path_1.default.sep + fileName;
const tempFilePath = downloads + path_1.default.sep + fileName + '-temp';
let cmd = '';
cmd = jqPath + ` -sc 'sort_by( .ts_millis )[]' '${filePath}' > '${tempFilePath}'`;
exports.socketIoManager.emitStatusToBrowser(socket, 'Sorting: ' + cmd);
yield execCommand(cmd, socket);
fs_1.default.rmSync(filePath);
fs_1.default.renameSync(tempFilePath, filePath);
callback();
}));
socket.on('json field exists', (fileName, jsonField, callback) => __awaiter(this, void 0, void 0, function* () {
const downloads = process.env.HOME + "" + path_1.default.sep + 'Downloads';
const rg = rgPath;
const filter = `'"${jsonField}":'`;
let cmd = rg + ' -F -m 1 ' + filter + ' ' + downloads + path_1.default.sep + fileName;
const data = yield ripgrep(cmd, [], socket, 10 * 1000);
//console.log('json field exists?', jsonField, data.length > 0);
callback(data.length > 0);
}));
socket.on('is sorted', (fileName, timeFieldName, callback) => {
const matcher = new FileLineMatcher_1.default(socket, fileName);
matcher.setTimeFilter(timeFieldName, new Date(), new Date());
callback(matcher.isSorted());
});
socket.on('file line matcher', (fileName, timeFieldName, startTime, endTime, operator, filters, maxLines, callback) => {
//console.log('file line matcher', fileName, timeFieldName, startTime, endTime, operator, filters, maxLines);
const matcher = new FileLineMatcher_1.default(socket, fileName);
matcher.setTimeFilter(timeFieldName, new Date(startTime), new Date(endTime));
matcher.setFilters(filters);
matcher.setOperator(operator);
matcher.setMaxLines(maxLines);
const lines = matcher.read();
callback(lines);
});
socket.on('disconnect', () => {
this.closeAnyServersWithSocket(socket.id);
this.socketIoMap.delete(socket.id);
});
socket.on('error', (e) => {
console.error('error', e);
this.closeAnyServersWithSocket(socket.id);
this.socketIoMap.delete(socket.id);
});
const apFileSystem = new APFileSystem_1.default(socket);
yield apFileSystem.listen();
});
}
activateConfig(proxyConfigs, socket) {
return __awaiter(this, void 0, void 0, function* () {
ConsoleLog_1.default.debug('SocketIoManager.activateConfig');
for (const proxyConfig of proxyConfigs) {
if (proxyConfig.protocol === 'log:') {
// eslint-disable-next-line no-new
new LogProxy_1.default(proxyConfig);
}
else if (proxyConfig.protocol === 'grpc:' && USE_HTTP2) {
GrpcProxy_1.default.reverseProxy(proxyConfig);
}
else if (proxyConfig.protocol !== 'http:' &&
proxyConfig.protocol !== 'https:' &&
proxyConfig.protocol !== 'browser:') {
// eslint-disable-next-line no-new
new TcpProxy_1.default(proxyConfig);
}
}
this.socketIoMap.set(socket ? socket.id : CACHE_SOCKET_ID, new SocketIoInfo((socket || undefined), proxyConfigs));
if (socket !== undefined) {
this.closeAnyServersWithSocket(CACHE_SOCKET_ID);
this.socketIoMap.delete(CACHE_SOCKET_ID);
}
});
}
// Close 'any:' protocol servers that are running for the browser owning the socket
closeAnyServersWithSocket(socketId) {
this.socketIoMap.forEach((socketInfo, key) => {
if (socketId && key !== socketId)
return;
for (const proxyConfig of socketInfo.configs) {
if (proxyConfig.protocol === 'log:') {
LogProxy_1.default.destructor(proxyConfig);
}
if (proxyConfig.protocol === 'grpc:') {
GrpcProxy_1.default.destructor(proxyConfig);
}
else if (proxyConfig._server) {
TcpProxy_1.default.destructor(proxyConfig);
}
}
});
}
// Close 'any:' protocol servers the specified listening port
closeAnyServerWithPort(port) {
this.socketIoMap.forEach((socketInfo, _key) => {
for (const proxyConfig of socketInfo.configs) {
if (proxyConfig._server && proxyConfig.port === port) {
if (proxyConfig.protocol === 'grpc:' && USE_HTTP2) {
GrpcProxy_1.default.destructor(proxyConfig);
}
else {
TcpProxy_1.default.destructor(proxyConfig);
}
}
}
});
}
isMatch(needle, haystack) {
if (needle.indexOf('.*') !== -1) {
const match = haystack.match(needle);
return match !== null && match.length > 0;
}
else {
return haystack.startsWith(needle);
}
}
/**
* Find proxy config matching URL
* @params protocol
* @params clientHostName
* @param {*} reqUrl
* @param isForwardProxy
* @returns ProxyConfig
*/
findProxyConfigMatchingURL(protocol, clientHostName, reqUrl, proxyType = 'reverse') {
const reqUrlPath = reqUrl.pathname.replace(/\/\//g, '/');
const isForwardProxy = proxyType === 'forward';
let matchingProxyConfig;
// Find matching proxy configuration
this.socketIoMap.forEach((socketInfo, _key) => {
for (const proxyConfig of socketInfo.configs) {
if (proxyConfig.protocol !== protocol && proxyConfig.protocol !== 'browser:')
continue;
if ((this.isMatch(proxyConfig.path, reqUrlPath) ||
this.isMatch(proxyConfig.path, clientHostName + reqUrlPath)) &&
isForwardProxy === (proxyConfig.protocol === 'browser:')) {
if (matchingProxyConfig === undefined || proxyConfig.path.length > matchingProxyConfig.path.length) {
matchingProxyConfig = proxyConfig;
}
}
}
});
return matchingProxyConfig;
}
findGrpcProxyConfig(hostname, port) {
let matchingProxyConfig;
// Find matching proxy configuration
this.socketIoMap.forEach((socketInfo, _key) => {
for (const proxyConfig of socketInfo.configs) {
if (proxyConfig.protocol === 'grpc:' && proxyConfig.hostname === hostname && proxyConfig.port === port) {
ConsoleLog_1.default.debug(`findGrpcProxy(${hostname}, ${port}) return:`, proxyConfig);
matchingProxyConfig = proxyConfig;
}
}
});
return matchingProxyConfig;
}
/**
* Emit message to browser.
* @param {*} message
* @param {*} proxyConfig
*/
emitMessageToBrowser(messageType, message, inProxyConfig) {
let queueCount = 0;
const isDynamic = inProxyConfig === undefined || inProxyConfig.comment === ProxyConfig_1.DYNAMICALLY_ADDED;
const emittedSocketId = {};
message.type = messageType;
const path = inProxyConfig ? inProxyConfig.path : '';
let emitted = false;
this.socketIoMap.forEach((socketInfo, socketId) => {
for (const proxyConfig of socketInfo.configs) {
if (inProxyConfig === undefined || isDynamic ||
(proxyConfig.path === path && inProxyConfig.protocol === proxyConfig.protocol)) {
if (proxyConfig.protocol === 'log:' && inProxyConfig !== proxyConfig)
continue;
if (emittedSocketId[socketId])
continue;
if (!proxyConfig.recording) {
if (proxyConfig.protocol !== 'log:') {
ConsoleLog_1.default.info('Record is disabled for protocol ', proxyConfig.protocol);
}
continue;
}
message.proxyConfig = isDynamic ? inProxyConfig : proxyConfig;
// Remove _server: net.Server
if (message.proxyConfig && message.proxyConfig._server) {
const pc = message.proxyConfig;
const server = pc._server;
delete pc._server;
message.proxyConfig = Object.assign({}, pc);
pc._server = server;
}
if (socketInfo.socket) {
if (socketInfo.queuedMessages.length > 0) {
socketInfo.queuedMessages.push(message);
queueCount += socketInfo.queuedMessages.length;
}
else {
queueCount += this.emitMessageWithFlowControl([message], socketInfo, socketId);
}
emittedSocketId[socketId] = true;
emitted = true;
}
}
}
});
if (!emitted) {
// console.error(message.sequenceNumber, 'no browser socket to emit to', message.url)
}
return queueCount;
}
emitMessageWithFlowControl(messages, socketInfo, socketId) {
if (socketInfo.messagesOut >= MAX_OUT) {
socketInfo.queuedMessages = socketInfo.queuedMessages.concat(messages);
}
else {
if (socketInfo.socket) {
++socketInfo.seqNum;
++socketInfo.messagesOut;
socketInfo.socket.emit('reqResJson', messages, socketInfo.queuedMessages.length,
// callback:
(_response) => {
--socketInfo.messagesOut;
ConsoleLog_1.default.info(`out=${socketInfo.messagesOut}`, `sent=${messages.length}`, `queued=${socketInfo.queuedMessages.length}`, `(${_response})`);
const count = Math.min(exports.BATCH_SIZE, socketInfo.queuedMessages.length);
if (count > 0) {
this.emitMessageWithFlowControl(socketInfo.queuedMessages.splice(0, count), socketInfo, socketId);
}
});
}
}
return socketInfo.queuedMessages.length;
}
isBreakpointEnabled() {
let enabled = false;
this.socketIoMap.forEach((socketInfo, _socketId) => {
if (socketInfo.breakpointEnabled) {
enabled = true;
}
});
return enabled;
}
handleBreakpoint(message) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise(resolve => {
let socket;
this.socketIoMap.forEach((socketInfo, _socketId) => {
if (socketInfo.breakpointEnabled) {
socket = socketInfo.socket;
message.proxyConfig = socketInfo.configs[0];
}
});
// Breakpoint found?
if (socket) {
breakpointQueue.push({ message, socket, resolve });
// Only one breakpoint inprogress?
if (breakpointQueue.length === 1) {
handleBreakpoints();
function handleBreakpoints() {
const bpMessage = breakpointQueue[0];
bpMessage.socket.emit('breakpoint', bpMessage.message, (message2) => {
bpMessage.resolve(message2);
breakpointQueue.shift();
if (breakpointQueue.length > 0) {
handleBreakpoints();
}
});
}
}
}
else {
resolve(message);
}
});
});
}
emitStatusToBrowser(socket, message) {
socket.emit('status dialog', message);
}
emitErrorToBrowser(socket, message) {
socket.emit('error dialog', message);
}
}
exports.default = SocketIoManager;
function execCommand(command, socket) {
return __awaiter(this, void 0, void 0, function* () {
//console.log(command);
return yield new Promise(resolve => {
const tokens = command.split(' ');
const p = (0, child_process_1.spawn)(tokens[0], tokens.slice(1), { shell: true });
p.stderr.on('data', (data) => {
console.error('spawn error', data.toString());
exports.socketIoManager.emitErrorToBrowser(socket, command + ': ' + data.toString());
resolve();
});
p.on('exit', () => {
resolve();
});
});
});
}
function ripgrep(command, filters, socket, timeout) {
return __awaiter(this, void 0, void 0, function* () {
//console.log(command);
let result = [];
let size = 0;
let progressTime = Date.now();
return yield new Promise(resolve => {
const tokens = command.split(' ');
const p = (0, child_process_1.spawn)(tokens[0], tokens.slice(1), { shell: true });
p.stdout.on('data', (data) => {
result.push(data.toString());
if (timeout === undefined) {
size += data.length;
if (Date.now() - progressTime >= 1000 * 1) {
exports.socketIoManager.emitStatusToBrowser(socket, size + ' bytes match filters: ' + filters);
progressTime = Date.now();
}
}
});
p.stderr.on('data', (data) => {
console.error('ripgrep error', data.toString());
exports.socketIoManager.emitErrorToBrowser(socket, command + ': ' + data.toString());
resolve(result);
});
p.on('exit', () => {
resolve(result);
});
});
});
}
function getDateNow() {
// return json.sequenceNumber; // used for testing only
const date = new Date();
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const seconds = date.getSeconds().toString().padStart(2, '0');
const msecs = (date.getMilliseconds() / 1000).toFixed(3).toString().replace('0.', '');
return `${date.toDateString()} ${hours}:${minutes}:${seconds}.${msecs}`;
}
//# sourceMappingURL=SocketIoManager.js.map