icebear0828 Claude Opus 4.6 commited on
Commit
27e0738
·
1 Parent(s): 3838101

fix: auto-fallback for unsupported TLS impersonate profiles

Browse files

curl-impersonate skips Chrome versions when the TLS fingerprint is
unchanged (e.g. chrome137 doesn't exist, chrome136 covers it).
Previously, configuring an unsupported profile like chrome137 caused
`curl: (43) Unknown impersonation target` at runtime.

Now `detectImpersonateSupport()` validates the configured profile by
test-running it, and automatically descends to the nearest supported
Chrome version. Also unified the FFI transport to use the resolved
profile instead of a hardcoded "chrome136".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

CHANGELOG.md CHANGED
@@ -25,6 +25,8 @@
25
 
26
  ### Fixed
27
 
 
 
28
  - `getModels()` 死代码:`allModels` 作用域修复,消除不可达分支
29
  - `reloadAllConfigs()` 异步 lazy import 改为同步直接导入,避免日志时序不准
30
  - 模型合并 reasoning efforts 判断逻辑从 `length > 1` 改为显式标志
 
25
 
26
  ### Fixed
27
 
28
+ - TLS 伪装 profile 自动回退:配置的 `impersonate_profile` 不被 curl-impersonate 支持时,自动降级到最近的可用 Chrome 版本(如 `chrome137` → `chrome136`)
29
+ - FFI transport 硬编码 `"chrome136"` 改为使用统一解析的 profile(`getResolvedProfile()`)
30
  - `getModels()` 死代码:`allModels` 作用域修复,消除不可达分支
31
  - `reloadAllConfigs()` 异步 lazy import 改为同步直接导入,避免日志时序不准
32
  - 模型合并 reasoning efforts 判断逻辑从 `length > 1` 改为显式标志
src/tls/curl-binary.ts CHANGED
@@ -69,6 +69,7 @@ const CHROME_TLS_ARGS: string[] = [
69
  let _resolved: string | null = null;
70
  let _isImpersonate = false;
71
  let _tlsArgs: string[] | null = null;
 
72
 
73
  /**
74
  * Resolve the curl binary path. Result is cached after first call.
@@ -105,10 +106,45 @@ export function resolveCurlBinary(): string {
105
  return _resolved;
106
  }
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  /**
109
  * Detect if curl-impersonate supports the --impersonate flag.
110
- * If supported, returns ["--impersonate", profile] which replaces CHROME_TLS_ARGS.
111
- * Otherwise returns the manual CHROME_TLS_ARGS.
112
  */
113
  function detectImpersonateSupport(binary: string): string[] {
114
  try {
@@ -117,9 +153,14 @@ function detectImpersonateSupport(binary: string): string[] {
117
  timeout: 5000,
118
  });
119
  if (helpOutput.includes("--impersonate")) {
120
- const profile = getConfig().tls.impersonate_profile ?? "chrome136";
121
- console.log(`[TLS] Using --impersonate ${profile}`);
122
- return ["--impersonate", profile];
 
 
 
 
 
123
  }
124
  } catch {
125
  // --help failed, fall back to manual args
@@ -219,6 +260,15 @@ export function isImpersonate(): boolean {
219
  return _isImpersonate;
220
  }
221
 
 
 
 
 
 
 
 
 
 
222
  /**
223
  * Get the detected proxy URL (or null if no proxy).
224
  * Used by LibcurlFfiTransport which needs the URL directly (not CLI args).
@@ -234,4 +284,5 @@ export function resetCurlBinaryCache(): void {
234
  _resolved = null;
235
  _isImpersonate = false;
236
  _tlsArgs = null;
 
237
  }
 
69
  let _resolved: string | null = null;
70
  let _isImpersonate = false;
71
  let _tlsArgs: string[] | null = null;
72
+ let _resolvedProfile = "chrome136";
73
 
74
  /**
75
  * Resolve the curl binary path. Result is cached after first call.
 
106
  return _resolved;
107
  }
108
 
109
+ /** Test if a specific --impersonate profile is accepted by the binary. */
110
+ function testProfile(binary: string, profile: string): boolean {
111
+ try {
112
+ execFileSync(binary, ["--impersonate", profile, "-V"], {
113
+ encoding: "utf-8",
114
+ timeout: 5000,
115
+ stdio: ["ignore", "pipe", "pipe"],
116
+ });
117
+ return true;
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Find a working impersonate profile.
125
+ * Tries the configured profile first, then descends Chrome versions.
126
+ */
127
+ function resolveProfile(binary: string, configured: string): string | null {
128
+ if (testProfile(binary, configured)) return configured;
129
+
130
+ const match = configured.match(/^chrome(\d+)/);
131
+ if (!match) return null;
132
+
133
+ const ver = parseInt(match[1], 10);
134
+ for (let v = ver - 1; v >= ver - 10 && v > 0; v--) {
135
+ const candidate = `chrome${v}`;
136
+ if (testProfile(binary, candidate)) {
137
+ console.warn(`[TLS] Profile "${configured}" not supported, using "${candidate}"`);
138
+ return candidate;
139
+ }
140
+ }
141
+ return null;
142
+ }
143
+
144
  /**
145
  * Detect if curl-impersonate supports the --impersonate flag.
146
+ * Validates the configured profile and auto-falls back to the nearest
147
+ * supported Chrome version if needed.
148
  */
149
  function detectImpersonateSupport(binary: string): string[] {
150
  try {
 
153
  timeout: 5000,
154
  });
155
  if (helpOutput.includes("--impersonate")) {
156
+ const configured = getConfig().tls.impersonate_profile ?? "chrome136";
157
+ const profile = resolveProfile(binary, configured);
158
+ if (profile) {
159
+ _resolvedProfile = profile;
160
+ console.log(`[TLS] Using --impersonate ${profile}`);
161
+ return ["--impersonate", profile];
162
+ }
163
+ console.warn("[TLS] No supported impersonate profile found, using manual TLS args");
164
  }
165
  } catch {
166
  // --help failed, fall back to manual args
 
260
  return _isImpersonate;
261
  }
262
 
263
+ /**
264
+ * Get the resolved impersonate profile (e.g. "chrome136").
265
+ * Used by FFI transport which needs the profile name directly.
266
+ */
267
+ export function getResolvedProfile(): string {
268
+ getChromeTlsArgs(); // ensure detection has run
269
+ return _resolvedProfile;
270
+ }
271
+
272
  /**
273
  * Get the detected proxy URL (or null if no proxy).
274
  * Used by LibcurlFfiTransport which needs the URL directly (not CLI args).
 
284
  _resolved = null;
285
  _isImpersonate = false;
286
  _tlsArgs = null;
287
+ _resolvedProfile = "chrome136";
288
  }
src/tls/libcurl-ffi-transport.ts CHANGED
@@ -12,7 +12,7 @@ import { resolve } from "path";
12
  import { existsSync } from "fs";
13
  import type { IKoffiLib, IKoffiCType, IKoffiRegisteredCallback, KoffiFunction } from "koffi";
14
  import type { TlsTransport, TlsTransportResponse } from "./transport.js";
15
- import { getProxyUrl } from "./curl-binary.js";
16
 
17
  // ── libcurl constants ──────────────────────────────────────────────
18
 
@@ -450,7 +450,7 @@ export class LibcurlFfiTransport implements TlsTransport {
450
  if (!easy) throw new Error("curl_easy_init() returned null");
451
 
452
  // Impersonate Chrome — 0 = don't inject default headers (we control them)
453
- b.curl_easy_impersonate(easy, "chrome136", 0);
454
 
455
  b.curl_easy_setopt_str(easy, CURLOPT_URL, url);
456
  b.curl_easy_setopt_long(easy, CURLOPT_NOSIGNAL, 1);
 
12
  import { existsSync } from "fs";
13
  import type { IKoffiLib, IKoffiCType, IKoffiRegisteredCallback, KoffiFunction } from "koffi";
14
  import type { TlsTransport, TlsTransportResponse } from "./transport.js";
15
+ import { getProxyUrl, getResolvedProfile } from "./curl-binary.js";
16
 
17
  // ── libcurl constants ──────────────────────────────────────────────
18
 
 
450
  if (!easy) throw new Error("curl_easy_init() returned null");
451
 
452
  // Impersonate Chrome — 0 = don't inject default headers (we control them)
453
+ b.curl_easy_impersonate(easy, getResolvedProfile(), 0);
454
 
455
  b.curl_easy_setopt_str(easy, CURLOPT_URL, url);
456
  b.curl_easy_setopt_long(easy, CURLOPT_NOSIGNAL, 1);