From ccd42c1d1a9ff6e36add036dddfda2ecec767df2 Mon Sep 17 00:00:00 2001 From: weak-fox <827367480@qq.com> Date: Mon, 23 Mar 2026 00:10:22 +0800 Subject: [PATCH 01/16] Retry OpenAI privacy opt-out after failed states --- backend/internal/service/admin_service.go | 6 +- .../service/openai_privacy_retry_test.go | 89 +++++++++++++++++++ .../service/openai_privacy_service.go | 13 +++ .../internal/service/token_refresh_service.go | 7 +- 4 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 backend/internal/service/openai_privacy_retry_test.go diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index ccd681a3..294ff54a 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -2635,10 +2635,8 @@ func (s *adminServiceImpl) EnsureOpenAIPrivacy(ctx context.Context, account *Acc if s.privacyClientFactory == nil { return "" } - if account.Extra != nil { - if _, ok := account.Extra["privacy_mode"]; ok { - return "" - } + if shouldSkipOpenAIPrivacyEnsure(account.Extra) { + return "" } token, _ := account.Credentials["access_token"].(string) diff --git a/backend/internal/service/openai_privacy_retry_test.go b/backend/internal/service/openai_privacy_retry_test.go new file mode 100644 index 00000000..24534ea9 --- /dev/null +++ b/backend/internal/service/openai_privacy_retry_test.go @@ -0,0 +1,89 @@ +//go:build unit + +package service + +import ( + "context" + "errors" + "testing" + + "github.com/Wei-Shaw/sub2api/internal/config" + "github.com/imroc/req/v3" + "github.com/stretchr/testify/require" +) + +func TestAdminService_EnsureOpenAIPrivacy_RetriesNonSuccessModes(t *testing.T) { + t.Parallel() + + for _, mode := range []string{PrivacyModeFailed, PrivacyModeCFBlocked} { + t.Run(mode, func(t *testing.T) { + t.Parallel() + + privacyCalls := 0 + svc := &adminServiceImpl{ + accountRepo: &mockAccountRepoForGemini{}, + privacyClientFactory: func(proxyURL string) (*req.Client, error) { + privacyCalls++ + return nil, errors.New("factory failed") + }, + } + + account := &Account{ + ID: 101, + Platform: PlatformOpenAI, + Type: AccountTypeOAuth, + Credentials: map[string]any{ + "access_token": "token-1", + }, + Extra: map[string]any{ + "privacy_mode": mode, + }, + } + + got := svc.EnsureOpenAIPrivacy(context.Background(), account) + + require.Equal(t, PrivacyModeFailed, got) + require.Equal(t, 1, privacyCalls) + }) + } +} + +func TestTokenRefreshService_ensureOpenAIPrivacy_RetriesNonSuccessModes(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + TokenRefresh: config.TokenRefreshConfig{ + MaxRetries: 1, + RetryBackoffSeconds: 0, + }, + } + + for _, mode := range []string{PrivacyModeFailed, PrivacyModeCFBlocked} { + t.Run(mode, func(t *testing.T) { + t.Parallel() + + service := NewTokenRefreshService(&tokenRefreshAccountRepo{}, nil, nil, nil, nil, nil, nil, cfg, nil) + privacyCalls := 0 + service.SetPrivacyDeps(func(proxyURL string) (*req.Client, error) { + privacyCalls++ + return nil, errors.New("factory failed") + }, nil) + + account := &Account{ + ID: 202, + Platform: PlatformOpenAI, + Type: AccountTypeOAuth, + Credentials: map[string]any{ + "access_token": "token-2", + }, + Extra: map[string]any{ + "privacy_mode": mode, + }, + } + + service.ensureOpenAIPrivacy(context.Background(), account) + + require.Equal(t, 1, privacyCalls) + }) + } +} diff --git a/backend/internal/service/openai_privacy_service.go b/backend/internal/service/openai_privacy_service.go index 90cd522d..9f4a6a86 100644 --- a/backend/internal/service/openai_privacy_service.go +++ b/backend/internal/service/openai_privacy_service.go @@ -22,6 +22,19 @@ const ( PrivacyModeCFBlocked = "training_set_cf_blocked" ) +func shouldSkipOpenAIPrivacyEnsure(extra map[string]any) bool { + if extra == nil { + return false + } + raw, ok := extra["privacy_mode"] + if !ok { + return false + } + mode, _ := raw.(string) + mode = strings.TrimSpace(mode) + return mode != PrivacyModeFailed && mode != PrivacyModeCFBlocked +} + // disableOpenAITraining calls ChatGPT settings API to turn off "Improve the model for everyone". // Returns privacy_mode value: "training_off" on success, "cf_blocked" / "failed" on failure. func disableOpenAITraining(ctx context.Context, clientFactory PrivacyClientFactory, accessToken, proxyURL string) string { diff --git a/backend/internal/service/token_refresh_service.go b/backend/internal/service/token_refresh_service.go index 582afcd3..c0ab3593 100644 --- a/backend/internal/service/token_refresh_service.go +++ b/backend/internal/service/token_refresh_service.go @@ -442,11 +442,8 @@ func (s *TokenRefreshService) ensureOpenAIPrivacy(ctx context.Context, account * if s.privacyClientFactory == nil { return } - // 已设置过则跳过 - if account.Extra != nil { - if _, ok := account.Extra["privacy_mode"]; ok { - return - } + if shouldSkipOpenAIPrivacyEnsure(account.Extra) { + return } token, _ := account.Credentials["access_token"].(string) From 73d72651b48cf52edf4d6c75cb2cbb2c83b33bf0 Mon Sep 17 00:00:00 2001 From: Wang Lvyuan <74089601+LvyuanW@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:17:42 +0800 Subject: [PATCH 02/16] feat: support bulk OpenAI passthrough toggle --- .../account/BulkEditAccountModal.vue | 437 +++++++++++------- .../__tests__/BulkEditAccountModal.spec.ts | 75 ++- 2 files changed, 339 insertions(+), 173 deletions(-) diff --git a/frontend/src/components/account/BulkEditAccountModal.vue b/frontend/src/components/account/BulkEditAccountModal.vue index baecd6f6..68dc4fcc 100644 --- a/frontend/src/components/account/BulkEditAccountModal.vue +++ b/frontend/src/components/account/BulkEditAccountModal.vue @@ -31,6 +31,57 @@

+ +
+
+
+ +

+ {{ t('admin.accounts.openai.oauthPassthroughDesc') }} +

+
+ +
+
+ +
+
+
@@ -89,100 +140,30 @@ role="group" aria-labelledby="bulk-edit-model-restriction-label" > - -
- - -
- - -
-
-

- - - - {{ t('admin.accounts.selectAllowedModels') }} -

-
- - - -

- {{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }} - {{ - t('admin.accounts.supportsAllModels') - }} +

+

+ {{ t('admin.accounts.openai.modelRestrictionDisabledByPassthrough') }}

- -
-
-

+

@@ -821,7 +883,6 @@ import { buildModelMappingObject as buildModelMappingPayload, getPresetMappingsByPlatform } from '@/composables/useModelWhitelist' - interface Props { show: boolean accountIds: number[] @@ -843,6 +904,15 @@ const appStore = useAppStore() // Platform awareness const isMixedPlatform = computed(() => props.selectedPlatforms.length > 1) +const allOpenAIPassthroughCapable = computed(() => { + return ( + props.selectedPlatforms.length === 1 && + props.selectedPlatforms[0] === 'openai' && + props.selectedTypes.length > 0 && + props.selectedTypes.every(t => t === 'oauth' || t === 'apikey') + ) +}) + // 是否全部为 Anthropic OAuth/SetupToken(RPM 配置仅在此条件下显示) const allAnthropicOAuthOrSetupToken = computed(() => { return ( @@ -886,6 +956,7 @@ const enablePriority = ref(false) const enableRateMultiplier = ref(false) const enableStatus = ref(false) const enableGroups = ref(false) +const enableOpenAIPassthrough = ref(false) const enableRpmLimit = ref(false) // State - field values @@ -907,6 +978,7 @@ const priority = ref(1) const rateMultiplier = ref(1) const status = ref<'active' | 'inactive'>('active') const groupIds = ref([]) +const openaiPassthroughEnabled = ref(false) const rpmLimitEnabled = ref(false) const bulkBaseRpm = ref(null) const bulkRpmStrategy = ref<'tiered' | 'sticky_exempt'>('tiered') @@ -933,6 +1005,11 @@ const statusOptions = computed(() => [ { value: 'active', label: t('common.active') }, { value: 'inactive', label: t('common.inactive') } ]) +const isOpenAIModelRestrictionDisabled = computed(() => + allOpenAIPassthroughCapable.value && + enableOpenAIPassthrough.value && + openaiPassthroughEnabled.value +) // Model mapping helpers const addModelMapping = () => { @@ -1015,6 +1092,12 @@ const buildUpdatePayload = (): Record | null => { const updates: Record = {} const credentials: Record = {} let credentialsChanged = false + const ensureExtra = (): Record => { + if (!updates.extra) { + updates.extra = {} + } + return updates.extra as Record + } if (enableProxy.value) { // 后端期望 proxy_id: 0 表示清除代理,而不是 null @@ -1055,7 +1138,15 @@ const buildUpdatePayload = (): Record | null => { } } - if (enableModelRestriction.value) { + if (enableOpenAIPassthrough.value) { + const extra = ensureExtra() + extra.openai_passthrough = openaiPassthroughEnabled.value + if (!openaiPassthroughEnabled.value) { + extra.openai_oauth_passthrough = false + } + } + + if (enableModelRestriction.value && !isOpenAIModelRestrictionDisabled.value) { // 统一使用 model_mapping 字段 if (modelRestrictionMode.value === 'whitelist') { // 白名单模式:将模型转换为 model_mapping 格式(key=value) @@ -1091,7 +1182,7 @@ const buildUpdatePayload = (): Record | null => { // RPM limit settings (写入 extra 字段) if (enableRpmLimit.value) { - const extra: Record = {} + const extra = ensureExtra() if (rpmLimitEnabled.value && bulkBaseRpm.value != null && bulkBaseRpm.value > 0) { extra.base_rpm = bulkBaseRpm.value extra.rpm_strategy = bulkRpmStrategy.value @@ -1111,8 +1202,7 @@ const buildUpdatePayload = (): Record | null => { // UMQ mode(独立于 RPM 保存) if (userMsgQueueMode.value !== null) { - if (!updates.extra) updates.extra = {} - const umqExtra = updates.extra as Record + const umqExtra = ensureExtra() umqExtra.user_msg_queue_mode = userMsgQueueMode.value // '' = 清除账号级覆盖 umqExtra.user_msg_queue_enabled = false // 清理旧字段(JSONB merge) } @@ -1168,6 +1258,7 @@ const handleSubmit = async () => { const hasAnyFieldEnabled = enableBaseUrl.value || + enableOpenAIPassthrough.value || enableModelRestriction.value || enableCustomErrorCodes.value || enableInterceptWarmup.value || @@ -1269,10 +1360,12 @@ watch( enableRateMultiplier.value = false enableStatus.value = false enableGroups.value = false + enableOpenAIPassthrough.value = false enableRpmLimit.value = false // Reset all values baseUrl.value = '' + openaiPassthroughEnabled.value = false modelRestrictionMode.value = 'whitelist' allowedModels.value = [] modelMappings.value = [] diff --git a/frontend/src/components/account/__tests__/BulkEditAccountModal.spec.ts b/frontend/src/components/account/__tests__/BulkEditAccountModal.spec.ts index 3598ff11..6458359e 100644 --- a/frontend/src/components/account/__tests__/BulkEditAccountModal.spec.ts +++ b/frontend/src/components/account/__tests__/BulkEditAccountModal.spec.ts @@ -50,7 +50,21 @@ function mountModal(extraProps: Record = {}) { stubs: { BaseDialog: { template: '
' }, ConfirmDialog: true, - Select: true, + Select: { + props: ['modelValue', 'options'], + emits: ['update:modelValue'], + template: ` + + ` + }, ProxySelector: true, GroupSelector: true, Icon: true @@ -115,4 +129,63 @@ describe('BulkEditAccountModal', () => { } }) }) + + it('OpenAI 账号批量编辑可开启自动透传', async () => { + const wrapper = mountModal({ + selectedPlatforms: ['openai'], + selectedTypes: ['oauth'] + }) + + await wrapper.get('#bulk-edit-openai-passthrough-enabled').setValue(true) + await wrapper.get('#bulk-edit-openai-passthrough-toggle').trigger('click') + await wrapper.get('#bulk-edit-account-form').trigger('submit.prevent') + await flushPromises() + + expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledTimes(1) + expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledWith([1, 2], { + extra: { + openai_passthrough: true + } + }) + }) + + it('OpenAI 账号批量编辑可关闭自动透传', async () => { + const wrapper = mountModal({ + selectedPlatforms: ['openai'], + selectedTypes: ['apikey'] + }) + + await wrapper.get('#bulk-edit-openai-passthrough-enabled').setValue(true) + await wrapper.get('#bulk-edit-account-form').trigger('submit.prevent') + await flushPromises() + + expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledTimes(1) + expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledWith([1, 2], { + extra: { + openai_passthrough: false, + openai_oauth_passthrough: false + } + }) + }) + + it('开启 OpenAI 自动透传时不再同时提交模型限制', async () => { + const wrapper = mountModal({ + selectedPlatforms: ['openai'], + selectedTypes: ['oauth'] + }) + + await wrapper.get('#bulk-edit-openai-passthrough-enabled').setValue(true) + await wrapper.get('#bulk-edit-openai-passthrough-toggle').trigger('click') + await wrapper.get('#bulk-edit-model-restriction-enabled').setValue(true) + await wrapper.get('#bulk-edit-account-form').trigger('submit.prevent') + await flushPromises() + + expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledTimes(1) + expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledWith([1, 2], { + extra: { + openai_passthrough: true + } + }) + expect(wrapper.text()).toContain('admin.accounts.openai.modelRestrictionDisabledByPassthrough') + }) }) From fdad55956e3a98d68054676f556b3a24f6c0a807 Mon Sep 17 00:00:00 2001 From: Ikko Ashimine Date: Wed, 25 Mar 2026 00:34:56 +0900 Subject: [PATCH 03/16] docs: add Japanese README --- README.md | 2 +- README_CN.md | 2 +- README_JA.md | 585 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 587 insertions(+), 2 deletions(-) create mode 100644 README_JA.md diff --git a/README.md b/README.md index 97dea364..41a5aca1 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ **AI API Gateway Platform for Subscription Quota Distribution** -English | [中文](README_CN.md) +English | [中文](README_CN.md) | [日本語](README_JA.md)
diff --git a/README_CN.md b/README_CN.md index b1f5341d..3380cce7 100644 --- a/README_CN.md +++ b/README_CN.md @@ -12,7 +12,7 @@ **AI API 网关平台 - 订阅配额分发管理** -[English](README.md) | 中文 +[English](README.md) | 中文 | [日本語](README_JA.md)
diff --git a/README_JA.md b/README_JA.md new file mode 100644 index 00000000..c60b1a8e --- /dev/null +++ b/README_JA.md @@ -0,0 +1,585 @@ +# Sub2API + +
+ +[![Go](https://img.shields.io/badge/Go-1.25.7-00ADD8.svg)](https://golang.org/) +[![Vue](https://img.shields.io/badge/Vue-3.4+-4FC08D.svg)](https://vuejs.org/) +[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-336791.svg)](https://www.postgresql.org/) +[![Redis](https://img.shields.io/badge/Redis-7+-DC382D.svg)](https://redis.io/) +[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED.svg)](https://www.docker.com/) + +Wei-Shaw%2Fsub2api | Trendshift + +**サブスクリプションクォータ配分のための AI API ゲートウェイプラットフォーム** + +[English](README.md) | [中文](README_CN.md) | 日本語 + +
+ +> **Sub2API が公式に使用しているドメインは `sub2api.org` と `pincc.ai` のみです。Sub2API の名称を使用している他のウェブサイトは、サードパーティによるデプロイやサービスであり、本プロジェクトとは一切関係がありません。ご利用の際はご自身で確認・判断をお願いします。** + +--- + +## デモ + +Sub2API をオンラインでお試しください: **[https://demo.sub2api.org/](https://demo.sub2api.org/)** + +デモ用認証情報(共有デモ環境です。セルフホスト環境では**自動作成されません**): + +| メールアドレス | パスワード | +|-------|----------| +| admin@sub2api.org | admin123 | + +## 概要 + +Sub2API は、AI 製品のサブスクリプションから API クォータを配分・管理するために設計された AI API ゲートウェイプラットフォームです。ユーザーはプラットフォームが生成した API キーを通じて上流の AI サービスにアクセスでき、プラットフォームは認証、課金、負荷分散、リクエスト転送を処理します。 + +## 機能 + +- **マルチアカウント管理** - 複数の上流アカウントタイプ(OAuth、APIキー)をサポート +- **APIキー配布** - ユーザー向けの APIキーの生成と管理 +- **精密な課金** - トークンレベルの使用量追跡とコスト計算 +- **スマートスケジューリング** - スティッキーセッション付きのインテリジェントなアカウント選択 +- **同時実行制御** - ユーザーごと・アカウントごとの同時実行数制限 +- **レート制限** - 設定可能なリクエスト数およびトークンレート制限 +- **管理ダッシュボード** - 監視・管理のための Web インターフェース +- **外部システム連携** - 外部システム(決済、チケット管理など)を iframe 経由で管理ダッシュボードに埋め込み可能 + +## セルフホストが不要な方へ + + + + + + +
pinccPinCC は Sub2API 上に構築された公式リレーサービスで、Claude Code、Codex、Gemini などの人気モデルへの安定したアクセスを提供します。デプロイやメンテナンスは不要で、すぐにご利用いただけます。
+ +## エコシステム + +Sub2API を拡張・統合するコミュニティプロジェクト: + +| プロジェクト | 説明 | 機能 | +|---------|-------------|----------| +| [Sub2ApiPay](https://github.com/touwaeriol/sub2apipay) | セルフサービス決済システム | セルフサービスによるチャージおよびサブスクリプション購入。YiPay プロトコル、WeChat Pay、Alipay、Stripe 対応。iframe での埋め込み可能 | +| [sub2api-mobile](https://github.com/ckken/sub2api-mobile) | モバイル管理コンソール | ユーザー管理、アカウント管理、監視ダッシュボード、マルチバックエンド切り替えが可能なクロスプラットフォームアプリ(iOS/Android/Web)。Expo + React Native で構築 | + +## 技術スタック + +| コンポーネント | 技術 | +|-----------|------------| +| バックエンド | Go 1.25.7, Gin, Ent | +| フロントエンド | Vue 3.4+, Vite 5+, TailwindCSS | +| データベース | PostgreSQL 15+ | +| キャッシュ/キュー | Redis 7+ | + +--- + +## Nginx リバースプロキシに関する注意 + +Sub2API(または CRS)を Nginx でリバースプロキシし、Codex CLI と組み合わせて使用する場合、Nginx の `http` ブロックに以下の設定を追加してください: + +```nginx +underscores_in_headers on; +``` + +Nginx はデフォルトでアンダースコアを含むヘッダー(例: `session_id`)を破棄するため、マルチアカウント構成でのスティッキーセッションルーティングに支障をきたします。 + +--- + +## デプロイ + +### 方法1: スクリプトによるインストール(推奨) + +GitHub Releases からビルド済みバイナリをダウンロードするワンクリックインストールスクリプトです。 + +#### 前提条件 + +- Linux サーバー(amd64 または arm64) +- PostgreSQL 15+(インストール済みかつ稼働中) +- Redis 7+(インストール済みかつ稼働中) +- root 権限 + +#### インストール手順 + +```bash +curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | sudo bash +``` + +スクリプトは以下を実行します: +1. システムアーキテクチャの検出 +2. 最新リリースのダウンロード +3. バイナリを `/opt/sub2api` にインストール +4. systemd サービスの作成 +5. システムユーザーと権限の設定 + +#### インストール後の作業 + +```bash +# 1. サービスを起動 +sudo systemctl start sub2api + +# 2. 起動時の自動起動を有効化 +sudo systemctl enable sub2api + +# 3. ブラウザでセットアップウィザードを開く +# http://YOUR_SERVER_IP:8080 +``` + +セットアップウィザードでは以下の設定を行います: +- データベース設定 +- Redis 設定 +- 管理者アカウントの作成 + +#### アップグレード + +**管理ダッシュボード**の左上にある**アップデートを確認**ボタンをクリックすることで、ダッシュボードから直接アップグレードできます。 + +Web インターフェースでは以下が可能です: +- 新しいバージョンの自動確認 +- ワンクリックでのアップデートのダウンロードと適用 +- 必要に応じたロールバック + +#### よく使うコマンド + +```bash +# ステータスを確認 +sudo systemctl status sub2api + +# ログを表示 +sudo journalctl -u sub2api -f + +# サービスを再起動 +sudo systemctl restart sub2api + +# アンインストール +curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | sudo bash -s -- uninstall -y +``` + +--- + +### 方法2: Docker Compose(推奨) + +PostgreSQL と Redis のコンテナを含む Docker Compose でデプロイします。 + +#### 前提条件 + +- Docker 20.10+ +- Docker Compose v2+ + +#### クイックスタート(ワンクリックデプロイ) + +自動デプロイスクリプトを使用して簡単にセットアップできます: + +```bash +# デプロイ用ディレクトリを作成 +mkdir -p sub2api-deploy && cd sub2api-deploy + +# デプロイ準備スクリプトをダウンロードして実行 +curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/docker-deploy.sh | bash + +# サービスを起動 +docker compose up -d + +# ログを表示 +docker compose logs -f sub2api +``` + +**スクリプトの動作内容:** +- `docker-compose.local.yml`(`docker-compose.yml` として保存)と `.env.example` をダウンロード +- セキュアな認証情報(JWT_SECRET、TOTP_ENCRYPTION_KEY、POSTGRES_PASSWORD)を自動生成 +- 自動生成されたシークレットで `.env` ファイルを作成 +- データディレクトリを作成(バックアップ・移行が容易なローカルディレクトリを使用) +- 生成された認証情報を参照用に表示 + +#### 手動デプロイ + +手動でセットアップする場合: + +```bash +# 1. リポジトリをクローン +git clone https://github.com/Wei-Shaw/sub2api.git +cd sub2api/deploy + +# 2. 環境設定ファイルをコピー +cp .env.example .env + +# 3. 設定を編集(セキュアなパスワードを生成) +nano .env +``` + +**`.env` の必須設定:** + +```bash +# PostgreSQL パスワード(必須) +POSTGRES_PASSWORD=your_secure_password_here + +# JWT シークレット(推奨 - 再起動後もユーザーのログイン状態を保持) +JWT_SECRET=your_jwt_secret_here + +# TOTP 暗号化キー(推奨 - 再起動後も二要素認証を維持) +TOTP_ENCRYPTION_KEY=your_totp_key_here + +# オプション: 管理者アカウント +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD=your_admin_password + +# オプション: カスタムポート +SERVER_PORT=8080 +``` + +**セキュアなシークレットの生成方法:** +```bash +# JWT_SECRET を生成 +openssl rand -hex 32 + +# TOTP_ENCRYPTION_KEY を生成 +openssl rand -hex 32 + +# POSTGRES_PASSWORD を生成 +openssl rand -hex 32 +``` + +```bash +# 4. データディレクトリを作成(ローカルバージョンの場合) +mkdir -p data postgres_data redis_data + +# 5. すべてのサービスを起動 +# オプション A: ローカルディレクトリバージョン(推奨 - 移行が容易) +docker compose -f docker-compose.local.yml up -d + +# オプション B: 名前付きボリュームバージョン(シンプルなセットアップ) +docker compose up -d + +# 6. ステータスを確認 +docker compose -f docker-compose.local.yml ps + +# 7. ログを表示 +docker compose -f docker-compose.local.yml logs -f sub2api +``` + +#### デプロイバージョン + +| バージョン | データストレージ | 移行 | 推奨用途 | +|---------|-------------|-----------|----------| +| **docker-compose.local.yml** | ローカルディレクトリ | ✅ 容易(ディレクトリ全体を tar) | 本番環境、頻繁なバックアップ | +| **docker-compose.yml** | 名前付きボリューム | ⚠️ docker コマンドが必要 | シンプルなセットアップ | + +**推奨:** データ管理が容易な `docker-compose.local.yml`(スクリプトによるデプロイ)を使用してください。 + +#### アクセス + +ブラウザで `http://YOUR_SERVER_IP:8080` を開いてください。 + +管理者パスワードが自動生成された場合は、ログで確認できます: +```bash +docker compose -f docker-compose.local.yml logs sub2api | grep "admin password" +``` + +#### アップグレード + +```bash +# 最新イメージをプルしてコンテナを再作成 +docker compose -f docker-compose.local.yml pull +docker compose -f docker-compose.local.yml up -d +``` + +#### 簡単な移行(ローカルディレクトリバージョン) + +`docker-compose.local.yml` を使用している場合、新しいサーバーへの移行が簡単です: + +```bash +# 移行元サーバーにて +docker compose -f docker-compose.local.yml down +cd .. +tar czf sub2api-complete.tar.gz sub2api-deploy/ + +# 新しいサーバーに転送 +scp sub2api-complete.tar.gz user@new-server:/path/ + +# 移行先サーバーにて +tar xzf sub2api-complete.tar.gz +cd sub2api-deploy/ +docker compose -f docker-compose.local.yml up -d +``` + +#### よく使うコマンド + +```bash +# すべてのサービスを停止 +docker compose -f docker-compose.local.yml down + +# 再起動 +docker compose -f docker-compose.local.yml restart + +# すべてのログを表示 +docker compose -f docker-compose.local.yml logs -f + +# すべてのデータを削除(注意!) +docker compose -f docker-compose.local.yml down +rm -rf data/ postgres_data/ redis_data/ +``` + +--- + +### 方法3: ソースからビルド + +開発やカスタマイズのためにソースコードからビルドして実行します。 + +#### 前提条件 + +- Go 1.21+ +- Node.js 18+ +- PostgreSQL 15+ +- Redis 7+ + +#### ビルド手順 + +```bash +# 1. リポジトリをクローン +git clone https://github.com/Wei-Shaw/sub2api.git +cd sub2api + +# 2. pnpm をインストール(未インストールの場合) +npm install -g pnpm + +# 3. フロントエンドをビルド +cd frontend +pnpm install +pnpm run build +# 出力先: ../backend/internal/web/dist/ + +# 4. フロントエンドを組み込んだバックエンドをビルド +cd ../backend +go build -tags embed -o sub2api ./cmd/server + +# 5. 設定ファイルを作成 +cp ../deploy/config.example.yaml ./config.yaml + +# 6. 設定を編集 +nano config.yaml +``` + +> **注意:** `-tags embed` フラグはフロントエンドをバイナリに組み込みます。このフラグがない場合、バイナリはフロントエンド UI を提供しません。 + +**`config.yaml` の主要設定:** + +```yaml +server: + host: "0.0.0.0" + port: 8080 + mode: "release" + +database: + host: "localhost" + port: 5432 + user: "postgres" + password: "your_password" + dbname: "sub2api" + +redis: + host: "localhost" + port: 6379 + password: "" + +jwt: + secret: "change-this-to-a-secure-random-string" + expire_hour: 24 + +default: + user_concurrency: 5 + user_balance: 0 + api_key_prefix: "sk-" + rate_multiplier: 1.0 +``` + +### Sora ステータス(一時的に利用不可) + +> ⚠️ Sora 関連の機能は、上流統合およびメディア配信の技術的問題により一時的に利用できません。 +> 現時点では本番環境で Sora に依存しないでください。 +> 既存の `gateway.sora_*` 設定キーは予約されていますが、これらの問題が解決されるまで有効にならない場合があります。 + +`config.yaml` では追加のセキュリティ関連オプションも利用できます: + +- `cors.allowed_origins` - CORS 許可リスト +- `security.url_allowlist` - 上流/価格/CRS ホストの許可リスト +- `security.url_allowlist.enabled` - URL バリデーションの無効化(注意して使用) +- `security.url_allowlist.allow_insecure_http` - バリデーション無効時に HTTP URL を許可 +- `security.url_allowlist.allow_private_hosts` - プライベート/ローカル IP アドレスを許可 +- `security.response_headers.enabled` - 設定可能なレスポンスヘッダーフィルタリングを有効化(無効時はデフォルトの許可リストを使用) +- `security.csp` - Content-Security-Policy ヘッダーの制御 +- `billing.circuit_breaker` - 課金エラー時にフェイルクローズ +- `server.trusted_proxies` - X-Forwarded-For パースの有効化 +- `turnstile.required` - リリースモードでの Turnstile 必須化 + +**⚠️ セキュリティ警告: HTTP URL 設定** + +`security.url_allowlist.enabled=false` の場合、システムはデフォルトで最小限の URL バリデーションを行い、**HTTP URL を拒否**して HTTPS のみを許可します。HTTP URL を許可するには(開発環境や内部テスト用など)、以下を明示的に設定する必要があります: + +```yaml +security: + url_allowlist: + enabled: false # 許可リストチェックを無効化 + allow_insecure_http: true # HTTP URL を許可(⚠️ セキュリティリスクあり) +``` + +**または環境変数で設定:** + +```bash +SECURITY_URL_ALLOWLIST_ENABLED=false +SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=true +``` + +**HTTP を許可するリスク:** +- API キーとデータが**平文**で送信される(傍受の危険性) +- **中間者攻撃(MITM)**を受けやすい +- **本番環境には不適切** + +**HTTP を使用すべき場面:** +- ✅ ローカルサーバーでの開発・テスト(http://localhost) +- ✅ 信頼できるエンドポイントを持つ内部ネットワーク +- ✅ HTTPS 取得前のアカウント接続テスト +- ❌ 本番環境(HTTPS のみを使用) + +**この設定なしで表示されるエラー例:** +``` +Invalid base URL: invalid url scheme: http +``` + +URL バリデーションまたはレスポンスヘッダーフィルタリングを無効にする場合は、ネットワーク層を強化してください: +- 上流ドメイン/IP のエグレス許可リストを適用 +- プライベート/ループバック/リンクローカル範囲をブロック +- TLS のみのアウトバウンドトラフィックを強制 +- プロキシで機密性の高い上流レスポンスヘッダーを除去 + +```bash +# 6. アプリケーションを実行 +./sub2api +``` + +#### 開発モード + +```bash +# バックエンド(ホットリロード付き) +cd backend +go run ./cmd/server + +# フロントエンド(ホットリロード付き) +cd frontend +pnpm run dev +``` + +#### コード生成 + +`backend/ent/schema` を編集した場合、Ent + Wire を再生成してください: + +```bash +cd backend +go generate ./ent +go generate ./cmd/server +``` + +--- + +## シンプルモード + +シンプルモードは、フル SaaS 機能を必要とせず、素早くアクセスしたい個人開発者や社内チーム向けに設計されています。 + +- 有効化: 環境変数 `RUN_MODE=simple` を設定 +- 違い: SaaS 関連機能を非表示にし、課金プロセスをスキップ +- セキュリティに関する注意: 本番環境では `SIMPLE_MODE_CONFIRM=true` も設定する必要があります + +--- + +## Antigravity サポート + +Sub2API は [Antigravity](https://antigravity.so/) アカウントをサポートしています。認証後、Claude および Gemini モデル用の専用エンドポイントが利用可能になります。 + +### 専用エンドポイント + +| エンドポイント | モデル | +|----------|-------| +| `/antigravity/v1/messages` | Claude モデル | +| `/antigravity/v1beta/` | Gemini モデル | + +### Claude Code の設定 + +```bash +export ANTHROPIC_BASE_URL="http://localhost:8080/antigravity" +export ANTHROPIC_AUTH_TOKEN="sk-xxx" +``` + +### ハイブリッドスケジューリングモード + +Antigravity アカウントはオプションの**ハイブリッドスケジューリング**をサポートしています。有効にすると、汎用エンドポイント `/v1/messages` および `/v1beta/` も Antigravity アカウントにリクエストをルーティングします。 + +> **⚠️ 警告**: Anthropic Claude と Antigravity Claude は**同じ会話コンテキスト内で混在させることはできません**。グループを使用して適切に分離してください。 + +### 既知の問題 + +Claude Code では、Plan Mode を自動的に終了できません。(通常、ネイティブの Claude API を使用する場合、計画が完了すると Claude Code はユーザーに計画を承認または拒否するオプションをポップアップ表示します。) + +**回避策**: `Shift + Tab` を押して手動で Plan Mode を終了し、計画を承認または拒否するためのレスポンスを入力してください。 + +--- + +## プロジェクト構成 + +``` +sub2api/ +├── backend/ # Go バックエンドサービス +│ ├── cmd/server/ # アプリケーションエントリ +│ ├── internal/ # 内部モジュール +│ │ ├── config/ # 設定 +│ │ ├── model/ # データモデル +│ │ ├── service/ # ビジネスロジック +│ │ ├── handler/ # HTTP ハンドラー +│ │ └── gateway/ # API ゲートウェイコア +│ └── resources/ # 静的リソース +│ +├── frontend/ # Vue 3 フロントエンド +│ └── src/ +│ ├── api/ # API 呼び出し +│ ├── stores/ # 状態管理 +│ ├── views/ # ページコンポーネント +│ └── components/ # 再利用可能なコンポーネント +│ +└── deploy/ # デプロイファイル + ├── docker-compose.yml # Docker Compose 設定 + ├── .env.example # Docker Compose 用環境変数 + ├── config.example.yaml # バイナリデプロイ用フル設定ファイル + └── install.sh # ワンクリックインストールスクリプト +``` + +## 免責事項 + +> **本プロジェクトをご利用の前に、以下をよくお読みください:** +> +> :rotating_light: **利用規約違反のリスク**: 本プロジェクトの使用は Anthropic の利用規約に違反する可能性があります。使用前に Anthropic のユーザー契約をよくお読みください。本プロジェクトの使用に起因するすべてのリスクは、ユーザー自身が負うものとします。 +> +> :book: **免責事項**: 本プロジェクトは技術的な学習および研究目的のみで提供されています。作者は、本プロジェクトの使用によるアカウント停止、サービス中断、その他の損失について一切の責任を負いません。 + +--- + +## スター履歴 + + + + + + Star History Chart + + + +--- + +## ライセンス + +MIT License + +--- + +
+ +**このプロジェクトが役に立ったら、ぜひスターをお願いします!** + +
From c2965c0fb0c86133f3d8f1e48037f95a29def876 Mon Sep 17 00:00:00 2001 From: QTom Date: Wed, 25 Mar 2026 13:05:47 +0800 Subject: [PATCH 04/16] =?UTF-8?q?feat(antigravity):=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E9=9A=90=E7=A7=81=E5=B9=B6=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=90=8E=E5=8F=B0=E6=89=8B=E5=8A=A8=E9=87=8D=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 Antigravity OAuth 隐私设置能力,在账号创建、刷新、导入和后台 Token 刷新路径自动调用 setUserSettings + fetchUserInfo 关闭遥测; 持久化后同步内存 Extra,错误处理改为日志记录。 Made-with: Cursor --- .../internal/handler/admin/account_data.go | 27 +++- .../internal/handler/admin/account_handler.go | 63 ++++++++ .../handler/admin/admin_service_stub_test.go | 8 ++ backend/internal/pkg/antigravity/client.go | 136 ++++++++++++++++++ backend/internal/server/routes/admin.go | 1 + backend/internal/service/admin_service.go | 80 +++++++++++ .../service/antigravity_privacy_service.go | 81 +++++++++++ .../antigravity_privacy_service_test.go | 18 +++ .../internal/service/token_refresh_service.go | 52 ++++++- frontend/src/api/admin/accounts.ts | 13 +- .../admin/account/AccountActionMenu.vue | 7 +- .../components/common/PlatformTypeBadge.vue | 15 +- frontend/src/i18n/locales/en.ts | 3 + frontend/src/i18n/locales/zh.ts | 3 + frontend/src/views/admin/AccountsView.vue | 13 +- 15 files changed, 512 insertions(+), 8 deletions(-) create mode 100644 backend/internal/service/antigravity_privacy_service.go create mode 100644 backend/internal/service/antigravity_privacy_service_test.go diff --git a/backend/internal/handler/admin/account_data.go b/backend/internal/handler/admin/account_data.go index 322ae590..12139b51 100644 --- a/backend/internal/handler/admin/account_data.go +++ b/backend/internal/handler/admin/account_data.go @@ -267,6 +267,9 @@ func (h *AccountHandler) importData(ctx context.Context, req DataImportRequest) } } + // 收集需要异步设置隐私的 Antigravity OAuth 账号 + var privacyAccounts []*service.Account + for i := range dataPayload.Accounts { item := dataPayload.Accounts[i] if err := validateDataAccount(item); err != nil { @@ -314,7 +317,8 @@ func (h *AccountHandler) importData(ctx context.Context, req DataImportRequest) SkipDefaultGroupBind: skipDefaultGroupBind, } - if _, err := h.adminService.CreateAccount(ctx, accountInput); err != nil { + created, err := h.adminService.CreateAccount(ctx, accountInput) + if err != nil { result.AccountFailed++ result.Errors = append(result.Errors, DataImportError{ Kind: "account", @@ -323,9 +327,30 @@ func (h *AccountHandler) importData(ctx context.Context, req DataImportRequest) }) continue } + // 收集 Antigravity OAuth 账号,稍后异步设置隐私 + if created.Platform == service.PlatformAntigravity && created.Type == service.AccountTypeOAuth { + privacyAccounts = append(privacyAccounts, created) + } result.AccountCreated++ } + // 异步设置 Antigravity 隐私,避免大量导入时阻塞请求 + if len(privacyAccounts) > 0 { + adminSvc := h.adminService + go func() { + defer func() { + if r := recover(); r != nil { + slog.Error("import_antigravity_privacy_panic", "recover", r) + } + }() + bgCtx := context.Background() + for _, acc := range privacyAccounts { + adminSvc.ForceAntigravityPrivacy(bgCtx, acc) + } + slog.Info("import_antigravity_privacy_done", "count", len(privacyAccounts)) + }() + } + return result, nil } diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go index 9eaf0bfd..6711abae 100644 --- a/backend/internal/handler/admin/account_handler.go +++ b/backend/internal/handler/admin/account_handler.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "log" + "log/slog" "net/http" "strconv" "strings" @@ -536,6 +537,8 @@ func (h *AccountHandler) Create(c *gin.Context) { if execErr != nil { return nil, execErr } + // Antigravity OAuth: 新账号直接设置隐私 + h.adminService.ForceAntigravityPrivacy(ctx, account) return h.buildAccountResponseWithRuntime(ctx, account), nil }) if err != nil { @@ -883,6 +886,8 @@ func (h *AccountHandler) refreshSingleAccount(ctx context.Context, account *serv // OpenAI OAuth: 刷新成功后检查并设置 privacy_mode h.adminService.EnsureOpenAIPrivacy(ctx, updatedAccount) + // Antigravity OAuth: 刷新成功后检查并设置 privacy_mode + h.adminService.EnsureAntigravityPrivacy(ctx, updatedAccount) return updatedAccount, "", nil } @@ -1154,6 +1159,8 @@ func (h *AccountHandler) BatchCreate(c *gin.Context) { success := 0 failed := 0 results := make([]gin.H, 0, len(req.Accounts)) + // 收集需要异步设置隐私的 Antigravity OAuth 账号 + var privacyAccounts []*service.Account for _, item := range req.Accounts { if item.RateMultiplier != nil && *item.RateMultiplier < 0 { @@ -1196,6 +1203,10 @@ func (h *AccountHandler) BatchCreate(c *gin.Context) { }) continue } + // 收集 Antigravity OAuth 账号,稍后异步设置隐私 + if account.Platform == service.PlatformAntigravity && account.Type == service.AccountTypeOAuth { + privacyAccounts = append(privacyAccounts, account) + } success++ results = append(results, gin.H{ "name": item.Name, @@ -1204,6 +1215,22 @@ func (h *AccountHandler) BatchCreate(c *gin.Context) { }) } + // 异步设置 Antigravity 隐私,避免批量创建时阻塞请求 + if len(privacyAccounts) > 0 { + adminSvc := h.adminService + go func() { + defer func() { + if r := recover(); r != nil { + slog.Error("batch_create_antigravity_privacy_panic", "recover", r) + } + }() + bgCtx := context.Background() + for _, acc := range privacyAccounts { + adminSvc.ForceAntigravityPrivacy(bgCtx, acc) + } + }() + } + return gin.H{ "success": success, "failed": failed, @@ -1869,6 +1896,42 @@ func (h *AccountHandler) GetAvailableModels(c *gin.Context) { response.Success(c, models) } +// SetPrivacy handles setting privacy for a single Antigravity OAuth account +// POST /api/v1/admin/accounts/:id/set-privacy +func (h *AccountHandler) SetPrivacy(c *gin.Context) { + accountID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid account ID") + return + } + account, err := h.adminService.GetAccount(c.Request.Context(), accountID) + if err != nil { + response.NotFound(c, "Account not found") + return + } + if account.Platform != service.PlatformAntigravity || account.Type != service.AccountTypeOAuth { + response.BadRequest(c, "Only Antigravity OAuth accounts support privacy setting") + return + } + mode := h.adminService.ForceAntigravityPrivacy(c.Request.Context(), account) + if mode == "" { + response.BadRequest(c, "Cannot set privacy: missing access_token") + return + } + // 从 DB 重新读取以确保返回最新状态 + updated, err := h.adminService.GetAccount(c.Request.Context(), accountID) + if err != nil { + // 隐私已设置成功但读取失败,回退到内存更新 + if account.Extra == nil { + account.Extra = make(map[string]any) + } + account.Extra["privacy_mode"] = mode + response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account)) + return + } + response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), updated)) +} + // RefreshTier handles refreshing Google One tier for a single account // POST /api/v1/admin/accounts/:id/refresh-tier func (h *AccountHandler) RefreshTier(c *gin.Context) { diff --git a/backend/internal/handler/admin/admin_service_stub_test.go b/backend/internal/handler/admin/admin_service_stub_test.go index 4ed0a623..745c5610 100644 --- a/backend/internal/handler/admin/admin_service_stub_test.go +++ b/backend/internal/handler/admin/admin_service_stub_test.go @@ -445,6 +445,14 @@ func (s *stubAdminService) EnsureOpenAIPrivacy(ctx context.Context, account *ser return "" } +func (s *stubAdminService) EnsureAntigravityPrivacy(ctx context.Context, account *service.Account) string { + return "" +} + +func (s *stubAdminService) ForceAntigravityPrivacy(ctx context.Context, account *service.Account) string { + return "" +} + func (s *stubAdminService) ReplaceUserGroup(ctx context.Context, userID, oldGroupID, newGroupID int64) (*service.ReplaceUserGroupResult, error) { return &service.ReplaceUserGroupResult{MigratedKeys: 0}, nil } diff --git a/backend/internal/pkg/antigravity/client.go b/backend/internal/pkg/antigravity/client.go index f24ff5a8..11138679 100644 --- a/backend/internal/pkg/antigravity/client.go +++ b/backend/internal/pkg/antigravity/client.go @@ -704,3 +704,139 @@ func (c *Client) FetchAvailableModels(ctx context.Context, accessToken, projectI return nil, nil, lastErr } + +// ── Privacy API ────────────────────────────────────────────────────── + +// privacyBaseURL 隐私设置 API 仅使用 daily 端点(与 Antigravity 客户端行为一致) +const privacyBaseURL = antigravityDailyBaseURL + +// SetUserSettingsRequest setUserSettings 请求体 +type SetUserSettingsRequest struct { + UserSettings map[string]any `json:"user_settings"` +} + +// FetchUserInfoRequest fetchUserInfo 请求体 +type FetchUserInfoRequest struct { + Project string `json:"project"` +} + +// FetchUserInfoResponse fetchUserInfo 响应体 +type FetchUserInfoResponse struct { + UserSettings map[string]any `json:"userSettings,omitempty"` + RegionCode string `json:"regionCode,omitempty"` +} + +// IsPrivate 判断隐私是否已设置:userSettings 为空或不含 telemetryEnabled 表示已设置 +func (r *FetchUserInfoResponse) IsPrivate() bool { + if r == nil || r.UserSettings == nil { + return true + } + _, hasTelemetry := r.UserSettings["telemetryEnabled"] + return !hasTelemetry +} + +// SetUserSettingsResponse setUserSettings 响应体 +type SetUserSettingsResponse struct { + UserSettings map[string]any `json:"userSettings,omitempty"` +} + +// IsSuccess 判断 setUserSettings 是否成功:返回 {"userSettings":{}} 且无 telemetryEnabled +func (r *SetUserSettingsResponse) IsSuccess() bool { + if r == nil { + return false + } + // userSettings 为 nil 或空 map 均视为成功 + if r.UserSettings == nil || len(r.UserSettings) == 0 { + return true + } + // 如果包含 telemetryEnabled 字段,说明未成功清除 + _, hasTelemetry := r.UserSettings["telemetryEnabled"] + return !hasTelemetry +} + +// SetUserSettings 调用 setUserSettings API 设置用户隐私,返回解析后的响应 +func (c *Client) SetUserSettings(ctx context.Context, accessToken string) (*SetUserSettingsResponse, error) { + // 发送空 user_settings 以清除隐私设置 + payload := SetUserSettingsRequest{UserSettings: map[string]any{}} + bodyBytes, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("序列化请求失败: %w", err) + } + + apiURL := privacyBaseURL + "/v1internal:setUserSettings" + req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(bodyBytes)) + if err != nil { + return nil, fmt.Errorf("创建请求失败: %w", err) + } + req.Header.Set("Authorization", "Bearer "+accessToken) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "*/*") + req.Header.Set("User-Agent", GetUserAgent()) + req.Header.Set("X-Goog-Api-Client", "gl-node/22.21.1") + req.Host = "daily-cloudcode-pa.googleapis.com" + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("setUserSettings 请求失败: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("读取响应失败: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("setUserSettings 失败 (HTTP %d): %s", resp.StatusCode, string(respBody)) + } + + var result SetUserSettingsResponse + if err := json.Unmarshal(respBody, &result); err != nil { + return nil, fmt.Errorf("响应解析失败: %w", err) + } + + return &result, nil +} + +// FetchUserInfo 调用 fetchUserInfo API 获取用户隐私设置状态 +func (c *Client) FetchUserInfo(ctx context.Context, accessToken, projectID string) (*FetchUserInfoResponse, error) { + reqBody := FetchUserInfoRequest{Project: projectID} + bodyBytes, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("序列化请求失败: %w", err) + } + + apiURL := privacyBaseURL + "/v1internal:fetchUserInfo" + req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(bodyBytes)) + if err != nil { + return nil, fmt.Errorf("创建请求失败: %w", err) + } + req.Header.Set("Authorization", "Bearer "+accessToken) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "*/*") + req.Header.Set("User-Agent", GetUserAgent()) + req.Header.Set("X-Goog-Api-Client", "gl-node/22.21.1") + req.Host = "daily-cloudcode-pa.googleapis.com" + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("fetchUserInfo 请求失败: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("读取响应失败: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("fetchUserInfo 失败 (HTTP %d): %s", resp.StatusCode, string(respBody)) + } + + var result FetchUserInfoResponse + if err := json.Unmarshal(respBody, &result); err != nil { + return nil, fmt.Errorf("响应解析失败: %w", err) + } + + return &result, nil +} diff --git a/backend/internal/server/routes/admin.go b/backend/internal/server/routes/admin.go index c4ddeab3..6fd239bb 100644 --- a/backend/internal/server/routes/admin.go +++ b/backend/internal/server/routes/admin.go @@ -257,6 +257,7 @@ func registerAccountRoutes(admin *gin.RouterGroup, h *handler.Handlers) { accounts.POST("/:id/test", h.Admin.Account.Test) accounts.POST("/:id/recover-state", h.Admin.Account.RecoverState) accounts.POST("/:id/refresh", h.Admin.Account.Refresh) + accounts.POST("/:id/set-privacy", h.Admin.Account.SetPrivacy) accounts.POST("/:id/refresh-tier", h.Admin.Account.RefreshTier) accounts.GET("/:id/stats", h.Admin.Account.GetStats) accounts.POST("/:id/clear-error", h.Admin.Account.ClearError) diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index ed85ee34..3ac6a144 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -65,6 +65,10 @@ type AdminService interface { SetAccountError(ctx context.Context, id int64, errorMsg string) error // EnsureOpenAIPrivacy 检查 OpenAI OAuth 账号 privacy_mode,未设置则尝试关闭训练数据共享并持久化。 EnsureOpenAIPrivacy(ctx context.Context, account *Account) string + // EnsureAntigravityPrivacy 检查 Antigravity OAuth 账号 privacy_mode,未设置则调用 setUserSettings 并持久化。 + EnsureAntigravityPrivacy(ctx context.Context, account *Account) string + // ForceAntigravityPrivacy 强制重新设置 Antigravity OAuth 账号隐私,无论当前状态。 + ForceAntigravityPrivacy(ctx context.Context, account *Account) string SetAccountSchedulable(ctx context.Context, id int64, schedulable bool) (*Account, error) BulkUpdateAccounts(ctx context.Context, input *BulkUpdateAccountsInput) (*BulkUpdateAccountsResult, error) CheckMixedChannelRisk(ctx context.Context, currentAccountID int64, currentAccountPlatform string, groupIDs []int64) error @@ -2661,3 +2665,79 @@ func (s *adminServiceImpl) EnsureOpenAIPrivacy(ctx context.Context, account *Acc _ = s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{"privacy_mode": mode}) return mode } + +// EnsureAntigravityPrivacy 检查 Antigravity OAuth 账号隐私状态。 +// 如果 Extra["privacy_mode"] 已存在(无论成功或失败),直接跳过。 +// 仅对从未设置过隐私的账号执行 setUserSettings + fetchUserInfo 流程。 +// 用户可通过前端 ForceAntigravityPrivacy(SetPrivacy 按钮)强制重新设置。 +func (s *adminServiceImpl) EnsureAntigravityPrivacy(ctx context.Context, account *Account) string { + if account.Platform != PlatformAntigravity || account.Type != AccountTypeOAuth { + return "" + } + // 已设置过则跳过(无论成功或失败),用户可通过 Force 手动重试 + if account.Extra != nil { + if existing, ok := account.Extra["privacy_mode"].(string); ok && existing != "" { + return existing + } + } + + token, _ := account.Credentials["access_token"].(string) + if token == "" { + return "" + } + + projectID, _ := account.Credentials["project_id"].(string) + + var proxyURL string + if account.ProxyID != nil { + if p, err := s.proxyRepo.GetByID(ctx, *account.ProxyID); err == nil && p != nil { + proxyURL = p.URL() + } + } + + mode := setAntigravityPrivacy(ctx, token, projectID, proxyURL) + if mode == "" { + return "" + } + + if err := s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{"privacy_mode": mode}); err != nil { + logger.LegacyPrintf("service.admin", "update_antigravity_privacy_mode_failed: account_id=%d err=%v", account.ID, err) + return mode + } + applyAntigravityPrivacyMode(account, mode) + return mode +} + +// ForceAntigravityPrivacy 强制重新设置 Antigravity OAuth 账号隐私,无论当前状态。 +func (s *adminServiceImpl) ForceAntigravityPrivacy(ctx context.Context, account *Account) string { + if account.Platform != PlatformAntigravity || account.Type != AccountTypeOAuth { + return "" + } + + token, _ := account.Credentials["access_token"].(string) + if token == "" { + return "" + } + + projectID, _ := account.Credentials["project_id"].(string) + + var proxyURL string + if account.ProxyID != nil { + if p, err := s.proxyRepo.GetByID(ctx, *account.ProxyID); err == nil && p != nil { + proxyURL = p.URL() + } + } + + mode := setAntigravityPrivacy(ctx, token, projectID, proxyURL) + if mode == "" { + return "" + } + + if err := s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{"privacy_mode": mode}); err != nil { + logger.LegacyPrintf("service.admin", "force_update_antigravity_privacy_mode_failed: account_id=%d err=%v", account.ID, err) + return mode + } + applyAntigravityPrivacyMode(account, mode) + return mode +} + diff --git a/backend/internal/service/antigravity_privacy_service.go b/backend/internal/service/antigravity_privacy_service.go new file mode 100644 index 00000000..50fe07f6 --- /dev/null +++ b/backend/internal/service/antigravity_privacy_service.go @@ -0,0 +1,81 @@ +package service + +import ( + "context" + "log/slog" + "strings" + "time" + + "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity" +) + +const ( + AntigravityPrivacySet = "privacy_set" + AntigravityPrivacyFailed = "privacy_set_failed" +) + +// setAntigravityPrivacy 调用 Antigravity API 设置隐私并验证结果。 +// 流程: +// 1. setUserSettings 清空设置 → 检查返回值 {"userSettings":{}} +// 2. fetchUserInfo 二次验证隐私是否已生效(需要 project_id) +// +// 返回 privacy_mode 值:"privacy_set" 成功,"privacy_set_failed" 失败,空串表示无法执行。 +func setAntigravityPrivacy(ctx context.Context, accessToken, projectID, proxyURL string) string { + if accessToken == "" { + return "" + } + + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + client, err := antigravity.NewClient(proxyURL) + if err != nil { + slog.Warn("antigravity_privacy_client_error", "error", err.Error()) + return AntigravityPrivacyFailed + } + + // 第 1 步:调用 setUserSettings,检查返回值 + setResp, err := client.SetUserSettings(ctx, accessToken) + if err != nil { + slog.Warn("antigravity_privacy_set_failed", "error", err.Error()) + return AntigravityPrivacyFailed + } + if !setResp.IsSuccess() { + slog.Warn("antigravity_privacy_set_response_not_empty", + "user_settings", setResp.UserSettings, + ) + return AntigravityPrivacyFailed + } + + // 第 2 步:调用 fetchUserInfo 二次验证隐私是否已生效 + if strings.TrimSpace(projectID) == "" { + slog.Warn("antigravity_privacy_missing_project_id") + return AntigravityPrivacyFailed + } + userInfo, err := client.FetchUserInfo(ctx, accessToken, projectID) + if err != nil { + slog.Warn("antigravity_privacy_verify_failed", "error", err.Error()) + return AntigravityPrivacyFailed + } + if !userInfo.IsPrivate() { + slog.Warn("antigravity_privacy_verify_not_private", + "user_settings", userInfo.UserSettings, + ) + return AntigravityPrivacyFailed + } + + slog.Info("antigravity_privacy_set_success") + return AntigravityPrivacySet +} + +func applyAntigravityPrivacyMode(account *Account, mode string) { + if account == nil || strings.TrimSpace(mode) == "" { + return + } + extra := make(map[string]any, len(account.Extra)+1) + for k, v := range account.Extra { + extra[k] = v + } + extra["privacy_mode"] = mode + account.Extra = extra +} diff --git a/backend/internal/service/antigravity_privacy_service_test.go b/backend/internal/service/antigravity_privacy_service_test.go new file mode 100644 index 00000000..e1e41334 --- /dev/null +++ b/backend/internal/service/antigravity_privacy_service_test.go @@ -0,0 +1,18 @@ +//go:build unit + +package service + +import "testing" + +func TestApplyAntigravityPrivacyMode_SetsInMemoryExtra(t *testing.T) { + account := &Account{} + + applyAntigravityPrivacyMode(account, AntigravityPrivacySet) + + if account.Extra == nil { + t.Fatal("expected account.Extra to be initialized") + } + if got := account.Extra["privacy_mode"]; got != AntigravityPrivacySet { + t.Fatalf("expected privacy_mode %q, got %v", AntigravityPrivacySet, got) + } +} diff --git a/backend/internal/service/token_refresh_service.go b/backend/internal/service/token_refresh_service.go index 24b7424f..f3d7ce58 100644 --- a/backend/internal/service/token_refresh_service.go +++ b/backend/internal/service/token_refresh_service.go @@ -128,7 +128,7 @@ func (s *TokenRefreshService) Start() { ) } -// Stop 停止刷新服务 +// Stop 停止刷新服务(可安全多次调用) func (s *TokenRefreshService) Stop() { close(s.stopCh) s.wg.Wait() @@ -404,6 +404,8 @@ func (s *TokenRefreshService) postRefreshActions(ctx context.Context, account *A } // OpenAI OAuth: 刷新成功后,检查是否已设置 privacy_mode,未设置则尝试关闭训练数据共享 s.ensureOpenAIPrivacy(ctx, account) + // Antigravity OAuth: 刷新成功后,检查是否已设置 privacy_mode,未设置则调用 setUserSettings + s.ensureAntigravityPrivacy(ctx, account) } // errRefreshSkipped 表示刷新被跳过(锁竞争或已被其他路径刷新),不计入 failed 或 refreshed @@ -477,3 +479,51 @@ func (s *TokenRefreshService) ensureOpenAIPrivacy(ctx context.Context, account * ) } } + +// ensureAntigravityPrivacy 后台刷新中检查 Antigravity OAuth 账号隐私状态。 +// 仅做 Extra["privacy_mode"] 存在性检查,不发起 HTTP 请求,避免每轮循环产生额外网络开销。 +// 用户可通过前端 SetPrivacy 按钮强制重新设置。 +func (s *TokenRefreshService) ensureAntigravityPrivacy(ctx context.Context, account *Account) { + if account.Platform != PlatformAntigravity || account.Type != AccountTypeOAuth { + return + } + // 已设置过(无论成功或失败)则跳过,不发 HTTP + if account.Extra != nil { + if _, ok := account.Extra["privacy_mode"]; ok { + return + } + } + + token, _ := account.Credentials["access_token"].(string) + if token == "" { + return + } + + projectID, _ := account.Credentials["project_id"].(string) + + var proxyURL string + if account.ProxyID != nil && s.proxyRepo != nil { + if p, err := s.proxyRepo.GetByID(ctx, *account.ProxyID); err == nil && p != nil { + proxyURL = p.URL() + } + } + + mode := setAntigravityPrivacy(ctx, token, projectID, proxyURL) + if mode == "" { + return + } + + if err := s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{"privacy_mode": mode}); err != nil { + slog.Warn("token_refresh.update_antigravity_privacy_mode_failed", + "account_id", account.ID, + "error", err, + ) + } else { + applyAntigravityPrivacyMode(account, mode) + slog.Info("token_refresh.antigravity_privacy_mode_set", + "account_id", account.ID, + "privacy_mode", mode, + ) + } +} + diff --git a/frontend/src/api/admin/accounts.ts b/frontend/src/api/admin/accounts.ts index ece5a30f..fd93fe7e 100644 --- a/frontend/src/api/admin/accounts.ts +++ b/frontend/src/api/admin/accounts.ts @@ -627,6 +627,16 @@ export async function batchRefresh(accountIds: number[]): Promise { + const { data } = await apiClient.post(`/admin/accounts/${id}/set-privacy`) + return data +} + export const accountsAPI = { list, listWithEtag, @@ -663,7 +673,8 @@ export const accountsAPI = { importData, getAntigravityDefaultModelMapping, batchClearError, - batchRefresh + batchRefresh, + setPrivacy } export default accountsAPI diff --git a/frontend/src/components/admin/account/AccountActionMenu.vue b/frontend/src/components/admin/account/AccountActionMenu.vue index f5bc5aa0..e682ddaf 100644 --- a/frontend/src/components/admin/account/AccountActionMenu.vue +++ b/frontend/src/components/admin/account/AccountActionMenu.vue @@ -32,6 +32,10 @@ {{ t('admin.accounts.refreshToken') }} +
- + {{ planLabel }} { return 'Pro' case 'free': return 'Free' + case 'abnormal': + return t('admin.accounts.subscriptionAbnormal') default: return props.planType } @@ -139,6 +141,13 @@ const typeClass = computed(() => { return 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400' }) +const planBadgeClass = computed(() => { + if (props.planType && props.planType.toLowerCase() === 'abnormal') { + return 'bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400' + } + return typeClass.value +}) + // Privacy badge — shows different states for OpenAI/Antigravity OAuth privacy setting const privacyBadge = computed(() => { if (props.type !== 'oauth' || !props.privacyMode) return null diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index ab34945f..334de1f1 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -1987,6 +1987,7 @@ export default { privacyAntigravitySet: 'Telemetry and marketing emails disabled', privacyAntigravityFailed: 'Privacy setting failed', setPrivacy: 'Set Privacy', + subscriptionAbnormal: 'Abnormal', // Capacity status tooltips capacity: { windowCost: { diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 6120f94e..d296f436 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -2025,6 +2025,7 @@ export default { privacyAntigravitySet: '已关闭遥测和营销邮件', privacyAntigravityFailed: '隐私设置失败', setPrivacy: '设置隐私', + subscriptionAbnormal: '异常', // 容量状态提示 capacity: { windowCost: { From 975e6b15635a3284505457ddb1e2bda895d3abc4 Mon Sep 17 00:00:00 2001 From: QTom Date: Wed, 25 Mar 2026 19:03:12 +0800 Subject: [PATCH 06/16] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20golangci-lint?= =?UTF-8?q?=20=E6=8A=A5=E5=91=8A=E7=9A=84=205=20=E4=B8=AA=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - gofmt: 修复 admin_service/antigravity_oauth_service/token_refresh_service 格式 - staticcheck S1009: 移除 SetUserSettingsResponse.IsSuccess 中冗余的 nil 检查 - unused: 将仅测试使用的 applyAntigravitySubscriptionResult 移至测试文件 Made-with: Cursor --- backend/internal/pkg/antigravity/client.go | 2 +- backend/internal/service/admin_service.go | 1 - .../service/antigravity_oauth_service.go | 2 +- .../antigravity_privacy_service_test.go | 28 ++++++++++++++++++- .../antigravity_subscription_service.go | 24 ---------------- .../internal/service/token_refresh_service.go | 1 - 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/backend/internal/pkg/antigravity/client.go b/backend/internal/pkg/antigravity/client.go index 880db797..fdd7fea1 100644 --- a/backend/internal/pkg/antigravity/client.go +++ b/backend/internal/pkg/antigravity/client.go @@ -767,7 +767,7 @@ func (r *SetUserSettingsResponse) IsSuccess() bool { return false } // userSettings 为 nil 或空 map 均视为成功 - if r.UserSettings == nil || len(r.UserSettings) == 0 { + if len(r.UserSettings) == 0 { return true } // 如果包含 telemetryEnabled 字段,说明未成功清除 diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index 3ac6a144..10f71bbc 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -2740,4 +2740,3 @@ func (s *adminServiceImpl) ForceAntigravityPrivacy(ctx context.Context, account applyAntigravityPrivacyMode(account, mode) return mode } - diff --git a/backend/internal/service/antigravity_oauth_service.go b/backend/internal/service/antigravity_oauth_service.go index e0caae4c..a300d898 100644 --- a/backend/internal/service/antigravity_oauth_service.go +++ b/backend/internal/service/antigravity_oauth_service.go @@ -322,7 +322,7 @@ func (s *AntigravityOAuthService) RefreshAccountToken(ctx context.Context, accou // loadCodeAssistResult 封装 loadProjectIDWithRetry 的返回结果, // 同时携带从 LoadCodeAssist 响应中提取的 plan_type 信息。 type loadCodeAssistResult struct { - ProjectID string + ProjectID string Subscription *AntigravitySubscriptionResult } diff --git a/backend/internal/service/antigravity_privacy_service_test.go b/backend/internal/service/antigravity_privacy_service_test.go index 11f05ab9..893500a6 100644 --- a/backend/internal/service/antigravity_privacy_service_test.go +++ b/backend/internal/service/antigravity_privacy_service_test.go @@ -2,7 +2,33 @@ package service -import "testing" +import ( + "testing" +) + +func applyAntigravitySubscriptionResult(account *Account, result AntigravitySubscriptionResult) (map[string]any, map[string]any) { + credentials := make(map[string]any) + for k, v := range account.Credentials { + credentials[k] = v + } + credentials["plan_type"] = result.PlanType + + extra := make(map[string]any) + for k, v := range account.Extra { + extra[k] = v + } + if result.SubscriptionStatus != "" { + extra["subscription_status"] = result.SubscriptionStatus + } else { + delete(extra, "subscription_status") + } + if result.SubscriptionError != "" { + extra["subscription_error"] = result.SubscriptionError + } else { + delete(extra, "subscription_error") + } + return credentials, extra +} func TestApplyAntigravityPrivacyMode_SetsInMemoryExtra(t *testing.T) { account := &Account{} diff --git a/backend/internal/service/antigravity_subscription_service.go b/backend/internal/service/antigravity_subscription_service.go index 46904427..04559be8 100644 --- a/backend/internal/service/antigravity_subscription_service.go +++ b/backend/internal/service/antigravity_subscription_service.go @@ -36,27 +36,3 @@ func NormalizeAntigravitySubscription(resp *antigravity.LoadCodeAssistResponse) PlanType: antigravity.TierIDToPlanType(tierID), } } - -func applyAntigravitySubscriptionResult(account *Account, result AntigravitySubscriptionResult) (map[string]any, map[string]any) { - credentials := make(map[string]any) - for k, v := range account.Credentials { - credentials[k] = v - } - credentials["plan_type"] = result.PlanType - - extra := make(map[string]any) - for k, v := range account.Extra { - extra[k] = v - } - if result.SubscriptionStatus != "" { - extra["subscription_status"] = result.SubscriptionStatus - } else { - delete(extra, "subscription_status") - } - if result.SubscriptionError != "" { - extra["subscription_error"] = result.SubscriptionError - } else { - delete(extra, "subscription_error") - } - return credentials, extra -} diff --git a/backend/internal/service/token_refresh_service.go b/backend/internal/service/token_refresh_service.go index f3d7ce58..ac14aa56 100644 --- a/backend/internal/service/token_refresh_service.go +++ b/backend/internal/service/token_refresh_service.go @@ -526,4 +526,3 @@ func (s *TokenRefreshService) ensureAntigravityPrivacy(ctx context.Context, acco ) } } - From 587557121536ad6ffa62b9ca5a255caebadb6add Mon Sep 17 00:00:00 2001 From: QTom Date: Sat, 21 Mar 2026 18:45:00 +0800 Subject: [PATCH 07/16] =?UTF-8?q?fix(ratelimit):=20OpenAI=20401=20token=5F?= =?UTF-8?q?invalidated/token=5Frevoked=20=E5=8F=8A=20402=20deactivated=5Fw?= =?UTF-8?q?orkspace=20=E6=A0=87=E8=AE=B0=E8=B4=A6=E5=8F=B7=E5=BC=82?= =?UTF-8?q?=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 401 token_invalidated / token_revoked: OAuth token 被永久作废,跳过临时不可调度逻辑,直接 SetError - 402 deactivated_workspace: 解析 detail.code 字段,标记工作区已停用 Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/internal/service/ratelimit_service.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/backend/internal/service/ratelimit_service.go b/backend/internal/service/ratelimit_service.go index afe5816d..aa0ae200 100644 --- a/backend/internal/service/ratelimit_service.go +++ b/backend/internal/service/ratelimit_service.go @@ -12,6 +12,7 @@ import ( "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/pkg/logger" + "github.com/tidwall/gjson" ) // RateLimitService 处理限流和过载状态管理 @@ -149,6 +150,17 @@ func (s *RateLimitService) HandleUpstreamError(ctx context.Context, account *Acc } // 其他 400 错误(如参数问题)不处理,不禁用账号 case 401: + // OpenAI: token_invalidated / token_revoked 表示 token 被永久作废(非过期),直接标记 error + openai401Code := extractUpstreamErrorCode(responseBody) + if account.Platform == PlatformOpenAI && (openai401Code == "token_invalidated" || openai401Code == "token_revoked") { + msg := "Token revoked (401): account authentication permanently revoked" + if upstreamMsg != "" { + msg = "Token revoked (401): " + upstreamMsg + } + s.handleAuthError(ctx, account, msg) + shouldDisable = true + break + } // OAuth 账号在 401 错误时临时不可调度(给 token 刷新窗口);非 OAuth 账号保持原有 SetError 行为。 // Antigravity 除外:其 401 由 applyErrorPolicy 的 temp_unschedulable_rules 自行控制。 if account.Type == AccountTypeOAuth && account.Platform != PlatformAntigravity { @@ -192,6 +204,13 @@ func (s *RateLimitService) HandleUpstreamError(ctx context.Context, account *Acc shouldDisable = true } case 402: + // OpenAI: deactivated_workspace 表示工作区已停用,直接标记 error + if account.Platform == PlatformOpenAI && gjson.GetBytes(responseBody, "detail.code").String() == "deactivated_workspace" { + msg := "Workspace deactivated (402): workspace has been deactivated" + s.handleAuthError(ctx, account, msg) + shouldDisable = true + break + } // 支付要求:余额不足或计费问题,停止调度 msg := "Payment required (402): insufficient balance or billing issue" if upstreamMsg != "" { From 7c6dc9dda88d3ac7549a29865f71adea6de781e1 Mon Sep 17 00:00:00 2001 From: Dave King Date: Wed, 25 Mar 2026 12:19:17 +0000 Subject: [PATCH 08/16] fix: add account and proxy details to gateway.forward_failed log The forward_failed error log only included account_id, making it difficult to identify which account and proxy caused the failure without querying the database. Add account_name, account_platform, and proxy details (id, name, host, port) to the log fields. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/internal/handler/gateway_handler.go | 34 ++++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/backend/internal/handler/gateway_handler.go b/backend/internal/handler/gateway_handler.go index b9285c04..a0d8b2e9 100644 --- a/backend/internal/handler/gateway_handler.go +++ b/backend/internal/handler/gateway_handler.go @@ -422,11 +422,24 @@ func (h *GatewayHandler) Messages(c *gin.Context) { } } wroteFallback := h.ensureForwardErrorResponse(c, streamStarted) - reqLog.Error("gateway.forward_failed", + forwardFailedFields := []zap.Field{ zap.Int64("account_id", account.ID), + zap.String("account_name", account.Name), + zap.String("account_platform", account.Platform), zap.Bool("fallback_error_response_written", wroteFallback), zap.Error(err), - ) + } + if account.Proxy != nil { + forwardFailedFields = append(forwardFailedFields, + zap.Int64("proxy_id", account.Proxy.ID), + zap.String("proxy_name", account.Proxy.Name), + zap.String("proxy_host", account.Proxy.Host), + zap.Int("proxy_port", account.Proxy.Port), + ) + } else if account.ProxyID != nil { + forwardFailedFields = append(forwardFailedFields, zap.Int64p("proxy_id", account.ProxyID)) + } + reqLog.Error("gateway.forward_failed", forwardFailedFields...) return } @@ -741,11 +754,24 @@ func (h *GatewayHandler) Messages(c *gin.Context) { } } wroteFallback := h.ensureForwardErrorResponse(c, streamStarted) - reqLog.Error("gateway.forward_failed", + forwardFailedFields := []zap.Field{ zap.Int64("account_id", account.ID), + zap.String("account_name", account.Name), + zap.String("account_platform", account.Platform), zap.Bool("fallback_error_response_written", wroteFallback), zap.Error(err), - ) + } + if account.Proxy != nil { + forwardFailedFields = append(forwardFailedFields, + zap.Int64("proxy_id", account.Proxy.ID), + zap.String("proxy_name", account.Proxy.Name), + zap.String("proxy_host", account.Proxy.Host), + zap.Int("proxy_port", account.Proxy.Port), + ) + } else if account.ProxyID != nil { + forwardFailedFields = append(forwardFailedFields, zap.Int64p("proxy_id", account.ProxyID)) + } + reqLog.Error("gateway.forward_failed", forwardFailedFields...) return } From b20e142249fd752e2a5471d4cc21d3b4b90f4739 Mon Sep 17 00:00:00 2001 From: shaw Date: Thu, 26 Mar 2026 10:22:03 +0800 Subject: [PATCH 09/16] =?UTF-8?q?feat:=20=E7=BD=91=E5=85=B3=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E5=A4=B4=20wire=20casing=20=E4=BF=9D=E6=8C=81?= =?UTF-8?q?=E3=80=81=E8=BD=AC=E5=8F=91=E8=A1=8C=E4=B8=BA=E5=BC=80=E5=85=B3?= =?UTF-8?q?=E3=80=81=E8=B0=83=E8=AF=95=E6=97=A5=E5=BF=97=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E5=8F=8A=20accept-encoding=20=E6=81=A2=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 header_util.go,通过 setHeaderRaw/getHeaderRaw/addHeaderRaw 绕过 Go 的 canonical-case 规范化,保持真实 Claude CLI 抓包的请求头大小写 (如 "x-app" 而非 "X-App","X-Stainless-OS" 而非 "X-Stainless-Os") - 新增管理后台开关:指纹统一化(默认开启)和 metadata 透传(默认关闭), 使用 atomic.Value + singleflight 缓存模式,60s TTL - 调试日志从控制台 body 打印升级为文件级完整快照 (按真实 wire 顺序输出 headers + 格式化 JSON body + 上下文元数据) - 恢复 accept-encoding 到白名单,在 http_upstream.go 新增 decompressResponseBody 处理 gzip/brotli/deflate 解压(Go 显式设置 Accept-Encoding 时不会自动解压) - OAuth 服务 axios UA 从 1.8.4 更新至 1.13.6 - 测试断言改用 getHeaderRaw 适配 raw header 存储方式 --- .../internal/handler/admin/setting_handler.go | 26 ++ backend/internal/handler/dto/settings.go | 4 + .../repository/claude_oauth_service.go | 4 +- backend/internal/repository/http_upstream.go | 63 ++++ backend/internal/server/api_contract_test.go | 2 + backend/internal/service/domain_constants.go | 6 + ...teway_anthropic_apikey_passthrough_test.go | 28 +- .../service/gateway_debug_env_test.go | 23 +- backend/internal/service/gateway_service.go | 322 +++++++++++------- backend/internal/service/header_util.go | 157 +++++++++ backend/internal/service/identity_service.go | 17 +- backend/internal/service/setting_service.go | 83 +++++ backend/internal/service/settings_view.go | 4 + frontend/src/api/admin/settings.ts | 6 + frontend/src/i18n/locales/en.ts | 8 + frontend/src/i18n/locales/zh.ts | 8 + frontend/src/views/admin/SettingsView.vue | 48 ++- 17 files changed, 655 insertions(+), 154 deletions(-) create mode 100644 backend/internal/service/header_util.go diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go index b5a7eb77..f57244fb 100644 --- a/backend/internal/handler/admin/setting_handler.go +++ b/backend/internal/handler/admin/setting_handler.go @@ -129,6 +129,8 @@ func (h *SettingHandler) GetSettings(c *gin.Context) { MaxClaudeCodeVersion: settings.MaxClaudeCodeVersion, AllowUngroupedKeyScheduling: settings.AllowUngroupedKeyScheduling, BackendModeEnabled: settings.BackendModeEnabled, + EnableFingerprintUnification: settings.EnableFingerprintUnification, + EnableMetadataPassthrough: settings.EnableMetadataPassthrough, }) } @@ -209,6 +211,10 @@ type UpdateSettingsRequest struct { // Backend Mode BackendModeEnabled bool `json:"backend_mode_enabled"` + + // Gateway forwarding behavior + EnableFingerprintUnification *bool `json:"enable_fingerprint_unification"` + EnableMetadataPassthrough *bool `json:"enable_metadata_passthrough"` } // UpdateSettings 更新系统设置 @@ -601,6 +607,18 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { } return previousSettings.OpsMetricsIntervalSeconds }(), + EnableFingerprintUnification: func() bool { + if req.EnableFingerprintUnification != nil { + return *req.EnableFingerprintUnification + } + return previousSettings.EnableFingerprintUnification + }(), + EnableMetadataPassthrough: func() bool { + if req.EnableMetadataPassthrough != nil { + return *req.EnableMetadataPassthrough + } + return previousSettings.EnableMetadataPassthrough + }(), } if err := h.settingService.UpdateSettings(c.Request.Context(), settings); err != nil { @@ -679,6 +697,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { MaxClaudeCodeVersion: updatedSettings.MaxClaudeCodeVersion, AllowUngroupedKeyScheduling: updatedSettings.AllowUngroupedKeyScheduling, BackendModeEnabled: updatedSettings.BackendModeEnabled, + EnableFingerprintUnification: updatedSettings.EnableFingerprintUnification, + EnableMetadataPassthrough: updatedSettings.EnableMetadataPassthrough, }) } @@ -851,6 +871,12 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings, if before.CustomMenuItems != after.CustomMenuItems { changed = append(changed, "custom_menu_items") } + if before.EnableFingerprintUnification != after.EnableFingerprintUnification { + changed = append(changed, "enable_fingerprint_unification") + } + if before.EnableMetadataPassthrough != after.EnableMetadataPassthrough { + changed = append(changed, "enable_metadata_passthrough") + } return changed } diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go index 7ea34aa0..59d7f688 100644 --- a/backend/internal/handler/dto/settings.go +++ b/backend/internal/handler/dto/settings.go @@ -94,6 +94,10 @@ type SystemSettings struct { // Backend Mode BackendModeEnabled bool `json:"backend_mode_enabled"` + + // Gateway forwarding behavior + EnableFingerprintUnification bool `json:"enable_fingerprint_unification"` + EnableMetadataPassthrough bool `json:"enable_metadata_passthrough"` } type DefaultSubscriptionSetting struct { diff --git a/backend/internal/repository/claude_oauth_service.go b/backend/internal/repository/claude_oauth_service.go index b754bd55..fee5c645 100644 --- a/backend/internal/repository/claude_oauth_service.go +++ b/backend/internal/repository/claude_oauth_service.go @@ -212,7 +212,7 @@ func (s *claudeOAuthService) ExchangeCodeForToken(ctx context.Context, code, cod SetContext(ctx). SetHeader("Accept", "application/json, text/plain, */*"). SetHeader("Content-Type", "application/json"). - SetHeader("User-Agent", "axios/1.8.4"). + SetHeader("User-Agent", "axios/1.13.6"). SetBody(reqBody). SetSuccessResult(&tokenResp). Post(s.tokenURL) @@ -250,7 +250,7 @@ func (s *claudeOAuthService) RefreshToken(ctx context.Context, refreshToken, pro SetContext(ctx). SetHeader("Accept", "application/json, text/plain, */*"). SetHeader("Content-Type", "application/json"). - SetHeader("User-Agent", "axios/1.8.4"). + SetHeader("User-Agent", "axios/1.13.6"). SetBody(reqBody). SetSuccessResult(&tokenResp). Post(s.tokenURL) diff --git a/backend/internal/repository/http_upstream.go b/backend/internal/repository/http_upstream.go index a4674c1a..12523a91 100644 --- a/backend/internal/repository/http_upstream.go +++ b/backend/internal/repository/http_upstream.go @@ -1,6 +1,8 @@ package repository import ( + "compress/flate" + "compress/gzip" "errors" "fmt" "io" @@ -13,6 +15,8 @@ import ( "sync/atomic" "time" + "github.com/andybalholm/brotli" + "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/pkg/proxyurl" "github.com/Wei-Shaw/sub2api/internal/pkg/proxyutil" @@ -143,6 +147,9 @@ func (s *httpUpstreamService) Do(req *http.Request, proxyURL string, accountID i return nil, err } + // 如果上游返回了压缩内容,解压后再交给业务层 + decompressResponseBody(resp) + // 包装响应体,在关闭时自动减少计数并更新时间戳 // 这确保了流式响应(如 SSE)在完全读取前不会被淘汰 resp.Body = wrapTrackedBody(resp.Body, func() { @@ -218,6 +225,9 @@ func (s *httpUpstreamService) DoWithTLS(req *http.Request, proxyURL string, acco slog.Debug("tls_fingerprint_request_success", "account_id", accountID, "status", resp.StatusCode) + // 如果上游返回了压缩内容,解压后再交给业务层 + decompressResponseBody(resp) + // 包装响应体,在关闭时自动减少计数并更新时间戳 resp.Body = wrapTrackedBody(resp.Body, func() { atomic.AddInt64(&entry.inFlight, -1) @@ -884,3 +894,56 @@ func wrapTrackedBody(body io.ReadCloser, onClose func()) io.ReadCloser { } return &trackedBody{ReadCloser: body, onClose: onClose} } + +// decompressResponseBody 根据 Content-Encoding 解压响应体。 +// 当请求显式设置了 accept-encoding 时,Go 的 Transport 不会自动解压,需要手动处理。 +// 解压成功后会删除 Content-Encoding 和 Content-Length header(长度已不准确)。 +func decompressResponseBody(resp *http.Response) { + if resp == nil || resp.Body == nil { + return + } + ce := strings.ToLower(strings.TrimSpace(resp.Header.Get("Content-Encoding"))) + if ce == "" { + return + } + + var reader io.Reader + switch ce { + case "gzip": + gr, err := gzip.NewReader(resp.Body) + if err != nil { + return // 解压失败,保持原样 + } + reader = gr + case "br": + reader = brotli.NewReader(resp.Body) + case "deflate": + reader = flate.NewReader(resp.Body) + default: + return + } + + originalBody := resp.Body + resp.Body = &decompressedBody{reader: reader, closer: originalBody} + resp.Header.Del("Content-Encoding") + resp.Header.Del("Content-Length") // 解压后长度不确定 + resp.ContentLength = -1 +} + +// decompressedBody 组合解压 reader 和原始 body 的 close。 +type decompressedBody struct { + reader io.Reader + closer io.Closer +} + +func (d *decompressedBody) Read(p []byte) (int, error) { + return d.reader.Read(p) +} + +func (d *decompressedBody) Close() error { + // 如果 reader 本身也是 Closer(如 gzip.Reader),先关闭它 + if rc, ok := d.reader.(io.Closer); ok { + _ = rc.Close() + } + return d.closer.Close() +} diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go index 880fe8a0..ac4e05de 100644 --- a/backend/internal/server/api_contract_test.go +++ b/backend/internal/server/api_contract_test.go @@ -540,6 +540,8 @@ func TestAPIContracts(t *testing.T) { "max_claude_code_version": "", "allow_ungrouped_key_scheduling": false, "backend_mode_enabled": false, + "enable_fingerprint_unification": true, + "enable_metadata_passthrough": false, "custom_menu_items": [], "custom_endpoints": [] } diff --git a/backend/internal/service/domain_constants.go b/backend/internal/service/domain_constants.go index 4ae5a469..ecac0db0 100644 --- a/backend/internal/service/domain_constants.go +++ b/backend/internal/service/domain_constants.go @@ -235,6 +235,12 @@ const ( // SettingKeyBackendModeEnabled Backend 模式:禁用用户注册和自助服务,仅管理员可登录 SettingKeyBackendModeEnabled = "backend_mode_enabled" + + // Gateway Forwarding Behavior + // SettingKeyEnableFingerprintUnification 是否统一 OAuth 账号的 X-Stainless-* 指纹头(默认 true) + SettingKeyEnableFingerprintUnification = "enable_fingerprint_unification" + // SettingKeyEnableMetadataPassthrough 是否透传客户端原始 metadata.user_id(默认 false) + SettingKeyEnableMetadataPassthrough = "enable_metadata_passthrough" ) // AdminAPIKeyPrefix is the prefix for admin API keys (distinct from user "sk-" keys). diff --git a/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go b/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go index a01dd02a..f4e1b533 100644 --- a/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go +++ b/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go @@ -175,13 +175,13 @@ func TestGatewayService_AnthropicAPIKeyPassthrough_ForwardStreamPreservesBodyAnd require.Equal(t, "claude-3-haiku-20240307", gjson.GetBytes(upstream.lastBody, "model").String(), "透传模式应应用账号级模型映射") - require.Equal(t, "upstream-anthropic-key", upstream.lastReq.Header.Get("x-api-key")) - require.Empty(t, upstream.lastReq.Header.Get("authorization")) - require.Empty(t, upstream.lastReq.Header.Get("x-goog-api-key")) - require.Empty(t, upstream.lastReq.Header.Get("cookie")) - require.Equal(t, "2023-06-01", upstream.lastReq.Header.Get("anthropic-version")) - require.Equal(t, "interleaved-thinking-2025-05-14", upstream.lastReq.Header.Get("anthropic-beta")) - require.Empty(t, upstream.lastReq.Header.Get("x-stainless-lang"), "API Key 透传不应注入 OAuth 指纹头") + require.Equal(t, "upstream-anthropic-key", getHeaderRaw(upstream.lastReq.Header, "x-api-key")) + require.Empty(t, getHeaderRaw(upstream.lastReq.Header, "authorization")) + require.Empty(t, getHeaderRaw(upstream.lastReq.Header, "x-goog-api-key")) + require.Empty(t, getHeaderRaw(upstream.lastReq.Header, "cookie")) + require.Equal(t, "2023-06-01", getHeaderRaw(upstream.lastReq.Header, "anthropic-version")) + require.Equal(t, "interleaved-thinking-2025-05-14", getHeaderRaw(upstream.lastReq.Header, "anthropic-beta")) + require.Empty(t, getHeaderRaw(upstream.lastReq.Header, "x-stainless-lang"), "API Key 透传不应注入 OAuth 指纹头") require.Contains(t, rec.Body.String(), `"cached_tokens":7`) require.NotContains(t, rec.Body.String(), `"cache_read_input_tokens":7`, "透传输出不应被网关改写") @@ -257,9 +257,9 @@ func TestGatewayService_AnthropicAPIKeyPassthrough_ForwardCountTokensPreservesBo require.NoError(t, err) require.Equal(t, "claude-3-opus-20240229", gjson.GetBytes(upstream.lastBody, "model").String(), "count_tokens 透传模式应应用账号级模型映射") - require.Equal(t, "upstream-anthropic-key", upstream.lastReq.Header.Get("x-api-key")) - require.Empty(t, upstream.lastReq.Header.Get("authorization")) - require.Empty(t, upstream.lastReq.Header.Get("cookie")) + require.Equal(t, "upstream-anthropic-key", getHeaderRaw(upstream.lastReq.Header, "x-api-key")) + require.Empty(t, getHeaderRaw(upstream.lastReq.Header, "authorization")) + require.Empty(t, getHeaderRaw(upstream.lastReq.Header, "cookie")) require.Equal(t, http.StatusOK, rec.Code) require.JSONEq(t, upstreamRespBody, rec.Body.String()) require.Empty(t, rec.Header().Get("Set-Cookie")) @@ -684,8 +684,8 @@ func TestGatewayService_AnthropicOAuth_NotAffectedByAPIKeyPassthroughToggle(t *t req, err := svc.buildUpstreamRequest(context.Background(), c, account, []byte(`{"model":"claude-3-7-sonnet-20250219"}`), "oauth-token", "oauth", "claude-3-7-sonnet-20250219", true, false) require.NoError(t, err) - require.Equal(t, "Bearer oauth-token", req.Header.Get("authorization")) - require.Contains(t, req.Header.Get("anthropic-beta"), claude.BetaOAuth, "OAuth 链路仍应按原逻辑补齐 oauth beta") + require.Equal(t, "Bearer oauth-token", getHeaderRaw(req.Header, "authorization")) + require.Contains(t, getHeaderRaw(req.Header, "anthropic-beta"), claude.BetaOAuth, "OAuth 链路仍应按原逻辑补齐 oauth beta") } func TestGatewayService_AnthropicOAuth_ForwardPreservesBillingHeaderSystemBlock(t *testing.T) { @@ -755,8 +755,8 @@ func TestGatewayService_AnthropicOAuth_ForwardPreservesBillingHeaderSystemBlock( require.NoError(t, err) require.NotNil(t, result) require.NotNil(t, upstream.lastReq) - require.Equal(t, "Bearer oauth-token", upstream.lastReq.Header.Get("authorization")) - require.Contains(t, upstream.lastReq.Header.Get("anthropic-beta"), claude.BetaOAuth) + require.Equal(t, "Bearer oauth-token", getHeaderRaw(upstream.lastReq.Header, "authorization")) + require.Contains(t, getHeaderRaw(upstream.lastReq.Header, "anthropic-beta"), claude.BetaOAuth) system := gjson.GetBytes(upstream.lastBody, "system") require.True(t, system.Exists()) diff --git a/backend/internal/service/gateway_debug_env_test.go b/backend/internal/service/gateway_debug_env_test.go index 4f48dc70..bd88a667 100644 --- a/backend/internal/service/gateway_debug_env_test.go +++ b/backend/internal/service/gateway_debug_env_test.go @@ -2,31 +2,28 @@ package service import "testing" -func TestDebugGatewayBodyLoggingEnabled(t *testing.T) { - t.Run("default disabled", func(t *testing.T) { - t.Setenv(debugGatewayBodyEnv, "") - if debugGatewayBodyLoggingEnabled() { - t.Fatalf("expected debug gateway body logging to be disabled by default") +func TestParseDebugEnvBool(t *testing.T) { + t.Run("empty is false", func(t *testing.T) { + if parseDebugEnvBool("") { + t.Fatalf("expected false for empty string") } }) - t.Run("enabled with true-like values", func(t *testing.T) { + t.Run("true-like values", func(t *testing.T) { for _, value := range []string{"1", "true", "TRUE", "yes", "on"} { t.Run(value, func(t *testing.T) { - t.Setenv(debugGatewayBodyEnv, value) - if !debugGatewayBodyLoggingEnabled() { - t.Fatalf("expected debug gateway body logging to be enabled for %q", value) + if !parseDebugEnvBool(value) { + t.Fatalf("expected true for %q", value) } }) } }) - t.Run("disabled with other values", func(t *testing.T) { + t.Run("false-like values", func(t *testing.T) { for _, value := range []string{"0", "false", "off", "debug"} { t.Run(value, func(t *testing.T) { - t.Setenv(debugGatewayBodyEnv, value) - if debugGatewayBodyLoggingEnabled() { - t.Fatalf("expected debug gateway body logging to be disabled for %q", value) + if parseDebugEnvBool(value) { + t.Fatalf("expected false for %q", value) } }) } diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 402975d7..ae66ae4a 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -13,6 +13,7 @@ import ( mathrand "math/rand" "net/http" "os" + "path/filepath" "regexp" "sort" "strconv" @@ -366,6 +367,7 @@ var allowedHeaders = map[string]bool{ "sec-fetch-mode": true, "user-agent": true, "content-type": true, + "accept-encoding": true, } // GatewayCache 定义网关服务的缓存操作接口。 @@ -563,6 +565,7 @@ type GatewayService struct { responseHeaderFilter *responseheaders.CompiledHeaderFilter debugModelRouting atomic.Bool debugClaudeMimic atomic.Bool + debugGatewayBodyFile atomic.Pointer[os.File] // non-nil when SUB2API_DEBUG_GATEWAY_BODY is set } // NewGatewayService creates a new GatewayService @@ -630,6 +633,9 @@ func NewGatewayService( ) svc.debugModelRouting.Store(parseDebugEnvBool(os.Getenv("SUB2API_DEBUG_MODEL_ROUTING"))) svc.debugClaudeMimic.Store(parseDebugEnvBool(os.Getenv("SUB2API_DEBUG_CLAUDE_MIMIC"))) + if path := strings.TrimSpace(os.Getenv(debugGatewayBodyEnv)); path != "" { + svc.initDebugGatewayBodyFile(path) + } return svc } @@ -4048,8 +4054,15 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A reqStream := parsed.Stream originalModel := reqModel - // === DEBUG: 打印客户端原始请求 body === - debugLogRequestBody("CLIENT_ORIGINAL", body) + // === DEBUG: 打印客户端原始请求(headers + body 摘要)=== + if c != nil { + s.debugLogGatewaySnapshot("CLIENT_ORIGINAL", c.Request.Header, body, map[string]string{ + "account": fmt.Sprintf("%d(%s)", account.ID, account.Name), + "account_type": string(account.Type), + "model": reqModel, + "stream": strconv.FormatBool(reqStream), + }) + } isClaudeCode := isClaudeCodeRequest(ctx, c, parsed) shouldMimicClaudeCode := account.IsOAuth() && !isClaudeCode @@ -4066,9 +4079,13 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A if s.identityService != nil { fp, err := s.identityService.GetOrCreateFingerprint(ctx, account.ID, c.Request.Header) if err == nil && fp != nil { - if metadataUserID := s.buildOAuthMetadataUserID(parsed, account, fp); metadataUserID != "" { - normalizeOpts.injectMetadata = true - normalizeOpts.metadataUserID = metadataUserID + // metadata 透传开启时跳过 metadata 注入 + _, mimicMPT := s.settingService.GetGatewayForwardingSettings(ctx) + if !mimicMPT { + if metadataUserID := s.buildOAuthMetadataUserID(parsed, account, fp); metadataUserID != "" { + normalizeOpts.injectMetadata = true + normalizeOpts.metadataUserID = metadataUserID + } } } } @@ -4840,8 +4857,9 @@ func (s *GatewayService) buildUpstreamRequestAnthropicAPIKeyPassthrough( if !allowedHeaders[lowerKey] { continue } + wireKey := resolveWireCasing(key) for _, v := range values { - req.Header.Add(key, v) + addHeaderRaw(req.Header, wireKey, v) } } } @@ -4851,13 +4869,13 @@ func (s *GatewayService) buildUpstreamRequestAnthropicAPIKeyPassthrough( req.Header.Del("x-api-key") req.Header.Del("x-goog-api-key") req.Header.Del("cookie") - req.Header.Set("x-api-key", token) + setHeaderRaw(req.Header, "x-api-key", token) - if req.Header.Get("content-type") == "" { - req.Header.Set("content-type", "application/json") + if getHeaderRaw(req.Header, "content-type") == "" { + setHeaderRaw(req.Header, "content-type", "application/json") } - if req.Header.Get("anthropic-version") == "" { - req.Header.Set("anthropic-version", "2023-06-01") + if getHeaderRaw(req.Header, "anthropic-version") == "" { + setHeaderRaw(req.Header, "anthropic-version", "2023-06-01") } return req, nil @@ -5591,8 +5609,12 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex clientHeaders = c.Request.Header } - // OAuth账号:应用统一指纹 + // OAuth账号:应用统一指纹和metadata重写(受设置开关控制) var fingerprint *Fingerprint + enableFP, enableMPT := true, false + if s.settingService != nil { + enableFP, enableMPT = s.settingService.GetGatewayForwardingSettings(ctx) + } if account.IsOAuth() && s.identityService != nil { // 1. 获取或创建指纹(包含随机生成的ClientID) fp, err := s.identityService.GetOrCreateFingerprint(ctx, account.ID, clientHeaders) @@ -5600,40 +5622,43 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex logger.LegacyPrintf("service.gateway", "Warning: failed to get fingerprint for account %d: %v", account.ID, err) // 失败时降级为透传原始headers } else { - fingerprint = fp + if enableFP { + fingerprint = fp + } // 2. 重写metadata.user_id(需要指纹中的ClientID和账号的account_uuid) // 如果启用了会话ID伪装,会在重写后替换 session 部分为固定值 - accountUUID := account.GetExtraString("account_uuid") - if accountUUID != "" && fp.ClientID != "" { - if newBody, err := s.identityService.RewriteUserIDWithMasking(ctx, body, account, accountUUID, fp.ClientID, fp.UserAgent); err == nil && len(newBody) > 0 { - body = newBody + // 当 metadata 透传开启时跳过重写 + if !enableMPT { + accountUUID := account.GetExtraString("account_uuid") + if accountUUID != "" && fp.ClientID != "" { + if newBody, err := s.identityService.RewriteUserIDWithMasking(ctx, body, account, accountUUID, fp.ClientID, fp.UserAgent); err == nil && len(newBody) > 0 { + body = newBody + } } } } } - // === DEBUG: 打印转发给上游的 body(metadata 已重写) === - debugLogRequestBody("UPSTREAM_FORWARD", body) - req, err := http.NewRequestWithContext(ctx, "POST", targetURL, bytes.NewReader(body)) if err != nil { return nil, err } - // 设置认证头 + // 设置认证头(保持原始大小写) if tokenType == "oauth" { - req.Header.Set("authorization", "Bearer "+token) + setHeaderRaw(req.Header, "authorization", "Bearer "+token) } else { - req.Header.Set("x-api-key", token) + setHeaderRaw(req.Header, "x-api-key", token) } - // 白名单透传headers + // 白名单透传headers(恢复真实 wire casing) for key, values := range clientHeaders { lowerKey := strings.ToLower(key) if allowedHeaders[lowerKey] { + wireKey := resolveWireCasing(key) for _, v := range values { - req.Header.Add(key, v) + addHeaderRaw(req.Header, wireKey, v) } } } @@ -5643,15 +5668,15 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex s.identityService.ApplyFingerprint(req, fingerprint) } - // 确保必要的headers存在 - if req.Header.Get("content-type") == "" { - req.Header.Set("content-type", "application/json") + // 确保必要的headers存在(保持原始大小写) + if getHeaderRaw(req.Header, "content-type") == "" { + setHeaderRaw(req.Header, "content-type", "application/json") } - if req.Header.Get("anthropic-version") == "" { - req.Header.Set("anthropic-version", "2023-06-01") + if getHeaderRaw(req.Header, "anthropic-version") == "" { + setHeaderRaw(req.Header, "anthropic-version", "2023-06-01") } if tokenType == "oauth" { - applyClaudeOAuthHeaderDefaults(req, reqStream) + applyClaudeOAuthHeaderDefaults(req) } // Build effective drop set: merge static defaults with dynamic beta policy filter rules @@ -5667,31 +5692,41 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex // - 保留 incoming beta 的同时,确保 OAuth 所需 beta 存在 applyClaudeCodeMimicHeaders(req, reqStream) - incomingBeta := req.Header.Get("anthropic-beta") + incomingBeta := getHeaderRaw(req.Header, "anthropic-beta") // Match real Claude CLI traffic (per mitmproxy reports): // messages requests typically use only oauth + interleaved-thinking. // Also drop claude-code beta if a downstream client added it. requiredBetas := []string{claude.BetaOAuth, claude.BetaInterleavedThinking} - req.Header.Set("anthropic-beta", mergeAnthropicBetaDropping(requiredBetas, incomingBeta, effectiveDropWithClaudeCodeSet)) + setHeaderRaw(req.Header, "anthropic-beta", mergeAnthropicBetaDropping(requiredBetas, incomingBeta, effectiveDropWithClaudeCodeSet)) } else { // Claude Code 客户端:尽量透传原始 header,仅补齐 oauth beta - clientBetaHeader := req.Header.Get("anthropic-beta") - req.Header.Set("anthropic-beta", stripBetaTokensWithSet(s.getBetaHeader(modelID, clientBetaHeader), effectiveDropSet)) + clientBetaHeader := getHeaderRaw(req.Header, "anthropic-beta") + setHeaderRaw(req.Header, "anthropic-beta", stripBetaTokensWithSet(s.getBetaHeader(modelID, clientBetaHeader), effectiveDropSet)) } } else { // API-key accounts: apply beta policy filter to strip controlled tokens - if existingBeta := req.Header.Get("anthropic-beta"); existingBeta != "" { - req.Header.Set("anthropic-beta", stripBetaTokensWithSet(existingBeta, effectiveDropSet)) + if existingBeta := getHeaderRaw(req.Header, "anthropic-beta"); existingBeta != "" { + setHeaderRaw(req.Header, "anthropic-beta", stripBetaTokensWithSet(existingBeta, effectiveDropSet)) } else if s.cfg != nil && s.cfg.Gateway.InjectBetaForAPIKey { // API-key:仅在请求显式使用 beta 特性且客户端未提供时,按需补齐(默认关闭) if requestNeedsBetaFeatures(body) { if beta := defaultAPIKeyBetaHeader(body); beta != "" { - req.Header.Set("anthropic-beta", beta) + setHeaderRaw(req.Header, "anthropic-beta", beta) } } } } + // === DEBUG: 打印上游转发请求(headers + body 摘要),与 CLIENT_ORIGINAL 对比 === + s.debugLogGatewaySnapshot("UPSTREAM_FORWARD", req.Header, body, map[string]string{ + "url": req.URL.String(), + "token_type": tokenType, + "mimic_claude_code": strconv.FormatBool(mimicClaudeCode), + "fingerprint_applied": strconv.FormatBool(fingerprint != nil), + "enable_fp": strconv.FormatBool(enableFP), + "enable_mpt": strconv.FormatBool(enableMPT), + }) + // Always capture a compact fingerprint line for later error diagnostics. // We only print it when needed (or when the explicit debug flag is enabled). if c != nil && tokenType == "oauth" { @@ -5771,24 +5806,21 @@ func defaultAPIKeyBetaHeader(body []byte) string { return claude.APIKeyBetaHeader } -func applyClaudeOAuthHeaderDefaults(req *http.Request, isStream bool) { +func applyClaudeOAuthHeaderDefaults(req *http.Request) { if req == nil { return } - if req.Header.Get("accept") == "" { - req.Header.Set("accept", "application/json") + if getHeaderRaw(req.Header, "Accept") == "" { + setHeaderRaw(req.Header, "Accept", "application/json") } for key, value := range claude.DefaultHeaders { if value == "" { continue } - if req.Header.Get(key) == "" { - req.Header.Set(key, value) + if getHeaderRaw(req.Header, key) == "" { + setHeaderRaw(req.Header, resolveWireCasing(key), value) } } - if isStream && req.Header.Get("x-stainless-helper-method") == "" { - req.Header.Set("x-stainless-helper-method", "stream") - } } func mergeAnthropicBeta(required []string, incoming string) string { @@ -6083,18 +6115,19 @@ func applyClaudeCodeMimicHeaders(req *http.Request, isStream bool) { return } // Start with the standard defaults (fill missing). - applyClaudeOAuthHeaderDefaults(req, isStream) + applyClaudeOAuthHeaderDefaults(req) // Then force key headers to match Claude Code fingerprint regardless of what the client sent. + // 使用 resolveWireCasing 确保 key 与真实 wire format 一致(如 "x-app" 而非 "X-App") for key, value := range claude.DefaultHeaders { if value == "" { continue } - req.Header.Set(key, value) + setHeaderRaw(req.Header, resolveWireCasing(key), value) } // Real Claude CLI uses Accept: application/json (even for streaming). - req.Header.Set("accept", "application/json") + setHeaderRaw(req.Header, "Accept", "application/json") if isStream { - req.Header.Set("x-stainless-helper-method", "stream") + setHeaderRaw(req.Header, "x-stainless-helper-method", "stream") } } @@ -8197,8 +8230,9 @@ func (s *GatewayService) buildCountTokensRequestAnthropicAPIKeyPassthrough( if !allowedHeaders[lowerKey] { continue } + wireKey := resolveWireCasing(key) for _, v := range values { - req.Header.Add(key, v) + addHeaderRaw(req.Header, wireKey, v) } } } @@ -8239,15 +8273,23 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con clientHeaders = c.Request.Header } - // OAuth 账号:应用统一指纹和重写 userID + // OAuth 账号:应用统一指纹和重写 userID(受设置开关控制) // 如果启用了会话ID伪装,会在重写后替换 session 部分为固定值 + ctEnableFP, ctEnableMPT := true, false + if s.settingService != nil { + ctEnableFP, ctEnableMPT = s.settingService.GetGatewayForwardingSettings(ctx) + } + var ctFingerprint *Fingerprint if account.IsOAuth() && s.identityService != nil { fp, err := s.identityService.GetOrCreateFingerprint(ctx, account.ID, clientHeaders) if err == nil { - accountUUID := account.GetExtraString("account_uuid") - if accountUUID != "" && fp.ClientID != "" { - if newBody, err := s.identityService.RewriteUserIDWithMasking(ctx, body, account, accountUUID, fp.ClientID, fp.UserAgent); err == nil && len(newBody) > 0 { - body = newBody + ctFingerprint = fp + if !ctEnableMPT { + accountUUID := account.GetExtraString("account_uuid") + if accountUUID != "" && fp.ClientID != "" { + if newBody, err := s.identityService.RewriteUserIDWithMasking(ctx, body, account, accountUUID, fp.ClientID, fp.UserAgent); err == nil && len(newBody) > 0 { + body = newBody + } } } } @@ -8258,40 +8300,38 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con return nil, err } - // 设置认证头 + // 设置认证头(保持原始大小写) if tokenType == "oauth" { - req.Header.Set("authorization", "Bearer "+token) + setHeaderRaw(req.Header, "authorization", "Bearer "+token) } else { - req.Header.Set("x-api-key", token) + setHeaderRaw(req.Header, "x-api-key", token) } - // 白名单透传 headers + // 白名单透传 headers(恢复真实 wire casing) for key, values := range clientHeaders { lowerKey := strings.ToLower(key) if allowedHeaders[lowerKey] { + wireKey := resolveWireCasing(key) for _, v := range values { - req.Header.Add(key, v) + addHeaderRaw(req.Header, wireKey, v) } } } - // OAuth 账号:应用指纹到请求头 - if account.IsOAuth() && s.identityService != nil { - fp, _ := s.identityService.GetOrCreateFingerprint(ctx, account.ID, clientHeaders) - if fp != nil { - s.identityService.ApplyFingerprint(req, fp) - } + // OAuth 账号:应用指纹到请求头(受设置开关控制) + if ctEnableFP && ctFingerprint != nil { + s.identityService.ApplyFingerprint(req, ctFingerprint) } - // 确保必要的 headers 存在 - if req.Header.Get("content-type") == "" { - req.Header.Set("content-type", "application/json") + // 确保必要的 headers 存在(保持原始大小写) + if getHeaderRaw(req.Header, "content-type") == "" { + setHeaderRaw(req.Header, "content-type", "application/json") } - if req.Header.Get("anthropic-version") == "" { - req.Header.Set("anthropic-version", "2023-06-01") + if getHeaderRaw(req.Header, "anthropic-version") == "" { + setHeaderRaw(req.Header, "anthropic-version", "2023-06-01") } if tokenType == "oauth" { - applyClaudeOAuthHeaderDefaults(req, false) + applyClaudeOAuthHeaderDefaults(req) } // Build effective drop set for count_tokens: merge static defaults with dynamic beta policy filter rules @@ -8302,30 +8342,30 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con if mimicClaudeCode { applyClaudeCodeMimicHeaders(req, false) - incomingBeta := req.Header.Get("anthropic-beta") + incomingBeta := getHeaderRaw(req.Header, "anthropic-beta") requiredBetas := []string{claude.BetaClaudeCode, claude.BetaOAuth, claude.BetaInterleavedThinking, claude.BetaTokenCounting} - req.Header.Set("anthropic-beta", mergeAnthropicBetaDropping(requiredBetas, incomingBeta, ctEffectiveDropSet)) + setHeaderRaw(req.Header, "anthropic-beta", mergeAnthropicBetaDropping(requiredBetas, incomingBeta, ctEffectiveDropSet)) } else { - clientBetaHeader := req.Header.Get("anthropic-beta") + clientBetaHeader := getHeaderRaw(req.Header, "anthropic-beta") if clientBetaHeader == "" { - req.Header.Set("anthropic-beta", claude.CountTokensBetaHeader) + setHeaderRaw(req.Header, "anthropic-beta", claude.CountTokensBetaHeader) } else { beta := s.getBetaHeader(modelID, clientBetaHeader) if !strings.Contains(beta, claude.BetaTokenCounting) { beta = beta + "," + claude.BetaTokenCounting } - req.Header.Set("anthropic-beta", stripBetaTokensWithSet(beta, ctEffectiveDropSet)) + setHeaderRaw(req.Header, "anthropic-beta", stripBetaTokensWithSet(beta, ctEffectiveDropSet)) } } } else { // API-key accounts: apply beta policy filter to strip controlled tokens - if existingBeta := req.Header.Get("anthropic-beta"); existingBeta != "" { - req.Header.Set("anthropic-beta", stripBetaTokensWithSet(existingBeta, ctEffectiveDropSet)) + if existingBeta := getHeaderRaw(req.Header, "anthropic-beta"); existingBeta != "" { + setHeaderRaw(req.Header, "anthropic-beta", stripBetaTokensWithSet(existingBeta, ctEffectiveDropSet)) } else if s.cfg != nil && s.cfg.Gateway.InjectBetaForAPIKey { // API-key:与 messages 同步的按需 beta 注入(默认关闭) if requestNeedsBetaFeatures(body) { if beta := defaultAPIKeyBetaHeader(body); beta != "" { - req.Header.Set("anthropic-beta", beta) + setHeaderRaw(req.Header, "anthropic-beta", beta) } } } @@ -8496,42 +8536,94 @@ func reconcileCachedTokens(usage map[string]any) bool { return true } -func debugGatewayBodyLoggingEnabled() bool { - raw := strings.TrimSpace(os.Getenv(debugGatewayBodyEnv)) - if raw == "" { - return false - } +const debugGatewayBodyDefaultFilename = "gateway_debug.log" - switch strings.ToLower(raw) { - case "1", "true", "yes", "on": - return true - default: - return false - } -} - -// debugLogRequestBody 打印请求 body 用于调试 metadata.user_id 重写。 -// 默认关闭,仅在设置环境变量时启用: +// initDebugGatewayBodyFile 初始化网关调试日志文件。 // -// SUB2API_DEBUG_GATEWAY_BODY=1 -func debugLogRequestBody(tag string, body []byte) { - if !debugGatewayBodyLoggingEnabled() { +// - "1"/"true" 等布尔值 → 当前目录下 gateway_debug.log +// - 已有目录路径 → 该目录下 gateway_debug.log +// - 其他 → 视为完整文件路径 +func (s *GatewayService) initDebugGatewayBodyFile(path string) { + if parseDebugEnvBool(path) { + path = debugGatewayBodyDefaultFilename + } + + // 如果 path 指向一个已存在的目录,自动追加默认文件名 + if info, err := os.Stat(path); err == nil && info.IsDir() { + path = filepath.Join(path, debugGatewayBodyDefaultFilename) + } + + // 确保父目录存在 + if dir := filepath.Dir(path); dir != "." { + if err := os.MkdirAll(dir, 0755); err != nil { + slog.Error("failed to create gateway debug log directory", "dir", dir, "error", err) + return + } + } + + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + slog.Error("failed to open gateway debug log file", "path", path, "error", err) return } - - if len(body) == 0 { - logger.LegacyPrintf("service.gateway", "[DEBUG_%s] body is empty", tag) - return - } - - // 提取 metadata 字段完整打印 - metadataResult := gjson.GetBytes(body, "metadata") - if metadataResult.Exists() { - logger.LegacyPrintf("service.gateway", "[DEBUG_%s] metadata = %s", tag, metadataResult.Raw) - } else { - logger.LegacyPrintf("service.gateway", "[DEBUG_%s] metadata field not found", tag) - } - - // 全量打印 body - logger.LegacyPrintf("service.gateway", "[DEBUG_%s] body (%d bytes) = %s", tag, len(body), string(body)) + s.debugGatewayBodyFile.Store(f) + slog.Info("gateway debug logging enabled", "path", path) +} + +// debugLogGatewaySnapshot 将网关请求的完整快照(headers + body)写入独立的调试日志文件, +// 用于对比客户端原始请求和上游转发请求。 +// +// 启用方式(环境变量): +// +// SUB2API_DEBUG_GATEWAY_BODY=1 # 写入 gateway_debug.log +// SUB2API_DEBUG_GATEWAY_BODY=/tmp/gateway_debug.log # 写入指定路径 +// +// tag: "CLIENT_ORIGINAL" 或 "UPSTREAM_FORWARD" +func (s *GatewayService) debugLogGatewaySnapshot(tag string, headers http.Header, body []byte, extra map[string]string) { + f := s.debugGatewayBodyFile.Load() + if f == nil { + return + } + + var buf strings.Builder + ts := time.Now().Format("2006-01-02 15:04:05.000") + fmt.Fprintf(&buf, "\n========== [%s] %s ==========\n", ts, tag) + + // 1. context + if len(extra) > 0 { + fmt.Fprint(&buf, "--- context ---\n") + extraKeys := make([]string, 0, len(extra)) + for k := range extra { + extraKeys = append(extraKeys, k) + } + sort.Strings(extraKeys) + for _, k := range extraKeys { + fmt.Fprintf(&buf, " %s: %s\n", k, extra[k]) + } + } + + // 2. headers(按真实 Claude CLI wire 顺序排列,便于与抓包对比;auth 脱敏) + fmt.Fprint(&buf, "--- headers ---\n") + for _, k := range sortHeadersByWireOrder(headers) { + for _, v := range headers[k] { + fmt.Fprintf(&buf, " %s: %s\n", k, safeHeaderValueForLog(k, v)) + } + } + + // 3. body(完整输出,格式化 JSON 便于 diff) + fmt.Fprint(&buf, "--- body ---\n") + if len(body) == 0 { + fmt.Fprint(&buf, " (empty)\n") + } else { + var pretty bytes.Buffer + if json.Indent(&pretty, body, " ", " ") == nil { + fmt.Fprintf(&buf, " %s\n", pretty.Bytes()) + } else { + // JSON 格式化失败时原样输出 + fmt.Fprintf(&buf, " %s\n", body) + } + } + + // 写入文件(调试用,并发写入可能交错但不影响可读性) + _, _ = f.WriteString(buf.String()) } diff --git a/backend/internal/service/header_util.go b/backend/internal/service/header_util.go new file mode 100644 index 00000000..6acfee5a --- /dev/null +++ b/backend/internal/service/header_util.go @@ -0,0 +1,157 @@ +package service + +import ( + "net/http" + "strings" +) + +// headerWireCasing 定义每个白名单 header 在真实 Claude CLI 抓包中的准确大小写。 +// Go 的 HTTP server 解析请求时会将所有 header key 转为 Canonical 形式(如 x-app → X-App), +// 此 map 用于在转发时恢复到真实的 wire format。 +// +// 来源:对真实 Claude CLI (claude-cli/2.1.81) 到 api.anthropic.com 的 HTTPS 流量抓包。 +var headerWireCasing = map[string]string{ + // Title case + "accept": "Accept", + "user-agent": "User-Agent", + + // X-Stainless-* 保持 SDK 原始大小写 + "x-stainless-retry-count": "X-Stainless-Retry-Count", + "x-stainless-timeout": "X-Stainless-Timeout", + "x-stainless-lang": "X-Stainless-Lang", + "x-stainless-package-version": "X-Stainless-Package-Version", + "x-stainless-os": "X-Stainless-OS", + "x-stainless-arch": "X-Stainless-Arch", + "x-stainless-runtime": "X-Stainless-Runtime", + "x-stainless-runtime-version": "X-Stainless-Runtime-Version", + "x-stainless-helper-method": "x-stainless-helper-method", + + // Anthropic SDK 自身设置的 header,全小写 + "anthropic-dangerous-direct-browser-access": "anthropic-dangerous-direct-browser-access", + "anthropic-version": "anthropic-version", + "anthropic-beta": "anthropic-beta", + "x-app": "x-app", + "content-type": "content-type", + "accept-language": "accept-language", + "sec-fetch-mode": "sec-fetch-mode", + "accept-encoding": "accept-encoding", + "authorization": "authorization", +} + +// headerWireOrder 定义真实 Claude CLI 发送 header 的顺序(基于抓包)。 +// 用于 debug log 按此顺序输出,便于与抓包结果直接对比。 +var headerWireOrder = []string{ + "Accept", + "X-Stainless-Retry-Count", + "X-Stainless-Timeout", + "X-Stainless-Lang", + "X-Stainless-Package-Version", + "X-Stainless-OS", + "X-Stainless-Arch", + "X-Stainless-Runtime", + "X-Stainless-Runtime-Version", + "anthropic-dangerous-direct-browser-access", + "anthropic-version", + "authorization", + "x-app", + "User-Agent", + "content-type", + "anthropic-beta", + "accept-language", + "sec-fetch-mode", + "accept-encoding", + "x-stainless-helper-method", +} + +// headerWireOrderSet 用于快速判断某个 key 是否在 headerWireOrder 中(按 lowercase 匹配)。 +var headerWireOrderSet map[string]struct{} + +func init() { + headerWireOrderSet = make(map[string]struct{}, len(headerWireOrder)) + for _, k := range headerWireOrder { + headerWireOrderSet[strings.ToLower(k)] = struct{}{} + } +} + +// resolveWireCasing 将 Go canonical key(如 X-Stainless-Os)映射为真实 wire casing(如 X-Stainless-OS)。 +// 如果 map 中没有对应条目,返回原始 key 不变。 +func resolveWireCasing(key string) string { + if wk, ok := headerWireCasing[strings.ToLower(key)]; ok { + return wk + } + return key +} + +// setHeaderRaw sets a header bypassing Go's canonical-case normalization. +// The key is stored exactly as provided, preserving original casing. +// +// It first removes any existing value under the canonical key, the wire casing key, +// and the exact raw key, preventing duplicates from any source. +func setHeaderRaw(h http.Header, key, value string) { + h.Del(key) // remove canonical form (e.g. "Anthropic-Beta") + if wk := resolveWireCasing(key); wk != key { + delete(h, wk) // remove wire casing form if different + } + delete(h, key) // remove exact raw key if it differs from canonical + h[key] = []string{value} +} + +// addHeaderRaw appends a header value bypassing Go's canonical-case normalization. +func addHeaderRaw(h http.Header, key, value string) { + h[key] = append(h[key], value) +} + +// getHeaderRaw reads a header value, trying multiple key forms to handle the mismatch +// between Go canonical keys, wire casing keys, and raw keys: +// 1. exact key as provided +// 2. wire casing form (from headerWireCasing) +// 3. Go canonical form (via http.Header.Get) +func getHeaderRaw(h http.Header, key string) string { + // 1. exact key + if vals := h[key]; len(vals) > 0 { + return vals[0] + } + // 2. wire casing (e.g. looking up "Anthropic-Dangerous-Direct-Browser-Access" finds "anthropic-dangerous-direct-browser-access") + if wk := resolveWireCasing(key); wk != key { + if vals := h[wk]; len(vals) > 0 { + return vals[0] + } + } + // 3. canonical fallback + return h.Get(key) +} + +// sortHeadersByWireOrder 按照真实 Claude CLI 的 header 顺序返回排序后的 key 列表。 +// 在 headerWireOrder 中定义的 key 按其顺序排列,未定义的 key 追加到末尾。 +func sortHeadersByWireOrder(h http.Header) []string { + // 构建 lowercase -> actual map key 的映射 + present := make(map[string]string, len(h)) + for k := range h { + present[strings.ToLower(k)] = k + } + + result := make([]string, 0, len(h)) + seen := make(map[string]struct{}, len(h)) + + // 先按 wire order 输出 + for _, wk := range headerWireOrder { + lk := strings.ToLower(wk) + if actual, ok := present[lk]; ok { + if _, dup := seen[lk]; !dup { + result = append(result, actual) + seen[lk] = struct{}{} + } + } + } + + // 再追加不在 wire order 中的 header + for k := range h { + lk := strings.ToLower(k) + if _, ok := seen[lk]; !ok { + result = append(result, k) + seen[lk] = struct{}{} + } + } + + return result +} diff --git a/backend/internal/service/identity_service.go b/backend/internal/service/identity_service.go index 428f5bfd..3d706508 100644 --- a/backend/internal/service/identity_service.go +++ b/backend/internal/service/identity_service.go @@ -174,6 +174,7 @@ func getHeaderOrDefault(headers http.Header, key, defaultValue string) string { } // ApplyFingerprint 将指纹应用到请求头(覆盖原有的x-stainless-*头) +// 使用 setHeaderRaw 保持原始大小写(如 X-Stainless-OS 而非 X-Stainless-Os) func (s *IdentityService) ApplyFingerprint(req *http.Request, fp *Fingerprint) { if fp == nil { return @@ -181,27 +182,27 @@ func (s *IdentityService) ApplyFingerprint(req *http.Request, fp *Fingerprint) { // 设置user-agent if fp.UserAgent != "" { - req.Header.Set("user-agent", fp.UserAgent) + setHeaderRaw(req.Header, "User-Agent", fp.UserAgent) } - // 设置x-stainless-*头 + // 设置x-stainless-*头(保持与 claude.DefaultHeaders 一致的大小写) if fp.StainlessLang != "" { - req.Header.Set("X-Stainless-Lang", fp.StainlessLang) + setHeaderRaw(req.Header, "X-Stainless-Lang", fp.StainlessLang) } if fp.StainlessPackageVersion != "" { - req.Header.Set("X-Stainless-Package-Version", fp.StainlessPackageVersion) + setHeaderRaw(req.Header, "X-Stainless-Package-Version", fp.StainlessPackageVersion) } if fp.StainlessOS != "" { - req.Header.Set("X-Stainless-OS", fp.StainlessOS) + setHeaderRaw(req.Header, "X-Stainless-OS", fp.StainlessOS) } if fp.StainlessArch != "" { - req.Header.Set("X-Stainless-Arch", fp.StainlessArch) + setHeaderRaw(req.Header, "X-Stainless-Arch", fp.StainlessArch) } if fp.StainlessRuntime != "" { - req.Header.Set("X-Stainless-Runtime", fp.StainlessRuntime) + setHeaderRaw(req.Header, "X-Stainless-Runtime", fp.StainlessRuntime) } if fp.StainlessRuntimeVersion != "" { - req.Header.Set("X-Stainless-Runtime-Version", fp.StainlessRuntimeVersion) + setHeaderRaw(req.Header, "X-Stainless-Runtime-Version", fp.StainlessRuntimeVersion) } } diff --git a/backend/internal/service/setting_service.go b/backend/internal/service/setting_service.go index 44d20491..1a24bad1 100644 --- a/backend/internal/service/setting_service.go +++ b/backend/internal/service/setting_service.go @@ -79,6 +79,20 @@ const backendModeCacheTTL = 60 * time.Second const backendModeErrorTTL = 5 * time.Second const backendModeDBTimeout = 5 * time.Second +// cachedGatewayForwardingSettings 缓存网关转发行为设置(进程内缓存,60s TTL) +type cachedGatewayForwardingSettings struct { + fingerprintUnification bool + metadataPassthrough bool + expiresAt int64 // unix nano +} + +var gatewayForwardingCache atomic.Value // *cachedGatewayForwardingSettings +var gatewayForwardingSF singleflight.Group + +const gatewayForwardingCacheTTL = 60 * time.Second +const gatewayForwardingErrorTTL = 5 * time.Second +const gatewayForwardingDBTimeout = 5 * time.Second + // DefaultSubscriptionGroupReader validates group references used by default subscriptions. type DefaultSubscriptionGroupReader interface { GetByID(ctx context.Context, id int64) (*Group, error) @@ -510,6 +524,10 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet // Backend Mode updates[SettingKeyBackendModeEnabled] = strconv.FormatBool(settings.BackendModeEnabled) + // Gateway forwarding behavior + updates[SettingKeyEnableFingerprintUnification] = strconv.FormatBool(settings.EnableFingerprintUnification) + updates[SettingKeyEnableMetadataPassthrough] = strconv.FormatBool(settings.EnableMetadataPassthrough) + err = s.settingRepo.SetMultiple(ctx, updates) if err == nil { // 先使 inflight singleflight 失效,再刷新缓存,缩小旧值覆盖新值的竞态窗口 @@ -524,6 +542,12 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet value: settings.BackendModeEnabled, expiresAt: time.Now().Add(backendModeCacheTTL).UnixNano(), }) + gatewayForwardingSF.Forget("gateway_forwarding") + gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{ + fingerprintUnification: settings.EnableFingerprintUnification, + metadataPassthrough: settings.EnableMetadataPassthrough, + expiresAt: time.Now().Add(gatewayForwardingCacheTTL).UnixNano(), + }) if s.onUpdate != nil { s.onUpdate() // Invalidate cache after settings update } @@ -626,6 +650,57 @@ func (s *SettingService) IsBackendModeEnabled(ctx context.Context) bool { return false } +// GetGatewayForwardingSettings returns cached gateway forwarding settings. +// Uses in-process atomic.Value cache with 60s TTL, zero-lock hot path. +// Returns (fingerprintUnification, metadataPassthrough). +func (s *SettingService) GetGatewayForwardingSettings(ctx context.Context) (fingerprintUnification, metadataPassthrough bool) { + if cached, ok := gatewayForwardingCache.Load().(*cachedGatewayForwardingSettings); ok && cached != nil { + if time.Now().UnixNano() < cached.expiresAt { + return cached.fingerprintUnification, cached.metadataPassthrough + } + } + type gwfResult struct { + fp, mp bool + } + val, _, _ := gatewayForwardingSF.Do("gateway_forwarding", func() (any, error) { + if cached, ok := gatewayForwardingCache.Load().(*cachedGatewayForwardingSettings); ok && cached != nil { + if time.Now().UnixNano() < cached.expiresAt { + return gwfResult{cached.fingerprintUnification, cached.metadataPassthrough}, nil + } + } + dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), gatewayForwardingDBTimeout) + defer cancel() + values, err := s.settingRepo.GetMultiple(dbCtx, []string{ + SettingKeyEnableFingerprintUnification, + SettingKeyEnableMetadataPassthrough, + }) + if err != nil { + slog.Warn("failed to get gateway forwarding settings", "error", err) + gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{ + fingerprintUnification: true, + metadataPassthrough: false, + expiresAt: time.Now().Add(gatewayForwardingErrorTTL).UnixNano(), + }) + return gwfResult{true, false}, nil + } + fp := true + if v, ok := values[SettingKeyEnableFingerprintUnification]; ok && v != "" { + fp = v == "true" + } + mp := values[SettingKeyEnableMetadataPassthrough] == "true" + gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{ + fingerprintUnification: fp, + metadataPassthrough: mp, + expiresAt: time.Now().Add(gatewayForwardingCacheTTL).UnixNano(), + }) + return gwfResult{fp, mp}, nil + }) + if r, ok := val.(gwfResult); ok { + return r.fp, r.mp + } + return true, false // fail-open defaults +} + // IsEmailVerifyEnabled 检查是否开启邮件验证 func (s *SettingService) IsEmailVerifyEnabled(ctx context.Context) bool { value, err := s.settingRepo.GetValue(ctx, SettingKeyEmailVerifyEnabled) @@ -923,6 +998,14 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin // 分组隔离 result.AllowUngroupedKeyScheduling = settings[SettingKeyAllowUngroupedKeyScheduling] == "true" + // Gateway forwarding behavior (defaults: fingerprint=true, metadata_passthrough=false) + if v, ok := settings[SettingKeyEnableFingerprintUnification]; ok && v != "" { + result.EnableFingerprintUnification = v == "true" + } else { + result.EnableFingerprintUnification = true // default: enabled (current behavior) + } + result.EnableMetadataPassthrough = settings[SettingKeyEnableMetadataPassthrough] == "true" + return result } diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go index cf1d5eed..4e29dba5 100644 --- a/backend/internal/service/settings_view.go +++ b/backend/internal/service/settings_view.go @@ -75,6 +75,10 @@ type SystemSettings struct { // Backend 模式:禁用用户注册和自助服务,仅管理员可登录 BackendModeEnabled bool + + // Gateway forwarding behavior + EnableFingerprintUnification bool // 是否统一 OAuth 账号的指纹头(默认 true) + EnableMetadataPassthrough bool // 是否透传客户端原始 metadata(默认 false) } type DefaultSubscriptionSetting struct { diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts index 83258bcc..196e3788 100644 --- a/frontend/src/api/admin/settings.ts +++ b/frontend/src/api/admin/settings.ts @@ -86,6 +86,10 @@ export interface SystemSettings { // 分组隔离 allow_ungrouped_key_scheduling: boolean + + // Gateway forwarding behavior + enable_fingerprint_unification: boolean + enable_metadata_passthrough: boolean } export interface UpdateSettingsRequest { @@ -142,6 +146,8 @@ export interface UpdateSettingsRequest { min_claude_code_version?: string max_claude_code_version?: string allow_ungrouped_key_scheduling?: boolean + enable_fingerprint_unification?: boolean + enable_metadata_passthrough?: boolean } /** diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index ad916a65..4353b14b 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -4171,6 +4171,14 @@ export default { allowUngroupedKey: 'Allow Ungrouped Key Scheduling', allowUngroupedKeyHint: 'When disabled, API Keys not assigned to any group cannot make requests (403 Forbidden). Keep disabled to ensure all Keys belong to a specific group.' }, + gatewayForwarding: { + title: 'Request Forwarding', + description: 'Control how requests are forwarded to upstream OAuth accounts', + fingerprintUnification: 'Fingerprint Unification', + fingerprintUnificationHint: 'Unify X-Stainless-* headers across users sharing the same OAuth account. Disabling passes through each client\'s original headers.', + metadataPassthrough: 'Metadata Passthrough', + metadataPassthroughHint: 'Pass through client\'s original metadata.user_id without rewriting. May improve upstream cache hit rates.', + }, site: { title: 'Site Settings', description: 'Customize site branding', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 042fca26..6dac7fee 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -4334,6 +4334,14 @@ export default { allowUngroupedKey: '允许未分组 Key 调度', allowUngroupedKeyHint: '关闭后,未分配到任何分组的 API Key 将无法发起请求(返回 403)。建议保持关闭以确保所有 Key 都归属明确的分组。' }, + gatewayForwarding: { + title: '请求转发行为', + description: '控制请求转发到上游 OAuth 账号时的行为', + fingerprintUnification: '指纹统一化', + fingerprintUnificationHint: '统一共享同一 OAuth 账号的用户的 X-Stainless-* 请求头。关闭后透传客户端原始请求头。', + metadataPassthrough: 'Metadata 透传', + metadataPassthroughHint: '透传客户端原始 metadata.user_id,不进行重写。可能提高上游缓存命中率。', + }, site: { title: '站点设置', description: '自定义站点品牌', diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index 00105eb9..0e510aa9 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -1171,6 +1171,45 @@
+ + +
+
+

+ {{ t('admin.settings.gatewayForwarding.title') }} +

+

+ {{ t('admin.settings.gatewayForwarding.description') }} +

+
+
+ +
+
+ +

+ {{ t('admin.settings.gatewayForwarding.fingerprintUnificationHint') }} +

+
+ +
+ + +
+
+ +

+ {{ t('admin.settings.gatewayForwarding.metadataPassthroughHint') }} +

+
+ +
+
+
@@ -2066,7 +2105,10 @@ const form = reactive({ min_claude_code_version: '', max_claude_code_version: '', // 分组隔离 - allow_ungrouped_key_scheduling: false + allow_ungrouped_key_scheduling: false, + // Gateway forwarding behavior + enable_fingerprint_unification: true, + enable_metadata_passthrough: false }) const defaultSubscriptionGroupOptions = computed(() => @@ -2373,7 +2415,9 @@ async function saveSettings() { identity_patch_prompt: form.identity_patch_prompt, min_claude_code_version: form.min_claude_code_version, max_claude_code_version: form.max_claude_code_version, - allow_ungrouped_key_scheduling: form.allow_ungrouped_key_scheduling + allow_ungrouped_key_scheduling: form.allow_ungrouped_key_scheduling, + enable_fingerprint_unification: form.enable_fingerprint_unification, + enable_metadata_passthrough: form.enable_metadata_passthrough } const updated = await adminAPI.settings.updateSettings(payload) Object.assign(form, updated) From d571f300e55744d082f2c764ad902a5d54dec8cb Mon Sep 17 00:00:00 2001 From: shaw Date: Thu, 26 Mar 2026 16:43:38 +0800 Subject: [PATCH 10/16] =?UTF-8?q?feat(rectifier):=20=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E6=95=B4=E6=B5=81=E5=99=A8=E5=A2=9E=E5=8A=A0=20API=20Key=20?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E7=AD=BE=E5=90=8D=E6=95=B4=E6=B5=81=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增独立开关控制 API Key 账号的签名整流功能,支持配置自定义 匹配关键词以捕获不同格式的上游错误响应。 - 新增 apikey_signature_enabled 开关(默认关闭) - 新增 apikey_signature_patterns 自定义关键词配置 - 内置签名检测规则对 API Key 账号同样生效 - 自定义关键词对完整响应体做不区分大小写匹配 - 重试二阶段检测仅做模式匹配,不重复校验开关 - Handler 层校验关键词数量(≤50)和长度(≤500) - API 响应 nil patterns 统一序列化为空数组 - OAuth/SetupToken/Upstream/Bedrock 账号行为不变 --- .../internal/handler/admin/setting_handler.go | 42 +++++++++- backend/internal/handler/dto/settings.go | 8 +- backend/internal/service/gateway_service.go | 59 ++++++++++++- backend/internal/service/settings_view.go | 8 +- frontend/src/api/admin/settings.ts | 2 + frontend/src/i18n/locales/en.ts | 8 ++ frontend/src/i18n/locales/zh.ts | 8 ++ frontend/src/views/admin/SettingsView.vue | 83 ++++++++++++++++++- 8 files changed, 204 insertions(+), 14 deletions(-) diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go index f57244fb..397526a7 100644 --- a/backend/internal/handler/admin/setting_handler.go +++ b/backend/internal/handler/admin/setting_handler.go @@ -1594,18 +1594,26 @@ func (h *SettingHandler) GetRectifierSettings(c *gin.Context) { return } + patterns := settings.APIKeySignaturePatterns + if patterns == nil { + patterns = []string{} + } response.Success(c, dto.RectifierSettings{ Enabled: settings.Enabled, ThinkingSignatureEnabled: settings.ThinkingSignatureEnabled, ThinkingBudgetEnabled: settings.ThinkingBudgetEnabled, + APIKeySignatureEnabled: settings.APIKeySignatureEnabled, + APIKeySignaturePatterns: patterns, }) } // UpdateRectifierSettingsRequest 更新整流器配置请求 type UpdateRectifierSettingsRequest struct { - Enabled bool `json:"enabled"` - ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"` - ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"` + Enabled bool `json:"enabled"` + ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"` + ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"` + APIKeySignatureEnabled bool `json:"apikey_signature_enabled"` + APIKeySignaturePatterns []string `json:"apikey_signature_patterns"` } // UpdateRectifierSettings 更新请求整流器配置 @@ -1617,10 +1625,32 @@ func (h *SettingHandler) UpdateRectifierSettings(c *gin.Context) { return } + // 校验并清理自定义匹配关键词 + const maxPatterns = 50 + const maxPatternLen = 500 + if len(req.APIKeySignaturePatterns) > maxPatterns { + response.BadRequest(c, "Too many signature patterns (max 50)") + return + } + var cleanedPatterns []string + for _, p := range req.APIKeySignaturePatterns { + p = strings.TrimSpace(p) + if p == "" { + continue + } + if len(p) > maxPatternLen { + response.BadRequest(c, "Signature pattern too long (max 500 characters)") + return + } + cleanedPatterns = append(cleanedPatterns, p) + } + settings := &service.RectifierSettings{ Enabled: req.Enabled, ThinkingSignatureEnabled: req.ThinkingSignatureEnabled, ThinkingBudgetEnabled: req.ThinkingBudgetEnabled, + APIKeySignatureEnabled: req.APIKeySignatureEnabled, + APIKeySignaturePatterns: cleanedPatterns, } if err := h.settingService.SetRectifierSettings(c.Request.Context(), settings); err != nil { @@ -1635,10 +1665,16 @@ func (h *SettingHandler) UpdateRectifierSettings(c *gin.Context) { return } + updatedPatterns := updatedSettings.APIKeySignaturePatterns + if updatedPatterns == nil { + updatedPatterns = []string{} + } response.Success(c, dto.RectifierSettings{ Enabled: updatedSettings.Enabled, ThinkingSignatureEnabled: updatedSettings.ThinkingSignatureEnabled, ThinkingBudgetEnabled: updatedSettings.ThinkingBudgetEnabled, + APIKeySignatureEnabled: updatedSettings.APIKeySignatureEnabled, + APIKeySignaturePatterns: updatedPatterns, }) } diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go index 59d7f688..47bab091 100644 --- a/backend/internal/handler/dto/settings.go +++ b/backend/internal/handler/dto/settings.go @@ -188,9 +188,11 @@ type StreamTimeoutSettings struct { // RectifierSettings 请求整流器配置 DTO type RectifierSettings struct { - Enabled bool `json:"enabled"` - ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"` - ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"` + Enabled bool `json:"enabled"` + ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"` + ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"` + APIKeySignatureEnabled bool `json:"apikey_signature_enabled"` + APIKeySignaturePatterns []string `json:"apikey_signature_patterns"` } // BetaPolicyRule Beta 策略规则 DTO diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index ae66ae4a..5de6dcae 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -4188,7 +4188,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A if readErr == nil { _ = resp.Body.Close() - if s.isThinkingBlockSignatureError(respBody) && s.settingService.IsSignatureRectifierEnabled(ctx) { + if s.shouldRectifySignatureError(ctx, account, respBody) { appendOpsUpstreamError(c, OpsUpstreamErrorEvent{ Platform: account.Platform, AccountID: account.ID, @@ -4243,7 +4243,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A retryRespBody, retryReadErr := io.ReadAll(io.LimitReader(retryResp.Body, 2<<20)) _ = retryResp.Body.Close() - if retryReadErr == nil && retryResp.StatusCode == 400 && s.isThinkingBlockSignatureError(retryRespBody) { + if retryReadErr == nil && retryResp.StatusCode == 400 && s.isSignatureErrorPattern(ctx, account, retryRespBody) { appendOpsUpstreamError(c, OpsUpstreamErrorEvent{ Platform: account.Platform, AccountID: account.ID, @@ -6145,6 +6145,59 @@ func truncateForLog(b []byte, maxBytes int) string { return s } +// shouldRectifySignatureError 统一判断是否应触发签名整流(strip thinking blocks 并重试)。 +// 根据账号类型检查对应的开关和匹配模式。 +func (s *GatewayService) shouldRectifySignatureError(ctx context.Context, account *Account, respBody []byte) bool { + if account.Type == AccountTypeAPIKey { + // API Key 账号:独立开关,一次读取配置 + settings, err := s.settingService.GetRectifierSettings(ctx) + if err != nil || !settings.Enabled || !settings.APIKeySignatureEnabled { + return false + } + // 先检查内置模式(同 OAuth),再检查自定义关键词 + if s.isThinkingBlockSignatureError(respBody) { + return true + } + return matchSignaturePatterns(respBody, settings.APIKeySignaturePatterns) + } + // OAuth/SetupToken/Upstream/Bedrock 等:保持原有行为(内置模式 + 原开关) + return s.isThinkingBlockSignatureError(respBody) && s.settingService.IsSignatureRectifierEnabled(ctx) +} + +// isSignatureErrorPattern 仅做模式匹配,不检查开关。 +// 用于已进入重试流程后的二阶段检测(此时开关已在首次调用时验证过)。 +func (s *GatewayService) isSignatureErrorPattern(ctx context.Context, account *Account, respBody []byte) bool { + if s.isThinkingBlockSignatureError(respBody) { + return true + } + if account.Type == AccountTypeAPIKey { + settings, err := s.settingService.GetRectifierSettings(ctx) + if err != nil { + return false + } + return matchSignaturePatterns(respBody, settings.APIKeySignaturePatterns) + } + return false +} + +// matchSignaturePatterns 检查响应体是否匹配自定义关键词列表(不区分大小写)。 +func matchSignaturePatterns(respBody []byte, patterns []string) bool { + if len(patterns) == 0 { + return false + } + bodyLower := strings.ToLower(string(respBody)) + for _, p := range patterns { + p = strings.TrimSpace(p) + if p == "" { + continue + } + if strings.Contains(bodyLower, strings.ToLower(p)) { + return true + } + } + return false +} + // isThinkingBlockSignatureError 检测是否是thinking block相关错误 // 这类错误可以通过过滤thinking blocks并重试来解决 func (s *GatewayService) isThinkingBlockSignatureError(respBody []byte) bool { @@ -8013,7 +8066,7 @@ func (s *GatewayService) ForwardCountTokens(ctx context.Context, c *gin.Context, } // 检测 thinking block 签名错误(400)并重试一次(过滤 thinking blocks) - if resp.StatusCode == 400 && s.isThinkingBlockSignatureError(respBody) && s.settingService.IsSignatureRectifierEnabled(ctx) { + if resp.StatusCode == 400 && s.shouldRectifySignatureError(ctx, account, respBody) { logger.LegacyPrintf("service.gateway", "Account %d: detected thinking block signature error on count_tokens, retrying with filtered thinking blocks", account.ID) filteredBody := FilterThinkingBlocksForRetry(body) diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go index 4e29dba5..411939bb 100644 --- a/backend/internal/service/settings_view.go +++ b/backend/internal/service/settings_view.go @@ -190,9 +190,11 @@ func DefaultStreamTimeoutSettings() *StreamTimeoutSettings { // RectifierSettings 请求整流器配置 type RectifierSettings struct { - Enabled bool `json:"enabled"` // 总开关 - ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"` // Thinking 签名整流 - ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"` // Thinking Budget 整流 + Enabled bool `json:"enabled"` // 总开关 + ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"` // Thinking 签名整流 + ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"` // Thinking Budget 整流 + APIKeySignatureEnabled bool `json:"apikey_signature_enabled"` // API Key 签名整流开关 + APIKeySignaturePatterns []string `json:"apikey_signature_patterns"` // API Key 自定义匹配关键词 } // DefaultRectifierSettings 返回默认的整流器配置(全部启用) diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts index 196e3788..cabdd5aa 100644 --- a/frontend/src/api/admin/settings.ts +++ b/frontend/src/api/admin/settings.ts @@ -323,6 +323,8 @@ export interface RectifierSettings { enabled: boolean thinking_signature_enabled: boolean thinking_budget_enabled: boolean + apikey_signature_enabled: boolean + apikey_signature_patterns: string[] } /** diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 42f58a77..54d757bb 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -4473,6 +4473,14 @@ export default { thinkingSignatureHint: 'Automatically strip signatures and retry when upstream returns thinking block signature validation errors', thinkingBudget: 'Thinking Budget Rectifier', thinkingBudgetHint: 'Automatically set budget to 32000 and retry when upstream returns budget_tokens constraint error (≥1024)', + apikeySignature: 'API Key Signature Rectifier', + apikeySignatureHint: + 'Automatically strip signatures and retry when API Key accounts receive signature-related errors (built-in patterns always apply)', + apikeyPatterns: 'Custom Match Patterns', + apikeyPatternsHint: + 'Additional keywords matched against the response body (case-insensitive). Built-in patterns always apply; use these for supplementary matching.', + apikeyPatternPlaceholder: 'e.g., thinking_error', + addPattern: 'Add Pattern', saved: 'Rectifier settings saved', saveFailed: 'Failed to save rectifier settings' }, diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 7ca78373..ac75188d 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -4637,6 +4637,14 @@ export default { thinkingSignatureHint: '当上游返回 thinking block 签名校验错误时,自动去除签名并重试', thinkingBudget: 'Thinking Budget 整流', thinkingBudgetHint: '当上游返回 budget_tokens 约束错误(≥1024)时,自动将 budget 设为 32000 并重试', + apikeySignature: 'API Key 签名整流', + apikeySignatureHint: + '当 API Key 账号的上游返回签名相关错误时,自动去除签名并重试(内置规则始终生效)', + apikeyPatterns: '自定义匹配关键词', + apikeyPatternsHint: + '额外的关键词,匹配响应体中的内容(不区分大小写)。内置规则始终生效,此处用于补充额外匹配。', + apikeyPatternPlaceholder: '例如:thinking_error 或 签名无效', + addPattern: '添加关键词', saved: '整流器设置保存成功', saveFailed: '保存整流器设置失败' }, diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index 0e510aa9..198d484b 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -454,6 +454,72 @@ + + +
+
+ +

+ {{ t('admin.settings.rectifier.apikeySignatureHint') }} +

+
+ +
+ + +
+
+ +

+ {{ t('admin.settings.rectifier.apikeyPatternsHint') }} +

+
+
+ + +
+ +
@@ -2010,7 +2076,9 @@ const rectifierSaving = ref(false) const rectifierForm = reactive({ enabled: true, thinking_signature_enabled: true, - thinking_budget_enabled: true + thinking_budget_enabled: true, + apikey_signature_enabled: false, + apikey_signature_patterns: [] as string[] }) // Beta Policy 状态 @@ -2626,6 +2694,10 @@ async function loadRectifierSettings() { try { const settings = await adminAPI.settings.getRectifierSettings() Object.assign(rectifierForm, settings) + // 确保 patterns 是数组(旧数据可能为 null) + if (!Array.isArray(rectifierForm.apikey_signature_patterns)) { + rectifierForm.apikey_signature_patterns = [] + } } catch (error: any) { console.error('Failed to load rectifier settings:', error) } finally { @@ -2639,9 +2711,16 @@ async function saveRectifierSettings() { const updated = await adminAPI.settings.updateRectifierSettings({ enabled: rectifierForm.enabled, thinking_signature_enabled: rectifierForm.thinking_signature_enabled, - thinking_budget_enabled: rectifierForm.thinking_budget_enabled + thinking_budget_enabled: rectifierForm.thinking_budget_enabled, + apikey_signature_enabled: rectifierForm.apikey_signature_enabled, + apikey_signature_patterns: rectifierForm.apikey_signature_patterns.filter( + (p) => p.trim() !== '' + ) }) Object.assign(rectifierForm, updated) + if (!Array.isArray(rectifierForm.apikey_signature_patterns)) { + rectifierForm.apikey_signature_patterns = [] + } appStore.showSuccess(t('admin.settings.rectifier.saved')) } catch (error: any) { appStore.showError( From 1854050df3be90d2556ee3bff220cb1705d04bcd Mon Sep 17 00:00:00 2001 From: shaw Date: Fri, 27 Mar 2026 14:23:28 +0800 Subject: [PATCH 11/16] =?UTF-8?q?feat(tls-fingerprint):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20TLS=20=E6=8C=87=E7=BA=B9=20Profile=20=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E7=AE=A1=E7=90=86=E5=8F=8A=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E8=B4=A8=E9=87=8F=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: - 新增 TLS 指纹 Profile CRUD 管理(Ent schema + 迁移 + Admin API + 前端管理界面) - 支持账号绑定数据库中的自定义 TLS Profile,或随机选择(profile_id=-1) - HTTPUpstream.DoWithTLS 接口从 bool 改为 *tlsfingerprint.Profile,支持按账号指定 Profile - AccountUsageService 注入 TLSFingerprintProfileService,统一 usage 场景与网关的 Profile 解析逻辑 代码优化: - 删除已被 TLSFingerprintProfileService 完全取代的 registry.go 死代码(418 行) - 提取 3 个 dialer 的重复 TLS 握手逻辑为 performTLSHandshake() 共用函数 - 修复 GetTLSFingerprintProfileID 缺少 json.Number 处理的 bug - gateway_service.Forward 中 ResolveTLSProfile 从重试循环内重复调用改为预解析局部变量 - 删除冗余的 buildClientHelloSpec() 单行 wrapper 和 int64(e.ID) 无效转换 - tls_fingerprint_profile_cache.go 日志从 log.Printf 改为 slog 结构化日志 - dialer_capture_test.go 添加 //go:build integration 标签,防止 CI 失败 - 去重 TestProfileExpectation 类型至共享 test_types_test.go - 修复 9 个测试文件缺少 tlsfingerprint import 的编译错误 - 修复 error_policy_integration_test.go 中 handleError 回调签名被错误替换的问题 --- backend/cmd/server/wire_gen.go | 12 +- backend/ent/client.go | 163 +- backend/ent/ent.go | 2 + backend/ent/hook/hook.go | 12 + backend/ent/intercept/intercept.go | 30 + backend/ent/migrate/schema.go | 28 + backend/ent/mutation.go | 1376 +++++++++++++++++ backend/ent/predicate/predicate.go | 3 + backend/ent/runtime/runtime.go | 38 + backend/ent/schema/tls_fingerprint_profile.go | 100 ++ backend/ent/tlsfingerprintprofile.go | 275 ++++ .../tlsfingerprintprofile.go | 121 ++ backend/ent/tlsfingerprintprofile/where.go | 415 +++++ backend/ent/tlsfingerprintprofile_create.go | 1341 ++++++++++++++++ backend/ent/tlsfingerprintprofile_delete.go | 88 ++ backend/ent/tlsfingerprintprofile_query.go | 564 +++++++ backend/ent/tlsfingerprintprofile_update.go | 881 +++++++++++ backend/ent/tx.go | 3 + backend/internal/config/config.go | 24 +- .../admin/tls_fingerprint_profile_handler.go | 234 +++ backend/internal/handler/dto/mappers.go | 4 + backend/internal/handler/dto/types.go | 3 +- ...eway_handler_warmup_intercept_unit_test.go | 7 +- backend/internal/handler/handler.go | 47 +- .../handler/sora_client_handler_test.go | 2 +- .../handler/sora_gateway_handler_test.go | 1 + backend/internal/handler/wire.go | 49 +- .../internal/model/tls_fingerprint_profile.go | 54 + backend/internal/pkg/tlsfingerprint/dialer.go | 464 +++--- .../pkg/tlsfingerprint/dialer_capture_test.go | 368 +++++ .../tlsfingerprint/dialer_integration_test.go | 100 +- .../pkg/tlsfingerprint/dialer_test.go | 91 +- .../internal/pkg/tlsfingerprint/registry.go | 171 -- .../pkg/tlsfingerprint/registry_test.go | 243 --- .../pkg/tlsfingerprint/test_types_test.go | 8 + .../repository/claude_usage_service.go | 7 +- backend/internal/repository/http_upstream.go | 40 +- .../tls_fingerprint_profile_cache.go | 122 ++ .../tls_fingerprint_profile_repo.go | 213 +++ backend/internal/repository/wire.go | 2 + backend/internal/server/routes/admin.go | 14 + backend/internal/service/account.go | 25 + .../internal/service/account_test_service.go | 43 +- .../service/account_test_service_sora_test.go | 5 +- .../internal/service/account_usage_service.go | 22 +- .../antigravity_gateway_service_test.go | 5 +- .../service/antigravity_rate_limit_test.go | 5 +- .../service/antigravity_smart_retry_test.go | 3 +- .../service/error_policy_integration_test.go | 3 +- ...teway_anthropic_apikey_passthrough_test.go | 3 +- .../gateway_forward_as_chat_completions.go | 2 +- .../service/gateway_forward_as_responses.go | 2 +- .../service/gateway_record_usage_test.go | 1 + backend/internal/service/gateway_service.go | 26 +- .../gemini_messages_compat_service_test.go | 3 +- .../internal/service/http_upstream_port.go | 55 +- .../service/openai_oauth_passthrough_test.go | 3 +- .../openai_ws_protocol_forward_test.go | 3 +- .../tls_fingerprint_profile_service.go | 259 ++++ backend/internal/service/wire.go | 1 + .../080_create_tls_fingerprint_profiles.sql | 29 + frontend/src/api/admin/index.ts | 8 +- .../src/api/admin/tlsFingerprintProfile.ts | 98 ++ .../components/account/CreateAccountModal.vue | 21 + .../components/account/EditAccountModal.vue | 28 + .../admin/TLSFingerprintProfilesModal.vue | 625 ++++++++ frontend/src/i18n/locales/en.ts | 60 +- frontend/src/i18n/locales/zh.ts | 60 +- frontend/src/types/index.ts | 1 + frontend/src/views/admin/AccountsView.vue | 13 + 70 files changed, 8095 insertions(+), 1037 deletions(-) create mode 100644 backend/ent/schema/tls_fingerprint_profile.go create mode 100644 backend/ent/tlsfingerprintprofile.go create mode 100644 backend/ent/tlsfingerprintprofile/tlsfingerprintprofile.go create mode 100644 backend/ent/tlsfingerprintprofile/where.go create mode 100644 backend/ent/tlsfingerprintprofile_create.go create mode 100644 backend/ent/tlsfingerprintprofile_delete.go create mode 100644 backend/ent/tlsfingerprintprofile_query.go create mode 100644 backend/ent/tlsfingerprintprofile_update.go create mode 100644 backend/internal/handler/admin/tls_fingerprint_profile_handler.go create mode 100644 backend/internal/model/tls_fingerprint_profile.go create mode 100644 backend/internal/pkg/tlsfingerprint/dialer_capture_test.go delete mode 100644 backend/internal/pkg/tlsfingerprint/registry.go delete mode 100644 backend/internal/pkg/tlsfingerprint/registry_test.go create mode 100644 backend/internal/repository/tls_fingerprint_profile_cache.go create mode 100644 backend/internal/repository/tls_fingerprint_profile_repo.go create mode 100644 backend/internal/service/tls_fingerprint_profile_service.go create mode 100644 backend/migrations/080_create_tls_fingerprint_profiles.sql create mode 100644 frontend/src/api/admin/tlsFingerprintProfile.ts create mode 100644 frontend/src/components/admin/TLSFingerprintProfilesModal.vue diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index 2c1ac5b0..300cda00 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -132,14 +132,17 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { antigravityQuotaFetcher := service.NewAntigravityQuotaFetcher(proxyRepository) usageCache := service.NewUsageCache() identityCache := repository.NewIdentityCache(redisClient) - accountUsageService := service.NewAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher, geminiQuotaService, antigravityQuotaFetcher, usageCache, identityCache) geminiTokenProvider := service.ProvideGeminiTokenProvider(accountRepository, geminiTokenCache, geminiOAuthService, oauthRefreshAPI) gatewayCache := repository.NewGatewayCache(redisClient) schedulerOutboxRepository := repository.NewSchedulerOutboxRepository(db) schedulerSnapshotService := service.ProvideSchedulerSnapshotService(schedulerCache, schedulerOutboxRepository, accountRepository, groupRepository, configConfig) antigravityTokenProvider := service.ProvideAntigravityTokenProvider(accountRepository, geminiTokenCache, antigravityOAuthService, oauthRefreshAPI, tempUnschedCache) antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, schedulerSnapshotService, antigravityTokenProvider, rateLimitService, httpUpstream, settingService) - accountTestService := service.NewAccountTestService(accountRepository, geminiTokenProvider, antigravityGatewayService, httpUpstream, configConfig) + tlsFingerprintProfileRepository := repository.NewTLSFingerprintProfileRepository(client) + tlsFingerprintProfileCache := repository.NewTLSFingerprintProfileCache(redisClient) + tlsFingerprintProfileService := service.NewTLSFingerprintProfileService(tlsFingerprintProfileRepository, tlsFingerprintProfileCache) + accountUsageService := service.NewAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher, geminiQuotaService, antigravityQuotaFetcher, usageCache, identityCache, tlsFingerprintProfileService) + accountTestService := service.NewAccountTestService(accountRepository, geminiTokenProvider, antigravityGatewayService, httpUpstream, configConfig, tlsFingerprintProfileService) crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService, geminiOAuthService, configConfig) sessionLimitCache := repository.ProvideSessionLimitCache(redisClient, configConfig) rpmCache := repository.NewRPMCache(redisClient) @@ -171,7 +174,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { deferredService := service.ProvideDeferredService(accountRepository, timingWheelService) claudeTokenProvider := service.ProvideClaudeTokenProvider(accountRepository, geminiTokenCache, oAuthService, oauthRefreshAPI) digestSessionStore := service.NewDigestSessionStore() - gatewayService := service.NewGatewayService(accountRepository, groupRepository, usageLogRepository, usageBillingRepository, userRepository, userSubscriptionRepository, userGroupRateRepository, gatewayCache, configConfig, schedulerSnapshotService, concurrencyService, billingService, rateLimitService, billingCacheService, identityService, httpUpstream, deferredService, claudeTokenProvider, sessionLimitCache, rpmCache, digestSessionStore, settingService) + gatewayService := service.NewGatewayService(accountRepository, groupRepository, usageLogRepository, usageBillingRepository, userRepository, userSubscriptionRepository, userGroupRateRepository, gatewayCache, configConfig, schedulerSnapshotService, concurrencyService, billingService, rateLimitService, billingCacheService, identityService, httpUpstream, deferredService, claudeTokenProvider, sessionLimitCache, rpmCache, digestSessionStore, settingService, tlsFingerprintProfileService) openAITokenProvider := service.ProvideOpenAITokenProvider(accountRepository, geminiTokenCache, openAIOAuthService, oauthRefreshAPI) openAIGatewayService := service.NewOpenAIGatewayService(accountRepository, usageLogRepository, usageBillingRepository, userRepository, userSubscriptionRepository, userGroupRateRepository, gatewayCache, configConfig, schedulerSnapshotService, concurrencyService, billingService, rateLimitService, billingCacheService, httpUpstream, deferredService, openAITokenProvider) geminiMessagesCompatService := service.NewGeminiMessagesCompatService(accountRepository, groupRepository, gatewayCache, schedulerSnapshotService, geminiTokenProvider, rateLimitService, httpUpstream, antigravityGatewayService, configConfig) @@ -203,12 +206,13 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { errorPassthroughCache := repository.NewErrorPassthroughCache(redisClient) errorPassthroughService := service.NewErrorPassthroughService(errorPassthroughRepository, errorPassthroughCache) errorPassthroughHandler := admin.NewErrorPassthroughHandler(errorPassthroughService) + tlsFingerprintProfileHandler := admin.NewTLSFingerprintProfileHandler(tlsFingerprintProfileService) adminAPIKeyHandler := admin.NewAdminAPIKeyHandler(adminService) scheduledTestPlanRepository := repository.NewScheduledTestPlanRepository(db) scheduledTestResultRepository := repository.NewScheduledTestResultRepository(db) scheduledTestService := service.ProvideScheduledTestService(scheduledTestPlanRepository, scheduledTestResultRepository) scheduledTestHandler := admin.NewScheduledTestHandler(scheduledTestService) - adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, dataManagementHandler, backupHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, adminAPIKeyHandler, scheduledTestHandler) + adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, dataManagementHandler, backupHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, tlsFingerprintProfileHandler, adminAPIKeyHandler, scheduledTestHandler) usageRecordWorkerPool := service.NewUsageRecordWorkerPool(configConfig) userMsgQueueCache := repository.NewUserMsgQueueCache(redisClient) userMessageQueueService := service.ProvideUserMessageQueueService(userMsgQueueCache, rpmCache, configConfig) diff --git a/backend/ent/client.go b/backend/ent/client.go index 7ebbaa32..4129d6c5 100644 --- a/backend/ent/client.go +++ b/backend/ent/client.go @@ -29,6 +29,7 @@ import ( "github.com/Wei-Shaw/sub2api/ent/redeemcode" "github.com/Wei-Shaw/sub2api/ent/securitysecret" "github.com/Wei-Shaw/sub2api/ent/setting" + "github.com/Wei-Shaw/sub2api/ent/tlsfingerprintprofile" "github.com/Wei-Shaw/sub2api/ent/usagecleanuptask" "github.com/Wei-Shaw/sub2api/ent/usagelog" "github.com/Wei-Shaw/sub2api/ent/user" @@ -73,6 +74,8 @@ type Client struct { SecuritySecret *SecuritySecretClient // Setting is the client for interacting with the Setting builders. Setting *SettingClient + // TLSFingerprintProfile is the client for interacting with the TLSFingerprintProfile builders. + TLSFingerprintProfile *TLSFingerprintProfileClient // UsageCleanupTask is the client for interacting with the UsageCleanupTask builders. UsageCleanupTask *UsageCleanupTaskClient // UsageLog is the client for interacting with the UsageLog builders. @@ -112,6 +115,7 @@ func (c *Client) init() { c.RedeemCode = NewRedeemCodeClient(c.config) c.SecuritySecret = NewSecuritySecretClient(c.config) c.Setting = NewSettingClient(c.config) + c.TLSFingerprintProfile = NewTLSFingerprintProfileClient(c.config) c.UsageCleanupTask = NewUsageCleanupTaskClient(c.config) c.UsageLog = NewUsageLogClient(c.config) c.User = NewUserClient(c.config) @@ -225,6 +229,7 @@ func (c *Client) Tx(ctx context.Context) (*Tx, error) { RedeemCode: NewRedeemCodeClient(cfg), SecuritySecret: NewSecuritySecretClient(cfg), Setting: NewSettingClient(cfg), + TLSFingerprintProfile: NewTLSFingerprintProfileClient(cfg), UsageCleanupTask: NewUsageCleanupTaskClient(cfg), UsageLog: NewUsageLogClient(cfg), User: NewUserClient(cfg), @@ -265,6 +270,7 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) RedeemCode: NewRedeemCodeClient(cfg), SecuritySecret: NewSecuritySecretClient(cfg), Setting: NewSettingClient(cfg), + TLSFingerprintProfile: NewTLSFingerprintProfileClient(cfg), UsageCleanupTask: NewUsageCleanupTaskClient(cfg), UsageLog: NewUsageLogClient(cfg), User: NewUserClient(cfg), @@ -304,8 +310,9 @@ func (c *Client) Use(hooks ...Hook) { c.APIKey, c.Account, c.AccountGroup, c.Announcement, c.AnnouncementRead, c.ErrorPassthroughRule, c.Group, c.IdempotencyRecord, c.PromoCode, c.PromoCodeUsage, c.Proxy, c.RedeemCode, c.SecuritySecret, c.Setting, - c.UsageCleanupTask, c.UsageLog, c.User, c.UserAllowedGroup, - c.UserAttributeDefinition, c.UserAttributeValue, c.UserSubscription, + c.TLSFingerprintProfile, c.UsageCleanupTask, c.UsageLog, c.User, + c.UserAllowedGroup, c.UserAttributeDefinition, c.UserAttributeValue, + c.UserSubscription, } { n.Use(hooks...) } @@ -318,8 +325,9 @@ func (c *Client) Intercept(interceptors ...Interceptor) { c.APIKey, c.Account, c.AccountGroup, c.Announcement, c.AnnouncementRead, c.ErrorPassthroughRule, c.Group, c.IdempotencyRecord, c.PromoCode, c.PromoCodeUsage, c.Proxy, c.RedeemCode, c.SecuritySecret, c.Setting, - c.UsageCleanupTask, c.UsageLog, c.User, c.UserAllowedGroup, - c.UserAttributeDefinition, c.UserAttributeValue, c.UserSubscription, + c.TLSFingerprintProfile, c.UsageCleanupTask, c.UsageLog, c.User, + c.UserAllowedGroup, c.UserAttributeDefinition, c.UserAttributeValue, + c.UserSubscription, } { n.Intercept(interceptors...) } @@ -356,6 +364,8 @@ func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) { return c.SecuritySecret.mutate(ctx, m) case *SettingMutation: return c.Setting.mutate(ctx, m) + case *TLSFingerprintProfileMutation: + return c.TLSFingerprintProfile.mutate(ctx, m) case *UsageCleanupTaskMutation: return c.UsageCleanupTask.mutate(ctx, m) case *UsageLogMutation: @@ -2612,6 +2622,139 @@ func (c *SettingClient) mutate(ctx context.Context, m *SettingMutation) (Value, } } +// TLSFingerprintProfileClient is a client for the TLSFingerprintProfile schema. +type TLSFingerprintProfileClient struct { + config +} + +// NewTLSFingerprintProfileClient returns a client for the TLSFingerprintProfile from the given config. +func NewTLSFingerprintProfileClient(c config) *TLSFingerprintProfileClient { + return &TLSFingerprintProfileClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `tlsfingerprintprofile.Hooks(f(g(h())))`. +func (c *TLSFingerprintProfileClient) Use(hooks ...Hook) { + c.hooks.TLSFingerprintProfile = append(c.hooks.TLSFingerprintProfile, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `tlsfingerprintprofile.Intercept(f(g(h())))`. +func (c *TLSFingerprintProfileClient) Intercept(interceptors ...Interceptor) { + c.inters.TLSFingerprintProfile = append(c.inters.TLSFingerprintProfile, interceptors...) +} + +// Create returns a builder for creating a TLSFingerprintProfile entity. +func (c *TLSFingerprintProfileClient) Create() *TLSFingerprintProfileCreate { + mutation := newTLSFingerprintProfileMutation(c.config, OpCreate) + return &TLSFingerprintProfileCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of TLSFingerprintProfile entities. +func (c *TLSFingerprintProfileClient) CreateBulk(builders ...*TLSFingerprintProfileCreate) *TLSFingerprintProfileCreateBulk { + return &TLSFingerprintProfileCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *TLSFingerprintProfileClient) MapCreateBulk(slice any, setFunc func(*TLSFingerprintProfileCreate, int)) *TLSFingerprintProfileCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &TLSFingerprintProfileCreateBulk{err: fmt.Errorf("calling to TLSFingerprintProfileClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*TLSFingerprintProfileCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &TLSFingerprintProfileCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for TLSFingerprintProfile. +func (c *TLSFingerprintProfileClient) Update() *TLSFingerprintProfileUpdate { + mutation := newTLSFingerprintProfileMutation(c.config, OpUpdate) + return &TLSFingerprintProfileUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *TLSFingerprintProfileClient) UpdateOne(_m *TLSFingerprintProfile) *TLSFingerprintProfileUpdateOne { + mutation := newTLSFingerprintProfileMutation(c.config, OpUpdateOne, withTLSFingerprintProfile(_m)) + return &TLSFingerprintProfileUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *TLSFingerprintProfileClient) UpdateOneID(id int64) *TLSFingerprintProfileUpdateOne { + mutation := newTLSFingerprintProfileMutation(c.config, OpUpdateOne, withTLSFingerprintProfileID(id)) + return &TLSFingerprintProfileUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for TLSFingerprintProfile. +func (c *TLSFingerprintProfileClient) Delete() *TLSFingerprintProfileDelete { + mutation := newTLSFingerprintProfileMutation(c.config, OpDelete) + return &TLSFingerprintProfileDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *TLSFingerprintProfileClient) DeleteOne(_m *TLSFingerprintProfile) *TLSFingerprintProfileDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *TLSFingerprintProfileClient) DeleteOneID(id int64) *TLSFingerprintProfileDeleteOne { + builder := c.Delete().Where(tlsfingerprintprofile.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &TLSFingerprintProfileDeleteOne{builder} +} + +// Query returns a query builder for TLSFingerprintProfile. +func (c *TLSFingerprintProfileClient) Query() *TLSFingerprintProfileQuery { + return &TLSFingerprintProfileQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeTLSFingerprintProfile}, + inters: c.Interceptors(), + } +} + +// Get returns a TLSFingerprintProfile entity by its id. +func (c *TLSFingerprintProfileClient) Get(ctx context.Context, id int64) (*TLSFingerprintProfile, error) { + return c.Query().Where(tlsfingerprintprofile.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *TLSFingerprintProfileClient) GetX(ctx context.Context, id int64) *TLSFingerprintProfile { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *TLSFingerprintProfileClient) Hooks() []Hook { + return c.hooks.TLSFingerprintProfile +} + +// Interceptors returns the client interceptors. +func (c *TLSFingerprintProfileClient) Interceptors() []Interceptor { + return c.inters.TLSFingerprintProfile +} + +func (c *TLSFingerprintProfileClient) mutate(ctx context.Context, m *TLSFingerprintProfileMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&TLSFingerprintProfileCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&TLSFingerprintProfileUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&TLSFingerprintProfileUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&TLSFingerprintProfileDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown TLSFingerprintProfile mutation op: %q", m.Op()) + } +} + // UsageCleanupTaskClient is a client for the UsageCleanupTask schema. type UsageCleanupTaskClient struct { config @@ -3889,16 +4032,16 @@ type ( hooks struct { APIKey, Account, AccountGroup, Announcement, AnnouncementRead, ErrorPassthroughRule, Group, IdempotencyRecord, PromoCode, PromoCodeUsage, - Proxy, RedeemCode, SecuritySecret, Setting, UsageCleanupTask, UsageLog, User, - UserAllowedGroup, UserAttributeDefinition, UserAttributeValue, - UserSubscription []ent.Hook + Proxy, RedeemCode, SecuritySecret, Setting, TLSFingerprintProfile, + UsageCleanupTask, UsageLog, User, UserAllowedGroup, UserAttributeDefinition, + UserAttributeValue, UserSubscription []ent.Hook } inters struct { APIKey, Account, AccountGroup, Announcement, AnnouncementRead, ErrorPassthroughRule, Group, IdempotencyRecord, PromoCode, PromoCodeUsage, - Proxy, RedeemCode, SecuritySecret, Setting, UsageCleanupTask, UsageLog, User, - UserAllowedGroup, UserAttributeDefinition, UserAttributeValue, - UserSubscription []ent.Interceptor + Proxy, RedeemCode, SecuritySecret, Setting, TLSFingerprintProfile, + UsageCleanupTask, UsageLog, User, UserAllowedGroup, UserAttributeDefinition, + UserAttributeValue, UserSubscription []ent.Interceptor } ) diff --git a/backend/ent/ent.go b/backend/ent/ent.go index 5197e4d8..bdeaed8a 100644 --- a/backend/ent/ent.go +++ b/backend/ent/ent.go @@ -26,6 +26,7 @@ import ( "github.com/Wei-Shaw/sub2api/ent/redeemcode" "github.com/Wei-Shaw/sub2api/ent/securitysecret" "github.com/Wei-Shaw/sub2api/ent/setting" + "github.com/Wei-Shaw/sub2api/ent/tlsfingerprintprofile" "github.com/Wei-Shaw/sub2api/ent/usagecleanuptask" "github.com/Wei-Shaw/sub2api/ent/usagelog" "github.com/Wei-Shaw/sub2api/ent/user" @@ -107,6 +108,7 @@ func checkColumn(t, c string) error { redeemcode.Table: redeemcode.ValidColumn, securitysecret.Table: securitysecret.ValidColumn, setting.Table: setting.ValidColumn, + tlsfingerprintprofile.Table: tlsfingerprintprofile.ValidColumn, usagecleanuptask.Table: usagecleanuptask.ValidColumn, usagelog.Table: usagelog.ValidColumn, user.Table: user.ValidColumn, diff --git a/backend/ent/hook/hook.go b/backend/ent/hook/hook.go index 49d7f3c5..f6f7b4e9 100644 --- a/backend/ent/hook/hook.go +++ b/backend/ent/hook/hook.go @@ -177,6 +177,18 @@ func (f SettingFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, err return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.SettingMutation", m) } +// The TLSFingerprintProfileFunc type is an adapter to allow the use of ordinary +// function as TLSFingerprintProfile mutator. +type TLSFingerprintProfileFunc func(context.Context, *ent.TLSFingerprintProfileMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f TLSFingerprintProfileFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.TLSFingerprintProfileMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.TLSFingerprintProfileMutation", m) +} + // The UsageCleanupTaskFunc type is an adapter to allow the use of ordinary // function as UsageCleanupTask mutator. type UsageCleanupTaskFunc func(context.Context, *ent.UsageCleanupTaskMutation) (ent.Value, error) diff --git a/backend/ent/intercept/intercept.go b/backend/ent/intercept/intercept.go index e7746402..13169ca7 100644 --- a/backend/ent/intercept/intercept.go +++ b/backend/ent/intercept/intercept.go @@ -23,6 +23,7 @@ import ( "github.com/Wei-Shaw/sub2api/ent/redeemcode" "github.com/Wei-Shaw/sub2api/ent/securitysecret" "github.com/Wei-Shaw/sub2api/ent/setting" + "github.com/Wei-Shaw/sub2api/ent/tlsfingerprintprofile" "github.com/Wei-Shaw/sub2api/ent/usagecleanuptask" "github.com/Wei-Shaw/sub2api/ent/usagelog" "github.com/Wei-Shaw/sub2api/ent/user" @@ -466,6 +467,33 @@ func (f TraverseSetting) Traverse(ctx context.Context, q ent.Query) error { return fmt.Errorf("unexpected query type %T. expect *ent.SettingQuery", q) } +// The TLSFingerprintProfileFunc type is an adapter to allow the use of ordinary function as a Querier. +type TLSFingerprintProfileFunc func(context.Context, *ent.TLSFingerprintProfileQuery) (ent.Value, error) + +// Query calls f(ctx, q). +func (f TLSFingerprintProfileFunc) Query(ctx context.Context, q ent.Query) (ent.Value, error) { + if q, ok := q.(*ent.TLSFingerprintProfileQuery); ok { + return f(ctx, q) + } + return nil, fmt.Errorf("unexpected query type %T. expect *ent.TLSFingerprintProfileQuery", q) +} + +// The TraverseTLSFingerprintProfile type is an adapter to allow the use of ordinary function as Traverser. +type TraverseTLSFingerprintProfile func(context.Context, *ent.TLSFingerprintProfileQuery) error + +// Intercept is a dummy implementation of Intercept that returns the next Querier in the pipeline. +func (f TraverseTLSFingerprintProfile) Intercept(next ent.Querier) ent.Querier { + return next +} + +// Traverse calls f(ctx, q). +func (f TraverseTLSFingerprintProfile) Traverse(ctx context.Context, q ent.Query) error { + if q, ok := q.(*ent.TLSFingerprintProfileQuery); ok { + return f(ctx, q) + } + return fmt.Errorf("unexpected query type %T. expect *ent.TLSFingerprintProfileQuery", q) +} + // The UsageCleanupTaskFunc type is an adapter to allow the use of ordinary function as a Querier. type UsageCleanupTaskFunc func(context.Context, *ent.UsageCleanupTaskQuery) (ent.Value, error) @@ -686,6 +714,8 @@ func NewQuery(q ent.Query) (Query, error) { return &query[*ent.SecuritySecretQuery, predicate.SecuritySecret, securitysecret.OrderOption]{typ: ent.TypeSecuritySecret, tq: q}, nil case *ent.SettingQuery: return &query[*ent.SettingQuery, predicate.Setting, setting.OrderOption]{typ: ent.TypeSetting, tq: q}, nil + case *ent.TLSFingerprintProfileQuery: + return &query[*ent.TLSFingerprintProfileQuery, predicate.TLSFingerprintProfile, tlsfingerprintprofile.OrderOption]{typ: ent.TypeTLSFingerprintProfile, tq: q}, nil case *ent.UsageCleanupTaskQuery: return &query[*ent.UsageCleanupTaskQuery, predicate.UsageCleanupTask, usagecleanuptask.OrderOption]{typ: ent.TypeUsageCleanupTask, tq: q}, nil case *ent.UsageLogQuery: diff --git a/backend/ent/migrate/schema.go b/backend/ent/migrate/schema.go index c4f3af5e..c472d7e0 100644 --- a/backend/ent/migrate/schema.go +++ b/backend/ent/migrate/schema.go @@ -673,6 +673,30 @@ var ( Columns: SettingsColumns, PrimaryKey: []*schema.Column{SettingsColumns[0]}, } + // TLSFingerprintProfilesColumns holds the columns for the "tls_fingerprint_profiles" table. + TLSFingerprintProfilesColumns = []*schema.Column{ + {Name: "id", Type: field.TypeInt64, Increment: true}, + {Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}}, + {Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}}, + {Name: "name", Type: field.TypeString, Unique: true, Size: 100}, + {Name: "description", Type: field.TypeString, Nullable: true, Size: 2147483647}, + {Name: "enable_grease", Type: field.TypeBool, Default: false}, + {Name: "cipher_suites", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, + {Name: "curves", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, + {Name: "point_formats", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, + {Name: "signature_algorithms", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, + {Name: "alpn_protocols", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, + {Name: "supported_versions", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, + {Name: "key_share_groups", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, + {Name: "psk_modes", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, + {Name: "extensions", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, + } + // TLSFingerprintProfilesTable holds the schema information for the "tls_fingerprint_profiles" table. + TLSFingerprintProfilesTable = &schema.Table{ + Name: "tls_fingerprint_profiles", + Columns: TLSFingerprintProfilesColumns, + PrimaryKey: []*schema.Column{TLSFingerprintProfilesColumns[0]}, + } // UsageCleanupTasksColumns holds the columns for the "usage_cleanup_tasks" table. UsageCleanupTasksColumns = []*schema.Column{ {Name: "id", Type: field.TypeInt64, Increment: true}, @@ -1111,6 +1135,7 @@ var ( RedeemCodesTable, SecuritySecretsTable, SettingsTable, + TLSFingerprintProfilesTable, UsageCleanupTasksTable, UsageLogsTable, UsersTable, @@ -1175,6 +1200,9 @@ func init() { SettingsTable.Annotation = &entsql.Annotation{ Table: "settings", } + TLSFingerprintProfilesTable.Annotation = &entsql.Annotation{ + Table: "tls_fingerprint_profiles", + } UsageCleanupTasksTable.Annotation = &entsql.Annotation{ Table: "usage_cleanup_tasks", } diff --git a/backend/ent/mutation.go b/backend/ent/mutation.go index 10f7afe4..42c63c2e 100644 --- a/backend/ent/mutation.go +++ b/backend/ent/mutation.go @@ -27,6 +27,7 @@ import ( "github.com/Wei-Shaw/sub2api/ent/redeemcode" "github.com/Wei-Shaw/sub2api/ent/securitysecret" "github.com/Wei-Shaw/sub2api/ent/setting" + "github.com/Wei-Shaw/sub2api/ent/tlsfingerprintprofile" "github.com/Wei-Shaw/sub2api/ent/usagecleanuptask" "github.com/Wei-Shaw/sub2api/ent/usagelog" "github.com/Wei-Shaw/sub2api/ent/user" @@ -60,6 +61,7 @@ const ( TypeRedeemCode = "RedeemCode" TypeSecuritySecret = "SecuritySecret" TypeSetting = "Setting" + TypeTLSFingerprintProfile = "TLSFingerprintProfile" TypeUsageCleanupTask = "UsageCleanupTask" TypeUsageLog = "UsageLog" TypeUser = "User" @@ -17148,6 +17150,1380 @@ func (m *SettingMutation) ResetEdge(name string) error { return fmt.Errorf("unknown Setting edge %s", name) } +// TLSFingerprintProfileMutation represents an operation that mutates the TLSFingerprintProfile nodes in the graph. +type TLSFingerprintProfileMutation struct { + config + op Op + typ string + id *int64 + created_at *time.Time + updated_at *time.Time + name *string + description *string + enable_grease *bool + cipher_suites *[]uint16 + appendcipher_suites []uint16 + curves *[]uint16 + appendcurves []uint16 + point_formats *[]uint16 + appendpoint_formats []uint16 + signature_algorithms *[]uint16 + appendsignature_algorithms []uint16 + alpn_protocols *[]string + appendalpn_protocols []string + supported_versions *[]uint16 + appendsupported_versions []uint16 + key_share_groups *[]uint16 + appendkey_share_groups []uint16 + psk_modes *[]uint16 + appendpsk_modes []uint16 + extensions *[]uint16 + appendextensions []uint16 + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*TLSFingerprintProfile, error) + predicates []predicate.TLSFingerprintProfile +} + +var _ ent.Mutation = (*TLSFingerprintProfileMutation)(nil) + +// tlsfingerprintprofileOption allows management of the mutation configuration using functional options. +type tlsfingerprintprofileOption func(*TLSFingerprintProfileMutation) + +// newTLSFingerprintProfileMutation creates new mutation for the TLSFingerprintProfile entity. +func newTLSFingerprintProfileMutation(c config, op Op, opts ...tlsfingerprintprofileOption) *TLSFingerprintProfileMutation { + m := &TLSFingerprintProfileMutation{ + config: c, + op: op, + typ: TypeTLSFingerprintProfile, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withTLSFingerprintProfileID sets the ID field of the mutation. +func withTLSFingerprintProfileID(id int64) tlsfingerprintprofileOption { + return func(m *TLSFingerprintProfileMutation) { + var ( + err error + once sync.Once + value *TLSFingerprintProfile + ) + m.oldValue = func(ctx context.Context) (*TLSFingerprintProfile, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().TLSFingerprintProfile.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withTLSFingerprintProfile sets the old TLSFingerprintProfile of the mutation. +func withTLSFingerprintProfile(node *TLSFingerprintProfile) tlsfingerprintprofileOption { + return func(m *TLSFingerprintProfileMutation) { + m.oldValue = func(context.Context) (*TLSFingerprintProfile, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m TLSFingerprintProfileMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m TLSFingerprintProfileMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *TLSFingerprintProfileMutation) ID() (id int64, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *TLSFingerprintProfileMutation) IDs(ctx context.Context) ([]int64, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []int64{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().TLSFingerprintProfile.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetCreatedAt sets the "created_at" field. +func (m *TLSFingerprintProfileMutation) SetCreatedAt(t time.Time) { + m.created_at = &t +} + +// CreatedAt returns the value of the "created_at" field in the mutation. +func (m *TLSFingerprintProfileMutation) CreatedAt() (r time.Time, exists bool) { + v := m.created_at + if v == nil { + return + } + return *v, true +} + +// OldCreatedAt returns the old "created_at" field's value of the TLSFingerprintProfile entity. +// If the TLSFingerprintProfile object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TLSFingerprintProfileMutation) OldCreatedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedAt: %w", err) + } + return oldValue.CreatedAt, nil +} + +// ResetCreatedAt resets all changes to the "created_at" field. +func (m *TLSFingerprintProfileMutation) ResetCreatedAt() { + m.created_at = nil +} + +// SetUpdatedAt sets the "updated_at" field. +func (m *TLSFingerprintProfileMutation) SetUpdatedAt(t time.Time) { + m.updated_at = &t +} + +// UpdatedAt returns the value of the "updated_at" field in the mutation. +func (m *TLSFingerprintProfileMutation) UpdatedAt() (r time.Time, exists bool) { + v := m.updated_at + if v == nil { + return + } + return *v, true +} + +// OldUpdatedAt returns the old "updated_at" field's value of the TLSFingerprintProfile entity. +// If the TLSFingerprintProfile object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TLSFingerprintProfileMutation) OldUpdatedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUpdatedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUpdatedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUpdatedAt: %w", err) + } + return oldValue.UpdatedAt, nil +} + +// ResetUpdatedAt resets all changes to the "updated_at" field. +func (m *TLSFingerprintProfileMutation) ResetUpdatedAt() { + m.updated_at = nil +} + +// SetName sets the "name" field. +func (m *TLSFingerprintProfileMutation) SetName(s string) { + m.name = &s +} + +// Name returns the value of the "name" field in the mutation. +func (m *TLSFingerprintProfileMutation) Name() (r string, exists bool) { + v := m.name + if v == nil { + return + } + return *v, true +} + +// OldName returns the old "name" field's value of the TLSFingerprintProfile entity. +// If the TLSFingerprintProfile object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TLSFingerprintProfileMutation) OldName(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldName is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldName requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldName: %w", err) + } + return oldValue.Name, nil +} + +// ResetName resets all changes to the "name" field. +func (m *TLSFingerprintProfileMutation) ResetName() { + m.name = nil +} + +// SetDescription sets the "description" field. +func (m *TLSFingerprintProfileMutation) SetDescription(s string) { + m.description = &s +} + +// Description returns the value of the "description" field in the mutation. +func (m *TLSFingerprintProfileMutation) Description() (r string, exists bool) { + v := m.description + if v == nil { + return + } + return *v, true +} + +// OldDescription returns the old "description" field's value of the TLSFingerprintProfile entity. +// If the TLSFingerprintProfile object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TLSFingerprintProfileMutation) OldDescription(ctx context.Context) (v *string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDescription is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDescription requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDescription: %w", err) + } + return oldValue.Description, nil +} + +// ClearDescription clears the value of the "description" field. +func (m *TLSFingerprintProfileMutation) ClearDescription() { + m.description = nil + m.clearedFields[tlsfingerprintprofile.FieldDescription] = struct{}{} +} + +// DescriptionCleared returns if the "description" field was cleared in this mutation. +func (m *TLSFingerprintProfileMutation) DescriptionCleared() bool { + _, ok := m.clearedFields[tlsfingerprintprofile.FieldDescription] + return ok +} + +// ResetDescription resets all changes to the "description" field. +func (m *TLSFingerprintProfileMutation) ResetDescription() { + m.description = nil + delete(m.clearedFields, tlsfingerprintprofile.FieldDescription) +} + +// SetEnableGrease sets the "enable_grease" field. +func (m *TLSFingerprintProfileMutation) SetEnableGrease(b bool) { + m.enable_grease = &b +} + +// EnableGrease returns the value of the "enable_grease" field in the mutation. +func (m *TLSFingerprintProfileMutation) EnableGrease() (r bool, exists bool) { + v := m.enable_grease + if v == nil { + return + } + return *v, true +} + +// OldEnableGrease returns the old "enable_grease" field's value of the TLSFingerprintProfile entity. +// If the TLSFingerprintProfile object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TLSFingerprintProfileMutation) OldEnableGrease(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldEnableGrease is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldEnableGrease requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldEnableGrease: %w", err) + } + return oldValue.EnableGrease, nil +} + +// ResetEnableGrease resets all changes to the "enable_grease" field. +func (m *TLSFingerprintProfileMutation) ResetEnableGrease() { + m.enable_grease = nil +} + +// SetCipherSuites sets the "cipher_suites" field. +func (m *TLSFingerprintProfileMutation) SetCipherSuites(u []uint16) { + m.cipher_suites = &u + m.appendcipher_suites = nil +} + +// CipherSuites returns the value of the "cipher_suites" field in the mutation. +func (m *TLSFingerprintProfileMutation) CipherSuites() (r []uint16, exists bool) { + v := m.cipher_suites + if v == nil { + return + } + return *v, true +} + +// OldCipherSuites returns the old "cipher_suites" field's value of the TLSFingerprintProfile entity. +// If the TLSFingerprintProfile object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TLSFingerprintProfileMutation) OldCipherSuites(ctx context.Context) (v []uint16, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCipherSuites is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCipherSuites requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCipherSuites: %w", err) + } + return oldValue.CipherSuites, nil +} + +// AppendCipherSuites adds u to the "cipher_suites" field. +func (m *TLSFingerprintProfileMutation) AppendCipherSuites(u []uint16) { + m.appendcipher_suites = append(m.appendcipher_suites, u...) +} + +// AppendedCipherSuites returns the list of values that were appended to the "cipher_suites" field in this mutation. +func (m *TLSFingerprintProfileMutation) AppendedCipherSuites() ([]uint16, bool) { + if len(m.appendcipher_suites) == 0 { + return nil, false + } + return m.appendcipher_suites, true +} + +// ClearCipherSuites clears the value of the "cipher_suites" field. +func (m *TLSFingerprintProfileMutation) ClearCipherSuites() { + m.cipher_suites = nil + m.appendcipher_suites = nil + m.clearedFields[tlsfingerprintprofile.FieldCipherSuites] = struct{}{} +} + +// CipherSuitesCleared returns if the "cipher_suites" field was cleared in this mutation. +func (m *TLSFingerprintProfileMutation) CipherSuitesCleared() bool { + _, ok := m.clearedFields[tlsfingerprintprofile.FieldCipherSuites] + return ok +} + +// ResetCipherSuites resets all changes to the "cipher_suites" field. +func (m *TLSFingerprintProfileMutation) ResetCipherSuites() { + m.cipher_suites = nil + m.appendcipher_suites = nil + delete(m.clearedFields, tlsfingerprintprofile.FieldCipherSuites) +} + +// SetCurves sets the "curves" field. +func (m *TLSFingerprintProfileMutation) SetCurves(u []uint16) { + m.curves = &u + m.appendcurves = nil +} + +// Curves returns the value of the "curves" field in the mutation. +func (m *TLSFingerprintProfileMutation) Curves() (r []uint16, exists bool) { + v := m.curves + if v == nil { + return + } + return *v, true +} + +// OldCurves returns the old "curves" field's value of the TLSFingerprintProfile entity. +// If the TLSFingerprintProfile object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TLSFingerprintProfileMutation) OldCurves(ctx context.Context) (v []uint16, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCurves is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCurves requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCurves: %w", err) + } + return oldValue.Curves, nil +} + +// AppendCurves adds u to the "curves" field. +func (m *TLSFingerprintProfileMutation) AppendCurves(u []uint16) { + m.appendcurves = append(m.appendcurves, u...) +} + +// AppendedCurves returns the list of values that were appended to the "curves" field in this mutation. +func (m *TLSFingerprintProfileMutation) AppendedCurves() ([]uint16, bool) { + if len(m.appendcurves) == 0 { + return nil, false + } + return m.appendcurves, true +} + +// ClearCurves clears the value of the "curves" field. +func (m *TLSFingerprintProfileMutation) ClearCurves() { + m.curves = nil + m.appendcurves = nil + m.clearedFields[tlsfingerprintprofile.FieldCurves] = struct{}{} +} + +// CurvesCleared returns if the "curves" field was cleared in this mutation. +func (m *TLSFingerprintProfileMutation) CurvesCleared() bool { + _, ok := m.clearedFields[tlsfingerprintprofile.FieldCurves] + return ok +} + +// ResetCurves resets all changes to the "curves" field. +func (m *TLSFingerprintProfileMutation) ResetCurves() { + m.curves = nil + m.appendcurves = nil + delete(m.clearedFields, tlsfingerprintprofile.FieldCurves) +} + +// SetPointFormats sets the "point_formats" field. +func (m *TLSFingerprintProfileMutation) SetPointFormats(u []uint16) { + m.point_formats = &u + m.appendpoint_formats = nil +} + +// PointFormats returns the value of the "point_formats" field in the mutation. +func (m *TLSFingerprintProfileMutation) PointFormats() (r []uint16, exists bool) { + v := m.point_formats + if v == nil { + return + } + return *v, true +} + +// OldPointFormats returns the old "point_formats" field's value of the TLSFingerprintProfile entity. +// If the TLSFingerprintProfile object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TLSFingerprintProfileMutation) OldPointFormats(ctx context.Context) (v []uint16, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldPointFormats is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldPointFormats requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldPointFormats: %w", err) + } + return oldValue.PointFormats, nil +} + +// AppendPointFormats adds u to the "point_formats" field. +func (m *TLSFingerprintProfileMutation) AppendPointFormats(u []uint16) { + m.appendpoint_formats = append(m.appendpoint_formats, u...) +} + +// AppendedPointFormats returns the list of values that were appended to the "point_formats" field in this mutation. +func (m *TLSFingerprintProfileMutation) AppendedPointFormats() ([]uint16, bool) { + if len(m.appendpoint_formats) == 0 { + return nil, false + } + return m.appendpoint_formats, true +} + +// ClearPointFormats clears the value of the "point_formats" field. +func (m *TLSFingerprintProfileMutation) ClearPointFormats() { + m.point_formats = nil + m.appendpoint_formats = nil + m.clearedFields[tlsfingerprintprofile.FieldPointFormats] = struct{}{} +} + +// PointFormatsCleared returns if the "point_formats" field was cleared in this mutation. +func (m *TLSFingerprintProfileMutation) PointFormatsCleared() bool { + _, ok := m.clearedFields[tlsfingerprintprofile.FieldPointFormats] + return ok +} + +// ResetPointFormats resets all changes to the "point_formats" field. +func (m *TLSFingerprintProfileMutation) ResetPointFormats() { + m.point_formats = nil + m.appendpoint_formats = nil + delete(m.clearedFields, tlsfingerprintprofile.FieldPointFormats) +} + +// SetSignatureAlgorithms sets the "signature_algorithms" field. +func (m *TLSFingerprintProfileMutation) SetSignatureAlgorithms(u []uint16) { + m.signature_algorithms = &u + m.appendsignature_algorithms = nil +} + +// SignatureAlgorithms returns the value of the "signature_algorithms" field in the mutation. +func (m *TLSFingerprintProfileMutation) SignatureAlgorithms() (r []uint16, exists bool) { + v := m.signature_algorithms + if v == nil { + return + } + return *v, true +} + +// OldSignatureAlgorithms returns the old "signature_algorithms" field's value of the TLSFingerprintProfile entity. +// If the TLSFingerprintProfile object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TLSFingerprintProfileMutation) OldSignatureAlgorithms(ctx context.Context) (v []uint16, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSignatureAlgorithms is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSignatureAlgorithms requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSignatureAlgorithms: %w", err) + } + return oldValue.SignatureAlgorithms, nil +} + +// AppendSignatureAlgorithms adds u to the "signature_algorithms" field. +func (m *TLSFingerprintProfileMutation) AppendSignatureAlgorithms(u []uint16) { + m.appendsignature_algorithms = append(m.appendsignature_algorithms, u...) +} + +// AppendedSignatureAlgorithms returns the list of values that were appended to the "signature_algorithms" field in this mutation. +func (m *TLSFingerprintProfileMutation) AppendedSignatureAlgorithms() ([]uint16, bool) { + if len(m.appendsignature_algorithms) == 0 { + return nil, false + } + return m.appendsignature_algorithms, true +} + +// ClearSignatureAlgorithms clears the value of the "signature_algorithms" field. +func (m *TLSFingerprintProfileMutation) ClearSignatureAlgorithms() { + m.signature_algorithms = nil + m.appendsignature_algorithms = nil + m.clearedFields[tlsfingerprintprofile.FieldSignatureAlgorithms] = struct{}{} +} + +// SignatureAlgorithmsCleared returns if the "signature_algorithms" field was cleared in this mutation. +func (m *TLSFingerprintProfileMutation) SignatureAlgorithmsCleared() bool { + _, ok := m.clearedFields[tlsfingerprintprofile.FieldSignatureAlgorithms] + return ok +} + +// ResetSignatureAlgorithms resets all changes to the "signature_algorithms" field. +func (m *TLSFingerprintProfileMutation) ResetSignatureAlgorithms() { + m.signature_algorithms = nil + m.appendsignature_algorithms = nil + delete(m.clearedFields, tlsfingerprintprofile.FieldSignatureAlgorithms) +} + +// SetAlpnProtocols sets the "alpn_protocols" field. +func (m *TLSFingerprintProfileMutation) SetAlpnProtocols(s []string) { + m.alpn_protocols = &s + m.appendalpn_protocols = nil +} + +// AlpnProtocols returns the value of the "alpn_protocols" field in the mutation. +func (m *TLSFingerprintProfileMutation) AlpnProtocols() (r []string, exists bool) { + v := m.alpn_protocols + if v == nil { + return + } + return *v, true +} + +// OldAlpnProtocols returns the old "alpn_protocols" field's value of the TLSFingerprintProfile entity. +// If the TLSFingerprintProfile object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TLSFingerprintProfileMutation) OldAlpnProtocols(ctx context.Context) (v []string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAlpnProtocols is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAlpnProtocols requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAlpnProtocols: %w", err) + } + return oldValue.AlpnProtocols, nil +} + +// AppendAlpnProtocols adds s to the "alpn_protocols" field. +func (m *TLSFingerprintProfileMutation) AppendAlpnProtocols(s []string) { + m.appendalpn_protocols = append(m.appendalpn_protocols, s...) +} + +// AppendedAlpnProtocols returns the list of values that were appended to the "alpn_protocols" field in this mutation. +func (m *TLSFingerprintProfileMutation) AppendedAlpnProtocols() ([]string, bool) { + if len(m.appendalpn_protocols) == 0 { + return nil, false + } + return m.appendalpn_protocols, true +} + +// ClearAlpnProtocols clears the value of the "alpn_protocols" field. +func (m *TLSFingerprintProfileMutation) ClearAlpnProtocols() { + m.alpn_protocols = nil + m.appendalpn_protocols = nil + m.clearedFields[tlsfingerprintprofile.FieldAlpnProtocols] = struct{}{} +} + +// AlpnProtocolsCleared returns if the "alpn_protocols" field was cleared in this mutation. +func (m *TLSFingerprintProfileMutation) AlpnProtocolsCleared() bool { + _, ok := m.clearedFields[tlsfingerprintprofile.FieldAlpnProtocols] + return ok +} + +// ResetAlpnProtocols resets all changes to the "alpn_protocols" field. +func (m *TLSFingerprintProfileMutation) ResetAlpnProtocols() { + m.alpn_protocols = nil + m.appendalpn_protocols = nil + delete(m.clearedFields, tlsfingerprintprofile.FieldAlpnProtocols) +} + +// SetSupportedVersions sets the "supported_versions" field. +func (m *TLSFingerprintProfileMutation) SetSupportedVersions(u []uint16) { + m.supported_versions = &u + m.appendsupported_versions = nil +} + +// SupportedVersions returns the value of the "supported_versions" field in the mutation. +func (m *TLSFingerprintProfileMutation) SupportedVersions() (r []uint16, exists bool) { + v := m.supported_versions + if v == nil { + return + } + return *v, true +} + +// OldSupportedVersions returns the old "supported_versions" field's value of the TLSFingerprintProfile entity. +// If the TLSFingerprintProfile object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TLSFingerprintProfileMutation) OldSupportedVersions(ctx context.Context) (v []uint16, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSupportedVersions is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSupportedVersions requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSupportedVersions: %w", err) + } + return oldValue.SupportedVersions, nil +} + +// AppendSupportedVersions adds u to the "supported_versions" field. +func (m *TLSFingerprintProfileMutation) AppendSupportedVersions(u []uint16) { + m.appendsupported_versions = append(m.appendsupported_versions, u...) +} + +// AppendedSupportedVersions returns the list of values that were appended to the "supported_versions" field in this mutation. +func (m *TLSFingerprintProfileMutation) AppendedSupportedVersions() ([]uint16, bool) { + if len(m.appendsupported_versions) == 0 { + return nil, false + } + return m.appendsupported_versions, true +} + +// ClearSupportedVersions clears the value of the "supported_versions" field. +func (m *TLSFingerprintProfileMutation) ClearSupportedVersions() { + m.supported_versions = nil + m.appendsupported_versions = nil + m.clearedFields[tlsfingerprintprofile.FieldSupportedVersions] = struct{}{} +} + +// SupportedVersionsCleared returns if the "supported_versions" field was cleared in this mutation. +func (m *TLSFingerprintProfileMutation) SupportedVersionsCleared() bool { + _, ok := m.clearedFields[tlsfingerprintprofile.FieldSupportedVersions] + return ok +} + +// ResetSupportedVersions resets all changes to the "supported_versions" field. +func (m *TLSFingerprintProfileMutation) ResetSupportedVersions() { + m.supported_versions = nil + m.appendsupported_versions = nil + delete(m.clearedFields, tlsfingerprintprofile.FieldSupportedVersions) +} + +// SetKeyShareGroups sets the "key_share_groups" field. +func (m *TLSFingerprintProfileMutation) SetKeyShareGroups(u []uint16) { + m.key_share_groups = &u + m.appendkey_share_groups = nil +} + +// KeyShareGroups returns the value of the "key_share_groups" field in the mutation. +func (m *TLSFingerprintProfileMutation) KeyShareGroups() (r []uint16, exists bool) { + v := m.key_share_groups + if v == nil { + return + } + return *v, true +} + +// OldKeyShareGroups returns the old "key_share_groups" field's value of the TLSFingerprintProfile entity. +// If the TLSFingerprintProfile object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TLSFingerprintProfileMutation) OldKeyShareGroups(ctx context.Context) (v []uint16, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldKeyShareGroups is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldKeyShareGroups requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldKeyShareGroups: %w", err) + } + return oldValue.KeyShareGroups, nil +} + +// AppendKeyShareGroups adds u to the "key_share_groups" field. +func (m *TLSFingerprintProfileMutation) AppendKeyShareGroups(u []uint16) { + m.appendkey_share_groups = append(m.appendkey_share_groups, u...) +} + +// AppendedKeyShareGroups returns the list of values that were appended to the "key_share_groups" field in this mutation. +func (m *TLSFingerprintProfileMutation) AppendedKeyShareGroups() ([]uint16, bool) { + if len(m.appendkey_share_groups) == 0 { + return nil, false + } + return m.appendkey_share_groups, true +} + +// ClearKeyShareGroups clears the value of the "key_share_groups" field. +func (m *TLSFingerprintProfileMutation) ClearKeyShareGroups() { + m.key_share_groups = nil + m.appendkey_share_groups = nil + m.clearedFields[tlsfingerprintprofile.FieldKeyShareGroups] = struct{}{} +} + +// KeyShareGroupsCleared returns if the "key_share_groups" field was cleared in this mutation. +func (m *TLSFingerprintProfileMutation) KeyShareGroupsCleared() bool { + _, ok := m.clearedFields[tlsfingerprintprofile.FieldKeyShareGroups] + return ok +} + +// ResetKeyShareGroups resets all changes to the "key_share_groups" field. +func (m *TLSFingerprintProfileMutation) ResetKeyShareGroups() { + m.key_share_groups = nil + m.appendkey_share_groups = nil + delete(m.clearedFields, tlsfingerprintprofile.FieldKeyShareGroups) +} + +// SetPskModes sets the "psk_modes" field. +func (m *TLSFingerprintProfileMutation) SetPskModes(u []uint16) { + m.psk_modes = &u + m.appendpsk_modes = nil +} + +// PskModes returns the value of the "psk_modes" field in the mutation. +func (m *TLSFingerprintProfileMutation) PskModes() (r []uint16, exists bool) { + v := m.psk_modes + if v == nil { + return + } + return *v, true +} + +// OldPskModes returns the old "psk_modes" field's value of the TLSFingerprintProfile entity. +// If the TLSFingerprintProfile object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TLSFingerprintProfileMutation) OldPskModes(ctx context.Context) (v []uint16, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldPskModes is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldPskModes requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldPskModes: %w", err) + } + return oldValue.PskModes, nil +} + +// AppendPskModes adds u to the "psk_modes" field. +func (m *TLSFingerprintProfileMutation) AppendPskModes(u []uint16) { + m.appendpsk_modes = append(m.appendpsk_modes, u...) +} + +// AppendedPskModes returns the list of values that were appended to the "psk_modes" field in this mutation. +func (m *TLSFingerprintProfileMutation) AppendedPskModes() ([]uint16, bool) { + if len(m.appendpsk_modes) == 0 { + return nil, false + } + return m.appendpsk_modes, true +} + +// ClearPskModes clears the value of the "psk_modes" field. +func (m *TLSFingerprintProfileMutation) ClearPskModes() { + m.psk_modes = nil + m.appendpsk_modes = nil + m.clearedFields[tlsfingerprintprofile.FieldPskModes] = struct{}{} +} + +// PskModesCleared returns if the "psk_modes" field was cleared in this mutation. +func (m *TLSFingerprintProfileMutation) PskModesCleared() bool { + _, ok := m.clearedFields[tlsfingerprintprofile.FieldPskModes] + return ok +} + +// ResetPskModes resets all changes to the "psk_modes" field. +func (m *TLSFingerprintProfileMutation) ResetPskModes() { + m.psk_modes = nil + m.appendpsk_modes = nil + delete(m.clearedFields, tlsfingerprintprofile.FieldPskModes) +} + +// SetExtensions sets the "extensions" field. +func (m *TLSFingerprintProfileMutation) SetExtensions(u []uint16) { + m.extensions = &u + m.appendextensions = nil +} + +// Extensions returns the value of the "extensions" field in the mutation. +func (m *TLSFingerprintProfileMutation) Extensions() (r []uint16, exists bool) { + v := m.extensions + if v == nil { + return + } + return *v, true +} + +// OldExtensions returns the old "extensions" field's value of the TLSFingerprintProfile entity. +// If the TLSFingerprintProfile object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *TLSFingerprintProfileMutation) OldExtensions(ctx context.Context) (v []uint16, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldExtensions is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldExtensions requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldExtensions: %w", err) + } + return oldValue.Extensions, nil +} + +// AppendExtensions adds u to the "extensions" field. +func (m *TLSFingerprintProfileMutation) AppendExtensions(u []uint16) { + m.appendextensions = append(m.appendextensions, u...) +} + +// AppendedExtensions returns the list of values that were appended to the "extensions" field in this mutation. +func (m *TLSFingerprintProfileMutation) AppendedExtensions() ([]uint16, bool) { + if len(m.appendextensions) == 0 { + return nil, false + } + return m.appendextensions, true +} + +// ClearExtensions clears the value of the "extensions" field. +func (m *TLSFingerprintProfileMutation) ClearExtensions() { + m.extensions = nil + m.appendextensions = nil + m.clearedFields[tlsfingerprintprofile.FieldExtensions] = struct{}{} +} + +// ExtensionsCleared returns if the "extensions" field was cleared in this mutation. +func (m *TLSFingerprintProfileMutation) ExtensionsCleared() bool { + _, ok := m.clearedFields[tlsfingerprintprofile.FieldExtensions] + return ok +} + +// ResetExtensions resets all changes to the "extensions" field. +func (m *TLSFingerprintProfileMutation) ResetExtensions() { + m.extensions = nil + m.appendextensions = nil + delete(m.clearedFields, tlsfingerprintprofile.FieldExtensions) +} + +// Where appends a list predicates to the TLSFingerprintProfileMutation builder. +func (m *TLSFingerprintProfileMutation) Where(ps ...predicate.TLSFingerprintProfile) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the TLSFingerprintProfileMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *TLSFingerprintProfileMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.TLSFingerprintProfile, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *TLSFingerprintProfileMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *TLSFingerprintProfileMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (TLSFingerprintProfile). +func (m *TLSFingerprintProfileMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *TLSFingerprintProfileMutation) Fields() []string { + fields := make([]string, 0, 14) + if m.created_at != nil { + fields = append(fields, tlsfingerprintprofile.FieldCreatedAt) + } + if m.updated_at != nil { + fields = append(fields, tlsfingerprintprofile.FieldUpdatedAt) + } + if m.name != nil { + fields = append(fields, tlsfingerprintprofile.FieldName) + } + if m.description != nil { + fields = append(fields, tlsfingerprintprofile.FieldDescription) + } + if m.enable_grease != nil { + fields = append(fields, tlsfingerprintprofile.FieldEnableGrease) + } + if m.cipher_suites != nil { + fields = append(fields, tlsfingerprintprofile.FieldCipherSuites) + } + if m.curves != nil { + fields = append(fields, tlsfingerprintprofile.FieldCurves) + } + if m.point_formats != nil { + fields = append(fields, tlsfingerprintprofile.FieldPointFormats) + } + if m.signature_algorithms != nil { + fields = append(fields, tlsfingerprintprofile.FieldSignatureAlgorithms) + } + if m.alpn_protocols != nil { + fields = append(fields, tlsfingerprintprofile.FieldAlpnProtocols) + } + if m.supported_versions != nil { + fields = append(fields, tlsfingerprintprofile.FieldSupportedVersions) + } + if m.key_share_groups != nil { + fields = append(fields, tlsfingerprintprofile.FieldKeyShareGroups) + } + if m.psk_modes != nil { + fields = append(fields, tlsfingerprintprofile.FieldPskModes) + } + if m.extensions != nil { + fields = append(fields, tlsfingerprintprofile.FieldExtensions) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *TLSFingerprintProfileMutation) Field(name string) (ent.Value, bool) { + switch name { + case tlsfingerprintprofile.FieldCreatedAt: + return m.CreatedAt() + case tlsfingerprintprofile.FieldUpdatedAt: + return m.UpdatedAt() + case tlsfingerprintprofile.FieldName: + return m.Name() + case tlsfingerprintprofile.FieldDescription: + return m.Description() + case tlsfingerprintprofile.FieldEnableGrease: + return m.EnableGrease() + case tlsfingerprintprofile.FieldCipherSuites: + return m.CipherSuites() + case tlsfingerprintprofile.FieldCurves: + return m.Curves() + case tlsfingerprintprofile.FieldPointFormats: + return m.PointFormats() + case tlsfingerprintprofile.FieldSignatureAlgorithms: + return m.SignatureAlgorithms() + case tlsfingerprintprofile.FieldAlpnProtocols: + return m.AlpnProtocols() + case tlsfingerprintprofile.FieldSupportedVersions: + return m.SupportedVersions() + case tlsfingerprintprofile.FieldKeyShareGroups: + return m.KeyShareGroups() + case tlsfingerprintprofile.FieldPskModes: + return m.PskModes() + case tlsfingerprintprofile.FieldExtensions: + return m.Extensions() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *TLSFingerprintProfileMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case tlsfingerprintprofile.FieldCreatedAt: + return m.OldCreatedAt(ctx) + case tlsfingerprintprofile.FieldUpdatedAt: + return m.OldUpdatedAt(ctx) + case tlsfingerprintprofile.FieldName: + return m.OldName(ctx) + case tlsfingerprintprofile.FieldDescription: + return m.OldDescription(ctx) + case tlsfingerprintprofile.FieldEnableGrease: + return m.OldEnableGrease(ctx) + case tlsfingerprintprofile.FieldCipherSuites: + return m.OldCipherSuites(ctx) + case tlsfingerprintprofile.FieldCurves: + return m.OldCurves(ctx) + case tlsfingerprintprofile.FieldPointFormats: + return m.OldPointFormats(ctx) + case tlsfingerprintprofile.FieldSignatureAlgorithms: + return m.OldSignatureAlgorithms(ctx) + case tlsfingerprintprofile.FieldAlpnProtocols: + return m.OldAlpnProtocols(ctx) + case tlsfingerprintprofile.FieldSupportedVersions: + return m.OldSupportedVersions(ctx) + case tlsfingerprintprofile.FieldKeyShareGroups: + return m.OldKeyShareGroups(ctx) + case tlsfingerprintprofile.FieldPskModes: + return m.OldPskModes(ctx) + case tlsfingerprintprofile.FieldExtensions: + return m.OldExtensions(ctx) + } + return nil, fmt.Errorf("unknown TLSFingerprintProfile field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *TLSFingerprintProfileMutation) SetField(name string, value ent.Value) error { + switch name { + case tlsfingerprintprofile.FieldCreatedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedAt(v) + return nil + case tlsfingerprintprofile.FieldUpdatedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUpdatedAt(v) + return nil + case tlsfingerprintprofile.FieldName: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetName(v) + return nil + case tlsfingerprintprofile.FieldDescription: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDescription(v) + return nil + case tlsfingerprintprofile.FieldEnableGrease: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetEnableGrease(v) + return nil + case tlsfingerprintprofile.FieldCipherSuites: + v, ok := value.([]uint16) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCipherSuites(v) + return nil + case tlsfingerprintprofile.FieldCurves: + v, ok := value.([]uint16) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCurves(v) + return nil + case tlsfingerprintprofile.FieldPointFormats: + v, ok := value.([]uint16) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetPointFormats(v) + return nil + case tlsfingerprintprofile.FieldSignatureAlgorithms: + v, ok := value.([]uint16) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSignatureAlgorithms(v) + return nil + case tlsfingerprintprofile.FieldAlpnProtocols: + v, ok := value.([]string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAlpnProtocols(v) + return nil + case tlsfingerprintprofile.FieldSupportedVersions: + v, ok := value.([]uint16) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSupportedVersions(v) + return nil + case tlsfingerprintprofile.FieldKeyShareGroups: + v, ok := value.([]uint16) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetKeyShareGroups(v) + return nil + case tlsfingerprintprofile.FieldPskModes: + v, ok := value.([]uint16) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetPskModes(v) + return nil + case tlsfingerprintprofile.FieldExtensions: + v, ok := value.([]uint16) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetExtensions(v) + return nil + } + return fmt.Errorf("unknown TLSFingerprintProfile field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *TLSFingerprintProfileMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *TLSFingerprintProfileMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *TLSFingerprintProfileMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown TLSFingerprintProfile numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *TLSFingerprintProfileMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(tlsfingerprintprofile.FieldDescription) { + fields = append(fields, tlsfingerprintprofile.FieldDescription) + } + if m.FieldCleared(tlsfingerprintprofile.FieldCipherSuites) { + fields = append(fields, tlsfingerprintprofile.FieldCipherSuites) + } + if m.FieldCleared(tlsfingerprintprofile.FieldCurves) { + fields = append(fields, tlsfingerprintprofile.FieldCurves) + } + if m.FieldCleared(tlsfingerprintprofile.FieldPointFormats) { + fields = append(fields, tlsfingerprintprofile.FieldPointFormats) + } + if m.FieldCleared(tlsfingerprintprofile.FieldSignatureAlgorithms) { + fields = append(fields, tlsfingerprintprofile.FieldSignatureAlgorithms) + } + if m.FieldCleared(tlsfingerprintprofile.FieldAlpnProtocols) { + fields = append(fields, tlsfingerprintprofile.FieldAlpnProtocols) + } + if m.FieldCleared(tlsfingerprintprofile.FieldSupportedVersions) { + fields = append(fields, tlsfingerprintprofile.FieldSupportedVersions) + } + if m.FieldCleared(tlsfingerprintprofile.FieldKeyShareGroups) { + fields = append(fields, tlsfingerprintprofile.FieldKeyShareGroups) + } + if m.FieldCleared(tlsfingerprintprofile.FieldPskModes) { + fields = append(fields, tlsfingerprintprofile.FieldPskModes) + } + if m.FieldCleared(tlsfingerprintprofile.FieldExtensions) { + fields = append(fields, tlsfingerprintprofile.FieldExtensions) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *TLSFingerprintProfileMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *TLSFingerprintProfileMutation) ClearField(name string) error { + switch name { + case tlsfingerprintprofile.FieldDescription: + m.ClearDescription() + return nil + case tlsfingerprintprofile.FieldCipherSuites: + m.ClearCipherSuites() + return nil + case tlsfingerprintprofile.FieldCurves: + m.ClearCurves() + return nil + case tlsfingerprintprofile.FieldPointFormats: + m.ClearPointFormats() + return nil + case tlsfingerprintprofile.FieldSignatureAlgorithms: + m.ClearSignatureAlgorithms() + return nil + case tlsfingerprintprofile.FieldAlpnProtocols: + m.ClearAlpnProtocols() + return nil + case tlsfingerprintprofile.FieldSupportedVersions: + m.ClearSupportedVersions() + return nil + case tlsfingerprintprofile.FieldKeyShareGroups: + m.ClearKeyShareGroups() + return nil + case tlsfingerprintprofile.FieldPskModes: + m.ClearPskModes() + return nil + case tlsfingerprintprofile.FieldExtensions: + m.ClearExtensions() + return nil + } + return fmt.Errorf("unknown TLSFingerprintProfile nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *TLSFingerprintProfileMutation) ResetField(name string) error { + switch name { + case tlsfingerprintprofile.FieldCreatedAt: + m.ResetCreatedAt() + return nil + case tlsfingerprintprofile.FieldUpdatedAt: + m.ResetUpdatedAt() + return nil + case tlsfingerprintprofile.FieldName: + m.ResetName() + return nil + case tlsfingerprintprofile.FieldDescription: + m.ResetDescription() + return nil + case tlsfingerprintprofile.FieldEnableGrease: + m.ResetEnableGrease() + return nil + case tlsfingerprintprofile.FieldCipherSuites: + m.ResetCipherSuites() + return nil + case tlsfingerprintprofile.FieldCurves: + m.ResetCurves() + return nil + case tlsfingerprintprofile.FieldPointFormats: + m.ResetPointFormats() + return nil + case tlsfingerprintprofile.FieldSignatureAlgorithms: + m.ResetSignatureAlgorithms() + return nil + case tlsfingerprintprofile.FieldAlpnProtocols: + m.ResetAlpnProtocols() + return nil + case tlsfingerprintprofile.FieldSupportedVersions: + m.ResetSupportedVersions() + return nil + case tlsfingerprintprofile.FieldKeyShareGroups: + m.ResetKeyShareGroups() + return nil + case tlsfingerprintprofile.FieldPskModes: + m.ResetPskModes() + return nil + case tlsfingerprintprofile.FieldExtensions: + m.ResetExtensions() + return nil + } + return fmt.Errorf("unknown TLSFingerprintProfile field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *TLSFingerprintProfileMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *TLSFingerprintProfileMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *TLSFingerprintProfileMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *TLSFingerprintProfileMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *TLSFingerprintProfileMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *TLSFingerprintProfileMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *TLSFingerprintProfileMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown TLSFingerprintProfile unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *TLSFingerprintProfileMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown TLSFingerprintProfile edge %s", name) +} + // UsageCleanupTaskMutation represents an operation that mutates the UsageCleanupTask nodes in the graph. type UsageCleanupTaskMutation struct { config diff --git a/backend/ent/predicate/predicate.go b/backend/ent/predicate/predicate.go index 89d933fc..a652ab3f 100644 --- a/backend/ent/predicate/predicate.go +++ b/backend/ent/predicate/predicate.go @@ -48,6 +48,9 @@ type SecuritySecret func(*sql.Selector) // Setting is the predicate function for setting builders. type Setting func(*sql.Selector) +// TLSFingerprintProfile is the predicate function for tlsfingerprintprofile builders. +type TLSFingerprintProfile func(*sql.Selector) + // UsageCleanupTask is the predicate function for usagecleanuptask builders. type UsageCleanupTask func(*sql.Selector) diff --git a/backend/ent/runtime/runtime.go b/backend/ent/runtime/runtime.go index 19c58d76..ca95f13f 100644 --- a/backend/ent/runtime/runtime.go +++ b/backend/ent/runtime/runtime.go @@ -20,6 +20,7 @@ import ( "github.com/Wei-Shaw/sub2api/ent/schema" "github.com/Wei-Shaw/sub2api/ent/securitysecret" "github.com/Wei-Shaw/sub2api/ent/setting" + "github.com/Wei-Shaw/sub2api/ent/tlsfingerprintprofile" "github.com/Wei-Shaw/sub2api/ent/usagecleanuptask" "github.com/Wei-Shaw/sub2api/ent/usagelog" "github.com/Wei-Shaw/sub2api/ent/user" @@ -746,6 +747,43 @@ func init() { setting.DefaultUpdatedAt = settingDescUpdatedAt.Default.(func() time.Time) // setting.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field. setting.UpdateDefaultUpdatedAt = settingDescUpdatedAt.UpdateDefault.(func() time.Time) + tlsfingerprintprofileMixin := schema.TLSFingerprintProfile{}.Mixin() + tlsfingerprintprofileMixinFields0 := tlsfingerprintprofileMixin[0].Fields() + _ = tlsfingerprintprofileMixinFields0 + tlsfingerprintprofileFields := schema.TLSFingerprintProfile{}.Fields() + _ = tlsfingerprintprofileFields + // tlsfingerprintprofileDescCreatedAt is the schema descriptor for created_at field. + tlsfingerprintprofileDescCreatedAt := tlsfingerprintprofileMixinFields0[0].Descriptor() + // tlsfingerprintprofile.DefaultCreatedAt holds the default value on creation for the created_at field. + tlsfingerprintprofile.DefaultCreatedAt = tlsfingerprintprofileDescCreatedAt.Default.(func() time.Time) + // tlsfingerprintprofileDescUpdatedAt is the schema descriptor for updated_at field. + tlsfingerprintprofileDescUpdatedAt := tlsfingerprintprofileMixinFields0[1].Descriptor() + // tlsfingerprintprofile.DefaultUpdatedAt holds the default value on creation for the updated_at field. + tlsfingerprintprofile.DefaultUpdatedAt = tlsfingerprintprofileDescUpdatedAt.Default.(func() time.Time) + // tlsfingerprintprofile.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field. + tlsfingerprintprofile.UpdateDefaultUpdatedAt = tlsfingerprintprofileDescUpdatedAt.UpdateDefault.(func() time.Time) + // tlsfingerprintprofileDescName is the schema descriptor for name field. + tlsfingerprintprofileDescName := tlsfingerprintprofileFields[0].Descriptor() + // tlsfingerprintprofile.NameValidator is a validator for the "name" field. It is called by the builders before save. + tlsfingerprintprofile.NameValidator = func() func(string) error { + validators := tlsfingerprintprofileDescName.Validators + fns := [...]func(string) error{ + validators[0].(func(string) error), + validators[1].(func(string) error), + } + return func(name string) error { + for _, fn := range fns { + if err := fn(name); err != nil { + return err + } + } + return nil + } + }() + // tlsfingerprintprofileDescEnableGrease is the schema descriptor for enable_grease field. + tlsfingerprintprofileDescEnableGrease := tlsfingerprintprofileFields[2].Descriptor() + // tlsfingerprintprofile.DefaultEnableGrease holds the default value on creation for the enable_grease field. + tlsfingerprintprofile.DefaultEnableGrease = tlsfingerprintprofileDescEnableGrease.Default.(bool) usagecleanuptaskMixin := schema.UsageCleanupTask{}.Mixin() usagecleanuptaskMixinFields0 := usagecleanuptaskMixin[0].Fields() _ = usagecleanuptaskMixinFields0 diff --git a/backend/ent/schema/tls_fingerprint_profile.go b/backend/ent/schema/tls_fingerprint_profile.go new file mode 100644 index 00000000..86856d05 --- /dev/null +++ b/backend/ent/schema/tls_fingerprint_profile.go @@ -0,0 +1,100 @@ +// Package schema 定义 Ent ORM 的数据库 schema。 +package schema + +import ( + "github.com/Wei-Shaw/sub2api/ent/schema/mixins" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" +) + +// TLSFingerprintProfile 定义 TLS 指纹配置模板的 schema。 +// +// TLS 指纹模板用于模拟特定客户端(如 Claude Code / Node.js)的 TLS 握手特征。 +// 每个模板包含完整的 ClientHello 参数:加密套件、曲线、扩展等。 +// 通过 Account.Extra.tls_fingerprint_profile_id 绑定到具体账号。 +type TLSFingerprintProfile struct { + ent.Schema +} + +// Annotations 返回 schema 的注解配置。 +func (TLSFingerprintProfile) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "tls_fingerprint_profiles"}, + } +} + +// Mixin 返回该 schema 使用的混入组件。 +func (TLSFingerprintProfile) Mixin() []ent.Mixin { + return []ent.Mixin{ + mixins.TimeMixin{}, + } +} + +// Fields 定义 TLS 指纹模板实体的所有字段。 +func (TLSFingerprintProfile) Fields() []ent.Field { + return []ent.Field{ + // name: 模板名称,唯一标识 + field.String("name"). + MaxLen(100). + NotEmpty(). + Unique(), + + // description: 模板描述 + field.Text("description"). + Optional(). + Nillable(), + + // enable_grease: 是否启用 GREASE 扩展(Chrome 使用,Node.js 不使用) + field.Bool("enable_grease"). + Default(false), + + // cipher_suites: TLS 加密套件列表(顺序敏感,影响 JA3) + field.JSON("cipher_suites", []uint16{}). + Optional(). + SchemaType(map[string]string{dialect.Postgres: "jsonb"}), + + // curves: 椭圆曲线/支持的组列表 + field.JSON("curves", []uint16{}). + Optional(). + SchemaType(map[string]string{dialect.Postgres: "jsonb"}), + + // point_formats: EC 点格式列表 + field.JSON("point_formats", []uint16{}). + Optional(). + SchemaType(map[string]string{dialect.Postgres: "jsonb"}), + + // signature_algorithms: 签名算法列表 + field.JSON("signature_algorithms", []uint16{}). + Optional(). + SchemaType(map[string]string{dialect.Postgres: "jsonb"}), + + // alpn_protocols: ALPN 协议列表(如 ["http/1.1"]) + field.JSON("alpn_protocols", []string{}). + Optional(). + SchemaType(map[string]string{dialect.Postgres: "jsonb"}), + + // supported_versions: 支持的 TLS 版本列表(如 [0x0304, 0x0303]) + field.JSON("supported_versions", []uint16{}). + Optional(). + SchemaType(map[string]string{dialect.Postgres: "jsonb"}), + + // key_share_groups: Key Share 中发送的曲线组(如 [29] 即 X25519) + field.JSON("key_share_groups", []uint16{}). + Optional(). + SchemaType(map[string]string{dialect.Postgres: "jsonb"}), + + // psk_modes: PSK 密钥交换模式(如 [1] 即 psk_dhe_ke) + field.JSON("psk_modes", []uint16{}). + Optional(). + SchemaType(map[string]string{dialect.Postgres: "jsonb"}), + + // extensions: TLS 扩展类型 ID 列表,按发送顺序排列 + field.JSON("extensions", []uint16{}). + Optional(). + SchemaType(map[string]string{dialect.Postgres: "jsonb"}), + } +} diff --git a/backend/ent/tlsfingerprintprofile.go b/backend/ent/tlsfingerprintprofile.go new file mode 100644 index 00000000..c9455609 --- /dev/null +++ b/backend/ent/tlsfingerprintprofile.go @@ -0,0 +1,275 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/Wei-Shaw/sub2api/ent/tlsfingerprintprofile" +) + +// TLSFingerprintProfile is the model entity for the TLSFingerprintProfile schema. +type TLSFingerprintProfile struct { + config `json:"-"` + // ID of the ent. + ID int64 `json:"id,omitempty"` + // CreatedAt holds the value of the "created_at" field. + CreatedAt time.Time `json:"created_at,omitempty"` + // UpdatedAt holds the value of the "updated_at" field. + UpdatedAt time.Time `json:"updated_at,omitempty"` + // Name holds the value of the "name" field. + Name string `json:"name,omitempty"` + // Description holds the value of the "description" field. + Description *string `json:"description,omitempty"` + // EnableGrease holds the value of the "enable_grease" field. + EnableGrease bool `json:"enable_grease,omitempty"` + // CipherSuites holds the value of the "cipher_suites" field. + CipherSuites []uint16 `json:"cipher_suites,omitempty"` + // Curves holds the value of the "curves" field. + Curves []uint16 `json:"curves,omitempty"` + // PointFormats holds the value of the "point_formats" field. + PointFormats []uint16 `json:"point_formats,omitempty"` + // SignatureAlgorithms holds the value of the "signature_algorithms" field. + SignatureAlgorithms []uint16 `json:"signature_algorithms,omitempty"` + // AlpnProtocols holds the value of the "alpn_protocols" field. + AlpnProtocols []string `json:"alpn_protocols,omitempty"` + // SupportedVersions holds the value of the "supported_versions" field. + SupportedVersions []uint16 `json:"supported_versions,omitempty"` + // KeyShareGroups holds the value of the "key_share_groups" field. + KeyShareGroups []uint16 `json:"key_share_groups,omitempty"` + // PskModes holds the value of the "psk_modes" field. + PskModes []uint16 `json:"psk_modes,omitempty"` + // Extensions holds the value of the "extensions" field. + Extensions []uint16 `json:"extensions,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*TLSFingerprintProfile) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case tlsfingerprintprofile.FieldCipherSuites, tlsfingerprintprofile.FieldCurves, tlsfingerprintprofile.FieldPointFormats, tlsfingerprintprofile.FieldSignatureAlgorithms, tlsfingerprintprofile.FieldAlpnProtocols, tlsfingerprintprofile.FieldSupportedVersions, tlsfingerprintprofile.FieldKeyShareGroups, tlsfingerprintprofile.FieldPskModes, tlsfingerprintprofile.FieldExtensions: + values[i] = new([]byte) + case tlsfingerprintprofile.FieldEnableGrease: + values[i] = new(sql.NullBool) + case tlsfingerprintprofile.FieldID: + values[i] = new(sql.NullInt64) + case tlsfingerprintprofile.FieldName, tlsfingerprintprofile.FieldDescription: + values[i] = new(sql.NullString) + case tlsfingerprintprofile.FieldCreatedAt, tlsfingerprintprofile.FieldUpdatedAt: + values[i] = new(sql.NullTime) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the TLSFingerprintProfile fields. +func (_m *TLSFingerprintProfile) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case tlsfingerprintprofile.FieldID: + value, ok := values[i].(*sql.NullInt64) + if !ok { + return fmt.Errorf("unexpected type %T for field id", value) + } + _m.ID = int64(value.Int64) + case tlsfingerprintprofile.FieldCreatedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created_at", values[i]) + } else if value.Valid { + _m.CreatedAt = value.Time + } + case tlsfingerprintprofile.FieldUpdatedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field updated_at", values[i]) + } else if value.Valid { + _m.UpdatedAt = value.Time + } + case tlsfingerprintprofile.FieldName: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field name", values[i]) + } else if value.Valid { + _m.Name = value.String + } + case tlsfingerprintprofile.FieldDescription: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field description", values[i]) + } else if value.Valid { + _m.Description = new(string) + *_m.Description = value.String + } + case tlsfingerprintprofile.FieldEnableGrease: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field enable_grease", values[i]) + } else if value.Valid { + _m.EnableGrease = value.Bool + } + case tlsfingerprintprofile.FieldCipherSuites: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field cipher_suites", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &_m.CipherSuites); err != nil { + return fmt.Errorf("unmarshal field cipher_suites: %w", err) + } + } + case tlsfingerprintprofile.FieldCurves: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field curves", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &_m.Curves); err != nil { + return fmt.Errorf("unmarshal field curves: %w", err) + } + } + case tlsfingerprintprofile.FieldPointFormats: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field point_formats", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &_m.PointFormats); err != nil { + return fmt.Errorf("unmarshal field point_formats: %w", err) + } + } + case tlsfingerprintprofile.FieldSignatureAlgorithms: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field signature_algorithms", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &_m.SignatureAlgorithms); err != nil { + return fmt.Errorf("unmarshal field signature_algorithms: %w", err) + } + } + case tlsfingerprintprofile.FieldAlpnProtocols: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field alpn_protocols", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &_m.AlpnProtocols); err != nil { + return fmt.Errorf("unmarshal field alpn_protocols: %w", err) + } + } + case tlsfingerprintprofile.FieldSupportedVersions: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field supported_versions", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &_m.SupportedVersions); err != nil { + return fmt.Errorf("unmarshal field supported_versions: %w", err) + } + } + case tlsfingerprintprofile.FieldKeyShareGroups: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field key_share_groups", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &_m.KeyShareGroups); err != nil { + return fmt.Errorf("unmarshal field key_share_groups: %w", err) + } + } + case tlsfingerprintprofile.FieldPskModes: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field psk_modes", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &_m.PskModes); err != nil { + return fmt.Errorf("unmarshal field psk_modes: %w", err) + } + } + case tlsfingerprintprofile.FieldExtensions: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field extensions", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &_m.Extensions); err != nil { + return fmt.Errorf("unmarshal field extensions: %w", err) + } + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the TLSFingerprintProfile. +// This includes values selected through modifiers, order, etc. +func (_m *TLSFingerprintProfile) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// Update returns a builder for updating this TLSFingerprintProfile. +// Note that you need to call TLSFingerprintProfile.Unwrap() before calling this method if this TLSFingerprintProfile +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *TLSFingerprintProfile) Update() *TLSFingerprintProfileUpdateOne { + return NewTLSFingerprintProfileClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the TLSFingerprintProfile entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *TLSFingerprintProfile) Unwrap() *TLSFingerprintProfile { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: TLSFingerprintProfile is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *TLSFingerprintProfile) String() string { + var builder strings.Builder + builder.WriteString("TLSFingerprintProfile(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("created_at=") + builder.WriteString(_m.CreatedAt.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("updated_at=") + builder.WriteString(_m.UpdatedAt.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("name=") + builder.WriteString(_m.Name) + builder.WriteString(", ") + if v := _m.Description; v != nil { + builder.WriteString("description=") + builder.WriteString(*v) + } + builder.WriteString(", ") + builder.WriteString("enable_grease=") + builder.WriteString(fmt.Sprintf("%v", _m.EnableGrease)) + builder.WriteString(", ") + builder.WriteString("cipher_suites=") + builder.WriteString(fmt.Sprintf("%v", _m.CipherSuites)) + builder.WriteString(", ") + builder.WriteString("curves=") + builder.WriteString(fmt.Sprintf("%v", _m.Curves)) + builder.WriteString(", ") + builder.WriteString("point_formats=") + builder.WriteString(fmt.Sprintf("%v", _m.PointFormats)) + builder.WriteString(", ") + builder.WriteString("signature_algorithms=") + builder.WriteString(fmt.Sprintf("%v", _m.SignatureAlgorithms)) + builder.WriteString(", ") + builder.WriteString("alpn_protocols=") + builder.WriteString(fmt.Sprintf("%v", _m.AlpnProtocols)) + builder.WriteString(", ") + builder.WriteString("supported_versions=") + builder.WriteString(fmt.Sprintf("%v", _m.SupportedVersions)) + builder.WriteString(", ") + builder.WriteString("key_share_groups=") + builder.WriteString(fmt.Sprintf("%v", _m.KeyShareGroups)) + builder.WriteString(", ") + builder.WriteString("psk_modes=") + builder.WriteString(fmt.Sprintf("%v", _m.PskModes)) + builder.WriteString(", ") + builder.WriteString("extensions=") + builder.WriteString(fmt.Sprintf("%v", _m.Extensions)) + builder.WriteByte(')') + return builder.String() +} + +// TLSFingerprintProfiles is a parsable slice of TLSFingerprintProfile. +type TLSFingerprintProfiles []*TLSFingerprintProfile diff --git a/backend/ent/tlsfingerprintprofile/tlsfingerprintprofile.go b/backend/ent/tlsfingerprintprofile/tlsfingerprintprofile.go new file mode 100644 index 00000000..49426d36 --- /dev/null +++ b/backend/ent/tlsfingerprintprofile/tlsfingerprintprofile.go @@ -0,0 +1,121 @@ +// Code generated by ent, DO NOT EDIT. + +package tlsfingerprintprofile + +import ( + "time" + + "entgo.io/ent/dialect/sql" +) + +const ( + // Label holds the string label denoting the tlsfingerprintprofile type in the database. + Label = "tls_fingerprint_profile" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldCreatedAt holds the string denoting the created_at field in the database. + FieldCreatedAt = "created_at" + // FieldUpdatedAt holds the string denoting the updated_at field in the database. + FieldUpdatedAt = "updated_at" + // FieldName holds the string denoting the name field in the database. + FieldName = "name" + // FieldDescription holds the string denoting the description field in the database. + FieldDescription = "description" + // FieldEnableGrease holds the string denoting the enable_grease field in the database. + FieldEnableGrease = "enable_grease" + // FieldCipherSuites holds the string denoting the cipher_suites field in the database. + FieldCipherSuites = "cipher_suites" + // FieldCurves holds the string denoting the curves field in the database. + FieldCurves = "curves" + // FieldPointFormats holds the string denoting the point_formats field in the database. + FieldPointFormats = "point_formats" + // FieldSignatureAlgorithms holds the string denoting the signature_algorithms field in the database. + FieldSignatureAlgorithms = "signature_algorithms" + // FieldAlpnProtocols holds the string denoting the alpn_protocols field in the database. + FieldAlpnProtocols = "alpn_protocols" + // FieldSupportedVersions holds the string denoting the supported_versions field in the database. + FieldSupportedVersions = "supported_versions" + // FieldKeyShareGroups holds the string denoting the key_share_groups field in the database. + FieldKeyShareGroups = "key_share_groups" + // FieldPskModes holds the string denoting the psk_modes field in the database. + FieldPskModes = "psk_modes" + // FieldExtensions holds the string denoting the extensions field in the database. + FieldExtensions = "extensions" + // Table holds the table name of the tlsfingerprintprofile in the database. + Table = "tls_fingerprint_profiles" +) + +// Columns holds all SQL columns for tlsfingerprintprofile fields. +var Columns = []string{ + FieldID, + FieldCreatedAt, + FieldUpdatedAt, + FieldName, + FieldDescription, + FieldEnableGrease, + FieldCipherSuites, + FieldCurves, + FieldPointFormats, + FieldSignatureAlgorithms, + FieldAlpnProtocols, + FieldSupportedVersions, + FieldKeyShareGroups, + FieldPskModes, + FieldExtensions, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // DefaultCreatedAt holds the default value on creation for the "created_at" field. + DefaultCreatedAt func() time.Time + // DefaultUpdatedAt holds the default value on creation for the "updated_at" field. + DefaultUpdatedAt func() time.Time + // UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field. + UpdateDefaultUpdatedAt func() time.Time + // NameValidator is a validator for the "name" field. It is called by the builders before save. + NameValidator func(string) error + // DefaultEnableGrease holds the default value on creation for the "enable_grease" field. + DefaultEnableGrease bool +) + +// OrderOption defines the ordering options for the TLSFingerprintProfile queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByCreatedAt orders the results by the created_at field. +func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedAt, opts...).ToFunc() +} + +// ByUpdatedAt orders the results by the updated_at field. +func ByUpdatedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUpdatedAt, opts...).ToFunc() +} + +// ByName orders the results by the name field. +func ByName(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldName, opts...).ToFunc() +} + +// ByDescription orders the results by the description field. +func ByDescription(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDescription, opts...).ToFunc() +} + +// ByEnableGrease orders the results by the enable_grease field. +func ByEnableGrease(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldEnableGrease, opts...).ToFunc() +} diff --git a/backend/ent/tlsfingerprintprofile/where.go b/backend/ent/tlsfingerprintprofile/where.go new file mode 100644 index 00000000..f7d1ba27 --- /dev/null +++ b/backend/ent/tlsfingerprintprofile/where.go @@ -0,0 +1,415 @@ +// Code generated by ent, DO NOT EDIT. + +package tlsfingerprintprofile + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/Wei-Shaw/sub2api/ent/predicate" +) + +// ID filters vertices based on their ID field. +func ID(id int64) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id int64) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id int64) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...int64) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...int64) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id int64) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id int64) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id int64) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id int64) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldLTE(FieldID, id)) +} + +// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ. +func CreatedAt(v time.Time) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldEQ(FieldCreatedAt, v)) +} + +// UpdatedAt applies equality check predicate on the "updated_at" field. It's identical to UpdatedAtEQ. +func UpdatedAt(v time.Time) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldEQ(FieldUpdatedAt, v)) +} + +// Name applies equality check predicate on the "name" field. It's identical to NameEQ. +func Name(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldEQ(FieldName, v)) +} + +// Description applies equality check predicate on the "description" field. It's identical to DescriptionEQ. +func Description(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldEQ(FieldDescription, v)) +} + +// EnableGrease applies equality check predicate on the "enable_grease" field. It's identical to EnableGreaseEQ. +func EnableGrease(v bool) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldEQ(FieldEnableGrease, v)) +} + +// CreatedAtEQ applies the EQ predicate on the "created_at" field. +func CreatedAtEQ(v time.Time) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldEQ(FieldCreatedAt, v)) +} + +// CreatedAtNEQ applies the NEQ predicate on the "created_at" field. +func CreatedAtNEQ(v time.Time) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNEQ(FieldCreatedAt, v)) +} + +// CreatedAtIn applies the In predicate on the "created_at" field. +func CreatedAtIn(vs ...time.Time) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldIn(FieldCreatedAt, vs...)) +} + +// CreatedAtNotIn applies the NotIn predicate on the "created_at" field. +func CreatedAtNotIn(vs ...time.Time) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNotIn(FieldCreatedAt, vs...)) +} + +// CreatedAtGT applies the GT predicate on the "created_at" field. +func CreatedAtGT(v time.Time) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldGT(FieldCreatedAt, v)) +} + +// CreatedAtGTE applies the GTE predicate on the "created_at" field. +func CreatedAtGTE(v time.Time) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldGTE(FieldCreatedAt, v)) +} + +// CreatedAtLT applies the LT predicate on the "created_at" field. +func CreatedAtLT(v time.Time) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldLT(FieldCreatedAt, v)) +} + +// CreatedAtLTE applies the LTE predicate on the "created_at" field. +func CreatedAtLTE(v time.Time) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldLTE(FieldCreatedAt, v)) +} + +// UpdatedAtEQ applies the EQ predicate on the "updated_at" field. +func UpdatedAtEQ(v time.Time) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldEQ(FieldUpdatedAt, v)) +} + +// UpdatedAtNEQ applies the NEQ predicate on the "updated_at" field. +func UpdatedAtNEQ(v time.Time) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNEQ(FieldUpdatedAt, v)) +} + +// UpdatedAtIn applies the In predicate on the "updated_at" field. +func UpdatedAtIn(vs ...time.Time) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldIn(FieldUpdatedAt, vs...)) +} + +// UpdatedAtNotIn applies the NotIn predicate on the "updated_at" field. +func UpdatedAtNotIn(vs ...time.Time) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNotIn(FieldUpdatedAt, vs...)) +} + +// UpdatedAtGT applies the GT predicate on the "updated_at" field. +func UpdatedAtGT(v time.Time) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldGT(FieldUpdatedAt, v)) +} + +// UpdatedAtGTE applies the GTE predicate on the "updated_at" field. +func UpdatedAtGTE(v time.Time) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldGTE(FieldUpdatedAt, v)) +} + +// UpdatedAtLT applies the LT predicate on the "updated_at" field. +func UpdatedAtLT(v time.Time) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldLT(FieldUpdatedAt, v)) +} + +// UpdatedAtLTE applies the LTE predicate on the "updated_at" field. +func UpdatedAtLTE(v time.Time) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldLTE(FieldUpdatedAt, v)) +} + +// NameEQ applies the EQ predicate on the "name" field. +func NameEQ(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldEQ(FieldName, v)) +} + +// NameNEQ applies the NEQ predicate on the "name" field. +func NameNEQ(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNEQ(FieldName, v)) +} + +// NameIn applies the In predicate on the "name" field. +func NameIn(vs ...string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldIn(FieldName, vs...)) +} + +// NameNotIn applies the NotIn predicate on the "name" field. +func NameNotIn(vs ...string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNotIn(FieldName, vs...)) +} + +// NameGT applies the GT predicate on the "name" field. +func NameGT(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldGT(FieldName, v)) +} + +// NameGTE applies the GTE predicate on the "name" field. +func NameGTE(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldGTE(FieldName, v)) +} + +// NameLT applies the LT predicate on the "name" field. +func NameLT(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldLT(FieldName, v)) +} + +// NameLTE applies the LTE predicate on the "name" field. +func NameLTE(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldLTE(FieldName, v)) +} + +// NameContains applies the Contains predicate on the "name" field. +func NameContains(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldContains(FieldName, v)) +} + +// NameHasPrefix applies the HasPrefix predicate on the "name" field. +func NameHasPrefix(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldHasPrefix(FieldName, v)) +} + +// NameHasSuffix applies the HasSuffix predicate on the "name" field. +func NameHasSuffix(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldHasSuffix(FieldName, v)) +} + +// NameEqualFold applies the EqualFold predicate on the "name" field. +func NameEqualFold(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldEqualFold(FieldName, v)) +} + +// NameContainsFold applies the ContainsFold predicate on the "name" field. +func NameContainsFold(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldContainsFold(FieldName, v)) +} + +// DescriptionEQ applies the EQ predicate on the "description" field. +func DescriptionEQ(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldEQ(FieldDescription, v)) +} + +// DescriptionNEQ applies the NEQ predicate on the "description" field. +func DescriptionNEQ(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNEQ(FieldDescription, v)) +} + +// DescriptionIn applies the In predicate on the "description" field. +func DescriptionIn(vs ...string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldIn(FieldDescription, vs...)) +} + +// DescriptionNotIn applies the NotIn predicate on the "description" field. +func DescriptionNotIn(vs ...string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNotIn(FieldDescription, vs...)) +} + +// DescriptionGT applies the GT predicate on the "description" field. +func DescriptionGT(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldGT(FieldDescription, v)) +} + +// DescriptionGTE applies the GTE predicate on the "description" field. +func DescriptionGTE(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldGTE(FieldDescription, v)) +} + +// DescriptionLT applies the LT predicate on the "description" field. +func DescriptionLT(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldLT(FieldDescription, v)) +} + +// DescriptionLTE applies the LTE predicate on the "description" field. +func DescriptionLTE(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldLTE(FieldDescription, v)) +} + +// DescriptionContains applies the Contains predicate on the "description" field. +func DescriptionContains(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldContains(FieldDescription, v)) +} + +// DescriptionHasPrefix applies the HasPrefix predicate on the "description" field. +func DescriptionHasPrefix(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldHasPrefix(FieldDescription, v)) +} + +// DescriptionHasSuffix applies the HasSuffix predicate on the "description" field. +func DescriptionHasSuffix(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldHasSuffix(FieldDescription, v)) +} + +// DescriptionIsNil applies the IsNil predicate on the "description" field. +func DescriptionIsNil() predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldIsNull(FieldDescription)) +} + +// DescriptionNotNil applies the NotNil predicate on the "description" field. +func DescriptionNotNil() predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNotNull(FieldDescription)) +} + +// DescriptionEqualFold applies the EqualFold predicate on the "description" field. +func DescriptionEqualFold(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldEqualFold(FieldDescription, v)) +} + +// DescriptionContainsFold applies the ContainsFold predicate on the "description" field. +func DescriptionContainsFold(v string) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldContainsFold(FieldDescription, v)) +} + +// EnableGreaseEQ applies the EQ predicate on the "enable_grease" field. +func EnableGreaseEQ(v bool) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldEQ(FieldEnableGrease, v)) +} + +// EnableGreaseNEQ applies the NEQ predicate on the "enable_grease" field. +func EnableGreaseNEQ(v bool) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNEQ(FieldEnableGrease, v)) +} + +// CipherSuitesIsNil applies the IsNil predicate on the "cipher_suites" field. +func CipherSuitesIsNil() predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldIsNull(FieldCipherSuites)) +} + +// CipherSuitesNotNil applies the NotNil predicate on the "cipher_suites" field. +func CipherSuitesNotNil() predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNotNull(FieldCipherSuites)) +} + +// CurvesIsNil applies the IsNil predicate on the "curves" field. +func CurvesIsNil() predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldIsNull(FieldCurves)) +} + +// CurvesNotNil applies the NotNil predicate on the "curves" field. +func CurvesNotNil() predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNotNull(FieldCurves)) +} + +// PointFormatsIsNil applies the IsNil predicate on the "point_formats" field. +func PointFormatsIsNil() predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldIsNull(FieldPointFormats)) +} + +// PointFormatsNotNil applies the NotNil predicate on the "point_formats" field. +func PointFormatsNotNil() predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNotNull(FieldPointFormats)) +} + +// SignatureAlgorithmsIsNil applies the IsNil predicate on the "signature_algorithms" field. +func SignatureAlgorithmsIsNil() predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldIsNull(FieldSignatureAlgorithms)) +} + +// SignatureAlgorithmsNotNil applies the NotNil predicate on the "signature_algorithms" field. +func SignatureAlgorithmsNotNil() predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNotNull(FieldSignatureAlgorithms)) +} + +// AlpnProtocolsIsNil applies the IsNil predicate on the "alpn_protocols" field. +func AlpnProtocolsIsNil() predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldIsNull(FieldAlpnProtocols)) +} + +// AlpnProtocolsNotNil applies the NotNil predicate on the "alpn_protocols" field. +func AlpnProtocolsNotNil() predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNotNull(FieldAlpnProtocols)) +} + +// SupportedVersionsIsNil applies the IsNil predicate on the "supported_versions" field. +func SupportedVersionsIsNil() predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldIsNull(FieldSupportedVersions)) +} + +// SupportedVersionsNotNil applies the NotNil predicate on the "supported_versions" field. +func SupportedVersionsNotNil() predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNotNull(FieldSupportedVersions)) +} + +// KeyShareGroupsIsNil applies the IsNil predicate on the "key_share_groups" field. +func KeyShareGroupsIsNil() predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldIsNull(FieldKeyShareGroups)) +} + +// KeyShareGroupsNotNil applies the NotNil predicate on the "key_share_groups" field. +func KeyShareGroupsNotNil() predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNotNull(FieldKeyShareGroups)) +} + +// PskModesIsNil applies the IsNil predicate on the "psk_modes" field. +func PskModesIsNil() predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldIsNull(FieldPskModes)) +} + +// PskModesNotNil applies the NotNil predicate on the "psk_modes" field. +func PskModesNotNil() predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNotNull(FieldPskModes)) +} + +// ExtensionsIsNil applies the IsNil predicate on the "extensions" field. +func ExtensionsIsNil() predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldIsNull(FieldExtensions)) +} + +// ExtensionsNotNil applies the NotNil predicate on the "extensions" field. +func ExtensionsNotNil() predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.FieldNotNull(FieldExtensions)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.TLSFingerprintProfile) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.TLSFingerprintProfile) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.TLSFingerprintProfile) predicate.TLSFingerprintProfile { + return predicate.TLSFingerprintProfile(sql.NotPredicates(p)) +} diff --git a/backend/ent/tlsfingerprintprofile_create.go b/backend/ent/tlsfingerprintprofile_create.go new file mode 100644 index 00000000..70a5e6be --- /dev/null +++ b/backend/ent/tlsfingerprintprofile_create.go @@ -0,0 +1,1341 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/Wei-Shaw/sub2api/ent/tlsfingerprintprofile" +) + +// TLSFingerprintProfileCreate is the builder for creating a TLSFingerprintProfile entity. +type TLSFingerprintProfileCreate struct { + config + mutation *TLSFingerprintProfileMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetCreatedAt sets the "created_at" field. +func (_c *TLSFingerprintProfileCreate) SetCreatedAt(v time.Time) *TLSFingerprintProfileCreate { + _c.mutation.SetCreatedAt(v) + return _c +} + +// SetNillableCreatedAt sets the "created_at" field if the given value is not nil. +func (_c *TLSFingerprintProfileCreate) SetNillableCreatedAt(v *time.Time) *TLSFingerprintProfileCreate { + if v != nil { + _c.SetCreatedAt(*v) + } + return _c +} + +// SetUpdatedAt sets the "updated_at" field. +func (_c *TLSFingerprintProfileCreate) SetUpdatedAt(v time.Time) *TLSFingerprintProfileCreate { + _c.mutation.SetUpdatedAt(v) + return _c +} + +// SetNillableUpdatedAt sets the "updated_at" field if the given value is not nil. +func (_c *TLSFingerprintProfileCreate) SetNillableUpdatedAt(v *time.Time) *TLSFingerprintProfileCreate { + if v != nil { + _c.SetUpdatedAt(*v) + } + return _c +} + +// SetName sets the "name" field. +func (_c *TLSFingerprintProfileCreate) SetName(v string) *TLSFingerprintProfileCreate { + _c.mutation.SetName(v) + return _c +} + +// SetDescription sets the "description" field. +func (_c *TLSFingerprintProfileCreate) SetDescription(v string) *TLSFingerprintProfileCreate { + _c.mutation.SetDescription(v) + return _c +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (_c *TLSFingerprintProfileCreate) SetNillableDescription(v *string) *TLSFingerprintProfileCreate { + if v != nil { + _c.SetDescription(*v) + } + return _c +} + +// SetEnableGrease sets the "enable_grease" field. +func (_c *TLSFingerprintProfileCreate) SetEnableGrease(v bool) *TLSFingerprintProfileCreate { + _c.mutation.SetEnableGrease(v) + return _c +} + +// SetNillableEnableGrease sets the "enable_grease" field if the given value is not nil. +func (_c *TLSFingerprintProfileCreate) SetNillableEnableGrease(v *bool) *TLSFingerprintProfileCreate { + if v != nil { + _c.SetEnableGrease(*v) + } + return _c +} + +// SetCipherSuites sets the "cipher_suites" field. +func (_c *TLSFingerprintProfileCreate) SetCipherSuites(v []uint16) *TLSFingerprintProfileCreate { + _c.mutation.SetCipherSuites(v) + return _c +} + +// SetCurves sets the "curves" field. +func (_c *TLSFingerprintProfileCreate) SetCurves(v []uint16) *TLSFingerprintProfileCreate { + _c.mutation.SetCurves(v) + return _c +} + +// SetPointFormats sets the "point_formats" field. +func (_c *TLSFingerprintProfileCreate) SetPointFormats(v []uint16) *TLSFingerprintProfileCreate { + _c.mutation.SetPointFormats(v) + return _c +} + +// SetSignatureAlgorithms sets the "signature_algorithms" field. +func (_c *TLSFingerprintProfileCreate) SetSignatureAlgorithms(v []uint16) *TLSFingerprintProfileCreate { + _c.mutation.SetSignatureAlgorithms(v) + return _c +} + +// SetAlpnProtocols sets the "alpn_protocols" field. +func (_c *TLSFingerprintProfileCreate) SetAlpnProtocols(v []string) *TLSFingerprintProfileCreate { + _c.mutation.SetAlpnProtocols(v) + return _c +} + +// SetSupportedVersions sets the "supported_versions" field. +func (_c *TLSFingerprintProfileCreate) SetSupportedVersions(v []uint16) *TLSFingerprintProfileCreate { + _c.mutation.SetSupportedVersions(v) + return _c +} + +// SetKeyShareGroups sets the "key_share_groups" field. +func (_c *TLSFingerprintProfileCreate) SetKeyShareGroups(v []uint16) *TLSFingerprintProfileCreate { + _c.mutation.SetKeyShareGroups(v) + return _c +} + +// SetPskModes sets the "psk_modes" field. +func (_c *TLSFingerprintProfileCreate) SetPskModes(v []uint16) *TLSFingerprintProfileCreate { + _c.mutation.SetPskModes(v) + return _c +} + +// SetExtensions sets the "extensions" field. +func (_c *TLSFingerprintProfileCreate) SetExtensions(v []uint16) *TLSFingerprintProfileCreate { + _c.mutation.SetExtensions(v) + return _c +} + +// Mutation returns the TLSFingerprintProfileMutation object of the builder. +func (_c *TLSFingerprintProfileCreate) Mutation() *TLSFingerprintProfileMutation { + return _c.mutation +} + +// Save creates the TLSFingerprintProfile in the database. +func (_c *TLSFingerprintProfileCreate) Save(ctx context.Context) (*TLSFingerprintProfile, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *TLSFingerprintProfileCreate) SaveX(ctx context.Context) *TLSFingerprintProfile { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *TLSFingerprintProfileCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *TLSFingerprintProfileCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *TLSFingerprintProfileCreate) defaults() { + if _, ok := _c.mutation.CreatedAt(); !ok { + v := tlsfingerprintprofile.DefaultCreatedAt() + _c.mutation.SetCreatedAt(v) + } + if _, ok := _c.mutation.UpdatedAt(); !ok { + v := tlsfingerprintprofile.DefaultUpdatedAt() + _c.mutation.SetUpdatedAt(v) + } + if _, ok := _c.mutation.EnableGrease(); !ok { + v := tlsfingerprintprofile.DefaultEnableGrease + _c.mutation.SetEnableGrease(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *TLSFingerprintProfileCreate) check() error { + if _, ok := _c.mutation.CreatedAt(); !ok { + return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "TLSFingerprintProfile.created_at"`)} + } + if _, ok := _c.mutation.UpdatedAt(); !ok { + return &ValidationError{Name: "updated_at", err: errors.New(`ent: missing required field "TLSFingerprintProfile.updated_at"`)} + } + if _, ok := _c.mutation.Name(); !ok { + return &ValidationError{Name: "name", err: errors.New(`ent: missing required field "TLSFingerprintProfile.name"`)} + } + if v, ok := _c.mutation.Name(); ok { + if err := tlsfingerprintprofile.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "TLSFingerprintProfile.name": %w`, err)} + } + } + if _, ok := _c.mutation.EnableGrease(); !ok { + return &ValidationError{Name: "enable_grease", err: errors.New(`ent: missing required field "TLSFingerprintProfile.enable_grease"`)} + } + return nil +} + +func (_c *TLSFingerprintProfileCreate) sqlSave(ctx context.Context) (*TLSFingerprintProfile, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + id := _spec.ID.Value.(int64) + _node.ID = int64(id) + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *TLSFingerprintProfileCreate) createSpec() (*TLSFingerprintProfile, *sqlgraph.CreateSpec) { + var ( + _node = &TLSFingerprintProfile{config: _c.config} + _spec = sqlgraph.NewCreateSpec(tlsfingerprintprofile.Table, sqlgraph.NewFieldSpec(tlsfingerprintprofile.FieldID, field.TypeInt64)) + ) + _spec.OnConflict = _c.conflict + if value, ok := _c.mutation.CreatedAt(); ok { + _spec.SetField(tlsfingerprintprofile.FieldCreatedAt, field.TypeTime, value) + _node.CreatedAt = value + } + if value, ok := _c.mutation.UpdatedAt(); ok { + _spec.SetField(tlsfingerprintprofile.FieldUpdatedAt, field.TypeTime, value) + _node.UpdatedAt = value + } + if value, ok := _c.mutation.Name(); ok { + _spec.SetField(tlsfingerprintprofile.FieldName, field.TypeString, value) + _node.Name = value + } + if value, ok := _c.mutation.Description(); ok { + _spec.SetField(tlsfingerprintprofile.FieldDescription, field.TypeString, value) + _node.Description = &value + } + if value, ok := _c.mutation.EnableGrease(); ok { + _spec.SetField(tlsfingerprintprofile.FieldEnableGrease, field.TypeBool, value) + _node.EnableGrease = value + } + if value, ok := _c.mutation.CipherSuites(); ok { + _spec.SetField(tlsfingerprintprofile.FieldCipherSuites, field.TypeJSON, value) + _node.CipherSuites = value + } + if value, ok := _c.mutation.Curves(); ok { + _spec.SetField(tlsfingerprintprofile.FieldCurves, field.TypeJSON, value) + _node.Curves = value + } + if value, ok := _c.mutation.PointFormats(); ok { + _spec.SetField(tlsfingerprintprofile.FieldPointFormats, field.TypeJSON, value) + _node.PointFormats = value + } + if value, ok := _c.mutation.SignatureAlgorithms(); ok { + _spec.SetField(tlsfingerprintprofile.FieldSignatureAlgorithms, field.TypeJSON, value) + _node.SignatureAlgorithms = value + } + if value, ok := _c.mutation.AlpnProtocols(); ok { + _spec.SetField(tlsfingerprintprofile.FieldAlpnProtocols, field.TypeJSON, value) + _node.AlpnProtocols = value + } + if value, ok := _c.mutation.SupportedVersions(); ok { + _spec.SetField(tlsfingerprintprofile.FieldSupportedVersions, field.TypeJSON, value) + _node.SupportedVersions = value + } + if value, ok := _c.mutation.KeyShareGroups(); ok { + _spec.SetField(tlsfingerprintprofile.FieldKeyShareGroups, field.TypeJSON, value) + _node.KeyShareGroups = value + } + if value, ok := _c.mutation.PskModes(); ok { + _spec.SetField(tlsfingerprintprofile.FieldPskModes, field.TypeJSON, value) + _node.PskModes = value + } + if value, ok := _c.mutation.Extensions(); ok { + _spec.SetField(tlsfingerprintprofile.FieldExtensions, field.TypeJSON, value) + _node.Extensions = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.TLSFingerprintProfile.Create(). +// SetCreatedAt(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.TLSFingerprintProfileUpsert) { +// SetCreatedAt(v+v). +// }). +// Exec(ctx) +func (_c *TLSFingerprintProfileCreate) OnConflict(opts ...sql.ConflictOption) *TLSFingerprintProfileUpsertOne { + _c.conflict = opts + return &TLSFingerprintProfileUpsertOne{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.TLSFingerprintProfile.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *TLSFingerprintProfileCreate) OnConflictColumns(columns ...string) *TLSFingerprintProfileUpsertOne { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &TLSFingerprintProfileUpsertOne{ + create: _c, + } +} + +type ( + // TLSFingerprintProfileUpsertOne is the builder for "upsert"-ing + // one TLSFingerprintProfile node. + TLSFingerprintProfileUpsertOne struct { + create *TLSFingerprintProfileCreate + } + + // TLSFingerprintProfileUpsert is the "OnConflict" setter. + TLSFingerprintProfileUpsert struct { + *sql.UpdateSet + } +) + +// SetUpdatedAt sets the "updated_at" field. +func (u *TLSFingerprintProfileUpsert) SetUpdatedAt(v time.Time) *TLSFingerprintProfileUpsert { + u.Set(tlsfingerprintprofile.FieldUpdatedAt, v) + return u +} + +// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsert) UpdateUpdatedAt() *TLSFingerprintProfileUpsert { + u.SetExcluded(tlsfingerprintprofile.FieldUpdatedAt) + return u +} + +// SetName sets the "name" field. +func (u *TLSFingerprintProfileUpsert) SetName(v string) *TLSFingerprintProfileUpsert { + u.Set(tlsfingerprintprofile.FieldName, v) + return u +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsert) UpdateName() *TLSFingerprintProfileUpsert { + u.SetExcluded(tlsfingerprintprofile.FieldName) + return u +} + +// SetDescription sets the "description" field. +func (u *TLSFingerprintProfileUpsert) SetDescription(v string) *TLSFingerprintProfileUpsert { + u.Set(tlsfingerprintprofile.FieldDescription, v) + return u +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsert) UpdateDescription() *TLSFingerprintProfileUpsert { + u.SetExcluded(tlsfingerprintprofile.FieldDescription) + return u +} + +// ClearDescription clears the value of the "description" field. +func (u *TLSFingerprintProfileUpsert) ClearDescription() *TLSFingerprintProfileUpsert { + u.SetNull(tlsfingerprintprofile.FieldDescription) + return u +} + +// SetEnableGrease sets the "enable_grease" field. +func (u *TLSFingerprintProfileUpsert) SetEnableGrease(v bool) *TLSFingerprintProfileUpsert { + u.Set(tlsfingerprintprofile.FieldEnableGrease, v) + return u +} + +// UpdateEnableGrease sets the "enable_grease" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsert) UpdateEnableGrease() *TLSFingerprintProfileUpsert { + u.SetExcluded(tlsfingerprintprofile.FieldEnableGrease) + return u +} + +// SetCipherSuites sets the "cipher_suites" field. +func (u *TLSFingerprintProfileUpsert) SetCipherSuites(v []uint16) *TLSFingerprintProfileUpsert { + u.Set(tlsfingerprintprofile.FieldCipherSuites, v) + return u +} + +// UpdateCipherSuites sets the "cipher_suites" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsert) UpdateCipherSuites() *TLSFingerprintProfileUpsert { + u.SetExcluded(tlsfingerprintprofile.FieldCipherSuites) + return u +} + +// ClearCipherSuites clears the value of the "cipher_suites" field. +func (u *TLSFingerprintProfileUpsert) ClearCipherSuites() *TLSFingerprintProfileUpsert { + u.SetNull(tlsfingerprintprofile.FieldCipherSuites) + return u +} + +// SetCurves sets the "curves" field. +func (u *TLSFingerprintProfileUpsert) SetCurves(v []uint16) *TLSFingerprintProfileUpsert { + u.Set(tlsfingerprintprofile.FieldCurves, v) + return u +} + +// UpdateCurves sets the "curves" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsert) UpdateCurves() *TLSFingerprintProfileUpsert { + u.SetExcluded(tlsfingerprintprofile.FieldCurves) + return u +} + +// ClearCurves clears the value of the "curves" field. +func (u *TLSFingerprintProfileUpsert) ClearCurves() *TLSFingerprintProfileUpsert { + u.SetNull(tlsfingerprintprofile.FieldCurves) + return u +} + +// SetPointFormats sets the "point_formats" field. +func (u *TLSFingerprintProfileUpsert) SetPointFormats(v []uint16) *TLSFingerprintProfileUpsert { + u.Set(tlsfingerprintprofile.FieldPointFormats, v) + return u +} + +// UpdatePointFormats sets the "point_formats" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsert) UpdatePointFormats() *TLSFingerprintProfileUpsert { + u.SetExcluded(tlsfingerprintprofile.FieldPointFormats) + return u +} + +// ClearPointFormats clears the value of the "point_formats" field. +func (u *TLSFingerprintProfileUpsert) ClearPointFormats() *TLSFingerprintProfileUpsert { + u.SetNull(tlsfingerprintprofile.FieldPointFormats) + return u +} + +// SetSignatureAlgorithms sets the "signature_algorithms" field. +func (u *TLSFingerprintProfileUpsert) SetSignatureAlgorithms(v []uint16) *TLSFingerprintProfileUpsert { + u.Set(tlsfingerprintprofile.FieldSignatureAlgorithms, v) + return u +} + +// UpdateSignatureAlgorithms sets the "signature_algorithms" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsert) UpdateSignatureAlgorithms() *TLSFingerprintProfileUpsert { + u.SetExcluded(tlsfingerprintprofile.FieldSignatureAlgorithms) + return u +} + +// ClearSignatureAlgorithms clears the value of the "signature_algorithms" field. +func (u *TLSFingerprintProfileUpsert) ClearSignatureAlgorithms() *TLSFingerprintProfileUpsert { + u.SetNull(tlsfingerprintprofile.FieldSignatureAlgorithms) + return u +} + +// SetAlpnProtocols sets the "alpn_protocols" field. +func (u *TLSFingerprintProfileUpsert) SetAlpnProtocols(v []string) *TLSFingerprintProfileUpsert { + u.Set(tlsfingerprintprofile.FieldAlpnProtocols, v) + return u +} + +// UpdateAlpnProtocols sets the "alpn_protocols" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsert) UpdateAlpnProtocols() *TLSFingerprintProfileUpsert { + u.SetExcluded(tlsfingerprintprofile.FieldAlpnProtocols) + return u +} + +// ClearAlpnProtocols clears the value of the "alpn_protocols" field. +func (u *TLSFingerprintProfileUpsert) ClearAlpnProtocols() *TLSFingerprintProfileUpsert { + u.SetNull(tlsfingerprintprofile.FieldAlpnProtocols) + return u +} + +// SetSupportedVersions sets the "supported_versions" field. +func (u *TLSFingerprintProfileUpsert) SetSupportedVersions(v []uint16) *TLSFingerprintProfileUpsert { + u.Set(tlsfingerprintprofile.FieldSupportedVersions, v) + return u +} + +// UpdateSupportedVersions sets the "supported_versions" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsert) UpdateSupportedVersions() *TLSFingerprintProfileUpsert { + u.SetExcluded(tlsfingerprintprofile.FieldSupportedVersions) + return u +} + +// ClearSupportedVersions clears the value of the "supported_versions" field. +func (u *TLSFingerprintProfileUpsert) ClearSupportedVersions() *TLSFingerprintProfileUpsert { + u.SetNull(tlsfingerprintprofile.FieldSupportedVersions) + return u +} + +// SetKeyShareGroups sets the "key_share_groups" field. +func (u *TLSFingerprintProfileUpsert) SetKeyShareGroups(v []uint16) *TLSFingerprintProfileUpsert { + u.Set(tlsfingerprintprofile.FieldKeyShareGroups, v) + return u +} + +// UpdateKeyShareGroups sets the "key_share_groups" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsert) UpdateKeyShareGroups() *TLSFingerprintProfileUpsert { + u.SetExcluded(tlsfingerprintprofile.FieldKeyShareGroups) + return u +} + +// ClearKeyShareGroups clears the value of the "key_share_groups" field. +func (u *TLSFingerprintProfileUpsert) ClearKeyShareGroups() *TLSFingerprintProfileUpsert { + u.SetNull(tlsfingerprintprofile.FieldKeyShareGroups) + return u +} + +// SetPskModes sets the "psk_modes" field. +func (u *TLSFingerprintProfileUpsert) SetPskModes(v []uint16) *TLSFingerprintProfileUpsert { + u.Set(tlsfingerprintprofile.FieldPskModes, v) + return u +} + +// UpdatePskModes sets the "psk_modes" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsert) UpdatePskModes() *TLSFingerprintProfileUpsert { + u.SetExcluded(tlsfingerprintprofile.FieldPskModes) + return u +} + +// ClearPskModes clears the value of the "psk_modes" field. +func (u *TLSFingerprintProfileUpsert) ClearPskModes() *TLSFingerprintProfileUpsert { + u.SetNull(tlsfingerprintprofile.FieldPskModes) + return u +} + +// SetExtensions sets the "extensions" field. +func (u *TLSFingerprintProfileUpsert) SetExtensions(v []uint16) *TLSFingerprintProfileUpsert { + u.Set(tlsfingerprintprofile.FieldExtensions, v) + return u +} + +// UpdateExtensions sets the "extensions" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsert) UpdateExtensions() *TLSFingerprintProfileUpsert { + u.SetExcluded(tlsfingerprintprofile.FieldExtensions) + return u +} + +// ClearExtensions clears the value of the "extensions" field. +func (u *TLSFingerprintProfileUpsert) ClearExtensions() *TLSFingerprintProfileUpsert { + u.SetNull(tlsfingerprintprofile.FieldExtensions) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create. +// Using this option is equivalent to using: +// +// client.TLSFingerprintProfile.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// ). +// Exec(ctx) +func (u *TLSFingerprintProfileUpsertOne) UpdateNewValues() *TLSFingerprintProfileUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.CreatedAt(); exists { + s.SetIgnore(tlsfingerprintprofile.FieldCreatedAt) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.TLSFingerprintProfile.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *TLSFingerprintProfileUpsertOne) Ignore() *TLSFingerprintProfileUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *TLSFingerprintProfileUpsertOne) DoNothing() *TLSFingerprintProfileUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the TLSFingerprintProfileCreate.OnConflict +// documentation for more info. +func (u *TLSFingerprintProfileUpsertOne) Update(set func(*TLSFingerprintProfileUpsert)) *TLSFingerprintProfileUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&TLSFingerprintProfileUpsert{UpdateSet: update}) + })) + return u +} + +// SetUpdatedAt sets the "updated_at" field. +func (u *TLSFingerprintProfileUpsertOne) SetUpdatedAt(v time.Time) *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetUpdatedAt(v) + }) +} + +// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertOne) UpdateUpdatedAt() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateUpdatedAt() + }) +} + +// SetName sets the "name" field. +func (u *TLSFingerprintProfileUpsertOne) SetName(v string) *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertOne) UpdateName() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateName() + }) +} + +// SetDescription sets the "description" field. +func (u *TLSFingerprintProfileUpsertOne) SetDescription(v string) *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetDescription(v) + }) +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertOne) UpdateDescription() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateDescription() + }) +} + +// ClearDescription clears the value of the "description" field. +func (u *TLSFingerprintProfileUpsertOne) ClearDescription() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.ClearDescription() + }) +} + +// SetEnableGrease sets the "enable_grease" field. +func (u *TLSFingerprintProfileUpsertOne) SetEnableGrease(v bool) *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetEnableGrease(v) + }) +} + +// UpdateEnableGrease sets the "enable_grease" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertOne) UpdateEnableGrease() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateEnableGrease() + }) +} + +// SetCipherSuites sets the "cipher_suites" field. +func (u *TLSFingerprintProfileUpsertOne) SetCipherSuites(v []uint16) *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetCipherSuites(v) + }) +} + +// UpdateCipherSuites sets the "cipher_suites" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertOne) UpdateCipherSuites() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateCipherSuites() + }) +} + +// ClearCipherSuites clears the value of the "cipher_suites" field. +func (u *TLSFingerprintProfileUpsertOne) ClearCipherSuites() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.ClearCipherSuites() + }) +} + +// SetCurves sets the "curves" field. +func (u *TLSFingerprintProfileUpsertOne) SetCurves(v []uint16) *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetCurves(v) + }) +} + +// UpdateCurves sets the "curves" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertOne) UpdateCurves() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateCurves() + }) +} + +// ClearCurves clears the value of the "curves" field. +func (u *TLSFingerprintProfileUpsertOne) ClearCurves() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.ClearCurves() + }) +} + +// SetPointFormats sets the "point_formats" field. +func (u *TLSFingerprintProfileUpsertOne) SetPointFormats(v []uint16) *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetPointFormats(v) + }) +} + +// UpdatePointFormats sets the "point_formats" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertOne) UpdatePointFormats() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdatePointFormats() + }) +} + +// ClearPointFormats clears the value of the "point_formats" field. +func (u *TLSFingerprintProfileUpsertOne) ClearPointFormats() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.ClearPointFormats() + }) +} + +// SetSignatureAlgorithms sets the "signature_algorithms" field. +func (u *TLSFingerprintProfileUpsertOne) SetSignatureAlgorithms(v []uint16) *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetSignatureAlgorithms(v) + }) +} + +// UpdateSignatureAlgorithms sets the "signature_algorithms" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertOne) UpdateSignatureAlgorithms() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateSignatureAlgorithms() + }) +} + +// ClearSignatureAlgorithms clears the value of the "signature_algorithms" field. +func (u *TLSFingerprintProfileUpsertOne) ClearSignatureAlgorithms() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.ClearSignatureAlgorithms() + }) +} + +// SetAlpnProtocols sets the "alpn_protocols" field. +func (u *TLSFingerprintProfileUpsertOne) SetAlpnProtocols(v []string) *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetAlpnProtocols(v) + }) +} + +// UpdateAlpnProtocols sets the "alpn_protocols" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertOne) UpdateAlpnProtocols() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateAlpnProtocols() + }) +} + +// ClearAlpnProtocols clears the value of the "alpn_protocols" field. +func (u *TLSFingerprintProfileUpsertOne) ClearAlpnProtocols() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.ClearAlpnProtocols() + }) +} + +// SetSupportedVersions sets the "supported_versions" field. +func (u *TLSFingerprintProfileUpsertOne) SetSupportedVersions(v []uint16) *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetSupportedVersions(v) + }) +} + +// UpdateSupportedVersions sets the "supported_versions" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertOne) UpdateSupportedVersions() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateSupportedVersions() + }) +} + +// ClearSupportedVersions clears the value of the "supported_versions" field. +func (u *TLSFingerprintProfileUpsertOne) ClearSupportedVersions() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.ClearSupportedVersions() + }) +} + +// SetKeyShareGroups sets the "key_share_groups" field. +func (u *TLSFingerprintProfileUpsertOne) SetKeyShareGroups(v []uint16) *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetKeyShareGroups(v) + }) +} + +// UpdateKeyShareGroups sets the "key_share_groups" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertOne) UpdateKeyShareGroups() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateKeyShareGroups() + }) +} + +// ClearKeyShareGroups clears the value of the "key_share_groups" field. +func (u *TLSFingerprintProfileUpsertOne) ClearKeyShareGroups() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.ClearKeyShareGroups() + }) +} + +// SetPskModes sets the "psk_modes" field. +func (u *TLSFingerprintProfileUpsertOne) SetPskModes(v []uint16) *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetPskModes(v) + }) +} + +// UpdatePskModes sets the "psk_modes" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertOne) UpdatePskModes() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdatePskModes() + }) +} + +// ClearPskModes clears the value of the "psk_modes" field. +func (u *TLSFingerprintProfileUpsertOne) ClearPskModes() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.ClearPskModes() + }) +} + +// SetExtensions sets the "extensions" field. +func (u *TLSFingerprintProfileUpsertOne) SetExtensions(v []uint16) *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetExtensions(v) + }) +} + +// UpdateExtensions sets the "extensions" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertOne) UpdateExtensions() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateExtensions() + }) +} + +// ClearExtensions clears the value of the "extensions" field. +func (u *TLSFingerprintProfileUpsertOne) ClearExtensions() *TLSFingerprintProfileUpsertOne { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.ClearExtensions() + }) +} + +// Exec executes the query. +func (u *TLSFingerprintProfileUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for TLSFingerprintProfileCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *TLSFingerprintProfileUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *TLSFingerprintProfileUpsertOne) ID(ctx context.Context) (id int64, err error) { + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *TLSFingerprintProfileUpsertOne) IDX(ctx context.Context) int64 { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// TLSFingerprintProfileCreateBulk is the builder for creating many TLSFingerprintProfile entities in bulk. +type TLSFingerprintProfileCreateBulk struct { + config + err error + builders []*TLSFingerprintProfileCreate + conflict []sql.ConflictOption +} + +// Save creates the TLSFingerprintProfile entities in the database. +func (_c *TLSFingerprintProfileCreateBulk) Save(ctx context.Context) ([]*TLSFingerprintProfile, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*TLSFingerprintProfile, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*TLSFingerprintProfileMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = _c.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + if specs[i].ID.Value != nil { + id := specs[i].ID.Value.(int64) + nodes[i].ID = int64(id) + } + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *TLSFingerprintProfileCreateBulk) SaveX(ctx context.Context) []*TLSFingerprintProfile { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *TLSFingerprintProfileCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *TLSFingerprintProfileCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.TLSFingerprintProfile.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.TLSFingerprintProfileUpsert) { +// SetCreatedAt(v+v). +// }). +// Exec(ctx) +func (_c *TLSFingerprintProfileCreateBulk) OnConflict(opts ...sql.ConflictOption) *TLSFingerprintProfileUpsertBulk { + _c.conflict = opts + return &TLSFingerprintProfileUpsertBulk{ + create: _c, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.TLSFingerprintProfile.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (_c *TLSFingerprintProfileCreateBulk) OnConflictColumns(columns ...string) *TLSFingerprintProfileUpsertBulk { + _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...)) + return &TLSFingerprintProfileUpsertBulk{ + create: _c, + } +} + +// TLSFingerprintProfileUpsertBulk is the builder for "upsert"-ing +// a bulk of TLSFingerprintProfile nodes. +type TLSFingerprintProfileUpsertBulk struct { + create *TLSFingerprintProfileCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.TLSFingerprintProfile.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// ). +// Exec(ctx) +func (u *TLSFingerprintProfileUpsertBulk) UpdateNewValues() *TLSFingerprintProfileUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.CreatedAt(); exists { + s.SetIgnore(tlsfingerprintprofile.FieldCreatedAt) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.TLSFingerprintProfile.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *TLSFingerprintProfileUpsertBulk) Ignore() *TLSFingerprintProfileUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *TLSFingerprintProfileUpsertBulk) DoNothing() *TLSFingerprintProfileUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the TLSFingerprintProfileCreateBulk.OnConflict +// documentation for more info. +func (u *TLSFingerprintProfileUpsertBulk) Update(set func(*TLSFingerprintProfileUpsert)) *TLSFingerprintProfileUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&TLSFingerprintProfileUpsert{UpdateSet: update}) + })) + return u +} + +// SetUpdatedAt sets the "updated_at" field. +func (u *TLSFingerprintProfileUpsertBulk) SetUpdatedAt(v time.Time) *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetUpdatedAt(v) + }) +} + +// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertBulk) UpdateUpdatedAt() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateUpdatedAt() + }) +} + +// SetName sets the "name" field. +func (u *TLSFingerprintProfileUpsertBulk) SetName(v string) *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertBulk) UpdateName() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateName() + }) +} + +// SetDescription sets the "description" field. +func (u *TLSFingerprintProfileUpsertBulk) SetDescription(v string) *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetDescription(v) + }) +} + +// UpdateDescription sets the "description" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertBulk) UpdateDescription() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateDescription() + }) +} + +// ClearDescription clears the value of the "description" field. +func (u *TLSFingerprintProfileUpsertBulk) ClearDescription() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.ClearDescription() + }) +} + +// SetEnableGrease sets the "enable_grease" field. +func (u *TLSFingerprintProfileUpsertBulk) SetEnableGrease(v bool) *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetEnableGrease(v) + }) +} + +// UpdateEnableGrease sets the "enable_grease" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertBulk) UpdateEnableGrease() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateEnableGrease() + }) +} + +// SetCipherSuites sets the "cipher_suites" field. +func (u *TLSFingerprintProfileUpsertBulk) SetCipherSuites(v []uint16) *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetCipherSuites(v) + }) +} + +// UpdateCipherSuites sets the "cipher_suites" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertBulk) UpdateCipherSuites() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateCipherSuites() + }) +} + +// ClearCipherSuites clears the value of the "cipher_suites" field. +func (u *TLSFingerprintProfileUpsertBulk) ClearCipherSuites() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.ClearCipherSuites() + }) +} + +// SetCurves sets the "curves" field. +func (u *TLSFingerprintProfileUpsertBulk) SetCurves(v []uint16) *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetCurves(v) + }) +} + +// UpdateCurves sets the "curves" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertBulk) UpdateCurves() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateCurves() + }) +} + +// ClearCurves clears the value of the "curves" field. +func (u *TLSFingerprintProfileUpsertBulk) ClearCurves() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.ClearCurves() + }) +} + +// SetPointFormats sets the "point_formats" field. +func (u *TLSFingerprintProfileUpsertBulk) SetPointFormats(v []uint16) *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetPointFormats(v) + }) +} + +// UpdatePointFormats sets the "point_formats" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertBulk) UpdatePointFormats() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdatePointFormats() + }) +} + +// ClearPointFormats clears the value of the "point_formats" field. +func (u *TLSFingerprintProfileUpsertBulk) ClearPointFormats() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.ClearPointFormats() + }) +} + +// SetSignatureAlgorithms sets the "signature_algorithms" field. +func (u *TLSFingerprintProfileUpsertBulk) SetSignatureAlgorithms(v []uint16) *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetSignatureAlgorithms(v) + }) +} + +// UpdateSignatureAlgorithms sets the "signature_algorithms" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertBulk) UpdateSignatureAlgorithms() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateSignatureAlgorithms() + }) +} + +// ClearSignatureAlgorithms clears the value of the "signature_algorithms" field. +func (u *TLSFingerprintProfileUpsertBulk) ClearSignatureAlgorithms() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.ClearSignatureAlgorithms() + }) +} + +// SetAlpnProtocols sets the "alpn_protocols" field. +func (u *TLSFingerprintProfileUpsertBulk) SetAlpnProtocols(v []string) *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetAlpnProtocols(v) + }) +} + +// UpdateAlpnProtocols sets the "alpn_protocols" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertBulk) UpdateAlpnProtocols() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateAlpnProtocols() + }) +} + +// ClearAlpnProtocols clears the value of the "alpn_protocols" field. +func (u *TLSFingerprintProfileUpsertBulk) ClearAlpnProtocols() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.ClearAlpnProtocols() + }) +} + +// SetSupportedVersions sets the "supported_versions" field. +func (u *TLSFingerprintProfileUpsertBulk) SetSupportedVersions(v []uint16) *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetSupportedVersions(v) + }) +} + +// UpdateSupportedVersions sets the "supported_versions" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertBulk) UpdateSupportedVersions() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateSupportedVersions() + }) +} + +// ClearSupportedVersions clears the value of the "supported_versions" field. +func (u *TLSFingerprintProfileUpsertBulk) ClearSupportedVersions() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.ClearSupportedVersions() + }) +} + +// SetKeyShareGroups sets the "key_share_groups" field. +func (u *TLSFingerprintProfileUpsertBulk) SetKeyShareGroups(v []uint16) *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetKeyShareGroups(v) + }) +} + +// UpdateKeyShareGroups sets the "key_share_groups" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertBulk) UpdateKeyShareGroups() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateKeyShareGroups() + }) +} + +// ClearKeyShareGroups clears the value of the "key_share_groups" field. +func (u *TLSFingerprintProfileUpsertBulk) ClearKeyShareGroups() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.ClearKeyShareGroups() + }) +} + +// SetPskModes sets the "psk_modes" field. +func (u *TLSFingerprintProfileUpsertBulk) SetPskModes(v []uint16) *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetPskModes(v) + }) +} + +// UpdatePskModes sets the "psk_modes" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertBulk) UpdatePskModes() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdatePskModes() + }) +} + +// ClearPskModes clears the value of the "psk_modes" field. +func (u *TLSFingerprintProfileUpsertBulk) ClearPskModes() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.ClearPskModes() + }) +} + +// SetExtensions sets the "extensions" field. +func (u *TLSFingerprintProfileUpsertBulk) SetExtensions(v []uint16) *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.SetExtensions(v) + }) +} + +// UpdateExtensions sets the "extensions" field to the value that was provided on create. +func (u *TLSFingerprintProfileUpsertBulk) UpdateExtensions() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.UpdateExtensions() + }) +} + +// ClearExtensions clears the value of the "extensions" field. +func (u *TLSFingerprintProfileUpsertBulk) ClearExtensions() *TLSFingerprintProfileUpsertBulk { + return u.Update(func(s *TLSFingerprintProfileUpsert) { + s.ClearExtensions() + }) +} + +// Exec executes the query. +func (u *TLSFingerprintProfileUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the TLSFingerprintProfileCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for TLSFingerprintProfileCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *TLSFingerprintProfileUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/backend/ent/tlsfingerprintprofile_delete.go b/backend/ent/tlsfingerprintprofile_delete.go new file mode 100644 index 00000000..2f6dea2e --- /dev/null +++ b/backend/ent/tlsfingerprintprofile_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/Wei-Shaw/sub2api/ent/predicate" + "github.com/Wei-Shaw/sub2api/ent/tlsfingerprintprofile" +) + +// TLSFingerprintProfileDelete is the builder for deleting a TLSFingerprintProfile entity. +type TLSFingerprintProfileDelete struct { + config + hooks []Hook + mutation *TLSFingerprintProfileMutation +} + +// Where appends a list predicates to the TLSFingerprintProfileDelete builder. +func (_d *TLSFingerprintProfileDelete) Where(ps ...predicate.TLSFingerprintProfile) *TLSFingerprintProfileDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *TLSFingerprintProfileDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *TLSFingerprintProfileDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *TLSFingerprintProfileDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(tlsfingerprintprofile.Table, sqlgraph.NewFieldSpec(tlsfingerprintprofile.FieldID, field.TypeInt64)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// TLSFingerprintProfileDeleteOne is the builder for deleting a single TLSFingerprintProfile entity. +type TLSFingerprintProfileDeleteOne struct { + _d *TLSFingerprintProfileDelete +} + +// Where appends a list predicates to the TLSFingerprintProfileDelete builder. +func (_d *TLSFingerprintProfileDeleteOne) Where(ps ...predicate.TLSFingerprintProfile) *TLSFingerprintProfileDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *TLSFingerprintProfileDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{tlsfingerprintprofile.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *TLSFingerprintProfileDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/backend/ent/tlsfingerprintprofile_query.go b/backend/ent/tlsfingerprintprofile_query.go new file mode 100644 index 00000000..d1ef4f1d --- /dev/null +++ b/backend/ent/tlsfingerprintprofile_query.go @@ -0,0 +1,564 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/Wei-Shaw/sub2api/ent/predicate" + "github.com/Wei-Shaw/sub2api/ent/tlsfingerprintprofile" +) + +// TLSFingerprintProfileQuery is the builder for querying TLSFingerprintProfile entities. +type TLSFingerprintProfileQuery struct { + config + ctx *QueryContext + order []tlsfingerprintprofile.OrderOption + inters []Interceptor + predicates []predicate.TLSFingerprintProfile + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the TLSFingerprintProfileQuery builder. +func (_q *TLSFingerprintProfileQuery) Where(ps ...predicate.TLSFingerprintProfile) *TLSFingerprintProfileQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *TLSFingerprintProfileQuery) Limit(limit int) *TLSFingerprintProfileQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *TLSFingerprintProfileQuery) Offset(offset int) *TLSFingerprintProfileQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *TLSFingerprintProfileQuery) Unique(unique bool) *TLSFingerprintProfileQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *TLSFingerprintProfileQuery) Order(o ...tlsfingerprintprofile.OrderOption) *TLSFingerprintProfileQuery { + _q.order = append(_q.order, o...) + return _q +} + +// First returns the first TLSFingerprintProfile entity from the query. +// Returns a *NotFoundError when no TLSFingerprintProfile was found. +func (_q *TLSFingerprintProfileQuery) First(ctx context.Context) (*TLSFingerprintProfile, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{tlsfingerprintprofile.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *TLSFingerprintProfileQuery) FirstX(ctx context.Context) *TLSFingerprintProfile { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first TLSFingerprintProfile ID from the query. +// Returns a *NotFoundError when no TLSFingerprintProfile ID was found. +func (_q *TLSFingerprintProfileQuery) FirstID(ctx context.Context) (id int64, err error) { + var ids []int64 + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{tlsfingerprintprofile.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *TLSFingerprintProfileQuery) FirstIDX(ctx context.Context) int64 { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single TLSFingerprintProfile entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one TLSFingerprintProfile entity is found. +// Returns a *NotFoundError when no TLSFingerprintProfile entities are found. +func (_q *TLSFingerprintProfileQuery) Only(ctx context.Context) (*TLSFingerprintProfile, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{tlsfingerprintprofile.Label} + default: + return nil, &NotSingularError{tlsfingerprintprofile.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *TLSFingerprintProfileQuery) OnlyX(ctx context.Context) *TLSFingerprintProfile { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only TLSFingerprintProfile ID in the query. +// Returns a *NotSingularError when more than one TLSFingerprintProfile ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *TLSFingerprintProfileQuery) OnlyID(ctx context.Context) (id int64, err error) { + var ids []int64 + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{tlsfingerprintprofile.Label} + default: + err = &NotSingularError{tlsfingerprintprofile.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *TLSFingerprintProfileQuery) OnlyIDX(ctx context.Context) int64 { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of TLSFingerprintProfiles. +func (_q *TLSFingerprintProfileQuery) All(ctx context.Context) ([]*TLSFingerprintProfile, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*TLSFingerprintProfile, *TLSFingerprintProfileQuery]() + return withInterceptors[[]*TLSFingerprintProfile](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *TLSFingerprintProfileQuery) AllX(ctx context.Context) []*TLSFingerprintProfile { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of TLSFingerprintProfile IDs. +func (_q *TLSFingerprintProfileQuery) IDs(ctx context.Context) (ids []int64, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(tlsfingerprintprofile.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *TLSFingerprintProfileQuery) IDsX(ctx context.Context) []int64 { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *TLSFingerprintProfileQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*TLSFingerprintProfileQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *TLSFingerprintProfileQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *TLSFingerprintProfileQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *TLSFingerprintProfileQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the TLSFingerprintProfileQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *TLSFingerprintProfileQuery) Clone() *TLSFingerprintProfileQuery { + if _q == nil { + return nil + } + return &TLSFingerprintProfileQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]tlsfingerprintprofile.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.TLSFingerprintProfile{}, _q.predicates...), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// CreatedAt time.Time `json:"created_at,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.TLSFingerprintProfile.Query(). +// GroupBy(tlsfingerprintprofile.FieldCreatedAt). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *TLSFingerprintProfileQuery) GroupBy(field string, fields ...string) *TLSFingerprintProfileGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &TLSFingerprintProfileGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = tlsfingerprintprofile.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// CreatedAt time.Time `json:"created_at,omitempty"` +// } +// +// client.TLSFingerprintProfile.Query(). +// Select(tlsfingerprintprofile.FieldCreatedAt). +// Scan(ctx, &v) +func (_q *TLSFingerprintProfileQuery) Select(fields ...string) *TLSFingerprintProfileSelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &TLSFingerprintProfileSelect{TLSFingerprintProfileQuery: _q} + sbuild.label = tlsfingerprintprofile.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a TLSFingerprintProfileSelect configured with the given aggregations. +func (_q *TLSFingerprintProfileQuery) Aggregate(fns ...AggregateFunc) *TLSFingerprintProfileSelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *TLSFingerprintProfileQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !tlsfingerprintprofile.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *TLSFingerprintProfileQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*TLSFingerprintProfile, error) { + var ( + nodes = []*TLSFingerprintProfile{} + _spec = _q.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*TLSFingerprintProfile).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &TLSFingerprintProfile{config: _q.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (_q *TLSFingerprintProfileQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + if len(_q.modifiers) > 0 { + _spec.Modifiers = _q.modifiers + } + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *TLSFingerprintProfileQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(tlsfingerprintprofile.Table, tlsfingerprintprofile.Columns, sqlgraph.NewFieldSpec(tlsfingerprintprofile.FieldID, field.TypeInt64)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, tlsfingerprintprofile.FieldID) + for i := range fields { + if fields[i] != tlsfingerprintprofile.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *TLSFingerprintProfileQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(tlsfingerprintprofile.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = tlsfingerprintprofile.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, m := range _q.modifiers { + m(selector) + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// ForUpdate locks the selected rows against concurrent updates, and prevent them from being +// updated, deleted or "selected ... for update" by other sessions, until the transaction is +// either committed or rolled-back. +func (_q *TLSFingerprintProfileQuery) ForUpdate(opts ...sql.LockOption) *TLSFingerprintProfileQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForUpdate(opts...) + }) + return _q +} + +// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock +// on any rows that are read. Other sessions can read the rows, but cannot modify them +// until your transaction commits. +func (_q *TLSFingerprintProfileQuery) ForShare(opts ...sql.LockOption) *TLSFingerprintProfileQuery { + if _q.driver.Dialect() == dialect.Postgres { + _q.Unique(false) + } + _q.modifiers = append(_q.modifiers, func(s *sql.Selector) { + s.ForShare(opts...) + }) + return _q +} + +// TLSFingerprintProfileGroupBy is the group-by builder for TLSFingerprintProfile entities. +type TLSFingerprintProfileGroupBy struct { + selector + build *TLSFingerprintProfileQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *TLSFingerprintProfileGroupBy) Aggregate(fns ...AggregateFunc) *TLSFingerprintProfileGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *TLSFingerprintProfileGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*TLSFingerprintProfileQuery, *TLSFingerprintProfileGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *TLSFingerprintProfileGroupBy) sqlScan(ctx context.Context, root *TLSFingerprintProfileQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// TLSFingerprintProfileSelect is the builder for selecting fields of TLSFingerprintProfile entities. +type TLSFingerprintProfileSelect struct { + *TLSFingerprintProfileQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *TLSFingerprintProfileSelect) Aggregate(fns ...AggregateFunc) *TLSFingerprintProfileSelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *TLSFingerprintProfileSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*TLSFingerprintProfileQuery, *TLSFingerprintProfileSelect](ctx, _s.TLSFingerprintProfileQuery, _s, _s.inters, v) +} + +func (_s *TLSFingerprintProfileSelect) sqlScan(ctx context.Context, root *TLSFingerprintProfileQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/backend/ent/tlsfingerprintprofile_update.go b/backend/ent/tlsfingerprintprofile_update.go new file mode 100644 index 00000000..3b12508c --- /dev/null +++ b/backend/ent/tlsfingerprintprofile_update.go @@ -0,0 +1,881 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/dialect/sql/sqljson" + "entgo.io/ent/schema/field" + "github.com/Wei-Shaw/sub2api/ent/predicate" + "github.com/Wei-Shaw/sub2api/ent/tlsfingerprintprofile" +) + +// TLSFingerprintProfileUpdate is the builder for updating TLSFingerprintProfile entities. +type TLSFingerprintProfileUpdate struct { + config + hooks []Hook + mutation *TLSFingerprintProfileMutation +} + +// Where appends a list predicates to the TLSFingerprintProfileUpdate builder. +func (_u *TLSFingerprintProfileUpdate) Where(ps ...predicate.TLSFingerprintProfile) *TLSFingerprintProfileUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetUpdatedAt sets the "updated_at" field. +func (_u *TLSFingerprintProfileUpdate) SetUpdatedAt(v time.Time) *TLSFingerprintProfileUpdate { + _u.mutation.SetUpdatedAt(v) + return _u +} + +// SetName sets the "name" field. +func (_u *TLSFingerprintProfileUpdate) SetName(v string) *TLSFingerprintProfileUpdate { + _u.mutation.SetName(v) + return _u +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (_u *TLSFingerprintProfileUpdate) SetNillableName(v *string) *TLSFingerprintProfileUpdate { + if v != nil { + _u.SetName(*v) + } + return _u +} + +// SetDescription sets the "description" field. +func (_u *TLSFingerprintProfileUpdate) SetDescription(v string) *TLSFingerprintProfileUpdate { + _u.mutation.SetDescription(v) + return _u +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (_u *TLSFingerprintProfileUpdate) SetNillableDescription(v *string) *TLSFingerprintProfileUpdate { + if v != nil { + _u.SetDescription(*v) + } + return _u +} + +// ClearDescription clears the value of the "description" field. +func (_u *TLSFingerprintProfileUpdate) ClearDescription() *TLSFingerprintProfileUpdate { + _u.mutation.ClearDescription() + return _u +} + +// SetEnableGrease sets the "enable_grease" field. +func (_u *TLSFingerprintProfileUpdate) SetEnableGrease(v bool) *TLSFingerprintProfileUpdate { + _u.mutation.SetEnableGrease(v) + return _u +} + +// SetNillableEnableGrease sets the "enable_grease" field if the given value is not nil. +func (_u *TLSFingerprintProfileUpdate) SetNillableEnableGrease(v *bool) *TLSFingerprintProfileUpdate { + if v != nil { + _u.SetEnableGrease(*v) + } + return _u +} + +// SetCipherSuites sets the "cipher_suites" field. +func (_u *TLSFingerprintProfileUpdate) SetCipherSuites(v []uint16) *TLSFingerprintProfileUpdate { + _u.mutation.SetCipherSuites(v) + return _u +} + +// AppendCipherSuites appends value to the "cipher_suites" field. +func (_u *TLSFingerprintProfileUpdate) AppendCipherSuites(v []uint16) *TLSFingerprintProfileUpdate { + _u.mutation.AppendCipherSuites(v) + return _u +} + +// ClearCipherSuites clears the value of the "cipher_suites" field. +func (_u *TLSFingerprintProfileUpdate) ClearCipherSuites() *TLSFingerprintProfileUpdate { + _u.mutation.ClearCipherSuites() + return _u +} + +// SetCurves sets the "curves" field. +func (_u *TLSFingerprintProfileUpdate) SetCurves(v []uint16) *TLSFingerprintProfileUpdate { + _u.mutation.SetCurves(v) + return _u +} + +// AppendCurves appends value to the "curves" field. +func (_u *TLSFingerprintProfileUpdate) AppendCurves(v []uint16) *TLSFingerprintProfileUpdate { + _u.mutation.AppendCurves(v) + return _u +} + +// ClearCurves clears the value of the "curves" field. +func (_u *TLSFingerprintProfileUpdate) ClearCurves() *TLSFingerprintProfileUpdate { + _u.mutation.ClearCurves() + return _u +} + +// SetPointFormats sets the "point_formats" field. +func (_u *TLSFingerprintProfileUpdate) SetPointFormats(v []uint16) *TLSFingerprintProfileUpdate { + _u.mutation.SetPointFormats(v) + return _u +} + +// AppendPointFormats appends value to the "point_formats" field. +func (_u *TLSFingerprintProfileUpdate) AppendPointFormats(v []uint16) *TLSFingerprintProfileUpdate { + _u.mutation.AppendPointFormats(v) + return _u +} + +// ClearPointFormats clears the value of the "point_formats" field. +func (_u *TLSFingerprintProfileUpdate) ClearPointFormats() *TLSFingerprintProfileUpdate { + _u.mutation.ClearPointFormats() + return _u +} + +// SetSignatureAlgorithms sets the "signature_algorithms" field. +func (_u *TLSFingerprintProfileUpdate) SetSignatureAlgorithms(v []uint16) *TLSFingerprintProfileUpdate { + _u.mutation.SetSignatureAlgorithms(v) + return _u +} + +// AppendSignatureAlgorithms appends value to the "signature_algorithms" field. +func (_u *TLSFingerprintProfileUpdate) AppendSignatureAlgorithms(v []uint16) *TLSFingerprintProfileUpdate { + _u.mutation.AppendSignatureAlgorithms(v) + return _u +} + +// ClearSignatureAlgorithms clears the value of the "signature_algorithms" field. +func (_u *TLSFingerprintProfileUpdate) ClearSignatureAlgorithms() *TLSFingerprintProfileUpdate { + _u.mutation.ClearSignatureAlgorithms() + return _u +} + +// SetAlpnProtocols sets the "alpn_protocols" field. +func (_u *TLSFingerprintProfileUpdate) SetAlpnProtocols(v []string) *TLSFingerprintProfileUpdate { + _u.mutation.SetAlpnProtocols(v) + return _u +} + +// AppendAlpnProtocols appends value to the "alpn_protocols" field. +func (_u *TLSFingerprintProfileUpdate) AppendAlpnProtocols(v []string) *TLSFingerprintProfileUpdate { + _u.mutation.AppendAlpnProtocols(v) + return _u +} + +// ClearAlpnProtocols clears the value of the "alpn_protocols" field. +func (_u *TLSFingerprintProfileUpdate) ClearAlpnProtocols() *TLSFingerprintProfileUpdate { + _u.mutation.ClearAlpnProtocols() + return _u +} + +// SetSupportedVersions sets the "supported_versions" field. +func (_u *TLSFingerprintProfileUpdate) SetSupportedVersions(v []uint16) *TLSFingerprintProfileUpdate { + _u.mutation.SetSupportedVersions(v) + return _u +} + +// AppendSupportedVersions appends value to the "supported_versions" field. +func (_u *TLSFingerprintProfileUpdate) AppendSupportedVersions(v []uint16) *TLSFingerprintProfileUpdate { + _u.mutation.AppendSupportedVersions(v) + return _u +} + +// ClearSupportedVersions clears the value of the "supported_versions" field. +func (_u *TLSFingerprintProfileUpdate) ClearSupportedVersions() *TLSFingerprintProfileUpdate { + _u.mutation.ClearSupportedVersions() + return _u +} + +// SetKeyShareGroups sets the "key_share_groups" field. +func (_u *TLSFingerprintProfileUpdate) SetKeyShareGroups(v []uint16) *TLSFingerprintProfileUpdate { + _u.mutation.SetKeyShareGroups(v) + return _u +} + +// AppendKeyShareGroups appends value to the "key_share_groups" field. +func (_u *TLSFingerprintProfileUpdate) AppendKeyShareGroups(v []uint16) *TLSFingerprintProfileUpdate { + _u.mutation.AppendKeyShareGroups(v) + return _u +} + +// ClearKeyShareGroups clears the value of the "key_share_groups" field. +func (_u *TLSFingerprintProfileUpdate) ClearKeyShareGroups() *TLSFingerprintProfileUpdate { + _u.mutation.ClearKeyShareGroups() + return _u +} + +// SetPskModes sets the "psk_modes" field. +func (_u *TLSFingerprintProfileUpdate) SetPskModes(v []uint16) *TLSFingerprintProfileUpdate { + _u.mutation.SetPskModes(v) + return _u +} + +// AppendPskModes appends value to the "psk_modes" field. +func (_u *TLSFingerprintProfileUpdate) AppendPskModes(v []uint16) *TLSFingerprintProfileUpdate { + _u.mutation.AppendPskModes(v) + return _u +} + +// ClearPskModes clears the value of the "psk_modes" field. +func (_u *TLSFingerprintProfileUpdate) ClearPskModes() *TLSFingerprintProfileUpdate { + _u.mutation.ClearPskModes() + return _u +} + +// SetExtensions sets the "extensions" field. +func (_u *TLSFingerprintProfileUpdate) SetExtensions(v []uint16) *TLSFingerprintProfileUpdate { + _u.mutation.SetExtensions(v) + return _u +} + +// AppendExtensions appends value to the "extensions" field. +func (_u *TLSFingerprintProfileUpdate) AppendExtensions(v []uint16) *TLSFingerprintProfileUpdate { + _u.mutation.AppendExtensions(v) + return _u +} + +// ClearExtensions clears the value of the "extensions" field. +func (_u *TLSFingerprintProfileUpdate) ClearExtensions() *TLSFingerprintProfileUpdate { + _u.mutation.ClearExtensions() + return _u +} + +// Mutation returns the TLSFingerprintProfileMutation object of the builder. +func (_u *TLSFingerprintProfileUpdate) Mutation() *TLSFingerprintProfileMutation { + return _u.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *TLSFingerprintProfileUpdate) Save(ctx context.Context) (int, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *TLSFingerprintProfileUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *TLSFingerprintProfileUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *TLSFingerprintProfileUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *TLSFingerprintProfileUpdate) defaults() { + if _, ok := _u.mutation.UpdatedAt(); !ok { + v := tlsfingerprintprofile.UpdateDefaultUpdatedAt() + _u.mutation.SetUpdatedAt(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *TLSFingerprintProfileUpdate) check() error { + if v, ok := _u.mutation.Name(); ok { + if err := tlsfingerprintprofile.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "TLSFingerprintProfile.name": %w`, err)} + } + } + return nil +} + +func (_u *TLSFingerprintProfileUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(tlsfingerprintprofile.Table, tlsfingerprintprofile.Columns, sqlgraph.NewFieldSpec(tlsfingerprintprofile.FieldID, field.TypeInt64)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.UpdatedAt(); ok { + _spec.SetField(tlsfingerprintprofile.FieldUpdatedAt, field.TypeTime, value) + } + if value, ok := _u.mutation.Name(); ok { + _spec.SetField(tlsfingerprintprofile.FieldName, field.TypeString, value) + } + if value, ok := _u.mutation.Description(); ok { + _spec.SetField(tlsfingerprintprofile.FieldDescription, field.TypeString, value) + } + if _u.mutation.DescriptionCleared() { + _spec.ClearField(tlsfingerprintprofile.FieldDescription, field.TypeString) + } + if value, ok := _u.mutation.EnableGrease(); ok { + _spec.SetField(tlsfingerprintprofile.FieldEnableGrease, field.TypeBool, value) + } + if value, ok := _u.mutation.CipherSuites(); ok { + _spec.SetField(tlsfingerprintprofile.FieldCipherSuites, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedCipherSuites(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, tlsfingerprintprofile.FieldCipherSuites, value) + }) + } + if _u.mutation.CipherSuitesCleared() { + _spec.ClearField(tlsfingerprintprofile.FieldCipherSuites, field.TypeJSON) + } + if value, ok := _u.mutation.Curves(); ok { + _spec.SetField(tlsfingerprintprofile.FieldCurves, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedCurves(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, tlsfingerprintprofile.FieldCurves, value) + }) + } + if _u.mutation.CurvesCleared() { + _spec.ClearField(tlsfingerprintprofile.FieldCurves, field.TypeJSON) + } + if value, ok := _u.mutation.PointFormats(); ok { + _spec.SetField(tlsfingerprintprofile.FieldPointFormats, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedPointFormats(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, tlsfingerprintprofile.FieldPointFormats, value) + }) + } + if _u.mutation.PointFormatsCleared() { + _spec.ClearField(tlsfingerprintprofile.FieldPointFormats, field.TypeJSON) + } + if value, ok := _u.mutation.SignatureAlgorithms(); ok { + _spec.SetField(tlsfingerprintprofile.FieldSignatureAlgorithms, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedSignatureAlgorithms(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, tlsfingerprintprofile.FieldSignatureAlgorithms, value) + }) + } + if _u.mutation.SignatureAlgorithmsCleared() { + _spec.ClearField(tlsfingerprintprofile.FieldSignatureAlgorithms, field.TypeJSON) + } + if value, ok := _u.mutation.AlpnProtocols(); ok { + _spec.SetField(tlsfingerprintprofile.FieldAlpnProtocols, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedAlpnProtocols(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, tlsfingerprintprofile.FieldAlpnProtocols, value) + }) + } + if _u.mutation.AlpnProtocolsCleared() { + _spec.ClearField(tlsfingerprintprofile.FieldAlpnProtocols, field.TypeJSON) + } + if value, ok := _u.mutation.SupportedVersions(); ok { + _spec.SetField(tlsfingerprintprofile.FieldSupportedVersions, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedSupportedVersions(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, tlsfingerprintprofile.FieldSupportedVersions, value) + }) + } + if _u.mutation.SupportedVersionsCleared() { + _spec.ClearField(tlsfingerprintprofile.FieldSupportedVersions, field.TypeJSON) + } + if value, ok := _u.mutation.KeyShareGroups(); ok { + _spec.SetField(tlsfingerprintprofile.FieldKeyShareGroups, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedKeyShareGroups(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, tlsfingerprintprofile.FieldKeyShareGroups, value) + }) + } + if _u.mutation.KeyShareGroupsCleared() { + _spec.ClearField(tlsfingerprintprofile.FieldKeyShareGroups, field.TypeJSON) + } + if value, ok := _u.mutation.PskModes(); ok { + _spec.SetField(tlsfingerprintprofile.FieldPskModes, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedPskModes(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, tlsfingerprintprofile.FieldPskModes, value) + }) + } + if _u.mutation.PskModesCleared() { + _spec.ClearField(tlsfingerprintprofile.FieldPskModes, field.TypeJSON) + } + if value, ok := _u.mutation.Extensions(); ok { + _spec.SetField(tlsfingerprintprofile.FieldExtensions, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedExtensions(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, tlsfingerprintprofile.FieldExtensions, value) + }) + } + if _u.mutation.ExtensionsCleared() { + _spec.ClearField(tlsfingerprintprofile.FieldExtensions, field.TypeJSON) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{tlsfingerprintprofile.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// TLSFingerprintProfileUpdateOne is the builder for updating a single TLSFingerprintProfile entity. +type TLSFingerprintProfileUpdateOne struct { + config + fields []string + hooks []Hook + mutation *TLSFingerprintProfileMutation +} + +// SetUpdatedAt sets the "updated_at" field. +func (_u *TLSFingerprintProfileUpdateOne) SetUpdatedAt(v time.Time) *TLSFingerprintProfileUpdateOne { + _u.mutation.SetUpdatedAt(v) + return _u +} + +// SetName sets the "name" field. +func (_u *TLSFingerprintProfileUpdateOne) SetName(v string) *TLSFingerprintProfileUpdateOne { + _u.mutation.SetName(v) + return _u +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (_u *TLSFingerprintProfileUpdateOne) SetNillableName(v *string) *TLSFingerprintProfileUpdateOne { + if v != nil { + _u.SetName(*v) + } + return _u +} + +// SetDescription sets the "description" field. +func (_u *TLSFingerprintProfileUpdateOne) SetDescription(v string) *TLSFingerprintProfileUpdateOne { + _u.mutation.SetDescription(v) + return _u +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (_u *TLSFingerprintProfileUpdateOne) SetNillableDescription(v *string) *TLSFingerprintProfileUpdateOne { + if v != nil { + _u.SetDescription(*v) + } + return _u +} + +// ClearDescription clears the value of the "description" field. +func (_u *TLSFingerprintProfileUpdateOne) ClearDescription() *TLSFingerprintProfileUpdateOne { + _u.mutation.ClearDescription() + return _u +} + +// SetEnableGrease sets the "enable_grease" field. +func (_u *TLSFingerprintProfileUpdateOne) SetEnableGrease(v bool) *TLSFingerprintProfileUpdateOne { + _u.mutation.SetEnableGrease(v) + return _u +} + +// SetNillableEnableGrease sets the "enable_grease" field if the given value is not nil. +func (_u *TLSFingerprintProfileUpdateOne) SetNillableEnableGrease(v *bool) *TLSFingerprintProfileUpdateOne { + if v != nil { + _u.SetEnableGrease(*v) + } + return _u +} + +// SetCipherSuites sets the "cipher_suites" field. +func (_u *TLSFingerprintProfileUpdateOne) SetCipherSuites(v []uint16) *TLSFingerprintProfileUpdateOne { + _u.mutation.SetCipherSuites(v) + return _u +} + +// AppendCipherSuites appends value to the "cipher_suites" field. +func (_u *TLSFingerprintProfileUpdateOne) AppendCipherSuites(v []uint16) *TLSFingerprintProfileUpdateOne { + _u.mutation.AppendCipherSuites(v) + return _u +} + +// ClearCipherSuites clears the value of the "cipher_suites" field. +func (_u *TLSFingerprintProfileUpdateOne) ClearCipherSuites() *TLSFingerprintProfileUpdateOne { + _u.mutation.ClearCipherSuites() + return _u +} + +// SetCurves sets the "curves" field. +func (_u *TLSFingerprintProfileUpdateOne) SetCurves(v []uint16) *TLSFingerprintProfileUpdateOne { + _u.mutation.SetCurves(v) + return _u +} + +// AppendCurves appends value to the "curves" field. +func (_u *TLSFingerprintProfileUpdateOne) AppendCurves(v []uint16) *TLSFingerprintProfileUpdateOne { + _u.mutation.AppendCurves(v) + return _u +} + +// ClearCurves clears the value of the "curves" field. +func (_u *TLSFingerprintProfileUpdateOne) ClearCurves() *TLSFingerprintProfileUpdateOne { + _u.mutation.ClearCurves() + return _u +} + +// SetPointFormats sets the "point_formats" field. +func (_u *TLSFingerprintProfileUpdateOne) SetPointFormats(v []uint16) *TLSFingerprintProfileUpdateOne { + _u.mutation.SetPointFormats(v) + return _u +} + +// AppendPointFormats appends value to the "point_formats" field. +func (_u *TLSFingerprintProfileUpdateOne) AppendPointFormats(v []uint16) *TLSFingerprintProfileUpdateOne { + _u.mutation.AppendPointFormats(v) + return _u +} + +// ClearPointFormats clears the value of the "point_formats" field. +func (_u *TLSFingerprintProfileUpdateOne) ClearPointFormats() *TLSFingerprintProfileUpdateOne { + _u.mutation.ClearPointFormats() + return _u +} + +// SetSignatureAlgorithms sets the "signature_algorithms" field. +func (_u *TLSFingerprintProfileUpdateOne) SetSignatureAlgorithms(v []uint16) *TLSFingerprintProfileUpdateOne { + _u.mutation.SetSignatureAlgorithms(v) + return _u +} + +// AppendSignatureAlgorithms appends value to the "signature_algorithms" field. +func (_u *TLSFingerprintProfileUpdateOne) AppendSignatureAlgorithms(v []uint16) *TLSFingerprintProfileUpdateOne { + _u.mutation.AppendSignatureAlgorithms(v) + return _u +} + +// ClearSignatureAlgorithms clears the value of the "signature_algorithms" field. +func (_u *TLSFingerprintProfileUpdateOne) ClearSignatureAlgorithms() *TLSFingerprintProfileUpdateOne { + _u.mutation.ClearSignatureAlgorithms() + return _u +} + +// SetAlpnProtocols sets the "alpn_protocols" field. +func (_u *TLSFingerprintProfileUpdateOne) SetAlpnProtocols(v []string) *TLSFingerprintProfileUpdateOne { + _u.mutation.SetAlpnProtocols(v) + return _u +} + +// AppendAlpnProtocols appends value to the "alpn_protocols" field. +func (_u *TLSFingerprintProfileUpdateOne) AppendAlpnProtocols(v []string) *TLSFingerprintProfileUpdateOne { + _u.mutation.AppendAlpnProtocols(v) + return _u +} + +// ClearAlpnProtocols clears the value of the "alpn_protocols" field. +func (_u *TLSFingerprintProfileUpdateOne) ClearAlpnProtocols() *TLSFingerprintProfileUpdateOne { + _u.mutation.ClearAlpnProtocols() + return _u +} + +// SetSupportedVersions sets the "supported_versions" field. +func (_u *TLSFingerprintProfileUpdateOne) SetSupportedVersions(v []uint16) *TLSFingerprintProfileUpdateOne { + _u.mutation.SetSupportedVersions(v) + return _u +} + +// AppendSupportedVersions appends value to the "supported_versions" field. +func (_u *TLSFingerprintProfileUpdateOne) AppendSupportedVersions(v []uint16) *TLSFingerprintProfileUpdateOne { + _u.mutation.AppendSupportedVersions(v) + return _u +} + +// ClearSupportedVersions clears the value of the "supported_versions" field. +func (_u *TLSFingerprintProfileUpdateOne) ClearSupportedVersions() *TLSFingerprintProfileUpdateOne { + _u.mutation.ClearSupportedVersions() + return _u +} + +// SetKeyShareGroups sets the "key_share_groups" field. +func (_u *TLSFingerprintProfileUpdateOne) SetKeyShareGroups(v []uint16) *TLSFingerprintProfileUpdateOne { + _u.mutation.SetKeyShareGroups(v) + return _u +} + +// AppendKeyShareGroups appends value to the "key_share_groups" field. +func (_u *TLSFingerprintProfileUpdateOne) AppendKeyShareGroups(v []uint16) *TLSFingerprintProfileUpdateOne { + _u.mutation.AppendKeyShareGroups(v) + return _u +} + +// ClearKeyShareGroups clears the value of the "key_share_groups" field. +func (_u *TLSFingerprintProfileUpdateOne) ClearKeyShareGroups() *TLSFingerprintProfileUpdateOne { + _u.mutation.ClearKeyShareGroups() + return _u +} + +// SetPskModes sets the "psk_modes" field. +func (_u *TLSFingerprintProfileUpdateOne) SetPskModes(v []uint16) *TLSFingerprintProfileUpdateOne { + _u.mutation.SetPskModes(v) + return _u +} + +// AppendPskModes appends value to the "psk_modes" field. +func (_u *TLSFingerprintProfileUpdateOne) AppendPskModes(v []uint16) *TLSFingerprintProfileUpdateOne { + _u.mutation.AppendPskModes(v) + return _u +} + +// ClearPskModes clears the value of the "psk_modes" field. +func (_u *TLSFingerprintProfileUpdateOne) ClearPskModes() *TLSFingerprintProfileUpdateOne { + _u.mutation.ClearPskModes() + return _u +} + +// SetExtensions sets the "extensions" field. +func (_u *TLSFingerprintProfileUpdateOne) SetExtensions(v []uint16) *TLSFingerprintProfileUpdateOne { + _u.mutation.SetExtensions(v) + return _u +} + +// AppendExtensions appends value to the "extensions" field. +func (_u *TLSFingerprintProfileUpdateOne) AppendExtensions(v []uint16) *TLSFingerprintProfileUpdateOne { + _u.mutation.AppendExtensions(v) + return _u +} + +// ClearExtensions clears the value of the "extensions" field. +func (_u *TLSFingerprintProfileUpdateOne) ClearExtensions() *TLSFingerprintProfileUpdateOne { + _u.mutation.ClearExtensions() + return _u +} + +// Mutation returns the TLSFingerprintProfileMutation object of the builder. +func (_u *TLSFingerprintProfileUpdateOne) Mutation() *TLSFingerprintProfileMutation { + return _u.mutation +} + +// Where appends a list predicates to the TLSFingerprintProfileUpdate builder. +func (_u *TLSFingerprintProfileUpdateOne) Where(ps ...predicate.TLSFingerprintProfile) *TLSFingerprintProfileUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *TLSFingerprintProfileUpdateOne) Select(field string, fields ...string) *TLSFingerprintProfileUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated TLSFingerprintProfile entity. +func (_u *TLSFingerprintProfileUpdateOne) Save(ctx context.Context) (*TLSFingerprintProfile, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *TLSFingerprintProfileUpdateOne) SaveX(ctx context.Context) *TLSFingerprintProfile { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *TLSFingerprintProfileUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *TLSFingerprintProfileUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *TLSFingerprintProfileUpdateOne) defaults() { + if _, ok := _u.mutation.UpdatedAt(); !ok { + v := tlsfingerprintprofile.UpdateDefaultUpdatedAt() + _u.mutation.SetUpdatedAt(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *TLSFingerprintProfileUpdateOne) check() error { + if v, ok := _u.mutation.Name(); ok { + if err := tlsfingerprintprofile.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "TLSFingerprintProfile.name": %w`, err)} + } + } + return nil +} + +func (_u *TLSFingerprintProfileUpdateOne) sqlSave(ctx context.Context) (_node *TLSFingerprintProfile, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(tlsfingerprintprofile.Table, tlsfingerprintprofile.Columns, sqlgraph.NewFieldSpec(tlsfingerprintprofile.FieldID, field.TypeInt64)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "TLSFingerprintProfile.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, tlsfingerprintprofile.FieldID) + for _, f := range fields { + if !tlsfingerprintprofile.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != tlsfingerprintprofile.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.UpdatedAt(); ok { + _spec.SetField(tlsfingerprintprofile.FieldUpdatedAt, field.TypeTime, value) + } + if value, ok := _u.mutation.Name(); ok { + _spec.SetField(tlsfingerprintprofile.FieldName, field.TypeString, value) + } + if value, ok := _u.mutation.Description(); ok { + _spec.SetField(tlsfingerprintprofile.FieldDescription, field.TypeString, value) + } + if _u.mutation.DescriptionCleared() { + _spec.ClearField(tlsfingerprintprofile.FieldDescription, field.TypeString) + } + if value, ok := _u.mutation.EnableGrease(); ok { + _spec.SetField(tlsfingerprintprofile.FieldEnableGrease, field.TypeBool, value) + } + if value, ok := _u.mutation.CipherSuites(); ok { + _spec.SetField(tlsfingerprintprofile.FieldCipherSuites, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedCipherSuites(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, tlsfingerprintprofile.FieldCipherSuites, value) + }) + } + if _u.mutation.CipherSuitesCleared() { + _spec.ClearField(tlsfingerprintprofile.FieldCipherSuites, field.TypeJSON) + } + if value, ok := _u.mutation.Curves(); ok { + _spec.SetField(tlsfingerprintprofile.FieldCurves, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedCurves(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, tlsfingerprintprofile.FieldCurves, value) + }) + } + if _u.mutation.CurvesCleared() { + _spec.ClearField(tlsfingerprintprofile.FieldCurves, field.TypeJSON) + } + if value, ok := _u.mutation.PointFormats(); ok { + _spec.SetField(tlsfingerprintprofile.FieldPointFormats, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedPointFormats(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, tlsfingerprintprofile.FieldPointFormats, value) + }) + } + if _u.mutation.PointFormatsCleared() { + _spec.ClearField(tlsfingerprintprofile.FieldPointFormats, field.TypeJSON) + } + if value, ok := _u.mutation.SignatureAlgorithms(); ok { + _spec.SetField(tlsfingerprintprofile.FieldSignatureAlgorithms, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedSignatureAlgorithms(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, tlsfingerprintprofile.FieldSignatureAlgorithms, value) + }) + } + if _u.mutation.SignatureAlgorithmsCleared() { + _spec.ClearField(tlsfingerprintprofile.FieldSignatureAlgorithms, field.TypeJSON) + } + if value, ok := _u.mutation.AlpnProtocols(); ok { + _spec.SetField(tlsfingerprintprofile.FieldAlpnProtocols, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedAlpnProtocols(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, tlsfingerprintprofile.FieldAlpnProtocols, value) + }) + } + if _u.mutation.AlpnProtocolsCleared() { + _spec.ClearField(tlsfingerprintprofile.FieldAlpnProtocols, field.TypeJSON) + } + if value, ok := _u.mutation.SupportedVersions(); ok { + _spec.SetField(tlsfingerprintprofile.FieldSupportedVersions, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedSupportedVersions(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, tlsfingerprintprofile.FieldSupportedVersions, value) + }) + } + if _u.mutation.SupportedVersionsCleared() { + _spec.ClearField(tlsfingerprintprofile.FieldSupportedVersions, field.TypeJSON) + } + if value, ok := _u.mutation.KeyShareGroups(); ok { + _spec.SetField(tlsfingerprintprofile.FieldKeyShareGroups, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedKeyShareGroups(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, tlsfingerprintprofile.FieldKeyShareGroups, value) + }) + } + if _u.mutation.KeyShareGroupsCleared() { + _spec.ClearField(tlsfingerprintprofile.FieldKeyShareGroups, field.TypeJSON) + } + if value, ok := _u.mutation.PskModes(); ok { + _spec.SetField(tlsfingerprintprofile.FieldPskModes, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedPskModes(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, tlsfingerprintprofile.FieldPskModes, value) + }) + } + if _u.mutation.PskModesCleared() { + _spec.ClearField(tlsfingerprintprofile.FieldPskModes, field.TypeJSON) + } + if value, ok := _u.mutation.Extensions(); ok { + _spec.SetField(tlsfingerprintprofile.FieldExtensions, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedExtensions(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, tlsfingerprintprofile.FieldExtensions, value) + }) + } + if _u.mutation.ExtensionsCleared() { + _spec.ClearField(tlsfingerprintprofile.FieldExtensions, field.TypeJSON) + } + _node = &TLSFingerprintProfile{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{tlsfingerprintprofile.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/backend/ent/tx.go b/backend/ent/tx.go index cd3b2296..b5aea447 100644 --- a/backend/ent/tx.go +++ b/backend/ent/tx.go @@ -42,6 +42,8 @@ type Tx struct { SecuritySecret *SecuritySecretClient // Setting is the client for interacting with the Setting builders. Setting *SettingClient + // TLSFingerprintProfile is the client for interacting with the TLSFingerprintProfile builders. + TLSFingerprintProfile *TLSFingerprintProfileClient // UsageCleanupTask is the client for interacting with the UsageCleanupTask builders. UsageCleanupTask *UsageCleanupTaskClient // UsageLog is the client for interacting with the UsageLog builders. @@ -201,6 +203,7 @@ func (tx *Tx) init() { tx.RedeemCode = NewRedeemCodeClient(tx.config) tx.SecuritySecret = NewSecuritySecretClient(tx.config) tx.Setting = NewSettingClient(tx.config) + tx.TLSFingerprintProfile = NewTLSFingerprintProfileClient(tx.config) tx.UsageCleanupTask = NewUsageCleanupTaskClient(tx.config) tx.UsageLog = NewUsageLogClient(tx.config) tx.User = NewUserClient(tx.config) diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index e90e56af..d1cb76db 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -656,17 +656,33 @@ type TLSFingerprintConfig struct { } // TLSProfileConfig 单个TLS指纹模板的配置 +// 所有列表字段为空时使用内置默认值(Claude CLI 2.x / Node.js 20.x) +// 建议通过 TLS 指纹采集工具 (tests/tls-fingerprint-web) 获取完整配置 type TLSProfileConfig struct { // Name: 模板显示名称 Name string `mapstructure:"name"` // EnableGREASE: 是否启用GREASE扩展(Chrome使用,Node.js不使用) EnableGREASE bool `mapstructure:"enable_grease"` - // CipherSuites: TLS加密套件列表(空则使用内置默认值) + // CipherSuites: TLS加密套件列表 CipherSuites []uint16 `mapstructure:"cipher_suites"` - // Curves: 椭圆曲线列表(空则使用内置默认值) + // Curves: 椭圆曲线列表 Curves []uint16 `mapstructure:"curves"` - // PointFormats: 点格式列表(空则使用内置默认值) - PointFormats []uint8 `mapstructure:"point_formats"` + // PointFormats: 点格式列表 + PointFormats []uint16 `mapstructure:"point_formats"` + // SignatureAlgorithms: 签名算法列表 + SignatureAlgorithms []uint16 `mapstructure:"signature_algorithms"` + // ALPNProtocols: ALPN协议列表(如 ["h2", "http/1.1"]) + ALPNProtocols []string `mapstructure:"alpn_protocols"` + // SupportedVersions: 支持的TLS版本列表(如 [0x0304, 0x0303] 即 TLS1.3, TLS1.2) + SupportedVersions []uint16 `mapstructure:"supported_versions"` + // KeyShareGroups: Key Share中发送的曲线组(如 [29] 即 X25519) + KeyShareGroups []uint16 `mapstructure:"key_share_groups"` + // PSKModes: PSK密钥交换模式(如 [1] 即 psk_dhe_ke) + PSKModes []uint16 `mapstructure:"psk_modes"` + // Extensions: TLS扩展类型ID列表,按发送顺序排列 + // 空则使用内置默认顺序 [0,11,10,35,16,22,23,13,43,45,51] + // GREASE值(如0x0a0a)会自动插入GREASE扩展 + Extensions []uint16 `mapstructure:"extensions"` } // GatewaySchedulingConfig accounts scheduling configuration. diff --git a/backend/internal/handler/admin/tls_fingerprint_profile_handler.go b/backend/internal/handler/admin/tls_fingerprint_profile_handler.go new file mode 100644 index 00000000..38f97555 --- /dev/null +++ b/backend/internal/handler/admin/tls_fingerprint_profile_handler.go @@ -0,0 +1,234 @@ +package admin + +import ( + "strconv" + + "github.com/Wei-Shaw/sub2api/internal/model" + "github.com/Wei-Shaw/sub2api/internal/pkg/response" + "github.com/Wei-Shaw/sub2api/internal/service" + "github.com/gin-gonic/gin" +) + +// TLSFingerprintProfileHandler 处理 TLS 指纹模板的 HTTP 请求 +type TLSFingerprintProfileHandler struct { + service *service.TLSFingerprintProfileService +} + +// NewTLSFingerprintProfileHandler 创建 TLS 指纹模板处理器 +func NewTLSFingerprintProfileHandler(service *service.TLSFingerprintProfileService) *TLSFingerprintProfileHandler { + return &TLSFingerprintProfileHandler{service: service} +} + +// CreateTLSFingerprintProfileRequest 创建模板请求 +type CreateTLSFingerprintProfileRequest struct { + Name string `json:"name" binding:"required"` + Description *string `json:"description"` + EnableGREASE *bool `json:"enable_grease"` + CipherSuites []uint16 `json:"cipher_suites"` + Curves []uint16 `json:"curves"` + PointFormats []uint16 `json:"point_formats"` + SignatureAlgorithms []uint16 `json:"signature_algorithms"` + ALPNProtocols []string `json:"alpn_protocols"` + SupportedVersions []uint16 `json:"supported_versions"` + KeyShareGroups []uint16 `json:"key_share_groups"` + PSKModes []uint16 `json:"psk_modes"` + Extensions []uint16 `json:"extensions"` +} + +// UpdateTLSFingerprintProfileRequest 更新模板请求(部分更新) +type UpdateTLSFingerprintProfileRequest struct { + Name *string `json:"name"` + Description *string `json:"description"` + EnableGREASE *bool `json:"enable_grease"` + CipherSuites []uint16 `json:"cipher_suites"` + Curves []uint16 `json:"curves"` + PointFormats []uint16 `json:"point_formats"` + SignatureAlgorithms []uint16 `json:"signature_algorithms"` + ALPNProtocols []string `json:"alpn_protocols"` + SupportedVersions []uint16 `json:"supported_versions"` + KeyShareGroups []uint16 `json:"key_share_groups"` + PSKModes []uint16 `json:"psk_modes"` + Extensions []uint16 `json:"extensions"` +} + +// List 获取所有模板 +// GET /api/v1/admin/tls-fingerprint-profiles +func (h *TLSFingerprintProfileHandler) List(c *gin.Context) { + profiles, err := h.service.List(c.Request.Context()) + if err != nil { + response.ErrorFrom(c, err) + return + } + response.Success(c, profiles) +} + +// GetByID 根据 ID 获取模板 +// GET /api/v1/admin/tls-fingerprint-profiles/:id +func (h *TLSFingerprintProfileHandler) GetByID(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid profile ID") + return + } + + profile, err := h.service.GetByID(c.Request.Context(), id) + if err != nil { + response.ErrorFrom(c, err) + return + } + if profile == nil { + response.NotFound(c, "Profile not found") + return + } + + response.Success(c, profile) +} + +// Create 创建模板 +// POST /api/v1/admin/tls-fingerprint-profiles +func (h *TLSFingerprintProfileHandler) Create(c *gin.Context) { + var req CreateTLSFingerprintProfileRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + profile := &model.TLSFingerprintProfile{ + Name: req.Name, + Description: req.Description, + CipherSuites: req.CipherSuites, + Curves: req.Curves, + PointFormats: req.PointFormats, + SignatureAlgorithms: req.SignatureAlgorithms, + ALPNProtocols: req.ALPNProtocols, + SupportedVersions: req.SupportedVersions, + KeyShareGroups: req.KeyShareGroups, + PSKModes: req.PSKModes, + Extensions: req.Extensions, + } + + if req.EnableGREASE != nil { + profile.EnableGREASE = *req.EnableGREASE + } + + created, err := h.service.Create(c.Request.Context(), profile) + if err != nil { + if _, ok := err.(*model.ValidationError); ok { + response.BadRequest(c, err.Error()) + return + } + response.ErrorFrom(c, err) + return + } + + response.Success(c, created) +} + +// Update 更新模板(支持部分更新) +// PUT /api/v1/admin/tls-fingerprint-profiles/:id +func (h *TLSFingerprintProfileHandler) Update(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid profile ID") + return + } + + var req UpdateTLSFingerprintProfileRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + existing, err := h.service.GetByID(c.Request.Context(), id) + if err != nil { + response.ErrorFrom(c, err) + return + } + if existing == nil { + response.NotFound(c, "Profile not found") + return + } + + // 部分更新 + profile := &model.TLSFingerprintProfile{ + ID: id, + Name: existing.Name, + Description: existing.Description, + EnableGREASE: existing.EnableGREASE, + CipherSuites: existing.CipherSuites, + Curves: existing.Curves, + PointFormats: existing.PointFormats, + SignatureAlgorithms: existing.SignatureAlgorithms, + ALPNProtocols: existing.ALPNProtocols, + SupportedVersions: existing.SupportedVersions, + KeyShareGroups: existing.KeyShareGroups, + PSKModes: existing.PSKModes, + Extensions: existing.Extensions, + } + + if req.Name != nil { + profile.Name = *req.Name + } + if req.Description != nil { + profile.Description = req.Description + } + if req.EnableGREASE != nil { + profile.EnableGREASE = *req.EnableGREASE + } + if req.CipherSuites != nil { + profile.CipherSuites = req.CipherSuites + } + if req.Curves != nil { + profile.Curves = req.Curves + } + if req.PointFormats != nil { + profile.PointFormats = req.PointFormats + } + if req.SignatureAlgorithms != nil { + profile.SignatureAlgorithms = req.SignatureAlgorithms + } + if req.ALPNProtocols != nil { + profile.ALPNProtocols = req.ALPNProtocols + } + if req.SupportedVersions != nil { + profile.SupportedVersions = req.SupportedVersions + } + if req.KeyShareGroups != nil { + profile.KeyShareGroups = req.KeyShareGroups + } + if req.PSKModes != nil { + profile.PSKModes = req.PSKModes + } + if req.Extensions != nil { + profile.Extensions = req.Extensions + } + + updated, err := h.service.Update(c.Request.Context(), profile) + if err != nil { + if _, ok := err.(*model.ValidationError); ok { + response.BadRequest(c, err.Error()) + return + } + response.ErrorFrom(c, err) + return + } + + response.Success(c, updated) +} + +// Delete 删除模板 +// DELETE /api/v1/admin/tls-fingerprint-profiles/:id +func (h *TLSFingerprintProfileHandler) Delete(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid profile ID") + return + } + + if err := h.service.Delete(c.Request.Context(), id); err != nil { + response.ErrorFrom(c, err) + return + } + + response.Success(c, gin.H{"message": "Profile deleted successfully"}) +} diff --git a/backend/internal/handler/dto/mappers.go b/backend/internal/handler/dto/mappers.go index 8150aa8e..e39b36d3 100644 --- a/backend/internal/handler/dto/mappers.go +++ b/backend/internal/handler/dto/mappers.go @@ -252,6 +252,10 @@ func AccountFromServiceShallow(a *service.Account) *Account { enabled := true out.EnableTLSFingerprint = &enabled } + // TLS指纹模板ID + if profileID := a.GetTLSFingerprintProfileID(); profileID > 0 { + out.TLSFingerprintProfileID = &profileID + } // 会话ID伪装开关 if a.IsSessionIDMaskingEnabled() { enabled := true diff --git a/backend/internal/handler/dto/types.go b/backend/internal/handler/dto/types.go index d4a24e10..aa419d6b 100644 --- a/backend/internal/handler/dto/types.go +++ b/backend/internal/handler/dto/types.go @@ -185,7 +185,8 @@ type Account struct { // TLS指纹伪装(仅 Anthropic OAuth/SetupToken 账号有效) // 从 extra 字段提取,方便前端显示和编辑 - EnableTLSFingerprint *bool `json:"enable_tls_fingerprint,omitempty"` + EnableTLSFingerprint *bool `json:"enable_tls_fingerprint,omitempty"` + TLSFingerprintProfileID *int64 `json:"tls_fingerprint_profile_id,omitempty"` // 会话ID伪装(仅 Anthropic OAuth/SetupToken 账号有效) // 启用后将在15分钟内固定 metadata.user_id 中的 session ID diff --git a/backend/internal/handler/gateway_handler_warmup_intercept_unit_test.go b/backend/internal/handler/gateway_handler_warmup_intercept_unit_test.go index b9dbe0ce..69c8d1d5 100644 --- a/backend/internal/handler/gateway_handler_warmup_intercept_unit_test.go +++ b/backend/internal/handler/gateway_handler_warmup_intercept_unit_test.go @@ -75,8 +75,10 @@ func (f *fakeGroupRepo) ListActive(context.Context) ([]service.Group, error) { r func (f *fakeGroupRepo) ListActiveByPlatform(context.Context, string) ([]service.Group, error) { return nil, nil } -func (f *fakeGroupRepo) ExistsByName(context.Context, string) (bool, error) { return false, nil } -func (f *fakeGroupRepo) GetAccountCount(context.Context, int64) (int64, int64, error) { return 0, 0, nil } +func (f *fakeGroupRepo) ExistsByName(context.Context, string) (bool, error) { return false, nil } +func (f *fakeGroupRepo) GetAccountCount(context.Context, int64) (int64, int64, error) { + return 0, 0, nil +} func (f *fakeGroupRepo) DeleteAccountGroupsByGroupID(context.Context, int64) (int64, error) { return 0, nil } @@ -158,6 +160,7 @@ func newTestGatewayHandler(t *testing.T, group *service.Group, accounts []*servi nil, // rpmCache nil, // digestStore nil, // settingService + nil, // tlsFPProfileService ) // RunModeSimple:跳过计费检查,避免引入 repo/cache 依赖。 diff --git a/backend/internal/handler/handler.go b/backend/internal/handler/handler.go index 89d556cc..b2467eac 100644 --- a/backend/internal/handler/handler.go +++ b/backend/internal/handler/handler.go @@ -6,29 +6,30 @@ import ( // AdminHandlers contains all admin-related HTTP handlers type AdminHandlers struct { - Dashboard *admin.DashboardHandler - User *admin.UserHandler - Group *admin.GroupHandler - Account *admin.AccountHandler - Announcement *admin.AnnouncementHandler - DataManagement *admin.DataManagementHandler - Backup *admin.BackupHandler - OAuth *admin.OAuthHandler - OpenAIOAuth *admin.OpenAIOAuthHandler - GeminiOAuth *admin.GeminiOAuthHandler - AntigravityOAuth *admin.AntigravityOAuthHandler - Proxy *admin.ProxyHandler - Redeem *admin.RedeemHandler - Promo *admin.PromoHandler - Setting *admin.SettingHandler - Ops *admin.OpsHandler - System *admin.SystemHandler - Subscription *admin.SubscriptionHandler - Usage *admin.UsageHandler - UserAttribute *admin.UserAttributeHandler - ErrorPassthrough *admin.ErrorPassthroughHandler - APIKey *admin.AdminAPIKeyHandler - ScheduledTest *admin.ScheduledTestHandler + Dashboard *admin.DashboardHandler + User *admin.UserHandler + Group *admin.GroupHandler + Account *admin.AccountHandler + Announcement *admin.AnnouncementHandler + DataManagement *admin.DataManagementHandler + Backup *admin.BackupHandler + OAuth *admin.OAuthHandler + OpenAIOAuth *admin.OpenAIOAuthHandler + GeminiOAuth *admin.GeminiOAuthHandler + AntigravityOAuth *admin.AntigravityOAuthHandler + Proxy *admin.ProxyHandler + Redeem *admin.RedeemHandler + Promo *admin.PromoHandler + Setting *admin.SettingHandler + Ops *admin.OpsHandler + System *admin.SystemHandler + Subscription *admin.SubscriptionHandler + Usage *admin.UsageHandler + UserAttribute *admin.UserAttributeHandler + ErrorPassthrough *admin.ErrorPassthroughHandler + TLSFingerprintProfile *admin.TLSFingerprintProfileHandler + APIKey *admin.AdminAPIKeyHandler + ScheduledTest *admin.ScheduledTestHandler } // Handlers contains all HTTP handlers diff --git a/backend/internal/handler/sora_client_handler_test.go b/backend/internal/handler/sora_client_handler_test.go index 625f159d..fe035b6f 100644 --- a/backend/internal/handler/sora_client_handler_test.go +++ b/backend/internal/handler/sora_client_handler_test.go @@ -2224,7 +2224,7 @@ func (s *stubSoraClientForHandler) GetVideoTask(_ context.Context, _ *service.Ac func newMinimalGatewayService(accountRepo service.AccountRepository) *service.GatewayService { return service.NewGatewayService( accountRepo, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, ) } diff --git a/backend/internal/handler/sora_gateway_handler_test.go b/backend/internal/handler/sora_gateway_handler_test.go index 084e4ae1..c790a36c 100644 --- a/backend/internal/handler/sora_gateway_handler_test.go +++ b/backend/internal/handler/sora_gateway_handler_test.go @@ -464,6 +464,7 @@ func TestSoraGatewayHandler_ChatCompletions(t *testing.T) { nil, // rpmCache nil, // digestStore nil, // settingService + nil, // tlsFPProfileService ) soraClient := &stubSoraClient{imageURLs: []string{"https://example.com/a.png"}} diff --git a/backend/internal/handler/wire.go b/backend/internal/handler/wire.go index f3aadcf3..02ddd030 100644 --- a/backend/internal/handler/wire.go +++ b/backend/internal/handler/wire.go @@ -30,33 +30,35 @@ func ProvideAdminHandlers( usageHandler *admin.UsageHandler, userAttributeHandler *admin.UserAttributeHandler, errorPassthroughHandler *admin.ErrorPassthroughHandler, + tlsFingerprintProfileHandler *admin.TLSFingerprintProfileHandler, apiKeyHandler *admin.AdminAPIKeyHandler, scheduledTestHandler *admin.ScheduledTestHandler, ) *AdminHandlers { return &AdminHandlers{ - Dashboard: dashboardHandler, - User: userHandler, - Group: groupHandler, - Account: accountHandler, - Announcement: announcementHandler, - DataManagement: dataManagementHandler, - Backup: backupHandler, - OAuth: oauthHandler, - OpenAIOAuth: openaiOAuthHandler, - GeminiOAuth: geminiOAuthHandler, - AntigravityOAuth: antigravityOAuthHandler, - Proxy: proxyHandler, - Redeem: redeemHandler, - Promo: promoHandler, - Setting: settingHandler, - Ops: opsHandler, - System: systemHandler, - Subscription: subscriptionHandler, - Usage: usageHandler, - UserAttribute: userAttributeHandler, - ErrorPassthrough: errorPassthroughHandler, - APIKey: apiKeyHandler, - ScheduledTest: scheduledTestHandler, + Dashboard: dashboardHandler, + User: userHandler, + Group: groupHandler, + Account: accountHandler, + Announcement: announcementHandler, + DataManagement: dataManagementHandler, + Backup: backupHandler, + OAuth: oauthHandler, + OpenAIOAuth: openaiOAuthHandler, + GeminiOAuth: geminiOAuthHandler, + AntigravityOAuth: antigravityOAuthHandler, + Proxy: proxyHandler, + Redeem: redeemHandler, + Promo: promoHandler, + Setting: settingHandler, + Ops: opsHandler, + System: systemHandler, + Subscription: subscriptionHandler, + Usage: usageHandler, + UserAttribute: userAttributeHandler, + ErrorPassthrough: errorPassthroughHandler, + TLSFingerprintProfile: tlsFingerprintProfileHandler, + APIKey: apiKeyHandler, + ScheduledTest: scheduledTestHandler, } } @@ -145,6 +147,7 @@ var ProviderSet = wire.NewSet( admin.NewUsageHandler, admin.NewUserAttributeHandler, admin.NewErrorPassthroughHandler, + admin.NewTLSFingerprintProfileHandler, admin.NewAdminAPIKeyHandler, admin.NewScheduledTestHandler, diff --git a/backend/internal/model/tls_fingerprint_profile.go b/backend/internal/model/tls_fingerprint_profile.go new file mode 100644 index 00000000..ef57af7a --- /dev/null +++ b/backend/internal/model/tls_fingerprint_profile.go @@ -0,0 +1,54 @@ +// Package model 定义服务层使用的数据模型。 +package model + +import ( + "time" + + "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" +) + +// TLSFingerprintProfile TLS 指纹配置模板 +// 包含完整的 ClientHello 参数,用于模拟特定客户端的 TLS 握手特征 +type TLSFingerprintProfile struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + EnableGREASE bool `json:"enable_grease"` + CipherSuites []uint16 `json:"cipher_suites"` + Curves []uint16 `json:"curves"` + PointFormats []uint16 `json:"point_formats"` + SignatureAlgorithms []uint16 `json:"signature_algorithms"` + ALPNProtocols []string `json:"alpn_protocols"` + SupportedVersions []uint16 `json:"supported_versions"` + KeyShareGroups []uint16 `json:"key_share_groups"` + PSKModes []uint16 `json:"psk_modes"` + Extensions []uint16 `json:"extensions"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// Validate 验证模板配置的有效性 +func (p *TLSFingerprintProfile) Validate() error { + if p.Name == "" { + return &ValidationError{Field: "name", Message: "name is required"} + } + return nil +} + +// ToTLSProfile 将领域模型转换为运行时使用的 tlsfingerprint.Profile +// 空切片字段会在 dialer 中 fallback 到内置默认值 +func (p *TLSFingerprintProfile) ToTLSProfile() *tlsfingerprint.Profile { + return &tlsfingerprint.Profile{ + Name: p.Name, + EnableGREASE: p.EnableGREASE, + CipherSuites: p.CipherSuites, + Curves: p.Curves, + PointFormats: p.PointFormats, + SignatureAlgorithms: p.SignatureAlgorithms, + ALPNProtocols: p.ALPNProtocols, + SupportedVersions: p.SupportedVersions, + KeyShareGroups: p.KeyShareGroups, + PSKModes: p.PSKModes, + Extensions: p.Extensions, + } +} diff --git a/backend/internal/pkg/tlsfingerprint/dialer.go b/backend/internal/pkg/tlsfingerprint/dialer.go index 4f25a34a..c8d8369f 100644 --- a/backend/internal/pkg/tlsfingerprint/dialer.go +++ b/backend/internal/pkg/tlsfingerprint/dialer.go @@ -17,12 +17,19 @@ import ( ) // Profile contains TLS fingerprint configuration. +// All slice fields use built-in defaults when empty. type Profile struct { - Name string // Profile name for identification - CipherSuites []uint16 - Curves []uint16 - PointFormats []uint8 - EnableGREASE bool + Name string // Profile name for identification + CipherSuites []uint16 + Curves []uint16 + PointFormats []uint16 + EnableGREASE bool + SignatureAlgorithms []uint16 // Empty uses defaultSignatureAlgorithms + ALPNProtocols []string // Empty uses ["http/1.1"] + SupportedVersions []uint16 // Empty uses [TLS1.3, TLS1.2] + KeyShareGroups []uint16 // Empty uses [X25519] + PSKModes []uint16 // Empty uses [psk_dhe_ke] + Extensions []uint16 // Extension type IDs in order; empty uses default Node.js 24.x order } // Dialer creates TLS connections with custom fingerprints. @@ -45,154 +52,67 @@ type SOCKS5ProxyDialer struct { proxyURL *url.URL } -// Default TLS fingerprint values captured from Claude CLI 2.x (Node.js 20.x + OpenSSL 3.x) -// Captured using: tshark -i lo -f "tcp port 8443" -Y "tls.handshake.type == 1" -V -// JA3 Hash: 1a28e69016765d92e3b381168d68922c -// -// Note: JA3/JA4 may have slight variations due to: -// - Session ticket presence/absence -// - Extension negotiation state +// Default TLS fingerprint values captured from Claude Code (Node.js 24.x) +// Captured via tls-fingerprint-web capture server +// JA3 Hash: 44f88fca027f27bab4bb08d4af15f23e +// JA4: t13d1714h1_5b57614c22b0_7baf387fc6ff var ( - // defaultCipherSuites contains all 59 cipher suites from Claude CLI + // defaultCipherSuites contains the 17 cipher suites from Node.js 24.x // Order is critical for JA3 fingerprint matching defaultCipherSuites = []uint16{ - // TLS 1.3 cipher suites (MUST be first) + // TLS 1.3 cipher suites + 0x1301, // TLS_AES_128_GCM_SHA256 0x1302, // TLS_AES_256_GCM_SHA384 0x1303, // TLS_CHACHA20_POLY1305_SHA256 - 0x1301, // TLS_AES_128_GCM_SHA256 // ECDHE + AES-GCM - 0xc02f, // TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 0xc02b, // TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 - 0xc030, // TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + 0xc02f, // TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 0xc02c, // TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + 0xc030, // TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 - // DHE + AES-GCM - 0x009e, // TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 - - // ECDHE/DHE + AES-CBC-SHA256/384 - 0xc027, // TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 - 0x0067, // TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 - 0xc028, // TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 - 0x006b, // TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 - - // DHE-DSS/RSA + AES-GCM - 0x00a3, // TLS_DHE_DSS_WITH_AES_256_GCM_SHA384 - 0x009f, // TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 - - // ChaCha20-Poly1305 + // ECDHE + ChaCha20-Poly1305 0xcca9, // TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 0xcca8, // TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 - 0xccaa, // TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 - // AES-CCM (256-bit) - 0xc0af, // TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8 - 0xc0ad, // TLS_ECDHE_ECDSA_WITH_AES_256_CCM - 0xc0a3, // TLS_DHE_RSA_WITH_AES_256_CCM_8 - 0xc09f, // TLS_DHE_RSA_WITH_AES_256_CCM - - // ARIA (256-bit) - 0xc05d, // TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384 - 0xc061, // TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384 - 0xc057, // TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384 - 0xc053, // TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384 - - // DHE-DSS + AES-GCM (128-bit) - 0x00a2, // TLS_DHE_DSS_WITH_AES_128_GCM_SHA256 - - // AES-CCM (128-bit) - 0xc0ae, // TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8 - 0xc0ac, // TLS_ECDHE_ECDSA_WITH_AES_128_CCM - 0xc0a2, // TLS_DHE_RSA_WITH_AES_128_CCM_8 - 0xc09e, // TLS_DHE_RSA_WITH_AES_128_CCM - - // ARIA (128-bit) - 0xc05c, // TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256 - 0xc060, // TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256 - 0xc056, // TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256 - 0xc052, // TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256 - - // ECDHE/DHE + AES-CBC-SHA384/256 (more) - 0xc024, // TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 - 0x006a, // TLS_DHE_DSS_WITH_AES_256_CBC_SHA256 - 0xc023, // TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 - 0x0040, // TLS_DHE_DSS_WITH_AES_128_CBC_SHA256 - - // ECDHE/DHE + AES-CBC-SHA (legacy) - 0xc00a, // TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA - 0xc014, // TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA - 0x0039, // TLS_DHE_RSA_WITH_AES_256_CBC_SHA - 0x0038, // TLS_DHE_DSS_WITH_AES_256_CBC_SHA + // ECDHE + AES-CBC-SHA (legacy fallback) 0xc009, // TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA 0xc013, // TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA - 0x0033, // TLS_DHE_RSA_WITH_AES_128_CBC_SHA - 0x0032, // TLS_DHE_DSS_WITH_AES_128_CBC_SHA + 0xc00a, // TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA + 0xc014, // TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA - // RSA + AES-GCM/CCM/ARIA (non-PFS, 256-bit) - 0x009d, // TLS_RSA_WITH_AES_256_GCM_SHA384 - 0xc0a1, // TLS_RSA_WITH_AES_256_CCM_8 - 0xc09d, // TLS_RSA_WITH_AES_256_CCM - 0xc051, // TLS_RSA_WITH_ARIA_256_GCM_SHA384 - - // RSA + AES-GCM/CCM/ARIA (non-PFS, 128-bit) + // RSA + AES-GCM (non-PFS) 0x009c, // TLS_RSA_WITH_AES_128_GCM_SHA256 - 0xc0a0, // TLS_RSA_WITH_AES_128_CCM_8 - 0xc09c, // TLS_RSA_WITH_AES_128_CCM - 0xc050, // TLS_RSA_WITH_ARIA_128_GCM_SHA256 + 0x009d, // TLS_RSA_WITH_AES_256_GCM_SHA384 - // RSA + AES-CBC (non-PFS, legacy) - 0x003d, // TLS_RSA_WITH_AES_256_CBC_SHA256 - 0x003c, // TLS_RSA_WITH_AES_128_CBC_SHA256 - 0x0035, // TLS_RSA_WITH_AES_256_CBC_SHA + // RSA + AES-CBC-SHA (non-PFS, legacy) 0x002f, // TLS_RSA_WITH_AES_128_CBC_SHA - - // Renegotiation indication - 0x00ff, // TLS_EMPTY_RENEGOTIATION_INFO_SCSV + 0x0035, // TLS_RSA_WITH_AES_256_CBC_SHA } - // defaultCurves contains the 10 supported groups from Claude CLI (including FFDHE) + // defaultCurves contains the 3 supported groups from Node.js 24.x defaultCurves = []utls.CurveID{ - utls.X25519, // 0x001d - utls.CurveP256, // 0x0017 (secp256r1) - utls.CurveID(0x001e), // x448 - utls.CurveP521, // 0x0019 (secp521r1) - utls.CurveP384, // 0x0018 (secp384r1) - utls.CurveID(0x0100), // ffdhe2048 - utls.CurveID(0x0101), // ffdhe3072 - utls.CurveID(0x0102), // ffdhe4096 - utls.CurveID(0x0103), // ffdhe6144 - utls.CurveID(0x0104), // ffdhe8192 + utls.X25519, // 0x001d + utls.CurveP256, // 0x0017 (secp256r1) + utls.CurveP384, // 0x0018 (secp384r1) } - // defaultPointFormats contains all 3 point formats from Claude CLI - defaultPointFormats = []uint8{ + // defaultPointFormats contains point formats from Node.js 24.x + defaultPointFormats = []uint16{ 0, // uncompressed - 1, // ansiX962_compressed_prime - 2, // ansiX962_compressed_char2 } - // defaultSignatureAlgorithms contains the 20 signature algorithms from Claude CLI + // defaultSignatureAlgorithms contains the 9 signature algorithms from Node.js 24.x defaultSignatureAlgorithms = []utls.SignatureScheme{ 0x0403, // ecdsa_secp256r1_sha256 - 0x0503, // ecdsa_secp384r1_sha384 - 0x0603, // ecdsa_secp521r1_sha512 - 0x0807, // ed25519 - 0x0808, // ed448 - 0x0809, // rsa_pss_pss_sha256 - 0x080a, // rsa_pss_pss_sha384 - 0x080b, // rsa_pss_pss_sha512 0x0804, // rsa_pss_rsae_sha256 - 0x0805, // rsa_pss_rsae_sha384 - 0x0806, // rsa_pss_rsae_sha512 0x0401, // rsa_pkcs1_sha256 + 0x0503, // ecdsa_secp384r1_sha384 + 0x0805, // rsa_pss_rsae_sha384 0x0501, // rsa_pkcs1_sha384 + 0x0806, // rsa_pss_rsae_sha512 0x0601, // rsa_pkcs1_sha512 - 0x0303, // ecdsa_sha224 - 0x0301, // rsa_pkcs1_sha224 - 0x0302, // dsa_sha224 - 0x0402, // dsa_sha256 - 0x0502, // dsa_sha384 - 0x0602, // dsa_sha512 + 0x0201, // rsa_pkcs1_sha1 } ) @@ -256,49 +176,7 @@ func (d *SOCKS5ProxyDialer) DialTLSContext(ctx context.Context, network, addr st slog.Debug("tls_fingerprint_socks5_tunnel_established") // Step 3: Perform TLS handshake on the tunnel with utls fingerprint - host, _, err := net.SplitHostPort(addr) - if err != nil { - host = addr - } - slog.Debug("tls_fingerprint_socks5_starting_handshake", "host", host) - - // Build ClientHello specification from profile (Node.js/Claude CLI fingerprint) - spec := buildClientHelloSpecFromProfile(d.profile) - slog.Debug("tls_fingerprint_socks5_clienthello_spec", - "cipher_suites", len(spec.CipherSuites), - "extensions", len(spec.Extensions), - "compression_methods", spec.CompressionMethods, - "tls_vers_max", spec.TLSVersMax, - "tls_vers_min", spec.TLSVersMin) - - if d.profile != nil { - slog.Debug("tls_fingerprint_socks5_using_profile", "name", d.profile.Name, "grease", d.profile.EnableGREASE) - } - - // Create uTLS connection on the tunnel - tlsConn := utls.UClient(conn, &utls.Config{ - ServerName: host, - }, utls.HelloCustom) - - if err := tlsConn.ApplyPreset(spec); err != nil { - slog.Debug("tls_fingerprint_socks5_apply_preset_failed", "error", err) - _ = conn.Close() - return nil, fmt.Errorf("apply TLS preset: %w", err) - } - - if err := tlsConn.HandshakeContext(ctx); err != nil { - slog.Debug("tls_fingerprint_socks5_handshake_failed", "error", err) - _ = conn.Close() - return nil, fmt.Errorf("TLS handshake failed: %w", err) - } - - state := tlsConn.ConnectionState() - slog.Debug("tls_fingerprint_socks5_handshake_success", - "version", state.Version, - "cipher_suite", state.CipherSuite, - "alpn", state.NegotiatedProtocol) - - return tlsConn, nil + return performTLSHandshake(ctx, conn, d.profile, addr) } // DialTLSContext establishes a TLS connection through HTTP proxy with the configured fingerprint. @@ -358,7 +236,8 @@ func (d *HTTPProxyDialer) DialTLSContext(ctx context.Context, network, addr stri slog.Debug("tls_fingerprint_http_proxy_read_response_failed", "error", err) return nil, fmt.Errorf("read CONNECT response: %w", err) } - defer func() { _ = resp.Body.Close() }() + // CONNECT response has no body; do not defer resp.Body.Close() as it wraps the + // same conn that will be used for the TLS handshake. if resp.StatusCode != http.StatusOK { _ = conn.Close() @@ -368,47 +247,7 @@ func (d *HTTPProxyDialer) DialTLSContext(ctx context.Context, network, addr stri slog.Debug("tls_fingerprint_http_proxy_tunnel_established") // Step 4: Perform TLS handshake on the tunnel with utls fingerprint - host, _, err := net.SplitHostPort(addr) - if err != nil { - host = addr - } - slog.Debug("tls_fingerprint_http_proxy_starting_handshake", "host", host) - - // Build ClientHello specification (reuse the shared method) - spec := buildClientHelloSpecFromProfile(d.profile) - slog.Debug("tls_fingerprint_http_proxy_clienthello_spec", - "cipher_suites", len(spec.CipherSuites), - "extensions", len(spec.Extensions)) - - if d.profile != nil { - slog.Debug("tls_fingerprint_http_proxy_using_profile", "name", d.profile.Name, "grease", d.profile.EnableGREASE) - } - - // Create uTLS connection on the tunnel - // Note: TLS 1.3 cipher suites are handled automatically by utls when TLS 1.3 is in SupportedVersions - tlsConn := utls.UClient(conn, &utls.Config{ - ServerName: host, - }, utls.HelloCustom) - - if err := tlsConn.ApplyPreset(spec); err != nil { - slog.Debug("tls_fingerprint_http_proxy_apply_preset_failed", "error", err) - _ = conn.Close() - return nil, fmt.Errorf("apply TLS preset: %w", err) - } - - if err := tlsConn.HandshakeContext(ctx); err != nil { - slog.Debug("tls_fingerprint_http_proxy_handshake_failed", "error", err) - _ = conn.Close() - return nil, fmt.Errorf("TLS handshake failed: %w", err) - } - - state := tlsConn.ConnectionState() - slog.Debug("tls_fingerprint_http_proxy_handshake_success", - "version", state.Version, - "cipher_suite", state.CipherSuite, - "alpn", state.NegotiatedProtocol) - - return tlsConn, nil + return performTLSHandshake(ctx, conn, d.profile, addr) } // DialTLSContext establishes a TLS connection with the configured fingerprint. @@ -423,53 +262,35 @@ func (d *Dialer) DialTLSContext(ctx context.Context, network, addr string) (net. } slog.Debug("tls_fingerprint_tcp_connected", "addr", addr) - // Extract hostname for SNI + // Perform TLS handshake with utls fingerprint + return performTLSHandshake(ctx, conn, d.profile, addr) +} + +// performTLSHandshake performs the uTLS handshake on an established connection. +// It builds a ClientHello spec from the profile, applies it, and completes the handshake. +// On failure, conn is closed and an error is returned. +func performTLSHandshake(ctx context.Context, conn net.Conn, profile *Profile, addr string) (net.Conn, error) { host, _, err := net.SplitHostPort(addr) if err != nil { host = addr } - slog.Debug("tls_fingerprint_sni_hostname", "host", host) - // Build ClientHello specification - spec := d.buildClientHelloSpec() - slog.Debug("tls_fingerprint_clienthello_spec", - "cipher_suites", len(spec.CipherSuites), - "extensions", len(spec.Extensions)) + spec := buildClientHelloSpecFromProfile(profile) + tlsConn := utls.UClient(conn, &utls.Config{ServerName: host}, utls.HelloCustom) - // Log profile info - if d.profile != nil { - slog.Debug("tls_fingerprint_using_profile", "name", d.profile.Name, "grease", d.profile.EnableGREASE) - } else { - slog.Debug("tls_fingerprint_using_default_profile") - } - - // Create uTLS connection - // Note: TLS 1.3 cipher suites are handled automatically by utls when TLS 1.3 is in SupportedVersions - tlsConn := utls.UClient(conn, &utls.Config{ - ServerName: host, - }, utls.HelloCustom) - - // Apply fingerprint if err := tlsConn.ApplyPreset(spec); err != nil { - slog.Debug("tls_fingerprint_apply_preset_failed", "error", err) _ = conn.Close() - return nil, err + return nil, fmt.Errorf("apply TLS preset: %w", err) } - slog.Debug("tls_fingerprint_preset_applied") - // Perform TLS handshake if err := tlsConn.HandshakeContext(ctx); err != nil { - slog.Debug("tls_fingerprint_handshake_failed", - "error", err, - "local_addr", conn.LocalAddr(), - "remote_addr", conn.RemoteAddr()) _ = conn.Close() return nil, fmt.Errorf("TLS handshake failed: %w", err) } - // Log successful handshake details state := tlsConn.ConnectionState() slog.Debug("tls_fingerprint_handshake_success", + "host", host, "version", state.Version, "cipher_suite", state.CipherSuite, "alpn", state.NegotiatedProtocol) @@ -477,11 +298,6 @@ func (d *Dialer) DialTLSContext(ctx context.Context, network, addr string) (net. return tlsConn, nil } -// buildClientHelloSpec constructs the ClientHello specification based on the profile. -func (d *Dialer) buildClientHelloSpec() *utls.ClientHelloSpec { - return buildClientHelloSpecFromProfile(d.profile) -} - // toUTLSCurves converts uint16 slice to utls.CurveID slice. func toUTLSCurves(curves []uint16) []utls.CurveID { result := make([]utls.CurveID, len(curves)) @@ -491,70 +307,143 @@ func toUTLSCurves(curves []uint16) []utls.CurveID { return result } +// defaultExtensionOrder is the Node.js 24.x extension order. +// Used when Profile.Extensions is empty. +var defaultExtensionOrder = []uint16{ + 0, // server_name + 65037, // encrypted_client_hello + 23, // extended_master_secret + 65281, // renegotiation_info + 10, // supported_groups + 11, // ec_point_formats + 35, // session_ticket + 16, // alpn + 5, // status_request + 13, // signature_algorithms + 18, // signed_certificate_timestamp + 51, // key_share + 45, // psk_key_exchange_modes + 43, // supported_versions +} + +// isGREASEValue checks if a uint16 value matches the TLS GREASE pattern (0x?a?a). +func isGREASEValue(v uint16) bool { + return v&0x0f0f == 0x0a0a && v>>8 == v&0xff +} + // buildClientHelloSpecFromProfile constructs ClientHelloSpec from a Profile. // This is a standalone function that can be used by both Dialer and HTTPProxyDialer. func buildClientHelloSpecFromProfile(profile *Profile) *utls.ClientHelloSpec { - // Get cipher suites - var cipherSuites []uint16 + // Resolve effective values (profile overrides or built-in defaults) + cipherSuites := defaultCipherSuites if profile != nil && len(profile.CipherSuites) > 0 { cipherSuites = profile.CipherSuites - } else { - cipherSuites = defaultCipherSuites } - // Get curves - var curves []utls.CurveID + curves := defaultCurves if profile != nil && len(profile.Curves) > 0 { curves = toUTLSCurves(profile.Curves) - } else { - curves = defaultCurves } - // Get point formats - var pointFormats []uint8 + pointFormats := defaultPointFormats if profile != nil && len(profile.PointFormats) > 0 { pointFormats = profile.PointFormats - } else { - pointFormats = defaultPointFormats } - // Check if GREASE is enabled + signatureAlgorithms := defaultSignatureAlgorithms + if profile != nil && len(profile.SignatureAlgorithms) > 0 { + signatureAlgorithms = make([]utls.SignatureScheme, len(profile.SignatureAlgorithms)) + for i, s := range profile.SignatureAlgorithms { + signatureAlgorithms[i] = utls.SignatureScheme(s) + } + } + + alpnProtocols := []string{"http/1.1"} + if profile != nil && len(profile.ALPNProtocols) > 0 { + alpnProtocols = profile.ALPNProtocols + } + + supportedVersions := []uint16{utls.VersionTLS13, utls.VersionTLS12} + if profile != nil && len(profile.SupportedVersions) > 0 { + supportedVersions = profile.SupportedVersions + } + + keyShareGroups := []utls.CurveID{utls.X25519} + if profile != nil && len(profile.KeyShareGroups) > 0 { + keyShareGroups = toUTLSCurves(profile.KeyShareGroups) + } + + pskModes := []uint16{uint16(utls.PskModeDHE)} + if profile != nil && len(profile.PSKModes) > 0 { + pskModes = profile.PSKModes + } + enableGREASE := profile != nil && profile.EnableGREASE - extensions := make([]utls.TLSExtension, 0, 16) - - if enableGREASE { - extensions = append(extensions, &utls.UtlsGREASEExtension{}) + // Build key shares + keyShares := make([]utls.KeyShare, len(keyShareGroups)) + for i, g := range keyShareGroups { + keyShares[i] = utls.KeyShare{Group: g} } - // SNI extension - MUST be explicitly added for HelloCustom mode - // utls will populate the server name from Config.ServerName - extensions = append(extensions, &utls.SNIExtension{}) + // Determine extension order + extOrder := defaultExtensionOrder + if profile != nil && len(profile.Extensions) > 0 { + extOrder = profile.Extensions + } - // Claude CLI extension order (captured from tshark): - // server_name(0), ec_point_formats(11), supported_groups(10), session_ticket(35), - // alpn(16), encrypt_then_mac(22), extended_master_secret(23), - // signature_algorithms(13), supported_versions(43), - // psk_key_exchange_modes(45), key_share(51) - extensions = append(extensions, - &utls.SupportedPointsExtension{SupportedPoints: pointFormats}, - &utls.SupportedCurvesExtension{Curves: curves}, - &utls.SessionTicketExtension{}, - &utls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}}, - &utls.GenericExtension{Id: 22}, - &utls.ExtendedMasterSecretExtension{}, - &utls.SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: defaultSignatureAlgorithms}, - &utls.SupportedVersionsExtension{Versions: []uint16{ - utls.VersionTLS13, - utls.VersionTLS12, - }}, - &utls.PSKKeyExchangeModesExtension{Modes: []uint8{utls.PskModeDHE}}, - &utls.KeyShareExtension{KeyShares: []utls.KeyShare{ - {Group: utls.X25519}, - }}, - ) + // Build extensions list from the ordered IDs. + // Parametric extensions (curves, sigalgs, etc.) are populated with resolved profile values. + // Unknown IDs use GenericExtension (sends type ID with empty data). + extensions := make([]utls.TLSExtension, 0, len(extOrder)+2) + for _, id := range extOrder { + if isGREASEValue(id) { + extensions = append(extensions, &utls.UtlsGREASEExtension{}) + continue + } + switch id { + case 0: // server_name + extensions = append(extensions, &utls.SNIExtension{}) + case 5: // status_request (OCSP) + extensions = append(extensions, &utls.StatusRequestExtension{}) + case 10: // supported_groups + extensions = append(extensions, &utls.SupportedCurvesExtension{Curves: curves}) + case 11: // ec_point_formats + extensions = append(extensions, &utls.SupportedPointsExtension{SupportedPoints: toUint8s(pointFormats)}) + case 13: // signature_algorithms + extensions = append(extensions, &utls.SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: signatureAlgorithms}) + case 16: // alpn + extensions = append(extensions, &utls.ALPNExtension{AlpnProtocols: alpnProtocols}) + case 18: // signed_certificate_timestamp + extensions = append(extensions, &utls.SCTExtension{}) + case 23: // extended_master_secret + extensions = append(extensions, &utls.ExtendedMasterSecretExtension{}) + case 35: // session_ticket + extensions = append(extensions, &utls.SessionTicketExtension{}) + case 43: // supported_versions + extensions = append(extensions, &utls.SupportedVersionsExtension{Versions: supportedVersions}) + case 45: // psk_key_exchange_modes + extensions = append(extensions, &utls.PSKKeyExchangeModesExtension{Modes: toUint8s(pskModes)}) + case 50: // signature_algorithms_cert + extensions = append(extensions, &utls.SignatureAlgorithmsCertExtension{SupportedSignatureAlgorithms: signatureAlgorithms}) + case 51: // key_share + extensions = append(extensions, &utls.KeyShareExtension{KeyShares: keyShares}) + case 0xfe0d: // encrypted_client_hello (ECH, 65037) + // Send GREASE ECH with random payload — mimics Node.js behavior when no real ECHConfig is available. + // An empty GenericExtension causes "error decoding message" from servers that validate ECH format. + extensions = append(extensions, &utls.GREASEEncryptedClientHelloExtension{}) + case 0xff01: // renegotiation_info + extensions = append(extensions, &utls.RenegotiationInfoExtension{}) + default: + // Unknown extension — send as GenericExtension (type ID + empty data). + // This covers encrypt_then_mac(22) and any future extensions. + extensions = append(extensions, &utls.GenericExtension{Id: id}) + } + } - if enableGREASE { + // For default extension order with EnableGREASE, wrap with GREASE bookends + if enableGREASE && (profile == nil || len(profile.Extensions) == 0) { + extensions = append([]utls.TLSExtension{&utls.UtlsGREASEExtension{}}, extensions...) extensions = append(extensions, &utls.UtlsGREASEExtension{}) } @@ -566,3 +455,12 @@ func buildClientHelloSpecFromProfile(profile *Profile) *utls.ClientHelloSpec { TLSVersMin: utls.VersionTLS10, } } + +// toUint8s converts []uint16 to []uint8 (for utls fields that require []uint8). +func toUint8s(vals []uint16) []uint8 { + out := make([]uint8, len(vals)) + for i, v := range vals { + out[i] = uint8(v) + } + return out +} diff --git a/backend/internal/pkg/tlsfingerprint/dialer_capture_test.go b/backend/internal/pkg/tlsfingerprint/dialer_capture_test.go new file mode 100644 index 00000000..de9d79a0 --- /dev/null +++ b/backend/internal/pkg/tlsfingerprint/dialer_capture_test.go @@ -0,0 +1,368 @@ +//go:build integration + +package tlsfingerprint + +import ( + "context" + "encoding/json" + "io" + "net/http" + "os" + "strings" + "testing" + "time" + + utls "github.com/refraction-networking/utls" +) + +// CapturedFingerprint mirrors the Fingerprint struct from tls-fingerprint-web. +// Used to deserialize the JSON response from the capture server. +type CapturedFingerprint struct { + JA3Raw string `json:"ja3_raw"` + JA3Hash string `json:"ja3_hash"` + JA4 string `json:"ja4"` + HTTP2 string `json:"http2"` + CipherSuites []int `json:"cipher_suites"` + Curves []int `json:"curves"` + PointFormats []int `json:"point_formats"` + Extensions []int `json:"extensions"` + SignatureAlgorithms []int `json:"signature_algorithms"` + ALPNProtocols []string `json:"alpn_protocols"` + SupportedVersions []int `json:"supported_versions"` + KeyShareGroups []int `json:"key_share_groups"` + PSKModes []int `json:"psk_modes"` + CompressCertAlgos []int `json:"compress_cert_algos"` + EnableGREASE bool `json:"enable_grease"` +} + +// TestDialerAgainstCaptureServer connects to the tls-fingerprint-web capture server +// and verifies that the dialer's TLS fingerprint matches the configured Profile. +// +// Default capture server: https://tls.sub2api.org:8090 +// Override with env: TLSFINGERPRINT_CAPTURE_URL=https://localhost:8443 +// +// Run: go test -v -run TestDialerAgainstCaptureServer ./internal/pkg/tlsfingerprint/... +func TestDialerAgainstCaptureServer(t *testing.T) { + captureURL := os.Getenv("TLSFINGERPRINT_CAPTURE_URL") + if captureURL == "" { + captureURL = "https://tls.sub2api.org:8090" + } + + tests := []struct { + name string + profile *Profile + }{ + { + name: "default_profile", + profile: &Profile{ + Name: "default", + EnableGREASE: false, + // All empty → uses built-in defaults + }, + }, + { + name: "linux_x64_node_v22171", + profile: &Profile{ + Name: "linux_x64_node_v22171", + EnableGREASE: false, + CipherSuites: []uint16{4866, 4867, 4865, 49199, 49195, 49200, 49196, 158, 49191, 103, 49192, 107, 163, 159, 52393, 52392, 52394, 49327, 49325, 49315, 49311, 49245, 49249, 49239, 49235, 162, 49326, 49324, 49314, 49310, 49244, 49248, 49238, 49234, 49188, 106, 49187, 64, 49162, 49172, 57, 56, 49161, 49171, 51, 50, 157, 49313, 49309, 49233, 156, 49312, 49308, 49232, 61, 60, 53, 47, 255}, + Curves: []uint16{29, 23, 30, 25, 24, 256, 257, 258, 259, 260}, + PointFormats: []uint16{0, 1, 2}, + SignatureAlgorithms: []uint16{0x0403, 0x0503, 0x0603, 0x0807, 0x0808, 0x0809, 0x080a, 0x080b, 0x0804, 0x0805, 0x0806, 0x0401, 0x0501, 0x0601, 0x0303, 0x0301, 0x0302, 0x0402, 0x0502, 0x0602}, + ALPNProtocols: []string{"http/1.1"}, + SupportedVersions: []uint16{0x0304, 0x0303}, + KeyShareGroups: []uint16{29}, + PSKModes: []uint16{1}, + Extensions: []uint16{0, 11, 10, 35, 16, 22, 23, 13, 43, 45, 51}, + }, + }, + { + name: "macos_arm64_node_v2430", + profile: &Profile{ + Name: "MacOS_arm64_node_v2430", + EnableGREASE: false, + CipherSuites: []uint16{4865, 4866, 4867, 49195, 49199, 49196, 49200, 52393, 52392, 49161, 49171, 49162, 49172, 156, 157, 47, 53}, + Curves: []uint16{29, 23, 24}, + PointFormats: []uint16{0}, + SignatureAlgorithms: []uint16{0x0403, 0x0804, 0x0401, 0x0503, 0x0805, 0x0501, 0x0806, 0x0601, 0x0201}, + ALPNProtocols: []string{"http/1.1"}, + SupportedVersions: []uint16{0x0304, 0x0303}, + KeyShareGroups: []uint16{29}, + PSKModes: []uint16{1}, + Extensions: []uint16{0, 65037, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43}, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + captured := fetchCapturedFingerprint(t, captureURL, tc.profile) + if captured == nil { + return + } + + t.Logf("JA3 Hash: %s", captured.JA3Hash) + t.Logf("JA4: %s", captured.JA4) + + // Resolve effective profile values (what the dialer actually uses) + effectiveCipherSuites := tc.profile.CipherSuites + if len(effectiveCipherSuites) == 0 { + effectiveCipherSuites = defaultCipherSuites + } + effectiveCurves := tc.profile.Curves + if len(effectiveCurves) == 0 { + effectiveCurves = make([]uint16, len(defaultCurves)) + for i, c := range defaultCurves { + effectiveCurves[i] = uint16(c) + } + } + effectivePointFormats := tc.profile.PointFormats + if len(effectivePointFormats) == 0 { + effectivePointFormats = defaultPointFormats + } + effectiveSigAlgs := tc.profile.SignatureAlgorithms + if len(effectiveSigAlgs) == 0 { + effectiveSigAlgs = make([]uint16, len(defaultSignatureAlgorithms)) + for i, s := range defaultSignatureAlgorithms { + effectiveSigAlgs[i] = uint16(s) + } + } + effectiveALPN := tc.profile.ALPNProtocols + if len(effectiveALPN) == 0 { + effectiveALPN = []string{"http/1.1"} + } + effectiveVersions := tc.profile.SupportedVersions + if len(effectiveVersions) == 0 { + effectiveVersions = []uint16{0x0304, 0x0303} + } + effectiveKeyShare := tc.profile.KeyShareGroups + if len(effectiveKeyShare) == 0 { + effectiveKeyShare = []uint16{29} // X25519 + } + effectivePSKModes := tc.profile.PSKModes + if len(effectivePSKModes) == 0 { + effectivePSKModes = []uint16{1} // psk_dhe_ke + } + + // Verify each field + assertIntSliceEqual(t, "cipher_suites", uint16sToInts(effectiveCipherSuites), captured.CipherSuites) + assertIntSliceEqual(t, "curves", uint16sToInts(effectiveCurves), captured.Curves) + assertIntSliceEqual(t, "point_formats", uint16sToInts(effectivePointFormats), captured.PointFormats) + assertIntSliceEqual(t, "signature_algorithms", uint16sToInts(effectiveSigAlgs), captured.SignatureAlgorithms) + assertStringSliceEqual(t, "alpn_protocols", effectiveALPN, captured.ALPNProtocols) + assertIntSliceEqual(t, "supported_versions", uint16sToInts(effectiveVersions), captured.SupportedVersions) + assertIntSliceEqual(t, "key_share_groups", uint16sToInts(effectiveKeyShare), captured.KeyShareGroups) + assertIntSliceEqual(t, "psk_modes", uint16sToInts(effectivePSKModes), captured.PSKModes) + + if captured.EnableGREASE != tc.profile.EnableGREASE { + t.Errorf("enable_grease: got %v, want %v", captured.EnableGREASE, tc.profile.EnableGREASE) + } else { + t.Logf(" enable_grease: %v OK", captured.EnableGREASE) + } + + // Verify extension order + // Use profile.Extensions if set, otherwise the default order (Node.js 24.x) + expectedExtOrder := uint16sToInts(defaultExtensionOrder) + if len(tc.profile.Extensions) > 0 { + expectedExtOrder = uint16sToInts(tc.profile.Extensions) + } + // Strip GREASE values from both expected and captured for comparison + var filteredExpected, filteredActual []int + for _, e := range expectedExtOrder { + if !isGREASEValue(uint16(e)) { + filteredExpected = append(filteredExpected, e) + } + } + for _, e := range captured.Extensions { + if !isGREASEValue(uint16(e)) { + filteredActual = append(filteredActual, e) + } + } + assertIntSliceEqual(t, "extensions (order, non-GREASE)", filteredExpected, filteredActual) + + // Print full captured data as JSON for debugging + capturedJSON, _ := json.MarshalIndent(captured, " ", " ") + t.Logf("Full captured fingerprint:\n %s", string(capturedJSON)) + }) + } +} + +func fetchCapturedFingerprint(t *testing.T, captureURL string, profile *Profile) *CapturedFingerprint { + t.Helper() + + dialer := NewDialer(profile, nil) + client := &http.Client{ + Transport: &http.Transport{ + DialTLSContext: dialer.DialTLSContext, + }, + Timeout: 10 * time.Second, + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", captureURL, strings.NewReader(`{"model":"test"}`)) + if err != nil { + t.Fatalf("create request: %v", err) + return nil + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer test-token") + + resp, err := client.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + return nil + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("read body: %v", err) + return nil + } + + var fp CapturedFingerprint + if err := json.Unmarshal(body, &fp); err != nil { + t.Logf("Response body: %s", string(body)) + t.Fatalf("parse response: %v", err) + return nil + } + + return &fp +} + +func uint16sToInts(vals []uint16) []int { + result := make([]int, len(vals)) + for i, v := range vals { + result[i] = int(v) + } + return result +} + +func assertIntSliceEqual(t *testing.T, name string, expected, actual []int) { + t.Helper() + if len(expected) != len(actual) { + t.Errorf("%s: length mismatch: got %d, want %d", name, len(actual), len(expected)) + if len(actual) < 20 && len(expected) < 20 { + t.Errorf(" got: %v", actual) + t.Errorf(" want: %v", expected) + } + return + } + mismatches := 0 + for i := range expected { + if expected[i] != actual[i] { + if mismatches < 5 { + t.Errorf("%s[%d]: got %d (0x%04x), want %d (0x%04x)", name, i, actual[i], actual[i], expected[i], expected[i]) + } + mismatches++ + } + } + if mismatches == 0 { + t.Logf(" %s: %d items OK", name, len(expected)) + } else if mismatches > 5 { + t.Errorf(" %s: %d/%d mismatches (showing first 5)", name, mismatches, len(expected)) + } +} + +func assertStringSliceEqual(t *testing.T, name string, expected, actual []string) { + t.Helper() + if len(expected) != len(actual) { + t.Errorf("%s: length mismatch: got %d (%v), want %d (%v)", name, len(actual), actual, len(expected), expected) + return + } + for i := range expected { + if expected[i] != actual[i] { + t.Errorf("%s[%d]: got %q, want %q", name, i, actual[i], expected[i]) + return + } + } + t.Logf(" %s: %v OK", name, expected) +} + +// TestBuildClientHelloSpecNewFields tests that new Profile fields are correctly applied. +func TestBuildClientHelloSpecNewFields(t *testing.T) { + // Test custom ALPN, versions, key shares, PSK modes + profile := &Profile{ + Name: "custom_full", + EnableGREASE: false, + CipherSuites: []uint16{0x1301, 0x1302}, + Curves: []uint16{29, 23}, + PointFormats: []uint16{0}, + SignatureAlgorithms: []uint16{0x0403, 0x0804}, + ALPNProtocols: []string{"h2", "http/1.1"}, + SupportedVersions: []uint16{0x0304}, + KeyShareGroups: []uint16{29, 23}, + PSKModes: []uint16{1}, + } + + spec := buildClientHelloSpecFromProfile(profile) + + // Verify cipher suites + if len(spec.CipherSuites) != 2 || spec.CipherSuites[0] != 0x1301 { + t.Errorf("cipher suites: got %v", spec.CipherSuites) + } + + // Check extensions for expected values + var foundALPN, foundVersions, foundKeyShare, foundPSK, foundSigAlgs bool + for _, ext := range spec.Extensions { + switch e := ext.(type) { + case *utls.ALPNExtension: + foundALPN = true + if len(e.AlpnProtocols) != 2 || e.AlpnProtocols[0] != "h2" { + t.Errorf("ALPN: got %v, want [h2, http/1.1]", e.AlpnProtocols) + } + case *utls.SupportedVersionsExtension: + foundVersions = true + if len(e.Versions) != 1 || e.Versions[0] != 0x0304 { + t.Errorf("versions: got %v, want [0x0304]", e.Versions) + } + case *utls.KeyShareExtension: + foundKeyShare = true + if len(e.KeyShares) != 2 { + t.Errorf("key shares: got %d entries, want 2", len(e.KeyShares)) + } + case *utls.PSKKeyExchangeModesExtension: + foundPSK = true + if len(e.Modes) != 1 || e.Modes[0] != 1 { + t.Errorf("PSK modes: got %v, want [1]", e.Modes) + } + case *utls.SignatureAlgorithmsExtension: + foundSigAlgs = true + if len(e.SupportedSignatureAlgorithms) != 2 { + t.Errorf("sig algs: got %d, want 2", len(e.SupportedSignatureAlgorithms)) + } + } + } + + for name, found := range map[string]bool{ + "ALPN": foundALPN, "Versions": foundVersions, "KeyShare": foundKeyShare, + "PSK": foundPSK, "SigAlgs": foundSigAlgs, + } { + if !found { + t.Errorf("extension %s not found in spec", name) + } + } + + // Test nil profile uses all defaults + specDefault := buildClientHelloSpecFromProfile(nil) + for _, ext := range specDefault.Extensions { + switch e := ext.(type) { + case *utls.ALPNExtension: + if len(e.AlpnProtocols) != 1 || e.AlpnProtocols[0] != "http/1.1" { + t.Errorf("default ALPN: got %v, want [http/1.1]", e.AlpnProtocols) + } + case *utls.SupportedVersionsExtension: + if len(e.Versions) != 2 { + t.Errorf("default versions: got %v, want 2 entries", e.Versions) + } + case *utls.KeyShareExtension: + if len(e.KeyShares) != 1 { + t.Errorf("default key shares: got %d, want 1", len(e.KeyShares)) + } + } + } + + t.Log("TestBuildClientHelloSpecNewFields passed") +} diff --git a/backend/internal/pkg/tlsfingerprint/dialer_integration_test.go b/backend/internal/pkg/tlsfingerprint/dialer_integration_test.go index 3f668fbe..38cddd0d 100644 --- a/backend/internal/pkg/tlsfingerprint/dialer_integration_test.go +++ b/backend/internal/pkg/tlsfingerprint/dialer_integration_test.go @@ -40,16 +40,15 @@ func skipIfExternalServiceUnavailable(t *testing.T, err error) { // TestJA3Fingerprint verifies the JA3/JA4 fingerprint matches expected value. // This test uses tls.peet.ws to verify the fingerprint. -// Expected JA3 hash: 1a28e69016765d92e3b381168d68922c (Claude CLI / Node.js 20.x) -// Expected JA4: t13d5911h1_a33745022dd6_1f22a2ca17c4 (d=domain) or t13i5911h1_... (i=IP) +// Expected JA3 hash: 44f88fca027f27bab4bb08d4af15f23e (Node.js 24.x) +// Expected JA4: t13d1714h1_5b57614c22b0_7baf387fc6ff func TestJA3Fingerprint(t *testing.T) { - // Skip if network is unavailable or if running in short mode if testing.Short() { t.Skip("skipping integration test in short mode") } profile := &Profile{ - Name: "Claude CLI Test", + Name: "Default Profile Test", EnableGREASE: false, } dialer := NewDialer(profile, nil) @@ -61,7 +60,6 @@ func TestJA3Fingerprint(t *testing.T) { Timeout: 30 * time.Second, } - // Use tls.peet.ws fingerprint detection API ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -69,7 +67,7 @@ func TestJA3Fingerprint(t *testing.T) { if err != nil { t.Fatalf("failed to create request: %v", err) } - req.Header.Set("User-Agent", "Claude Code/2.0.0 Node.js/20.0.0") + req.Header.Set("User-Agent", "Claude Code/2.0.0 Node.js/24.3.0") resp, err := client.Do(req) skipIfExternalServiceUnavailable(t, err) @@ -86,71 +84,23 @@ func TestJA3Fingerprint(t *testing.T) { t.Fatalf("failed to parse fingerprint response: %v", err) } - // Log all fingerprint information t.Logf("JA3: %s", fpResp.TLS.JA3) t.Logf("JA3 Hash: %s", fpResp.TLS.JA3Hash) t.Logf("JA4: %s", fpResp.TLS.JA4) - t.Logf("PeetPrint: %s", fpResp.TLS.PeetPrint) - t.Logf("PeetPrint Hash: %s", fpResp.TLS.PeetPrintHash) - // Verify JA3 hash matches expected value - expectedJA3Hash := "1a28e69016765d92e3b381168d68922c" + expectedJA3Hash := "44f88fca027f27bab4bb08d4af15f23e" if fpResp.TLS.JA3Hash == expectedJA3Hash { - t.Logf("✓ JA3 hash matches expected value: %s", expectedJA3Hash) + t.Logf("✓ JA3 hash matches: %s", expectedJA3Hash) } else { t.Errorf("✗ JA3 hash mismatch: got %s, expected %s", fpResp.TLS.JA3Hash, expectedJA3Hash) } - // Verify JA4 fingerprint - // JA4 format: t[version][sni][cipher_count][ext_count][alpn]_[cipher_hash]_[ext_hash] - // Expected: t13d5910h1 (d=domain) or t13i5910h1 (i=IP) - // The suffix _a33745022dd6_1f22a2ca17c4 should match - expectedJA4Suffix := "_a33745022dd6_1f22a2ca17c4" - if strings.HasSuffix(fpResp.TLS.JA4, expectedJA4Suffix) { - t.Logf("✓ JA4 suffix matches expected value: %s", expectedJA4Suffix) + expectedJA4CipherHash := "_5b57614c22b0_" + if strings.Contains(fpResp.TLS.JA4, expectedJA4CipherHash) { + t.Logf("✓ JA4 cipher hash matches: %s", expectedJA4CipherHash) } else { - t.Errorf("✗ JA4 suffix mismatch: got %s, expected suffix %s", fpResp.TLS.JA4, expectedJA4Suffix) + t.Errorf("✗ JA4 cipher hash mismatch: got %s, expected containing %s", fpResp.TLS.JA4, expectedJA4CipherHash) } - - // Verify JA4 prefix (t13d5911h1 or t13i5911h1) - // d = domain (SNI present), i = IP (no SNI) - // Since we connect to tls.peet.ws (domain), we expect 'd' - expectedJA4Prefix := "t13d5911h1" - if strings.HasPrefix(fpResp.TLS.JA4, expectedJA4Prefix) { - t.Logf("✓ JA4 prefix matches: %s (t13=TLS1.3, d=domain, 59=ciphers, 11=extensions, h1=HTTP/1.1)", expectedJA4Prefix) - } else { - // Also accept 'i' variant for IP connections - altPrefix := "t13i5911h1" - if strings.HasPrefix(fpResp.TLS.JA4, altPrefix) { - t.Logf("✓ JA4 prefix matches (IP variant): %s", altPrefix) - } else { - t.Errorf("✗ JA4 prefix mismatch: got %s, expected %s or %s", fpResp.TLS.JA4, expectedJA4Prefix, altPrefix) - } - } - - // Verify JA3 contains expected cipher suites (TLS 1.3 ciphers at the beginning) - if strings.Contains(fpResp.TLS.JA3, "4866-4867-4865") { - t.Logf("✓ JA3 contains expected TLS 1.3 cipher suites") - } else { - t.Logf("Warning: JA3 does not contain expected TLS 1.3 cipher suites") - } - - // Verify extension list (should be 11 extensions including SNI) - // Expected: 0-11-10-35-16-22-23-13-43-45-51 - expectedExtensions := "0-11-10-35-16-22-23-13-43-45-51" - if strings.Contains(fpResp.TLS.JA3, expectedExtensions) { - t.Logf("✓ JA3 contains expected extension list: %s", expectedExtensions) - } else { - t.Logf("Warning: JA3 extension list may differ") - } -} - -// TestProfileExpectation defines expected fingerprint values for a profile. -type TestProfileExpectation struct { - Profile *Profile - ExpectedJA3 string // Expected JA3 hash (empty = don't check) - ExpectedJA4 string // Expected full JA4 (empty = don't check) - JA4CipherHash string // Expected JA4 cipher hash - the stable middle part (empty = don't check) } // TestAllProfiles tests multiple TLS fingerprint profiles against tls.peet.ws. @@ -164,30 +114,24 @@ func TestAllProfiles(t *testing.T) { // These profiles are from config.yaml gateway.tls_fingerprint.profiles profiles := []TestProfileExpectation{ { - // Linux x64 Node.js v22.17.1 - // Expected JA3 Hash: 1a28e69016765d92e3b381168d68922c - // Expected JA4: t13d5911h1_a33745022dd6_1f22a2ca17c4 + // Default profile (Node.js 24.x) + Profile: &Profile{ + Name: "default_node_v24", + EnableGREASE: false, + }, + JA4CipherHash: "5b57614c22b0", + }, + { + // Linux x64 Node.js v22.17.1 (explicit profile with v22 extensions) Profile: &Profile{ Name: "linux_x64_node_v22171", EnableGREASE: false, CipherSuites: []uint16{4866, 4867, 4865, 49199, 49195, 49200, 49196, 158, 49191, 103, 49192, 107, 163, 159, 52393, 52392, 52394, 49327, 49325, 49315, 49311, 49245, 49249, 49239, 49235, 162, 49326, 49324, 49314, 49310, 49244, 49248, 49238, 49234, 49188, 106, 49187, 64, 49162, 49172, 57, 56, 49161, 49171, 51, 50, 157, 49313, 49309, 49233, 156, 49312, 49308, 49232, 61, 60, 53, 47, 255}, Curves: []uint16{29, 23, 30, 25, 24, 256, 257, 258, 259, 260}, - PointFormats: []uint8{0, 1, 2}, + PointFormats: []uint16{0, 1, 2}, + Extensions: []uint16{0, 11, 10, 35, 16, 22, 23, 13, 43, 45, 51}, }, - JA4CipherHash: "a33745022dd6", // stable part - }, - { - // MacOS arm64 Node.js v22.18.0 - // Expected JA3 Hash: 70cb5ca646080902703ffda87036a5ea - // Expected JA4: t13d5912h1_a33745022dd6_dbd39dd1d406 - Profile: &Profile{ - Name: "macos_arm64_node_v22180", - EnableGREASE: false, - CipherSuites: []uint16{4866, 4867, 4865, 49199, 49195, 49200, 49196, 158, 49191, 103, 49192, 107, 163, 159, 52393, 52392, 52394, 49327, 49325, 49315, 49311, 49245, 49249, 49239, 49235, 162, 49326, 49324, 49314, 49310, 49244, 49248, 49238, 49234, 49188, 106, 49187, 64, 49162, 49172, 57, 56, 49161, 49171, 51, 50, 157, 49313, 49309, 49233, 156, 49312, 49308, 49232, 61, 60, 53, 47, 255}, - Curves: []uint16{29, 23, 30, 25, 24, 256, 257, 258, 259, 260}, - PointFormats: []uint8{0, 1, 2}, - }, - JA4CipherHash: "a33745022dd6", // stable part (same cipher suites) + JA4CipherHash: "a33745022dd6", }, } diff --git a/backend/internal/pkg/tlsfingerprint/dialer_test.go b/backend/internal/pkg/tlsfingerprint/dialer_test.go index 6d3db174..048418c9 100644 --- a/backend/internal/pkg/tlsfingerprint/dialer_test.go +++ b/backend/internal/pkg/tlsfingerprint/dialer_test.go @@ -55,13 +55,13 @@ func TestDialerBasicConnection(t *testing.T) { // TestJA3Fingerprint verifies the JA3/JA4 fingerprint matches expected value. // This test uses tls.peet.ws to verify the fingerprint. -// Expected JA3 hash: 1a28e69016765d92e3b381168d68922c (Claude CLI / Node.js 20.x) -// Expected JA4: t13d5911h1_a33745022dd6_1f22a2ca17c4 (d=domain) or t13i5911h1_... (i=IP) +// Expected JA3 hash: 44f88fca027f27bab4bb08d4af15f23e (Node.js 24.x) +// Expected JA4: t13d1714h1_5b57614c22b0_7baf387fc6ff func TestJA3Fingerprint(t *testing.T) { skipNetworkTest(t) profile := &Profile{ - Name: "Claude CLI Test", + Name: "Default Profile Test", EnableGREASE: false, } dialer := NewDialer(profile, nil) @@ -81,7 +81,7 @@ func TestJA3Fingerprint(t *testing.T) { if err != nil { t.Fatalf("failed to create request: %v", err) } - req.Header.Set("User-Agent", "Claude Code/2.0.0 Node.js/20.0.0") + req.Header.Set("User-Agent", "Claude Code/2.0.0 Node.js/24.3.0") resp, err := client.Do(req) if err != nil { @@ -107,34 +107,28 @@ func TestJA3Fingerprint(t *testing.T) { t.Logf("PeetPrint: %s", fpResp.TLS.PeetPrint) t.Logf("PeetPrint Hash: %s", fpResp.TLS.PeetPrintHash) - // Verify JA3 hash matches expected value - expectedJA3Hash := "1a28e69016765d92e3b381168d68922c" + // Verify JA3 hash matches expected value (Node.js 24.x default) + expectedJA3Hash := "44f88fca027f27bab4bb08d4af15f23e" if fpResp.TLS.JA3Hash == expectedJA3Hash { t.Logf("✓ JA3 hash matches expected value: %s", expectedJA3Hash) } else { t.Errorf("✗ JA3 hash mismatch: got %s, expected %s", fpResp.TLS.JA3Hash, expectedJA3Hash) } - // Verify JA4 fingerprint - // JA4 format: t[version][sni][cipher_count][ext_count][alpn]_[cipher_hash]_[ext_hash] - // Expected: t13d5910h1 (d=domain) or t13i5910h1 (i=IP) - // The suffix _a33745022dd6_1f22a2ca17c4 should match - expectedJA4Suffix := "_a33745022dd6_1f22a2ca17c4" - if strings.HasSuffix(fpResp.TLS.JA4, expectedJA4Suffix) { - t.Logf("✓ JA4 suffix matches expected value: %s", expectedJA4Suffix) + // Verify JA4 cipher hash (stable middle part) + expectedJA4CipherHash := "_5b57614c22b0_" + if strings.Contains(fpResp.TLS.JA4, expectedJA4CipherHash) { + t.Logf("✓ JA4 cipher hash matches: %s", expectedJA4CipherHash) } else { - t.Errorf("✗ JA4 suffix mismatch: got %s, expected suffix %s", fpResp.TLS.JA4, expectedJA4Suffix) + t.Errorf("✗ JA4 cipher hash mismatch: got %s, expected containing %s", fpResp.TLS.JA4, expectedJA4CipherHash) } - // Verify JA4 prefix (t13d5911h1 or t13i5911h1) - // d = domain (SNI present), i = IP (no SNI) - // Since we connect to tls.peet.ws (domain), we expect 'd' - expectedJA4Prefix := "t13d5911h1" + // Verify JA4 prefix (t13d1714h1 or t13i1714h1) + expectedJA4Prefix := "t13d1714h1" if strings.HasPrefix(fpResp.TLS.JA4, expectedJA4Prefix) { - t.Logf("✓ JA4 prefix matches: %s (t13=TLS1.3, d=domain, 59=ciphers, 11=extensions, h1=HTTP/1.1)", expectedJA4Prefix) + t.Logf("✓ JA4 prefix matches: %s (t13=TLS1.3, d=domain, 17=ciphers, 14=extensions, h1=HTTP/1.1)", expectedJA4Prefix) } else { - // Also accept 'i' variant for IP connections - altPrefix := "t13i5911h1" + altPrefix := "t13i1714h1" if strings.HasPrefix(fpResp.TLS.JA4, altPrefix) { t.Logf("✓ JA4 prefix matches (IP variant): %s", altPrefix) } else { @@ -142,16 +136,15 @@ func TestJA3Fingerprint(t *testing.T) { } } - // Verify JA3 contains expected cipher suites (TLS 1.3 ciphers at the beginning) - if strings.Contains(fpResp.TLS.JA3, "4866-4867-4865") { + // Verify JA3 contains expected TLS 1.3 cipher suites + if strings.Contains(fpResp.TLS.JA3, "4865-4866-4867") { t.Logf("✓ JA3 contains expected TLS 1.3 cipher suites") } else { t.Logf("Warning: JA3 does not contain expected TLS 1.3 cipher suites") } - // Verify extension list (should be 11 extensions including SNI) - // Expected: 0-11-10-35-16-22-23-13-43-45-51 - expectedExtensions := "0-11-10-35-16-22-23-13-43-45-51" + // Verify extension list (14 extensions, Node.js 24.x order) + expectedExtensions := "0-65037-23-65281-10-11-35-16-5-13-18-51-45-43" if strings.Contains(fpResp.TLS.JA3, expectedExtensions) { t.Logf("✓ JA3 contains expected extension list: %s", expectedExtensions) } else { @@ -186,8 +179,8 @@ func TestDialerWithProfile(t *testing.T) { // Build specs and compare // Note: We can't directly compare JA3 without making network requests // but we can verify the specs are different - spec1 := dialer1.buildClientHelloSpec() - spec2 := dialer2.buildClientHelloSpec() + spec1 := buildClientHelloSpecFromProfile(dialer1.profile) + spec2 := buildClientHelloSpecFromProfile(dialer2.profile) // Profile with GREASE should have more extensions if len(spec2.Extensions) <= len(spec1.Extensions) { @@ -296,47 +289,33 @@ func mustParseURL(rawURL string) *url.URL { return u } -// TestProfileExpectation defines expected fingerprint values for a profile. -type TestProfileExpectation struct { - Profile *Profile - ExpectedJA3 string // Expected JA3 hash (empty = don't check) - ExpectedJA4 string // Expected full JA4 (empty = don't check) - JA4CipherHash string // Expected JA4 cipher hash - the stable middle part (empty = don't check) -} - // TestAllProfiles tests multiple TLS fingerprint profiles against tls.peet.ws. // Run with: go test -v -run TestAllProfiles ./internal/pkg/tlsfingerprint/... func TestAllProfiles(t *testing.T) { skipNetworkTest(t) - // Define all profiles to test with their expected fingerprints - // These profiles are from config.yaml gateway.tls_fingerprint.profiles profiles := []TestProfileExpectation{ { - // Linux x64 Node.js v22.17.1 - // Expected JA3 Hash: 1a28e69016765d92e3b381168d68922c - // Expected JA4: t13d5911h1_a33745022dd6_1f22a2ca17c4 + // Default profile (Node.js 24.x) + // JA3 Hash: 44f88fca027f27bab4bb08d4af15f23e + // JA4: t13d1714h1_5b57614c22b0_7baf387fc6ff + Profile: &Profile{ + Name: "default_node_v24", + EnableGREASE: false, + }, + JA4CipherHash: "5b57614c22b0", + }, + { + // Linux x64 Node.js v22.17.1 (explicit profile) Profile: &Profile{ Name: "linux_x64_node_v22171", EnableGREASE: false, CipherSuites: []uint16{4866, 4867, 4865, 49199, 49195, 49200, 49196, 158, 49191, 103, 49192, 107, 163, 159, 52393, 52392, 52394, 49327, 49325, 49315, 49311, 49245, 49249, 49239, 49235, 162, 49326, 49324, 49314, 49310, 49244, 49248, 49238, 49234, 49188, 106, 49187, 64, 49162, 49172, 57, 56, 49161, 49171, 51, 50, 157, 49313, 49309, 49233, 156, 49312, 49308, 49232, 61, 60, 53, 47, 255}, Curves: []uint16{29, 23, 30, 25, 24, 256, 257, 258, 259, 260}, - PointFormats: []uint8{0, 1, 2}, + PointFormats: []uint16{0, 1, 2}, + Extensions: []uint16{0, 11, 10, 35, 16, 22, 23, 13, 43, 45, 51}, }, - JA4CipherHash: "a33745022dd6", // stable part - }, - { - // MacOS arm64 Node.js v22.18.0 - // Expected JA3 Hash: 70cb5ca646080902703ffda87036a5ea - // Expected JA4: t13d5912h1_a33745022dd6_dbd39dd1d406 - Profile: &Profile{ - Name: "macos_arm64_node_v22180", - EnableGREASE: false, - CipherSuites: []uint16{4866, 4867, 4865, 49199, 49195, 49200, 49196, 158, 49191, 103, 49192, 107, 163, 159, 52393, 52392, 52394, 49327, 49325, 49315, 49311, 49245, 49249, 49239, 49235, 162, 49326, 49324, 49314, 49310, 49244, 49248, 49238, 49234, 49188, 106, 49187, 64, 49162, 49172, 57, 56, 49161, 49171, 51, 50, 157, 49313, 49309, 49233, 156, 49312, 49308, 49232, 61, 60, 53, 47, 255}, - Curves: []uint16{29, 23, 30, 25, 24, 256, 257, 258, 259, 260}, - PointFormats: []uint8{0, 1, 2}, - }, - JA4CipherHash: "a33745022dd6", // stable part (same cipher suites) + JA4CipherHash: "a33745022dd6", }, } diff --git a/backend/internal/pkg/tlsfingerprint/registry.go b/backend/internal/pkg/tlsfingerprint/registry.go deleted file mode 100644 index 6e9dc539..00000000 --- a/backend/internal/pkg/tlsfingerprint/registry.go +++ /dev/null @@ -1,171 +0,0 @@ -// Package tlsfingerprint provides TLS fingerprint simulation for HTTP clients. -package tlsfingerprint - -import ( - "log/slog" - "sort" - "sync" - - "github.com/Wei-Shaw/sub2api/internal/config" -) - -// DefaultProfileName is the name of the built-in Claude CLI profile. -const DefaultProfileName = "claude_cli_v2" - -// Registry manages TLS fingerprint profiles. -// It holds a collection of profiles that can be used for TLS fingerprint simulation. -// Profiles are selected based on account ID using modulo operation. -type Registry struct { - mu sync.RWMutex - profiles map[string]*Profile - profileNames []string // Sorted list of profile names for deterministic selection -} - -// NewRegistry creates a new TLS fingerprint profile registry. -// It initializes with the built-in default profile. -func NewRegistry() *Registry { - r := &Registry{ - profiles: make(map[string]*Profile), - profileNames: make([]string, 0), - } - - // Register the built-in default profile - r.registerBuiltinProfile() - - return r -} - -// NewRegistryFromConfig creates a new registry and loads profiles from config. -// If the config has custom profiles defined, they will be merged with the built-in default. -func NewRegistryFromConfig(cfg *config.TLSFingerprintConfig) *Registry { - r := NewRegistry() - - if cfg == nil || !cfg.Enabled { - slog.Debug("tls_registry_disabled", "reason", "disabled or no config") - return r - } - - // Load custom profiles from config - for name, profileCfg := range cfg.Profiles { - profile := &Profile{ - Name: profileCfg.Name, - EnableGREASE: profileCfg.EnableGREASE, - CipherSuites: profileCfg.CipherSuites, - Curves: profileCfg.Curves, - PointFormats: profileCfg.PointFormats, - } - - // If the profile has empty values, they will use defaults in dialer - r.RegisterProfile(name, profile) - slog.Debug("tls_registry_loaded_profile", "key", name, "name", profileCfg.Name) - } - - slog.Debug("tls_registry_initialized", "profile_count", len(r.profileNames), "profiles", r.profileNames) - return r -} - -// registerBuiltinProfile adds the default Claude CLI profile to the registry. -func (r *Registry) registerBuiltinProfile() { - defaultProfile := &Profile{ - Name: "Claude CLI 2.x (Node.js 20.x + OpenSSL 3.x)", - EnableGREASE: false, // Node.js does not use GREASE - // Empty slices will cause dialer to use built-in defaults - CipherSuites: nil, - Curves: nil, - PointFormats: nil, - } - r.RegisterProfile(DefaultProfileName, defaultProfile) -} - -// RegisterProfile adds or updates a profile in the registry. -func (r *Registry) RegisterProfile(name string, profile *Profile) { - r.mu.Lock() - defer r.mu.Unlock() - - // Check if this is a new profile - _, exists := r.profiles[name] - r.profiles[name] = profile - - if !exists { - r.profileNames = append(r.profileNames, name) - // Keep names sorted for deterministic selection - sort.Strings(r.profileNames) - } -} - -// GetProfile returns a profile by name. -// Returns nil if the profile does not exist. -func (r *Registry) GetProfile(name string) *Profile { - r.mu.RLock() - defer r.mu.RUnlock() - return r.profiles[name] -} - -// GetDefaultProfile returns the built-in default profile. -func (r *Registry) GetDefaultProfile() *Profile { - return r.GetProfile(DefaultProfileName) -} - -// GetProfileByAccountID returns a profile for the given account ID. -// The profile is selected using: profileNames[accountID % len(profiles)] -// This ensures deterministic profile assignment for each account. -func (r *Registry) GetProfileByAccountID(accountID int64) *Profile { - r.mu.RLock() - defer r.mu.RUnlock() - - if len(r.profileNames) == 0 { - return nil - } - - // Use modulo to select profile index - // Use absolute value to handle negative IDs (though unlikely) - idx := accountID - if idx < 0 { - idx = -idx - } - selectedIndex := int(idx % int64(len(r.profileNames))) - selectedName := r.profileNames[selectedIndex] - - return r.profiles[selectedName] -} - -// ProfileCount returns the number of registered profiles. -func (r *Registry) ProfileCount() int { - r.mu.RLock() - defer r.mu.RUnlock() - return len(r.profiles) -} - -// ProfileNames returns a sorted list of all registered profile names. -func (r *Registry) ProfileNames() []string { - r.mu.RLock() - defer r.mu.RUnlock() - - // Return a copy to prevent modification - names := make([]string, len(r.profileNames)) - copy(names, r.profileNames) - return names -} - -// Global registry instance for convenience -var globalRegistry *Registry -var globalRegistryOnce sync.Once - -// GlobalRegistry returns the global TLS fingerprint registry. -// The registry is lazily initialized with the default profile. -func GlobalRegistry() *Registry { - globalRegistryOnce.Do(func() { - globalRegistry = NewRegistry() - }) - return globalRegistry -} - -// InitGlobalRegistry initializes the global registry with configuration. -// This should be called during application startup. -// It is safe to call multiple times; subsequent calls will update the registry. -func InitGlobalRegistry(cfg *config.TLSFingerprintConfig) *Registry { - globalRegistryOnce.Do(func() { - globalRegistry = NewRegistryFromConfig(cfg) - }) - return globalRegistry -} diff --git a/backend/internal/pkg/tlsfingerprint/registry_test.go b/backend/internal/pkg/tlsfingerprint/registry_test.go deleted file mode 100644 index 752ba0cc..00000000 --- a/backend/internal/pkg/tlsfingerprint/registry_test.go +++ /dev/null @@ -1,243 +0,0 @@ -package tlsfingerprint - -import ( - "testing" - - "github.com/Wei-Shaw/sub2api/internal/config" -) - -func TestNewRegistry(t *testing.T) { - r := NewRegistry() - - // Should have exactly one profile (the default) - if r.ProfileCount() != 1 { - t.Errorf("expected 1 profile, got %d", r.ProfileCount()) - } - - // Should have the default profile - profile := r.GetDefaultProfile() - if profile == nil { - t.Error("expected default profile to exist") - } - - // Default profile name should be in the list - names := r.ProfileNames() - if len(names) != 1 || names[0] != DefaultProfileName { - t.Errorf("expected profile names to be [%s], got %v", DefaultProfileName, names) - } -} - -func TestRegisterProfile(t *testing.T) { - r := NewRegistry() - - // Register a new profile - customProfile := &Profile{ - Name: "Custom Profile", - EnableGREASE: true, - } - r.RegisterProfile("custom", customProfile) - - // Should now have 2 profiles - if r.ProfileCount() != 2 { - t.Errorf("expected 2 profiles, got %d", r.ProfileCount()) - } - - // Should be able to retrieve the custom profile - retrieved := r.GetProfile("custom") - if retrieved == nil { - t.Fatal("expected custom profile to exist") - } - if retrieved.Name != "Custom Profile" { - t.Errorf("expected profile name 'Custom Profile', got '%s'", retrieved.Name) - } - if !retrieved.EnableGREASE { - t.Error("expected EnableGREASE to be true") - } -} - -func TestGetProfile(t *testing.T) { - r := NewRegistry() - - // Get existing profile - profile := r.GetProfile(DefaultProfileName) - if profile == nil { - t.Error("expected default profile to exist") - } - - // Get non-existing profile - nonExistent := r.GetProfile("nonexistent") - if nonExistent != nil { - t.Error("expected nil for non-existent profile") - } -} - -func TestGetProfileByAccountID(t *testing.T) { - r := NewRegistry() - - // With only default profile, all account IDs should return the same profile - for i := int64(0); i < 10; i++ { - profile := r.GetProfileByAccountID(i) - if profile == nil { - t.Errorf("expected profile for account %d, got nil", i) - } - } - - // Add more profiles - r.RegisterProfile("profile_a", &Profile{Name: "Profile A"}) - r.RegisterProfile("profile_b", &Profile{Name: "Profile B"}) - - // Now we have 3 profiles: claude_cli_v2, profile_a, profile_b - // Names are sorted, so order is: claude_cli_v2, profile_a, profile_b - expectedOrder := []string{DefaultProfileName, "profile_a", "profile_b"} - names := r.ProfileNames() - for i, name := range expectedOrder { - if names[i] != name { - t.Errorf("expected name at index %d to be %s, got %s", i, name, names[i]) - } - } - - // Test modulo selection - // Account ID 0 % 3 = 0 -> claude_cli_v2 - // Account ID 1 % 3 = 1 -> profile_a - // Account ID 2 % 3 = 2 -> profile_b - // Account ID 3 % 3 = 0 -> claude_cli_v2 - testCases := []struct { - accountID int64 - expectedName string - }{ - {0, "Claude CLI 2.x (Node.js 20.x + OpenSSL 3.x)"}, - {1, "Profile A"}, - {2, "Profile B"}, - {3, "Claude CLI 2.x (Node.js 20.x + OpenSSL 3.x)"}, - {4, "Profile A"}, - {5, "Profile B"}, - {100, "Profile A"}, // 100 % 3 = 1 - {-1, "Profile A"}, // |-1| % 3 = 1 - {-3, "Claude CLI 2.x (Node.js 20.x + OpenSSL 3.x)"}, // |-3| % 3 = 0 - } - - for _, tc := range testCases { - profile := r.GetProfileByAccountID(tc.accountID) - if profile == nil { - t.Errorf("expected profile for account %d, got nil", tc.accountID) - continue - } - if profile.Name != tc.expectedName { - t.Errorf("account %d: expected profile name '%s', got '%s'", tc.accountID, tc.expectedName, profile.Name) - } - } -} - -func TestNewRegistryFromConfig(t *testing.T) { - // Test with nil config - r := NewRegistryFromConfig(nil) - if r.ProfileCount() != 1 { - t.Errorf("expected 1 profile with nil config, got %d", r.ProfileCount()) - } - - // Test with disabled config - disabledCfg := &config.TLSFingerprintConfig{ - Enabled: false, - } - r = NewRegistryFromConfig(disabledCfg) - if r.ProfileCount() != 1 { - t.Errorf("expected 1 profile with disabled config, got %d", r.ProfileCount()) - } - - // Test with enabled config and custom profiles - enabledCfg := &config.TLSFingerprintConfig{ - Enabled: true, - Profiles: map[string]config.TLSProfileConfig{ - "custom1": { - Name: "Custom Profile 1", - EnableGREASE: true, - }, - "custom2": { - Name: "Custom Profile 2", - EnableGREASE: false, - }, - }, - } - r = NewRegistryFromConfig(enabledCfg) - - // Should have 3 profiles: default + 2 custom - if r.ProfileCount() != 3 { - t.Errorf("expected 3 profiles, got %d", r.ProfileCount()) - } - - // Check custom profiles exist - custom1 := r.GetProfile("custom1") - if custom1 == nil || custom1.Name != "Custom Profile 1" { - t.Error("expected custom1 profile to exist with correct name") - } - custom2 := r.GetProfile("custom2") - if custom2 == nil || custom2.Name != "Custom Profile 2" { - t.Error("expected custom2 profile to exist with correct name") - } -} - -func TestProfileNames(t *testing.T) { - r := NewRegistry() - - // Add profiles in non-alphabetical order - r.RegisterProfile("zebra", &Profile{Name: "Zebra"}) - r.RegisterProfile("alpha", &Profile{Name: "Alpha"}) - r.RegisterProfile("beta", &Profile{Name: "Beta"}) - - names := r.ProfileNames() - - // Should be sorted alphabetically - expected := []string{"alpha", "beta", DefaultProfileName, "zebra"} - if len(names) != len(expected) { - t.Errorf("expected %d names, got %d", len(expected), len(names)) - } - for i, name := range expected { - if names[i] != name { - t.Errorf("expected name at index %d to be %s, got %s", i, name, names[i]) - } - } - - // Test that returned slice is a copy (modifying it shouldn't affect registry) - names[0] = "modified" - originalNames := r.ProfileNames() - if originalNames[0] == "modified" { - t.Error("modifying returned slice should not affect registry") - } -} - -func TestConcurrentAccess(t *testing.T) { - r := NewRegistry() - - // Run concurrent reads and writes - done := make(chan bool) - - // Writers - for i := 0; i < 10; i++ { - go func(id int) { - for j := 0; j < 100; j++ { - r.RegisterProfile("concurrent"+string(rune('0'+id)), &Profile{Name: "Concurrent"}) - } - done <- true - }(i) - } - - // Readers - for i := 0; i < 10; i++ { - go func(id int) { - for j := 0; j < 100; j++ { - _ = r.ProfileCount() - _ = r.ProfileNames() - _ = r.GetProfileByAccountID(int64(id * j)) - _ = r.GetProfile(DefaultProfileName) - } - done <- true - }(i) - } - - // Wait for all goroutines - for i := 0; i < 20; i++ { - <-done - } - - // Test should pass without data races (run with -race flag) -} diff --git a/backend/internal/pkg/tlsfingerprint/test_types_test.go b/backend/internal/pkg/tlsfingerprint/test_types_test.go index 2bbf2d22..1711100d 100644 --- a/backend/internal/pkg/tlsfingerprint/test_types_test.go +++ b/backend/internal/pkg/tlsfingerprint/test_types_test.go @@ -8,6 +8,14 @@ type FingerprintResponse struct { HTTP2 any `json:"http2"` } +// TestProfileExpectation defines expected fingerprint values for a profile. +type TestProfileExpectation struct { + Profile *Profile + ExpectedJA3 string // Expected JA3 hash (empty = don't check) + ExpectedJA4 string // Expected full JA4 (empty = don't check) + JA4CipherHash string // Expected JA4 cipher hash - the stable middle part (empty = don't check) +} + // TLSInfo contains TLS fingerprint details. type TLSInfo struct { JA3 string `json:"ja3"` diff --git a/backend/internal/repository/claude_usage_service.go b/backend/internal/repository/claude_usage_service.go index 1264f6bb..b44adde2 100644 --- a/backend/internal/repository/claude_usage_service.go +++ b/backend/internal/repository/claude_usage_service.go @@ -68,10 +68,9 @@ func (s *claudeUsageService) FetchUsageWithOptions(ctx context.Context, opts *se var resp *http.Response - // 如果启用 TLS 指纹且有 HTTPUpstream,使用 DoWithTLS - if opts.EnableTLSFingerprint && s.httpUpstream != nil { - // accountConcurrency 传 0 使用默认连接池配置,usage 请求不需要特殊的并发设置 - resp, err = s.httpUpstream.DoWithTLS(req, opts.ProxyURL, opts.AccountID, 0, true) + // 如果有 TLS Profile 且有 HTTPUpstream,使用 DoWithTLS + if opts.TLSProfile != nil && s.httpUpstream != nil { + resp, err = s.httpUpstream.DoWithTLS(req, opts.ProxyURL, opts.AccountID, 0, opts.TLSProfile) if err != nil { return nil, fmt.Errorf("request with TLS fingerprint failed: %w", err) } diff --git a/backend/internal/repository/http_upstream.go b/backend/internal/repository/http_upstream.go index 12523a91..4309e997 100644 --- a/backend/internal/repository/http_upstream.go +++ b/backend/internal/repository/http_upstream.go @@ -161,26 +161,14 @@ func (s *httpUpstreamService) Do(req *http.Request, proxyURL string, accountID i } // DoWithTLS 执行带 TLS 指纹伪装的 HTTP 请求 -// 根据 enableTLSFingerprint 参数决定是否使用 TLS 指纹 // -// 参数: -// - req: HTTP 请求对象 -// - proxyURL: 代理地址,空字符串表示直连 -// - accountID: 账户 ID,用于账户级隔离和 TLS 指纹模板选择 -// - accountConcurrency: 账户并发限制,用于动态调整连接池大小 -// - enableTLSFingerprint: 是否启用 TLS 指纹伪装 -// -// TLS 指纹说明: -// - 当 enableTLSFingerprint=true 时,使用 utls 库模拟 Claude CLI 的 TLS 指纹 -// - 指纹模板根据 accountID % len(profiles) 自动选择 -// - 支持直连、HTTP/HTTPS 代理、SOCKS5 代理三种场景 -func (s *httpUpstreamService) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) { - // 如果未启用 TLS 指纹,直接使用标准请求路径 - if !enableTLSFingerprint { +// profile 为 nil 时不启用 TLS 指纹,行为与 Do 方法相同。 +// profile 非 nil 时使用指定的 Profile 进行 TLS 指纹伪装。 +func (s *httpUpstreamService) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) { + if profile == nil { return s.Do(req, proxyURL, accountID, accountConcurrency) } - // TLS 指纹已启用,记录调试日志 targetHost := "" if req != nil && req.URL != nil { targetHost = req.URL.Host @@ -189,46 +177,28 @@ func (s *httpUpstreamService) DoWithTLS(req *http.Request, proxyURL string, acco if proxyURL != "" { proxyInfo = proxyURL } - slog.Debug("tls_fingerprint_enabled", "account_id", accountID, "target", targetHost, "proxy", proxyInfo) + slog.Debug("tls_fingerprint_enabled", "account_id", accountID, "target", targetHost, "proxy", proxyInfo, "profile", profile.Name) if err := s.validateRequestHost(req); err != nil { return nil, err } - // 获取 TLS 指纹 Profile - registry := tlsfingerprint.GlobalRegistry() - profile := registry.GetProfileByAccountID(accountID) - if profile == nil { - // 如果获取不到 profile,回退到普通请求 - slog.Debug("tls_fingerprint_no_profile", "account_id", accountID, "fallback", "standard_request") - return s.Do(req, proxyURL, accountID, accountConcurrency) - } - - slog.Debug("tls_fingerprint_using_profile", "account_id", accountID, "profile", profile.Name, "grease", profile.EnableGREASE) - - // 获取或创建带 TLS 指纹的客户端 entry, err := s.acquireClientWithTLS(proxyURL, accountID, accountConcurrency, profile) if err != nil { slog.Debug("tls_fingerprint_acquire_client_failed", "account_id", accountID, "error", err) return nil, err } - // 执行请求 resp, err := entry.client.Do(req) if err != nil { - // 请求失败,立即减少计数 atomic.AddInt64(&entry.inFlight, -1) atomic.StoreInt64(&entry.lastUsed, time.Now().UnixNano()) slog.Debug("tls_fingerprint_request_failed", "account_id", accountID, "error", err) return nil, err } - slog.Debug("tls_fingerprint_request_success", "account_id", accountID, "status", resp.StatusCode) - - // 如果上游返回了压缩内容,解压后再交给业务层 decompressResponseBody(resp) - // 包装响应体,在关闭时自动减少计数并更新时间戳 resp.Body = wrapTrackedBody(resp.Body, func() { atomic.AddInt64(&entry.inFlight, -1) atomic.StoreInt64(&entry.lastUsed, time.Now().UnixNano()) diff --git a/backend/internal/repository/tls_fingerprint_profile_cache.go b/backend/internal/repository/tls_fingerprint_profile_cache.go new file mode 100644 index 00000000..81ee0434 --- /dev/null +++ b/backend/internal/repository/tls_fingerprint_profile_cache.go @@ -0,0 +1,122 @@ +package repository + +import ( + "context" + "encoding/json" + "log/slog" + "sync" + "time" + + "github.com/Wei-Shaw/sub2api/internal/model" + "github.com/Wei-Shaw/sub2api/internal/service" + "github.com/redis/go-redis/v9" +) + +const ( + tlsFPProfileCacheKey = "tls_fingerprint_profiles" + tlsFPProfilePubSubKey = "tls_fingerprint_profiles_updated" + tlsFPProfileCacheTTL = 24 * time.Hour +) + +type tlsFingerprintProfileCache struct { + rdb *redis.Client + localCache []*model.TLSFingerprintProfile + localMu sync.RWMutex +} + +// NewTLSFingerprintProfileCache 创建 TLS 指纹模板缓存 +func NewTLSFingerprintProfileCache(rdb *redis.Client) service.TLSFingerprintProfileCache { + return &tlsFingerprintProfileCache{ + rdb: rdb, + } +} + +// Get 从缓存获取模板列表 +func (c *tlsFingerprintProfileCache) Get(ctx context.Context) ([]*model.TLSFingerprintProfile, bool) { + c.localMu.RLock() + if c.localCache != nil { + profiles := c.localCache + c.localMu.RUnlock() + return profiles, true + } + c.localMu.RUnlock() + + data, err := c.rdb.Get(ctx, tlsFPProfileCacheKey).Bytes() + if err != nil { + if err != redis.Nil { + slog.Warn("tls_fp_profile_cache_get_failed", "error", err) + } + return nil, false + } + + var profiles []*model.TLSFingerprintProfile + if err := json.Unmarshal(data, &profiles); err != nil { + slog.Warn("tls_fp_profile_cache_unmarshal_failed", "error", err) + return nil, false + } + + c.localMu.Lock() + c.localCache = profiles + c.localMu.Unlock() + + return profiles, true +} + +// Set 设置缓存 +func (c *tlsFingerprintProfileCache) Set(ctx context.Context, profiles []*model.TLSFingerprintProfile) error { + data, err := json.Marshal(profiles) + if err != nil { + return err + } + + if err := c.rdb.Set(ctx, tlsFPProfileCacheKey, data, tlsFPProfileCacheTTL).Err(); err != nil { + return err + } + + c.localMu.Lock() + c.localCache = profiles + c.localMu.Unlock() + + return nil +} + +// Invalidate 使缓存失效 +func (c *tlsFingerprintProfileCache) Invalidate(ctx context.Context) error { + c.localMu.Lock() + c.localCache = nil + c.localMu.Unlock() + + return c.rdb.Del(ctx, tlsFPProfileCacheKey).Err() +} + +// NotifyUpdate 通知其他实例刷新缓存 +func (c *tlsFingerprintProfileCache) NotifyUpdate(ctx context.Context) error { + return c.rdb.Publish(ctx, tlsFPProfilePubSubKey, "refresh").Err() +} + +// SubscribeUpdates 订阅缓存更新通知 +func (c *tlsFingerprintProfileCache) SubscribeUpdates(ctx context.Context, handler func()) { + go func() { + sub := c.rdb.Subscribe(ctx, tlsFPProfilePubSubKey) + defer func() { _ = sub.Close() }() + + ch := sub.Channel() + for { + select { + case <-ctx.Done(): + slog.Debug("tls_fp_profile_cache_subscriber_stopped", "reason", "context_done") + return + case msg := <-ch: + if msg == nil { + slog.Warn("tls_fp_profile_cache_subscriber_stopped", "reason", "channel_closed") + return + } + c.localMu.Lock() + c.localCache = nil + c.localMu.Unlock() + + handler() + } + } + }() +} diff --git a/backend/internal/repository/tls_fingerprint_profile_repo.go b/backend/internal/repository/tls_fingerprint_profile_repo.go new file mode 100644 index 00000000..40bebdc3 --- /dev/null +++ b/backend/internal/repository/tls_fingerprint_profile_repo.go @@ -0,0 +1,213 @@ +package repository + +import ( + "context" + + "github.com/Wei-Shaw/sub2api/ent" + "github.com/Wei-Shaw/sub2api/ent/tlsfingerprintprofile" + "github.com/Wei-Shaw/sub2api/internal/model" + "github.com/Wei-Shaw/sub2api/internal/service" +) + +type tlsFingerprintProfileRepository struct { + client *ent.Client +} + +// NewTLSFingerprintProfileRepository 创建 TLS 指纹模板仓库 +func NewTLSFingerprintProfileRepository(client *ent.Client) service.TLSFingerprintProfileRepository { + return &tlsFingerprintProfileRepository{client: client} +} + +// List 获取所有模板 +func (r *tlsFingerprintProfileRepository) List(ctx context.Context) ([]*model.TLSFingerprintProfile, error) { + profiles, err := r.client.TLSFingerprintProfile.Query(). + Order(ent.Asc(tlsfingerprintprofile.FieldName)). + All(ctx) + if err != nil { + return nil, err + } + + result := make([]*model.TLSFingerprintProfile, len(profiles)) + for i, p := range profiles { + result[i] = r.toModel(p) + } + return result, nil +} + +// GetByID 根据 ID 获取模板 +func (r *tlsFingerprintProfileRepository) GetByID(ctx context.Context, id int64) (*model.TLSFingerprintProfile, error) { + p, err := r.client.TLSFingerprintProfile.Get(ctx, id) + if err != nil { + if ent.IsNotFound(err) { + return nil, nil + } + return nil, err + } + return r.toModel(p), nil +} + +// Create 创建模板 +func (r *tlsFingerprintProfileRepository) Create(ctx context.Context, p *model.TLSFingerprintProfile) (*model.TLSFingerprintProfile, error) { + builder := r.client.TLSFingerprintProfile.Create(). + SetName(p.Name). + SetEnableGrease(p.EnableGREASE) + + if p.Description != nil { + builder.SetDescription(*p.Description) + } + if len(p.CipherSuites) > 0 { + builder.SetCipherSuites(p.CipherSuites) + } + if len(p.Curves) > 0 { + builder.SetCurves(p.Curves) + } + if len(p.PointFormats) > 0 { + builder.SetPointFormats(p.PointFormats) + } + if len(p.SignatureAlgorithms) > 0 { + builder.SetSignatureAlgorithms(p.SignatureAlgorithms) + } + if len(p.ALPNProtocols) > 0 { + builder.SetAlpnProtocols(p.ALPNProtocols) + } + if len(p.SupportedVersions) > 0 { + builder.SetSupportedVersions(p.SupportedVersions) + } + if len(p.KeyShareGroups) > 0 { + builder.SetKeyShareGroups(p.KeyShareGroups) + } + if len(p.PSKModes) > 0 { + builder.SetPskModes(p.PSKModes) + } + if len(p.Extensions) > 0 { + builder.SetExtensions(p.Extensions) + } + + created, err := builder.Save(ctx) + if err != nil { + return nil, err + } + return r.toModel(created), nil +} + +// Update 更新模板 +func (r *tlsFingerprintProfileRepository) Update(ctx context.Context, p *model.TLSFingerprintProfile) (*model.TLSFingerprintProfile, error) { + builder := r.client.TLSFingerprintProfile.UpdateOneID(p.ID). + SetName(p.Name). + SetEnableGrease(p.EnableGREASE) + + if p.Description != nil { + builder.SetDescription(*p.Description) + } else { + builder.ClearDescription() + } + + if len(p.CipherSuites) > 0 { + builder.SetCipherSuites(p.CipherSuites) + } else { + builder.ClearCipherSuites() + } + if len(p.Curves) > 0 { + builder.SetCurves(p.Curves) + } else { + builder.ClearCurves() + } + if len(p.PointFormats) > 0 { + builder.SetPointFormats(p.PointFormats) + } else { + builder.ClearPointFormats() + } + if len(p.SignatureAlgorithms) > 0 { + builder.SetSignatureAlgorithms(p.SignatureAlgorithms) + } else { + builder.ClearSignatureAlgorithms() + } + if len(p.ALPNProtocols) > 0 { + builder.SetAlpnProtocols(p.ALPNProtocols) + } else { + builder.ClearAlpnProtocols() + } + if len(p.SupportedVersions) > 0 { + builder.SetSupportedVersions(p.SupportedVersions) + } else { + builder.ClearSupportedVersions() + } + if len(p.KeyShareGroups) > 0 { + builder.SetKeyShareGroups(p.KeyShareGroups) + } else { + builder.ClearKeyShareGroups() + } + if len(p.PSKModes) > 0 { + builder.SetPskModes(p.PSKModes) + } else { + builder.ClearPskModes() + } + if len(p.Extensions) > 0 { + builder.SetExtensions(p.Extensions) + } else { + builder.ClearExtensions() + } + + updated, err := builder.Save(ctx) + if err != nil { + return nil, err + } + return r.toModel(updated), nil +} + +// Delete 删除模板 +func (r *tlsFingerprintProfileRepository) Delete(ctx context.Context, id int64) error { + return r.client.TLSFingerprintProfile.DeleteOneID(id).Exec(ctx) +} + +// toModel 将 Ent 实体转换为服务模型 +func (r *tlsFingerprintProfileRepository) toModel(e *ent.TLSFingerprintProfile) *model.TLSFingerprintProfile { + p := &model.TLSFingerprintProfile{ + ID: e.ID, + Name: e.Name, + Description: e.Description, + EnableGREASE: e.EnableGrease, + CipherSuites: e.CipherSuites, + Curves: e.Curves, + PointFormats: e.PointFormats, + SignatureAlgorithms: e.SignatureAlgorithms, + ALPNProtocols: e.AlpnProtocols, + SupportedVersions: e.SupportedVersions, + KeyShareGroups: e.KeyShareGroups, + PSKModes: e.PskModes, + Extensions: e.Extensions, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + } + + // 确保切片不为 nil + if p.CipherSuites == nil { + p.CipherSuites = []uint16{} + } + if p.Curves == nil { + p.Curves = []uint16{} + } + if p.PointFormats == nil { + p.PointFormats = []uint16{} + } + if p.SignatureAlgorithms == nil { + p.SignatureAlgorithms = []uint16{} + } + if p.ALPNProtocols == nil { + p.ALPNProtocols = []string{} + } + if p.SupportedVersions == nil { + p.SupportedVersions = []uint16{} + } + if p.KeyShareGroups == nil { + p.KeyShareGroups = []uint16{} + } + if p.PSKModes == nil { + p.PSKModes = []uint16{} + } + if p.Extensions == nil { + p.Extensions = []uint16{} + } + + return p +} diff --git a/backend/internal/repository/wire.go b/backend/internal/repository/wire.go index 138bf59e..f65f9beb 100644 --- a/backend/internal/repository/wire.go +++ b/backend/internal/repository/wire.go @@ -73,6 +73,7 @@ var ProviderSet = wire.NewSet( NewUserAttributeValueRepository, NewUserGroupRateRepository, NewErrorPassthroughRepository, + NewTLSFingerprintProfileRepository, // Cache implementations NewGatewayCache, @@ -96,6 +97,7 @@ var ProviderSet = wire.NewSet( NewTotpCache, NewRefreshTokenCache, NewErrorPassthroughCache, + NewTLSFingerprintProfileCache, // Encryptors NewAESEncryptor, diff --git a/backend/internal/server/routes/admin.go b/backend/internal/server/routes/admin.go index 6fd239bb..e04dae85 100644 --- a/backend/internal/server/routes/admin.go +++ b/backend/internal/server/routes/admin.go @@ -79,6 +79,9 @@ func RegisterAdminRoutes( // 错误透传规则管理 registerErrorPassthroughRoutes(admin, h) + // TLS 指纹模板管理 + registerTLSFingerprintProfileRoutes(admin, h) + // API Key 管理 registerAdminAPIKeyRoutes(admin, h) @@ -553,3 +556,14 @@ func registerErrorPassthroughRoutes(admin *gin.RouterGroup, h *handler.Handlers) rules.DELETE("/:id", h.Admin.ErrorPassthrough.Delete) } } + +func registerTLSFingerprintProfileRoutes(admin *gin.RouterGroup, h *handler.Handlers) { + profiles := admin.Group("/tls-fingerprint-profiles") + { + profiles.GET("", h.Admin.TLSFingerprintProfile.List) + profiles.GET("/:id", h.Admin.TLSFingerprintProfile.GetByID) + profiles.POST("", h.Admin.TLSFingerprintProfile.Create) + profiles.PUT("/:id", h.Admin.TLSFingerprintProfile.Update) + profiles.DELETE("/:id", h.Admin.TLSFingerprintProfile.Delete) + } +} diff --git a/backend/internal/service/account.go b/backend/internal/service/account.go index d42c6a11..741e33e8 100644 --- a/backend/internal/service/account.go +++ b/backend/internal/service/account.go @@ -1165,6 +1165,31 @@ func (a *Account) IsTLSFingerprintEnabled() bool { return false } +// GetTLSFingerprintProfileID 获取账号绑定的 TLS 指纹模板 ID +// 返回 0 表示未绑定(使用内置默认 profile) +func (a *Account) GetTLSFingerprintProfileID() int64 { + if a.Extra == nil { + return 0 + } + v, ok := a.Extra["tls_fingerprint_profile_id"] + if !ok { + return 0 + } + switch id := v.(type) { + case float64: + return int64(id) + case int64: + return id + case int: + return int64(id) + case json.Number: + if i, err := id.Int64(); err == nil { + return i + } + } + return 0 +} + // GetUserMsgQueueMode 获取用户消息队列模式 // "serialize" = 串行队列, "throttle" = 软性限速, "" = 未设置(使用全局配置) func (a *Account) GetUserMsgQueueMode() string { diff --git a/backend/internal/service/account_test_service.go b/backend/internal/service/account_test_service.go index 12617336..fec98e12 100644 --- a/backend/internal/service/account_test_service.go +++ b/backend/internal/service/account_test_service.go @@ -23,6 +23,7 @@ import ( "github.com/Wei-Shaw/sub2api/internal/pkg/claude" "github.com/Wei-Shaw/sub2api/internal/pkg/geminicli" "github.com/Wei-Shaw/sub2api/internal/pkg/openai" + "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" "github.com/Wei-Shaw/sub2api/internal/util/soraerror" "github.com/Wei-Shaw/sub2api/internal/util/urlvalidator" "github.com/gin-gonic/gin" @@ -69,6 +70,7 @@ type AccountTestService struct { antigravityGatewayService *AntigravityGatewayService httpUpstream HTTPUpstream cfg *config.Config + tlsFPProfileService *TLSFingerprintProfileService soraTestGuardMu sync.Mutex soraTestLastRun map[int64]time.Time soraTestCooldown time.Duration @@ -83,6 +85,7 @@ func NewAccountTestService( antigravityGatewayService *AntigravityGatewayService, httpUpstream HTTPUpstream, cfg *config.Config, + tlsFPProfileService *TLSFingerprintProfileService, ) *AccountTestService { return &AccountTestService{ accountRepo: accountRepo, @@ -90,6 +93,7 @@ func NewAccountTestService( antigravityGatewayService: antigravityGatewayService, httpUpstream: httpUpstream, cfg: cfg, + tlsFPProfileService: tlsFPProfileService, soraTestLastRun: make(map[int64]time.Time), soraTestCooldown: defaultSoraTestCooldown, } @@ -300,7 +304,7 @@ func (s *AccountTestService) testClaudeAccountConnection(c *gin.Context, account proxyURL = account.Proxy.URL() } - resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled()) + resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account)) if err != nil { return s.sendErrorAndEnd(c, fmt.Sprintf("Request failed: %s", err.Error())) } @@ -390,7 +394,7 @@ func (s *AccountTestService) testBedrockAccountConnection(c *gin.Context, ctx co proxyURL = account.Proxy.URL() } - resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, false) + resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, nil) if err != nil { return s.sendErrorAndEnd(c, fmt.Sprintf("Request failed: %s", err.Error())) } @@ -520,7 +524,7 @@ func (s *AccountTestService) testOpenAIAccountConnection(c *gin.Context, account proxyURL = account.Proxy.URL() } - resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled()) + resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account)) if err != nil { return s.sendErrorAndEnd(c, fmt.Sprintf("Request failed: %s", err.Error())) } @@ -610,7 +614,7 @@ func (s *AccountTestService) testGeminiAccountConnection(c *gin.Context, account proxyURL = account.Proxy.URL() } - resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled()) + resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account)) if err != nil { return s.sendErrorAndEnd(c, fmt.Sprintf("Request failed: %s", err.Error())) } @@ -881,9 +885,9 @@ func (s *AccountTestService) testSoraAccountConnection(c *gin.Context, account * if account.ProxyID != nil && account.Proxy != nil { proxyURL = account.Proxy.URL() } - enableSoraTLSFingerprint := s.shouldEnableSoraTLSFingerprint() + soraTLSProfile := s.resolveSoraTLSProfile() - resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, enableSoraTLSFingerprint) + resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, soraTLSProfile) if err != nil { recorder.addStep("me", "failed", 0, "network_error", err.Error()) s.emitSoraProbeSummary(c, recorder) @@ -948,7 +952,7 @@ func (s *AccountTestService) testSoraAccountConnection(c *gin.Context, account * subReq.Header.Set("Origin", "https://sora.chatgpt.com") subReq.Header.Set("Referer", "https://sora.chatgpt.com/") - subResp, subErr := s.httpUpstream.DoWithTLS(subReq, proxyURL, account.ID, account.Concurrency, enableSoraTLSFingerprint) + subResp, subErr := s.httpUpstream.DoWithTLS(subReq, proxyURL, account.ID, account.Concurrency, soraTLSProfile) if subErr != nil { recorder.addStep("subscription", "failed", 0, "network_error", subErr.Error()) s.sendEvent(c, TestEvent{Type: "content", Text: fmt.Sprintf("Subscription check skipped: %s", subErr.Error())}) @@ -977,7 +981,7 @@ func (s *AccountTestService) testSoraAccountConnection(c *gin.Context, account * } // 追加 Sora2 能力探测(对齐 sora2api 的测试思路):邀请码 + 剩余额度。 - s.testSora2Capabilities(c, ctx, account, authToken, proxyURL, enableSoraTLSFingerprint, recorder) + s.testSora2Capabilities(c, ctx, account, authToken, proxyURL, soraTLSProfile, recorder) s.emitSoraProbeSummary(c, recorder) s.sendEvent(c, TestEvent{Type: "test_complete", Success: true}) @@ -990,7 +994,7 @@ func (s *AccountTestService) testSora2Capabilities( account *Account, authToken string, proxyURL string, - enableTLSFingerprint bool, + tlsProfile *tlsfingerprint.Profile, recorder *soraProbeRecorder, ) { inviteStatus, inviteHeader, inviteBody, err := s.fetchSoraTestEndpoint( @@ -999,7 +1003,7 @@ func (s *AccountTestService) testSora2Capabilities( authToken, soraInviteMineURL, proxyURL, - enableTLSFingerprint, + tlsProfile, ) if err != nil { if recorder != nil { @@ -1016,7 +1020,7 @@ func (s *AccountTestService) testSora2Capabilities( authToken, soraBootstrapURL, proxyURL, - enableTLSFingerprint, + tlsProfile, ) if bootstrapErr == nil && bootstrapStatus == http.StatusOK { if recorder != nil { @@ -1029,7 +1033,7 @@ func (s *AccountTestService) testSora2Capabilities( authToken, soraInviteMineURL, proxyURL, - enableTLSFingerprint, + tlsProfile, ) if err != nil { if recorder != nil { @@ -1081,7 +1085,7 @@ func (s *AccountTestService) testSora2Capabilities( authToken, soraRemainingURL, proxyURL, - enableTLSFingerprint, + tlsProfile, ) if remainingErr != nil { if recorder != nil { @@ -1122,7 +1126,7 @@ func (s *AccountTestService) fetchSoraTestEndpoint( authToken string, url string, proxyURL string, - enableTLSFingerprint bool, + tlsProfile *tlsfingerprint.Profile, ) (int, http.Header, []byte, error) { req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { @@ -1135,7 +1139,7 @@ func (s *AccountTestService) fetchSoraTestEndpoint( req.Header.Set("Origin", "https://sora.chatgpt.com") req.Header.Set("Referer", "https://sora.chatgpt.com/") - resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, enableTLSFingerprint) + resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, tlsProfile) if err != nil { return 0, nil, nil, err } @@ -1224,11 +1228,12 @@ func parseSoraRemainingSummary(body []byte) string { return strings.Join(parts, " | ") } -func (s *AccountTestService) shouldEnableSoraTLSFingerprint() bool { - if s == nil || s.cfg == nil { - return true +func (s *AccountTestService) resolveSoraTLSProfile() *tlsfingerprint.Profile { + if s == nil || s.cfg == nil || !s.cfg.Sora.Client.DisableTLSFingerprint { + // Sora TLS fingerprint enabled — use built-in default profile + return &tlsfingerprint.Profile{Name: "Built-in Default (Sora)"} } - return !s.cfg.Sora.Client.DisableTLSFingerprint + return nil // disabled } func isCloudflareChallengeResponse(statusCode int, headers http.Header, body []byte) bool { diff --git a/backend/internal/service/account_test_service_sora_test.go b/backend/internal/service/account_test_service_sora_test.go index 3dfac786..52f832a9 100644 --- a/backend/internal/service/account_test_service_sora_test.go +++ b/backend/internal/service/account_test_service_sora_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/Wei-Shaw/sub2api/internal/config" + "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) @@ -24,9 +25,9 @@ func (u *queuedHTTPUpstream) Do(_ *http.Request, _ string, _ int64, _ int) (*htt return nil, fmt.Errorf("unexpected Do call") } -func (u *queuedHTTPUpstream) DoWithTLS(req *http.Request, _ string, _ int64, _ int, enableTLSFingerprint bool) (*http.Response, error) { +func (u *queuedHTTPUpstream) DoWithTLS(req *http.Request, _ string, _ int64, _ int, profile *tlsfingerprint.Profile) (*http.Response, error) { u.requests = append(u.requests, req) - u.tlsFlags = append(u.tlsFlags, enableTLSFingerprint) + u.tlsFlags = append(u.tlsFlags, profile != nil) if len(u.responses) == 0 { return nil, fmt.Errorf("no mocked response") } diff --git a/backend/internal/service/account_usage_service.go b/backend/internal/service/account_usage_service.go index 2761d9c8..0e5741d8 100644 --- a/backend/internal/service/account_usage_service.go +++ b/backend/internal/service/account_usage_service.go @@ -17,6 +17,7 @@ import ( openaipkg "github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/timezone" + "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats" "golang.org/x/sync/errgroup" "golang.org/x/sync/singleflight" @@ -241,11 +242,11 @@ type ClaudeUsageResponse struct { // ClaudeUsageFetchOptions 包含获取 Claude 用量数据所需的所有选项 type ClaudeUsageFetchOptions struct { - AccessToken string // OAuth access token - ProxyURL string // 代理 URL(可选) - AccountID int64 // 账号 ID(用于 TLS 指纹选择) - EnableTLSFingerprint bool // 是否启用 TLS 指纹伪装 - Fingerprint *Fingerprint // 缓存的指纹信息(User-Agent 等) + AccessToken string // OAuth access token + ProxyURL string // 代理 URL(可选) + AccountID int64 // 账号 ID(用于连接池隔离) + TLSProfile *tlsfingerprint.Profile // TLS 指纹 Profile(nil 表示不启用) + Fingerprint *Fingerprint // 缓存的指纹信息(User-Agent 等) } // ClaudeUsageFetcher fetches usage data from Anthropic OAuth API @@ -264,6 +265,7 @@ type AccountUsageService struct { antigravityQuotaFetcher *AntigravityQuotaFetcher cache *UsageCache identityCache IdentityCache + tlsFPProfileService *TLSFingerprintProfileService } // NewAccountUsageService 创建AccountUsageService实例 @@ -275,6 +277,7 @@ func NewAccountUsageService( antigravityQuotaFetcher *AntigravityQuotaFetcher, cache *UsageCache, identityCache IdentityCache, + tlsFPProfileService *TLSFingerprintProfileService, ) *AccountUsageService { return &AccountUsageService{ accountRepo: accountRepo, @@ -284,6 +287,7 @@ func NewAccountUsageService( antigravityQuotaFetcher: antigravityQuotaFetcher, cache: cache, identityCache: identityCache, + tlsFPProfileService: tlsFPProfileService, } } @@ -1155,10 +1159,10 @@ func (s *AccountUsageService) fetchOAuthUsageRaw(ctx context.Context, account *A // 构建完整的选项 opts := &ClaudeUsageFetchOptions{ - AccessToken: accessToken, - ProxyURL: proxyURL, - AccountID: account.ID, - EnableTLSFingerprint: account.IsTLSFingerprintEnabled(), + AccessToken: accessToken, + ProxyURL: proxyURL, + AccountID: account.ID, + TLSProfile: s.tlsFPProfileService.ResolveTLSProfile(account), } // 尝试获取缓存的 Fingerprint(包含 User-Agent 等信息) diff --git a/backend/internal/service/antigravity_gateway_service_test.go b/backend/internal/service/antigravity_gateway_service_test.go index f5f9434c..1eb1451e 100644 --- a/backend/internal/service/antigravity_gateway_service_test.go +++ b/backend/internal/service/antigravity_gateway_service_test.go @@ -15,6 +15,7 @@ import ( "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity" + "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) @@ -130,7 +131,7 @@ func (s *httpUpstreamStub) Do(_ *http.Request, _ string, _ int64, _ int) (*http. return s.resp, s.err } -func (s *httpUpstreamStub) DoWithTLS(_ *http.Request, _ string, _ int64, _ int, _ bool) (*http.Response, error) { +func (s *httpUpstreamStub) DoWithTLS(_ *http.Request, _ string, _ int64, _ int, _ *tlsfingerprint.Profile) (*http.Response, error) { return s.resp, s.err } @@ -171,7 +172,7 @@ func (s *queuedHTTPUpstreamStub) Do(req *http.Request, _ string, _ int64, _ int) return resp, err } -func (s *queuedHTTPUpstreamStub) DoWithTLS(req *http.Request, proxyURL string, accountID int64, concurrency int, _ bool) (*http.Response, error) { +func (s *queuedHTTPUpstreamStub) DoWithTLS(req *http.Request, proxyURL string, accountID int64, concurrency int, _ *tlsfingerprint.Profile) (*http.Response, error) { return s.Do(req, proxyURL, accountID, concurrency) } diff --git a/backend/internal/service/antigravity_rate_limit_test.go b/backend/internal/service/antigravity_rate_limit_test.go index df1ce9b9..35e130dc 100644 --- a/backend/internal/service/antigravity_rate_limit_test.go +++ b/backend/internal/service/antigravity_rate_limit_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity" + "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" "github.com/stretchr/testify/require" ) @@ -40,7 +41,7 @@ func (r *recordingOKUpstream) Do(req *http.Request, proxyURL string, accountID i }, nil } -func (r *recordingOKUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) { +func (r *recordingOKUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) { return r.Do(req, proxyURL, accountID, accountConcurrency) } @@ -61,7 +62,7 @@ func (s *stubAntigravityUpstream) Do(req *http.Request, proxyURL string, account }, nil } -func (s *stubAntigravityUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) { +func (s *stubAntigravityUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) { return s.Do(req, proxyURL, accountID, accountConcurrency) } diff --git a/backend/internal/service/antigravity_smart_retry_test.go b/backend/internal/service/antigravity_smart_retry_test.go index 218a1288..ecaffcbc 100644 --- a/backend/internal/service/antigravity_smart_retry_test.go +++ b/backend/internal/service/antigravity_smart_retry_test.go @@ -10,6 +10,7 @@ import ( "strings" "testing" + "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" "github.com/stretchr/testify/require" ) @@ -93,7 +94,7 @@ func (m *mockSmartRetryUpstream) Do(req *http.Request, proxyURL string, accountI }, respErr } -func (m *mockSmartRetryUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) { +func (m *mockSmartRetryUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) { return m.Do(req, proxyURL, accountID, accountConcurrency) } diff --git a/backend/internal/service/error_policy_integration_test.go b/backend/internal/service/error_policy_integration_test.go index a8b42a2c..aa3e6ec4 100644 --- a/backend/internal/service/error_policy_integration_test.go +++ b/backend/internal/service/error_policy_integration_test.go @@ -12,6 +12,7 @@ import ( "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity" + "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" "github.com/stretchr/testify/require" ) @@ -35,7 +36,7 @@ func (u *epFixedUpstream) Do(req *http.Request, proxyURL string, accountID int64 }, nil } -func (u *epFixedUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) { +func (u *epFixedUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) { return u.Do(req, proxyURL, accountID, accountConcurrency) } diff --git a/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go b/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go index f4e1b533..6e19db32 100644 --- a/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go +++ b/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go @@ -15,6 +15,7 @@ import ( "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/pkg/claude" + "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" @@ -60,7 +61,7 @@ func (u *anthropicHTTPUpstreamRecorder) Do(req *http.Request, proxyURL string, a return u.resp, nil } -func (u *anthropicHTTPUpstreamRecorder) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) { +func (u *anthropicHTTPUpstreamRecorder) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) { return u.Do(req, proxyURL, accountID, accountConcurrency) } diff --git a/backend/internal/service/gateway_forward_as_chat_completions.go b/backend/internal/service/gateway_forward_as_chat_completions.go index d3c611e2..37b38f76 100644 --- a/backend/internal/service/gateway_forward_as_chat_completions.go +++ b/backend/internal/service/gateway_forward_as_chat_completions.go @@ -120,7 +120,7 @@ func (s *GatewayService) ForwardAsChatCompletions( } // 11. Send request - resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled()) + resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account)) if err != nil { if resp != nil && resp.Body != nil { _ = resp.Body.Close() diff --git a/backend/internal/service/gateway_forward_as_responses.go b/backend/internal/service/gateway_forward_as_responses.go index 5dca57f9..2c917112 100644 --- a/backend/internal/service/gateway_forward_as_responses.go +++ b/backend/internal/service/gateway_forward_as_responses.go @@ -117,7 +117,7 @@ func (s *GatewayService) ForwardAsResponses( } // 11. Send request - resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled()) + resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account)) if err != nil { if resp != nil && resp.Body != nil { _ = resp.Body.Close() diff --git a/backend/internal/service/gateway_record_usage_test.go b/backend/internal/service/gateway_record_usage_test.go index 1b2f5f51..48488dc8 100644 --- a/backend/internal/service/gateway_record_usage_test.go +++ b/backend/internal/service/gateway_record_usage_test.go @@ -40,6 +40,7 @@ func newGatewayRecordUsageServiceForTest(usageRepo UsageLogRepository, userRepo nil, nil, nil, + nil, ) } diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 5de6dcae..cb90343b 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -566,6 +566,7 @@ type GatewayService struct { debugModelRouting atomic.Bool debugClaudeMimic atomic.Bool debugGatewayBodyFile atomic.Pointer[os.File] // non-nil when SUB2API_DEBUG_GATEWAY_BODY is set + tlsFPProfileService *TLSFingerprintProfileService } // NewGatewayService creates a new GatewayService @@ -592,6 +593,7 @@ func NewGatewayService( rpmCache RPMCache, digestStore *DigestSessionStore, settingService *SettingService, + tlsFPProfileService *TLSFingerprintProfileService, ) *GatewayService { userGroupRateTTL := resolveUserGroupRateCacheTTL(cfg) modelsListTTL := resolveModelsListCacheTTL(cfg) @@ -623,6 +625,7 @@ func NewGatewayService( modelsListCache: gocache.New(modelsListTTL, time.Minute), modelsListCacheTTL: modelsListTTL, responseHeaderFilter: compileResponseHeaderFilter(cfg), + tlsFPProfileService: tlsFPProfileService, } svc.userGroupRateResolver = newUserGroupRateResolver( userGroupRateRepo, @@ -4133,9 +4136,12 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A proxyURL = account.Proxy.URL() } + // 解析 TLS 指纹 profile(同一请求生命周期内不变,避免重试循环中重复解析) + tlsProfile := s.tlsFPProfileService.ResolveTLSProfile(account) + // 调试日志:记录即将转发的账号信息 logger.LegacyPrintf("service.gateway", "[Forward] Using account: ID=%d Name=%s Platform=%s Type=%s TLSFingerprint=%v Proxy=%s", - account.ID, account.Name, account.Platform, account.Type, account.IsTLSFingerprintEnabled(), proxyURL) + account.ID, account.Name, account.Platform, account.Type, tlsProfile, proxyURL) // Pre-filter: strip empty text blocks (including nested in tool_result) to prevent upstream 400. body = StripEmptyTextBlocks(body) @@ -4155,7 +4161,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A } // 发送请求 - resp, err = s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled()) + resp, err = s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, tlsProfile) if err != nil { if resp != nil && resp.Body != nil { _ = resp.Body.Close() @@ -4233,7 +4239,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A retryReq, buildErr := s.buildUpstreamRequest(retryCtx, c, account, filteredBody, token, tokenType, reqModel, reqStream, shouldMimicClaudeCode) releaseRetryCtx() if buildErr == nil { - retryResp, retryErr := s.httpUpstream.DoWithTLS(retryReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled()) + retryResp, retryErr := s.httpUpstream.DoWithTLS(retryReq, proxyURL, account.ID, account.Concurrency, tlsProfile) if retryErr == nil { if retryResp.StatusCode < 400 { logger.LegacyPrintf("service.gateway", "Account %d: thinking block retry succeeded (blocks downgraded)", account.ID) @@ -4268,7 +4274,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A retryReq2, buildErr2 := s.buildUpstreamRequest(retryCtx2, c, account, filteredBody2, token, tokenType, reqModel, reqStream, shouldMimicClaudeCode) releaseRetryCtx2() if buildErr2 == nil { - retryResp2, retryErr2 := s.httpUpstream.DoWithTLS(retryReq2, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled()) + retryResp2, retryErr2 := s.httpUpstream.DoWithTLS(retryReq2, proxyURL, account.ID, account.Concurrency, tlsProfile) if retryErr2 == nil { resp = retryResp2 break @@ -4339,7 +4345,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A budgetRetryReq, buildErr := s.buildUpstreamRequest(budgetRetryCtx, c, account, rectifiedBody, token, tokenType, reqModel, reqStream, shouldMimicClaudeCode) releaseBudgetRetryCtx() if buildErr == nil { - budgetRetryResp, retryErr := s.httpUpstream.DoWithTLS(budgetRetryReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled()) + budgetRetryResp, retryErr := s.httpUpstream.DoWithTLS(budgetRetryReq, proxyURL, account.ID, account.Concurrency, tlsProfile) if retryErr == nil { resp = budgetRetryResp break @@ -4645,7 +4651,7 @@ func (s *GatewayService) forwardAnthropicAPIKeyPassthroughWithInput( return nil, err } - resp, err = s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled()) + resp, err = s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account)) if err != nil { if resp != nil && resp.Body != nil { _ = resp.Body.Close() @@ -5364,7 +5370,7 @@ func (s *GatewayService) executeBedrockUpstream( return nil, err } - resp, err = s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, false) + resp, err = s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, nil) if err != nil { if resp != nil && resp.Body != nil { _ = resp.Body.Close() @@ -8044,7 +8050,7 @@ func (s *GatewayService) ForwardCountTokens(ctx context.Context, c *gin.Context, } // 发送请求 - resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled()) + resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account)) if err != nil { setOpsUpstreamError(c, 0, sanitizeUpstreamErrorMessage(err.Error()), "") s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Request failed") @@ -8072,7 +8078,7 @@ func (s *GatewayService) ForwardCountTokens(ctx context.Context, c *gin.Context, filteredBody := FilterThinkingBlocksForRetry(body) retryReq, buildErr := s.buildCountTokensRequest(ctx, c, account, filteredBody, token, tokenType, reqModel, shouldMimicClaudeCode) if buildErr == nil { - retryResp, retryErr := s.httpUpstream.DoWithTLS(retryReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled()) + retryResp, retryErr := s.httpUpstream.DoWithTLS(retryReq, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account)) if retryErr == nil { resp = retryResp respBody, err = readUpstreamResponseBodyLimited(resp.Body, maxReadBytes) @@ -8161,7 +8167,7 @@ func (s *GatewayService) forwardCountTokensAnthropicAPIKeyPassthrough(ctx contex proxyURL = account.Proxy.URL() } - resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled()) + resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account)) if err != nil { setOpsUpstreamError(c, 0, sanitizeUpstreamErrorMessage(err.Error()), "") appendOpsUpstreamError(c, OpsUpstreamErrorEvent{ diff --git a/backend/internal/service/gemini_messages_compat_service_test.go b/backend/internal/service/gemini_messages_compat_service_test.go index 17f7e74e..f659f0e6 100644 --- a/backend/internal/service/gemini_messages_compat_service_test.go +++ b/backend/internal/service/gemini_messages_compat_service_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/Wei-Shaw/sub2api/internal/config" + "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) @@ -36,7 +37,7 @@ func (s *geminiCompatHTTPUpstreamStub) Do(req *http.Request, proxyURL string, ac return &resp, nil } -func (s *geminiCompatHTTPUpstreamStub) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) { +func (s *geminiCompatHTTPUpstreamStub) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) { return s.Do(req, proxyURL, accountID, accountConcurrency) } diff --git a/backend/internal/service/http_upstream_port.go b/backend/internal/service/http_upstream_port.go index 0e4cfbec..e8e76957 100644 --- a/backend/internal/service/http_upstream_port.go +++ b/backend/internal/service/http_upstream_port.go @@ -1,55 +1,24 @@ package service -import "net/http" +import ( + "net/http" + + "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" +) // HTTPUpstream 上游 HTTP 请求接口 // 用于向上游 API(Claude、OpenAI、Gemini 等)发送请求 -// 这是一个通用接口,可用于任何基于 HTTP 的上游服务 -// -// 设计说明: -// - 支持可选代理配置 -// - 支持账户级连接池隔离 -// - 实现类负责连接池管理和复用 -// - 支持可选的 TLS 指纹伪装 type HTTPUpstream interface { - // Do 执行 HTTP 请求 - // - // 参数: - // - req: HTTP 请求对象,由调用方构建 - // - proxyURL: 代理服务器地址,空字符串表示直连 - // - accountID: 账户 ID,用于连接池隔离(隔离策略为 account 或 account_proxy 时生效) - // - accountConcurrency: 账户并发限制,用于动态调整连接池大小 - // - // 返回: - // - *http.Response: HTTP 响应,调用方必须关闭 Body - // - error: 请求错误(网络错误、超时等) - // - // 注意: - // - 调用方必须关闭 resp.Body,否则会导致连接泄漏 - // - 响应体可能已被包装以跟踪请求生命周期 + // Do 执行 HTTP 请求(不启用 TLS 指纹) Do(req *http.Request, proxyURL string, accountID int64, accountConcurrency int) (*http.Response, error) // DoWithTLS 执行带 TLS 指纹伪装的 HTTP 请求 // - // 参数: - // - req: HTTP 请求对象,由调用方构建 - // - proxyURL: 代理服务器地址,空字符串表示直连 - // - accountID: 账户 ID,用于连接池隔离和 TLS 指纹模板选择 - // - accountConcurrency: 账户并发限制,用于动态调整连接池大小 - // - enableTLSFingerprint: 是否启用 TLS 指纹伪装 + // profile 参数: + // - nil: 不启用 TLS 指纹,行为与 Do 方法相同 + // - non-nil: 使用指定的 Profile 进行 TLS 指纹伪装 // - // 返回: - // - *http.Response: HTTP 响应,调用方必须关闭 Body - // - error: 请求错误(网络错误、超时等) - // - // TLS 指纹说明: - // - 当 enableTLSFingerprint=true 时,使用 utls 库模拟 Claude CLI 的 TLS 指纹 - // - TLS 指纹模板根据 accountID % len(profiles) 自动选择 - // - 支持直连、HTTP/HTTPS 代理、SOCKS5 代理三种场景 - // - 如果 enableTLSFingerprint=false,行为与 Do 方法相同 - // - // 注意: - // - 调用方必须关闭 resp.Body,否则会导致连接泄漏 - // - TLS 指纹客户端与普通客户端使用不同的缓存键,互不影响 - DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) + // Profile 由调用方通过 TLSFingerprintProfileService 解析后传入, + // 支持按账号绑定的数据库 profile 或内置默认 profile。 + DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) } diff --git a/backend/internal/service/openai_oauth_passthrough_test.go b/backend/internal/service/openai_oauth_passthrough_test.go index fe639576..97fa218d 100644 --- a/backend/internal/service/openai_oauth_passthrough_test.go +++ b/backend/internal/service/openai_oauth_passthrough_test.go @@ -14,6 +14,7 @@ import ( "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/pkg/logger" + "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" @@ -43,7 +44,7 @@ func (u *httpUpstreamRecorder) Do(req *http.Request, proxyURL string, accountID return u.resp, nil } -func (u *httpUpstreamRecorder) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) { +func (u *httpUpstreamRecorder) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) { return u.Do(req, proxyURL, accountID, accountConcurrency) } diff --git a/backend/internal/service/openai_ws_protocol_forward_test.go b/backend/internal/service/openai_ws_protocol_forward_test.go index 76c66f2f..8c5c9368 100644 --- a/backend/internal/service/openai_ws_protocol_forward_test.go +++ b/backend/internal/service/openai_ws_protocol_forward_test.go @@ -14,6 +14,7 @@ import ( "time" "github.com/Wei-Shaw/sub2api/internal/config" + "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" "github.com/stretchr/testify/require" @@ -57,7 +58,7 @@ func (u *httpUpstreamSequenceRecorder) Do(req *http.Request, proxyURL string, ac return u.responses[len(u.responses)-1], nil } -func (u *httpUpstreamSequenceRecorder) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) { +func (u *httpUpstreamSequenceRecorder) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) { return u.Do(req, proxyURL, accountID, accountConcurrency) } diff --git a/backend/internal/service/tls_fingerprint_profile_service.go b/backend/internal/service/tls_fingerprint_profile_service.go new file mode 100644 index 00000000..33937cc7 --- /dev/null +++ b/backend/internal/service/tls_fingerprint_profile_service.go @@ -0,0 +1,259 @@ +package service + +import ( + "context" + "math/rand/v2" + "sync" + "time" + + "github.com/Wei-Shaw/sub2api/internal/model" + "github.com/Wei-Shaw/sub2api/internal/pkg/logger" + "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" +) + +// TLSFingerprintProfileRepository 定义 TLS 指纹模板的数据访问接口 +type TLSFingerprintProfileRepository interface { + List(ctx context.Context) ([]*model.TLSFingerprintProfile, error) + GetByID(ctx context.Context, id int64) (*model.TLSFingerprintProfile, error) + Create(ctx context.Context, profile *model.TLSFingerprintProfile) (*model.TLSFingerprintProfile, error) + Update(ctx context.Context, profile *model.TLSFingerprintProfile) (*model.TLSFingerprintProfile, error) + Delete(ctx context.Context, id int64) error +} + +// TLSFingerprintProfileCache 定义 TLS 指纹模板的缓存接口 +type TLSFingerprintProfileCache interface { + Get(ctx context.Context) ([]*model.TLSFingerprintProfile, bool) + Set(ctx context.Context, profiles []*model.TLSFingerprintProfile) error + Invalidate(ctx context.Context) error + NotifyUpdate(ctx context.Context) error + SubscribeUpdates(ctx context.Context, handler func()) +} + +// TLSFingerprintProfileService TLS 指纹模板管理服务 +type TLSFingerprintProfileService struct { + repo TLSFingerprintProfileRepository + cache TLSFingerprintProfileCache + + // 本地 ID→Profile 映射缓存,用于 DoWithTLS 热路径快速查找 + localCache map[int64]*model.TLSFingerprintProfile + localMu sync.RWMutex +} + +// NewTLSFingerprintProfileService 创建 TLS 指纹模板服务 +func NewTLSFingerprintProfileService( + repo TLSFingerprintProfileRepository, + cache TLSFingerprintProfileCache, +) *TLSFingerprintProfileService { + svc := &TLSFingerprintProfileService{ + repo: repo, + cache: cache, + localCache: make(map[int64]*model.TLSFingerprintProfile), + } + + ctx := context.Background() + if err := svc.reloadFromDB(ctx); err != nil { + logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to load profiles from DB on startup: %v", err) + if fallbackErr := svc.refreshLocalCache(ctx); fallbackErr != nil { + logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to load profiles from cache fallback on startup: %v", fallbackErr) + } + } + + if cache != nil { + cache.SubscribeUpdates(ctx, func() { + if err := svc.refreshLocalCache(context.Background()); err != nil { + logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to refresh cache on notification: %v", err) + } + }) + } + + return svc +} + +// --- CRUD --- + +// List 获取所有模板 +func (s *TLSFingerprintProfileService) List(ctx context.Context) ([]*model.TLSFingerprintProfile, error) { + return s.repo.List(ctx) +} + +// GetByID 根据 ID 获取模板 +func (s *TLSFingerprintProfileService) GetByID(ctx context.Context, id int64) (*model.TLSFingerprintProfile, error) { + return s.repo.GetByID(ctx, id) +} + +// Create 创建模板 +func (s *TLSFingerprintProfileService) Create(ctx context.Context, profile *model.TLSFingerprintProfile) (*model.TLSFingerprintProfile, error) { + if err := profile.Validate(); err != nil { + return nil, err + } + + created, err := s.repo.Create(ctx, profile) + if err != nil { + return nil, err + } + + refreshCtx, cancel := s.newCacheRefreshContext() + defer cancel() + s.invalidateAndNotify(refreshCtx) + + return created, nil +} + +// Update 更新模板 +func (s *TLSFingerprintProfileService) Update(ctx context.Context, profile *model.TLSFingerprintProfile) (*model.TLSFingerprintProfile, error) { + if err := profile.Validate(); err != nil { + return nil, err + } + + updated, err := s.repo.Update(ctx, profile) + if err != nil { + return nil, err + } + + refreshCtx, cancel := s.newCacheRefreshContext() + defer cancel() + s.invalidateAndNotify(refreshCtx) + + return updated, nil +} + +// Delete 删除模板 +func (s *TLSFingerprintProfileService) Delete(ctx context.Context, id int64) error { + if err := s.repo.Delete(ctx, id); err != nil { + return err + } + + refreshCtx, cancel := s.newCacheRefreshContext() + defer cancel() + s.invalidateAndNotify(refreshCtx) + + return nil +} + +// --- 热路径:运行时 Profile 查找 --- + +// GetProfileByID 根据 ID 从本地缓存获取 Profile(用于 DoWithTLS 热路径) +// 返回 nil 表示未找到,调用方应 fallback 到内置默认 Profile +func (s *TLSFingerprintProfileService) GetProfileByID(id int64) *tlsfingerprint.Profile { + s.localMu.RLock() + p, ok := s.localCache[id] + s.localMu.RUnlock() + + if ok && p != nil { + return p.ToTLSProfile() + } + return nil +} + +// getRandomProfile 从本地缓存中随机选择一个 Profile +func (s *TLSFingerprintProfileService) getRandomProfile() *tlsfingerprint.Profile { + s.localMu.RLock() + defer s.localMu.RUnlock() + + if len(s.localCache) == 0 { + return nil + } + + // 收集所有 profile + profiles := make([]*model.TLSFingerprintProfile, 0, len(s.localCache)) + for _, p := range s.localCache { + if p != nil { + profiles = append(profiles, p) + } + } + if len(profiles) == 0 { + return nil + } + + return profiles[rand.IntN(len(profiles))].ToTLSProfile() +} + +// ResolveTLSProfile 根据 Account 的配置解析出运行时 TLS Profile +// +// 逻辑: +// 1. 未启用 TLS 指纹 → 返回 nil(不伪装) +// 2. 启用 + 绑定了 profile_id → 从缓存查找对应 profile +// 3. 启用 + 未绑定或找不到 → 返回空 Profile(使用代码内置默认值) +func (s *TLSFingerprintProfileService) ResolveTLSProfile(account *Account) *tlsfingerprint.Profile { + if account == nil || !account.IsTLSFingerprintEnabled() { + return nil + } + id := account.GetTLSFingerprintProfileID() + if id > 0 { + if p := s.GetProfileByID(id); p != nil { + return p + } + } + if id == -1 { + // 随机选择一个 profile + if p := s.getRandomProfile(); p != nil { + return p + } + } + // TLS 启用但无绑定 profile → 空 Profile → dialer 使用内置默认值 + return &tlsfingerprint.Profile{Name: "Built-in Default (Node.js 24.x)"} +} + +// --- 缓存管理 --- + +func (s *TLSFingerprintProfileService) refreshLocalCache(ctx context.Context) error { + if s.cache != nil { + if profiles, ok := s.cache.Get(ctx); ok { + s.setLocalCache(profiles) + return nil + } + } + return s.reloadFromDB(ctx) +} + +func (s *TLSFingerprintProfileService) reloadFromDB(ctx context.Context) error { + profiles, err := s.repo.List(ctx) + if err != nil { + return err + } + + if s.cache != nil { + if err := s.cache.Set(ctx, profiles); err != nil { + logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to set cache: %v", err) + } + } + + s.setLocalCache(profiles) + return nil +} + +func (s *TLSFingerprintProfileService) setLocalCache(profiles []*model.TLSFingerprintProfile) { + m := make(map[int64]*model.TLSFingerprintProfile, len(profiles)) + for _, p := range profiles { + m[p.ID] = p + } + + s.localMu.Lock() + s.localCache = m + s.localMu.Unlock() +} + +func (s *TLSFingerprintProfileService) newCacheRefreshContext() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), 3*time.Second) +} + +func (s *TLSFingerprintProfileService) invalidateAndNotify(ctx context.Context) { + if s.cache != nil { + if err := s.cache.Invalidate(ctx); err != nil { + logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to invalidate cache: %v", err) + } + } + + if err := s.reloadFromDB(ctx); err != nil { + logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to refresh local cache: %v", err) + s.localMu.Lock() + s.localCache = make(map[int64]*model.TLSFingerprintProfile) + s.localMu.Unlock() + } + + if s.cache != nil { + if err := s.cache.NotifyUpdate(ctx); err != nil { + logger.LegacyPrintf("service.tls_fp_profile", "[TLSFPProfileService] Failed to notify cache update: %v", err) + } + } +} diff --git a/backend/internal/service/wire.go b/backend/internal/service/wire.go index fca8fd6d..d79a3531 100644 --- a/backend/internal/service/wire.go +++ b/backend/internal/service/wire.go @@ -482,6 +482,7 @@ var ProviderSet = wire.NewSet( NewUsageCache, NewTotpService, NewErrorPassthroughService, + NewTLSFingerprintProfileService, NewDigestSessionStore, ProvideIdempotencyCoordinator, ProvideSystemOperationLockService, diff --git a/backend/migrations/080_create_tls_fingerprint_profiles.sql b/backend/migrations/080_create_tls_fingerprint_profiles.sql new file mode 100644 index 00000000..c13c21f8 --- /dev/null +++ b/backend/migrations/080_create_tls_fingerprint_profiles.sql @@ -0,0 +1,29 @@ +-- Create tls_fingerprint_profiles table for managing TLS fingerprint templates. +-- Each profile contains ClientHello parameters to simulate specific client TLS handshake characteristics. + +SET LOCAL lock_timeout = '5s'; +SET LOCAL statement_timeout = '10min'; + +CREATE TABLE IF NOT EXISTS tls_fingerprint_profiles ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE, + description TEXT, + enable_grease BOOLEAN NOT NULL DEFAULT false, + cipher_suites JSONB, + curves JSONB, + point_formats JSONB, + signature_algorithms JSONB, + alpn_protocols JSONB, + supported_versions JSONB, + key_share_groups JSONB, + psk_modes JSONB, + extensions JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +COMMENT ON TABLE tls_fingerprint_profiles IS 'TLS fingerprint templates for simulating specific client TLS handshake characteristics'; +COMMENT ON COLUMN tls_fingerprint_profiles.name IS 'Unique profile name, e.g. "macOS Node.js v24"'; +COMMENT ON COLUMN tls_fingerprint_profiles.enable_grease IS 'Whether to insert GREASE values in ClientHello extensions'; +COMMENT ON COLUMN tls_fingerprint_profiles.cipher_suites IS 'TLS cipher suite list as JSON array of uint16 (order-sensitive, affects JA3)'; +COMMENT ON COLUMN tls_fingerprint_profiles.extensions IS 'TLS extension type IDs in send order as JSON array of uint16'; diff --git a/frontend/src/api/admin/index.ts b/frontend/src/api/admin/index.ts index a6ebfc2c..9a3fb8c5 100644 --- a/frontend/src/api/admin/index.ts +++ b/frontend/src/api/admin/index.ts @@ -24,6 +24,7 @@ import dataManagementAPI from './dataManagement' import apiKeysAPI from './apiKeys' import scheduledTestsAPI from './scheduledTests' import backupAPI from './backup' +import tlsFingerprintProfileAPI from './tlsFingerprintProfile' /** * Unified admin API object for convenient access @@ -49,7 +50,8 @@ export const adminAPI = { dataManagement: dataManagementAPI, apiKeys: apiKeysAPI, scheduledTests: scheduledTestsAPI, - backup: backupAPI + backup: backupAPI, + tlsFingerprintProfiles: tlsFingerprintProfileAPI } export { @@ -73,7 +75,8 @@ export { dataManagementAPI, apiKeysAPI, scheduledTestsAPI, - backupAPI + backupAPI, + tlsFingerprintProfileAPI } export default adminAPI @@ -82,3 +85,4 @@ export default adminAPI export type { BalanceHistoryItem } from './users' export type { ErrorPassthroughRule, CreateRuleRequest, UpdateRuleRequest } from './errorPassthrough' export type { BackupAgentHealth, DataManagementConfig } from './dataManagement' +export type { TLSFingerprintProfile, CreateProfileRequest, UpdateProfileRequest } from './tlsFingerprintProfile' diff --git a/frontend/src/api/admin/tlsFingerprintProfile.ts b/frontend/src/api/admin/tlsFingerprintProfile.ts new file mode 100644 index 00000000..f6a26dd5 --- /dev/null +++ b/frontend/src/api/admin/tlsFingerprintProfile.ts @@ -0,0 +1,98 @@ +/** + * Admin TLS Fingerprint Profile API endpoints + * Handles TLS fingerprint profile CRUD for administrators + */ + +import { apiClient } from '../client' + +/** + * TLS fingerprint profile interface + */ +export interface TLSFingerprintProfile { + id: number + name: string + description: string | null + enable_grease: boolean + cipher_suites: number[] + curves: number[] + point_formats: number[] + signature_algorithms: number[] + alpn_protocols: string[] + supported_versions: number[] + key_share_groups: number[] + psk_modes: number[] + extensions: number[] + created_at: string + updated_at: string +} + +/** + * Create profile request + */ +export interface CreateProfileRequest { + name: string + description?: string | null + enable_grease?: boolean + cipher_suites?: number[] + curves?: number[] + point_formats?: number[] + signature_algorithms?: number[] + alpn_protocols?: string[] + supported_versions?: number[] + key_share_groups?: number[] + psk_modes?: number[] + extensions?: number[] +} + +/** + * Update profile request + */ +export interface UpdateProfileRequest { + name?: string + description?: string | null + enable_grease?: boolean + cipher_suites?: number[] + curves?: number[] + point_formats?: number[] + signature_algorithms?: number[] + alpn_protocols?: string[] + supported_versions?: number[] + key_share_groups?: number[] + psk_modes?: number[] + extensions?: number[] +} + +export async function list(): Promise { + const { data } = await apiClient.get('/admin/tls-fingerprint-profiles') + return data +} + +export async function getById(id: number): Promise { + const { data } = await apiClient.get(`/admin/tls-fingerprint-profiles/${id}`) + return data +} + +export async function create(profileData: CreateProfileRequest): Promise { + const { data } = await apiClient.post('/admin/tls-fingerprint-profiles', profileData) + return data +} + +export async function update(id: number, updates: UpdateProfileRequest): Promise { + const { data } = await apiClient.put(`/admin/tls-fingerprint-profiles/${id}`, updates) + return data +} + +export async function deleteProfile(id: number): Promise<{ message: string }> { + const { data } = await apiClient.delete<{ message: string }>(`/admin/tls-fingerprint-profiles/${id}`) + return data +} + +export const tlsFingerprintProfileAPI = { + list, + getById, + create, + update, + delete: deleteProfile +} + +export default tlsFingerprintProfileAPI diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index cff7ae1c..806b57db 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -2169,6 +2169,14 @@ /> + +
+ +
@@ -3082,6 +3090,8 @@ const umqModeOptions = computed(() => [ { value: 'serialize', label: t('admin.accounts.quotaControl.rpmLimit.umqModeSerialize') }, ]) const tlsFingerprintEnabled = ref(false) +const tlsFingerprintProfileId = ref(null) +const tlsFingerprintProfiles = ref<{ id: number; name: string }[]>([]) const sessionIdMaskingEnabled = ref(false) const cacheTTLOverrideEnabled = ref(false) const cacheTTLOverrideTarget = ref('5m') @@ -3247,6 +3257,10 @@ watch( () => props.show, (newVal) => { if (newVal) { + // Load TLS fingerprint profiles + adminAPI.tlsFingerprintProfiles.list() + .then(profiles => { tlsFingerprintProfiles.value = profiles.map(p => ({ id: p.id, name: p.name })) }) + .catch(() => { tlsFingerprintProfiles.value = [] }) // Modal opened - fill related models allowedModels.value = [...getModelsByPlatform(form.platform)] // Antigravity: 默认使用映射模式并填充默认映射 @@ -3747,6 +3761,7 @@ const resetForm = () => { rpmStickyBuffer.value = null userMsgQueueMode.value = '' tlsFingerprintEnabled.value = false + tlsFingerprintProfileId.value = null sessionIdMaskingEnabled.value = false cacheTTLOverrideEnabled.value = false cacheTTLOverrideTarget.value = '5m' @@ -4825,6 +4840,9 @@ const handleAnthropicExchange = async (authCode: string) => { // Add TLS fingerprint settings if (tlsFingerprintEnabled.value) { extra.enable_tls_fingerprint = true + if (tlsFingerprintProfileId.value) { + extra.tls_fingerprint_profile_id = tlsFingerprintProfileId.value + } } // Add session ID masking settings @@ -4940,6 +4958,9 @@ const handleCookieAuth = async (sessionKey: string) => { // Add TLS fingerprint settings if (tlsFingerprintEnabled.value) { extra.enable_tls_fingerprint = true + if (tlsFingerprintProfileId.value) { + extra.tls_fingerprint_profile_id = tlsFingerprintProfileId.value + } } // Add session ID masking settings diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index 5f3da1b7..da6c9715 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -1504,6 +1504,14 @@ /> + +
+ +
@@ -1841,6 +1849,8 @@ const umqModeOptions = computed(() => [ { value: 'serialize', label: t('admin.accounts.quotaControl.rpmLimit.umqModeSerialize') }, ]) const tlsFingerprintEnabled = ref(false) +const tlsFingerprintProfileId = ref(null) +const tlsFingerprintProfiles = ref<{ id: number; name: string }[]>([]) const sessionIdMaskingEnabled = ref(false) const cacheTTLOverrideEnabled = ref(false) const cacheTTLOverrideTarget = ref('5m') @@ -2255,11 +2265,21 @@ watch( } if (!wasShow || newAccount !== previousAccount) { syncFormFromAccount(newAccount) + loadTLSProfiles() } }, { immediate: true } ) +const loadTLSProfiles = async () => { + try { + const profiles = await adminAPI.tlsFingerprintProfiles.list() + tlsFingerprintProfiles.value = profiles.map(p => ({ id: p.id, name: p.name })) + } catch { + tlsFingerprintProfiles.value = [] + } +} + // Model mapping helpers const addModelMapping = () => { modelMappings.value.push({ from: '', to: '' }) @@ -2458,6 +2478,7 @@ function loadQuotaControlSettings(account: Account) { rpmStickyBuffer.value = null userMsgQueueMode.value = '' tlsFingerprintEnabled.value = false + tlsFingerprintProfileId.value = null sessionIdMaskingEnabled.value = false cacheTTLOverrideEnabled.value = false cacheTTLOverrideTarget.value = '5m' @@ -2495,6 +2516,7 @@ function loadQuotaControlSettings(account: Account) { if (account.enable_tls_fingerprint === true) { tlsFingerprintEnabled.value = true } + tlsFingerprintProfileId.value = account.tls_fingerprint_profile_id ?? null // Load session ID masking setting if (account.session_id_masking_enabled === true) { @@ -2932,8 +2954,14 @@ const handleSubmit = async () => { // TLS fingerprint setting if (tlsFingerprintEnabled.value) { newExtra.enable_tls_fingerprint = true + if (tlsFingerprintProfileId.value) { + newExtra.tls_fingerprint_profile_id = tlsFingerprintProfileId.value + } else { + delete newExtra.tls_fingerprint_profile_id + } } else { delete newExtra.enable_tls_fingerprint + delete newExtra.tls_fingerprint_profile_id } // Session ID masking setting diff --git a/frontend/src/components/admin/TLSFingerprintProfilesModal.vue b/frontend/src/components/admin/TLSFingerprintProfilesModal.vue new file mode 100644 index 00000000..69465346 --- /dev/null +++ b/frontend/src/components/admin/TLSFingerprintProfilesModal.vue @@ -0,0 +1,625 @@ +