Spaces:
Runtime error
Runtime error
File size: 6,076 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | /**
* Bypass / passthrough routing primitives used by `src/mitm/server.cjs`.
*
* This file exists because:
* - `server.cjs` runs as a standalone CommonJS process and cannot import
* the ESM/TS source of `src/mitm/passthrough.ts` or
* `src/mitm/targets/index.ts`.
* - We still want unit tests to cover the bypass/passthrough decision
* without spawning the actual TLS server (which would require certs and
* ROUTER_API_KEY).
*
* Defaults MUST mirror `DEFAULT_BYPASS_PATTERNS` in
* `src/mitm/passthrough.ts`. User bypass patterns are produced by
* `src/mitm/manager.ts::writeBypassJson` and loaded by `server.cjs` at
* boot from `<DATA_DIR>/mitm/bypass.json`.
*
* Plan reference:
* - 11-agent-bridge.plan.md Β§4.6 (passthrough/bypass)
* - master-plan-group-A.md Β§3.5 (header injection contract)
* - master-plan-group-A.md Β§12 #16 (passthrough acceptance criterion)
*/
"use strict";
// Default bypass patterns β banks, governments, SSO providers. Must stay in
// sync with src/mitm/passthrough.ts::DEFAULT_BYPASS_PATTERNS.
const DEFAULT_BYPASS_PATTERNS = [
/\.bank\./i,
/(^|\.)gov(\.|$)/i,
/(^|\.)okta\.com$/i,
/(^|\.)auth0\.com$/i,
];
/**
* Match a hostname against a simple glob pattern (only `*` wildcard).
* Linear, ReDoS-safe β mirrors `globMatch` in src/mitm/passthrough.ts.
*
* @param {string} hostname
* @param {string} pattern
* @returns {boolean}
*/
function bypassGlobMatch(hostname, pattern) {
const segments = String(pattern).toLowerCase().split("*");
if (segments.length > 9) return false;
const h = String(hostname).toLowerCase();
if (segments.length === 1) return h === segments[0];
const first = segments[0];
if (first && !h.startsWith(first)) return false;
const last = segments[segments.length - 1];
if (last && !h.endsWith(last)) return false;
let pos = first.length;
for (let i = 1; i < segments.length - 1; i++) {
const seg = segments[i];
if (seg === "") continue;
const idx = h.indexOf(seg, pos);
if (idx === -1) return false;
pos = idx + seg.length;
}
if (last) {
const minEnd = pos + last.length;
if (minEnd > h.length) return false;
}
return true;
}
/**
* Decide what to do with a connection (CONNECT or direct TLS) to `hostname`.
* Returns one of:
* - "bypass": tunnel without TLS decrypt; never log body/headers.
* - "target": hostname matches the AgentBridge target set β proceed with
* local TLS termination.
* - "passthrough": neither bypass nor target β transparent TCP tunnel.
*
* Precedence matches `src/mitm/targets/index.ts::routeConnection`:
* bypass > target > passthrough
*
* @param {string} hostname
* @param {Iterable<string>} targetHosts - set/array of known target hosts
* @param {string[]} userBypassPatterns - lowercased user glob strings
* @returns {"bypass" | "target" | "passthrough"}
*/
function routeBypass(hostname, targetHosts, userBypassPatterns) {
if (!hostname) return "passthrough";
const h = String(hostname).toLowerCase();
if (DEFAULT_BYPASS_PATTERNS.some((re) => re.test(h))) return "bypass";
const patterns = Array.isArray(userBypassPatterns) ? userBypassPatterns : [];
if (patterns.some((p) => bypassGlobMatch(h, p))) return "bypass";
// targetHosts may be a Set, an array, or any iterable with `.has` semantics.
if (targetHosts && typeof targetHosts.has === "function") {
if (targetHosts.has(h)) return "target";
} else if (Array.isArray(targetHosts)) {
if (targetHosts.includes(h)) return "target";
}
return "passthrough";
}
/**
* Parse a `bypass.json` file content blob into an array of lowercased user
* glob patterns. Pure function β no fs I/O β so this shim stays free of any
* path-traversal surface (CWE-22). The actual file read lives in
* `server.cjs`, which knows the trusted, pre-resolved file path.
*
* Returns [] when the input is missing or malformed β the proxy must keep
* working even when the user has never customized the bypass list.
*
* @param {string} raw - file contents (utf-8 JSON) or empty string
* @returns {string[]}
*/
function parseBypassJson(raw) {
if (typeof raw !== "string" || raw.length === 0) return [];
try {
const parsed = JSON.parse(raw);
if (!parsed || !Array.isArray(parsed.patterns)) return [];
return parsed.patterns
.filter((p) => typeof p === "string" && p.length > 0)
.map((p) => p.toLowerCase());
} catch {
return [];
}
}
/**
* True if `ip` is an IPv4 or IPv6 loopback address. (Gap 14 helper.)
* @param {string} ip
* @returns {boolean}
*/
function isLoopbackIp(ip) {
if (typeof ip !== "string") return false;
if (ip === "::1" || ip === "::ffff:127.0.0.1") return true;
return /^127\./.test(ip);
}
/**
* Defense-in-depth loop guard (Gap 14). The primary guard is the
* x-omniroute-source header; this is a structural backstop. If a forwarded
* request's resolved upstream is a loopback address on the MITM server's own
* listen port, dialing it would re-enter this same server β an infinite loop /
* fd storm. Callers should refuse instead of dialing themselves.
*
* @param {string} targetIp - resolved upstream IP
* @param {number} destPort - the port we would dial upstream
* @param {number} localPort - this MITM server's own listen port
* @returns {boolean}
*/
function isSelfLoopDestination(targetIp, destPort, localPort) {
return isLoopbackIp(targetIp) && Number(destPort) === Number(localPort);
}
/**
* Parse the MITM_VERBOSE env var into a routing-decision log level (Gap 15).
* Default 1 (log decisions) preserves existing behavior; 0 silences; higher
* levels are reserved for finer detail. Garbage falls back to the default.
*
* @param {string|undefined} envValue
* @returns {number}
*/
function parseVerboseLevel(envValue) {
const n = Number.parseInt(envValue, 10);
return Number.isInteger(n) && n >= 0 ? n : 1;
}
module.exports = {
DEFAULT_BYPASS_PATTERNS,
bypassGlobMatch,
routeBypass,
parseBypassJson,
isLoopbackIp,
isSelfLoopDestination,
parseVerboseLevel,
};
|