File size: 9,837 Bytes
22a7de1
 
 
 
 
 
 
 
 
 
 
 
 
 
e7285df
22a7de1
 
4a940a5
22a7de1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27e0738
22a7de1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4a940a5
22a7de1
 
 
 
 
 
 
 
 
 
 
 
d3a5a81
22a7de1
 
 
 
27e0738
38895cf
 
 
27e0738
38895cf
27e0738
38895cf
 
 
 
 
 
 
 
27e0738
 
38895cf
 
 
 
 
 
 
 
 
27e0738
38895cf
27e0738
 
22a7de1
 
27e0738
 
22a7de1
 
 
 
 
 
 
 
27e0738
38895cf
 
 
 
22a7de1
 
 
 
 
 
 
 
 
 
 
34fceda
22a7de1
 
 
 
 
 
 
 
34fceda
 
 
 
 
 
 
22a7de1
 
e7285df
 
 
 
 
 
 
 
 
 
 
 
d7d7389
 
 
 
 
 
 
 
e7285df
 
d7d7389
 
e7285df
d7d7389
e7285df
 
 
 
 
 
 
 
 
 
d7d7389
e7285df
 
 
d7d7389
 
 
 
 
 
 
e7285df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d85b21d
 
e7285df
d85b21d
 
e7285df
d85b21d
 
 
347f81b
 
 
 
 
 
 
 
 
27e0738
 
 
 
 
 
 
 
 
3d01305
 
 
 
 
 
 
 
85a4cbd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22a7de1
 
 
 
 
 
 
27e0738
22a7de1
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
/**
 * Resolves the curl binary and Chrome TLS profile args.
 *
 * When curl-impersonate is available, we call it directly (NOT via the
 * curl_chrome136 wrapper script) and pass the TLS-level parameters ourselves.
 * This avoids duplicate -H headers between the wrapper and our fingerprint manager.
 *
 * The Chrome TLS args are extracted from curl_chrome136 wrapper script.
 * HTTP headers (-H flags) are intentionally excluded β€” our fingerprint manager
 * in manager.ts handles those to match Codex Desktop exactly.
 */

import { existsSync } from "fs";
import { execFileSync } from "child_process";
import { createConnection } from "net";
import { resolve } from "path";
import { getConfig } from "../config.js";
import { getBinDir } from "../paths.js";

const IS_WIN = process.platform === "win32";
const BINARY_NAME = IS_WIN ? "curl-impersonate.exe" : "curl-impersonate";

/**
 * Chrome 136 TLS profile parameters.
 * Extracted from curl_chrome136 wrapper (lexiforest/curl-impersonate v1.4.4).
 * These control TLS fingerprint, HTTP/2 framing, and protocol negotiation.
 * HTTP-level headers are NOT included β€” our fingerprint manager handles those.
 */
const CHROME_TLS_ARGS: string[] = [
  // ── TLS cipher suites (exact Chrome 136 order) ──
  "--ciphers",
  [
    "TLS_AES_128_GCM_SHA256",
    "TLS_AES_256_GCM_SHA384",
    "TLS_CHACHA20_POLY1305_SHA256",
    "ECDHE-ECDSA-AES128-GCM-SHA256",
    "ECDHE-RSA-AES128-GCM-SHA256",
    "ECDHE-ECDSA-AES256-GCM-SHA384",
    "ECDHE-RSA-AES256-GCM-SHA384",
    "ECDHE-ECDSA-CHACHA20-POLY1305",
    "ECDHE-RSA-CHACHA20-POLY1305",
    "ECDHE-RSA-AES128-SHA",
    "ECDHE-RSA-AES256-SHA",
    "AES128-GCM-SHA256",
    "AES256-GCM-SHA384",
    "AES128-SHA",
    "AES256-SHA",
  ].join(":"),
  // ── Elliptic curves (includes post-quantum X25519MLKEM768) ──
  "--curves", "X25519MLKEM768:X25519:P-256:P-384",
  // ── HTTP/2 with Chrome-exact SETTINGS frame ──
  "--http2",
  "--http2-settings", "1:65536;2:0;4:6291456;6:262144",
  "--http2-window-update", "15663105",
  "--http2-stream-weight", "256",
  "--http2-stream-exclusive", "1",
  // ── TLS extensions (Chrome fingerprint) ──
  "--tlsv1.2",
  "--alps",
  "--tls-permute-extensions",
  "--cert-compression", "brotli",
  "--tls-grease",
  "--tls-use-new-alps-codepoint",
  "--tls-signed-cert-timestamps",
  "--ech", "grease",
  // ── Compression & cookies ──
  "--compressed",
];

let _resolved: string | null = null;
let _isImpersonate = false;
let _tlsArgs: string[] | null = null;
let _resolvedProfile = "chrome136";

/**
 * Resolve the curl binary path. Result is cached after first call.
 */
export function resolveCurlBinary(): string {
  if (_resolved) return _resolved;

  const config = getConfig();
  const setting = config.tls.curl_binary;

  if (setting !== "auto") {
    _resolved = setting;
    _isImpersonate = setting.includes("curl-impersonate");
    console.log(`[TLS] Using configured curl binary: ${_resolved}`);
    return _resolved;
  }

  // Auto-detect: look for curl-impersonate in bin/
  const binPath = resolve(getBinDir(), BINARY_NAME);
  if (existsSync(binPath)) {
    _resolved = binPath;
    _isImpersonate = true;
    console.log(`[TLS] Using curl-impersonate: ${_resolved}`);
    return _resolved;
  }

  // Fallback to system curl
  _resolved = "curl";
  _isImpersonate = false;
  console.warn(
    `[TLS] curl-impersonate not found at ${binPath}. ` +
    `Falling back to system curl. Run "npm/pnpm/bun run setup" to install curl-impersonate.`,
  );
  return _resolved;
}

/**
 * Chrome versions with distinct TLS fingerprints in curl-impersonate.
 * Only these versions are valid --impersonate targets.
 * Sorted ascending β€” update when curl-impersonate adds new profiles.
 */
const KNOWN_CHROME_PROFILES = [99, 100, 101, 104, 107, 110, 116, 119, 120, 123, 124, 131, 136];

/**
 * Map a configured profile to the nearest known-supported version.
 * e.g. "chrome137" β†’ "chrome136", "chrome125" β†’ "chrome124"
 * Non-chrome profiles (e.g. "firefox") are passed through unchanged.
 */
function resolveProfile(configured: string): string {
  const match = configured.match(/^chrome(\d+)$/);
  if (!match) return configured;

  const ver = parseInt(match[1], 10);
  let best: number | undefined;
  for (const known of KNOWN_CHROME_PROFILES) {
    if (known <= ver) best = known;
  }
  if (!best) return configured;

  const resolved = `chrome${best}`;
  if (resolved !== configured) {
    console.warn(`[TLS] Profile "${configured}" not in known targets, using "${resolved}"`);
  }
  return resolved;
}

/**
 * Detect if curl-impersonate supports the --impersonate flag.
 * Validates the configured profile and auto-falls back to the nearest
 * supported Chrome version if needed.
 */
function detectImpersonateSupport(binary: string): string[] {
  try {
    const helpOutput = execFileSync(binary, ["--help", "all"], {
      encoding: "utf-8",
      timeout: 5000,
    });
    if (helpOutput.includes("--impersonate")) {
      const configured = getConfig().tls.impersonate_profile ?? "chrome136";
      const profile = resolveProfile(configured);
      _resolvedProfile = profile;
      console.log(`[TLS] Using --impersonate ${profile}`);
      return ["--impersonate", profile];
    }
  } catch {
    // --help failed, fall back to manual args
  }
  return CHROME_TLS_ARGS;
}

/**
 * Get Chrome TLS profile args to prepend to curl commands.
 * Returns empty array when using system curl (args are curl-impersonate specific).
 * Uses --impersonate flag when available, otherwise falls back to manual CHROME_TLS_ARGS.
 * When force_http11 is enabled, adds --http1.1 to force HTTP/1.1 protocol.
 */
export function getChromeTlsArgs(): string[] {
  // Ensure binary is resolved first
  resolveCurlBinary();
  if (!_isImpersonate) return [];
  if (!_tlsArgs) {
    _tlsArgs = detectImpersonateSupport(_resolved!);
  }
  const args = [..._tlsArgs];
  // Force HTTP/1.1 when configured (for proxies that don't support HTTP/2)
  const config = getConfig();
  if (config.tls.force_http11) {
    args.push("--http1.1");
  }
  return args;
}

/**
 * Common local proxy ports to auto-detect.
 * Checked in order: mihomo/clash, v2ray, SOCKS5 common.
 */
const PROXY_PORTS = [
  { port: 7890, proto: "http" },   // mihomo / clash
  { port: 7897, proto: "http" },   // clash-verge
  { port: 10809, proto: "http" },  // v2ray HTTP
  { port: 1080, proto: "socks5" }, // SOCKS5 common
  { port: 10808, proto: "socks5" },// v2ray SOCKS5
];

/**
 * Hosts to probe for proxy detection.
 * 127.0.0.1 β€” bare-metal / host machine.
 * host.docker.internal β€” Docker container β†’ host machine
 * (DNS lookup fails on bare-metal β†’ ENOTFOUND β†’ handled by error callback, <5ms).
 */
const PROXY_HOSTS = ["127.0.0.1", "host.docker.internal"];

let _proxyUrl: string | null | undefined; // undefined = not yet detected

/** Probe a TCP port on the given host. Resolves true if a server is listening. */
function probePort(host: string, port: number, timeoutMs = 500): Promise<boolean> {
  return new Promise((resolve) => {
    const sock = createConnection({ host, port }, () => {
      sock.destroy();
      resolve(true);
    });
    sock.setTimeout(timeoutMs);
    sock.on("timeout", () => { sock.destroy(); resolve(false); });
    sock.on("error", () => { resolve(false); });
  });
}

/**
 * Detect a local proxy by probing common ports on localhost and Docker host.
 * Called once at startup, result is cached.
 */
async function detectLocalProxy(): Promise<string | null> {
  for (const host of PROXY_HOSTS) {
    for (const { port, proto } of PROXY_PORTS) {
      if (await probePort(host, port)) {
        const url = `${proto}://${host}:${port}`;
        console.log(`[Proxy] Auto-detected local proxy: ${url}`);
        return url;
      }
    }
  }
  return null;
}

/**
 * Initialize proxy detection. Called once at startup from index.ts.
 * Priority: config proxy_url > HTTPS_PROXY env > auto-detect local ports.
 */
export async function initProxy(): Promise<void> {
  const config = getConfig();
  if (config.tls.proxy_url) {
    _proxyUrl = config.tls.proxy_url;
    console.log(`[Proxy] Using configured proxy: ${_proxyUrl}`);
    return;
  }
  _proxyUrl = await detectLocalProxy();
  if (!_proxyUrl) {
    console.log("[Proxy] No local proxy detected β€” direct connection");
  }
}

/**
 * Get proxy args to prepend to curl commands.
 * Uses cached result from initProxy().
 */
export function getProxyArgs(): string[] {
  if (_proxyUrl) return ["-x", _proxyUrl];
  return [];
}

/**
 * Check if the resolved curl binary is curl-impersonate.
 * When true, it supports br/zstd decompression natively.
 */
export function isImpersonate(): boolean {
  resolveCurlBinary(); // ensure resolved
  return _isImpersonate;
}

/**
 * Get the resolved impersonate profile (e.g. "chrome136").
 * Used by FFI transport which needs the profile name directly.
 */
export function getResolvedProfile(): string {
  getChromeTlsArgs(); // ensure detection has run
  return _resolvedProfile;
}

/**
 * Get the detected proxy URL (or null if no proxy).
 * Used by LibcurlFfiTransport which needs the URL directly (not CLI args).
 */
export function getProxyUrl(): string | null {
  return _proxyUrl ?? null;
}

/**
 * Get curl diagnostic info for /debug/diagnostics endpoint.
 */
export function getCurlDiagnostics(): {
  binary: string | null;
  is_impersonate: boolean;
  profile: string;
  proxy_url: string | null;
} {
  return {
    binary: _resolved,
    is_impersonate: _isImpersonate,
    profile: _resolvedProfile,
    proxy_url: _proxyUrl ?? null,
  };
}

/**
 * Reset the cached binary path (useful for testing).
 */
export function resetCurlBinaryCache(): void {
  _resolved = null;
  _isImpersonate = false;
  _tlsArgs = null;
  _resolvedProfile = "chrome136";
}