├── .gitignore ├── README.md ├── dist ├── index.d.ts └── index.js ├── package-lock.json ├── package.json ├── requirements.txt ├── scripts ├── install_python_deps.js ├── install_python_deps.py └── proxy.py ├── src └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | node_modules 3 | .vscode 4 | __pycache__ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mitmproxy-node 2.1.1 2 | 3 | A bridge between Python's [`mitmproxy`](https://mitmproxy.org/) and Node.JS programs. Rewrite network requests using Node.JS! 4 | 5 | ## Why? 6 | 7 | It is far easier to rewrite JavaScript/HTML/etc using JavaScript than Python, but mitmproxy only accepts Python plugins. 8 | There are no decent alternatives to mitmproxy, so this package lets me use mitmproxy with Node.js-based rewriting code. 9 | 10 | ## What can I use this for? 11 | 12 | For transparently rewriting HTTP/HTTPS responses. The mitmproxy plugin lets every HTTP request go through to the server uninhibited, and then passes it to Node.js via a WebSocket for rewriting. You can optionally specify a list of paths that should be directly intercepted without being passed to the server. 13 | 14 | If you want to add additional functionality, such as filtering or whatnot, I'll accept pull requests so long as they do not noticeably hinder performance. 15 | 16 | ## How does it work? 17 | 18 | A Python plugin for `mitmproxy` starts a WebSocket server, and `mitmproxy-node` talks with it over WebSocket messages. The two communicate via binary messages to reduce marshaling-related overhead. 19 | 20 | ## Your Python plugin is bad and you should feel bad 21 | 22 | I have no idea what I am doing. PRs to improve my Python code are appreciated! 23 | 24 | ## Pre-requisites 25 | 26 | * [`mitmproxy` V4](https://mitmproxy.org/) must be installed and runnable from the terminal. The install method cannot be a prebuilt binary or homebrew, since those packages are missing the Python websockets module. Install via `pip` or from source. 27 | * Python 3.6, since I use the new async/await syntax in the mitmproxy plugin 28 | * `npm install` to pull in Node and PIP dependencies. 29 | 30 | ## Using 31 | 32 | You can either start `mitmproxy` manually with `mitmdump --anticache -s scripts/proxy.py`, or `mitmproxy-node` will do so automatically for you. 33 | `mitmproxy-node` auto-detects if `mitmproxy` is already running. 34 | If you frequently start/stop the proxy, it may be best to start it manually. 35 | 36 | ```javascript 37 | import MITMProxy from 'mitmproxy-node'; 38 | 39 | // Returns Promise 40 | async function makeProxy() { 41 | // Note: Your interceptor can also be asynchronous and return a Promise! 42 | return MITMProxy.Create(function(interceptedMsg) { 43 | const req = interceptedMsg.request; 44 | const res = interceptedMsg.response; 45 | if (req.rawUrl.contains("target.js") && res.getHeader('content-type').indexOf("javascript") !== -1) { 46 | interceptedMsg.setResponseBody(Buffer.from(`Hacked!`, 'utf8')); 47 | } 48 | }, ['/eval'] /* list of paths to directly intercept -- don't send to server */, 49 | true /* Be quiet; turn off for debug messages */, 50 | true /* Only intercept text or potentially-text requests (all mime types with *application* and *text* in them, plus responses with no mime type) */ 51 | ); 52 | } 53 | 54 | async function main() { 55 | const proxy = await makeProxy(); 56 | // when done: 57 | await proxy.shutdown(); 58 | } 59 | ``` 60 | 61 | Without fancy async/await: 62 | 63 | ```javascript 64 | import MITMProxy from 'mitmproxy-node'; 65 | 66 | // Returns Promise 67 | function makeProxy() { 68 | return MITMProxy.Create(function(interceptedMsg) { 69 | const req = interceptedMsg.request; 70 | const res = interceptedMsg.response; 71 | if (req.rawUrl.contains("target.js") && res.getHeader('content-type').indexOf("javascript") !== -1) { 72 | interceptedMsg.setResponseBody(Buffer.from(`Hacked!`, 'utf8')); 73 | } 74 | }, ['/eval'], true, true); 75 | } 76 | 77 | function main() { 78 | makeProxy().then((proxy) => { 79 | // when done 80 | proxy.shutdown.then(() => { 81 | // Proxy is closed! 82 | }); 83 | }); 84 | } 85 | ``` 86 | 87 | ## Building 88 | 89 | `npm run build` 90 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { Url } from 'url'; 3 | /** 4 | * Function that intercepts and rewrites HTTP responses. 5 | */ 6 | export declare type Interceptor = (m: InterceptedHTTPMessage) => void | Promise; 7 | /** 8 | * An interceptor that does nothing. 9 | */ 10 | export declare function nopInterceptor(m: InterceptedHTTPMessage): void; 11 | /** 12 | * The core HTTP response. 13 | */ 14 | export interface HTTPResponse { 15 | statusCode: number; 16 | headers: { 17 | [name: string]: string; 18 | }; 19 | body: Buffer; 20 | } 21 | /** 22 | * Metadata associated with an HTTP request. 23 | */ 24 | export interface HTTPRequestMetadata { 25 | method: string; 26 | url: string; 27 | headers: [string, string][]; 28 | } 29 | /** 30 | * Metadata associated with an HTTP response. 31 | */ 32 | export interface HTTPResponseMetadata { 33 | status_code: number; 34 | headers: [string, string][]; 35 | } 36 | /** 37 | * Abstract class that represents HTTP headers. 38 | */ 39 | export declare abstract class AbstractHTTPHeaders { 40 | private _headers; 41 | readonly headers: [string, string][]; 42 | constructor(headers: [string, string][]); 43 | private _indexOfHeader(name); 44 | /** 45 | * Get the value of the given header field. 46 | * If there are multiple fields with that name, this only returns the first field's value! 47 | * @param name Name of the header field 48 | */ 49 | getHeader(name: string): string; 50 | /** 51 | * Set the value of the given header field. Assumes that there is only one field with the given name. 52 | * If the field does not exist, it adds a new field with the name and value. 53 | * @param name Name of the field. 54 | * @param value New value. 55 | */ 56 | setHeader(name: string, value: string): void; 57 | /** 58 | * Removes the header field with the given name. Assumes that there is only one field with the given name. 59 | * Does nothing if field does not exist. 60 | * @param name Name of the field. 61 | */ 62 | removeHeader(name: string): void; 63 | /** 64 | * Removes all header fields. 65 | */ 66 | clearHeaders(): void; 67 | } 68 | /** 69 | * Represents a MITM-ed HTTP response from a server. 70 | */ 71 | export declare class InterceptedHTTPResponse extends AbstractHTTPHeaders { 72 | statusCode: number; 73 | constructor(metadata: HTTPResponseMetadata); 74 | toJSON(): HTTPResponseMetadata; 75 | } 76 | /** 77 | * Represents an intercepted HTTP request from a client. 78 | */ 79 | export declare class InterceptedHTTPRequest extends AbstractHTTPHeaders { 80 | method: string; 81 | rawUrl: string; 82 | url: Url; 83 | constructor(metadata: HTTPRequestMetadata); 84 | } 85 | /** 86 | * Represents an intercepted HTTP request/response pair. 87 | */ 88 | export declare class InterceptedHTTPMessage { 89 | /** 90 | * Unpack from a Buffer received from MITMProxy. 91 | * @param b 92 | */ 93 | static FromBuffer(b: Buffer): InterceptedHTTPMessage; 94 | readonly request: InterceptedHTTPRequest; 95 | readonly response: InterceptedHTTPResponse; 96 | readonly requestBody: Buffer; 97 | readonly responseBody: Buffer; 98 | private _responseBody; 99 | private constructor(); 100 | /** 101 | * Changes the body of the HTTP response. Appropriately updates content-length. 102 | * @param b The new body contents. 103 | */ 104 | setResponseBody(b: Buffer): void; 105 | /** 106 | * Changes the status code of the HTTP response. 107 | * @param code The new status code. 108 | */ 109 | setStatusCode(code: number): void; 110 | /** 111 | * Pack into a buffer for transmission to MITMProxy. 112 | */ 113 | toBuffer(): Buffer; 114 | } 115 | export declare class StashedItem { 116 | readonly rawUrl: string; 117 | readonly mimeType: string; 118 | readonly data: Buffer; 119 | constructor(rawUrl: string, mimeType: string, data: Buffer); 120 | readonly shortMimeType: string; 121 | readonly isHtml: boolean; 122 | readonly isJavaScript: boolean; 123 | } 124 | /** 125 | * Class that launches MITM proxy and talks to it via WebSockets. 126 | */ 127 | export default class MITMProxy { 128 | private static _activeProcesses; 129 | /** 130 | * Creates a new MITMProxy instance. 131 | * @param cb Called with intercepted HTTP requests / responses. 132 | * @param interceptPaths List of paths to completely intercept without sending to the server (e.g. ['/eval']) 133 | * @param quiet If true, do not print debugging messages (defaults to 'true'). 134 | * @param onlyInterceptTextFiles If true, only intercept text files (JavaScript/HTML/CSS/etc, and ignore media files). 135 | */ 136 | static Create(cb?: Interceptor, interceptPaths?: string[], quiet?: boolean, onlyInterceptTextFiles?: boolean, ignoreHosts?: string | null): Promise; 137 | private static _cleanupCalled; 138 | private static _cleanup(); 139 | private _stashEnabled; 140 | stashEnabled: boolean; 141 | private _mitmProcess; 142 | private _mitmError; 143 | private _wss; 144 | cb: Interceptor; 145 | readonly onlyInterceptTextFiles: boolean; 146 | private _stash; 147 | private _stashFilter; 148 | stashFilter: (url: string, item: StashedItem) => boolean; 149 | private constructor(); 150 | private _initializeWSS(wss); 151 | private _initializeMITMProxy(mitmProxy); 152 | /** 153 | * Retrieves the given URL from the stash. 154 | * @param url 155 | */ 156 | getFromStash(url: string): StashedItem; 157 | forEachStashItem(cb: (value: StashedItem, url: string) => void): void; 158 | /** 159 | * Requests the given URL from the proxy. 160 | */ 161 | proxyGet(urlString: string): Promise; 162 | shutdown(): Promise; 163 | } 164 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | Object.defineProperty(exports, "__esModule", { value: true }); 11 | const ws_1 = require("ws"); 12 | const child_process_1 = require("child_process"); 13 | const path_1 = require("path"); 14 | const url_1 = require("url"); 15 | const http_1 = require("http"); 16 | const https_1 = require("https"); 17 | const net_1 = require("net"); 18 | /** 19 | * Wait for the specified port to open. 20 | * @param port The port to watch for. 21 | * @param retries The number of times to retry before giving up. Defaults to 10. 22 | * @param interval The interval between retries, in milliseconds. Defaults to 500. 23 | */ 24 | function waitForPort(port, retries = 10, interval = 500) { 25 | return new Promise((resolve, reject) => { 26 | let retriesRemaining = retries; 27 | let retryInterval = interval; 28 | let timer = null; 29 | let socket = null; 30 | function clearTimerAndDestroySocket() { 31 | clearTimeout(timer); 32 | timer = null; 33 | if (socket) 34 | socket.destroy(); 35 | socket = null; 36 | } 37 | function retry() { 38 | tryToConnect(); 39 | } 40 | function tryToConnect() { 41 | clearTimerAndDestroySocket(); 42 | if (--retriesRemaining < 0) { 43 | reject(new Error('out of retries')); 44 | } 45 | socket = net_1.createConnection(port, "localhost", function () { 46 | clearTimerAndDestroySocket(); 47 | if (retriesRemaining >= 0) 48 | resolve(); 49 | }); 50 | timer = setTimeout(function () { retry(); }, retryInterval); 51 | socket.on('error', function (err) { 52 | clearTimerAndDestroySocket(); 53 | setTimeout(retry, retryInterval); 54 | }); 55 | } 56 | tryToConnect(); 57 | }); 58 | } 59 | /** 60 | * An interceptor that does nothing. 61 | */ 62 | function nopInterceptor(m) { } 63 | exports.nopInterceptor = nopInterceptor; 64 | /** 65 | * Abstract class that represents HTTP headers. 66 | */ 67 | class AbstractHTTPHeaders { 68 | // The raw headers, as a sequence of key/value pairs. 69 | // Since header fields may be repeated, this array may contain multiple entries for the same key. 70 | get headers() { 71 | return this._headers; 72 | } 73 | constructor(headers) { 74 | this._headers = headers; 75 | } 76 | _indexOfHeader(name) { 77 | const headers = this.headers; 78 | const len = headers.length; 79 | for (let i = 0; i < len; i++) { 80 | if (headers[i][0].toLowerCase() === name) { 81 | return i; 82 | } 83 | } 84 | return -1; 85 | } 86 | /** 87 | * Get the value of the given header field. 88 | * If there are multiple fields with that name, this only returns the first field's value! 89 | * @param name Name of the header field 90 | */ 91 | getHeader(name) { 92 | const index = this._indexOfHeader(name.toLowerCase()); 93 | if (index !== -1) { 94 | return this.headers[index][1]; 95 | } 96 | return ''; 97 | } 98 | /** 99 | * Set the value of the given header field. Assumes that there is only one field with the given name. 100 | * If the field does not exist, it adds a new field with the name and value. 101 | * @param name Name of the field. 102 | * @param value New value. 103 | */ 104 | setHeader(name, value) { 105 | const index = this._indexOfHeader(name.toLowerCase()); 106 | if (index !== -1) { 107 | this.headers[index][1] = value; 108 | } 109 | else { 110 | this.headers.push([name, value]); 111 | } 112 | } 113 | /** 114 | * Removes the header field with the given name. Assumes that there is only one field with the given name. 115 | * Does nothing if field does not exist. 116 | * @param name Name of the field. 117 | */ 118 | removeHeader(name) { 119 | const index = this._indexOfHeader(name.toLowerCase()); 120 | if (index !== -1) { 121 | this.headers.splice(index, 1); 122 | } 123 | } 124 | /** 125 | * Removes all header fields. 126 | */ 127 | clearHeaders() { 128 | this._headers = []; 129 | } 130 | } 131 | exports.AbstractHTTPHeaders = AbstractHTTPHeaders; 132 | /** 133 | * Represents a MITM-ed HTTP response from a server. 134 | */ 135 | class InterceptedHTTPResponse extends AbstractHTTPHeaders { 136 | constructor(metadata) { 137 | super(metadata.headers); 138 | this.statusCode = metadata.status_code; 139 | // We don't support chunked transfers. The proxy already de-chunks it for us. 140 | this.removeHeader('transfer-encoding'); 141 | // MITMProxy decodes the data for us. 142 | this.removeHeader('content-encoding'); 143 | // CSP is bad! 144 | this.removeHeader('content-security-policy'); 145 | this.removeHeader('x-webkit-csp'); 146 | this.removeHeader('x-content-security-policy'); 147 | } 148 | toJSON() { 149 | return { 150 | status_code: this.statusCode, 151 | headers: this.headers 152 | }; 153 | } 154 | } 155 | exports.InterceptedHTTPResponse = InterceptedHTTPResponse; 156 | /** 157 | * Represents an intercepted HTTP request from a client. 158 | */ 159 | class InterceptedHTTPRequest extends AbstractHTTPHeaders { 160 | constructor(metadata) { 161 | super(metadata.headers); 162 | this.method = metadata.method.toLowerCase(); 163 | this.rawUrl = metadata.url; 164 | this.url = url_1.parse(this.rawUrl); 165 | } 166 | } 167 | exports.InterceptedHTTPRequest = InterceptedHTTPRequest; 168 | /** 169 | * Represents an intercepted HTTP request/response pair. 170 | */ 171 | class InterceptedHTTPMessage { 172 | /** 173 | * Unpack from a Buffer received from MITMProxy. 174 | * @param b 175 | */ 176 | static FromBuffer(b) { 177 | const metadataSize = b.readInt32LE(0); 178 | const requestSize = b.readInt32LE(4); 179 | const responseSize = b.readInt32LE(8); 180 | const metadata = JSON.parse(b.toString("utf8", 12, 12 + metadataSize)); 181 | return new InterceptedHTTPMessage(new InterceptedHTTPRequest(metadata.request), new InterceptedHTTPResponse(metadata.response), b.slice(12 + metadataSize, 12 + metadataSize + requestSize), b.slice(12 + metadataSize + requestSize, 12 + metadataSize + requestSize + responseSize)); 182 | } 183 | // The body of the HTTP response. Read-only; change the response body via setResponseBody. 184 | get responseBody() { 185 | return this._responseBody; 186 | } 187 | constructor(request, response, requestBody, responseBody) { 188 | this.request = request; 189 | this.response = response; 190 | this.requestBody = requestBody; 191 | this._responseBody = responseBody; 192 | } 193 | /** 194 | * Changes the body of the HTTP response. Appropriately updates content-length. 195 | * @param b The new body contents. 196 | */ 197 | setResponseBody(b) { 198 | this._responseBody = b; 199 | // Update content-length. 200 | this.response.setHeader('content-length', `${b.length}`); 201 | // TODO: Content-encoding? 202 | } 203 | /** 204 | * Changes the status code of the HTTP response. 205 | * @param code The new status code. 206 | */ 207 | setStatusCode(code) { 208 | this.response.statusCode = code; 209 | } 210 | /** 211 | * Pack into a buffer for transmission to MITMProxy. 212 | */ 213 | toBuffer() { 214 | const metadata = Buffer.from(JSON.stringify(this.response), 'utf8'); 215 | const metadataLength = metadata.length; 216 | const responseLength = this._responseBody.length; 217 | const rv = Buffer.alloc(8 + metadataLength + responseLength); 218 | rv.writeInt32LE(metadataLength, 0); 219 | rv.writeInt32LE(responseLength, 4); 220 | metadata.copy(rv, 8); 221 | this._responseBody.copy(rv, 8 + metadataLength); 222 | return rv; 223 | } 224 | } 225 | exports.InterceptedHTTPMessage = InterceptedHTTPMessage; 226 | class StashedItem { 227 | constructor(rawUrl, mimeType, data) { 228 | this.rawUrl = rawUrl; 229 | this.mimeType = mimeType; 230 | this.data = data; 231 | } 232 | get shortMimeType() { 233 | let mime = this.mimeType.toLowerCase(); 234 | if (mime.indexOf(";") !== -1) { 235 | mime = mime.slice(0, mime.indexOf(";")); 236 | } 237 | return mime; 238 | } 239 | get isHtml() { 240 | return this.shortMimeType === "text/html"; 241 | } 242 | get isJavaScript() { 243 | switch (this.shortMimeType) { 244 | case 'text/javascript': 245 | case 'application/javascript': 246 | case 'text/x-javascript': 247 | case 'application/x-javascript': 248 | return true; 249 | default: 250 | return false; 251 | } 252 | } 253 | } 254 | exports.StashedItem = StashedItem; 255 | function defaultStashFilter(url, item) { 256 | return item.isJavaScript || item.isHtml; 257 | } 258 | /** 259 | * Class that launches MITM proxy and talks to it via WebSockets. 260 | */ 261 | class MITMProxy { 262 | constructor(cb, onlyInterceptTextFiles) { 263 | this._stashEnabled = false; 264 | this._mitmProcess = null; 265 | this._mitmError = null; 266 | this._wss = null; 267 | this._stash = new Map(); 268 | this._stashFilter = defaultStashFilter; 269 | this.cb = cb; 270 | this.onlyInterceptTextFiles = onlyInterceptTextFiles; 271 | } 272 | /** 273 | * Creates a new MITMProxy instance. 274 | * @param cb Called with intercepted HTTP requests / responses. 275 | * @param interceptPaths List of paths to completely intercept without sending to the server (e.g. ['/eval']) 276 | * @param quiet If true, do not print debugging messages (defaults to 'true'). 277 | * @param onlyInterceptTextFiles If true, only intercept text files (JavaScript/HTML/CSS/etc, and ignore media files). 278 | */ 279 | static Create(cb = nopInterceptor, interceptPaths = [], quiet = true, onlyInterceptTextFiles = false, ignoreHosts = null) { 280 | return __awaiter(this, void 0, void 0, function* () { 281 | // Construct WebSocket server, and wait for it to begin listening. 282 | const wss = new ws_1.Server({ port: 8765 }); 283 | const proxyConnected = new Promise((resolve, reject) => { 284 | wss.once('connection', () => { 285 | resolve(); 286 | }); 287 | }); 288 | const mp = new MITMProxy(cb, onlyInterceptTextFiles); 289 | // Set up WSS callbacks before MITMProxy connects. 290 | mp._initializeWSS(wss); 291 | yield new Promise((resolve, reject) => { 292 | wss.once('listening', () => { 293 | wss.removeListener('error', reject); 294 | resolve(); 295 | }); 296 | wss.once('error', reject); 297 | }); 298 | try { 299 | try { 300 | yield waitForPort(8080, 1); 301 | if (!quiet) { 302 | console.log(`MITMProxy already running.`); 303 | } 304 | } 305 | catch (e) { 306 | if (!quiet) { 307 | console.log(`MITMProxy not running; starting up mitmproxy.`); 308 | } 309 | // Start up MITM process. 310 | // --anticache means to disable caching, which gets in the way of transparently rewriting content. 311 | const scriptArgs = interceptPaths.length > 0 ? ["--set", `intercept=${interceptPaths.join(",")}`] : []; 312 | scriptArgs.push("--set", `onlyInterceptTextFiles=${onlyInterceptTextFiles}`); 313 | if (ignoreHosts) { 314 | scriptArgs.push(`--ignore-hosts`, ignoreHosts); 315 | } 316 | const options = ["--anticache", "-s", path_1.resolve(__dirname, `../scripts/proxy.py`)].concat(scriptArgs); 317 | if (quiet) { 318 | options.push('-q'); 319 | } 320 | // allow self-signed SSL certificates 321 | options.push("--ssl-insecure"); 322 | const mitmProcess = child_process_1.spawn("mitmdump", options, { 323 | stdio: 'inherit' 324 | }); 325 | const mitmProxyExited = new Promise((_, reject) => { 326 | mitmProcess.once('error', reject); 327 | mitmProcess.once('exit', reject); 328 | }); 329 | if (MITMProxy._activeProcesses.push(mitmProcess) === 1) { 330 | process.on('SIGINT', MITMProxy._cleanup); 331 | process.on('exit', MITMProxy._cleanup); 332 | } 333 | mp._initializeMITMProxy(mitmProcess); 334 | // Wait for port 8080 to come online. 335 | const waitingForPort = waitForPort(8080); 336 | try { 337 | // Fails if mitmproxy exits before port becomes available. 338 | yield Promise.race([mitmProxyExited, waitingForPort]); 339 | } 340 | catch (e) { 341 | if (e && typeof (e) === 'object' && e.code === "ENOENT") { 342 | 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.`); 343 | } 344 | else { 345 | throw new Error(`Unable to start mitmproxy: ${e}`); 346 | } 347 | } 348 | } 349 | yield proxyConnected; 350 | } 351 | catch (e) { 352 | yield new Promise((resolve) => wss.close(resolve)); 353 | throw e; 354 | } 355 | return mp; 356 | }); 357 | } 358 | static _cleanup() { 359 | if (MITMProxy._cleanupCalled) { 360 | return; 361 | } 362 | MITMProxy._cleanupCalled = true; 363 | MITMProxy._activeProcesses.forEach((p) => { 364 | p.kill('SIGKILL'); 365 | }); 366 | } 367 | // Toggle whether or not mitmproxy-node stashes modified server responses. 368 | // **Not used for performance**, but enables Node.js code to fetch previous server responses from the proxy. 369 | get stashEnabled() { 370 | return this._stashEnabled; 371 | } 372 | set stashEnabled(v) { 373 | if (!v) { 374 | this._stash.clear(); 375 | } 376 | this._stashEnabled = v; 377 | } 378 | get stashFilter() { 379 | return this._stashFilter; 380 | } 381 | set stashFilter(value) { 382 | if (typeof (value) === 'function') { 383 | this._stashFilter = value; 384 | } 385 | else if (value === null) { 386 | this._stashFilter = defaultStashFilter; 387 | } 388 | else { 389 | throw new Error(`Invalid stash filter: Expected a function.`); 390 | } 391 | } 392 | _initializeWSS(wss) { 393 | this._wss = wss; 394 | this._wss.on('connection', (ws) => { 395 | ws.on('error', (e) => { 396 | if (e.code !== "ECONNRESET") { 397 | console.log(`WebSocket error: ${e}`); 398 | } 399 | }); 400 | ws.on('message', (message) => __awaiter(this, void 0, void 0, function* () { 401 | const original = InterceptedHTTPMessage.FromBuffer(message); 402 | const rv = this.cb(original); 403 | if (rv && typeof (rv) === 'object' && rv.then) { 404 | yield rv; 405 | } 406 | // Remove transfer-encoding. We don't support chunked. 407 | if (this._stashEnabled) { 408 | const item = new StashedItem(original.request.rawUrl, original.response.getHeader('content-type'), original.responseBody); 409 | if (this._stashFilter(original.request.rawUrl, item)) { 410 | this._stash.set(original.request.rawUrl, item); 411 | } 412 | } 413 | ws.send(original.toBuffer()); 414 | })); 415 | }); 416 | } 417 | _initializeMITMProxy(mitmProxy) { 418 | this._mitmProcess = mitmProxy; 419 | this._mitmProcess.on('exit', (code, signal) => { 420 | const index = MITMProxy._activeProcesses.indexOf(this._mitmProcess); 421 | if (index !== -1) { 422 | MITMProxy._activeProcesses.splice(index, 1); 423 | } 424 | if (code !== null) { 425 | if (code !== 0) { 426 | this._mitmError = new Error(`Process exited with code ${code}.`); 427 | } 428 | } 429 | else { 430 | this._mitmError = new Error(`Process exited due to signal ${signal}.`); 431 | } 432 | }); 433 | this._mitmProcess.on('error', (err) => { 434 | this._mitmError = err; 435 | }); 436 | } 437 | /** 438 | * Retrieves the given URL from the stash. 439 | * @param url 440 | */ 441 | getFromStash(url) { 442 | return this._stash.get(url); 443 | } 444 | forEachStashItem(cb) { 445 | this._stash.forEach(cb); 446 | } 447 | /** 448 | * Requests the given URL from the proxy. 449 | */ 450 | proxyGet(urlString) { 451 | return __awaiter(this, void 0, void 0, function* () { 452 | const url = url_1.parse(urlString); 453 | const get = url.protocol === "http:" ? http_1.get : https_1.get; 454 | return new Promise((resolve, reject) => { 455 | const req = get({ 456 | url: urlString, 457 | headers: { 458 | host: url.host 459 | }, 460 | host: 'localhost', 461 | port: 8080, 462 | path: urlString 463 | }, (res) => { 464 | const data = new Array(); 465 | res.on('data', (chunk) => { 466 | data.push(chunk); 467 | }); 468 | res.on('end', () => { 469 | const d = Buffer.concat(data); 470 | resolve({ 471 | statusCode: res.statusCode, 472 | headers: res.headers, 473 | body: d 474 | }); 475 | }); 476 | res.once('error', reject); 477 | }); 478 | req.once('error', reject); 479 | }); 480 | }); 481 | } 482 | shutdown() { 483 | return __awaiter(this, void 0, void 0, function* () { 484 | return new Promise((resolve, reject) => { 485 | const closeWSS = () => { 486 | this._wss.close((err) => { 487 | if (err) { 488 | reject(err); 489 | } 490 | else { 491 | resolve(); 492 | } 493 | }); 494 | }; 495 | if (this._mitmProcess && !this._mitmProcess.killed) { 496 | this._mitmProcess.once('exit', (code, signal) => { 497 | closeWSS(); 498 | }); 499 | this._mitmProcess.kill('SIGTERM'); 500 | } 501 | else { 502 | closeWSS(); 503 | } 504 | }); 505 | }); 506 | } 507 | } 508 | MITMProxy._activeProcesses = []; 509 | MITMProxy._cleanupCalled = false; 510 | exports.default = MITMProxy; 511 | //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7OztBQUFBLDJCQUE2QztBQUM3QyxpREFBa0Q7QUFDbEQsK0JBQTZCO0FBQzdCLDZCQUEyQztBQUMzQywrQkFBb0M7QUFDcEMsaUNBQXNDO0FBQ3RDLDZCQUE2QztBQUU3Qzs7Ozs7R0FLRztBQUNILHFCQUFxQixJQUFZLEVBQUUsVUFBa0IsRUFBRSxFQUFFLFdBQW1CLEdBQUc7SUFDN0UsTUFBTSxDQUFDLElBQUksT0FBTyxDQUFPLENBQUMsT0FBTyxFQUFFLE1BQU0sRUFBRSxFQUFFO1FBQzNDLElBQUksZ0JBQWdCLEdBQUcsT0FBTyxDQUFDO1FBQy9CLElBQUksYUFBYSxHQUFHLFFBQVEsQ0FBQztRQUM3QixJQUFJLEtBQUssR0FBaUIsSUFBSSxDQUFDO1FBQy9CLElBQUksTUFBTSxHQUFXLElBQUksQ0FBQztRQUUxQjtZQUNFLFlBQVksQ0FBQyxLQUFLLENBQUMsQ0FBQztZQUNwQixLQUFLLEdBQUcsSUFBSSxDQUFDO1lBQ2IsRUFBRSxDQUFDLENBQUMsTUFBTSxDQUFDO2dCQUFDLE1BQU0sQ0FBQyxPQUFPLEVBQUUsQ0FBQztZQUM3QixNQUFNLEdBQUcsSUFBSSxDQUFDO1FBQ2hCLENBQUM7UUFFRDtZQUNFLFlBQVksRUFBRSxDQUFDO1FBQ2pCLENBQUM7UUFFRDtZQUNFLDBCQUEwQixFQUFFLENBQUM7WUFFN0IsRUFBRSxDQUFDLENBQUMsRUFBRSxnQkFBZ0IsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDO2dCQUMzQixNQUFNLENBQUMsSUFBSSxLQUFLLENBQUMsZ0JBQWdCLENBQUMsQ0FBQyxDQUFDO1lBQ3RDLENBQUM7WUFFRCxNQUFNLEdBQUcsc0JBQWdCLENBQUMsSUFBSSxFQUFFLFdBQVcsRUFBRTtnQkFDM0MsMEJBQTBCLEVBQUUsQ0FBQztnQkFDN0IsRUFBRSxDQUFDLENBQUMsZ0JBQWdCLElB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QUFBO0lBRVksUUFBUTs7WUFDbkIsTUFBTSxDQUFDLElBQUksT0FBTyxDQUFPLENBQUMsT0FBTyxFQUFFLE1BQU0sRUFBRSxFQUFFO2dCQUMzQyxNQUFNLFFBQVEsR0FBRyxHQUFHLEVBQUU7b0JBQ3BCLElBQUksQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsR0FBRyxFQUFFLEVBQUU7d0JBQ3RCLEVBQUUsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7NEJBQ1IsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDO3dCQUNkLENBQUM7d0JBQUMsSUFBSSxDQUFDLENBQUM7NEJBQ04sT0FBTyxFQUFFLENBQUM7d0JBQ1osQ0FBQztvQkFDSCxDQUFDLENBQUMsQ0FBQztnQkFDTCxDQUFDLENBQUM7Z0JBRUYsRUFBRSxDQUFDLENBQUMsSUFBSSxDQUFDLFlBQVksSUFBSSxDQUFDLElBQUksQ0FBQyxZQUFZLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQztvQkFDbkQsSUFBSSxDQUFDLFlBQVksQ0FBQyxJQUFJLENBQUMsTUFBTSxFQUFFLENBQUMsSUFBSSxFQUFFLE1BQU0sRUFBRSxFQUFFO3dCQUM5QyxRQUFRLEVBQUUsQ0FBQztvQkFDYixDQUFDLENBQUMsQ0FBQztvQkFDSCxJQUFJLENBQUMsWUFBWSxDQUFDLElBQUksQ0FBQyxTQUFTLENBQUMsQ0FBQztnQkFDcEMsQ0FBQztnQkFBQyxJQUFJLENBQUMsQ0FBQztvQkFDTixRQUFRLEVBQUUsQ0FBQztnQkFDYixDQUFDO1lBQ0gsQ0FBQyxDQUFDLENBQUM7UUFDTCxDQUFDO0tBQUE7O0FBelBjLDBCQUFnQixHQUFtQixFQUFFLENBQUM7QUF3RnRDLHdCQUFjLEdBQUcsS0FBSyxDQUFDO0FBekZ4Qyw0QkEyUEMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQge1NlcnZlciBhcyBXZWJTb2NrZXRTZXJ2ZXJ9IGZyb20gJ3dzJztcbmltcG9ydCB7c3Bhd24sIENoaWxkUHJvY2Vzc30gZnJvbSAnY2hpbGRfcHJvY2Vzcyc7XG5pbXBvcnQge3Jlc29sdmV9IGZyb20gJ3BhdGgnO1xuaW1wb3J0IHtwYXJzZSBhcyBwYXJzZVVSTCwgVXJsfSBmcm9tICd1cmwnO1xuaW1wb3J0IHtnZXQgYXMgaHR0cEdldH0gZnJvbSAnaHR0cCc7XG5pbXBvcnQge2dldCBhcyBodHRwc0dldH0gZnJvbSAnaHR0cHMnO1xuaW1wb3J0IHtjcmVhdGVDb25uZWN0aW9uLCBTb2NrZXR9IGZyb20gJ25ldCc7XG5cbi8qKlxuICogV2FpdCBmb3IgdGhlIHNwZWNpZmllZCBwb3J0IHRvIG9wZW4uXG4gKiBAcGFyYW0gcG9ydCBUaGUgcG9ydCB0byB3YXRjaCBmb3IuXG4gKiBAcGFyYW0gcmV0cmllcyBUaGUgbnVtYmVyIG9mIHRpbWVzIHRvIHJldHJ5IGJlZm9yZSBnaXZpbmcgdXAuIERlZmF1bHRzIHRvIDEwLlxuICogQHBhcmFtIGludGVydmFsIFRoZSBpbnRlcnZhbCBiZXR3ZWVuIHJldHJpZXMsIGluIG1pbGxpc2Vjb25kcy4gRGVmYXVsdHMgdG8gNTAwLlxuICovXG5mdW5jdGlvbiB3YWl0Rm9yUG9ydChwb3J0OiBudW1iZXIsIHJldHJpZXM6IG51bWJlciA9IDEwLCBpbnRlcnZhbDogbnVtYmVyID0gNTAwKTogUHJvbWlzZTx2b2lkPiB7XG4gIHJldHVybiBuZXcgUHJvbWlzZTx2b2lkPigocmVzb2x2ZSwgcmVqZWN0KSA9PiB7XG4gICAgbGV0IHJldHJpZXNSZW1haW5pbmcgPSByZXRyaWVzO1xuICAgIGxldCByZXRyeUludGVydmFsID0gaW50ZXJ2YWw7XG4gICAgbGV0IHRpbWVyOiBOb2RlSlMuVGltZXIgPSBudWxsO1xuICAgIGxldCBzb2NrZXQ6IFNvY2tldCA9IG51bGw7XG5cbiAgICBmdW5jdGlvbiBjbGVhclRpbWVyQW5kRGVzdHJveVNvY2tldCgpIHtcbiAgICAgIGNsZWFyVGltZW91dCh0aW1lcik7XG4gICAgICB0aW1lciA9IG51bGw7XG4gICAgICBpZiAoc29ja2V0KSBzb2NrZXQuZGVzdHJveSgpO1xuICAgICAgc29ja2V0ID0gbnVsbDtcbiAgICB9XG5cbiAgICBmdW5jdGlvbiByZXRyeSgpIHtcbiAgICAgIHRyeVRvQ29ubmVjdCgpO1xuICAgIH1cblxuICAgIGZ1bmN0aW9uIHRyeVRvQ29ubmVjdCgpIHtcbiAgICAgIGNsZWFyVGltZXJBbmREZXN0cm95U29ja2V0KCk7XG5cbiAgICAgIGlmICgtLXJldHJpZXNSZW1haW5pbmcgPCAwKSB7XG4gICAgICAgIHJlamVjdChuZXcgRXJyb3IoJ291dCBvZiByZXRyaWVzJykpO1xuICAgICAgfVxuXG4gICAgICBzb2NrZXQgPSBjcmVhdGVDb25uZWN0aW9uKHBvcnQsIFwibG9jYWxob3N0XCIsIGZ1bmN0aW9uKCkge1xuICAgICAgICBjbGVhclRpbWVyQW5kRGVzdHJveVNvY2tldCgpO1xuICAgICAgICBpZiAocmV0cmllc1JlbWFpbmluZyA+PSAwKSByZXNvbHZlKCk7XG4gICAgICB9KTtcblxuICAgICAgdGltZXIgPSBzZXRUaW1lb3V0KGZ1bmN0aW9uKCkgeyByZXRyeSgpOyB9LCByZXRyeUludGVydmFsKTtcblxuICAgICAgc29ja2V0Lm9uKCdlcnJvcicsIGZ1bmN0aW9uKGVycikge1xuICAgICAgICBjbGVhclRpbWVyQW5kRGVzdHJveVNvY2tldCgpO1xuICAgICAgICBzZXRUaW1lb3V0KHJldHJ5LCByZXRyeUludGVydmFsKTtcbiAgICAgIH0pO1xuICAgIH1cblxuICAgIHRyeVRvQ29ubmVjdCgpO1xuICB9KTtcbn1cblxuLyoqXG4gKiBGdW5jdGlvbiB0aGF0IGludGVyY2VwdHMgYW5kIHJld3JpdGVzIEhUVFAgcmVzcG9uc2VzLlxuICovXG5leHBvcnQgdHlwZSBJbnRlcmNlcHRvciA9IChtOiBJbnRlcmNlcHRlZEhUVFBNZXNzYWdlKSA9PiB2b2lkIHwgUHJvbWlzZTx2b2lkPjtcblxuLyoqXG4gKiBBbiBpbnRlcmNlcHRvciB0aGF0IGRvZXMgbm90aGluZy5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIG5vcEludGVyY2VwdG9yKG06IEludGVyY2VwdGVkSFRUUE1lc3NhZ2UpOiB2b2lkIHt9XG5cbi8qKlxuICogVGhlIGNvcmUgSFRUUCByZXNwb25zZS5cbiAqL1xuZXhwb3J0IGludGVyZmFjZSBIVFRQUmVzcG9uc2Uge1xuICBzdGF0dXNDb2RlOiBudW1iZXIsXG4gIGhlYWRlcnM6IHtbbmFtZTogc3RyaW5nXTogc3RyaW5nfTtcbiAgYm9keTogQnVmZmVyO1xufVxuXG4vKipcbiAqIE1ldGFkYXRhIGFzc29jaWF0ZWQgd2l0aCBhIHJlcXVlc3QvcmVzcG9uc2UgcGFpci5cbiAqL1xuaW50ZXJmYWNlIEhUVFBNZXNzYWdlTWV0YWRhdGEge1xuICByZXF1ZXN0OiBIVFRQUmVxdWVzdE1ldGFkYXRhO1xuICByZXNwb25zZTogSFRUUFJlc3BvbnNlTWV0YWRhdGE7XG59XG5cbi8qKlxuICogTWV0YWRhdGEgYXNzb2NpYXRlZCB3aXRoIGFuIEhUVFAgcmVxdWVzdC5cbiAqL1xuZXhwb3J0IGludGVyZmFjZSBIVFRQUmVxdWVzdE1ldGFkYXRhIHtcbiAgLy8gR0VULCBERUxFVEUsIFBPU1QsICBldGMuXG4gIG1ldGhvZDogc3RyaW5nO1xuICAvLyBUYXJnZXQgVVJMIGZvciB0aGUgcmVxdWVzdC5cbiAgdXJsOiBzdHJpbmc7XG4gIC8vIFRoZSBzZXQgb2YgaGVhZGVycyBmcm9tIHRoZSByZXF1ZXN0LCBhcyBrZXktdmFsdWUgcGFpcnMuXG4gIC8vIFNpbmNlIGhlYWRlciBmaWVsZHMgbWF5IGJlIHJlcGVhdGVkLCB0aGlzIGFycmF5IG1heSBjb250YWluIG11bHRpcGxlIGVudHJpZXMgZm9yIHRoZSBzYW1lIGtleS5cbiAgaGVhZGVyczogW3N0cmluZywgc3RyaW5nXVtdO1xufVxuXG4vKipcbiAqIE1ldGFkYXRhIGFzc29jaWF0ZWQgd2l0aCBhbiBIVFRQIHJlc3BvbnNlLlxuICovXG5leHBvcnQgaW50ZXJmYWNlIEhUVFBSZXNwb25zZU1ldGFkYXRhIHtcbiAgLy8gVGhlIG51bWVyaWNhbCBzdGF0dXMgY29kZS5cbiAgc3RhdHVzX2NvZGU6IG51bWJlcjtcbiAgLy8gVGhlIHNldCBvZiBoZWFkZXJzIGZyb20gdGhlIHJlc3BvbnNlLCBhcyBrZXktdmFsdWUgcGFpcnMuXG4gIC8vIFNpbmNlIGhlYWRlciBmaWVsZHMgbWF5IGJlIHJlcGVhdGVkLCB0aGlzIGFycmF5IG1heSBjb250YWluIG11bHRpcGxlIGVudHJpZXMgZm9yIHRoZSBzYW1lIGtleS5cbiAgaGVhZGVyczogW3N0cmluZywgc3RyaW5nXVtdO1xufVxuXG4vKipcbiAqIEFic3RyYWN0IGNsYXNzIHRoYXQgcmVwcmVzZW50cyBIVFRQIGhlYWRlcnMuXG4gKi9cbmV4cG9ydCBhYnN0cmFjdCBjbGFzcyBBYnN0cmFjdEhUVFBIZWFkZXJzIHtcbiAgcHJpdmF0ZSBfaGVhZGVyczogW3N0cmluZywgc3RyaW5nXVtdO1xuICAvLyBUaGUgcmF3IGhlYWRlcnMsIGFzIGEgc2VxdWVuY2Ugb2Yga2V5L3ZhbHVlIHBhaXJzLlxuICAvLyBTaW5jZSBoZWFkZXIgZmllbGRzIG1heSBiZSByZXBlYXRlZCwgdGhpcyBhcnJheSBtYXkgY29udGFpbiBtdWx0aXBsZSBlbnRyaWVzIGZvciB0aGUgc2FtZSBrZXkuXG4gIHB1YmxpYyBnZXQgaGVhZGVycygpOiBbc3RyaW5nLCBzdHJpbmddW10ge1xuICAgIHJldHVybiB0aGlzLl9oZWFkZXJzO1xuICB9XG4gIGNvbnN0cnVjdG9yKGhlYWRlcnM6IFtzdHJpbmcsIHN0cmluZ11bXSkge1xuICAgIHRoaXMuX2hlYWRlcnMgPSBoZWFkZXJzO1xuICB9XG5cbiAgcHJpdmF0ZSBfaW5kZXhPZkhlYWRlcihuYW1lOiBzdHJpbmcpOiBudW1iZXIge1xuICAgIGNvbnN0IGhlYWRlcnMgPSB0aGlzLmhlYWRlcnM7XG4gICAgY29uc3QgbGVuID0gaGVhZGVycy5sZW5ndGg7XG4gICAgZm9yIChsZXQgaSA9IDA7IGkgPCBsZW47IGkrKykge1xuICAgICAgaWYgKGhlYWRlcnNbaV1bMF0udG9Mb3dlckNhc2UoKSA9PT0gbmFtZSkge1xuICAgICAgICByZXR1cm4gaTtcbiAgICAgIH1cbiAgICB9XG4gICAgcmV0dXJuIC0xO1xuICB9XG5cbiAgLyoqXG4gICAqIEdldCB0aGUgdmFsdWUgb2YgdGhlIGdpdmVuIGhlYWRlciBmaWVsZC5cbiAgICogSWYgdGhlcmUgYXJlIG11bHRpcGxlIGZpZWxkcyB3aXRoIHRoYXQgbmFtZSwgdGhpcyBvbmx5IHJldHVybnMgdGhlIGZpcnN0IGZpZWxkJ3MgdmFsdWUhXG4gICAqIEBwYXJhbSBuYW1lIE5hbWUgb2YgdGhlIGhlYWRlciBmaWVsZFxuICAgKi9cbiAgcHVibGljIGdldEhlYWRlcihuYW1lOiBzdHJpbmcpOiBzdHJpbmcge1xuICAgIGNvbnN0IGluZGV4ID0gdGhpcy5faW5kZXhPZkhlYWRlcihuYW1lLnRvTG93ZXJDYXNlKCkpO1xuICAgIGlmIChpbmRleCAhPT0gLTEpIHtcbiAgICAgIHJldHVybiB0aGlzLmhlYWRlcnNbaW5kZXhdWzFdO1xuICAgIH1cbiAgICByZXR1cm4gJyc7XG4gIH1cblxuICAvKipcbiAgICogU2V0IHRoZSB2YWx1ZSBvZiB0aGUgZ2l2ZW4gaGVhZGVyIGZpZWxkLiBBc3N1bWVzIHRoYXQgdGhlcmUgaXMgb25seSBvbmUgZmllbGQgd2l0aCB0aGUgZ2l2ZW4gbmFtZS5cbiAgICogSWYgdGhlIGZpZWxkIGRvZXMgbm90IGV4aXN0LCBpdCBhZGRzIGEgbmV3IGZpZWxkIHdpdGggdGhlIG5hbWUgYW5kIHZhbHVlLlxuICAgKiBAcGFyYW0gbmFtZSBOYW1lIG9mIHRoZSBmaWVsZC5cbiAgICogQHBhcmFtIHZhbHVlIE5ldyB2YWx1ZS5cbiAgICovXG4gIHB1YmxpYyBzZXRIZWFkZXIobmFtZTogc3RyaW5nLCB2YWx1ZTogc3RyaW5nKTogdm9pZCB7XG4gICAgY29uc3QgaW5kZXggPSB0aGlzLl9pbmRleE9mSGVhZGVyKG5hbWUudG9Mb3dlckNhc2UoKSk7XG4gICAgaWYgKGluZGV4ICE9PSAtMSkge1xuICAgICAgdGhpcy5oZWFkZXJzW2luZGV4XVsxXSA9IHZhbHVlO1xuICAgIH0gZWxzZSB7XG4gICAgICB0aGlzLmhlYWRlcnMucHVzaChbbmFtZSwgdmFsdWVdKTtcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogUmVtb3ZlcyB0aGUgaGVhZGVyIGZpZWxkIHdpdGggdGhlIGdpdmVuIG5hbWUuIEFzc3VtZXMgdGhhdCB0aGVyZSBpcyBvbmx5IG9uZSBmaWVsZCB3aXRoIHRoZSBnaXZlbiBuYW1lLlxuICAgKiBEb2VzIG5vdGhpbmcgaWYgZmllbGQgZG9lcyBub3QgZXhpc3QuXG4gICAqIEBwYXJhbSBuYW1lIE5hbWUgb2YgdGhlIGZpZWxkLlxuICAgKi9cbiAgcHVibGljIHJlbW92ZUhlYWRlcihuYW1lOiBzdHJpbmcpOiB2b2lkIHtcbiAgICBjb25zdCBpbmRleCA9IHRoaXMuX2luZGV4T2ZIZWFkZXIobmFtZS50b0xvd2VyQ2FzZSgpKTtcbiAgICBpZiAoaW5kZXggIT09IC0xKSB7XG4gICAgICB0aGlzLmhlYWRlcnMuc3BsaWNlKGluZGV4LCAxKTtcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogUmVtb3ZlcyBhbGwgaGVhZGVyIGZpZWxkcy5cbiAgICovXG4gIHB1YmxpYyBjbGVhckhlYWRlcnMoKTogdm9pZCB7XG4gICAgdGhpcy5faGVhZGVycyA9IFtdO1xuICB9XG59XG5cbi8qKlxuICogUmVwcmVzZW50cyBhIE1JVE0tZWQgSFRUUCByZXNwb25zZSBmcm9tIGEgc2VydmVyLlxuICovXG5leHBvcnQgY2xhc3MgSW50ZXJjZXB0ZWRIVFRQUmVzcG9uc2UgZXh0ZW5kcyBBYnN0cmFjdEhUVFBIZWFkZXJzIHtcbiAgLy8gVGhlIHN0YXR1cyBjb2RlIG9mIHRoZSBIVFRQIHJlc3BvbnNlLlxuICBwdWJsaWMgc3RhdHVzQ29kZTogbnVtYmVyO1xuXG4gIGNvbnN0cnVjdG9yKG1ldGFkYXRhOiBIVFRQUmVzcG9uc2VNZXRhZGF0YSkge1xuICAgIHN1cGVyKG1ldGFkYXRhLmhlYWRlcnMpO1xuICAgIHRoaXMuc3RhdHVzQ29kZSA9IG1ldGFkYXRhLnN0YXR1c19jb2RlO1xuICAgIC8vIFdlIGRvbid0IHN1cHBvcnQgY2h1bmtlZCB0cmFuc2ZlcnMuIFRoZSBwcm94eSBhbHJlYWR5IGRlLWNodW5rcyBpdCBmb3IgdXMuXG4gICAgdGhpcy5yZW1vdmVIZWFkZXIoJ3RyYW5zZmVyLWVuY29kaW5nJyk7XG4gICAgLy8gTUlUTVByb3h5IGRlY29kZXMgdGhlIGRhdGEgZm9yIHVzLlxuICAgIHRoaXMucmVtb3ZlSGVhZGVyKCdjb250ZW50LWVuY29kaW5nJyk7XG4gICAgLy8gQ1NQIGlzIGJhZCFcbiAgICB0aGlzLnJlbW92ZUhlYWRlcignY29udGVudC1zZWN1cml0eS1wb2xpY3knKTtcbiAgICB0aGlzLnJlbW92ZUhlYWRlcigneC13ZWJraXQtY3NwJyk7XG4gICAgdGhpcy5yZW1vdmVIZWFkZXIoJ3gtY29udGVudC1zZWN1cml0eS1wb2xpY3knKTtcbiAgfVxuXG4gIHB1YmxpYyB0b0pTT04oKTogSFRUUFJlc3BvbnNlTWV0YWRhdGEge1xuICAgIHJldHVybiB7XG4gICAgICBzdGF0dXNfY29kZTogdGhpcy5zdGF0dXNDb2RlLFxuICAgICAgaGVhZGVyczogdGhpcy5oZWFkZXJzXG4gICAgfTtcbiAgfVxufVxuXG4vKipcbiAqIFJlcHJlc2VudHMgYW4gaW50ZXJjZXB0ZWQgSFRUUCByZXF1ZXN0IGZyb20gYSBjbGllbnQuXG4gKi9cbmV4cG9ydCBjbGFzcyBJbnRlcmNlcHRlZEhUVFBSZXF1ZXN0IGV4dGVuZHMgQWJzdHJhY3RIVFRQSGVhZGVycyB7XG4gIC8vIEhUVFAgbWV0aG9kIChHRVQvREVMRVRFL2V0YylcbiAgcHVibGljIG1ldGhvZDogc3RyaW5nO1xuICAvLyBUaGUgVVJMIGFzIGEgc3RyaW5nLlxuICBwdWJsaWMgcmF3VXJsOiBzdHJpbmc7XG4gIC8vIFRoZSBVUkwgYXMgYSBVUkwgb2JqZWN0LlxuICBwdWJsaWMgdXJsOiBVcmw7XG5cbiAgY29uc3RydWN0b3IobWV0YWRhdGE6IEhUVFBSZXF1ZXN0TWV0YWRhdGEpIHtcbiAgICBzdXBlcihtZXRhZGF0YS5oZWFkZXJzKTtcbiAgICB0aGlzLm1ldGhvZCA9IG1ldGFkYXRhLm1ldGhvZC50b0xvd2VyQ2FzZSgpO1xuICAgIHRoaXMucmF3VXJsID0gbWV0YWRhdGEudXJsO1xuICAgIHRoaXMudXJsID0gcGFyc2VVUkwodGhpcy5yYXdVcmwpO1xuICB9XG59XG5cbi8qKlxuICogUmVwcmVzZW50cyBhbiBpbnRlcmNlcHRlZCBIVFRQIHJlcXVlc3QvcmVzcG9uc2UgcGFpci5cbiAqL1xuZXhwb3J0IGNsYXNzIEludGVyY2VwdGVkSFRUUE1lc3NhZ2Uge1xuICAvKipcbiAgICogVW5wYWNrIGZyb20gYSBCdWZmZXIgcmVjZWl2ZWQgZnJvbSBNSVRNUHJveHkuXG4gICAqIEBwYXJhbSBiXG4gICAqL1xuICBwdWJsaWMgc3RhdGljIEZyb21CdWZmZXIoYjogQnVmZmVyKTogSW50ZXJjZXB0ZWRIVFRQTWVzc2FnZSB7XG4gICAgY29uc3QgbWV0YWRhdGFTaXplID0gYi5yZWFkSW50MzJMRSgwKTtcbiAgICBjb25zdCByZXF1ZXN0U2l6ZSA9IGIucmVhZEludDMyTEUoNCk7XG4gICAgY29uc3QgcmVzcG9uc2VTaXplID0gYi5yZWFkSW50MzJMRSg4KTtcbiAgICBjb25zdCBtZXRhZGF0YTogSFRUUE1lc3NhZ2VNZXRhZGF0YSA9IEpTT04ucGFyc2UoYi50b1N0cmluZyhcInV0ZjhcIiwgMTIsIDEyICsgbWV0YWRhdGFTaXplKSk7XG4gICAgcmV0dXJuIG5ldyBJbnRlcmNlcHRlZEhUVFBNZXNzYWdlKFxuICAgICAgbmV3IEludGVyY2VwdGVkSFRUUFJlcXVlc3QobWV0YWRhdGEucmVxdWVzdCksXG4gICAgICBuZXcgSW50ZXJjZXB0ZWRIVFRQUmVzcG9uc2UobWV0YWRhdGEucmVzcG9uc2UpLFxuICAgICAgYi5zbGljZSgxMiArIG1ldGFkYXRhU2l6ZSwgMTIgKyBtZXRhZGF0YVNpemUgKyByZXF1ZXN0U2l6ZSksXG4gICAgICBiLnNsaWNlKDEyICsgbWV0YWRhdGFTaXplICsgcmVxdWVzdFNpemUsIDEyICsgbWV0YWRhdGFTaXplICsgcmVxdWVzdFNpemUgKyByZXNwb25zZVNpemUpXG4gICAgKTtcbiAgfVxuXG4gIHB1YmxpYyByZWFkb25seSByZXF1ZXN0OiBJbnRlcmNlcHRlZEhUVFBSZXF1ZXN0O1xuICBwdWJsaWMgcmVhZG9ubHkgcmVzcG9uc2U6IEludGVyY2VwdGVkSFRUUFJlc3BvbnNlO1xuICAvLyBUaGUgYm9keSBvZiB0aGUgSFRUUCByZXF1ZXN0LlxuICBwdWJsaWMgcmVhZG9ubHkgcmVxdWVzdEJvZHk6IEJ1ZmZlcjtcbiAgLy8gVGhlIGJvZHkgb2YgdGhlIEhUVFAgcmVzcG9uc2UuIFJlYWQtb25seTsgY2hhbmdlIHRoZSByZXNwb25zZSBib2R5IHZpYSBzZXRSZXNwb25zZUJvZHkuXG4gIHB1YmxpYyBnZXQgcmVzcG9uc2VCb2R5KCk6IEJ1ZmZlciB7XG4gICAgcmV0dXJuIHRoaXMuX3Jlc3BvbnNlQm9keTtcbiAgfVxuICBwcml2YXRlIF9yZXNwb25zZUJvZHk6IEJ1ZmZlcjtcbiAgcHJpdmF0ZSBjb25zdHJ1Y3RvcihyZXF1ZXN0OiBJbnRlcmNlcHRlZEhUVFBSZXF1ZXN0LCByZXNwb25zZTogSW50ZXJjZXB0ZWRIVFRQUmVzcG9uc2UsIHJlcXVlc3RCb2R5OiBCdWZmZXIsIHJlc3BvbnNlQm9keTogQnVmZmVyKSB7XG4gICAgdGhpcy5yZXF1ZXN0ID0gcmVxdWVzdDtcbiAgICB0aGlzLnJlc3BvbnNlID0gcmVzcG9uc2U7XG4gICAgdGhpcy5yZXF1ZXN0Qm9keSA9IHJlcXVlc3RCb2R5O1xuICAgIHRoaXMuX3Jlc3BvbnNlQm9keSA9IHJlc3BvbnNlQm9keTtcbiAgfVxuXG4gIC8qKlxuICAgKiBDaGFuZ2VzIHRoZSBib2R5IG9mIHRoZSBIVFRQIHJlc3BvbnNlLiBBcHByb3ByaWF0ZWx5IHVwZGF0ZXMgY29udGVudC1sZW5ndGguXG4gICAqIEBwYXJhbSBiIFRoZSBuZXcgYm9keSBjb250ZW50cy5cbiAgICovXG4gIHB1YmxpYyBzZXRSZXNwb25zZUJvZHkoYjogQnVmZmVyKSB7XG4gICAgdGhpcy5fcmVzcG9uc2VCb2R5ID0gYjtcbiAgICAvLyBVcGRhdGUgY29udGVudC1sZW5ndGguXG4gICAgdGhpcy5yZXNwb25zZS5zZXRIZWFkZXIoJ2NvbnRlbnQtbGVuZ3RoJywgYCR7Yi5sZW5ndGh9YCk7XG4gICAgLy8gVE9ETzogQ29udGVudC1lbmNvZGluZz9cbiAgfVxuICBcbiAgLyoqXG4gICAqIENoYW5nZXMgdGhlIHN0YXR1cyBjb2RlIG9mIHRoZSBIVFRQIHJlc3BvbnNlLlxuICAgKiBAcGFyYW0gY29kZSBUaGUgbmV3IHN0YXR1cyBjb2RlLlxuICAgKi9cbiAgcHVibGljIHNldFN0YXR1c0NvZGUoY29kZTogbnVtYmVyKSB7XG4gICAgdGhpcy5yZXNwb25zZS5zdGF0dXNDb2RlID0gY29kZTtcbiAgfVxuXG4gIC8qKlxuICAgKiBQYWNrIGludG8gYSBidWZmZXIgZm9yIHRyYW5zbWlzc2lvbiB0byBNSVRNUHJveHkuXG4gICAqL1xuICBwdWJsaWMgdG9CdWZmZXIoKTogQnVmZmVyIHtcbiAgICBjb25zdCBtZXRhZGF0YSA9IEJ1ZmZlci5mcm9tKEpTT04uc3RyaW5naWZ5KHRoaXMucmVzcG9uc2UpLCAndXRmOCcpO1xuICAgIGNvbnN0IG1ldGFkYXRhTGVuZ3RoID0gbWV0YWRhdGEubGVuZ3RoO1xuICAgIGNvbnN0IHJlc3BvbnNlTGVuZ3RoID0gdGhpcy5fcmVzcG9uc2VCb2R5Lmxlbmd0aFxuICAgIGNvbnN0IHJ2ID0gQnVmZmVyLmFsbG9jKDggKyBtZXRhZGF0YUxlbmd0aCArIHJlc3BvbnNlTGVuZ3RoKTtcbiAgICBydi53cml0ZUludDMyTEUobWV0YWRhdGFMZW5ndGgsIDApO1xuICAgIHJ2LndyaXRlSW50MzJMRShyZXNwb25zZUxlbmd0aCwgNCk7XG4gICAgbWV0YWRhdGEuY29weShydiwgOCk7XG4gICAgdGhpcy5fcmVzcG9uc2VCb2R5LmNvcHkocnYsIDggKyBtZXRhZGF0YUxlbmd0aCk7XG4gICAgcmV0dXJuIHJ2O1xuICB9XG59XG5cbmV4cG9ydCBjbGFzcyBTdGFzaGVkSXRlbSB7XG4gIGNvbnN0cnVjdG9yKFxuICAgIHB1YmxpYyByZWFkb25seSByYXdVcmw6IHN0cmluZyxcbiAgICBwdWJsaWMgcmVhZG9ubHkgbWltZVR5cGU6IHN0cmluZyxcbiAgICBwdWJsaWMgcmVhZG9ubHkgZGF0YTogQnVmZmVyKSB7fVxuXG4gIHB1YmxpYyBnZXQgc2hvcnRNaW1lVHlwZSgpOiBzdHJpbmcge1xuICAgIGxldCBtaW1lID0gdGhpcy5taW1lVHlwZS50b0xvd2VyQ2FzZSgpO1xuICAgIGlmIChtaW1lLmluZGV4T2YoXCI7XCIpICE9PSAtMSkge1xuICAgICAgbWltZSA9IG1pbWUuc2xpY2UoMCwgbWltZS5pbmRleE9mKFwiO1wiKSk7XG4gICAgfVxuICAgIHJldHVybiBtaW1lO1xuICB9XG5cbiAgcHVibGljIGdldCBpc0h0bWwoKTogYm9vbGVhbiB7XG4gICAgcmV0dXJuIHRoaXMuc2hvcnRNaW1lVHlwZSA9PT0gXCJ0ZXh0L2h0bWxcIjtcbiAgfVxuXG4gIHB1YmxpYyBnZXQgaXNKYXZhU2NyaXB0KCk6IGJvb2xlYW4ge1xuICAgIHN3aXRjaCh0aGlzLnNob3J0TWltZVR5cGUpIHtcbiAgICAgIGNhc2UgJ3RleHQvamF2YXNjcmlwdCc6XG4gICAgICBjYXNlICdhcHBsaWNhdGlvbi9qYXZhc2NyaXB0JzpcbiAgICAgIGNhc2UgJ3RleHQveC1qYXZhc2NyaXB0JzpcbiAgICAgIGNhc2UgJ2FwcGxpY2F0aW9uL3gtamF2YXNjcmlwdCc6XG4gICAgICAgIHJldHVybiB0cnVlO1xuICAgICAgZGVmYXVsdDpcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgfVxufVxuXG5mdW5jdGlvbiBkZWZhdWx0U3Rhc2hGaWx0ZXIodXJsOiBzdHJpbmcsIGl0ZW06IFN0YXNoZWRJdGVtKTogYm9vbGVhbiB7XG4gIHJldHVybiBpdGVtLmlzSmF2YVNjcmlwdCB8fCBpdGVtLmlzSHRtbDtcbn1cblxuLyoqXG4gKiBDbGFzcyB0aGF0IGxhdW5jaGVzIE1JVE0gcHJveHkgYW5kIHRhbGtzIHRvIGl0IHZpYSBXZWJTb2NrZXRzLlxuICovXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBNSVRNUHJveHkge1xuICBwcml2YXRlIHN0YXRpYyBfYWN0aXZlUHJvY2Vzc2VzOiBDaGlsZFByb2Nlc3NbXSA9IFtdO1xuXG4gIC8qKlxuICAgKiBDcmVhdGVzIGEgbmV3IE1JVE1Qcm94eSBpbnN0YW5jZS5cbiAgICogQHBhcmFtIGNiIENhbGxlZCB3aXRoIGludGVyY2VwdGVkIEhUVFAgcmVxdWVzdHMgLyByZXNwb25zZXMuXG4gICAqIEBwYXJhbSBpbnRlcmNlcHRQYXRocyBMaXN0IG9mIHBhdGhzIHRvIGNvbXBsZXRlbHkgaW50ZXJjZXB0IHdpdGhvdXQgc2VuZGluZyB0byB0aGUgc2VydmVyIChlLmcuIFsnL2V2YWwnXSlcbiAgICogQHBhcmFtIHF1aWV0IElmIHRydWUsIGRvIG5vdCBwcmludCBkZWJ1Z2dpbmcgbWVzc2FnZXMgKGRlZmF1bHRzIHRvICd0cnVlJykuXG4gICAqIEBwYXJhbSBvbmx5SW50ZXJjZXB0VGV4dEZpbGVzIElmIHRydWUsIG9ubHkgaW50ZXJjZXB0IHRleHQgZmlsZXMgKEphdmFTY3JpcHQvSFRNTC9DU1MvZXRjLCBhbmQgaWdub3JlIG1lZGlhIGZpbGVzKS5cbiAgICovXG4gIHB1YmxpYyBzdGF0aWMgYXN5bmMgQ3JlYXRlKGNiOiBJbnRlcmNlcHRvciA9IG5vcEludGVyY2VwdG9yLCBpbnRlcmNlcHRQYXRoczogc3RyaW5nW10gPSBbXSwgcXVpZXQ6IGJvb2xlYW4gPSB0cnVlLCBvbmx5SW50ZXJjZXB0VGV4dEZpbGVzID0gZmFsc2UsIGlnbm9yZUhvc3RzOiBzdHJpbmcgfCBudWxsID0gbnVsbCk6IFByb21pc2U8TUlUTVByb3h5PiB7XG4gICAgLy8gQ29uc3RydWN0IFdlYlNvY2tldCBzZXJ2ZXIsIGFuZCB3YWl0IGZvciBpdCB0byBiZWdpbiBsaXN0ZW5pbmcuXG4gICAgY29uc3Qgd3NzID0gbmV3IFdlYlNvY2tldFNlcnZlcih7IHBvcnQ6IDg3NjUgfSk7XG4gICAgY29uc3QgcHJveHlDb25uZWN0ZWQgPSBuZXcgUHJvbWlzZTx2b2lkPigocmVzb2x2ZSwgcmVqZWN0KSA9PiB7XG4gICAgICB3c3Mub25jZSgnY29ubmVjdGlvbicsICgpID0+IHtcbiAgICAgICAgcmVzb2x2ZSgpO1xuICAgICAgfSk7XG4gICAgfSk7XG4gICAgY29uc3QgbXAgPSBuZXcgTUlUTVByb3h5KGNiLCBvbmx5SW50ZXJjZXB0VGV4dEZpbGVzKTtcbiAgICAvLyBTZXQgdXAgV1NTIGNhbGxiYWNrcyBiZWZvcmUgTUlUTVByb3h5IGNvbm5lY3RzLlxuICAgIG1wLl9pbml0aWFsaXplV1NTKHdzcyk7XG4gICAgYXdhaXQgbmV3IFByb21pc2U8dm9pZD4oKHJlc29sdmUsIHJlamVjdCkgPT4ge1xuICAgICAgd3NzLm9uY2UoJ2xpc3RlbmluZycsICgpID0+IHtcbiAgICAgICAgd3NzLnJlbW92ZUxpc3RlbmVyKCdlcnJvcicsIHJlamVjdCk7XG4gICAgICAgIHJlc29sdmUoKTtcbiAgICAgIH0pO1xuICAgICAgd3NzLm9uY2UoJ2Vycm9yJywgcmVqZWN0KTtcbiAgICB9KTtcblxuICAgIHRyeSB7XG4gICAgICB0cnkge1xuICAgICAgICBhd2FpdCB3YWl0Rm9yUG9ydCg4MDgwLCAxKTtcbiAgICAgICAgaWYgKCFxdWlldCkge1xuICAgICAgICAgIGNvbnNvbGUubG9nKGBNSVRNUHJveHkgYWxyZWFkeSBydW5uaW5nLmApO1xuICAgICAgICB9XG4gICAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgIGlmICghcXVpZXQpIHtcbiAgICAgICAgICBjb25zb2xlLmxvZyhgTUlUTVByb3h5IG5vdCBydW5uaW5nOyBzdGFydGluZyB1cCBtaXRtcHJveHkuYCk7XG4gICAgICAgIH1cbiAgICAgICAgLy8gU3RhcnQgdXAgTUlUTSBwcm9jZXNzLlxuICAgICAgICAvLyAtLWFudGljYWNoZSBtZWFucyB0byBkaXNhYmxlIGNhY2hpbmcsIHdoaWNoIGdldHMgaW4gdGhlIHdheSBvZiB0cmFuc3BhcmVudGx5IHJld3JpdGluZyBjb250ZW50LlxuICAgICAgICBjb25zdCBzY3JpcHRBcmdzID0gaW50ZXJjZXB0UGF0aHMubGVuZ3RoID4gMCA/IFtcIi0tc2V0XCIsIGBpbnRlcmNlcHQ9JHtpbnRlcmNlcHRQYXRocy5qb2luKFwiLFwiKX1gXSA6IFtdO1xuICAgICAgICBzY3JpcHRBcmdzLnB1c2goXCItLXNldFwiLCBgb25seUludGVyY2VwdFRleHRGaWxlcz0ke29ubHlJbnRlcmNlcHRUZXh0RmlsZXN9YCk7XG4gICAgICAgIGlmIChpZ25vcmVIb3N0cykge1xuICAgICAgICAgIHNjcmlwdEFyZ3MucHVzaChgLS1pZ25vcmUtaG9zdHNgLCBpZ25vcmVIb3N0cyk7XG4gICAgICAgIH1cblxuICAgICAgICBjb25zdCBvcHRpb25zID0gW1wiLS1hbnRpY2FjaGVcIiwgXCItc1wiLCByZXNvbHZlKF9fZGlybmFtZSwgYC4uL3NjcmlwdHMvcHJveHkucHlgKV0uY29uY2F0KHNjcmlwdEFyZ3MpO1xuICAgICAgICBpZiAocXVpZXQpIHtcbiAgICAgICAgICBvcHRpb25zLnB1c2goJy1xJyk7XG4gICAgICAgIH1cbiAgICAgICAgXG4gICAgICAgIC8vIGFsbG93IHNlbGYtc2lnbmVkIFNTTCBjZXJ0aWZpY2F0ZXNcbiAgICAgICAgb3B0aW9ucy5wdXNoKFwiLS1zc2wtaW5zZWN1cmVcIik7XG4gICAgICAgIFxuICAgICAgICBjb25zdCBtaXRtUHJvY2VzcyA9IHNwYXduKFwibWl0bWR1bXBcIiwgb3B0aW9ucywge1xuICAgICAgICAgIHN0ZGlvOiAnaW5oZXJpdCdcbiAgICAgICAgfSk7XG4gICAgICAgIGNvbnN0IG1pdG1Qcm94eUV4aXRlZCA9IG5ldyBQcm9taXNlPHZvaWQ+KChfLCByZWplY3QpID0+IHtcbiAgICAgICAgICBtaXRtUHJvY2Vzcy5vbmNlKCdlcnJvcicsIHJlamVjdCk7XG4gICAgICAgICAgbWl0bVByb2Nlc3Mub25jZSgnZXhpdCcsIHJlamVjdCk7XG4gICAgICAgIH0pO1xuICAgICAgICBpZiAoTUlUTVByb3h5Ll9hY3RpdmVQcm9jZXNzZXMucHVzaChtaXRtUHJvY2VzcykgPT09IDEpIHtcbiAgICAgICAgICBwcm9jZXNzLm9uKCdTSUdJTlQnLCBNSVRNUHJveHkuX2NsZWFudXApO1xuICAgICAgICAgIHByb2Nlc3Mub24oJ2V4aXQnLCBNSVRNUHJveHkuX2NsZWFudXApO1xuICAgICAgICB9XG4gICAgICAgIG1wLl9pbml0aWFsaXplTUlUTVByb3h5KG1pdG1Qcm9jZXNzKTtcbiAgICAgICAgLy8gV2FpdCBmb3IgcG9ydCA4MDgwIHRvIGNvbWUgb25saW5lLlxuICAgICAgICBjb25zdCB3YWl0aW5nRm9yUG9ydCA9IHdhaXRGb3JQb3J0KDgwODApO1xuICAgICAgICB0cnkge1xuICAgICAgICAgIC8vIEZhaWxzIGlmIG1pdG1wcm94eSBleGl0cyBiZWZvcmUgcG9ydCBiZWNvbWVzIGF2YWlsYWJsZS5cbiAgICAgICAgICBhd2FpdCBQcm9taXNlLnJhY2UoW21pdG1Qcm94eUV4aXRlZCwgd2FpdGluZ0ZvclBvcnRdKTtcbiAgICAgICAgfSBjYXRjaCAoZSkge1xuICAgICAgICAgIGlmIChlICYmIHR5cGVvZihlKSA9PT0gJ29iamVjdCcgJiYgZS5jb2RlID09PSBcIkVOT0VOVFwiKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYG1pdG1kdW1wLCB3aGljaCBpcyBhbiBleGVjdXRhYmxlIHRoYXQgc2hpcHMgd2l0aCBtaXRtcHJveHksIGlzIG5vdCBvbiB5b3VyIFBBVEguIFBsZWFzZSBlbnN1cmUgdGhhdCB5b3UgY2FuIHJ1biBtaXRtZHVtcCAtLXZlcnNpb24gc3VjY2Vzc2Z1bGx5IGZyb20geW91ciBjb21tYW5kIGxpbmUuYClcbiAgICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBVbmFibGUgdG8gc3RhcnQgbWl0bXByb3h5OiAke2V9YCk7XG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICB9XG4gICAgICBhd2FpdCBwcm94eUNvbm5lY3RlZDtcbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICBhd2FpdCBuZXcgUHJvbWlzZTxhbnk+KChyZXNvbHZlKSA9PiB3c3MuY2xvc2UocmVzb2x2ZSkpO1xuICAgICAgdGhyb3cgZTtcbiAgICB9XG5cbiAgICByZXR1cm4gbXA7XG4gIH1cblxuICBwcml2YXRlIHN0YXRpYyBfY2xlYW51cENhbGxlZCA9IGZhbHNlO1xuICBwcml2YXRlIHN0YXRpYyBfY2xlYW51cCgpOiB2b2lkIHtcbiAgICBpZiAoTUlUTVByb3h5Ll9jbGVhbnVwQ2FsbGVkKSB7XG4gICAgICByZXR1cm47XG4gICAgfVxuICAgIE1JVE1Qcm94eS5fY2xlYW51cENhbGxlZCA9IHRydWU7XG4gICAgTUlUTVByb3h5Ll9hY3RpdmVQcm9jZXNzZXMuZm9yRWFjaCgocCkgPT4ge1xuICAgICAgcC5raWxsKCdTSUdLSUxMJyk7XG4gICAgfSk7XG4gIH1cblxuICBwcml2YXRlIF9zdGFzaEVuYWJsZWQ6IGJvb2xlYW4gPSBmYWxzZTtcbiAgLy8gVG9nZ2xlIHdoZXRoZXIgb3Igbm90IG1pdG1wcm94eS1ub2RlIHN0YXNoZXMgbW9kaWZpZWQgc2VydmVyIHJlc3BvbnNlcy5cbiAgLy8gKipOb3QgdXNlZCBmb3IgcGVyZm9ybWFuY2UqKiwgYnV0IGVuYWJsZXMgTm9kZS5qcyBjb2RlIHRvIGZldGNoIHByZXZpb3VzIHNlcnZlciByZXNwb25zZXMgZnJvbSB0aGUgcHJveHkuXG4gIHB1YmxpYyBnZXQgc3Rhc2hFbmFibGVkKCk6IGJvb2xlYW4ge1xuICAgIHJldHVybiB0aGlzLl9zdGFzaEVuYWJsZWQ7XG4gIH1cbiAgcHVibGljIHNldCBzdGFzaEVuYWJsZWQodjogYm9vbGVhbikge1xuICAgIGlmICghdikge1xuICAgICAgdGhpcy5fc3Rhc2guY2xlYXIoKTtcbiAgICB9XG4gICAgdGhpcy5fc3Rhc2hFbmFibGVkID0gdjtcbiAgfVxuICBwcml2YXRlIF9taXRtUHJvY2VzczogQ2hpbGRQcm9jZXNzID0gbnVsbDtcbiAgcHJpdmF0ZSBfbWl0bUVycm9yOiBFcnJvciA9IG51bGw7XG4gIHByaXZhdGUgX3dzczogV2ViU29ja2V0U2VydmVyID0gbnVsbDtcbiAgcHVibGljIGNiOiBJbnRlcmNlcHRvcjtcbiAgcHVibGljIHJlYWRvbmx5IG9ubHlJbnRlcmNlcHRUZXh0RmlsZXM6IGJvb2xlYW47XG4gIHByaXZhdGUgX3N0YXNoID0gbmV3IE1hcDxzdHJpbmcsIFN0YXNoZWRJdGVtPigpO1xuICBwcml2YXRlIF9zdGFzaEZpbHRlcjogKHVybDogc3RyaW5nLCBpdGVtOiBTdGFzaGVkSXRlbSkgPT4gYm9vbGVhbiA9IGRlZmF1bHRTdGFzaEZpbHRlcjtcbiAgcHVibGljIGdldCBzdGFzaEZpbHRlcigpOiAodXJsOiBzdHJpbmcsIGl0ZW06IFN0YXNoZWRJdGVtKSA9PiBib29sZWFuIHtcbiAgICByZXR1cm4gdGhpcy5fc3Rhc2hGaWx0ZXI7XG4gIH1cbiAgcHVibGljIHNldCBzdGFzaEZpbHRlcih2YWx1ZTogKHVybDogc3RyaW5nLCBpdGVtOiBTdGFzaGVkSXRlbSkgPT4gYm9vbGVhbikge1xuICAgIGlmICh0eXBlb2YodmFsdWUpID09PSAnZnVuY3Rpb24nKSB7XG4gICAgICB0aGlzLl9zdGFzaEZpbHRlciA9IHZhbHVlO1xuICAgIH0gZWxzZSBpZiAodmFsdWUgPT09IG51bGwpIHtcbiAgICAgIHRoaXMuX3N0YXNoRmlsdGVyID0gZGVmYXVsdFN0YXNoRmlsdGVyO1xuICAgIH0gZWxzZSB7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoYEludmFsaWQgc3Rhc2ggZmlsdGVyOiBFeHBlY3RlZCBhIGZ1bmN0aW9uLmApO1xuICAgIH1cbiAgfVxuXG4gIHByaXZhdGUgY29uc3RydWN0b3IoY2I6IEludGVyY2VwdG9yLCBvbmx5SW50ZXJjZXB0VGV4dEZpbGVzOiBib29sZWFuKSB7XG4gICAgdGhpcy5jYiA9IGNiO1xuICAgIHRoaXMub25seUludGVyY2VwdFRleHRGaWxlcyA9IG9ubHlJbnRlcmNlcHRUZXh0RmlsZXM7XG4gIH1cblxuICBwcml2YXRlIF9pbml0aWFsaXplV1NTKHdzczogV2ViU29ja2V0U2VydmVyKTogdm9pZCB7XG4gICAgdGhpcy5fd3NzID0gd3NzO1xuICAgIHRoaXMuX3dzcy5vbignY29ubmVjdGlvbicsICh3cykgPT4ge1xuICAgICAgd3Mub24oJ2Vycm9yJywgKGUpID0+IHtcbiAgICAgICAgaWYgKChlIGFzIGFueSkuY29kZSAhPT0gXCJFQ09OTlJFU0VUXCIpIHtcbiAgICAgICAgICBjb25zb2xlLmxvZyhgV2ViU29ja2V0IGVycm9yOiAke2V9YCk7XG4gICAgICAgIH1cbiAgICAgIH0pO1xuICAgICAgd3Mub24oJ21lc3NhZ2UnLCBhc3luYyAobWVzc2FnZTogQnVmZmVyKSA9PiB7XG4gICAgICAgIGNvbnN0IG9yaWdpbmFsID0gSW50ZXJjZXB0ZWRIVFRQTWVzc2FnZS5Gcm9tQnVmZmVyKG1lc3NhZ2UpO1xuICAgICAgICBjb25zdCBydiA9IHRoaXMuY2Iob3JpZ2luYWwpO1xuICAgICAgICBpZiAocnYgJiYgdHlwZW9mKHJ2KSA9PT0gJ29iamVjdCcgJiYgcnYudGhlbikge1xuICAgICAgICAgIGF3YWl0IHJ2O1xuICAgICAgICB9XG4gICAgICAgIC8vIFJlbW92ZSB0cmFuc2Zlci1lbmNvZGluZy4gV2UgZG9uJ3Qgc3VwcG9ydCBjaHVua2VkLlxuICAgICAgICBpZiAodGhpcy5fc3Rhc2hFbmFibGVkKSB7XG4gICAgICAgICAgY29uc3QgaXRlbSA9IG5ldyBTdGFzaGVkSXRlbShvcmlnaW5hbC5yZXF1ZXN0LnJhd1VybCwgb3JpZ2luYWwucmVzcG9uc2UuZ2V0SGVhZGVyKCdjb250ZW50LXR5cGUnKSwgb3JpZ2luYWwucmVzcG9uc2VCb2R5KTtcbiAgICAgICAgICBpZiAodGhpcy5fc3Rhc2hGaWx0ZXIob3JpZ2luYWwucmVxdWVzdC5yYXdVcmwsIGl0ZW0pKSB7XG4gICAgICAgICAgICB0aGlzLl9zdGFzaC5zZXQob3JpZ2luYWwucmVxdWVzdC5yYXdVcmwsIGl0ZW0pO1xuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgICB3cy5zZW5kKG9yaWdpbmFsLnRvQnVmZmVyKCkpO1xuICAgICAgfSk7XG4gICAgfSk7XG4gIH1cblxuICBwcml2YXRlIF9pbml0aWFsaXplTUlUTVByb3h5KG1pdG1Qcm94eTogQ2hpbGRQcm9jZXNzKTogdm9pZCB7XG4gICAgdGhpcy5fbWl0bVByb2Nlc3MgPSBtaXRtUHJveHk7XG4gICAgdGhpcy5fbWl0bVByb2Nlc3Mub24oJ2V4aXQnLCAoY29kZSwgc2lnbmFsKSA9PiB7XG4gICAgICBjb25zdCBpbmRleCA9IE1JVE1Qcm94eS5fYWN0aXZlUHJvY2Vzc2VzLmluZGV4T2YodGhpcy5fbWl0bVByb2Nlc3MpO1xuICAgICAgaWYgKGluZGV4ICE9PSAtMSkge1xuICAgICAgICBNSVRNUHJveHkuX2FjdGl2ZVByb2Nlc3Nlcy5zcGxpY2UoaW5kZXgsIDEpO1xuICAgICAgfVxuICAgICAgaWYgKGNvZGUgIT09IG51bGwpIHtcbiAgICAgICAgaWYgKGNvZGUgIT09IDApIHtcbiAgICAgICAgICB0aGlzLl9taXRtRXJyb3IgPSBuZXcgRXJyb3IoYFByb2Nlc3MgZXhpdGVkIHdpdGggY29kZSAke2NvZGV9LmApO1xuICAgICAgICB9XG4gICAgICB9IGVsc2Uge1xuICAgICAgICB0aGlzLl9taXRtRXJyb3IgPSBuZXcgRXJyb3IoYFByb2Nlc3MgZXhpdGVkIGR1ZSB0byBzaWduYWwgJHtzaWduYWx9LmApO1xuICAgICAgfVxuICAgIH0pO1xuICAgIHRoaXMuX21pdG1Qcm9jZXNzLm9uKCdlcnJvcicsIChlcnIpID0+IHtcbiAgICAgIHRoaXMuX21pdG1FcnJvciA9IGVycjtcbiAgICB9KTtcbiAgfVxuXG4gIC8qKlxuICAgKiBSZXRyaWV2ZXMgdGhlIGdpdmVuIFVSTCBmcm9tIHRoZSBzdGFzaC5cbiAgICogQHBhcmFtIHVybFxuICAgKi9cbiAgcHVibGljIGdldEZyb21TdGFzaCh1cmw6IHN0cmluZyk6IFN0YXNoZWRJdGVtIHtcbiAgICByZXR1cm4gdGhpcy5fc3Rhc2guZ2V0KHVybCk7XG4gIH1cblxuICBwdWJsaWMgZm9yRWFjaFN0YXNoSXRlbShjYjogKHZhbHVlOiBTdGFzaGVkSXRlbSwgdXJsOiBzdHJpbmcpID0+IHZvaWQpOiB2b2lkIHtcbiAgICB0aGlzLl9zdGFzaC5mb3JFYWNoKGNiKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBSZXF1ZXN0cyB0aGUgZ2l2ZW4gVVJMIGZyb20gdGhlIHByb3h5LlxuICAgKi9cbiAgcHVibGljIGFzeW5jIHByb3h5R2V0KHVybFN0cmluZzogc3RyaW5nKTogUHJvbWlzZTxIVFRQUmVzcG9uc2U+IHtcbiAgICBjb25zdCB1cmwgPSBwYXJzZVVSTCh1cmxTdHJpbmcpO1xuICAgIGNvbnN0IGdldCA9IHVybC5wcm90b2NvbCA9PT0gXCJodHRwOlwiID8gaHR0cEdldCA6IGh0dHBzR2V0O1xuICAgIHJldHVybiBuZXcgUHJvbWlzZTxIVFRQUmVzcG9uc2U+KChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICAgIGNvbnN0IHJlcSA9IGdldCh7XG4gICAgICAgIHVybDogdXJsU3RyaW5nLFxuICAgICAgICBoZWFkZXJzOiB7XG4gICAgICAgICAgaG9zdDogdXJsLmhvc3RcbiAgICAgICAgfSxcbiAgICAgICAgaG9zdDogJ2xvY2FsaG9zdCcsXG4gICAgICAgIHBvcnQ6IDgwODAsXG4gICAgICAgIHBhdGg6IHVybFN0cmluZ1xuICAgICAgfSwgKHJlcykgPT4ge1xuICAgICAgICBjb25zdCBkYXRhID0gbmV3IEFycmF5PEJ1ZmZlcj4oKTtcbiAgICAgICAgcmVzLm9uKCdkYXRhJywgKGNodW5rOiBCdWZmZXIpID0+IHtcbiAgICAgICAgICBkYXRhLnB1c2goY2h1bmspO1xuICAgICAgICB9KTtcbiAgICAgICAgcmVzLm9uKCdlbmQnLCAoKSA9PiB7XG4gICAgICAgICAgY29uc3QgZCA9IEJ1ZmZlci5jb25jYXQoZGF0YSk7XG4gICAgICAgICAgcmVzb2x2ZSh7XG4gICAgICAgICAgICBzdGF0dXNDb2RlOiByZXMuc3RhdHVzQ29kZSxcbiAgICAgICAgICAgIGhlYWRlcnM6IHJlcy5oZWFkZXJzLFxuICAgICAgICAgICAgYm9keTogZFxuICAgICAgICAgIH0gYXMgSFRUUFJlc3BvbnNlKTtcbiAgICAgICAgfSk7XG4gICAgICAgIHJlcy5vbmNlKCdlcnJvcicsIHJlamVjdCk7XG4gICAgICB9KTtcbiAgICAgIHJlcS5vbmNlKCdlcnJvcicsIHJlamVjdCk7XG4gICAgfSk7XG4gIH1cblxuICBwdWJsaWMgYXN5bmMgc2h1dGRvd24oKTogUHJvbWlzZTx2b2lkPiB7XG4gICAgcmV0dXJuIG5ldyBQcm9taXNlPHZvaWQ+KChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICAgIGNvbnN0IGNsb3NlV1NTID0gKCkgPT4ge1xuICAgICAgICB0aGlzLl93c3MuY2xvc2UoKGVycikgPT4ge1xuICAgICAgICAgIGlmIChlcnIpIHtcbiAgICAgICAgICAgIHJlamVjdChlcnIpO1xuICAgICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICByZXNvbHZlKCk7XG4gICAgICAgICAgfVxuICAgICAgICB9KTtcbiAgICAgIH07XG5cbiAgICAgIGlmICh0aGlzLl9taXRtUHJvY2VzcyAmJiAhdGhpcy5fbWl0bVByb2Nlc3Mua2lsbGVkKSB7XG4gICAgICAgIHRoaXMuX21pdG1Qcm9jZXNzLm9uY2UoJ2V4aXQnLCAoY29kZSwgc2lnbmFsKSA9PiB7XG4gICAgICAgICAgY2xvc2VXU1MoKTtcbiAgICAgICAgfSk7XG4gICAgICAgIHRoaXMuX21pdG1Qcm9jZXNzLmtpbGwoJ1NJR1RFUk0nKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGNsb3NlV1NTKCk7XG4gICAgICB9XG4gICAgfSk7XG4gIH1cbn1cbiJdfQ== -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mitmproxy", 3 | "version": "2.1.2", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "7.0.44", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.44.tgz", 10 | "integrity": "sha512-5ZskbOk+/EIZErNRo8bgemhtw99PB+CsdOm2wM5qAgc+MwAVL6L9RZv2Hin7Y8S7FewCkPqNlw+3hTmT+PsnJA==" 11 | }, 12 | "@types/ws": { 13 | "version": "3.2.0", 14 | "resolved": "https://registry.npmjs.org/@types/ws/-/ws-3.2.0.tgz", 15 | "integrity": "sha512-XehU2SdII5wu7EUV1bAwCoTDZYZCCU7Es7gbHtJjGXq6Bs2AI4HuJ//wvPrVuuYwkkZseQzDUxsZF8Urnb3I1A==", 16 | "requires": { 17 | "@types/node": "*" 18 | } 19 | }, 20 | "async-limiter": { 21 | "version": "1.0.0", 22 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", 23 | "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" 24 | }, 25 | "safe-buffer": { 26 | "version": "5.1.1", 27 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 28 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 29 | }, 30 | "typescript": { 31 | "version": "2.5.3", 32 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3.tgz", 33 | "integrity": "sha512-ptLSQs2S4QuS6/OD1eAKG+S5G8QQtrU5RT32JULdZQtM1L3WTi34Wsu48Yndzi8xsObRAB9RPt/KhA9wlpEF6w==", 34 | "dev": true 35 | }, 36 | "ultron": { 37 | "version": "1.1.0", 38 | "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.0.tgz", 39 | "integrity": "sha1-sHoualQagV/Go0zNRTO67DB8qGQ=" 40 | }, 41 | "ws": { 42 | "version": "3.2.0", 43 | "resolved": "https://registry.npmjs.org/ws/-/ws-3.2.0.tgz", 44 | "integrity": "sha512-hTS3mkXm/j85jTQOIcwVz3yK3up9xHgPtgEhDBOH3G18LDOZmSAG1omJeXejLKJakx+okv8vS1sopgs7rw0kVw==", 45 | "requires": { 46 | "async-limiter": "~1.0.0", 47 | "safe-buffer": "~5.1.0", 48 | "ultron": "~1.1.0" 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mitmproxy", 3 | "version": "2.1.2", 4 | "description": "NodeJS mitmproxy adapter.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "postinstall": "node scripts/install_python_deps.js", 11 | "prepare": "tsc" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/jvilk/mitmproxy-node.git" 16 | }, 17 | "keywords": [ 18 | "proxy", 19 | "mitm", 20 | "rewriting", 21 | "man-in-the-middle", 22 | "transparent" 23 | ], 24 | "author": "John Vilk ", 25 | "license": "MIT", 26 | "homepage": "https://github.com/jvilk/mitmproxy-node", 27 | "dependencies": { 28 | "@types/node": "^7.0.44", 29 | "@types/ws": "^3.2.0", 30 | "ws": "^3.2.0" 31 | }, 32 | "devDependencies": { 33 | "typescript": "^2.5.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | websockets==6.0.0 -------------------------------------------------------------------------------- /scripts/install_python_deps.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const child_process = require('child_process'); 4 | 5 | const result = child_process.spawnSync("python3", ["scripts/install_python_deps.py"], {stdio: "inherit"}); 6 | if (result.error && result.error.code === "ENOENT") { 7 | const result2 = child_process.spawnSync("python", ["scripts/install_python_deps.py"], {stdio: "inherit"}); 8 | if (result2.error) { 9 | console.log("Failed to run python3 or python: "); 10 | console.log(result.error); 11 | console.log(result2.error); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /scripts/install_python_deps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Sanity checks the Python environment 4 | """ 5 | import sys 6 | from subprocess import check_call, check_output 7 | 8 | # Check Python version 9 | if sys.version_info < (3,6): 10 | sys.exit("mitmproxy-node requires a Python version >= 3.6, but found {}.{}".format(sys.version_info[0], sys.version_info[1])) 11 | 12 | # Verify that mitmproxy is installed 13 | try: 14 | mitmdump_output = str(check_output(["mitmdump", "--version"])) 15 | version = mitmdump_output.split("\\n")[0].split(" ")[1].split('.') 16 | print(version) 17 | version[0] = int(version[0]) 18 | version[1] = int(version[1]) 19 | version[2] = int(version[2]) 20 | if tuple(version) < (4,0,0): 21 | sys.exit("mitmproxy-node requires mitmproxy >= 4.0.0, but found {}.{}.{}".format(version[0], version[1], version[2])) 22 | except FileNotFoundError: 23 | sys.exit("mitmproxy-node requires mitmproxy to be installed. See http://docs.mitmproxy.org/en/stable/install.html for instructions.") 24 | 25 | # Install dependencies with pip3 first. 26 | # If pip3 isn't found, use pip. 27 | try: 28 | check_call(["pip3", "install", "-r", "requirements.txt"]) 29 | except FileNotFoundError: 30 | check_call(["pip", "install", "-r", "requirements.txt"]) 31 | -------------------------------------------------------------------------------- /scripts/proxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Interception proxy using mitmproxy (https://mitmproxy.org). 4 | Communicates to NodeJS via websockets. 5 | """ 6 | 7 | import argparse 8 | import asyncio 9 | import queue 10 | import json 11 | import threading 12 | import typing 13 | import traceback 14 | import sys 15 | import struct 16 | import websockets 17 | from mitmproxy import ctx 18 | from mitmproxy import http 19 | 20 | def convert_headers_to_bytes(header_entry): 21 | """ 22 | Converts a tuple of strings into a tuple of bytes. 23 | """ 24 | return [bytes(header_entry[0], "utf8"), bytes(header_entry[1], "utf8")] 25 | 26 | def convert_body_to_bytes(body): 27 | """ 28 | Converts a HTTP request/response body into a list of numbers. 29 | """ 30 | if body is None: 31 | return bytes() 32 | else: 33 | return body 34 | 35 | def is_text_response(headers): 36 | if 'content-type' in headers: 37 | ct = headers['content-type'].lower() 38 | # Allow all application/ and text/ MIME types. 39 | return 'application' in ct or 'text' in ct or ct.strip() == "" 40 | return True 41 | 42 | class WebSocketAdapter: 43 | """ 44 | Relays HTTP/HTTPS requests to a websocket server. 45 | Enables using MITMProxy from outside of Python. 46 | """ 47 | 48 | def websocket_thread(self): 49 | """ 50 | Main function of the websocket thread. Runs the websocket event loop 51 | until MITMProxy shuts down. 52 | """ 53 | self.worker_event_loop = asyncio.new_event_loop() 54 | self.worker_event_loop.run_until_complete(self.websocket_loop()) 55 | 56 | def __init__(self): 57 | self.queue = queue.Queue() 58 | self.intercept_paths = frozenset([]) 59 | self.only_intercept_text_files = False 60 | self.finished = False 61 | # Start websocket thread 62 | threading.Thread(target=self.websocket_thread).start() 63 | 64 | def load(self, loader): 65 | loader.add_option( 66 | "intercept", str, "", 67 | """ 68 | A list of HTTP paths, delimited by a comma, to intercept and pass to Node without hitting the server. 69 | E.g.: /foo,/bar 70 | """ 71 | ) 72 | loader.add_option( 73 | name = "onlyInterceptTextFiles", 74 | typespec = bool, 75 | default = False, 76 | help = "If true, the plugin only intercepts text files and passes through other types of files", 77 | ) 78 | return 79 | 80 | def configure(self, updates): 81 | if "intercept" in updates: 82 | self.intercept_paths = frozenset(ctx.options.intercept.split(",")) 83 | #print("Intercept paths:") 84 | #print(self.intercept_paths) 85 | if "onlyInterceptTextFiles" in updates: 86 | self.only_intercept_text_files = ctx.options.onlyInterceptTextFiles 87 | #print("Only intercept text files:") 88 | #print(self.only_intercept_text_files) 89 | return 90 | 91 | def send_message(self, metadata, data1, data2): 92 | """ 93 | Sends the given message on the WebSocket connection, 94 | and awaits a response. Metadata is a JSONable object, 95 | and data is bytes. 96 | """ 97 | metadata_bytes = bytes(json.dumps(metadata), 'utf8') 98 | data1_size = len(data1) 99 | data2_size = len(data2) 100 | metadata_size = len(metadata_bytes) 101 | msg = struct.pack(" { 16 | return new Promise((resolve, reject) => { 17 | let retriesRemaining = retries; 18 | let retryInterval = interval; 19 | let timer: NodeJS.Timer = null; 20 | let socket: Socket = null; 21 | 22 | function clearTimerAndDestroySocket() { 23 | clearTimeout(timer); 24 | timer = null; 25 | if (socket) socket.destroy(); 26 | socket = null; 27 | } 28 | 29 | function retry() { 30 | tryToConnect(); 31 | } 32 | 33 | function tryToConnect() { 34 | clearTimerAndDestroySocket(); 35 | 36 | if (--retriesRemaining < 0) { 37 | reject(new Error('out of retries')); 38 | } 39 | 40 | socket = createConnection(port, "localhost", function() { 41 | clearTimerAndDestroySocket(); 42 | if (retriesRemaining >= 0) resolve(); 43 | }); 44 | 45 | timer = setTimeout(function() { retry(); }, retryInterval); 46 | 47 | socket.on('error', function(err) { 48 | clearTimerAndDestroySocket(); 49 | setTimeout(retry, retryInterval); 50 | }); 51 | } 52 | 53 | tryToConnect(); 54 | }); 55 | } 56 | 57 | /** 58 | * Function that intercepts and rewrites HTTP responses. 59 | */ 60 | export type Interceptor = (m: InterceptedHTTPMessage) => void | Promise; 61 | 62 | /** 63 | * An interceptor that does nothing. 64 | */ 65 | export function nopInterceptor(m: InterceptedHTTPMessage): void {} 66 | 67 | /** 68 | * The core HTTP response. 69 | */ 70 | export interface HTTPResponse { 71 | statusCode: number, 72 | headers: {[name: string]: string}; 73 | body: Buffer; 74 | } 75 | 76 | /** 77 | * Metadata associated with a request/response pair. 78 | */ 79 | interface HTTPMessageMetadata { 80 | request: HTTPRequestMetadata; 81 | response: HTTPResponseMetadata; 82 | } 83 | 84 | /** 85 | * Metadata associated with an HTTP request. 86 | */ 87 | export interface HTTPRequestMetadata { 88 | // GET, DELETE, POST, etc. 89 | method: string; 90 | // Target URL for the request. 91 | url: string; 92 | // The set of headers from the request, as key-value pairs. 93 | // Since header fields may be repeated, this array may contain multiple entries for the same key. 94 | headers: [string, string][]; 95 | } 96 | 97 | /** 98 | * Metadata associated with an HTTP response. 99 | */ 100 | export interface HTTPResponseMetadata { 101 | // The numerical status code. 102 | status_code: number; 103 | // The set of headers from the response, as key-value pairs. 104 | // Since header fields may be repeated, this array may contain multiple entries for the same key. 105 | headers: [string, string][]; 106 | } 107 | 108 | /** 109 | * Abstract class that represents HTTP headers. 110 | */ 111 | export abstract class AbstractHTTPHeaders { 112 | private _headers: [string, string][]; 113 | // The raw headers, as a sequence of key/value pairs. 114 | // Since header fields may be repeated, this array may contain multiple entries for the same key. 115 | public get headers(): [string, string][] { 116 | return this._headers; 117 | } 118 | constructor(headers: [string, string][]) { 119 | this._headers = headers; 120 | } 121 | 122 | private _indexOfHeader(name: string): number { 123 | const headers = this.headers; 124 | const len = headers.length; 125 | for (let i = 0; i < len; i++) { 126 | if (headers[i][0].toLowerCase() === name) { 127 | return i; 128 | } 129 | } 130 | return -1; 131 | } 132 | 133 | /** 134 | * Get the value of the given header field. 135 | * If there are multiple fields with that name, this only returns the first field's value! 136 | * @param name Name of the header field 137 | */ 138 | public getHeader(name: string): string { 139 | const index = this._indexOfHeader(name.toLowerCase()); 140 | if (index !== -1) { 141 | return this.headers[index][1]; 142 | } 143 | return ''; 144 | } 145 | 146 | /** 147 | * Set the value of the given header field. Assumes that there is only one field with the given name. 148 | * If the field does not exist, it adds a new field with the name and value. 149 | * @param name Name of the field. 150 | * @param value New value. 151 | */ 152 | public setHeader(name: string, value: string): void { 153 | const index = this._indexOfHeader(name.toLowerCase()); 154 | if (index !== -1) { 155 | this.headers[index][1] = value; 156 | } else { 157 | this.headers.push([name, value]); 158 | } 159 | } 160 | 161 | /** 162 | * Removes the header field with the given name. Assumes that there is only one field with the given name. 163 | * Does nothing if field does not exist. 164 | * @param name Name of the field. 165 | */ 166 | public removeHeader(name: string): void { 167 | const index = this._indexOfHeader(name.toLowerCase()); 168 | if (index !== -1) { 169 | this.headers.splice(index, 1); 170 | } 171 | } 172 | 173 | /** 174 | * Removes all header fields. 175 | */ 176 | public clearHeaders(): void { 177 | this._headers = []; 178 | } 179 | } 180 | 181 | /** 182 | * Represents a MITM-ed HTTP response from a server. 183 | */ 184 | export class InterceptedHTTPResponse extends AbstractHTTPHeaders { 185 | // The status code of the HTTP response. 186 | public statusCode: number; 187 | 188 | constructor(metadata: HTTPResponseMetadata) { 189 | super(metadata.headers); 190 | this.statusCode = metadata.status_code; 191 | // We don't support chunked transfers. The proxy already de-chunks it for us. 192 | this.removeHeader('transfer-encoding'); 193 | // MITMProxy decodes the data for us. 194 | this.removeHeader('content-encoding'); 195 | // CSP is bad! 196 | this.removeHeader('content-security-policy'); 197 | this.removeHeader('x-webkit-csp'); 198 | this.removeHeader('x-content-security-policy'); 199 | } 200 | 201 | public toJSON(): HTTPResponseMetadata { 202 | return { 203 | status_code: this.statusCode, 204 | headers: this.headers 205 | }; 206 | } 207 | } 208 | 209 | /** 210 | * Represents an intercepted HTTP request from a client. 211 | */ 212 | export class InterceptedHTTPRequest extends AbstractHTTPHeaders { 213 | // HTTP method (GET/DELETE/etc) 214 | public method: string; 215 | // The URL as a string. 216 | public rawUrl: string; 217 | // The URL as a URL object. 218 | public url: Url; 219 | 220 | constructor(metadata: HTTPRequestMetadata) { 221 | super(metadata.headers); 222 | this.method = metadata.method.toLowerCase(); 223 | this.rawUrl = metadata.url; 224 | this.url = parseURL(this.rawUrl); 225 | } 226 | } 227 | 228 | /** 229 | * Represents an intercepted HTTP request/response pair. 230 | */ 231 | export class InterceptedHTTPMessage { 232 | /** 233 | * Unpack from a Buffer received from MITMProxy. 234 | * @param b 235 | */ 236 | public static FromBuffer(b: Buffer): InterceptedHTTPMessage { 237 | const metadataSize = b.readInt32LE(0); 238 | const requestSize = b.readInt32LE(4); 239 | const responseSize = b.readInt32LE(8); 240 | const metadata: HTTPMessageMetadata = JSON.parse(b.toString("utf8", 12, 12 + metadataSize)); 241 | return new InterceptedHTTPMessage( 242 | new InterceptedHTTPRequest(metadata.request), 243 | new InterceptedHTTPResponse(metadata.response), 244 | b.slice(12 + metadataSize, 12 + metadataSize + requestSize), 245 | b.slice(12 + metadataSize + requestSize, 12 + metadataSize + requestSize + responseSize) 246 | ); 247 | } 248 | 249 | public readonly request: InterceptedHTTPRequest; 250 | public readonly response: InterceptedHTTPResponse; 251 | // The body of the HTTP request. 252 | public readonly requestBody: Buffer; 253 | // The body of the HTTP response. Read-only; change the response body via setResponseBody. 254 | public get responseBody(): Buffer { 255 | return this._responseBody; 256 | } 257 | private _responseBody: Buffer; 258 | private constructor(request: InterceptedHTTPRequest, response: InterceptedHTTPResponse, requestBody: Buffer, responseBody: Buffer) { 259 | this.request = request; 260 | this.response = response; 261 | this.requestBody = requestBody; 262 | this._responseBody = responseBody; 263 | } 264 | 265 | /** 266 | * Changes the body of the HTTP response. Appropriately updates content-length. 267 | * @param b The new body contents. 268 | */ 269 | public setResponseBody(b: Buffer) { 270 | this._responseBody = b; 271 | // Update content-length. 272 | this.response.setHeader('content-length', `${b.length}`); 273 | // TODO: Content-encoding? 274 | } 275 | 276 | /** 277 | * Changes the status code of the HTTP response. 278 | * @param code The new status code. 279 | */ 280 | public setStatusCode(code: number) { 281 | this.response.statusCode = code; 282 | } 283 | 284 | /** 285 | * Pack into a buffer for transmission to MITMProxy. 286 | */ 287 | public toBuffer(): Buffer { 288 | const metadata = Buffer.from(JSON.stringify(this.response), 'utf8'); 289 | const metadataLength = metadata.length; 290 | const responseLength = this._responseBody.length 291 | const rv = Buffer.alloc(8 + metadataLength + responseLength); 292 | rv.writeInt32LE(metadataLength, 0); 293 | rv.writeInt32LE(responseLength, 4); 294 | metadata.copy(rv, 8); 295 | this._responseBody.copy(rv, 8 + metadataLength); 296 | return rv; 297 | } 298 | } 299 | 300 | export class StashedItem { 301 | constructor( 302 | public readonly rawUrl: string, 303 | public readonly mimeType: string, 304 | public readonly data: Buffer) {} 305 | 306 | public get shortMimeType(): string { 307 | let mime = this.mimeType.toLowerCase(); 308 | if (mime.indexOf(";") !== -1) { 309 | mime = mime.slice(0, mime.indexOf(";")); 310 | } 311 | return mime; 312 | } 313 | 314 | public get isHtml(): boolean { 315 | return this.shortMimeType === "text/html"; 316 | } 317 | 318 | public get isJavaScript(): boolean { 319 | switch(this.shortMimeType) { 320 | case 'text/javascript': 321 | case 'application/javascript': 322 | case 'text/x-javascript': 323 | case 'application/x-javascript': 324 | return true; 325 | default: 326 | return false; 327 | } 328 | } 329 | } 330 | 331 | function defaultStashFilter(url: string, item: StashedItem): boolean { 332 | return item.isJavaScript || item.isHtml; 333 | } 334 | 335 | /** 336 | * Class that launches MITM proxy and talks to it via WebSockets. 337 | */ 338 | export default class MITMProxy { 339 | private static _activeProcesses: ChildProcess[] = []; 340 | 341 | /** 342 | * Creates a new MITMProxy instance. 343 | * @param cb Called with intercepted HTTP requests / responses. 344 | * @param interceptPaths List of paths to completely intercept without sending to the server (e.g. ['/eval']) 345 | * @param quiet If true, do not print debugging messages (defaults to 'true'). 346 | * @param onlyInterceptTextFiles If true, only intercept text files (JavaScript/HTML/CSS/etc, and ignore media files). 347 | */ 348 | public static async Create(cb: Interceptor = nopInterceptor, interceptPaths: string[] = [], quiet: boolean = true, onlyInterceptTextFiles = false, ignoreHosts: string | null = null): Promise { 349 | // Construct WebSocket server, and wait for it to begin listening. 350 | const wss = new WebSocketServer({ port: 8765 }); 351 | const proxyConnected = new Promise((resolve, reject) => { 352 | wss.once('connection', () => { 353 | resolve(); 354 | }); 355 | }); 356 | const mp = new MITMProxy(cb, onlyInterceptTextFiles); 357 | // Set up WSS callbacks before MITMProxy connects. 358 | mp._initializeWSS(wss); 359 | await new Promise((resolve, reject) => { 360 | wss.once('listening', () => { 361 | wss.removeListener('error', reject); 362 | resolve(); 363 | }); 364 | wss.once('error', reject); 365 | }); 366 | 367 | try { 368 | try { 369 | await waitForPort(8080, 1); 370 | if (!quiet) { 371 | console.log(`MITMProxy already running.`); 372 | } 373 | } catch (e) { 374 | if (!quiet) { 375 | console.log(`MITMProxy not running; starting up mitmproxy.`); 376 | } 377 | // Start up MITM process. 378 | // --anticache means to disable caching, which gets in the way of transparently rewriting content. 379 | const scriptArgs = interceptPaths.length > 0 ? ["--set", `intercept=${interceptPaths.join(",")}`] : []; 380 | scriptArgs.push("--set", `onlyInterceptTextFiles=${onlyInterceptTextFiles}`); 381 | if (ignoreHosts) { 382 | scriptArgs.push(`--ignore-hosts`, ignoreHosts); 383 | } 384 | 385 | const options = ["--anticache", "-s", resolve(__dirname, `../scripts/proxy.py`)].concat(scriptArgs); 386 | if (quiet) { 387 | options.push('-q'); 388 | } 389 | 390 | // allow self-signed SSL certificates 391 | options.push("--ssl-insecure"); 392 | 393 | const mitmProcess = spawn("mitmdump", options, { 394 | stdio: 'inherit' 395 | }); 396 | const mitmProxyExited = new Promise((_, reject) => { 397 | mitmProcess.once('error', reject); 398 | mitmProcess.once('exit', reject); 399 | }); 400 | if (MITMProxy._activeProcesses.push(mitmProcess) === 1) { 401 | process.on('SIGINT', MITMProxy._cleanup); 402 | process.on('exit', MITMProxy._cleanup); 403 | } 404 | mp._initializeMITMProxy(mitmProcess); 405 | // Wait for port 8080 to come online. 406 | const waitingForPort = waitForPort(8080); 407 | try { 408 | // Fails if mitmproxy exits before port becomes available. 409 | await Promise.race([mitmProxyExited, waitingForPort]); 410 | } catch (e) { 411 | if (e && typeof(e) === 'object' && e.code === "ENOENT") { 412 | 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.`) 413 | } else { 414 | throw new Error(`Unable to start mitmproxy: ${e}`); 415 | } 416 | } 417 | } 418 | await proxyConnected; 419 | } catch (e) { 420 | await new Promise((resolve) => wss.close(resolve)); 421 | throw e; 422 | } 423 | 424 | return mp; 425 | } 426 | 427 | private static _cleanupCalled = false; 428 | private static _cleanup(): void { 429 | if (MITMProxy._cleanupCalled) { 430 | return; 431 | } 432 | MITMProxy._cleanupCalled = true; 433 | MITMProxy._activeProcesses.forEach((p) => { 434 | p.kill('SIGKILL'); 435 | }); 436 | } 437 | 438 | private _stashEnabled: boolean = false; 439 | // Toggle whether or not mitmproxy-node stashes modified server responses. 440 | // **Not used for performance**, but enables Node.js code to fetch previous server responses from the proxy. 441 | public get stashEnabled(): boolean { 442 | return this._stashEnabled; 443 | } 444 | public set stashEnabled(v: boolean) { 445 | if (!v) { 446 | this._stash.clear(); 447 | } 448 | this._stashEnabled = v; 449 | } 450 | private _mitmProcess: ChildProcess = null; 451 | private _mitmError: Error = null; 452 | private _wss: WebSocketServer = null; 453 | public cb: Interceptor; 454 | public readonly onlyInterceptTextFiles: boolean; 455 | private _stash = new Map(); 456 | private _stashFilter: (url: string, item: StashedItem) => boolean = defaultStashFilter; 457 | public get stashFilter(): (url: string, item: StashedItem) => boolean { 458 | return this._stashFilter; 459 | } 460 | public set stashFilter(value: (url: string, item: StashedItem) => boolean) { 461 | if (typeof(value) === 'function') { 462 | this._stashFilter = value; 463 | } else if (value === null) { 464 | this._stashFilter = defaultStashFilter; 465 | } else { 466 | throw new Error(`Invalid stash filter: Expected a function.`); 467 | } 468 | } 469 | 470 | private constructor(cb: Interceptor, onlyInterceptTextFiles: boolean) { 471 | this.cb = cb; 472 | this.onlyInterceptTextFiles = onlyInterceptTextFiles; 473 | } 474 | 475 | private _initializeWSS(wss: WebSocketServer): void { 476 | this._wss = wss; 477 | this._wss.on('connection', (ws) => { 478 | ws.on('error', (e) => { 479 | if ((e as any).code !== "ECONNRESET") { 480 | console.log(`WebSocket error: ${e}`); 481 | } 482 | }); 483 | ws.on('message', async (message: Buffer) => { 484 | const original = InterceptedHTTPMessage.FromBuffer(message); 485 | const rv = this.cb(original); 486 | if (rv && typeof(rv) === 'object' && rv.then) { 487 | await rv; 488 | } 489 | // Remove transfer-encoding. We don't support chunked. 490 | if (this._stashEnabled) { 491 | const item = new StashedItem(original.request.rawUrl, original.response.getHeader('content-type'), original.responseBody); 492 | if (this._stashFilter(original.request.rawUrl, item)) { 493 | this._stash.set(original.request.rawUrl, item); 494 | } 495 | } 496 | ws.send(original.toBuffer()); 497 | }); 498 | }); 499 | } 500 | 501 | private _initializeMITMProxy(mitmProxy: ChildProcess): void { 502 | this._mitmProcess = mitmProxy; 503 | this._mitmProcess.on('exit', (code, signal) => { 504 | const index = MITMProxy._activeProcesses.indexOf(this._mitmProcess); 505 | if (index !== -1) { 506 | MITMProxy._activeProcesses.splice(index, 1); 507 | } 508 | if (code !== null) { 509 | if (code !== 0) { 510 | this._mitmError = new Error(`Process exited with code ${code}.`); 511 | } 512 | } else { 513 | this._mitmError = new Error(`Process exited due to signal ${signal}.`); 514 | } 515 | }); 516 | this._mitmProcess.on('error', (err) => { 517 | this._mitmError = err; 518 | }); 519 | } 520 | 521 | /** 522 | * Retrieves the given URL from the stash. 523 | * @param url 524 | */ 525 | public getFromStash(url: string): StashedItem { 526 | return this._stash.get(url); 527 | } 528 | 529 | public forEachStashItem(cb: (value: StashedItem, url: string) => void): void { 530 | this._stash.forEach(cb); 531 | } 532 | 533 | /** 534 | * Requests the given URL from the proxy. 535 | */ 536 | public async proxyGet(urlString: string): Promise { 537 | const url = parseURL(urlString); 538 | const get = url.protocol === "http:" ? httpGet : httpsGet; 539 | return new Promise((resolve, reject) => { 540 | const req = get({ 541 | url: urlString, 542 | headers: { 543 | host: url.host 544 | }, 545 | host: 'localhost', 546 | port: 8080, 547 | path: urlString 548 | }, (res) => { 549 | const data = new Array(); 550 | res.on('data', (chunk: Buffer) => { 551 | data.push(chunk); 552 | }); 553 | res.on('end', () => { 554 | const d = Buffer.concat(data); 555 | resolve({ 556 | statusCode: res.statusCode, 557 | headers: res.headers, 558 | body: d 559 | } as HTTPResponse); 560 | }); 561 | res.once('error', reject); 562 | }); 563 | req.once('error', reject); 564 | }); 565 | } 566 | 567 | public async shutdown(): Promise { 568 | return new Promise((resolve, reject) => { 569 | const closeWSS = () => { 570 | this._wss.close((err) => { 571 | if (err) { 572 | reject(err); 573 | } else { 574 | resolve(); 575 | } 576 | }); 577 | }; 578 | 579 | if (this._mitmProcess && !this._mitmProcess.killed) { 580 | this._mitmProcess.once('exit', (code, signal) => { 581 | closeWSS(); 582 | }); 583 | this._mitmProcess.kill('SIGTERM'); 584 | } else { 585 | closeWSS(); 586 | } 587 | }); 588 | } 589 | } 590 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "noImplicitAny": true, 6 | "inlineSourceMap": true, 7 | "inlineSources": true, 8 | "noImplicitThis": true, 9 | "noUnusedLocals": true, 10 | "noImplicitReturns": true, 11 | "declaration": true, 12 | "lib": [ 13 | "dom", 14 | "es2015" 15 | ], 16 | "outDir": "dist" 17 | } 18 | } 19 | --------------------------------------------------------------------------------