Skip to content

feat: add channel/global http client TLS fingerprint support with uTLS - #3056

Closed
seefs001 wants to merge 5287 commits into
QuantumNous:mainfrom
seefs001:feature/utls-http-client
Closed

feat: add channel/global http client TLS fingerprint support with uTLS#3056
seefs001 wants to merge 5287 commits into
QuantumNous:mainfrom
seefs001:feature/utls-http-client

Conversation

@seefs001

@seefs001 seefs001 commented Feb 28, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Global and per-channel TLS fingerprint settings (Default, Chrome, Firefox, Safari, Edge, Custom)
    • Custom TLS ClientHello support for advanced TLS configuration
  • Improvements

    • Enhanced proxy support (HTTPS, HTTP CONNECT, SOCKS) and per-channel proxy/TLS application
    • UI: proxy TLS settings added to System Settings and Channel edit modal for easy configuration

feitianbubu and others added 30 commits January 27, 2026 17:33
feat(gemini): map OpenAI stop to Gemini stopSequences
fix: remove disable_parallel_tool_use if tool_choice=none
fix: /v1/responses/compact default billing
* feat: 引入通用 HTTP BodyStorage/DiskCache 缓存配置与管理

- 新增 common/body_storage.go 提供 HTTP 请求体存储抽象和文件缓存能力
- 增加 common/disk_cache_config.go 支持全局磁盘缓存配置
- main.go 挂载缓存初始化流程
- 新增和补充 controller/performance.go (及 unix/windows) 用于缓存性能监控接口
- middleware/body_cleanup.go 自动清理缓存文件
- router 挂载相关接口
- 前端 settings 页面新增性能监控设置 PerformanceSetting
- 优化缓存开关状态和模块热插拔能力
- 其他相关文件同步适配缓存扩展

* fix: 修复 BodyStorage 并发安全和错误处理问题

- 修复 diskStorage.Close() 竞态条件,先获取锁再执行 CAS
- 为 memoryStorage 添加互斥锁和 closed 状态检查
- 修复 CreateBodyStorageFromReader 在磁盘存储失败时的回退逻辑
- 添加缓存命中统计调用 (IncrementDiskCacheHits/IncrementMemoryCacheHits)
- 修复 gin.go 中 Seek 错误被忽略的问题
- 在 api-router 添加 BodyStorageCleanup 中间件
- 修复前端 formatBytes 对异常值的处理

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
…2793)

Explicitly cast Blocks, Bavail, and Bfree to uint64 for cross-platform compatibility,
as these fields are int64 on FreeBSD but uint64 on Linux.
feat: Support customizing the success and cancel url of Stripe.
…8684dd7ef068e576

feat: doubao add first and last image to video
…96dc78bae181e331

feat: task pre consume modelPrice default use setting value
…98ade9b372bd6f63

feat: CodeViewer click link and auto wrap
feat(gemini): support cached token billing
fix(ui): use distinct color palette for group tags
* fix: channel affinity log styles

* fix: Issue with incorrect data storage when switching key sources

* feat: support not retrying after a single rule configuration fails

* fix: render channel affinity tooltip as multiline content

* feat: channel affinity cache hit

* fix: prevent ChannelAffinityUsageCacheModal infinite loading and hide data before fetch

* chore: format backend with gofmt and frontend with prettier/eslint autofix
…-take-effect

fix: make channel Host override take effect
feat: /v1/responses qwen3 max && perplexity
seefs001 and others added 22 commits February 23, 2026 22:02
…assthrough

fix: skip Accept-Encoding during header passthrough (#2214)
feat: move user bindings to dedicated management modal
- Introduced a new test file for StreamScannerHandler, covering various scenarios including nil inputs, empty bodies, chunk processing, order preservation, and handler failures.
- Enhanced error handling and data processing logic in StreamScannerHandler to improve robustness and performance.
feat(web): add custom-model create hint and i18n translations
…uting

- Introduced RouteTag middleware to set route tags for different API endpoints.
- Updated logger to include route tags in log output.
- Applied RouteTag middleware across various routers including API, dashboard, relay, video, and web routers for consistent logging.
fix: align Vertex content fetch flow with Gemini and handle base64
fix: vertex ai video proxy and task polling improvements
fix: show built-in user bindings from user detail API in admin modal
@coderabbitai

coderabbitai Bot commented Feb 28, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Refactors code to replace proxy-string parameters with a typed dto.ChannelSettings across controllers, relay adaptors, services, and tests; adds TLS fingerprinting and custom ClientHello support; and implements a new, feature-rich HTTP client builder with UTLS support, proxy handling, and client caching.

Changes

Cohort / File(s) Summary
DTO / Settings / Model
dto/channel_settings.go, model/channel.go, setting/system_setting/proxy_setting.go
Added TLS fingerprint constants, normalization and validation; extended ChannelSettings with TLSFingerprint and TLSCustom; added system proxy setting type and validation in channel settings.
Core HTTP client service
service/http_client.go, service/http_client_test.go
Large new HTTP client subsystem: options struct, UTLS ClientHello mapping and custom spec parsing, SOCKS5/HTTP proxy and CONNECT support, client caching, helpers, and GetHttpClientWithChannelSetting; added tests.
Service: Codex & OAuth
service/codex_oauth.go, service/codex_credential_refresh.go
Replaced proxy-string variants with ChannelSettings-aware functions for token exchange/refresh and adjusted internal HTTP client creation to use channel settings.
Controller layer
controller/channel-billing.go, controller/channel.go, controller/codex_oauth.go, controller/codex_usage.go, controller/option.go, controller/video_proxy.go, controller/video_proxy_gemini.go
Call sites updated to use ChannelSettings instead of proxy strings; added TLS fingerprint validation in options update handler.
Relay core & adaptors
relay/channel/adapter.go, relay/channel/api_request.go, relay/channel/aws/relay-aws.go, relay/channel/coze/relay-coze.go, relay/channel/gemini/relay-gemini.go, relay/channel/vertex/service_account.go, relay/mjproxy_handler.go, relay/relay_task.go
Task/adapter interfaces and relay handlers switched to accept ChannelSettings; introduced channel-setting-aware Gemini fetch and Vertex service account/token exchange functions; consolidated HTTP client creation.
Relay task adaptors (many)
relay/channel/task/*/adaptor.go (ali, doubao, gemini, hailuo, jimeng, kling, sora, suno, vertex, vidu)
Updated FetchTask signatures to accept dto.ChannelSettings, switched internal client creation and token acquisition to channel-setting-aware helpers.
Service tests & polling
service/task_billing_test.go, service/task_polling.go
Updated mocks and interfaces to use dto.ChannelSettings in FetchTask; adjusted call sites to pass full channel settings.
Frontend
web/src/components/settings/SystemSetting.jsx, web/src/components/table/channels/modals/EditChannelModal.jsx
Added UI for system default TLS fingerprint and custom spec; added per-channel TLS fingerprint and custom JSON field with conditional rendering and payload handling.
Dependency / go.mod
go.mod
Added indirect dependency for UTLS support (utls v1.8.2).

Sequence Diagram(s)

sequenceDiagram
    participant Controller
    participant ChannelSettings
    participant Service
    participant HTTPClient

    rect rgba(100, 200, 0, 0.5)
    Note over Controller,Service: Request flow using ChannelSettings
    Controller->>ChannelSettings: settings := channel.GetSetting()
    ChannelSettings-->>Controller: {Proxy, TLSFingerprint, TLSCustom}
    Controller->>Service: GetHttpClientWithChannelSetting(settings)
    Service->>Service: normalize options, resolve ClientHello ID
    Service->>HTTPClient: buildUTLSClient or buildClassicClient (may use proxy)
    HTTPClient-->>Service: *http.Client (cached)
    Service-->>Controller: *http.Client
    Controller->>HTTPClient: Do(request)
    HTTPClient-->>Controller: Response
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

"I hop with joy through channels wide,
Settings tucked safely by my side,
Fingerprints of TLS I know,
Clients cached, and proxies go —
A rabbit cheers for unified guide!" 🐇✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main feature being added: TLS fingerprint support for HTTP clients at both channel and global levels, using the uTLS library. The title aligns well with the comprehensive changes throughout the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
relay/channel/vertex/service_account.go (3)

125-128: ⚠️ Potential issue | 🟡 Minor

Use common.DecodeJson instead of json.NewDecoder().Decode().

Per coding guidelines, all JSON operations must use wrapper functions from common/json.go. Direct use of encoding/json is not permitted in business code.

🔧 Proposed fix
-	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+	if err := common.DecodeJson(resp.Body, &result); err != nil {
 		return "", err
 	}

As per coding guidelines: "Do NOT directly import or call encoding/json in business code; use wrapper functions from common/json.go instead."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/vertex/service_account.go` around lines 125 - 128, Replace the
direct use of encoding/json decoder when parsing resp.Body into result with the
project wrapper: call common.DecodeJson(resp.Body, &result) (or the exact
wrapper function in common/json.go) inside the same block where result and resp
are used in service_account.go; remove the direct usage/import of encoding/json
from that file and ensure the common package is imported, returning the same
error from common.DecodeJson if it fails.

162-165: ⚠️ Potential issue | 🟡 Minor

Same issue: Use common.DecodeJson instead of direct json.NewDecoder().Decode().

This is the second occurrence in this file where encoding/json is used directly instead of the wrapper function.

🔧 Proposed fix
-	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+	if err := common.DecodeJson(resp.Body, &result); err != nil {
 		return "", err
 	}

Also remove the encoding/json import from line 6 once both usages are replaced.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/vertex/service_account.go` around lines 162 - 165, Replace the
direct use of json.NewDecoder(resp.Body).Decode(&result) with the wrapper
common.DecodeJson to mirror the other occurrences in service_account.go (i.e.,
replace the call that decodes into the local variable result/from resp.Body), so
call common.DecodeJson(resp.Body, &result) and handle the returned error the
same way; after updating both locations in this file remove the unused
encoding/json import.

60-63: ⚠️ Potential issue | 🟡 Minor

Remove the redundant conditional; SetDefault returns a bool indicating whether the key already existed.

The asynccache.SetDefault() method returns a bool (exist), not an error. The variable naming as err is misleading. More importantly, both branches of the conditional return newToken, nil, making the check pointless. Simplify by removing the conditional entirely.

Suggested fix
-	if err := Cache.SetDefault(cacheKey, newToken); err {
-		return newToken, nil
-	}
+	_ = Cache.SetDefault(cacheKey, newToken)
 	return newToken, nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/vertex/service_account.go` around lines 60 - 63, The code is
incorrectly treating Cache.SetDefault as returning an error and uses a redundant
if that returns newToken, nil in both branches; replace the conditional with a
direct call to Cache.SetDefault(cacheKey, newToken) (either ignore the boolean
return or capture it as exist) and then unconditionally return newToken, nil so
the SetDefault result isn't misinterpreted (references: Cache.SetDefault,
cacheKey, newToken).
service/codex_oauth.go (1)

319-322: ⚠️ Potential issue | 🟡 Minor

Use common.Unmarshal instead of json.Unmarshal.

Per coding guidelines, all JSON operations must use wrapper functions from common/json.go.

🔧 Proposed fix
 	var claims map[string]any
-	if err := json.Unmarshal(payloadRaw, &claims); err != nil {
+	if err := common.Unmarshal(payloadRaw, &claims); err != nil {
 		return nil, false
 	}

Also remove the encoding/json import from line 8 once this is fixed.

As per coding guidelines: "Use common.Marshal(), common.Unmarshal(), common.UnmarshalJsonStr(), common.DecodeJson(), or common.GetJsonType() from common/json.go for all JSON marshal/unmarshal operations in business code."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/codex_oauth.go` around lines 319 - 322, Replace the direct use of
json.Unmarshal for decoding the JWT payload into the local variable claims with
the project's wrapper common.Unmarshal: call common.Unmarshal(payloadRaw,
&claims) and handle its returned error the same way; update the code path that
currently checks "if err := json.Unmarshal(payloadRaw, &claims); err != nil {
return nil, false }" to use common.Unmarshal and remove the now-unused
encoding/json import; keep the behavior and return values identical when the
unmarshal fails.
🧹 Nitpick comments (4)
web/src/components/settings/SystemSetting.jsx (1)

317-332: Submit TLS custom/default updates sequentially to avoid partial state.

These two options are coupled. Sending them through updateOptions can run both writes concurrently, which is brittle for dependent settings.

♻️ Suggested patch
   const submitDefaultTLSFingerprint = async () => {
-    const options = [
-      {
-        key: 'proxy_setting.default_tls_fingerprint',
-        value: inputs['proxy_setting.default_tls_fingerprint'] || '',
-      },
-      {
-        key: 'proxy_setting.default_tls_custom',
-        value:
-          inputs['proxy_setting.default_tls_fingerprint'] === 'custom'
-            ? inputs['proxy_setting.default_tls_custom'] || ''
-            : '',
-      },
-    ];
-    await updateOptions(options);
+    const fingerprint = inputs['proxy_setting.default_tls_fingerprint'] || '';
+    const custom =
+      fingerprint === 'custom'
+        ? inputs['proxy_setting.default_tls_custom'] || ''
+        : '';
+
+    // 1) Persist custom spec first
+    await updateOptions([
+      { key: 'proxy_setting.default_tls_custom', value: custom },
+    ]);
+    // 2) Then switch fingerprint mode
+    await updateOptions([
+      { key: 'proxy_setting.default_tls_fingerprint', value: fingerprint },
+    ]);
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/settings/SystemSetting.jsx` around lines 317 - 332, The
current submitDefaultTLSFingerprint bundles two coupled options into one update
that may be applied concurrently; change submitDefaultTLSFingerprint so it
performs the writes sequentially: first await updateOptions with the single
option { key: 'proxy_setting.default_tls_fingerprint', value: ... } to persist
the chosen fingerprint, then await updateOptions for { key:
'proxy_setting.default_tls_custom', value: ... } (set to the custom value only
if the first value === 'custom', otherwise send an empty string) so the
dependent custom value is only written after the fingerprint is saved.
relay/channel/task/gemini/adaptor.go (1)

222-223: Use transport-agnostic error wording.

At Line 222, the message still says "proxy" even though this path now covers full channel settings.

♻️ Suggested tweak
-		return nil, fmt.Errorf("new proxy http client failed: %w", err)
+		return nil, fmt.Errorf("create http client failed: %w", err)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/task/gemini/adaptor.go` around lines 222 - 223, The error
message "new proxy http client failed: %w" is transport-specific; update the
return statement that wraps fmt.Errorf(err) (the line returning nil,
fmt.Errorf("new proxy http client failed: %w", err)) to use transport-agnostic
wording (e.g., "new channel http client failed" or "failed to create http
client") so it no longer references "proxy" while still including the wrapped
error; locate the return in adaptor.go where fmt.Errorf is used with that exact
string and replace only the message text accordingly.
service/http_client.go (1)

102-108: Potential infinite recursion if global default fingerprint is set incorrectly.

The interaction between getHTTPClientByOptions and GetHttpClient is subtle:

  • getHTTPClientByOptions returns early and calls GetHttpClient() when ProxyURL == "" && TLSFingerprint == ""
  • GetHttpClient normalizes options and may call getHTTPClientByOptions if a global default fingerprint is configured

This works correctly because the normalized options will have a non-empty fingerprint, avoiding the recursion. However, if normalizeHTTPClientOptions ever fails or returns empty fingerprint despite global defaults, this could cause infinite recursion. Consider adding a safeguard or documenting this relationship.

💡 Optional safeguard
 func getHTTPClientByOptions(options httpClientOptions) (*http.Client, error) {
 	if options.ProxyURL == "" && options.TLSFingerprint == "" {
-		if client := GetHttpClient(); client != nil {
-			return client, nil
+		// Direct fallback to avoid potential recursion via GetHttpClient
+		if httpClient != nil {
+			return httpClient, nil
 		}
 		return http.DefaultClient, nil
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/http_client.go` around lines 102 - 108, getHTTPClientByOptions can
recurse with GetHttpClient if normalizeHTTPClientOptions ever yields empty
fingerprint; add a defensive fallback: when ProxyURL=="" && TLSFingerprint==""
call GetHttpClient(), and if it returns nil or if you detect it may have called
back (use a simple package-level recursion sentinel like inGetHTTPClient) then
return http.DefaultClient instead of recursing; also ensure GetHttpClient
continues to call normalizeHTTPClientOptions as before but respects the sentinel
to avoid calling getHTTPClientByOptions again.
service/http_client_test.go (1)

12-26: Consider adding t.Parallel() guard or documenting non-parallel nature.

Tests that mutate global state (*proxySetting = ...) may cause race conditions if Go's test runner parallelizes tests. The current cleanup approach is correct for sequential execution. If these tests must remain sequential, consider adding a comment explaining why t.Parallel() should not be used, or alternatively, consider using a mock/stub pattern that doesn't modify global state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/http_client_test.go` around lines 12 - 26, The test
TestNormalizeHTTPClientOptions_UseGlobalDefaultFingerprint mutates global
proxySetting (variable proxySetting) which can race with other tests; either
make the test safe for parallel execution by adding t.Parallel() at the top of
the test and ensuring the test uses its own isolated proxySetting instance, or
explicitly document (a one-line comment) that the test must run sequentially and
refactor to avoid global mutation by using a setter/mocking approach instead of
directly changing *proxySetting; adjust normalizeHTTPClientOptions usage
accordingly so the test no longer relies on modifying global state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/option.go`:
- Around line 72-95: The handler currently accepts
"proxy_setting.default_tls_fingerprint" values without ensuring that when the
fingerprint equals the sentinel "custom" the companion field
"proxy_setting.default_tls_custom" is present and valid; update the switch case
handling for "proxy_setting.default_tls_fingerprint" (and/or add validation
after dto.NormalizeTLSFingerprint/ dto.IsValidTLSFingerprint) to check if
fingerprint == "custom" and then load and validate option.Value of
"proxy_setting.default_tls_custom" using the same JSON unmarshal/TrimSpace logic
from the "proxy_setting.default_tls_custom" case (reuse the customSpec
validation flow), returning the same HTTP error responses if the required
customSpec is missing or invalid, and only set option.Value when both
fingerprint and customSpec validations pass.

In `@relay/mjproxy_handler.go`:
- Around line 42-44: The error response in mjproxy_handler.go that returns
c.JSON(400, gin.H{"error": "proxy_url_invalid"}) is too specific; update the
handler that builds the channel HTTP client (the branch that currently emits
"proxy_url_invalid") to return a generic error key like
"http_client_config_error" (or similar) to cover proxy URL, TLS fingerprint, and
custom config failures, and ensure the JSON body still includes no sensitive
details.

In `@service/http_client.go`:
- Around line 280-290: The TLS config used for HTTPS proxy tunnels is missing
MinVersion; update the tls.Config passed to tls.Client in the block that creates
tlsConn (the code calling tls.Client(...), tlsConn.HandshakeContext and
hostWithoutPort(proxyURL.Host)) to set MinVersion to a secure minimum (e.g.,
tls.VersionTLS12 or your project-wide minimum constant), keeping
InsecureSkipVerify from common.TLSInsecureSkipVerify intact; ensure you
reference the same tls package symbol (tls.VersionTLS12) or a central constant
if one exists.

---

Outside diff comments:
In `@relay/channel/vertex/service_account.go`:
- Around line 125-128: Replace the direct use of encoding/json decoder when
parsing resp.Body into result with the project wrapper: call
common.DecodeJson(resp.Body, &result) (or the exact wrapper function in
common/json.go) inside the same block where result and resp are used in
service_account.go; remove the direct usage/import of encoding/json from that
file and ensure the common package is imported, returning the same error from
common.DecodeJson if it fails.
- Around line 162-165: Replace the direct use of
json.NewDecoder(resp.Body).Decode(&result) with the wrapper common.DecodeJson to
mirror the other occurrences in service_account.go (i.e., replace the call that
decodes into the local variable result/from resp.Body), so call
common.DecodeJson(resp.Body, &result) and handle the returned error the same
way; after updating both locations in this file remove the unused encoding/json
import.
- Around line 60-63: The code is incorrectly treating Cache.SetDefault as
returning an error and uses a redundant if that returns newToken, nil in both
branches; replace the conditional with a direct call to
Cache.SetDefault(cacheKey, newToken) (either ignore the boolean return or
capture it as exist) and then unconditionally return newToken, nil so the
SetDefault result isn't misinterpreted (references: Cache.SetDefault, cacheKey,
newToken).

In `@service/codex_oauth.go`:
- Around line 319-322: Replace the direct use of json.Unmarshal for decoding the
JWT payload into the local variable claims with the project's wrapper
common.Unmarshal: call common.Unmarshal(payloadRaw, &claims) and handle its
returned error the same way; update the code path that currently checks "if err
:= json.Unmarshal(payloadRaw, &claims); err != nil { return nil, false }" to use
common.Unmarshal and remove the now-unused encoding/json import; keep the
behavior and return values identical when the unmarshal fails.

---

Nitpick comments:
In `@relay/channel/task/gemini/adaptor.go`:
- Around line 222-223: The error message "new proxy http client failed: %w" is
transport-specific; update the return statement that wraps fmt.Errorf(err) (the
line returning nil, fmt.Errorf("new proxy http client failed: %w", err)) to use
transport-agnostic wording (e.g., "new channel http client failed" or "failed to
create http client") so it no longer references "proxy" while still including
the wrapped error; locate the return in adaptor.go where fmt.Errorf is used with
that exact string and replace only the message text accordingly.

In `@service/http_client_test.go`:
- Around line 12-26: The test
TestNormalizeHTTPClientOptions_UseGlobalDefaultFingerprint mutates global
proxySetting (variable proxySetting) which can race with other tests; either
make the test safe for parallel execution by adding t.Parallel() at the top of
the test and ensuring the test uses its own isolated proxySetting instance, or
explicitly document (a one-line comment) that the test must run sequentially and
refactor to avoid global mutation by using a setter/mocking approach instead of
directly changing *proxySetting; adjust normalizeHTTPClientOptions usage
accordingly so the test no longer relies on modifying global state.

In `@service/http_client.go`:
- Around line 102-108: getHTTPClientByOptions can recurse with GetHttpClient if
normalizeHTTPClientOptions ever yields empty fingerprint; add a defensive
fallback: when ProxyURL=="" && TLSFingerprint=="" call GetHttpClient(), and if
it returns nil or if you detect it may have called back (use a simple
package-level recursion sentinel like inGetHTTPClient) then return
http.DefaultClient instead of recursing; also ensure GetHttpClient continues to
call normalizeHTTPClientOptions as before but respects the sentinel to avoid
calling getHTTPClientByOptions again.

In `@web/src/components/settings/SystemSetting.jsx`:
- Around line 317-332: The current submitDefaultTLSFingerprint bundles two
coupled options into one update that may be applied concurrently; change
submitDefaultTLSFingerprint so it performs the writes sequentially: first await
updateOptions with the single option { key:
'proxy_setting.default_tls_fingerprint', value: ... } to persist the chosen
fingerprint, then await updateOptions for { key:
'proxy_setting.default_tls_custom', value: ... } (set to the custom value only
if the first value === 'custom', otherwise send an empty string) so the
dependent custom value is only written after the fingerprint is saved.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75fa039 and 14639a7.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (37)
  • controller/channel-billing.go
  • controller/channel.go
  • controller/codex_oauth.go
  • controller/codex_usage.go
  • controller/option.go
  • controller/video_proxy.go
  • controller/video_proxy_gemini.go
  • dto/channel_settings.go
  • go.mod
  • model/channel.go
  • relay/channel/adapter.go
  • relay/channel/api_request.go
  • relay/channel/aws/relay-aws.go
  • relay/channel/coze/relay-coze.go
  • relay/channel/gemini/relay-gemini.go
  • relay/channel/task/ali/adaptor.go
  • relay/channel/task/doubao/adaptor.go
  • relay/channel/task/gemini/adaptor.go
  • relay/channel/task/hailuo/adaptor.go
  • relay/channel/task/jimeng/adaptor.go
  • relay/channel/task/kling/adaptor.go
  • relay/channel/task/sora/adaptor.go
  • relay/channel/task/suno/adaptor.go
  • relay/channel/task/vertex/adaptor.go
  • relay/channel/task/vidu/adaptor.go
  • relay/channel/vertex/service_account.go
  • relay/mjproxy_handler.go
  • relay/relay_task.go
  • service/codex_credential_refresh.go
  • service/codex_oauth.go
  • service/http_client.go
  • service/http_client_test.go
  • service/task_billing_test.go
  • service/task_polling.go
  • setting/system_setting/proxy_setting.go
  • web/src/components/settings/SystemSetting.jsx
  • web/src/components/table/channels/modals/EditChannelModal.jsx

Comment thread controller/option.go
Comment on lines +72 to 95
case "proxy_setting.default_tls_fingerprint":
fingerprint := dto.NormalizeTLSFingerprint(option.Value.(string))
if !dto.IsValidTLSFingerprint(fingerprint) {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无效的 TLS 指纹配置",
})
return
}
option.Value = fingerprint
case "proxy_setting.default_tls_custom":
customSpec := strings.TrimSpace(option.Value.(string))
if customSpec != "" {
var customMap map[string]any
if err = common.Unmarshal([]byte(customSpec), &customMap); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "TLS 自定义配置必须是合法 JSON",
})
return
}
}
option.Value = customSpec
case "GitHubOAuthEnabled":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Enforce cross-field invariant when fingerprint is custom.

Line 72-82 currently validates only the enum value. It should also require a valid proxy_setting.default_tls_custom; otherwise an invalid global TLS config can be saved.

🛠️ Suggested patch
 case "proxy_setting.default_tls_fingerprint":
   fingerprint := dto.NormalizeTLSFingerprint(option.Value.(string))
   if !dto.IsValidTLSFingerprint(fingerprint) {
     c.JSON(http.StatusOK, gin.H{
       "success": false,
       "message": "无效的 TLS 指纹配置",
     })
     return
   }
+  if fingerprint == dto.TLSFingerprintCustom {
+    common.OptionMapRWMutex.RLock()
+    rawCustom := common.Interface2String(common.OptionMap["proxy_setting.default_tls_custom"])
+    common.OptionMapRWMutex.RUnlock()
+    if strings.TrimSpace(rawCustom) == "" {
+      c.JSON(http.StatusOK, gin.H{
+        "success": false,
+        "message": "TLS 指纹为自定义时,必须先配置默认 TLS 自定义 JSON",
+      })
+      return
+    }
+    var customMap map[string]any
+    if err = common.Unmarshal([]byte(rawCustom), &customMap); err != nil {
+      c.JSON(http.StatusOK, gin.H{
+        "success": false,
+        "message": "TLS 自定义配置必须是合法 JSON",
+      })
+      return
+    }
+  }
   option.Value = fingerprint
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/option.go` around lines 72 - 95, The handler currently accepts
"proxy_setting.default_tls_fingerprint" values without ensuring that when the
fingerprint equals the sentinel "custom" the companion field
"proxy_setting.default_tls_custom" is present and valid; update the switch case
handling for "proxy_setting.default_tls_fingerprint" (and/or add validation
after dto.NormalizeTLSFingerprint/ dto.IsValidTLSFingerprint) to check if
fingerprint == "custom" and then load and validate option.Value of
"proxy_setting.default_tls_custom" using the same JSON unmarshal/TrimSpace logic
from the "proxy_setting.default_tls_custom" case (reuse the customSpec
validation flow), returning the same HTTP error responses if the required
customSpec is missing or invalid, and only set option.Value when both
fingerprint and customSpec validations pass.

Comment thread relay/mjproxy_handler.go
Comment on lines 42 to 44
c.JSON(400, gin.H{
"error": "proxy_url_invalid",
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use a generic error key for channel HTTP client config failures.

Line 42 currently returns proxy_url_invalid, but this path now also covers TLS fingerprint/custom config errors. The current message is misleading.

💡 Suggested patch
-               "error": "proxy_url_invalid",
+               "error": "channel_http_client_config_invalid",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
c.JSON(400, gin.H{
"error": "proxy_url_invalid",
})
c.JSON(400, gin.H{
"error": "channel_http_client_config_invalid",
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/mjproxy_handler.go` around lines 42 - 44, The error response in
mjproxy_handler.go that returns c.JSON(400, gin.H{"error": "proxy_url_invalid"})
is too specific; update the handler that builds the channel HTTP client (the
branch that currently emits "proxy_url_invalid") to return a generic error key
like "http_client_config_error" (or similar) to cover proxy URL, TLS
fingerprint, and custom config failures, and ensure the JSON body still includes
no sensitive details.

Comment thread service/http_client.go
Comment on lines +280 to +290
if strings.EqualFold(proxyURL.Scheme, "https") {
tlsConn := tls.Client(conn, &tls.Config{
ServerName: hostWithoutPort(proxyURL.Host),
InsecureSkipVerify: common.TLSInsecureSkipVerify,
})
if err := tlsConn.HandshakeContext(ctx); err != nil {
_ = conn.Close()
return nil, err
}
conn = tlsConn
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Set MinVersion on TLS config for HTTPS proxy connections.

The static analysis tool correctly flags that MinVersion is missing from the TLS configuration used when connecting to HTTPS proxies. While this is for the proxy tunnel (not the destination), it's still a security best practice to enforce minimum TLS version.

🔧 Proposed fix
 		if strings.EqualFold(proxyURL.Scheme, "https") {
 			tlsConn := tls.Client(conn, &tls.Config{
 				ServerName:         hostWithoutPort(proxyURL.Host),
 				InsecureSkipVerify: common.TLSInsecureSkipVerify,
+				MinVersion:         tls.VersionTLS12,
 			})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if strings.EqualFold(proxyURL.Scheme, "https") {
tlsConn := tls.Client(conn, &tls.Config{
ServerName: hostWithoutPort(proxyURL.Host),
InsecureSkipVerify: common.TLSInsecureSkipVerify,
})
if err := tlsConn.HandshakeContext(ctx); err != nil {
_ = conn.Close()
return nil, err
}
conn = tlsConn
}
if strings.EqualFold(proxyURL.Scheme, "https") {
tlsConn := tls.Client(conn, &tls.Config{
ServerName: hostWithoutPort(proxyURL.Host),
InsecureSkipVerify: common.TLSInsecureSkipVerify,
MinVersion: tls.VersionTLS12,
})
if err := tlsConn.HandshakeContext(ctx); err != nil {
_ = conn.Close()
return nil, err
}
conn = tlsConn
}
🧰 Tools
🪛 ast-grep (0.41.0)

[warning] 280-283: MinVersionis missing from this TLS configuration. By default, TLS 1.2 is currently used as the minimum when acting as a client, and TLS 1.0 when acting as a server. General purpose web applications should default to TLS 1.3 with all other protocols disabled. Only where it is known that a web server must support legacy clients with unsupported an insecure browsers (such as Internet Explorer 10), it may be necessary to enable TLS 1.0 to provide support. AddMinVersion: tls.VersionTLS13' to the TLS configuration to bump the minimum version to TLS 1.3.
Context: tls.Config{
ServerName: hostWithoutPort(proxyURL.Host),
InsecureSkipVerify: common.TLSInsecureSkipVerify,
}
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures [REFERENCES]
https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(missing-ssl-minversion-go)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/http_client.go` around lines 280 - 290, The TLS config used for HTTPS
proxy tunnels is missing MinVersion; update the tls.Config passed to tls.Client
in the block that creates tlsConn (the code calling tls.Client(...),
tlsConn.HandshakeContext and hostWithoutPort(proxyURL.Host)) to set MinVersion
to a secure minimum (e.g., tls.VersionTLS12 or your project-wide minimum
constant), keeping InsecureSkipVerify from common.TLSInsecureSkipVerify intact;
ensure you reference the same tls package symbol (tls.VersionTLS12) or a central
constant if one exists.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
web/src/components/settings/SystemSetting.jsx (3)

317-343: Simplify value resolution—nested object fallbacks are unnecessary.

Semi UI's Form stores dot-notation field names (e.g., 'proxy_setting.default_tls_fingerprint') as literal string keys, not as nested objects. The fallbacks to formValues?.proxy_setting?.default_tls_fingerprint will never match and add confusion.

♻️ Suggested simplification
 const submitDefaultTLSFingerprint = async () => {
   const formValues = formApiRef.current?.getValues() || {};
   const selectedFingerprint =
-    formValues['proxy_setting.default_tls_fingerprint'] ??
-    formValues?.proxy_setting?.default_tls_fingerprint ??
-    inputs['proxy_setting.default_tls_fingerprint'] ??
-    inputs?.proxy_setting?.default_tls_fingerprint ??
-    '';
+    formValues['proxy_setting.default_tls_fingerprint'] ??
+    inputs['proxy_setting.default_tls_fingerprint'] ??
+    '';
   const customSpec =
-    formValues['proxy_setting.default_tls_custom'] ??
-    formValues?.proxy_setting?.default_tls_custom ??
-    inputs['proxy_setting.default_tls_custom'] ??
-    inputs?.proxy_setting?.default_tls_custom ??
-    '';
+    formValues['proxy_setting.default_tls_custom'] ??
+    inputs['proxy_setting.default_tls_custom'] ??
+    '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/settings/SystemSetting.jsx` around lines 317 - 343, In
submitDefaultTLSFingerprint, simplify value resolution by removing the
impossible nested-object fallbacks
(formValues?.proxy_setting?.default_tls_fingerprint and
inputs?.proxy_setting?.default_tls_fingerprint) and only read the flat
dot-notation keys that Semi stores (e.g.,
formValues['proxy_setting.default_tls_fingerprint'] and
inputs['proxy_setting.default_tls_fingerprint']); do the same for
default_tls_custom (use formValues['proxy_setting.default_tls_custom'] and
inputs['proxy_setting.default_tls_custom']), then build the options array the
same way and call updateOptions(options).

851-868: Consider validating JSON before submission.

The TextArea accepts custom ClientHello JSON, but there's no validation that the input is valid JSON. Invalid JSON sent to the server could cause issues or confusing error messages for users.

🛡️ Suggested validation in submit handler
 const submitDefaultTLSFingerprint = async () => {
   const formValues = formApiRef.current?.getValues() || {};
   const selectedFingerprint =
     formValues['proxy_setting.default_tls_fingerprint'] ??
     inputs['proxy_setting.default_tls_fingerprint'] ??
     '';
   const customSpec =
     formValues['proxy_setting.default_tls_custom'] ??
     inputs['proxy_setting.default_tls_custom'] ??
     '';

+  if (selectedFingerprint === 'custom' && customSpec) {
+    try {
+      JSON.parse(customSpec);
+    } catch (e) {
+      showError(t('自定义 TLS ClientHello 规格必须是有效的 JSON 格式'));
+      return;
+    }
+  }
+
   const options = [
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/settings/SystemSetting.jsx` around lines 851 - 868, The
TextArea for 'proxy_setting.default_tls_custom' in SystemSetting.jsx accepts
JSON but isn't validated; before submitting (in the submit handler used by this
component, e.g., handleSubmit/onSubmit), attempt to JSON.parse the value from
inputs['proxy_setting.default_tls_custom'] and if parsing fails set a validation
error (or show a user-facing error message) and stop submission; if parsing
succeeds, optionally store the parsed object or allow submission. Also consider
adding inline validation on change (in the onChange that calls setInputs) to
show immediate feedback.

844-850: Simplify the conditional expression.

Same unnecessary nested object fallbacks as in submitDefaultTLSFingerprint. The values object from Semi UI Form uses the exact field name string as the key.

♻️ Suggested simplification
-                   {((
-                     values?.['proxy_setting.default_tls_fingerprint'] ??
-                     values?.proxy_setting?.default_tls_fingerprint ??
-                     inputs['proxy_setting.default_tls_fingerprint'] ??
-                     inputs?.proxy_setting?.default_tls_fingerprint ??
-                     ''
-                   ) === 'custom') && (
+                   {(values?.['proxy_setting.default_tls_fingerprint'] ?? 
+                     inputs['proxy_setting.default_tls_fingerprint'] ?? 
+                     '') === 'custom' && (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/settings/SystemSetting.jsx` around lines 844 - 850, The
conditional with multiple nested fallbacks should be simplified: instead of
checking both dot-key and nested object forms on values and inputs, read the
form field directly by the exact key string
(values['proxy_setting.default_tls_fingerprint'] ??
inputs['proxy_setting.default_tls_fingerprint'] ?? '') and compare to 'custom';
remove the unnecessary checks against
values?.proxy_setting?.default_tls_fingerprint and
inputs?.proxy_setting?.default_tls_fingerprint so the conditional matches the
same approach used in submitDefaultTLSFingerprint.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@web/src/components/settings/SystemSetting.jsx`:
- Around line 317-343: In submitDefaultTLSFingerprint, simplify value resolution
by removing the impossible nested-object fallbacks
(formValues?.proxy_setting?.default_tls_fingerprint and
inputs?.proxy_setting?.default_tls_fingerprint) and only read the flat
dot-notation keys that Semi stores (e.g.,
formValues['proxy_setting.default_tls_fingerprint'] and
inputs['proxy_setting.default_tls_fingerprint']); do the same for
default_tls_custom (use formValues['proxy_setting.default_tls_custom'] and
inputs['proxy_setting.default_tls_custom']), then build the options array the
same way and call updateOptions(options).
- Around line 851-868: The TextArea for 'proxy_setting.default_tls_custom' in
SystemSetting.jsx accepts JSON but isn't validated; before submitting (in the
submit handler used by this component, e.g., handleSubmit/onSubmit), attempt to
JSON.parse the value from inputs['proxy_setting.default_tls_custom'] and if
parsing fails set a validation error (or show a user-facing error message) and
stop submission; if parsing succeeds, optionally store the parsed object or
allow submission. Also consider adding inline validation on change (in the
onChange that calls setInputs) to show immediate feedback.
- Around line 844-850: The conditional with multiple nested fallbacks should be
simplified: instead of checking both dot-key and nested object forms on values
and inputs, read the form field directly by the exact key string
(values['proxy_setting.default_tls_fingerprint'] ??
inputs['proxy_setting.default_tls_fingerprint'] ?? '') and compare to 'custom';
remove the unnecessary checks against
values?.proxy_setting?.default_tls_fingerprint and
inputs?.proxy_setting?.default_tls_fingerprint so the conditional matches the
same approach used in submitDefaultTLSFingerprint.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14639a7 and 42b6e38.

📒 Files selected for processing (1)
  • web/src/components/settings/SystemSetting.jsx

@seefs001
seefs001 marked this pull request as draft March 2, 2026 07:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: 支持在渠道设置里自定义 TLS 指纹 (Custom TLS Fingerprint per Channel)