UNPKG

@oxygenhq/mitmproxy-node

Version:
174 lines (160 loc) 6.82 kB
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;