@oxygenhq/mitmproxy-node
Version:
NodeJS mitmproxy adapter.
174 lines (160 loc) • 6.82 kB
JavaScript
const cp = require('child_process');
const path = require('path');
const http = require('http');
const net = require('net');
const detectPort = require('detect-port');
/**
* Wait for the specified port to open.
* @param port The port to watch for.
* @param retries The number of times to retry before giving up. Defaults to 10.
* @param interval The interval between retries, in milliseconds. Defaults to 500.
*/
function waitForPort(port, retries = 10, interval = 500) {
return new Promise((resolve, reject) => {
let retriesRemaining = retries;
let retryInterval = interval;
let timer = null;
let socket = null;
function clearTimerAndDestroySocket() {
clearTimeout(timer);
timer = null;
if (socket)
socket.destroy();
socket = null;
}
function retry() {
tryToConnect();
}
function tryToConnect() {
clearTimerAndDestroySocket();
if (--retriesRemaining < 0) {
reject(new Error('out of retries'));
}
socket = net.createConnection(port, 'localhost', function () {
clearTimerAndDestroySocket();
if (retriesRemaining >= 0)
resolve();
});
timer = setTimeout(function () { retry(); }, retryInterval);
socket.on('error', function (err) {
clearTimerAndDestroySocket();
setTimeout(retry, retryInterval);
});
}
tryToConnect();
});
}
/**
* Class that launches MITM proxy and listens to it over HTTP server.
*/
class MITMProxy {
constructor(onlyInterceptTextFiles) {
this._mitmProcess = null;
this._httpServer = null;
this.onlyInterceptTextFiles = onlyInterceptTextFiles;
}
/**
* Creates a new MITMProxy instance.
* @param cb Called with intercepted HTTP requests / responses.
* @param proxyPort Proxy port
* @param mitmCommPort If set, then connect to an externally launched mitmproxy and use the specified port for internal communication. Otherwise launch mitmproxy ourselves.
* @param interceptPaths List of paths to completely intercept without sending to the server (e.g. ['/eval'])
* @param quiet If true, do not print debugging messages (defaults to 'true').
* @param onlyInterceptTextFiles If true, only intercept text files (JavaScript/HTML/CSS/etc, and ignore media files).
*/
static async Create(cb, proxyPort, mitmCommPort = null, interceptPaths = [], quiet = true, onlyInterceptTextFiles = false, ignoreHosts = null) {
var mp = new MITMProxy(onlyInterceptTextFiles);
if (mitmCommPort) {
mp._httpServer = mp._initializeHTTPServer(mitmCommPort, cb);
try {
await waitForPort(proxyPort, 1);
if (!quiet) {
console.log('Connected to external mitmproxy.');
}
} catch (e) {
throw new Error(`Unable to connect to external mitmproxy: ${e}`);
}
} else {
const httpPort = await detectPort(8765);
mp._httpServer = mp._initializeHTTPServer(httpPort, cb);
if (!quiet) {
console.log('Starting up mitmproxy...');
}
// Start up MITM process.
// --anticache means to disable caching, which gets in the way of transparently rewriting content.
const scriptArgs = interceptPaths.length > 0 ? ['--set', `intercept=${interceptPaths.join(',')}`] : [];
scriptArgs.push('--set', `onlyInterceptTextFiles=${onlyInterceptTextFiles}`);
scriptArgs.push('--set', `httpCommPort=${httpPort}`);
if (ignoreHosts) {
scriptArgs.push('--ignore-hosts', ignoreHosts);
}
const options = ['--anticache', '-s', path.resolve(__dirname, 'scripts', 'proxy.py')].concat(scriptArgs);
if (quiet) {
options.push('-q');
}
// allow self-signed SSL certificates
options.push('--ssl-insecure');
let mitmDump = path.join(__dirname, 'mitmproxy', 'mitmdump');
if (process.platform === 'win32') {
mitmDump += '.exe';
} else {
mitmDump += '-' + process.platform;
}
mp._mitmProcess = cp.spawn(mitmDump, options, {
stdio: 'inherit'
});
const mitmProxyExited = new Promise((_, reject) => {
mp._mitmProcess.once('error', reject);
mp._mitmProcess.once('exit', reject);
});
// Wait for proxy port to come online.
const waitingForPort = waitForPort(proxyPort, 30);
try {
// Fails if mitmproxy exits before port becomes available.
await Promise.race([mitmProxyExited, waitingForPort]);
} catch (e) {
if (e && typeof (e) === 'object' && e.code === 'ENOENT') {
throw new Error('mitmdump, which is an executable that ships with mitmproxy, is not on your PATH. Please ensure that you can run mitmdump --version successfully from your command line.');
}
else {
throw new Error(`Unable to start mitmproxy: ${e}`);
}
}
}
return mp;
}
_initializeHTTPServer(port, cb) {
let data = '';
const server = http.createServer((req, res) => {
req.on('data', chunk => {
data += chunk;
});
req.on('end', () => {
res.end();
cb(JSON.parse(data));
data = '';
});
});
server.listen(port);
return server;
}
async shutdown(disposeMitm) {
return new Promise((resolve, reject) => {
this._httpServer.close();
if (disposeMitm && this._mitmProcess && !this._mitmProcess.killed) {
this._mitmProcess.once('exit', (code, signal) => {
resolve();
});
this._mitmProcess.once('error', (err) => {
reject(err);
});
// FIXME: this fails to terminate mitmdump child processes, at least on Windows
this._mitmProcess.kill('SIGTERM');
} else {
resolve();
}
});
}
}
exports.default = MITMProxy;
exports.__esModule = true;