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,
};