An optimized, scalable, and fully extensible Man-in-the-Middle (MITM) proxy framework for Node.js.
Built entirely in TypeScript, mitm-core provides a strictly-typed, phased pipeline for intercepting, modifying, and analyzing HTTP/HTTPS traffic. It is designed as a composable core — not a standalone tool — intended to be embedded into security testing platforms, debugging proxies, traffic analyzers, and network tooling.
- Prerequisites
- Features
- Architecture Overview
- Quick Start
- ProxyContext Reference
- Proxy Events Reference
- Scripts
- Roadmap
- Security Notice
- Contributing
- License
Because mitm-core intercepts HTTPS traffic by generating dynamic leaf certificates on the fly, it requires a local Root Certificate Authority (CA) to sign them.
Before starting the proxy, you must generate a Root CA and place it in the creds/__self__ directory at the root of your project.
Expected Directory Structure:
your-project/
├── creds/
│ └── __self__/
│ ├── CA.crt (Your public Root CA)
│ └── key.pem (Your private key)
├── package.json
└── index.js
You can easily generate these files using openssl. Run the following commands in your terminal:
# Create the required directories
mkdir -p creds/__self__
cd creds/__self__
# 1. Generate a 2048-bit private key
openssl genrsa -out key.pem 2048
# 2. Generate the Root CA certificate (Valid for 10 years)
# It will prompt you for details (Country, Org, etc.). You can leave most blank,
# but set the "Common Name" to something recognizable like "mitm-core-root".
openssl req -x509 -new -nodes -key key.pem -sha256 -days 3650 -out CA.crtFor your OS and browser to accept the intercepted HTTPS traffic without throwing privacy errors, you must tell your system to trust the CA.crt file you just generated.
- Double-click the
CA.crtfile. - Click Install Certificate.
- Select Local Machine and click Next.
- Choose Place all certificates in the following store and click Browse.
- Select Trusted Root Certification Authorities and click OK, then Finish.
-
Open the terminal and run:
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain CA.crt
(Alternatively: Double-click the
CA.crtfile to open Keychain Access, find the certificate, double-click it, expand the "Trust" section, and set "When using this certificate" to "Always Trust").
-
Copy the certificate to the system store and update:
sudo cp CA.crt /usr/local/share/ca-certificates/mitm-core.crt sudo update-ca-certificates
(Note: Firefox uses its own certificate store. If you are testing with Firefox, you will need to manually import CA.crt in Firefox Settings -> Privacy & Security -> View Certificates).
| Feature | Description |
|---|---|
| Native TLS Interception | Dynamically generates and signs leaf certificates to intercept HTTPS traffic on the fly using node-forge. |
| Phased Pipeline Architecture | Traffic flows through clearly defined, hackable phases: TCP → Handshake → Request → Response. |
| Strictly Typed Event Bus | Full IDE autocomplete for all proxy events (payloadEvents, tlsLifecycleEvents, etc.) via a custom generic event emitter. |
| Extensible Rule Engine | A flexible framework for defining custom traffic rules, allowing developers to easily build logic like bypassing TLS pinned domains. |
| Response Caching | Built-in LRU-based intelligent caching layer for intercepted HTTP responses. |
| Worker Pool Support | Uses piscina for CPU-bound tasks (e.g., certificate generation) to avoid blocking the event loop. |
| Memory Safe | Carefully designed stream pipelines and socket cleanup to prevent ECONNABORTED crashes and memory leaks. |
| Async Plugin Support | All pipeline hooks are fully async with proper error boundaries. |
| Zero Runtime Bloat | Only two production dependencies: lru-cache and piscina. |
mitm-core intercepts network traffic through a four-phase pipeline. Each phase is independently hookable, making the system highly composable.
Client
│ (incoming connection)
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Proxy │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Middleware → Pipeline.compile() │ │
│ │ │ │
│ │ Registers handlers per phase: │ │
│ │ • tcp → TcpHandler │ │
│ │ • connect → ConnectHandler │ │
│ │ • request → RequestHandler │ │
│ │ • response → ResponseHandler │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ (once at startup) │
│---------------------------------------------------------------------│
│ (per connection) │
│ │
│ Middleware -> Pipeline.run(ctx) |
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Phase: tcp │ │
│ │ TcpHandler │ │
│ │ • Creates ProxyContext for the incoming connection │ │
│ │ • Attaches listeners to handle socket-level errors │ │
│ └──────────────────────────────┬──────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Phase: connect │ │
│ │ ConnectHandler │ │
│ │ • Generates leaf certificate (node-forge) │ │
│ │ • Performs TLS handshake with client │ │
│ │ • Decrypts the HTTPS request │ │
│ └──────────────────────────────┬──────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Phase: request │ │
│ │ RequestHandler │ │
│ │ • Handles plain HTTP and decrypted HTTPS requests │ │
│ │ • Processes and forwards request to upstream server │ │
│ └──────────────────────────────┬──────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Phase: response │ │
│ │ ResponseHandler │ │
│ │ • Fetches upstream response │ │
│ │ • Optionally caches via LRU cache │ │
│ │ • Sends response back to client │ │
│ └──────────────────────────────┬──────────────────────────────┘ │
│ │ │
└──────────────────────────────────┼──────────────────────────────────┘
│ (response)
▼
Client
Proxy— The main proxy server implementation.Middleware— Manages middleware registration and orchestrates the proxy connection lifecycle. Configures event listeners to intercept network traffic, initializes request contexts, and triggers the processing pipeline.Pipeline— Orchestrates the proxy request lifecycle. Maps registered handlers to specific lifecycle phases, executes them sequentially, manages state transitions, and provides centralized error handling.Phase— Represents the distinct lifecycle stages within the pipeline:tcp(raw connection),handshake(protocol negotiation),request(client request), andresponse(upstream response).BaseHandler— Abstract base class for defining handler logic associated with a specificPhase. Subclasses must implement thehandlemethod and declare their targetphase.BasePlugin— Abstract base class for implementing plugins that hook into the proxy event system. Subclasses must specify the target event type and implement therunlogic.RuleEngine<T>— Abstract base class that provides a framework for managing rule configurations. Handles file watching, state parsing, and storage for specific rule implementations.IRuleParser<T>— Defines the strategy for parsing, matching, and formatting rule files. Implementations provide the domain-specific logic to interpret raw file data.
Using CommonJS (require)
const {
Proxy,
BasePlugin,
PipelineAbortSignal,
ProxyContext,
} = require("mitm-core");Using TypeScript / ES Modules (import)
import {
BasePlugin,
PipelineAbortSignal,
Proxy,
ProxyContext,
} from "mitm-core";import { Proxy } from "mitm-core";
const proxy = new Proxy();
proxy.listen(8001); // listens to port 8001import {
BasePlugin,
PipelineAbortSignal,
Proxy,
ProxyContext,
RuleEngine,
} from "mitm-core";
import net from "net";
import { type IRuleParser } from "mitm-core";
// Custom parser to load and compile bypass regex rules from a file
class BypassRuleParser implements IRuleParser<RegExp[]> {
parse(raw: string): RegExp[] {
return Array.from(
new Set(
raw
.split(/\r?\n/)
.map((l) => l.trim())
.filter((l) => l && !l.startsWith("#")),
),
).flatMap((l) => {
try {
return [new RegExp(l, "i")];
} catch (error) {
console.error(`Invalid regex skipped: "${l}"`);
return [];
}
});
}
match(rules: RegExp[], target: string): boolean {
return rules.some((r) => r.test(target));
}
formatForSave(host: string): string {
const escapedHost = host.replace(/\./g, "\\.");
return `^(?:[a-z0-9-]+\\.)*${escapedHost}$`;
}
}
// Engine to manage, query, and update the bypass rule store
class BypassRuleEngine extends RuleEngine<string[] | RegExp[]> {
readonly ruleName = "tls-bypass";
readonly rulePath = "rules/bypass.rules.txt";
readonly parser = new BypassRuleParser();
readonly defaultState: string[] = [];
public match(storeName: string, target: string): boolean {
const store = RuleEngine.getRuleStore(storeName);
if (!store) {
console.warn(`Rule store '${storeName}' not found.`);
return false;
}
return store.match(target);
}
public shouldBypass(host: string): boolean {
return this.match("tls-bypass", host);
}
public saveHostToBypass(host: string, error?: Error, force?: boolean) {
if (this.shouldBypass(host)) return;
const errorCode = error ? (error as any).code : undefined;
if (!force) return;
console.log(`Forcibly bypassing host: ${host}`);
this.store.appendRule(host);
}
}
// Plugin to intercept 'tunnel:pre_establish'
class BypassPlugin extends BasePlugin<"tunnel:pre_establish"> {
readonly event = "tunnel:pre_establish";
private bypassEngine: BypassRuleEngine;
constructor() {
super();
this.bypassEngine = RuleEngine.createRule(BypassRuleEngine);
}
async run({ ctx }: { ctx: ProxyContext }) {
const host = ctx.clientToProxyHost;
if (!host) return;
if (this.bypassEngine.shouldBypass(host)) {
const req = ctx.requestContext.req;
const socket = req?.socket!;
const hostHeader = req!.headers.host!;
const [host, portStr] = hostHeader.split(":");
const port = Number(portStr) || 443;
// Establish direct connection to upstream server
const upstream = net.connect(port, host, () => {
socket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
if (ctx.head && ctx.head.length > 0) {
upstream.write(ctx.head);
}
// Pipe traffic directly (TCP Tunneling)
socket!.pipe(upstream);
upstream.pipe(socket!);
});
upstream.on("error", (err: any) => {
console.error("Direct tunnel error:", ctx.clientToProxyHost, err);
socket?.destroy();
});
upstream.setNoDelay(true);
ctx.isHandled = true;
// Stop the MITM processing pipeline for this request
try {
} finally {
throw new PipelineAbortSignal();
}
}
}
}
const proxy = new Proxy();
const bypassPlugin = new BypassPlugin();
proxy.use(bypassPlugin).listen(8001);The ProxyContext (ctx) maintains the state for each connection and request cycle.
Persists for the lifespan of the underlying TCP connection.
| Property | Type | Description |
|---|---|---|
connectionId |
string |
UUID for the TCP connection. |
socket |
Duplex |
The underlying network stream. |
connectionType |
"tcp" | "http" | "https" |
Current protocol layer. |
head |
any |
Initial buffer chunk for protocol sniffing. |
error |
Error |
Socket or handshake level errors. |
isHandled |
boolean |
Set to true to halt pipeline execution if a plugin takes full control. |
customCertificates |
Map |
Domain-specific overrides for MITM certificates. |
Targeting and URL information.
| Property | Type | Description |
|---|---|---|
clientToProxyHost |
string |
Original host/port the client intended to reach. |
proxyToUpstreamHost |
string |
Destination host the proxy will dial. |
clientToProxyUrl |
string |
Original URL requested by the client. |
proxyToUpstreamUrl |
string |
Final URL requested upstream. |
Scoped to a single HTTP transaction. Cleared when the request finishes.
| Property | Type | Description |
|---|---|---|
requestId |
string |
UUID for this specific HTTP transaction. |
req |
IncomingMessage |
Incoming client request. |
res |
ServerResponse |
Response stream back to the client. |
upstreamReq |
ClientRequest |
Outbound request to the target server. |
upstreamRes |
IncomingMessage |
Raw response from the target server. |
state |
StateStore |
Transaction-specific key-value store. |
nextPhase |
Phase |
Target phase for the next middleware execution. |
mitm-core uses a fully asynchronous event emitter architecture. Attach listeners to specific lifecycle phases using proxy.on().
Fires immediately when a raw TCP socket is accepted.
- Payload:
{ socket } - Typical Use: IP filtering, rate limiting, or socket-level configuration.
proxy.on("tcp:connection", ({ socket }) => {
if (isBlocked(socket.remoteAddress)) socket.destroy();
});Fires when an HTTP CONNECT request is received (the start of an HTTPS tunnel).
- Payload:
{ req, head, socket, payloadEvent } - Typical Use: Early domain filtering before TLS handshake, logging tunnel requests.
proxy.on("tunnel:connect", ({ req, socket }) => {
const targetHost = req.url;
if (targetHost.includes("malicious.com")) {
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
socket.destroy();
}
});Fires right before the proxy generates a dynamic certificate and performs the TLS handshake.
- Payload:
{ ctx, socket } - Typical Use: TLS bypassing (direct TCP tunneling) for pinned apps, or injecting custom certificates.
proxy.on("tunnel:pre_establish", async ({ ctx, socket }) => {
if (shouldBypass(ctx.clientToProxyHost)) {
// Implement direct TCP piping logic here
ctx.isHandled = true; // Halt pipeline
}
});Fires after the TLS handshake with the client completes successfully.
- Payload:
{ ctx, socket } - Typical Use: Tracking active encrypted connections, logging handshake success.
proxy.on("tunnel:established", async ({ ctx }) => {
console.log(`[TLS Ready] ${ctx.clientToProxyHost}`);
});Fires when unencrypted HTTP traffic is received.
- Payload:
{ req, res } - Typical Use: Plain HTTP inspection, header manipulation, or forcing HTTPS redirects.
proxy.on("http:plain_request", ({ req, res }) => {
res.writeHead(301, { Location: `https://${req.headers.host}${req.url}` });
res.end();
});Fires when an HTTPS request is successfully decrypted by the proxy, before it goes upstream.
- Payload:
{ ctx } - Typical Use: Request body inspection, header modification, or serving cached responses.
proxy.on("http:decrypted_request", ({ ctx }) => {
const req = ctx.requestContext.req;
req.headers["x-intercepted-by"] = "mitm-core";
});Fires when a response is received from the upstream server, before returning to the client.
- Payload:
{ ctx } - Typical Use: Response body rewriting, header manipulation, or writing to cache.
proxy.on("decrypted_response", ({ ctx }) => {
const res = ctx.requestContext.upstreamRes;
if (res.statusCode === 404) {
console.log(`[404 Not Found] ${ctx.proxyToUpstreamUrl}`);
}
});Fires when a global or pipeline-level error is caught.
- Payload:
err(Standard Error object) - Typical Use: Centralized error logging and metric collection.
proxy.on("error", (err) => {
console.error("[Proxy Error]:", err.message);
});| Script | Command | Description |
|---|---|---|
dev |
tsx watch --inspect ./example/with-plugin/Proxy.ts |
Start dev server with live reload and Node.js inspector |
build |
tsup |
Compile TypeScript to dist/ |
clean |
node -e "fs.rmSync('dist', ...)" |
Delete the dist/ directory |
test |
mocha --config mocha.config.json |
Run the full test suite |
typecheck |
tsc --noEmit |
Type-check without emitting files |
lint |
prettier --check src |
Check formatting across src/ |
format |
prettier --write src |
Auto-format all files in src/ |
prepublishOnlyautomatically runstest,lint, andtypecheckbefore anynpm publish.
- Core Pipeline & TCP Tunneling
- Dynamic Certificate Generation
- Async Plugin Support & Error Boundaries
- HTTP/2 Support
- WebSocket Interception
- npm package publish
This tool is intended for authorized security research, traffic analysis, and debugging only.
Using mitm-core to intercept traffic on networks or systems you do not own or have explicit written permission to test is illegal in most jurisdictions. The author and contributors accept no liability for misuse. Always obtain proper authorization before deploying a MITM proxy.
Contributions are welcome! See CONTRIBUTING.md to get started.
MIT © Debanshu Panigrahi
See LICENSE for full terms.