feat(tls-fingerprint): 新增 TLS 指纹 Profile 数据库管理及代码质量优化
新增功能: - 新增 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 回调签名被错误替换的问题
This commit is contained in:
parent
d0a242df03
commit
574fa9dfbd
@ -137,14 +137,17 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
antigravityQuotaFetcher := service.NewAntigravityQuotaFetcher(proxyRepository)
|
antigravityQuotaFetcher := service.NewAntigravityQuotaFetcher(proxyRepository)
|
||||||
usageCache := service.NewUsageCache()
|
usageCache := service.NewUsageCache()
|
||||||
identityCache := repository.NewIdentityCache(redisClient)
|
identityCache := repository.NewIdentityCache(redisClient)
|
||||||
accountUsageService := service.NewAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher, geminiQuotaService, antigravityQuotaFetcher, usageCache, identityCache)
|
|
||||||
geminiTokenProvider := service.ProvideGeminiTokenProvider(accountRepository, geminiTokenCache, geminiOAuthService, oauthRefreshAPI)
|
geminiTokenProvider := service.ProvideGeminiTokenProvider(accountRepository, geminiTokenCache, geminiOAuthService, oauthRefreshAPI)
|
||||||
gatewayCache := repository.NewGatewayCache(redisClient)
|
gatewayCache := repository.NewGatewayCache(redisClient)
|
||||||
schedulerOutboxRepository := repository.NewSchedulerOutboxRepository(db)
|
schedulerOutboxRepository := repository.NewSchedulerOutboxRepository(db)
|
||||||
schedulerSnapshotService := service.ProvideSchedulerSnapshotService(schedulerCache, schedulerOutboxRepository, accountRepository, groupRepository, configConfig)
|
schedulerSnapshotService := service.ProvideSchedulerSnapshotService(schedulerCache, schedulerOutboxRepository, accountRepository, groupRepository, configConfig)
|
||||||
antigravityTokenProvider := service.ProvideAntigravityTokenProvider(accountRepository, geminiTokenCache, antigravityOAuthService, oauthRefreshAPI, tempUnschedCache)
|
antigravityTokenProvider := service.ProvideAntigravityTokenProvider(accountRepository, geminiTokenCache, antigravityOAuthService, oauthRefreshAPI, tempUnschedCache)
|
||||||
antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, schedulerSnapshotService, antigravityTokenProvider, rateLimitService, httpUpstream, settingService)
|
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)
|
crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService, geminiOAuthService, configConfig)
|
||||||
sessionLimitCache := repository.ProvideSessionLimitCache(redisClient, configConfig)
|
sessionLimitCache := repository.ProvideSessionLimitCache(redisClient, configConfig)
|
||||||
rpmCache := repository.NewRPMCache(redisClient)
|
rpmCache := repository.NewRPMCache(redisClient)
|
||||||
@ -176,7 +179,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
deferredService := service.ProvideDeferredService(accountRepository, timingWheelService)
|
deferredService := service.ProvideDeferredService(accountRepository, timingWheelService)
|
||||||
claudeTokenProvider := service.ProvideClaudeTokenProvider(accountRepository, geminiTokenCache, oAuthService, oauthRefreshAPI)
|
claudeTokenProvider := service.ProvideClaudeTokenProvider(accountRepository, geminiTokenCache, oAuthService, oauthRefreshAPI)
|
||||||
digestSessionStore := service.NewDigestSessionStore()
|
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)
|
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)
|
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)
|
geminiMessagesCompatService := service.NewGeminiMessagesCompatService(accountRepository, groupRepository, gatewayCache, schedulerSnapshotService, geminiTokenProvider, rateLimitService, httpUpstream, antigravityGatewayService, configConfig)
|
||||||
@ -208,12 +211,13 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
errorPassthroughCache := repository.NewErrorPassthroughCache(redisClient)
|
errorPassthroughCache := repository.NewErrorPassthroughCache(redisClient)
|
||||||
errorPassthroughService := service.NewErrorPassthroughService(errorPassthroughRepository, errorPassthroughCache)
|
errorPassthroughService := service.NewErrorPassthroughService(errorPassthroughRepository, errorPassthroughCache)
|
||||||
errorPassthroughHandler := admin.NewErrorPassthroughHandler(errorPassthroughService)
|
errorPassthroughHandler := admin.NewErrorPassthroughHandler(errorPassthroughService)
|
||||||
|
tlsFingerprintProfileHandler := admin.NewTLSFingerprintProfileHandler(tlsFingerprintProfileService)
|
||||||
adminAPIKeyHandler := admin.NewAdminAPIKeyHandler(adminService)
|
adminAPIKeyHandler := admin.NewAdminAPIKeyHandler(adminService)
|
||||||
scheduledTestPlanRepository := repository.NewScheduledTestPlanRepository(db)
|
scheduledTestPlanRepository := repository.NewScheduledTestPlanRepository(db)
|
||||||
scheduledTestResultRepository := repository.NewScheduledTestResultRepository(db)
|
scheduledTestResultRepository := repository.NewScheduledTestResultRepository(db)
|
||||||
scheduledTestService := service.ProvideScheduledTestService(scheduledTestPlanRepository, scheduledTestResultRepository)
|
scheduledTestService := service.ProvideScheduledTestService(scheduledTestPlanRepository, scheduledTestResultRepository)
|
||||||
scheduledTestHandler := admin.NewScheduledTestHandler(scheduledTestService)
|
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)
|
usageRecordWorkerPool := service.NewUsageRecordWorkerPool(configConfig)
|
||||||
userMsgQueueCache := repository.NewUserMsgQueueCache(redisClient)
|
userMsgQueueCache := repository.NewUserMsgQueueCache(redisClient)
|
||||||
userMessageQueueService := service.ProvideUserMessageQueueService(userMsgQueueCache, rpmCache, configConfig)
|
userMessageQueueService := service.ProvideUserMessageQueueService(userMsgQueueCache, rpmCache, configConfig)
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import (
|
|||||||
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
|
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/setting"
|
"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/usagecleanuptask"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/usagelog"
|
"github.com/Wei-Shaw/sub2api/ent/usagelog"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/user"
|
"github.com/Wei-Shaw/sub2api/ent/user"
|
||||||
@ -73,6 +74,8 @@ type Client struct {
|
|||||||
SecuritySecret *SecuritySecretClient
|
SecuritySecret *SecuritySecretClient
|
||||||
// Setting is the client for interacting with the Setting builders.
|
// Setting is the client for interacting with the Setting builders.
|
||||||
Setting *SettingClient
|
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 is the client for interacting with the UsageCleanupTask builders.
|
||||||
UsageCleanupTask *UsageCleanupTaskClient
|
UsageCleanupTask *UsageCleanupTaskClient
|
||||||
// UsageLog is the client for interacting with the UsageLog builders.
|
// UsageLog is the client for interacting with the UsageLog builders.
|
||||||
@ -112,6 +115,7 @@ func (c *Client) init() {
|
|||||||
c.RedeemCode = NewRedeemCodeClient(c.config)
|
c.RedeemCode = NewRedeemCodeClient(c.config)
|
||||||
c.SecuritySecret = NewSecuritySecretClient(c.config)
|
c.SecuritySecret = NewSecuritySecretClient(c.config)
|
||||||
c.Setting = NewSettingClient(c.config)
|
c.Setting = NewSettingClient(c.config)
|
||||||
|
c.TLSFingerprintProfile = NewTLSFingerprintProfileClient(c.config)
|
||||||
c.UsageCleanupTask = NewUsageCleanupTaskClient(c.config)
|
c.UsageCleanupTask = NewUsageCleanupTaskClient(c.config)
|
||||||
c.UsageLog = NewUsageLogClient(c.config)
|
c.UsageLog = NewUsageLogClient(c.config)
|
||||||
c.User = NewUserClient(c.config)
|
c.User = NewUserClient(c.config)
|
||||||
@ -225,6 +229,7 @@ func (c *Client) Tx(ctx context.Context) (*Tx, error) {
|
|||||||
RedeemCode: NewRedeemCodeClient(cfg),
|
RedeemCode: NewRedeemCodeClient(cfg),
|
||||||
SecuritySecret: NewSecuritySecretClient(cfg),
|
SecuritySecret: NewSecuritySecretClient(cfg),
|
||||||
Setting: NewSettingClient(cfg),
|
Setting: NewSettingClient(cfg),
|
||||||
|
TLSFingerprintProfile: NewTLSFingerprintProfileClient(cfg),
|
||||||
UsageCleanupTask: NewUsageCleanupTaskClient(cfg),
|
UsageCleanupTask: NewUsageCleanupTaskClient(cfg),
|
||||||
UsageLog: NewUsageLogClient(cfg),
|
UsageLog: NewUsageLogClient(cfg),
|
||||||
User: NewUserClient(cfg),
|
User: NewUserClient(cfg),
|
||||||
@ -265,6 +270,7 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)
|
|||||||
RedeemCode: NewRedeemCodeClient(cfg),
|
RedeemCode: NewRedeemCodeClient(cfg),
|
||||||
SecuritySecret: NewSecuritySecretClient(cfg),
|
SecuritySecret: NewSecuritySecretClient(cfg),
|
||||||
Setting: NewSettingClient(cfg),
|
Setting: NewSettingClient(cfg),
|
||||||
|
TLSFingerprintProfile: NewTLSFingerprintProfileClient(cfg),
|
||||||
UsageCleanupTask: NewUsageCleanupTaskClient(cfg),
|
UsageCleanupTask: NewUsageCleanupTaskClient(cfg),
|
||||||
UsageLog: NewUsageLogClient(cfg),
|
UsageLog: NewUsageLogClient(cfg),
|
||||||
User: NewUserClient(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.APIKey, c.Account, c.AccountGroup, c.Announcement, c.AnnouncementRead,
|
||||||
c.ErrorPassthroughRule, c.Group, c.IdempotencyRecord, c.PromoCode,
|
c.ErrorPassthroughRule, c.Group, c.IdempotencyRecord, c.PromoCode,
|
||||||
c.PromoCodeUsage, c.Proxy, c.RedeemCode, c.SecuritySecret, c.Setting,
|
c.PromoCodeUsage, c.Proxy, c.RedeemCode, c.SecuritySecret, c.Setting,
|
||||||
c.UsageCleanupTask, c.UsageLog, c.User, c.UserAllowedGroup,
|
c.TLSFingerprintProfile, c.UsageCleanupTask, c.UsageLog, c.User,
|
||||||
c.UserAttributeDefinition, c.UserAttributeValue, c.UserSubscription,
|
c.UserAllowedGroup, c.UserAttributeDefinition, c.UserAttributeValue,
|
||||||
|
c.UserSubscription,
|
||||||
} {
|
} {
|
||||||
n.Use(hooks...)
|
n.Use(hooks...)
|
||||||
}
|
}
|
||||||
@ -318,8 +325,9 @@ func (c *Client) Intercept(interceptors ...Interceptor) {
|
|||||||
c.APIKey, c.Account, c.AccountGroup, c.Announcement, c.AnnouncementRead,
|
c.APIKey, c.Account, c.AccountGroup, c.Announcement, c.AnnouncementRead,
|
||||||
c.ErrorPassthroughRule, c.Group, c.IdempotencyRecord, c.PromoCode,
|
c.ErrorPassthroughRule, c.Group, c.IdempotencyRecord, c.PromoCode,
|
||||||
c.PromoCodeUsage, c.Proxy, c.RedeemCode, c.SecuritySecret, c.Setting,
|
c.PromoCodeUsage, c.Proxy, c.RedeemCode, c.SecuritySecret, c.Setting,
|
||||||
c.UsageCleanupTask, c.UsageLog, c.User, c.UserAllowedGroup,
|
c.TLSFingerprintProfile, c.UsageCleanupTask, c.UsageLog, c.User,
|
||||||
c.UserAttributeDefinition, c.UserAttributeValue, c.UserSubscription,
|
c.UserAllowedGroup, c.UserAttributeDefinition, c.UserAttributeValue,
|
||||||
|
c.UserSubscription,
|
||||||
} {
|
} {
|
||||||
n.Intercept(interceptors...)
|
n.Intercept(interceptors...)
|
||||||
}
|
}
|
||||||
@ -356,6 +364,8 @@ func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) {
|
|||||||
return c.SecuritySecret.mutate(ctx, m)
|
return c.SecuritySecret.mutate(ctx, m)
|
||||||
case *SettingMutation:
|
case *SettingMutation:
|
||||||
return c.Setting.mutate(ctx, m)
|
return c.Setting.mutate(ctx, m)
|
||||||
|
case *TLSFingerprintProfileMutation:
|
||||||
|
return c.TLSFingerprintProfile.mutate(ctx, m)
|
||||||
case *UsageCleanupTaskMutation:
|
case *UsageCleanupTaskMutation:
|
||||||
return c.UsageCleanupTask.mutate(ctx, m)
|
return c.UsageCleanupTask.mutate(ctx, m)
|
||||||
case *UsageLogMutation:
|
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.
|
// UsageCleanupTaskClient is a client for the UsageCleanupTask schema.
|
||||||
type UsageCleanupTaskClient struct {
|
type UsageCleanupTaskClient struct {
|
||||||
config
|
config
|
||||||
@ -3889,16 +4032,16 @@ type (
|
|||||||
hooks struct {
|
hooks struct {
|
||||||
APIKey, Account, AccountGroup, Announcement, AnnouncementRead,
|
APIKey, Account, AccountGroup, Announcement, AnnouncementRead,
|
||||||
ErrorPassthroughRule, Group, IdempotencyRecord, PromoCode, PromoCodeUsage,
|
ErrorPassthroughRule, Group, IdempotencyRecord, PromoCode, PromoCodeUsage,
|
||||||
Proxy, RedeemCode, SecuritySecret, Setting, UsageCleanupTask, UsageLog, User,
|
Proxy, RedeemCode, SecuritySecret, Setting, TLSFingerprintProfile,
|
||||||
UserAllowedGroup, UserAttributeDefinition, UserAttributeValue,
|
UsageCleanupTask, UsageLog, User, UserAllowedGroup, UserAttributeDefinition,
|
||||||
UserSubscription []ent.Hook
|
UserAttributeValue, UserSubscription []ent.Hook
|
||||||
}
|
}
|
||||||
inters struct {
|
inters struct {
|
||||||
APIKey, Account, AccountGroup, Announcement, AnnouncementRead,
|
APIKey, Account, AccountGroup, Announcement, AnnouncementRead,
|
||||||
ErrorPassthroughRule, Group, IdempotencyRecord, PromoCode, PromoCodeUsage,
|
ErrorPassthroughRule, Group, IdempotencyRecord, PromoCode, PromoCodeUsage,
|
||||||
Proxy, RedeemCode, SecuritySecret, Setting, UsageCleanupTask, UsageLog, User,
|
Proxy, RedeemCode, SecuritySecret, Setting, TLSFingerprintProfile,
|
||||||
UserAllowedGroup, UserAttributeDefinition, UserAttributeValue,
|
UsageCleanupTask, UsageLog, User, UserAllowedGroup, UserAttributeDefinition,
|
||||||
UserSubscription []ent.Interceptor
|
UserAttributeValue, UserSubscription []ent.Interceptor
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import (
|
|||||||
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
|
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/setting"
|
"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/usagecleanuptask"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/usagelog"
|
"github.com/Wei-Shaw/sub2api/ent/usagelog"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/user"
|
"github.com/Wei-Shaw/sub2api/ent/user"
|
||||||
@ -107,6 +108,7 @@ func checkColumn(t, c string) error {
|
|||||||
redeemcode.Table: redeemcode.ValidColumn,
|
redeemcode.Table: redeemcode.ValidColumn,
|
||||||
securitysecret.Table: securitysecret.ValidColumn,
|
securitysecret.Table: securitysecret.ValidColumn,
|
||||||
setting.Table: setting.ValidColumn,
|
setting.Table: setting.ValidColumn,
|
||||||
|
tlsfingerprintprofile.Table: tlsfingerprintprofile.ValidColumn,
|
||||||
usagecleanuptask.Table: usagecleanuptask.ValidColumn,
|
usagecleanuptask.Table: usagecleanuptask.ValidColumn,
|
||||||
usagelog.Table: usagelog.ValidColumn,
|
usagelog.Table: usagelog.ValidColumn,
|
||||||
user.Table: user.ValidColumn,
|
user.Table: user.ValidColumn,
|
||||||
|
|||||||
@ -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)
|
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
|
// The UsageCleanupTaskFunc type is an adapter to allow the use of ordinary
|
||||||
// function as UsageCleanupTask mutator.
|
// function as UsageCleanupTask mutator.
|
||||||
type UsageCleanupTaskFunc func(context.Context, *ent.UsageCleanupTaskMutation) (ent.Value, error)
|
type UsageCleanupTaskFunc func(context.Context, *ent.UsageCleanupTaskMutation) (ent.Value, error)
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
|
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/setting"
|
"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/usagecleanuptask"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/usagelog"
|
"github.com/Wei-Shaw/sub2api/ent/usagelog"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/user"
|
"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)
|
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.
|
// 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)
|
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
|
return &query[*ent.SecuritySecretQuery, predicate.SecuritySecret, securitysecret.OrderOption]{typ: ent.TypeSecuritySecret, tq: q}, nil
|
||||||
case *ent.SettingQuery:
|
case *ent.SettingQuery:
|
||||||
return &query[*ent.SettingQuery, predicate.Setting, setting.OrderOption]{typ: ent.TypeSetting, tq: q}, nil
|
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:
|
case *ent.UsageCleanupTaskQuery:
|
||||||
return &query[*ent.UsageCleanupTaskQuery, predicate.UsageCleanupTask, usagecleanuptask.OrderOption]{typ: ent.TypeUsageCleanupTask, tq: q}, nil
|
return &query[*ent.UsageCleanupTaskQuery, predicate.UsageCleanupTask, usagecleanuptask.OrderOption]{typ: ent.TypeUsageCleanupTask, tq: q}, nil
|
||||||
case *ent.UsageLogQuery:
|
case *ent.UsageLogQuery:
|
||||||
|
|||||||
@ -673,6 +673,30 @@ var (
|
|||||||
Columns: SettingsColumns,
|
Columns: SettingsColumns,
|
||||||
PrimaryKey: []*schema.Column{SettingsColumns[0]},
|
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 holds the columns for the "usage_cleanup_tasks" table.
|
||||||
UsageCleanupTasksColumns = []*schema.Column{
|
UsageCleanupTasksColumns = []*schema.Column{
|
||||||
{Name: "id", Type: field.TypeInt64, Increment: true},
|
{Name: "id", Type: field.TypeInt64, Increment: true},
|
||||||
@ -1111,6 +1135,7 @@ var (
|
|||||||
RedeemCodesTable,
|
RedeemCodesTable,
|
||||||
SecuritySecretsTable,
|
SecuritySecretsTable,
|
||||||
SettingsTable,
|
SettingsTable,
|
||||||
|
TLSFingerprintProfilesTable,
|
||||||
UsageCleanupTasksTable,
|
UsageCleanupTasksTable,
|
||||||
UsageLogsTable,
|
UsageLogsTable,
|
||||||
UsersTable,
|
UsersTable,
|
||||||
@ -1175,6 +1200,9 @@ func init() {
|
|||||||
SettingsTable.Annotation = &entsql.Annotation{
|
SettingsTable.Annotation = &entsql.Annotation{
|
||||||
Table: "settings",
|
Table: "settings",
|
||||||
}
|
}
|
||||||
|
TLSFingerprintProfilesTable.Annotation = &entsql.Annotation{
|
||||||
|
Table: "tls_fingerprint_profiles",
|
||||||
|
}
|
||||||
UsageCleanupTasksTable.Annotation = &entsql.Annotation{
|
UsageCleanupTasksTable.Annotation = &entsql.Annotation{
|
||||||
Table: "usage_cleanup_tasks",
|
Table: "usage_cleanup_tasks",
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -48,6 +48,9 @@ type SecuritySecret func(*sql.Selector)
|
|||||||
// Setting is the predicate function for setting builders.
|
// Setting is the predicate function for setting builders.
|
||||||
type Setting func(*sql.Selector)
|
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.
|
// UsageCleanupTask is the predicate function for usagecleanuptask builders.
|
||||||
type UsageCleanupTask func(*sql.Selector)
|
type UsageCleanupTask func(*sql.Selector)
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/Wei-Shaw/sub2api/ent/schema"
|
"github.com/Wei-Shaw/sub2api/ent/schema"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/setting"
|
"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/usagecleanuptask"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/usagelog"
|
"github.com/Wei-Shaw/sub2api/ent/usagelog"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/user"
|
"github.com/Wei-Shaw/sub2api/ent/user"
|
||||||
@ -746,6 +747,43 @@ func init() {
|
|||||||
setting.DefaultUpdatedAt = settingDescUpdatedAt.Default.(func() time.Time)
|
setting.DefaultUpdatedAt = settingDescUpdatedAt.Default.(func() time.Time)
|
||||||
// setting.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field.
|
// setting.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field.
|
||||||
setting.UpdateDefaultUpdatedAt = settingDescUpdatedAt.UpdateDefault.(func() time.Time)
|
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()
|
usagecleanuptaskMixin := schema.UsageCleanupTask{}.Mixin()
|
||||||
usagecleanuptaskMixinFields0 := usagecleanuptaskMixin[0].Fields()
|
usagecleanuptaskMixinFields0 := usagecleanuptaskMixin[0].Fields()
|
||||||
_ = usagecleanuptaskMixinFields0
|
_ = usagecleanuptaskMixinFields0
|
||||||
|
|||||||
100
backend/ent/schema/tls_fingerprint_profile.go
Normal file
100
backend/ent/schema/tls_fingerprint_profile.go
Normal file
@ -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"}),
|
||||||
|
}
|
||||||
|
}
|
||||||
275
backend/ent/tlsfingerprintprofile.go
Normal file
275
backend/ent/tlsfingerprintprofile.go
Normal file
@ -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
|
||||||
121
backend/ent/tlsfingerprintprofile/tlsfingerprintprofile.go
Normal file
121
backend/ent/tlsfingerprintprofile/tlsfingerprintprofile.go
Normal file
@ -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()
|
||||||
|
}
|
||||||
415
backend/ent/tlsfingerprintprofile/where.go
Normal file
415
backend/ent/tlsfingerprintprofile/where.go
Normal file
@ -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))
|
||||||
|
}
|
||||||
1341
backend/ent/tlsfingerprintprofile_create.go
Normal file
1341
backend/ent/tlsfingerprintprofile_create.go
Normal file
File diff suppressed because it is too large
Load Diff
88
backend/ent/tlsfingerprintprofile_delete.go
Normal file
88
backend/ent/tlsfingerprintprofile_delete.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
564
backend/ent/tlsfingerprintprofile_query.go
Normal file
564
backend/ent/tlsfingerprintprofile_query.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
881
backend/ent/tlsfingerprintprofile_update.go
Normal file
881
backend/ent/tlsfingerprintprofile_update.go
Normal file
@ -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
|
||||||
|
}
|
||||||
@ -42,6 +42,8 @@ type Tx struct {
|
|||||||
SecuritySecret *SecuritySecretClient
|
SecuritySecret *SecuritySecretClient
|
||||||
// Setting is the client for interacting with the Setting builders.
|
// Setting is the client for interacting with the Setting builders.
|
||||||
Setting *SettingClient
|
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 is the client for interacting with the UsageCleanupTask builders.
|
||||||
UsageCleanupTask *UsageCleanupTaskClient
|
UsageCleanupTask *UsageCleanupTaskClient
|
||||||
// UsageLog is the client for interacting with the UsageLog builders.
|
// UsageLog is the client for interacting with the UsageLog builders.
|
||||||
@ -201,6 +203,7 @@ func (tx *Tx) init() {
|
|||||||
tx.RedeemCode = NewRedeemCodeClient(tx.config)
|
tx.RedeemCode = NewRedeemCodeClient(tx.config)
|
||||||
tx.SecuritySecret = NewSecuritySecretClient(tx.config)
|
tx.SecuritySecret = NewSecuritySecretClient(tx.config)
|
||||||
tx.Setting = NewSettingClient(tx.config)
|
tx.Setting = NewSettingClient(tx.config)
|
||||||
|
tx.TLSFingerprintProfile = NewTLSFingerprintProfileClient(tx.config)
|
||||||
tx.UsageCleanupTask = NewUsageCleanupTaskClient(tx.config)
|
tx.UsageCleanupTask = NewUsageCleanupTaskClient(tx.config)
|
||||||
tx.UsageLog = NewUsageLogClient(tx.config)
|
tx.UsageLog = NewUsageLogClient(tx.config)
|
||||||
tx.User = NewUserClient(tx.config)
|
tx.User = NewUserClient(tx.config)
|
||||||
|
|||||||
@ -673,17 +673,33 @@ type TLSFingerprintConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TLSProfileConfig 单个TLS指纹模板的配置
|
// TLSProfileConfig 单个TLS指纹模板的配置
|
||||||
|
// 所有列表字段为空时使用内置默认值(Claude CLI 2.x / Node.js 20.x)
|
||||||
|
// 建议通过 TLS 指纹采集工具 (tests/tls-fingerprint-web) 获取完整配置
|
||||||
type TLSProfileConfig struct {
|
type TLSProfileConfig struct {
|
||||||
// Name: 模板显示名称
|
// Name: 模板显示名称
|
||||||
Name string `mapstructure:"name"`
|
Name string `mapstructure:"name"`
|
||||||
// EnableGREASE: 是否启用GREASE扩展(Chrome使用,Node.js不使用)
|
// EnableGREASE: 是否启用GREASE扩展(Chrome使用,Node.js不使用)
|
||||||
EnableGREASE bool `mapstructure:"enable_grease"`
|
EnableGREASE bool `mapstructure:"enable_grease"`
|
||||||
// CipherSuites: TLS加密套件列表(空则使用内置默认值)
|
// CipherSuites: TLS加密套件列表
|
||||||
CipherSuites []uint16 `mapstructure:"cipher_suites"`
|
CipherSuites []uint16 `mapstructure:"cipher_suites"`
|
||||||
// Curves: 椭圆曲线列表(空则使用内置默认值)
|
// Curves: 椭圆曲线列表
|
||||||
Curves []uint16 `mapstructure:"curves"`
|
Curves []uint16 `mapstructure:"curves"`
|
||||||
// PointFormats: 点格式列表(空则使用内置默认值)
|
// PointFormats: 点格式列表
|
||||||
PointFormats []uint8 `mapstructure:"point_formats"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NodeTLSProxyConfig Node.js TLS 代理配置
|
// NodeTLSProxyConfig Node.js TLS 代理配置
|
||||||
|
|||||||
@ -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"})
|
||||||
|
}
|
||||||
@ -252,6 +252,10 @@ func AccountFromServiceShallow(a *service.Account) *Account {
|
|||||||
enabled := true
|
enabled := true
|
||||||
out.EnableTLSFingerprint = &enabled
|
out.EnableTLSFingerprint = &enabled
|
||||||
}
|
}
|
||||||
|
// TLS指纹模板ID
|
||||||
|
if profileID := a.GetTLSFingerprintProfileID(); profileID > 0 {
|
||||||
|
out.TLSFingerprintProfileID = &profileID
|
||||||
|
}
|
||||||
// 会话ID伪装开关
|
// 会话ID伪装开关
|
||||||
if a.IsSessionIDMaskingEnabled() {
|
if a.IsSessionIDMaskingEnabled() {
|
||||||
enabled := true
|
enabled := true
|
||||||
|
|||||||
@ -185,7 +185,8 @@ type Account struct {
|
|||||||
|
|
||||||
// TLS指纹伪装(仅 Anthropic OAuth/SetupToken 账号有效)
|
// TLS指纹伪装(仅 Anthropic OAuth/SetupToken 账号有效)
|
||||||
// 从 extra 字段提取,方便前端显示和编辑
|
// 从 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 账号有效)
|
// 会话ID伪装(仅 Anthropic OAuth/SetupToken 账号有效)
|
||||||
// 启用后将在15分钟内固定 metadata.user_id 中的 session ID
|
// 启用后将在15分钟内固定 metadata.user_id 中的 session ID
|
||||||
|
|||||||
@ -75,8 +75,10 @@ func (f *fakeGroupRepo) ListActive(context.Context) ([]service.Group, error) { r
|
|||||||
func (f *fakeGroupRepo) ListActiveByPlatform(context.Context, string) ([]service.Group, error) {
|
func (f *fakeGroupRepo) ListActiveByPlatform(context.Context, string) ([]service.Group, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
func (f *fakeGroupRepo) ExistsByName(context.Context, string) (bool, error) { return false, 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) GetAccountCount(context.Context, int64) (int64, int64, error) {
|
||||||
|
return 0, 0, nil
|
||||||
|
}
|
||||||
func (f *fakeGroupRepo) DeleteAccountGroupsByGroupID(context.Context, int64) (int64, error) {
|
func (f *fakeGroupRepo) DeleteAccountGroupsByGroupID(context.Context, int64) (int64, error) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
@ -158,6 +160,7 @@ func newTestGatewayHandler(t *testing.T, group *service.Group, accounts []*servi
|
|||||||
nil, // rpmCache
|
nil, // rpmCache
|
||||||
nil, // digestStore
|
nil, // digestStore
|
||||||
nil, // settingService
|
nil, // settingService
|
||||||
|
nil, // tlsFPProfileService
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunModeSimple:跳过计费检查,避免引入 repo/cache 依赖。
|
// RunModeSimple:跳过计费检查,避免引入 repo/cache 依赖。
|
||||||
|
|||||||
@ -6,29 +6,30 @@ import (
|
|||||||
|
|
||||||
// AdminHandlers contains all admin-related HTTP handlers
|
// AdminHandlers contains all admin-related HTTP handlers
|
||||||
type AdminHandlers struct {
|
type AdminHandlers struct {
|
||||||
Dashboard *admin.DashboardHandler
|
Dashboard *admin.DashboardHandler
|
||||||
User *admin.UserHandler
|
User *admin.UserHandler
|
||||||
Group *admin.GroupHandler
|
Group *admin.GroupHandler
|
||||||
Account *admin.AccountHandler
|
Account *admin.AccountHandler
|
||||||
Announcement *admin.AnnouncementHandler
|
Announcement *admin.AnnouncementHandler
|
||||||
DataManagement *admin.DataManagementHandler
|
DataManagement *admin.DataManagementHandler
|
||||||
Backup *admin.BackupHandler
|
Backup *admin.BackupHandler
|
||||||
OAuth *admin.OAuthHandler
|
OAuth *admin.OAuthHandler
|
||||||
OpenAIOAuth *admin.OpenAIOAuthHandler
|
OpenAIOAuth *admin.OpenAIOAuthHandler
|
||||||
GeminiOAuth *admin.GeminiOAuthHandler
|
GeminiOAuth *admin.GeminiOAuthHandler
|
||||||
AntigravityOAuth *admin.AntigravityOAuthHandler
|
AntigravityOAuth *admin.AntigravityOAuthHandler
|
||||||
Proxy *admin.ProxyHandler
|
Proxy *admin.ProxyHandler
|
||||||
Redeem *admin.RedeemHandler
|
Redeem *admin.RedeemHandler
|
||||||
Promo *admin.PromoHandler
|
Promo *admin.PromoHandler
|
||||||
Setting *admin.SettingHandler
|
Setting *admin.SettingHandler
|
||||||
Ops *admin.OpsHandler
|
Ops *admin.OpsHandler
|
||||||
System *admin.SystemHandler
|
System *admin.SystemHandler
|
||||||
Subscription *admin.SubscriptionHandler
|
Subscription *admin.SubscriptionHandler
|
||||||
Usage *admin.UsageHandler
|
Usage *admin.UsageHandler
|
||||||
UserAttribute *admin.UserAttributeHandler
|
UserAttribute *admin.UserAttributeHandler
|
||||||
ErrorPassthrough *admin.ErrorPassthroughHandler
|
ErrorPassthrough *admin.ErrorPassthroughHandler
|
||||||
APIKey *admin.AdminAPIKeyHandler
|
TLSFingerprintProfile *admin.TLSFingerprintProfileHandler
|
||||||
ScheduledTest *admin.ScheduledTestHandler
|
APIKey *admin.AdminAPIKeyHandler
|
||||||
|
ScheduledTest *admin.ScheduledTestHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handlers contains all HTTP handlers
|
// Handlers contains all HTTP handlers
|
||||||
|
|||||||
@ -2224,7 +2224,7 @@ func (s *stubSoraClientForHandler) GetVideoTask(_ context.Context, _ *service.Ac
|
|||||||
func newMinimalGatewayService(accountRepo service.AccountRepository) *service.GatewayService {
|
func newMinimalGatewayService(accountRepo service.AccountRepository) *service.GatewayService {
|
||||||
return service.NewGatewayService(
|
return service.NewGatewayService(
|
||||||
accountRepo, nil, nil, nil, nil, nil, nil, nil, nil,
|
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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -464,6 +464,7 @@ func TestSoraGatewayHandler_ChatCompletions(t *testing.T) {
|
|||||||
nil, // rpmCache
|
nil, // rpmCache
|
||||||
nil, // digestStore
|
nil, // digestStore
|
||||||
nil, // settingService
|
nil, // settingService
|
||||||
|
nil, // tlsFPProfileService
|
||||||
)
|
)
|
||||||
|
|
||||||
soraClient := &stubSoraClient{imageURLs: []string{"https://example.com/a.png"}}
|
soraClient := &stubSoraClient{imageURLs: []string{"https://example.com/a.png"}}
|
||||||
|
|||||||
@ -30,33 +30,35 @@ func ProvideAdminHandlers(
|
|||||||
usageHandler *admin.UsageHandler,
|
usageHandler *admin.UsageHandler,
|
||||||
userAttributeHandler *admin.UserAttributeHandler,
|
userAttributeHandler *admin.UserAttributeHandler,
|
||||||
errorPassthroughHandler *admin.ErrorPassthroughHandler,
|
errorPassthroughHandler *admin.ErrorPassthroughHandler,
|
||||||
|
tlsFingerprintProfileHandler *admin.TLSFingerprintProfileHandler,
|
||||||
apiKeyHandler *admin.AdminAPIKeyHandler,
|
apiKeyHandler *admin.AdminAPIKeyHandler,
|
||||||
scheduledTestHandler *admin.ScheduledTestHandler,
|
scheduledTestHandler *admin.ScheduledTestHandler,
|
||||||
) *AdminHandlers {
|
) *AdminHandlers {
|
||||||
return &AdminHandlers{
|
return &AdminHandlers{
|
||||||
Dashboard: dashboardHandler,
|
Dashboard: dashboardHandler,
|
||||||
User: userHandler,
|
User: userHandler,
|
||||||
Group: groupHandler,
|
Group: groupHandler,
|
||||||
Account: accountHandler,
|
Account: accountHandler,
|
||||||
Announcement: announcementHandler,
|
Announcement: announcementHandler,
|
||||||
DataManagement: dataManagementHandler,
|
DataManagement: dataManagementHandler,
|
||||||
Backup: backupHandler,
|
Backup: backupHandler,
|
||||||
OAuth: oauthHandler,
|
OAuth: oauthHandler,
|
||||||
OpenAIOAuth: openaiOAuthHandler,
|
OpenAIOAuth: openaiOAuthHandler,
|
||||||
GeminiOAuth: geminiOAuthHandler,
|
GeminiOAuth: geminiOAuthHandler,
|
||||||
AntigravityOAuth: antigravityOAuthHandler,
|
AntigravityOAuth: antigravityOAuthHandler,
|
||||||
Proxy: proxyHandler,
|
Proxy: proxyHandler,
|
||||||
Redeem: redeemHandler,
|
Redeem: redeemHandler,
|
||||||
Promo: promoHandler,
|
Promo: promoHandler,
|
||||||
Setting: settingHandler,
|
Setting: settingHandler,
|
||||||
Ops: opsHandler,
|
Ops: opsHandler,
|
||||||
System: systemHandler,
|
System: systemHandler,
|
||||||
Subscription: subscriptionHandler,
|
Subscription: subscriptionHandler,
|
||||||
Usage: usageHandler,
|
Usage: usageHandler,
|
||||||
UserAttribute: userAttributeHandler,
|
UserAttribute: userAttributeHandler,
|
||||||
ErrorPassthrough: errorPassthroughHandler,
|
ErrorPassthrough: errorPassthroughHandler,
|
||||||
APIKey: apiKeyHandler,
|
TLSFingerprintProfile: tlsFingerprintProfileHandler,
|
||||||
ScheduledTest: scheduledTestHandler,
|
APIKey: apiKeyHandler,
|
||||||
|
ScheduledTest: scheduledTestHandler,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,6 +147,7 @@ var ProviderSet = wire.NewSet(
|
|||||||
admin.NewUsageHandler,
|
admin.NewUsageHandler,
|
||||||
admin.NewUserAttributeHandler,
|
admin.NewUserAttributeHandler,
|
||||||
admin.NewErrorPassthroughHandler,
|
admin.NewErrorPassthroughHandler,
|
||||||
|
admin.NewTLSFingerprintProfileHandler,
|
||||||
admin.NewAdminAPIKeyHandler,
|
admin.NewAdminAPIKeyHandler,
|
||||||
admin.NewScheduledTestHandler,
|
admin.NewScheduledTestHandler,
|
||||||
|
|
||||||
|
|||||||
54
backend/internal/model/tls_fingerprint_profile.go
Normal file
54
backend/internal/model/tls_fingerprint_profile.go
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,12 +17,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Profile contains TLS fingerprint configuration.
|
// Profile contains TLS fingerprint configuration.
|
||||||
|
// All slice fields use built-in defaults when empty.
|
||||||
type Profile struct {
|
type Profile struct {
|
||||||
Name string // Profile name for identification
|
Name string // Profile name for identification
|
||||||
CipherSuites []uint16
|
CipherSuites []uint16
|
||||||
Curves []uint16
|
Curves []uint16
|
||||||
PointFormats []uint8
|
PointFormats []uint16
|
||||||
EnableGREASE bool
|
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.
|
// Dialer creates TLS connections with custom fingerprints.
|
||||||
@ -45,154 +52,67 @@ type SOCKS5ProxyDialer struct {
|
|||||||
proxyURL *url.URL
|
proxyURL *url.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default TLS fingerprint values captured from Claude CLI 2.x (Node.js 20.x + OpenSSL 3.x)
|
// Default TLS fingerprint values captured from Claude Code (Node.js 24.x)
|
||||||
// Captured using: tshark -i lo -f "tcp port 8443" -Y "tls.handshake.type == 1" -V
|
// Captured via tls-fingerprint-web capture server
|
||||||
// JA3 Hash: 1a28e69016765d92e3b381168d68922c
|
// JA3 Hash: 44f88fca027f27bab4bb08d4af15f23e
|
||||||
//
|
// JA4: t13d1714h1_5b57614c22b0_7baf387fc6ff
|
||||||
// Note: JA3/JA4 may have slight variations due to:
|
|
||||||
// - Session ticket presence/absence
|
|
||||||
// - Extension negotiation state
|
|
||||||
var (
|
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
|
// Order is critical for JA3 fingerprint matching
|
||||||
defaultCipherSuites = []uint16{
|
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
|
0x1302, // TLS_AES_256_GCM_SHA384
|
||||||
0x1303, // TLS_CHACHA20_POLY1305_SHA256
|
0x1303, // TLS_CHACHA20_POLY1305_SHA256
|
||||||
0x1301, // TLS_AES_128_GCM_SHA256
|
|
||||||
|
|
||||||
// ECDHE + AES-GCM
|
// ECDHE + AES-GCM
|
||||||
0xc02f, // TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
|
||||||
0xc02b, // TLS_ECDHE_ECDSA_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
|
0xc02c, // TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
|
||||||
|
0xc030, // TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
|
||||||
|
|
||||||
// DHE + AES-GCM
|
// ECDHE + ChaCha20-Poly1305
|
||||||
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
|
|
||||||
0xcca9, // TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
|
0xcca9, // TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
|
||||||
0xcca8, // TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
|
0xcca8, // TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
|
||||||
0xccaa, // TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256
|
|
||||||
|
|
||||||
// AES-CCM (256-bit)
|
// ECDHE + AES-CBC-SHA (legacy fallback)
|
||||||
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
|
|
||||||
0xc009, // TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
|
0xc009, // TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
|
||||||
0xc013, // TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
|
0xc013, // TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
|
||||||
0x0033, // TLS_DHE_RSA_WITH_AES_128_CBC_SHA
|
0xc00a, // TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
|
||||||
0x0032, // TLS_DHE_DSS_WITH_AES_128_CBC_SHA
|
0xc014, // TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
|
||||||
|
|
||||||
// RSA + AES-GCM/CCM/ARIA (non-PFS, 256-bit)
|
// RSA + AES-GCM (non-PFS)
|
||||||
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)
|
|
||||||
0x009c, // TLS_RSA_WITH_AES_128_GCM_SHA256
|
0x009c, // TLS_RSA_WITH_AES_128_GCM_SHA256
|
||||||
0xc0a0, // TLS_RSA_WITH_AES_128_CCM_8
|
0x009d, // TLS_RSA_WITH_AES_256_GCM_SHA384
|
||||||
0xc09c, // TLS_RSA_WITH_AES_128_CCM
|
|
||||||
0xc050, // TLS_RSA_WITH_ARIA_128_GCM_SHA256
|
|
||||||
|
|
||||||
// RSA + AES-CBC (non-PFS, legacy)
|
// RSA + AES-CBC-SHA (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
|
|
||||||
0x002f, // TLS_RSA_WITH_AES_128_CBC_SHA
|
0x002f, // TLS_RSA_WITH_AES_128_CBC_SHA
|
||||||
|
0x0035, // TLS_RSA_WITH_AES_256_CBC_SHA
|
||||||
// Renegotiation indication
|
|
||||||
0x00ff, // TLS_EMPTY_RENEGOTIATION_INFO_SCSV
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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{
|
defaultCurves = []utls.CurveID{
|
||||||
utls.X25519, // 0x001d
|
utls.X25519, // 0x001d
|
||||||
utls.CurveP256, // 0x0017 (secp256r1)
|
utls.CurveP256, // 0x0017 (secp256r1)
|
||||||
utls.CurveID(0x001e), // x448
|
utls.CurveP384, // 0x0018 (secp384r1)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultPointFormats contains all 3 point formats from Claude CLI
|
// defaultPointFormats contains point formats from Node.js 24.x
|
||||||
defaultPointFormats = []uint8{
|
defaultPointFormats = []uint16{
|
||||||
0, // uncompressed
|
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{
|
defaultSignatureAlgorithms = []utls.SignatureScheme{
|
||||||
0x0403, // ecdsa_secp256r1_sha256
|
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
|
0x0804, // rsa_pss_rsae_sha256
|
||||||
0x0805, // rsa_pss_rsae_sha384
|
|
||||||
0x0806, // rsa_pss_rsae_sha512
|
|
||||||
0x0401, // rsa_pkcs1_sha256
|
0x0401, // rsa_pkcs1_sha256
|
||||||
|
0x0503, // ecdsa_secp384r1_sha384
|
||||||
|
0x0805, // rsa_pss_rsae_sha384
|
||||||
0x0501, // rsa_pkcs1_sha384
|
0x0501, // rsa_pkcs1_sha384
|
||||||
|
0x0806, // rsa_pss_rsae_sha512
|
||||||
0x0601, // rsa_pkcs1_sha512
|
0x0601, // rsa_pkcs1_sha512
|
||||||
0x0303, // ecdsa_sha224
|
0x0201, // rsa_pkcs1_sha1
|
||||||
0x0301, // rsa_pkcs1_sha224
|
|
||||||
0x0302, // dsa_sha224
|
|
||||||
0x0402, // dsa_sha256
|
|
||||||
0x0502, // dsa_sha384
|
|
||||||
0x0602, // dsa_sha512
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -256,49 +176,7 @@ func (d *SOCKS5ProxyDialer) DialTLSContext(ctx context.Context, network, addr st
|
|||||||
slog.Debug("tls_fingerprint_socks5_tunnel_established")
|
slog.Debug("tls_fingerprint_socks5_tunnel_established")
|
||||||
|
|
||||||
// Step 3: Perform TLS handshake on the tunnel with utls fingerprint
|
// Step 3: Perform TLS handshake on the tunnel with utls fingerprint
|
||||||
host, _, err := net.SplitHostPort(addr)
|
return performTLSHandshake(ctx, conn, d.profile, 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialTLSContext establishes a TLS connection through HTTP proxy with the configured fingerprint.
|
// 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)
|
slog.Debug("tls_fingerprint_http_proxy_read_response_failed", "error", err)
|
||||||
return nil, fmt.Errorf("read CONNECT response: %w", 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 {
|
if resp.StatusCode != http.StatusOK {
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
@ -368,47 +247,7 @@ func (d *HTTPProxyDialer) DialTLSContext(ctx context.Context, network, addr stri
|
|||||||
slog.Debug("tls_fingerprint_http_proxy_tunnel_established")
|
slog.Debug("tls_fingerprint_http_proxy_tunnel_established")
|
||||||
|
|
||||||
// Step 4: Perform TLS handshake on the tunnel with utls fingerprint
|
// Step 4: Perform TLS handshake on the tunnel with utls fingerprint
|
||||||
host, _, err := net.SplitHostPort(addr)
|
return performTLSHandshake(ctx, conn, d.profile, 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialTLSContext establishes a TLS connection with the configured fingerprint.
|
// 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)
|
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)
|
host, _, err := net.SplitHostPort(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
host = addr
|
host = addr
|
||||||
}
|
}
|
||||||
slog.Debug("tls_fingerprint_sni_hostname", "host", host)
|
|
||||||
|
|
||||||
// Build ClientHello specification
|
spec := buildClientHelloSpecFromProfile(profile)
|
||||||
spec := d.buildClientHelloSpec()
|
tlsConn := utls.UClient(conn, &utls.Config{ServerName: host}, utls.HelloCustom)
|
||||||
slog.Debug("tls_fingerprint_clienthello_spec",
|
|
||||||
"cipher_suites", len(spec.CipherSuites),
|
|
||||||
"extensions", len(spec.Extensions))
|
|
||||||
|
|
||||||
// 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 {
|
if err := tlsConn.ApplyPreset(spec); err != nil {
|
||||||
slog.Debug("tls_fingerprint_apply_preset_failed", "error", err)
|
|
||||||
_ = conn.Close()
|
_ = 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 {
|
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()
|
_ = conn.Close()
|
||||||
return nil, fmt.Errorf("TLS handshake failed: %w", err)
|
return nil, fmt.Errorf("TLS handshake failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log successful handshake details
|
|
||||||
state := tlsConn.ConnectionState()
|
state := tlsConn.ConnectionState()
|
||||||
slog.Debug("tls_fingerprint_handshake_success",
|
slog.Debug("tls_fingerprint_handshake_success",
|
||||||
|
"host", host,
|
||||||
"version", state.Version,
|
"version", state.Version,
|
||||||
"cipher_suite", state.CipherSuite,
|
"cipher_suite", state.CipherSuite,
|
||||||
"alpn", state.NegotiatedProtocol)
|
"alpn", state.NegotiatedProtocol)
|
||||||
@ -477,11 +298,6 @@ func (d *Dialer) DialTLSContext(ctx context.Context, network, addr string) (net.
|
|||||||
return tlsConn, nil
|
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.
|
// toUTLSCurves converts uint16 slice to utls.CurveID slice.
|
||||||
func toUTLSCurves(curves []uint16) []utls.CurveID {
|
func toUTLSCurves(curves []uint16) []utls.CurveID {
|
||||||
result := make([]utls.CurveID, len(curves))
|
result := make([]utls.CurveID, len(curves))
|
||||||
@ -491,70 +307,143 @@ func toUTLSCurves(curves []uint16) []utls.CurveID {
|
|||||||
return result
|
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.
|
// buildClientHelloSpecFromProfile constructs ClientHelloSpec from a Profile.
|
||||||
// This is a standalone function that can be used by both Dialer and HTTPProxyDialer.
|
// This is a standalone function that can be used by both Dialer and HTTPProxyDialer.
|
||||||
func buildClientHelloSpecFromProfile(profile *Profile) *utls.ClientHelloSpec {
|
func buildClientHelloSpecFromProfile(profile *Profile) *utls.ClientHelloSpec {
|
||||||
// Get cipher suites
|
// Resolve effective values (profile overrides or built-in defaults)
|
||||||
var cipherSuites []uint16
|
cipherSuites := defaultCipherSuites
|
||||||
if profile != nil && len(profile.CipherSuites) > 0 {
|
if profile != nil && len(profile.CipherSuites) > 0 {
|
||||||
cipherSuites = profile.CipherSuites
|
cipherSuites = profile.CipherSuites
|
||||||
} else {
|
|
||||||
cipherSuites = defaultCipherSuites
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get curves
|
curves := defaultCurves
|
||||||
var curves []utls.CurveID
|
|
||||||
if profile != nil && len(profile.Curves) > 0 {
|
if profile != nil && len(profile.Curves) > 0 {
|
||||||
curves = toUTLSCurves(profile.Curves)
|
curves = toUTLSCurves(profile.Curves)
|
||||||
} else {
|
|
||||||
curves = defaultCurves
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get point formats
|
pointFormats := defaultPointFormats
|
||||||
var pointFormats []uint8
|
|
||||||
if profile != nil && len(profile.PointFormats) > 0 {
|
if profile != nil && len(profile.PointFormats) > 0 {
|
||||||
pointFormats = profile.PointFormats
|
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
|
enableGREASE := profile != nil && profile.EnableGREASE
|
||||||
|
|
||||||
extensions := make([]utls.TLSExtension, 0, 16)
|
// Build key shares
|
||||||
|
keyShares := make([]utls.KeyShare, len(keyShareGroups))
|
||||||
if enableGREASE {
|
for i, g := range keyShareGroups {
|
||||||
extensions = append(extensions, &utls.UtlsGREASEExtension{})
|
keyShares[i] = utls.KeyShare{Group: g}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SNI extension - MUST be explicitly added for HelloCustom mode
|
// Determine extension order
|
||||||
// utls will populate the server name from Config.ServerName
|
extOrder := defaultExtensionOrder
|
||||||
extensions = append(extensions, &utls.SNIExtension{})
|
if profile != nil && len(profile.Extensions) > 0 {
|
||||||
|
extOrder = profile.Extensions
|
||||||
|
}
|
||||||
|
|
||||||
// Claude CLI extension order (captured from tshark):
|
// Build extensions list from the ordered IDs.
|
||||||
// server_name(0), ec_point_formats(11), supported_groups(10), session_ticket(35),
|
// Parametric extensions (curves, sigalgs, etc.) are populated with resolved profile values.
|
||||||
// alpn(16), encrypt_then_mac(22), extended_master_secret(23),
|
// Unknown IDs use GenericExtension (sends type ID with empty data).
|
||||||
// signature_algorithms(13), supported_versions(43),
|
extensions := make([]utls.TLSExtension, 0, len(extOrder)+2)
|
||||||
// psk_key_exchange_modes(45), key_share(51)
|
for _, id := range extOrder {
|
||||||
extensions = append(extensions,
|
if isGREASEValue(id) {
|
||||||
&utls.SupportedPointsExtension{SupportedPoints: pointFormats},
|
extensions = append(extensions, &utls.UtlsGREASEExtension{})
|
||||||
&utls.SupportedCurvesExtension{Curves: curves},
|
continue
|
||||||
&utls.SessionTicketExtension{},
|
}
|
||||||
&utls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}},
|
switch id {
|
||||||
&utls.GenericExtension{Id: 22},
|
case 0: // server_name
|
||||||
&utls.ExtendedMasterSecretExtension{},
|
extensions = append(extensions, &utls.SNIExtension{})
|
||||||
&utls.SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: defaultSignatureAlgorithms},
|
case 5: // status_request (OCSP)
|
||||||
&utls.SupportedVersionsExtension{Versions: []uint16{
|
extensions = append(extensions, &utls.StatusRequestExtension{})
|
||||||
utls.VersionTLS13,
|
case 10: // supported_groups
|
||||||
utls.VersionTLS12,
|
extensions = append(extensions, &utls.SupportedCurvesExtension{Curves: curves})
|
||||||
}},
|
case 11: // ec_point_formats
|
||||||
&utls.PSKKeyExchangeModesExtension{Modes: []uint8{utls.PskModeDHE}},
|
extensions = append(extensions, &utls.SupportedPointsExtension{SupportedPoints: toUint8s(pointFormats)})
|
||||||
&utls.KeyShareExtension{KeyShares: []utls.KeyShare{
|
case 13: // signature_algorithms
|
||||||
{Group: utls.X25519},
|
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{})
|
extensions = append(extensions, &utls.UtlsGREASEExtension{})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -566,3 +455,12 @@ func buildClientHelloSpecFromProfile(profile *Profile) *utls.ClientHelloSpec {
|
|||||||
TLSVersMin: utls.VersionTLS10,
|
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
|
||||||
|
}
|
||||||
|
|||||||
368
backend/internal/pkg/tlsfingerprint/dialer_capture_test.go
Normal file
368
backend/internal/pkg/tlsfingerprint/dialer_capture_test.go
Normal file
@ -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")
|
||||||
|
}
|
||||||
@ -40,16 +40,15 @@ func skipIfExternalServiceUnavailable(t *testing.T, err error) {
|
|||||||
|
|
||||||
// TestJA3Fingerprint verifies the JA3/JA4 fingerprint matches expected value.
|
// TestJA3Fingerprint verifies the JA3/JA4 fingerprint matches expected value.
|
||||||
// This test uses tls.peet.ws to verify the fingerprint.
|
// This test uses tls.peet.ws to verify the fingerprint.
|
||||||
// Expected JA3 hash: 1a28e69016765d92e3b381168d68922c (Claude CLI / Node.js 20.x)
|
// Expected JA3 hash: 44f88fca027f27bab4bb08d4af15f23e (Node.js 24.x)
|
||||||
// Expected JA4: t13d5911h1_a33745022dd6_1f22a2ca17c4 (d=domain) or t13i5911h1_... (i=IP)
|
// Expected JA4: t13d1714h1_5b57614c22b0_7baf387fc6ff
|
||||||
func TestJA3Fingerprint(t *testing.T) {
|
func TestJA3Fingerprint(t *testing.T) {
|
||||||
// Skip if network is unavailable or if running in short mode
|
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("skipping integration test in short mode")
|
t.Skip("skipping integration test in short mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
profile := &Profile{
|
profile := &Profile{
|
||||||
Name: "Claude CLI Test",
|
Name: "Default Profile Test",
|
||||||
EnableGREASE: false,
|
EnableGREASE: false,
|
||||||
}
|
}
|
||||||
dialer := NewDialer(profile, nil)
|
dialer := NewDialer(profile, nil)
|
||||||
@ -61,7 +60,6 @@ func TestJA3Fingerprint(t *testing.T) {
|
|||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use tls.peet.ws fingerprint detection API
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@ -69,7 +67,7 @@ func TestJA3Fingerprint(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create request: %v", err)
|
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)
|
resp, err := client.Do(req)
|
||||||
skipIfExternalServiceUnavailable(t, err)
|
skipIfExternalServiceUnavailable(t, err)
|
||||||
@ -86,71 +84,23 @@ func TestJA3Fingerprint(t *testing.T) {
|
|||||||
t.Fatalf("failed to parse fingerprint response: %v", err)
|
t.Fatalf("failed to parse fingerprint response: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log all fingerprint information
|
|
||||||
t.Logf("JA3: %s", fpResp.TLS.JA3)
|
t.Logf("JA3: %s", fpResp.TLS.JA3)
|
||||||
t.Logf("JA3 Hash: %s", fpResp.TLS.JA3Hash)
|
t.Logf("JA3 Hash: %s", fpResp.TLS.JA3Hash)
|
||||||
t.Logf("JA4: %s", fpResp.TLS.JA4)
|
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 := "44f88fca027f27bab4bb08d4af15f23e"
|
||||||
expectedJA3Hash := "1a28e69016765d92e3b381168d68922c"
|
|
||||||
if fpResp.TLS.JA3Hash == expectedJA3Hash {
|
if fpResp.TLS.JA3Hash == expectedJA3Hash {
|
||||||
t.Logf("✓ JA3 hash matches expected value: %s", expectedJA3Hash)
|
t.Logf("✓ JA3 hash matches: %s", expectedJA3Hash)
|
||||||
} else {
|
} else {
|
||||||
t.Errorf("✗ JA3 hash mismatch: got %s, expected %s", fpResp.TLS.JA3Hash, expectedJA3Hash)
|
t.Errorf("✗ JA3 hash mismatch: got %s, expected %s", fpResp.TLS.JA3Hash, expectedJA3Hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify JA4 fingerprint
|
expectedJA4CipherHash := "_5b57614c22b0_"
|
||||||
// JA4 format: t[version][sni][cipher_count][ext_count][alpn]_[cipher_hash]_[ext_hash]
|
if strings.Contains(fpResp.TLS.JA4, expectedJA4CipherHash) {
|
||||||
// Expected: t13d5910h1 (d=domain) or t13i5910h1 (i=IP)
|
t.Logf("✓ JA4 cipher hash matches: %s", expectedJA4CipherHash)
|
||||||
// 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)
|
|
||||||
} else {
|
} 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.
|
// 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
|
// These profiles are from config.yaml gateway.tls_fingerprint.profiles
|
||||||
profiles := []TestProfileExpectation{
|
profiles := []TestProfileExpectation{
|
||||||
{
|
{
|
||||||
// Linux x64 Node.js v22.17.1
|
// Default profile (Node.js 24.x)
|
||||||
// Expected JA3 Hash: 1a28e69016765d92e3b381168d68922c
|
Profile: &Profile{
|
||||||
// Expected JA4: t13d5911h1_a33745022dd6_1f22a2ca17c4
|
Name: "default_node_v24",
|
||||||
|
EnableGREASE: false,
|
||||||
|
},
|
||||||
|
JA4CipherHash: "5b57614c22b0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Linux x64 Node.js v22.17.1 (explicit profile with v22 extensions)
|
||||||
Profile: &Profile{
|
Profile: &Profile{
|
||||||
Name: "linux_x64_node_v22171",
|
Name: "linux_x64_node_v22171",
|
||||||
EnableGREASE: false,
|
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},
|
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},
|
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
|
JA4CipherHash: "a33745022dd6",
|
||||||
},
|
|
||||||
{
|
|
||||||
// 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)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -55,13 +55,13 @@ func TestDialerBasicConnection(t *testing.T) {
|
|||||||
|
|
||||||
// TestJA3Fingerprint verifies the JA3/JA4 fingerprint matches expected value.
|
// TestJA3Fingerprint verifies the JA3/JA4 fingerprint matches expected value.
|
||||||
// This test uses tls.peet.ws to verify the fingerprint.
|
// This test uses tls.peet.ws to verify the fingerprint.
|
||||||
// Expected JA3 hash: 1a28e69016765d92e3b381168d68922c (Claude CLI / Node.js 20.x)
|
// Expected JA3 hash: 44f88fca027f27bab4bb08d4af15f23e (Node.js 24.x)
|
||||||
// Expected JA4: t13d5911h1_a33745022dd6_1f22a2ca17c4 (d=domain) or t13i5911h1_... (i=IP)
|
// Expected JA4: t13d1714h1_5b57614c22b0_7baf387fc6ff
|
||||||
func TestJA3Fingerprint(t *testing.T) {
|
func TestJA3Fingerprint(t *testing.T) {
|
||||||
skipNetworkTest(t)
|
skipNetworkTest(t)
|
||||||
|
|
||||||
profile := &Profile{
|
profile := &Profile{
|
||||||
Name: "Claude CLI Test",
|
Name: "Default Profile Test",
|
||||||
EnableGREASE: false,
|
EnableGREASE: false,
|
||||||
}
|
}
|
||||||
dialer := NewDialer(profile, nil)
|
dialer := NewDialer(profile, nil)
|
||||||
@ -81,7 +81,7 @@ func TestJA3Fingerprint(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create request: %v", err)
|
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)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -107,34 +107,28 @@ func TestJA3Fingerprint(t *testing.T) {
|
|||||||
t.Logf("PeetPrint: %s", fpResp.TLS.PeetPrint)
|
t.Logf("PeetPrint: %s", fpResp.TLS.PeetPrint)
|
||||||
t.Logf("PeetPrint Hash: %s", fpResp.TLS.PeetPrintHash)
|
t.Logf("PeetPrint Hash: %s", fpResp.TLS.PeetPrintHash)
|
||||||
|
|
||||||
// Verify JA3 hash matches expected value
|
// Verify JA3 hash matches expected value (Node.js 24.x default)
|
||||||
expectedJA3Hash := "1a28e69016765d92e3b381168d68922c"
|
expectedJA3Hash := "44f88fca027f27bab4bb08d4af15f23e"
|
||||||
if fpResp.TLS.JA3Hash == expectedJA3Hash {
|
if fpResp.TLS.JA3Hash == expectedJA3Hash {
|
||||||
t.Logf("✓ JA3 hash matches expected value: %s", expectedJA3Hash)
|
t.Logf("✓ JA3 hash matches expected value: %s", expectedJA3Hash)
|
||||||
} else {
|
} else {
|
||||||
t.Errorf("✗ JA3 hash mismatch: got %s, expected %s", fpResp.TLS.JA3Hash, expectedJA3Hash)
|
t.Errorf("✗ JA3 hash mismatch: got %s, expected %s", fpResp.TLS.JA3Hash, expectedJA3Hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify JA4 fingerprint
|
// Verify JA4 cipher hash (stable middle part)
|
||||||
// JA4 format: t[version][sni][cipher_count][ext_count][alpn]_[cipher_hash]_[ext_hash]
|
expectedJA4CipherHash := "_5b57614c22b0_"
|
||||||
// Expected: t13d5910h1 (d=domain) or t13i5910h1 (i=IP)
|
if strings.Contains(fpResp.TLS.JA4, expectedJA4CipherHash) {
|
||||||
// The suffix _a33745022dd6_1f22a2ca17c4 should match
|
t.Logf("✓ JA4 cipher hash matches: %s", expectedJA4CipherHash)
|
||||||
expectedJA4Suffix := "_a33745022dd6_1f22a2ca17c4"
|
|
||||||
if strings.HasSuffix(fpResp.TLS.JA4, expectedJA4Suffix) {
|
|
||||||
t.Logf("✓ JA4 suffix matches expected value: %s", expectedJA4Suffix)
|
|
||||||
} else {
|
} 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)
|
// Verify JA4 prefix (t13d1714h1 or t13i1714h1)
|
||||||
// d = domain (SNI present), i = IP (no SNI)
|
expectedJA4Prefix := "t13d1714h1"
|
||||||
// Since we connect to tls.peet.ws (domain), we expect 'd'
|
|
||||||
expectedJA4Prefix := "t13d5911h1"
|
|
||||||
if strings.HasPrefix(fpResp.TLS.JA4, expectedJA4Prefix) {
|
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 {
|
} else {
|
||||||
// Also accept 'i' variant for IP connections
|
altPrefix := "t13i1714h1"
|
||||||
altPrefix := "t13i5911h1"
|
|
||||||
if strings.HasPrefix(fpResp.TLS.JA4, altPrefix) {
|
if strings.HasPrefix(fpResp.TLS.JA4, altPrefix) {
|
||||||
t.Logf("✓ JA4 prefix matches (IP variant): %s", altPrefix)
|
t.Logf("✓ JA4 prefix matches (IP variant): %s", altPrefix)
|
||||||
} else {
|
} else {
|
||||||
@ -142,16 +136,15 @@ func TestJA3Fingerprint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify JA3 contains expected cipher suites (TLS 1.3 ciphers at the beginning)
|
// Verify JA3 contains expected TLS 1.3 cipher suites
|
||||||
if strings.Contains(fpResp.TLS.JA3, "4866-4867-4865") {
|
if strings.Contains(fpResp.TLS.JA3, "4865-4866-4867") {
|
||||||
t.Logf("✓ JA3 contains expected TLS 1.3 cipher suites")
|
t.Logf("✓ JA3 contains expected TLS 1.3 cipher suites")
|
||||||
} else {
|
} else {
|
||||||
t.Logf("Warning: JA3 does not contain expected TLS 1.3 cipher suites")
|
t.Logf("Warning: JA3 does not contain expected TLS 1.3 cipher suites")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify extension list (should be 11 extensions including SNI)
|
// Verify extension list (14 extensions, Node.js 24.x order)
|
||||||
// Expected: 0-11-10-35-16-22-23-13-43-45-51
|
expectedExtensions := "0-65037-23-65281-10-11-35-16-5-13-18-51-45-43"
|
||||||
expectedExtensions := "0-11-10-35-16-22-23-13-43-45-51"
|
|
||||||
if strings.Contains(fpResp.TLS.JA3, expectedExtensions) {
|
if strings.Contains(fpResp.TLS.JA3, expectedExtensions) {
|
||||||
t.Logf("✓ JA3 contains expected extension list: %s", expectedExtensions)
|
t.Logf("✓ JA3 contains expected extension list: %s", expectedExtensions)
|
||||||
} else {
|
} else {
|
||||||
@ -186,8 +179,8 @@ func TestDialerWithProfile(t *testing.T) {
|
|||||||
// Build specs and compare
|
// Build specs and compare
|
||||||
// Note: We can't directly compare JA3 without making network requests
|
// Note: We can't directly compare JA3 without making network requests
|
||||||
// but we can verify the specs are different
|
// but we can verify the specs are different
|
||||||
spec1 := dialer1.buildClientHelloSpec()
|
spec1 := buildClientHelloSpecFromProfile(dialer1.profile)
|
||||||
spec2 := dialer2.buildClientHelloSpec()
|
spec2 := buildClientHelloSpecFromProfile(dialer2.profile)
|
||||||
|
|
||||||
// Profile with GREASE should have more extensions
|
// Profile with GREASE should have more extensions
|
||||||
if len(spec2.Extensions) <= len(spec1.Extensions) {
|
if len(spec2.Extensions) <= len(spec1.Extensions) {
|
||||||
@ -296,47 +289,33 @@ func mustParseURL(rawURL string) *url.URL {
|
|||||||
return u
|
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.
|
// TestAllProfiles tests multiple TLS fingerprint profiles against tls.peet.ws.
|
||||||
// Run with: go test -v -run TestAllProfiles ./internal/pkg/tlsfingerprint/...
|
// Run with: go test -v -run TestAllProfiles ./internal/pkg/tlsfingerprint/...
|
||||||
func TestAllProfiles(t *testing.T) {
|
func TestAllProfiles(t *testing.T) {
|
||||||
skipNetworkTest(t)
|
skipNetworkTest(t)
|
||||||
|
|
||||||
// Define all profiles to test with their expected fingerprints
|
|
||||||
// These profiles are from config.yaml gateway.tls_fingerprint.profiles
|
|
||||||
profiles := []TestProfileExpectation{
|
profiles := []TestProfileExpectation{
|
||||||
{
|
{
|
||||||
// Linux x64 Node.js v22.17.1
|
// Default profile (Node.js 24.x)
|
||||||
// Expected JA3 Hash: 1a28e69016765d92e3b381168d68922c
|
// JA3 Hash: 44f88fca027f27bab4bb08d4af15f23e
|
||||||
// Expected JA4: t13d5911h1_a33745022dd6_1f22a2ca17c4
|
// 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{
|
Profile: &Profile{
|
||||||
Name: "linux_x64_node_v22171",
|
Name: "linux_x64_node_v22171",
|
||||||
EnableGREASE: false,
|
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},
|
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},
|
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
|
JA4CipherHash: "a33745022dd6",
|
||||||
},
|
|
||||||
{
|
|
||||||
// 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)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -8,6 +8,14 @@ type FingerprintResponse struct {
|
|||||||
HTTP2 any `json:"http2"`
|
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.
|
// TLSInfo contains TLS fingerprint details.
|
||||||
type TLSInfo struct {
|
type TLSInfo struct {
|
||||||
JA3 string `json:"ja3"`
|
JA3 string `json:"ja3"`
|
||||||
|
|||||||
@ -68,10 +68,9 @@ func (s *claudeUsageService) FetchUsageWithOptions(ctx context.Context, opts *se
|
|||||||
|
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
|
|
||||||
// 如果启用 TLS 指纹且有 HTTPUpstream,使用 DoWithTLS
|
// 如果有 TLS Profile 且有 HTTPUpstream,使用 DoWithTLS
|
||||||
if opts.EnableTLSFingerprint && s.httpUpstream != nil {
|
if opts.TLSProfile != nil && s.httpUpstream != nil {
|
||||||
// accountConcurrency 传 0 使用默认连接池配置,usage 请求不需要特殊的并发设置
|
resp, err = s.httpUpstream.DoWithTLS(req, opts.ProxyURL, opts.AccountID, 0, opts.TLSProfile)
|
||||||
resp, err = s.httpUpstream.DoWithTLS(req, opts.ProxyURL, opts.AccountID, 0, true)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("request with TLS fingerprint failed: %w", err)
|
return nil, fmt.Errorf("request with TLS fingerprint failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -164,88 +164,59 @@ func (s *httpUpstreamService) Do(req *http.Request, proxyURL string, accountID i
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DoWithTLS 执行带 TLS 指纹伪装的 HTTP 请求
|
// DoWithTLS 执行带 TLS 指纹伪装的 HTTP 请求
|
||||||
// 根据 enableTLSFingerprint 参数决定是否使用 TLS 指纹
|
|
||||||
//
|
//
|
||||||
// 参数:
|
// mode 决定指纹策略:
|
||||||
// - req: HTTP 请求对象
|
// - TLSModeOff / "": 不启用,行为与 Do 相同
|
||||||
// - proxyURL: 代理地址,空字符串表示直连
|
// - TLSModeNode: 走本地 Node.js TLS 代理(需 gateway.node_tls_proxy.enabled=true)
|
||||||
// - accountID: 账户 ID,用于账户级隔离和 TLS 指纹模板选择
|
// - TLSModeUTLS: 用 profile 模拟 TLS ClientHello(profile 为 nil 时降级为 Off)
|
||||||
// - accountConcurrency: 账户并发限制,用于动态调整连接池大小
|
func (s *httpUpstreamService) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, mode service.TLSMode, profile *tlsfingerprint.Profile) (*http.Response, error) {
|
||||||
// - enableTLSFingerprint: 是否启用 TLS 指纹伪装
|
switch mode {
|
||||||
//
|
case service.TLSModeNode:
|
||||||
// TLS 指纹说明:
|
if s.isNodeTLSProxyEnabled() && req != nil && req.URL != nil && s.shouldRouteViaNodeProxy(req) {
|
||||||
// - 优先使用 Node.js TLS 代理模式(gateway.node_tls_proxy.enabled):
|
|
||||||
// 将请求改为 HTTP 明文发送到本地 Node.js 代理,由 Node.js 原生 TLS 栈完成上游握手,
|
|
||||||
// JA3/JA4 指纹天然匹配 Claude CLI,无需 uTLS 模拟。
|
|
||||||
// - 回退到 uTLS 模式(gateway.tls_fingerprint.enabled):
|
|
||||||
// 使用 utls 库模拟 Claude CLI 的 TLS ClientHello。
|
|
||||||
func (s *httpUpstreamService) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) {
|
|
||||||
// 如果未启用 TLS 指纹,直接使用标准请求路径
|
|
||||||
if !enableTLSFingerprint {
|
|
||||||
return s.Do(req, proxyURL, accountID, accountConcurrency)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优先使用 Node.js TLS 代理模式(仅 Anthropic API)
|
|
||||||
// Antigravity (googleapis) 使用 Go 原生 TLS(更接近真实 BoringCrypto 指纹)
|
|
||||||
if s.isNodeTLSProxyEnabled() && req != nil && req.URL != nil {
|
|
||||||
host := req.URL.Hostname()
|
|
||||||
if host == "api.anthropic.com" {
|
|
||||||
return s.doViaNodeTLSProxy(req, proxyURL, accountID, accountConcurrency)
|
return s.doViaNodeTLSProxy(req, proxyURL, accountID, accountConcurrency)
|
||||||
}
|
}
|
||||||
}
|
return s.Do(req, proxyURL, accountID, accountConcurrency)
|
||||||
|
|
||||||
// TLS 指纹已启用,记录调试日志
|
case service.TLSModeUTLS:
|
||||||
targetHost := ""
|
if profile == nil {
|
||||||
if req != nil && req.URL != nil {
|
return s.Do(req, proxyURL, accountID, accountConcurrency)
|
||||||
targetHost = req.URL.Host
|
}
|
||||||
}
|
targetHost := ""
|
||||||
proxyInfo := "direct"
|
if req != nil && req.URL != nil {
|
||||||
if proxyURL != "" {
|
targetHost = req.URL.Host
|
||||||
proxyInfo = proxyURL
|
}
|
||||||
}
|
proxyInfo := "direct"
|
||||||
slog.Debug("tls_fingerprint_enabled", "account_id", accountID, "target", targetHost, "proxy", proxyInfo)
|
if proxyURL != "" {
|
||||||
|
proxyInfo = proxyURL
|
||||||
|
}
|
||||||
|
slog.Debug("tls_fingerprint_utls", "account_id", accountID, "target", targetHost, "proxy", proxyInfo, "profile", profile.Name)
|
||||||
|
|
||||||
if err := s.validateRequestHost(req); err != nil {
|
if err := s.validateRequestHost(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
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())
|
||||||
|
})
|
||||||
|
return resp, nil
|
||||||
|
|
||||||
// 获取 TLS 指纹 Profile
|
default: // TLSModeOff 或空字符串
|
||||||
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)
|
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)
|
|
||||||
|
|
||||||
// 包装响应体,在关闭时自动减少计数并更新时间戳
|
|
||||||
resp.Body = wrapTrackedBody(resp.Body, func() {
|
|
||||||
atomic.AddInt64(&entry.inFlight, -1)
|
|
||||||
atomic.StoreInt64(&entry.lastUsed, time.Now().UnixNano())
|
|
||||||
})
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
122
backend/internal/repository/tls_fingerprint_profile_cache.go
Normal file
122
backend/internal/repository/tls_fingerprint_profile_cache.go
Normal file
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
213
backend/internal/repository/tls_fingerprint_profile_repo.go
Normal file
213
backend/internal/repository/tls_fingerprint_profile_repo.go
Normal file
@ -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
|
||||||
|
}
|
||||||
@ -73,6 +73,7 @@ var ProviderSet = wire.NewSet(
|
|||||||
NewUserAttributeValueRepository,
|
NewUserAttributeValueRepository,
|
||||||
NewUserGroupRateRepository,
|
NewUserGroupRateRepository,
|
||||||
NewErrorPassthroughRepository,
|
NewErrorPassthroughRepository,
|
||||||
|
NewTLSFingerprintProfileRepository,
|
||||||
|
|
||||||
// Cache implementations
|
// Cache implementations
|
||||||
NewGatewayCache,
|
NewGatewayCache,
|
||||||
@ -96,6 +97,7 @@ var ProviderSet = wire.NewSet(
|
|||||||
NewTotpCache,
|
NewTotpCache,
|
||||||
NewRefreshTokenCache,
|
NewRefreshTokenCache,
|
||||||
NewErrorPassthroughCache,
|
NewErrorPassthroughCache,
|
||||||
|
NewTLSFingerprintProfileCache,
|
||||||
|
|
||||||
// Encryptors
|
// Encryptors
|
||||||
NewAESEncryptor,
|
NewAESEncryptor,
|
||||||
|
|||||||
@ -79,6 +79,9 @@ func RegisterAdminRoutes(
|
|||||||
// 错误透传规则管理
|
// 错误透传规则管理
|
||||||
registerErrorPassthroughRoutes(admin, h)
|
registerErrorPassthroughRoutes(admin, h)
|
||||||
|
|
||||||
|
// TLS 指纹模板管理
|
||||||
|
registerTLSFingerprintProfileRoutes(admin, h)
|
||||||
|
|
||||||
// API Key 管理
|
// API Key 管理
|
||||||
registerAdminAPIKeyRoutes(admin, h)
|
registerAdminAPIKeyRoutes(admin, h)
|
||||||
|
|
||||||
@ -552,3 +555,14 @@ func registerErrorPassthroughRoutes(admin *gin.RouterGroup, h *handler.Handlers)
|
|||||||
rules.DELETE("/:id", h.Admin.ErrorPassthrough.Delete)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1166,6 +1166,48 @@ func (a *Account) IsTLSFingerprintEnabled() bool {
|
|||||||
return geminiTLSFingerprintEnabled(a)
|
return geminiTLSFingerprintEnabled(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTLSMode 获取账号配置的 TLS 指纹模式
|
||||||
|
// 返回值: TLSModeNode / TLSModeUTLS / TLSModeOff
|
||||||
|
// 存储在 Extra["tls_mode"],未设置时:支持指纹的账号默认 node,其余 off
|
||||||
|
func (a *Account) GetTLSMode() TLSMode {
|
||||||
|
if a.Extra != nil {
|
||||||
|
if m, ok := a.Extra["tls_mode"].(string); ok && m != "" {
|
||||||
|
switch TLSMode(m) {
|
||||||
|
case TLSModeNode, TLSModeUTLS, TLSModeOff:
|
||||||
|
return TLSMode(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a.IsTLSFingerprintEnabled() {
|
||||||
|
return TLSModeNode
|
||||||
|
}
|
||||||
|
return TLSModeOff
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserMsgQueueMode 获取用户消息队列模式
|
// GetUserMsgQueueMode 获取用户消息队列模式
|
||||||
// "serialize" = 串行队列, "throttle" = 软性限速, "" = 未设置(使用全局配置)
|
// "serialize" = 串行队列, "throttle" = 软性限速, "" = 未设置(使用全局配置)
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/geminicli"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/geminicli"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
"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/soraerror"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/util/urlvalidator"
|
"github.com/Wei-Shaw/sub2api/internal/util/urlvalidator"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@ -69,6 +70,7 @@ type AccountTestService struct {
|
|||||||
antigravityGatewayService *AntigravityGatewayService
|
antigravityGatewayService *AntigravityGatewayService
|
||||||
httpUpstream HTTPUpstream
|
httpUpstream HTTPUpstream
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
|
tlsFPProfileService *TLSFingerprintProfileService
|
||||||
soraTestGuardMu sync.Mutex
|
soraTestGuardMu sync.Mutex
|
||||||
soraTestLastRun map[int64]time.Time
|
soraTestLastRun map[int64]time.Time
|
||||||
soraTestCooldown time.Duration
|
soraTestCooldown time.Duration
|
||||||
@ -83,6 +85,7 @@ func NewAccountTestService(
|
|||||||
antigravityGatewayService *AntigravityGatewayService,
|
antigravityGatewayService *AntigravityGatewayService,
|
||||||
httpUpstream HTTPUpstream,
|
httpUpstream HTTPUpstream,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
|
tlsFPProfileService *TLSFingerprintProfileService,
|
||||||
) *AccountTestService {
|
) *AccountTestService {
|
||||||
return &AccountTestService{
|
return &AccountTestService{
|
||||||
accountRepo: accountRepo,
|
accountRepo: accountRepo,
|
||||||
@ -90,6 +93,7 @@ func NewAccountTestService(
|
|||||||
antigravityGatewayService: antigravityGatewayService,
|
antigravityGatewayService: antigravityGatewayService,
|
||||||
httpUpstream: httpUpstream,
|
httpUpstream: httpUpstream,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
tlsFPProfileService: tlsFPProfileService,
|
||||||
soraTestLastRun: make(map[int64]time.Time),
|
soraTestLastRun: make(map[int64]time.Time),
|
||||||
soraTestCooldown: defaultSoraTestCooldown,
|
soraTestCooldown: defaultSoraTestCooldown,
|
||||||
}
|
}
|
||||||
@ -300,7 +304,7 @@ func (s *AccountTestService) testClaudeAccountConnection(c *gin.Context, account
|
|||||||
proxyURL = account.Proxy.URL()
|
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 {
|
if err != nil {
|
||||||
return s.sendErrorAndEnd(c, fmt.Sprintf("Request failed: %s", err.Error()))
|
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()
|
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 {
|
if err != nil {
|
||||||
return s.sendErrorAndEnd(c, fmt.Sprintf("Request failed: %s", err.Error()))
|
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()
|
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 {
|
if err != nil {
|
||||||
return s.sendErrorAndEnd(c, fmt.Sprintf("Request failed: %s", err.Error()))
|
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()
|
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 {
|
if err != nil {
|
||||||
return s.sendErrorAndEnd(c, fmt.Sprintf("Request failed: %s", err.Error()))
|
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 {
|
if account.ProxyID != nil && account.Proxy != nil {
|
||||||
proxyURL = account.Proxy.URL()
|
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 {
|
if err != nil {
|
||||||
recorder.addStep("me", "failed", 0, "network_error", err.Error())
|
recorder.addStep("me", "failed", 0, "network_error", err.Error())
|
||||||
s.emitSoraProbeSummary(c, recorder)
|
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("Origin", "https://sora.chatgpt.com")
|
||||||
subReq.Header.Set("Referer", "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 {
|
if subErr != nil {
|
||||||
recorder.addStep("subscription", "failed", 0, "network_error", subErr.Error())
|
recorder.addStep("subscription", "failed", 0, "network_error", subErr.Error())
|
||||||
s.sendEvent(c, TestEvent{Type: "content", Text: fmt.Sprintf("Subscription check skipped: %s", 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 的测试思路):邀请码 + 剩余额度。
|
// 追加 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.emitSoraProbeSummary(c, recorder)
|
||||||
s.sendEvent(c, TestEvent{Type: "test_complete", Success: true})
|
s.sendEvent(c, TestEvent{Type: "test_complete", Success: true})
|
||||||
@ -990,7 +994,7 @@ func (s *AccountTestService) testSora2Capabilities(
|
|||||||
account *Account,
|
account *Account,
|
||||||
authToken string,
|
authToken string,
|
||||||
proxyURL string,
|
proxyURL string,
|
||||||
enableTLSFingerprint bool,
|
tlsProfile *tlsfingerprint.Profile,
|
||||||
recorder *soraProbeRecorder,
|
recorder *soraProbeRecorder,
|
||||||
) {
|
) {
|
||||||
inviteStatus, inviteHeader, inviteBody, err := s.fetchSoraTestEndpoint(
|
inviteStatus, inviteHeader, inviteBody, err := s.fetchSoraTestEndpoint(
|
||||||
@ -999,7 +1003,7 @@ func (s *AccountTestService) testSora2Capabilities(
|
|||||||
authToken,
|
authToken,
|
||||||
soraInviteMineURL,
|
soraInviteMineURL,
|
||||||
proxyURL,
|
proxyURL,
|
||||||
enableTLSFingerprint,
|
tlsProfile,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if recorder != nil {
|
if recorder != nil {
|
||||||
@ -1016,7 +1020,7 @@ func (s *AccountTestService) testSora2Capabilities(
|
|||||||
authToken,
|
authToken,
|
||||||
soraBootstrapURL,
|
soraBootstrapURL,
|
||||||
proxyURL,
|
proxyURL,
|
||||||
enableTLSFingerprint,
|
tlsProfile,
|
||||||
)
|
)
|
||||||
if bootstrapErr == nil && bootstrapStatus == http.StatusOK {
|
if bootstrapErr == nil && bootstrapStatus == http.StatusOK {
|
||||||
if recorder != nil {
|
if recorder != nil {
|
||||||
@ -1029,7 +1033,7 @@ func (s *AccountTestService) testSora2Capabilities(
|
|||||||
authToken,
|
authToken,
|
||||||
soraInviteMineURL,
|
soraInviteMineURL,
|
||||||
proxyURL,
|
proxyURL,
|
||||||
enableTLSFingerprint,
|
tlsProfile,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if recorder != nil {
|
if recorder != nil {
|
||||||
@ -1081,7 +1085,7 @@ func (s *AccountTestService) testSora2Capabilities(
|
|||||||
authToken,
|
authToken,
|
||||||
soraRemainingURL,
|
soraRemainingURL,
|
||||||
proxyURL,
|
proxyURL,
|
||||||
enableTLSFingerprint,
|
tlsProfile,
|
||||||
)
|
)
|
||||||
if remainingErr != nil {
|
if remainingErr != nil {
|
||||||
if recorder != nil {
|
if recorder != nil {
|
||||||
@ -1122,7 +1126,7 @@ func (s *AccountTestService) fetchSoraTestEndpoint(
|
|||||||
authToken string,
|
authToken string,
|
||||||
url string,
|
url string,
|
||||||
proxyURL string,
|
proxyURL string,
|
||||||
enableTLSFingerprint bool,
|
tlsProfile *tlsfingerprint.Profile,
|
||||||
) (int, http.Header, []byte, error) {
|
) (int, http.Header, []byte, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1135,7 +1139,7 @@ func (s *AccountTestService) fetchSoraTestEndpoint(
|
|||||||
req.Header.Set("Origin", "https://sora.chatgpt.com")
|
req.Header.Set("Origin", "https://sora.chatgpt.com")
|
||||||
req.Header.Set("Referer", "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 {
|
if err != nil {
|
||||||
return 0, nil, nil, err
|
return 0, nil, nil, err
|
||||||
}
|
}
|
||||||
@ -1224,11 +1228,12 @@ func parseSoraRemainingSummary(body []byte) string {
|
|||||||
return strings.Join(parts, " | ")
|
return strings.Join(parts, " | ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AccountTestService) shouldEnableSoraTLSFingerprint() bool {
|
func (s *AccountTestService) resolveSoraTLSProfile() *tlsfingerprint.Profile {
|
||||||
if s == nil || s.cfg == nil {
|
if s == nil || s.cfg == nil || !s.cfg.Sora.Client.DisableTLSFingerprint {
|
||||||
return true
|
// 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 {
|
func isCloudflareChallengeResponse(statusCode int, headers http.Header, body []byte) bool {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/require"
|
"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")
|
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.requests = append(u.requests, req)
|
||||||
u.tlsFlags = append(u.tlsFlags, enableTLSFingerprint)
|
u.tlsFlags = append(u.tlsFlags, profile != nil)
|
||||||
if len(u.responses) == 0 {
|
if len(u.responses) == 0 {
|
||||||
return nil, fmt.Errorf("no mocked response")
|
return nil, fmt.Errorf("no mocked response")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import (
|
|||||||
openaipkg "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
openaipkg "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"golang.org/x/sync/singleflight"
|
"golang.org/x/sync/singleflight"
|
||||||
@ -241,11 +242,11 @@ type ClaudeUsageResponse struct {
|
|||||||
|
|
||||||
// ClaudeUsageFetchOptions 包含获取 Claude 用量数据所需的所有选项
|
// ClaudeUsageFetchOptions 包含获取 Claude 用量数据所需的所有选项
|
||||||
type ClaudeUsageFetchOptions struct {
|
type ClaudeUsageFetchOptions struct {
|
||||||
AccessToken string // OAuth access token
|
AccessToken string // OAuth access token
|
||||||
ProxyURL string // 代理 URL(可选)
|
ProxyURL string // 代理 URL(可选)
|
||||||
AccountID int64 // 账号 ID(用于 TLS 指纹选择)
|
AccountID int64 // 账号 ID(用于连接池隔离)
|
||||||
EnableTLSFingerprint bool // 是否启用 TLS 指纹伪装
|
TLSProfile *tlsfingerprint.Profile // TLS 指纹 Profile(nil 表示不启用)
|
||||||
Fingerprint *Fingerprint // 缓存的指纹信息(User-Agent 等)
|
Fingerprint *Fingerprint // 缓存的指纹信息(User-Agent 等)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClaudeUsageFetcher fetches usage data from Anthropic OAuth API
|
// ClaudeUsageFetcher fetches usage data from Anthropic OAuth API
|
||||||
@ -264,6 +265,7 @@ type AccountUsageService struct {
|
|||||||
antigravityQuotaFetcher *AntigravityQuotaFetcher
|
antigravityQuotaFetcher *AntigravityQuotaFetcher
|
||||||
cache *UsageCache
|
cache *UsageCache
|
||||||
identityCache IdentityCache
|
identityCache IdentityCache
|
||||||
|
tlsFPProfileService *TLSFingerprintProfileService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAccountUsageService 创建AccountUsageService实例
|
// NewAccountUsageService 创建AccountUsageService实例
|
||||||
@ -275,6 +277,7 @@ func NewAccountUsageService(
|
|||||||
antigravityQuotaFetcher *AntigravityQuotaFetcher,
|
antigravityQuotaFetcher *AntigravityQuotaFetcher,
|
||||||
cache *UsageCache,
|
cache *UsageCache,
|
||||||
identityCache IdentityCache,
|
identityCache IdentityCache,
|
||||||
|
tlsFPProfileService *TLSFingerprintProfileService,
|
||||||
) *AccountUsageService {
|
) *AccountUsageService {
|
||||||
return &AccountUsageService{
|
return &AccountUsageService{
|
||||||
accountRepo: accountRepo,
|
accountRepo: accountRepo,
|
||||||
@ -284,6 +287,7 @@ func NewAccountUsageService(
|
|||||||
antigravityQuotaFetcher: antigravityQuotaFetcher,
|
antigravityQuotaFetcher: antigravityQuotaFetcher,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
identityCache: identityCache,
|
identityCache: identityCache,
|
||||||
|
tlsFPProfileService: tlsFPProfileService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1155,10 +1159,10 @@ func (s *AccountUsageService) fetchOAuthUsageRaw(ctx context.Context, account *A
|
|||||||
|
|
||||||
// 构建完整的选项
|
// 构建完整的选项
|
||||||
opts := &ClaudeUsageFetchOptions{
|
opts := &ClaudeUsageFetchOptions{
|
||||||
AccessToken: accessToken,
|
AccessToken: accessToken,
|
||||||
ProxyURL: proxyURL,
|
ProxyURL: proxyURL,
|
||||||
AccountID: account.ID,
|
AccountID: account.ID,
|
||||||
EnableTLSFingerprint: account.IsTLSFingerprintEnabled(),
|
TLSProfile: s.tlsFPProfileService.ResolveTLSProfile(account),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试获取缓存的 Fingerprint(包含 User-Agent 等信息)
|
// 尝试获取缓存的 Fingerprint(包含 User-Agent 等信息)
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@ -130,7 +131,7 @@ func (s *httpUpstreamStub) Do(_ *http.Request, _ string, _ int64, _ int) (*http.
|
|||||||
return s.resp, s.err
|
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
|
return s.resp, s.err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,7 +172,7 @@ func (s *queuedHTTPUpstreamStub) Do(req *http.Request, _ string, _ int64, _ int)
|
|||||||
return resp, err
|
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)
|
return s.Do(req, proxyURL, accountID, concurrency)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -40,7 +41,7 @@ func (r *recordingOKUpstream) Do(req *http.Request, proxyURL string, accountID i
|
|||||||
}, nil
|
}, 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)
|
return r.Do(req, proxyURL, accountID, accountConcurrency)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,7 +62,7 @@ func (s *stubAntigravityUpstream) Do(req *http.Request, proxyURL string, account
|
|||||||
}, nil
|
}, 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)
|
return s.Do(req, proxyURL, accountID, accountConcurrency)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -93,7 +94,7 @@ func (m *mockSmartRetryUpstream) Do(req *http.Request, proxyURL string, accountI
|
|||||||
}, respErr
|
}, 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)
|
return m.Do(req, proxyURL, accountID, accountConcurrency)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,7 +36,7 @@ func (u *epFixedUpstream) Do(req *http.Request, proxyURL string, accountID int64
|
|||||||
}, nil
|
}, 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)
|
return u.Do(req, proxyURL, accountID, accountConcurrency)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
@ -60,7 +61,7 @@ func (u *anthropicHTTPUpstreamRecorder) Do(req *http.Request, proxyURL string, a
|
|||||||
return u.resp, nil
|
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)
|
return u.Do(req, proxyURL, accountID, accountConcurrency)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -120,7 +120,7 @@ func (s *GatewayService) ForwardAsChatCompletions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 11. Send request
|
// 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 err != nil {
|
||||||
if resp != nil && resp.Body != nil {
|
if resp != nil && resp.Body != nil {
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
|
|||||||
@ -117,7 +117,7 @@ func (s *GatewayService) ForwardAsResponses(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 11. Send request
|
// 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 err != nil {
|
||||||
if resp != nil && resp.Body != nil {
|
if resp != nil && resp.Body != nil {
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
|
|||||||
@ -40,6 +40,7 @@ func newGatewayRecordUsageServiceForTest(usageRepo UsageLogRepository, userRepo
|
|||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -563,6 +563,8 @@ type GatewayService struct {
|
|||||||
responseHeaderFilter *responseheaders.CompiledHeaderFilter
|
responseHeaderFilter *responseheaders.CompiledHeaderFilter
|
||||||
debugModelRouting atomic.Bool
|
debugModelRouting atomic.Bool
|
||||||
debugClaudeMimic 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
|
// NewGatewayService creates a new GatewayService
|
||||||
@ -589,6 +591,7 @@ func NewGatewayService(
|
|||||||
rpmCache RPMCache,
|
rpmCache RPMCache,
|
||||||
digestStore *DigestSessionStore,
|
digestStore *DigestSessionStore,
|
||||||
settingService *SettingService,
|
settingService *SettingService,
|
||||||
|
tlsFPProfileService *TLSFingerprintProfileService,
|
||||||
) *GatewayService {
|
) *GatewayService {
|
||||||
userGroupRateTTL := resolveUserGroupRateCacheTTL(cfg)
|
userGroupRateTTL := resolveUserGroupRateCacheTTL(cfg)
|
||||||
modelsListTTL := resolveModelsListCacheTTL(cfg)
|
modelsListTTL := resolveModelsListCacheTTL(cfg)
|
||||||
@ -620,6 +623,7 @@ func NewGatewayService(
|
|||||||
modelsListCache: gocache.New(modelsListTTL, time.Minute),
|
modelsListCache: gocache.New(modelsListTTL, time.Minute),
|
||||||
modelsListCacheTTL: modelsListTTL,
|
modelsListCacheTTL: modelsListTTL,
|
||||||
responseHeaderFilter: compileResponseHeaderFilter(cfg),
|
responseHeaderFilter: compileResponseHeaderFilter(cfg),
|
||||||
|
tlsFPProfileService: tlsFPProfileService,
|
||||||
}
|
}
|
||||||
svc.userGroupRateResolver = newUserGroupRateResolver(
|
svc.userGroupRateResolver = newUserGroupRateResolver(
|
||||||
userGroupRateRepo,
|
userGroupRateRepo,
|
||||||
@ -4136,9 +4140,12 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
|
|||||||
proxyURL = account.Proxy.URL()
|
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",
|
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.
|
// Pre-filter: strip empty text blocks (including nested in tool_result) to prevent upstream 400.
|
||||||
body = StripEmptyTextBlocks(body)
|
body = StripEmptyTextBlocks(body)
|
||||||
|
|
||||||
@ -4158,7 +4165,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 err != nil {
|
||||||
if resp != nil && resp.Body != nil {
|
if resp != nil && resp.Body != nil {
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
@ -4236,7 +4243,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)
|
retryReq, buildErr := s.buildUpstreamRequest(retryCtx, c, account, filteredBody, token, tokenType, reqModel, reqStream, shouldMimicClaudeCode)
|
||||||
releaseRetryCtx()
|
releaseRetryCtx()
|
||||||
if buildErr == nil {
|
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 retryErr == nil {
|
||||||
if retryResp.StatusCode < 400 {
|
if retryResp.StatusCode < 400 {
|
||||||
logger.LegacyPrintf("service.gateway", "Account %d: thinking block retry succeeded (blocks downgraded)", account.ID)
|
logger.LegacyPrintf("service.gateway", "Account %d: thinking block retry succeeded (blocks downgraded)", account.ID)
|
||||||
@ -4271,7 +4278,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)
|
retryReq2, buildErr2 := s.buildUpstreamRequest(retryCtx2, c, account, filteredBody2, token, tokenType, reqModel, reqStream, shouldMimicClaudeCode)
|
||||||
releaseRetryCtx2()
|
releaseRetryCtx2()
|
||||||
if buildErr2 == nil {
|
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 {
|
if retryErr2 == nil {
|
||||||
resp = retryResp2
|
resp = retryResp2
|
||||||
break
|
break
|
||||||
@ -4342,7 +4349,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)
|
budgetRetryReq, buildErr := s.buildUpstreamRequest(budgetRetryCtx, c, account, rectifiedBody, token, tokenType, reqModel, reqStream, shouldMimicClaudeCode)
|
||||||
releaseBudgetRetryCtx()
|
releaseBudgetRetryCtx()
|
||||||
if buildErr == nil {
|
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 {
|
if retryErr == nil {
|
||||||
resp = budgetRetryResp
|
resp = budgetRetryResp
|
||||||
break
|
break
|
||||||
@ -4648,7 +4655,7 @@ func (s *GatewayService) forwardAnthropicAPIKeyPassthroughWithInput(
|
|||||||
return nil, err
|
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 err != nil {
|
||||||
if resp != nil && resp.Body != nil {
|
if resp != nil && resp.Body != nil {
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
@ -5366,7 +5373,7 @@ func (s *GatewayService) executeBedrockUpstream(
|
|||||||
return nil, err
|
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 err != nil {
|
||||||
if resp != nil && resp.Body != nil {
|
if resp != nil && resp.Body != nil {
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
@ -7978,7 +7985,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 {
|
if err != nil {
|
||||||
setOpsUpstreamError(c, 0, sanitizeUpstreamErrorMessage(err.Error()), "")
|
setOpsUpstreamError(c, 0, sanitizeUpstreamErrorMessage(err.Error()), "")
|
||||||
s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Request failed")
|
s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Request failed")
|
||||||
@ -8006,7 +8013,7 @@ func (s *GatewayService) ForwardCountTokens(ctx context.Context, c *gin.Context,
|
|||||||
filteredBody := FilterThinkingBlocksForRetry(body)
|
filteredBody := FilterThinkingBlocksForRetry(body)
|
||||||
retryReq, buildErr := s.buildCountTokensRequest(ctx, c, account, filteredBody, token, tokenType, reqModel, shouldMimicClaudeCode)
|
retryReq, buildErr := s.buildCountTokensRequest(ctx, c, account, filteredBody, token, tokenType, reqModel, shouldMimicClaudeCode)
|
||||||
if buildErr == nil {
|
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 {
|
if retryErr == nil {
|
||||||
resp = retryResp
|
resp = retryResp
|
||||||
respBody, err = readUpstreamResponseBodyLimited(resp.Body, maxReadBytes)
|
respBody, err = readUpstreamResponseBodyLimited(resp.Body, maxReadBytes)
|
||||||
@ -8095,7 +8102,7 @@ func (s *GatewayService) forwardCountTokensAnthropicAPIKeyPassthrough(ctx contex
|
|||||||
proxyURL = account.Proxy.URL()
|
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 {
|
if err != nil {
|
||||||
setOpsUpstreamError(c, 0, sanitizeUpstreamErrorMessage(err.Error()), "")
|
setOpsUpstreamError(c, 0, sanitizeUpstreamErrorMessage(err.Error()), "")
|
||||||
appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
|
appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@ -36,7 +37,7 @@ func (s *geminiCompatHTTPUpstreamStub) Do(req *http.Request, proxyURL string, ac
|
|||||||
return &resp, nil
|
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)
|
return s.Do(req, proxyURL, accountID, accountConcurrency)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,55 +1,24 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||||
|
)
|
||||||
|
|
||||||
// HTTPUpstream 上游 HTTP 请求接口
|
// HTTPUpstream 上游 HTTP 请求接口
|
||||||
// 用于向上游 API(Claude、OpenAI、Gemini 等)发送请求
|
// 用于向上游 API(Claude、OpenAI、Gemini 等)发送请求
|
||||||
// 这是一个通用接口,可用于任何基于 HTTP 的上游服务
|
|
||||||
//
|
|
||||||
// 设计说明:
|
|
||||||
// - 支持可选代理配置
|
|
||||||
// - 支持账户级连接池隔离
|
|
||||||
// - 实现类负责连接池管理和复用
|
|
||||||
// - 支持可选的 TLS 指纹伪装
|
|
||||||
type HTTPUpstream interface {
|
type HTTPUpstream interface {
|
||||||
// Do 执行 HTTP 请求
|
// Do 执行 HTTP 请求(不启用 TLS 指纹)
|
||||||
//
|
|
||||||
// 参数:
|
|
||||||
// - req: HTTP 请求对象,由调用方构建
|
|
||||||
// - proxyURL: 代理服务器地址,空字符串表示直连
|
|
||||||
// - accountID: 账户 ID,用于连接池隔离(隔离策略为 account 或 account_proxy 时生效)
|
|
||||||
// - accountConcurrency: 账户并发限制,用于动态调整连接池大小
|
|
||||||
//
|
|
||||||
// 返回:
|
|
||||||
// - *http.Response: HTTP 响应,调用方必须关闭 Body
|
|
||||||
// - error: 请求错误(网络错误、超时等)
|
|
||||||
//
|
|
||||||
// 注意:
|
|
||||||
// - 调用方必须关闭 resp.Body,否则会导致连接泄漏
|
|
||||||
// - 响应体可能已被包装以跟踪请求生命周期
|
|
||||||
Do(req *http.Request, proxyURL string, accountID int64, accountConcurrency int) (*http.Response, error)
|
Do(req *http.Request, proxyURL string, accountID int64, accountConcurrency int) (*http.Response, error)
|
||||||
|
|
||||||
// DoWithTLS 执行带 TLS 指纹伪装的 HTTP 请求
|
// DoWithTLS 执行带 TLS 指纹伪装的 HTTP 请求
|
||||||
//
|
//
|
||||||
// 参数:
|
// profile 参数:
|
||||||
// - req: HTTP 请求对象,由调用方构建
|
// - nil: 不启用 TLS 指纹,行为与 Do 方法相同
|
||||||
// - proxyURL: 代理服务器地址,空字符串表示直连
|
// - non-nil: 使用指定的 Profile 进行 TLS 指纹伪装
|
||||||
// - accountID: 账户 ID,用于连接池隔离和 TLS 指纹模板选择
|
|
||||||
// - accountConcurrency: 账户并发限制,用于动态调整连接池大小
|
|
||||||
// - enableTLSFingerprint: 是否启用 TLS 指纹伪装
|
|
||||||
//
|
//
|
||||||
// 返回:
|
// Profile 由调用方通过 TLSFingerprintProfileService 解析后传入,
|
||||||
// - *http.Response: HTTP 响应,调用方必须关闭 Body
|
// 支持按账号绑定的数据库 profile 或内置默认 profile。
|
||||||
// - error: 请求错误(网络错误、超时等)
|
DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, 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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
@ -43,7 +44,7 @@ func (u *httpUpstreamRecorder) Do(req *http.Request, proxyURL string, accountID
|
|||||||
return u.resp, nil
|
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)
|
return u.Do(req, proxyURL, accountID, accountConcurrency)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/stretchr/testify/require"
|
"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
|
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)
|
return u.Do(req, proxyURL, accountID, accountConcurrency)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
259
backend/internal/service/tls_fingerprint_profile_service.go
Normal file
259
backend/internal/service/tls_fingerprint_profile_service.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -482,6 +482,7 @@ var ProviderSet = wire.NewSet(
|
|||||||
NewUsageCache,
|
NewUsageCache,
|
||||||
NewTotpService,
|
NewTotpService,
|
||||||
NewErrorPassthroughService,
|
NewErrorPassthroughService,
|
||||||
|
NewTLSFingerprintProfileService,
|
||||||
NewDigestSessionStore,
|
NewDigestSessionStore,
|
||||||
ProvideIdempotencyCoordinator,
|
ProvideIdempotencyCoordinator,
|
||||||
ProvideSystemOperationLockService,
|
ProvideSystemOperationLockService,
|
||||||
|
|||||||
29
backend/migrations/080_create_tls_fingerprint_profiles.sql
Normal file
29
backend/migrations/080_create_tls_fingerprint_profiles.sql
Normal file
@ -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';
|
||||||
@ -24,6 +24,7 @@ import dataManagementAPI from './dataManagement'
|
|||||||
import apiKeysAPI from './apiKeys'
|
import apiKeysAPI from './apiKeys'
|
||||||
import scheduledTestsAPI from './scheduledTests'
|
import scheduledTestsAPI from './scheduledTests'
|
||||||
import backupAPI from './backup'
|
import backupAPI from './backup'
|
||||||
|
import tlsFingerprintProfileAPI from './tlsFingerprintProfile'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unified admin API object for convenient access
|
* Unified admin API object for convenient access
|
||||||
@ -49,7 +50,8 @@ export const adminAPI = {
|
|||||||
dataManagement: dataManagementAPI,
|
dataManagement: dataManagementAPI,
|
||||||
apiKeys: apiKeysAPI,
|
apiKeys: apiKeysAPI,
|
||||||
scheduledTests: scheduledTestsAPI,
|
scheduledTests: scheduledTestsAPI,
|
||||||
backup: backupAPI
|
backup: backupAPI,
|
||||||
|
tlsFingerprintProfiles: tlsFingerprintProfileAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -73,7 +75,8 @@ export {
|
|||||||
dataManagementAPI,
|
dataManagementAPI,
|
||||||
apiKeysAPI,
|
apiKeysAPI,
|
||||||
scheduledTestsAPI,
|
scheduledTestsAPI,
|
||||||
backupAPI
|
backupAPI,
|
||||||
|
tlsFingerprintProfileAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
export default adminAPI
|
export default adminAPI
|
||||||
@ -82,3 +85,4 @@ export default adminAPI
|
|||||||
export type { BalanceHistoryItem } from './users'
|
export type { BalanceHistoryItem } from './users'
|
||||||
export type { ErrorPassthroughRule, CreateRuleRequest, UpdateRuleRequest } from './errorPassthrough'
|
export type { ErrorPassthroughRule, CreateRuleRequest, UpdateRuleRequest } from './errorPassthrough'
|
||||||
export type { BackupAgentHealth, DataManagementConfig } from './dataManagement'
|
export type { BackupAgentHealth, DataManagementConfig } from './dataManagement'
|
||||||
|
export type { TLSFingerprintProfile, CreateProfileRequest, UpdateProfileRequest } from './tlsFingerprintProfile'
|
||||||
|
|||||||
98
frontend/src/api/admin/tlsFingerprintProfile.ts
Normal file
98
frontend/src/api/admin/tlsFingerprintProfile.ts
Normal file
@ -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<TLSFingerprintProfile[]> {
|
||||||
|
const { data } = await apiClient.get<TLSFingerprintProfile[]>('/admin/tls-fingerprint-profiles')
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getById(id: number): Promise<TLSFingerprintProfile> {
|
||||||
|
const { data } = await apiClient.get<TLSFingerprintProfile>(`/admin/tls-fingerprint-profiles/${id}`)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function create(profileData: CreateProfileRequest): Promise<TLSFingerprintProfile> {
|
||||||
|
const { data } = await apiClient.post<TLSFingerprintProfile>('/admin/tls-fingerprint-profiles', profileData)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update(id: number, updates: UpdateProfileRequest): Promise<TLSFingerprintProfile> {
|
||||||
|
const { data } = await apiClient.put<TLSFingerprintProfile>(`/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
|
||||||
@ -2169,6 +2169,14 @@
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Profile selector -->
|
||||||
|
<div v-if="tlsFingerprintEnabled" class="mt-3">
|
||||||
|
<select v-model="tlsFingerprintProfileId" class="input">
|
||||||
|
<option :value="null">{{ t('admin.accounts.quotaControl.tlsFingerprint.defaultProfile') }}</option>
|
||||||
|
<option v-if="tlsFingerprintProfiles.length > 0" :value="-1">{{ t('admin.accounts.quotaControl.tlsFingerprint.randomProfile') }}</option>
|
||||||
|
<option v-for="p in tlsFingerprintProfiles" :key="p.id" :value="p.id">{{ p.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Session ID Masking -->
|
<!-- Session ID Masking -->
|
||||||
@ -3082,6 +3090,8 @@ const umqModeOptions = computed(() => [
|
|||||||
{ value: 'serialize', label: t('admin.accounts.quotaControl.rpmLimit.umqModeSerialize') },
|
{ value: 'serialize', label: t('admin.accounts.quotaControl.rpmLimit.umqModeSerialize') },
|
||||||
])
|
])
|
||||||
const tlsFingerprintEnabled = ref(false)
|
const tlsFingerprintEnabled = ref(false)
|
||||||
|
const tlsFingerprintProfileId = ref<number | null>(null)
|
||||||
|
const tlsFingerprintProfiles = ref<{ id: number; name: string }[]>([])
|
||||||
const sessionIdMaskingEnabled = ref(false)
|
const sessionIdMaskingEnabled = ref(false)
|
||||||
const cacheTTLOverrideEnabled = ref(false)
|
const cacheTTLOverrideEnabled = ref(false)
|
||||||
const cacheTTLOverrideTarget = ref<string>('5m')
|
const cacheTTLOverrideTarget = ref<string>('5m')
|
||||||
@ -3247,6 +3257,10 @@ watch(
|
|||||||
() => props.show,
|
() => props.show,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
if (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
|
// Modal opened - fill related models
|
||||||
allowedModels.value = [...getModelsByPlatform(form.platform)]
|
allowedModels.value = [...getModelsByPlatform(form.platform)]
|
||||||
// Antigravity: 默认使用映射模式并填充默认映射
|
// Antigravity: 默认使用映射模式并填充默认映射
|
||||||
@ -3747,6 +3761,7 @@ const resetForm = () => {
|
|||||||
rpmStickyBuffer.value = null
|
rpmStickyBuffer.value = null
|
||||||
userMsgQueueMode.value = ''
|
userMsgQueueMode.value = ''
|
||||||
tlsFingerprintEnabled.value = false
|
tlsFingerprintEnabled.value = false
|
||||||
|
tlsFingerprintProfileId.value = null
|
||||||
sessionIdMaskingEnabled.value = false
|
sessionIdMaskingEnabled.value = false
|
||||||
cacheTTLOverrideEnabled.value = false
|
cacheTTLOverrideEnabled.value = false
|
||||||
cacheTTLOverrideTarget.value = '5m'
|
cacheTTLOverrideTarget.value = '5m'
|
||||||
@ -4825,6 +4840,9 @@ const handleAnthropicExchange = async (authCode: string) => {
|
|||||||
// Add TLS fingerprint settings
|
// Add TLS fingerprint settings
|
||||||
if (tlsFingerprintEnabled.value) {
|
if (tlsFingerprintEnabled.value) {
|
||||||
extra.enable_tls_fingerprint = true
|
extra.enable_tls_fingerprint = true
|
||||||
|
if (tlsFingerprintProfileId.value) {
|
||||||
|
extra.tls_fingerprint_profile_id = tlsFingerprintProfileId.value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add session ID masking settings
|
// Add session ID masking settings
|
||||||
@ -4940,6 +4958,9 @@ const handleCookieAuth = async (sessionKey: string) => {
|
|||||||
// Add TLS fingerprint settings
|
// Add TLS fingerprint settings
|
||||||
if (tlsFingerprintEnabled.value) {
|
if (tlsFingerprintEnabled.value) {
|
||||||
extra.enable_tls_fingerprint = true
|
extra.enable_tls_fingerprint = true
|
||||||
|
if (tlsFingerprintProfileId.value) {
|
||||||
|
extra.tls_fingerprint_profile_id = tlsFingerprintProfileId.value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add session ID masking settings
|
// Add session ID masking settings
|
||||||
|
|||||||
@ -1504,6 +1504,14 @@
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Profile selector -->
|
||||||
|
<div v-if="tlsFingerprintEnabled" class="mt-3">
|
||||||
|
<select v-model="tlsFingerprintProfileId" class="input">
|
||||||
|
<option :value="null">{{ t('admin.accounts.quotaControl.tlsFingerprint.defaultProfile') }}</option>
|
||||||
|
<option v-if="tlsFingerprintProfiles.length > 0" :value="-1">{{ t('admin.accounts.quotaControl.tlsFingerprint.randomProfile') }}</option>
|
||||||
|
<option v-for="p in tlsFingerprintProfiles" :key="p.id" :value="p.id">{{ p.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Session ID Masking -->
|
<!-- Session ID Masking -->
|
||||||
@ -1841,6 +1849,8 @@ const umqModeOptions = computed(() => [
|
|||||||
{ value: 'serialize', label: t('admin.accounts.quotaControl.rpmLimit.umqModeSerialize') },
|
{ value: 'serialize', label: t('admin.accounts.quotaControl.rpmLimit.umqModeSerialize') },
|
||||||
])
|
])
|
||||||
const tlsFingerprintEnabled = ref(false)
|
const tlsFingerprintEnabled = ref(false)
|
||||||
|
const tlsFingerprintProfileId = ref<number | null>(null)
|
||||||
|
const tlsFingerprintProfiles = ref<{ id: number; name: string }[]>([])
|
||||||
const sessionIdMaskingEnabled = ref(false)
|
const sessionIdMaskingEnabled = ref(false)
|
||||||
const cacheTTLOverrideEnabled = ref(false)
|
const cacheTTLOverrideEnabled = ref(false)
|
||||||
const cacheTTLOverrideTarget = ref<string>('5m')
|
const cacheTTLOverrideTarget = ref<string>('5m')
|
||||||
@ -2255,11 +2265,21 @@ watch(
|
|||||||
}
|
}
|
||||||
if (!wasShow || newAccount !== previousAccount) {
|
if (!wasShow || newAccount !== previousAccount) {
|
||||||
syncFormFromAccount(newAccount)
|
syncFormFromAccount(newAccount)
|
||||||
|
loadTLSProfiles()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ 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
|
// Model mapping helpers
|
||||||
const addModelMapping = () => {
|
const addModelMapping = () => {
|
||||||
modelMappings.value.push({ from: '', to: '' })
|
modelMappings.value.push({ from: '', to: '' })
|
||||||
@ -2458,6 +2478,7 @@ function loadQuotaControlSettings(account: Account) {
|
|||||||
rpmStickyBuffer.value = null
|
rpmStickyBuffer.value = null
|
||||||
userMsgQueueMode.value = ''
|
userMsgQueueMode.value = ''
|
||||||
tlsFingerprintEnabled.value = false
|
tlsFingerprintEnabled.value = false
|
||||||
|
tlsFingerprintProfileId.value = null
|
||||||
sessionIdMaskingEnabled.value = false
|
sessionIdMaskingEnabled.value = false
|
||||||
cacheTTLOverrideEnabled.value = false
|
cacheTTLOverrideEnabled.value = false
|
||||||
cacheTTLOverrideTarget.value = '5m'
|
cacheTTLOverrideTarget.value = '5m'
|
||||||
@ -2495,6 +2516,7 @@ function loadQuotaControlSettings(account: Account) {
|
|||||||
if (account.enable_tls_fingerprint === true) {
|
if (account.enable_tls_fingerprint === true) {
|
||||||
tlsFingerprintEnabled.value = true
|
tlsFingerprintEnabled.value = true
|
||||||
}
|
}
|
||||||
|
tlsFingerprintProfileId.value = account.tls_fingerprint_profile_id ?? null
|
||||||
|
|
||||||
// Load session ID masking setting
|
// Load session ID masking setting
|
||||||
if (account.session_id_masking_enabled === true) {
|
if (account.session_id_masking_enabled === true) {
|
||||||
@ -2932,8 +2954,14 @@ const handleSubmit = async () => {
|
|||||||
// TLS fingerprint setting
|
// TLS fingerprint setting
|
||||||
if (tlsFingerprintEnabled.value) {
|
if (tlsFingerprintEnabled.value) {
|
||||||
newExtra.enable_tls_fingerprint = true
|
newExtra.enable_tls_fingerprint = true
|
||||||
|
if (tlsFingerprintProfileId.value) {
|
||||||
|
newExtra.tls_fingerprint_profile_id = tlsFingerprintProfileId.value
|
||||||
|
} else {
|
||||||
|
delete newExtra.tls_fingerprint_profile_id
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
delete newExtra.enable_tls_fingerprint
|
delete newExtra.enable_tls_fingerprint
|
||||||
|
delete newExtra.tls_fingerprint_profile_id
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session ID masking setting
|
// Session ID masking setting
|
||||||
|
|||||||
625
frontend/src/components/admin/TLSFingerprintProfilesModal.vue
Normal file
625
frontend/src/components/admin/TLSFingerprintProfilesModal.vue
Normal file
@ -0,0 +1,625 @@
|
|||||||
|
<template>
|
||||||
|
<BaseDialog
|
||||||
|
:show="show"
|
||||||
|
:title="t('admin.tlsFingerprintProfiles.title')"
|
||||||
|
width="wide"
|
||||||
|
@close="$emit('close')"
|
||||||
|
>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.tlsFingerprintProfiles.description') }}
|
||||||
|
</p>
|
||||||
|
<button @click="showCreateModal = true" class="btn btn-primary btn-sm">
|
||||||
|
<Icon name="plus" size="sm" class="mr-1" />
|
||||||
|
{{ t('admin.tlsFingerprintProfiles.createProfile') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Profiles Table -->
|
||||||
|
<div v-if="loading" class="flex items-center justify-center py-8">
|
||||||
|
<Icon name="refresh" size="lg" class="animate-spin text-gray-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="profiles.length === 0" class="py-8 text-center">
|
||||||
|
<div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-gray-100 dark:bg-dark-700">
|
||||||
|
<Icon name="shield" size="lg" class="text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<h4 class="mb-1 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{{ t('admin.tlsFingerprintProfiles.noProfiles') }}
|
||||||
|
</h4>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.tlsFingerprintProfiles.createFirstProfile') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="max-h-96 overflow-auto rounded-lg border border-gray-200 dark:border-dark-600">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-dark-700">
|
||||||
|
<thead class="sticky top-0 bg-gray-50 dark:bg-dark-700">
|
||||||
|
<tr>
|
||||||
|
<th class="px-3 py-2 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.tlsFingerprintProfiles.columns.name') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-3 py-2 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.tlsFingerprintProfiles.columns.description') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-3 py-2 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.tlsFingerprintProfiles.columns.grease') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-3 py-2 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.tlsFingerprintProfiles.columns.alpn') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-3 py-2 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.tlsFingerprintProfiles.columns.actions') }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 bg-white dark:divide-dark-700 dark:bg-dark-800">
|
||||||
|
<tr v-for="profile in profiles" :key="profile.id" class="hover:bg-gray-50 dark:hover:bg-dark-700">
|
||||||
|
<td class="px-3 py-2">
|
||||||
|
<div class="font-medium text-gray-900 dark:text-white text-sm">{{ profile.name }}</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2">
|
||||||
|
<div v-if="profile.description" class="text-sm text-gray-500 dark:text-gray-400 max-w-xs truncate">
|
||||||
|
{{ profile.description }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-xs text-gray-400 dark:text-gray-600">—</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2">
|
||||||
|
<Icon
|
||||||
|
:name="profile.enable_grease ? 'check' : 'lock'"
|
||||||
|
size="sm"
|
||||||
|
:class="profile.enable_grease ? 'text-green-500' : 'text-gray-400'"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2">
|
||||||
|
<div v-if="profile.alpn_protocols?.length" class="flex flex-wrap gap-1">
|
||||||
|
<span
|
||||||
|
v-for="proto in profile.alpn_protocols.slice(0, 3)"
|
||||||
|
:key="proto"
|
||||||
|
class="badge badge-primary text-xs"
|
||||||
|
>
|
||||||
|
{{ proto }}
|
||||||
|
</span>
|
||||||
|
<span v-if="profile.alpn_protocols.length > 3" class="text-xs text-gray-500">
|
||||||
|
+{{ profile.alpn_protocols.length - 3 }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-xs text-gray-400 dark:text-gray-600">—</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
@click="handleEdit(profile)"
|
||||||
|
class="p-1 text-gray-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||||
|
:title="t('common.edit')"
|
||||||
|
>
|
||||||
|
<Icon name="edit" size="sm" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="handleDelete(profile)"
|
||||||
|
class="p-1 text-gray-500 hover:text-red-600 dark:hover:text-red-400"
|
||||||
|
:title="t('common.delete')"
|
||||||
|
>
|
||||||
|
<Icon name="trash" size="sm" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button @click="$emit('close')" class="btn btn-secondary">
|
||||||
|
{{ t('common.close') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Create/Edit Modal -->
|
||||||
|
<BaseDialog
|
||||||
|
:show="showCreateModal || showEditModal"
|
||||||
|
:title="showEditModal ? t('admin.tlsFingerprintProfiles.editProfile') : t('admin.tlsFingerprintProfiles.createProfile')"
|
||||||
|
width="wide"
|
||||||
|
:z-index="60"
|
||||||
|
@close="closeFormModal"
|
||||||
|
>
|
||||||
|
<form @submit.prevent="handleSubmit" class="space-y-4">
|
||||||
|
<!-- Paste YAML -->
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('admin.tlsFingerprintProfiles.form.pasteYaml') }}</label>
|
||||||
|
<textarea
|
||||||
|
v-model="yamlInput"
|
||||||
|
rows="4"
|
||||||
|
class="input font-mono text-xs"
|
||||||
|
:placeholder="t('admin.tlsFingerprintProfiles.form.pasteYamlPlaceholder')"
|
||||||
|
@paste="handleYamlPaste"
|
||||||
|
/>
|
||||||
|
<div class="mt-1 flex items-center gap-2">
|
||||||
|
<button type="button" @click="parseYamlInput" class="btn btn-secondary btn-sm">
|
||||||
|
{{ t('admin.tlsFingerprintProfiles.form.parseYaml') }}
|
||||||
|
</button>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.tlsFingerprintProfiles.form.pasteYamlHint') }}
|
||||||
|
<a href="https://tls.sub2api.org" target="_blank" rel="noopener noreferrer" class="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 underline">{{ t('admin.tlsFingerprintProfiles.form.openCollector') }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-gray-200 dark:border-dark-600" />
|
||||||
|
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('admin.tlsFingerprintProfiles.form.name') }}</label>
|
||||||
|
<input
|
||||||
|
v-model="form.name"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
class="input"
|
||||||
|
:placeholder="t('admin.tlsFingerprintProfiles.form.namePlaceholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('admin.tlsFingerprintProfiles.form.description') }}</label>
|
||||||
|
<input
|
||||||
|
v-model="form.description"
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
:placeholder="t('admin.tlsFingerprintProfiles.form.descriptionPlaceholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- GREASE Toggle -->
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="form.enable_grease = !form.enable_grease"
|
||||||
|
:class="[
|
||||||
|
'relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||||
|
form.enable_grease ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||||
|
form.enable_grease ? 'translate-x-4' : 'translate-x-0'
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('admin.tlsFingerprintProfiles.form.enableGrease') }}
|
||||||
|
</span>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.tlsFingerprintProfiles.form.enableGreaseHint') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TLS Array Fields - 2 column grid -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="input-label text-xs">{{ t('admin.tlsFingerprintProfiles.form.cipherSuites') }}</label>
|
||||||
|
<textarea
|
||||||
|
v-model="fieldInputs.cipher_suites"
|
||||||
|
rows="2"
|
||||||
|
class="input font-mono text-xs"
|
||||||
|
:placeholder="'0x1301, 0x1302, 0xc02c'"
|
||||||
|
/>
|
||||||
|
<p class="input-hint text-xs">{{ t('admin.tlsFingerprintProfiles.form.cipherSuitesHint') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="input-label text-xs">{{ t('admin.tlsFingerprintProfiles.form.curves') }}</label>
|
||||||
|
<textarea
|
||||||
|
v-model="fieldInputs.curves"
|
||||||
|
rows="2"
|
||||||
|
class="input font-mono text-xs"
|
||||||
|
:placeholder="'29, 23, 24'"
|
||||||
|
/>
|
||||||
|
<p class="input-hint text-xs">{{ t('admin.tlsFingerprintProfiles.form.curvesHint') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="input-label text-xs">{{ t('admin.tlsFingerprintProfiles.form.signatureAlgorithms') }}</label>
|
||||||
|
<textarea
|
||||||
|
v-model="fieldInputs.signature_algorithms"
|
||||||
|
rows="2"
|
||||||
|
class="input font-mono text-xs"
|
||||||
|
:placeholder="'0x0403, 0x0804, 0x0401'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="input-label text-xs">{{ t('admin.tlsFingerprintProfiles.form.supportedVersions') }}</label>
|
||||||
|
<textarea
|
||||||
|
v-model="fieldInputs.supported_versions"
|
||||||
|
rows="2"
|
||||||
|
class="input font-mono text-xs"
|
||||||
|
:placeholder="'0x0304, 0x0303'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="input-label text-xs">{{ t('admin.tlsFingerprintProfiles.form.keyShareGroups') }}</label>
|
||||||
|
<textarea
|
||||||
|
v-model="fieldInputs.key_share_groups"
|
||||||
|
rows="2"
|
||||||
|
class="input font-mono text-xs"
|
||||||
|
:placeholder="'29, 23'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="input-label text-xs">{{ t('admin.tlsFingerprintProfiles.form.extensions') }}</label>
|
||||||
|
<textarea
|
||||||
|
v-model="fieldInputs.extensions"
|
||||||
|
rows="2"
|
||||||
|
class="input font-mono text-xs"
|
||||||
|
:placeholder="'0x0000, 0x0005, 0x000a'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="input-label text-xs">{{ t('admin.tlsFingerprintProfiles.form.pointFormats') }}</label>
|
||||||
|
<textarea
|
||||||
|
v-model="fieldInputs.point_formats"
|
||||||
|
rows="2"
|
||||||
|
class="input font-mono text-xs"
|
||||||
|
:placeholder="'0'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="input-label text-xs">{{ t('admin.tlsFingerprintProfiles.form.pskModes') }}</label>
|
||||||
|
<textarea
|
||||||
|
v-model="fieldInputs.psk_modes"
|
||||||
|
rows="2"
|
||||||
|
class="input font-mono text-xs"
|
||||||
|
:placeholder="'1'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ALPN Protocols - full width -->
|
||||||
|
<div>
|
||||||
|
<label class="input-label text-xs">{{ t('admin.tlsFingerprintProfiles.form.alpnProtocols') }}</label>
|
||||||
|
<textarea
|
||||||
|
v-model="fieldInputs.alpn_protocols"
|
||||||
|
rows="2"
|
||||||
|
class="input font-mono text-xs"
|
||||||
|
:placeholder="'h2, http/1.1'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<button @click="closeFormModal" type="button" class="btn btn-secondary">
|
||||||
|
{{ t('common.cancel') }}
|
||||||
|
</button>
|
||||||
|
<button @click="handleSubmit" :disabled="submitting" class="btn btn-primary">
|
||||||
|
<Icon v-if="submitting" name="refresh" size="sm" class="mr-1 animate-spin" />
|
||||||
|
{{ showEditModal ? t('common.update') : t('common.create') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</BaseDialog>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation -->
|
||||||
|
<ConfirmDialog
|
||||||
|
:show="showDeleteDialog"
|
||||||
|
:title="t('admin.tlsFingerprintProfiles.deleteProfile')"
|
||||||
|
:message="t('admin.tlsFingerprintProfiles.deleteConfirmMessage', { name: deletingProfile?.name })"
|
||||||
|
:confirm-text="t('common.delete')"
|
||||||
|
:cancel-text="t('common.cancel')"
|
||||||
|
:danger="true"
|
||||||
|
@confirm="confirmDelete"
|
||||||
|
@cancel="showDeleteDialog = false"
|
||||||
|
/>
|
||||||
|
</BaseDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { adminAPI } from '@/api/admin'
|
||||||
|
import type { TLSFingerprintProfile } from '@/api/admin/tlsFingerprintProfile'
|
||||||
|
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||||
|
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
||||||
|
import Icon from '@/components/icons/Icon.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
show: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
void emit // suppress unused warning - emit is used via $emit in template
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
const profiles = ref<TLSFingerprintProfile[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const showCreateModal = ref(false)
|
||||||
|
const showEditModal = ref(false)
|
||||||
|
const showDeleteDialog = ref(false)
|
||||||
|
const editingProfile = ref<TLSFingerprintProfile | null>(null)
|
||||||
|
const deletingProfile = ref<TLSFingerprintProfile | null>(null)
|
||||||
|
const yamlInput = ref('')
|
||||||
|
|
||||||
|
// Raw string inputs for array fields
|
||||||
|
const fieldInputs = reactive({
|
||||||
|
cipher_suites: '',
|
||||||
|
curves: '',
|
||||||
|
point_formats: '',
|
||||||
|
signature_algorithms: '',
|
||||||
|
alpn_protocols: '',
|
||||||
|
supported_versions: '',
|
||||||
|
key_share_groups: '',
|
||||||
|
psk_modes: '',
|
||||||
|
extensions: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
name: '',
|
||||||
|
description: null as string | null,
|
||||||
|
enable_grease: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Load profiles when dialog opens
|
||||||
|
watch(() => props.show, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
loadProfiles()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadProfiles = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
profiles.value = await adminAPI.tlsFingerprintProfiles.list()
|
||||||
|
} catch (error) {
|
||||||
|
appStore.showError(t('admin.tlsFingerprintProfiles.loadFailed'))
|
||||||
|
console.error('Error loading TLS fingerprint profiles:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
form.name = ''
|
||||||
|
form.description = null
|
||||||
|
form.enable_grease = false
|
||||||
|
fieldInputs.cipher_suites = ''
|
||||||
|
fieldInputs.curves = ''
|
||||||
|
fieldInputs.point_formats = ''
|
||||||
|
fieldInputs.signature_algorithms = ''
|
||||||
|
fieldInputs.alpn_protocols = ''
|
||||||
|
fieldInputs.supported_versions = ''
|
||||||
|
fieldInputs.key_share_groups = ''
|
||||||
|
fieldInputs.psk_modes = ''
|
||||||
|
fieldInputs.extensions = ''
|
||||||
|
yamlInput.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse YAML output from tls-fingerprint-web and fill form fields.
|
||||||
|
* Expected format:
|
||||||
|
* # comment lines
|
||||||
|
* profile_key:
|
||||||
|
* name: "Profile Name"
|
||||||
|
* enable_grease: false
|
||||||
|
* cipher_suites: [4866, 4867, ...]
|
||||||
|
* alpn_protocols: ["h2", "http/1.1"]
|
||||||
|
* ...
|
||||||
|
*/
|
||||||
|
const parseYamlInput = () => {
|
||||||
|
const text = yamlInput.value.trim()
|
||||||
|
if (!text) return
|
||||||
|
|
||||||
|
// Simple YAML parser for flat key-value structure
|
||||||
|
// Extracts "key: value" lines, handling arrays like [1, 2, 3] and ["h2", "http/1.1"]
|
||||||
|
const lines = text.split('\n')
|
||||||
|
|
||||||
|
let foundName = false
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim()
|
||||||
|
// Skip comments and empty lines
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) continue
|
||||||
|
|
||||||
|
// Match "key: value" pattern (must have at least 2 leading spaces to be a property)
|
||||||
|
const match = trimmed.match(/^(\w+):\s*(.+)$/)
|
||||||
|
if (!match) continue
|
||||||
|
|
||||||
|
const [, key, rawValue] = match
|
||||||
|
const value = rawValue.trim()
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case 'name': {
|
||||||
|
// Remove surrounding quotes
|
||||||
|
const unquoted = value.replace(/^["']|["']$/g, '')
|
||||||
|
if (unquoted) {
|
||||||
|
form.name = unquoted
|
||||||
|
foundName = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'enable_grease':
|
||||||
|
form.enable_grease = value === 'true'
|
||||||
|
break
|
||||||
|
case 'cipher_suites':
|
||||||
|
case 'curves':
|
||||||
|
case 'point_formats':
|
||||||
|
case 'signature_algorithms':
|
||||||
|
case 'supported_versions':
|
||||||
|
case 'key_share_groups':
|
||||||
|
case 'psk_modes':
|
||||||
|
case 'extensions': {
|
||||||
|
// Parse YAML array: [1, 2, 3] — values are decimal integers from tls-fingerprint-web
|
||||||
|
const arrMatch = value.match(/^\[(.*)?\]$/)
|
||||||
|
if (arrMatch) {
|
||||||
|
const inner = arrMatch[1] || ''
|
||||||
|
fieldInputs[key as keyof typeof fieldInputs] = inner
|
||||||
|
.split(',')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(s => s.length > 0)
|
||||||
|
.join(', ')
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'alpn_protocols': {
|
||||||
|
// Parse string array: ["h2", "http/1.1"]
|
||||||
|
const arrMatch = value.match(/^\[(.*)?\]$/)
|
||||||
|
if (arrMatch) {
|
||||||
|
const inner = arrMatch[1] || ''
|
||||||
|
fieldInputs.alpn_protocols = inner
|
||||||
|
.split(',')
|
||||||
|
.map(s => s.trim().replace(/^["']|["']$/g, ''))
|
||||||
|
.filter(s => s.length > 0)
|
||||||
|
.join(', ')
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundName) {
|
||||||
|
appStore.showSuccess(t('admin.tlsFingerprintProfiles.form.yamlParsed'))
|
||||||
|
} else {
|
||||||
|
appStore.showError(t('admin.tlsFingerprintProfiles.form.yamlParseFailed'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-parse on paste event
|
||||||
|
const handleYamlPaste = () => {
|
||||||
|
// Use nextTick to ensure v-model has updated
|
||||||
|
setTimeout(() => parseYamlInput(), 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeFormModal = () => {
|
||||||
|
showCreateModal.value = false
|
||||||
|
showEditModal.value = false
|
||||||
|
editingProfile.value = null
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a comma-separated string of numbers supporting both hex (0x...) and decimal
|
||||||
|
const parseNumericArray = (input: string): number[] => {
|
||||||
|
if (!input.trim()) return []
|
||||||
|
return input
|
||||||
|
.split(',')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(s => s.length > 0)
|
||||||
|
.map(s => s.startsWith('0x') || s.startsWith('0X') ? parseInt(s, 16) : parseInt(s, 10))
|
||||||
|
.filter(n => !isNaN(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a comma-separated string of string values
|
||||||
|
const parseStringArray = (input: string): string[] => {
|
||||||
|
if (!input.trim()) return []
|
||||||
|
return input
|
||||||
|
.split(',')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(s => s.length > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format a number as hex with 0x prefix and 4-digit padding
|
||||||
|
const formatHex = (n: number): string => '0x' + n.toString(16).padStart(4, '0')
|
||||||
|
|
||||||
|
// Format numeric arrays for display in textarea (null-safe)
|
||||||
|
const formatNumericArray = (arr: number[] | null | undefined): string => (arr ?? []).map(formatHex).join(', ')
|
||||||
|
|
||||||
|
// For point_formats and psk_modes (uint8), show as plain numbers (null-safe)
|
||||||
|
const formatPlainNumericArray = (arr: number[] | null | undefined): string => (arr ?? []).join(', ')
|
||||||
|
|
||||||
|
const handleEdit = (profile: TLSFingerprintProfile) => {
|
||||||
|
editingProfile.value = profile
|
||||||
|
form.name = profile.name
|
||||||
|
form.description = profile.description
|
||||||
|
form.enable_grease = profile.enable_grease
|
||||||
|
fieldInputs.cipher_suites = formatNumericArray(profile.cipher_suites)
|
||||||
|
fieldInputs.curves = formatPlainNumericArray(profile.curves)
|
||||||
|
fieldInputs.point_formats = formatPlainNumericArray(profile.point_formats)
|
||||||
|
fieldInputs.signature_algorithms = formatNumericArray(profile.signature_algorithms)
|
||||||
|
fieldInputs.alpn_protocols = (profile.alpn_protocols ?? []).join(', ')
|
||||||
|
fieldInputs.supported_versions = formatNumericArray(profile.supported_versions)
|
||||||
|
fieldInputs.key_share_groups = formatPlainNumericArray(profile.key_share_groups)
|
||||||
|
fieldInputs.psk_modes = formatPlainNumericArray(profile.psk_modes)
|
||||||
|
fieldInputs.extensions = formatNumericArray(profile.extensions)
|
||||||
|
showEditModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = (profile: TLSFingerprintProfile) => {
|
||||||
|
deletingProfile.value = profile
|
||||||
|
showDeleteDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!form.name.trim()) {
|
||||||
|
appStore.showError(t('admin.tlsFingerprintProfiles.form.name') + ' ' + t('common.required'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
name: form.name.trim(),
|
||||||
|
description: form.description?.trim() || null,
|
||||||
|
enable_grease: form.enable_grease,
|
||||||
|
cipher_suites: parseNumericArray(fieldInputs.cipher_suites),
|
||||||
|
curves: parseNumericArray(fieldInputs.curves),
|
||||||
|
point_formats: parseNumericArray(fieldInputs.point_formats),
|
||||||
|
signature_algorithms: parseNumericArray(fieldInputs.signature_algorithms),
|
||||||
|
alpn_protocols: parseStringArray(fieldInputs.alpn_protocols),
|
||||||
|
supported_versions: parseNumericArray(fieldInputs.supported_versions),
|
||||||
|
key_share_groups: parseNumericArray(fieldInputs.key_share_groups),
|
||||||
|
psk_modes: parseNumericArray(fieldInputs.psk_modes),
|
||||||
|
extensions: parseNumericArray(fieldInputs.extensions)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showEditModal.value && editingProfile.value) {
|
||||||
|
await adminAPI.tlsFingerprintProfiles.update(editingProfile.value.id, data)
|
||||||
|
appStore.showSuccess(t('admin.tlsFingerprintProfiles.updateSuccess'))
|
||||||
|
} else {
|
||||||
|
await adminAPI.tlsFingerprintProfiles.create(data)
|
||||||
|
appStore.showSuccess(t('admin.tlsFingerprintProfiles.createSuccess'))
|
||||||
|
}
|
||||||
|
|
||||||
|
closeFormModal()
|
||||||
|
loadProfiles()
|
||||||
|
} catch (error: any) {
|
||||||
|
appStore.showError(error.response?.data?.detail || t('admin.tlsFingerprintProfiles.saveFailed'))
|
||||||
|
console.error('Error saving TLS fingerprint profile:', error)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDelete = async () => {
|
||||||
|
if (!deletingProfile.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await adminAPI.tlsFingerprintProfiles.delete(deletingProfile.value.id)
|
||||||
|
appStore.showSuccess(t('admin.tlsFingerprintProfiles.deleteSuccess'))
|
||||||
|
showDeleteDialog.value = false
|
||||||
|
deletingProfile.value = null
|
||||||
|
loadProfiles()
|
||||||
|
} catch (error: any) {
|
||||||
|
appStore.showError(error.response?.data?.detail || t('admin.tlsFingerprintProfiles.deleteFailed'))
|
||||||
|
console.error('Error deleting TLS fingerprint profile:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -2304,7 +2304,9 @@ export default {
|
|||||||
},
|
},
|
||||||
tlsFingerprint: {
|
tlsFingerprint: {
|
||||||
label: 'TLS Fingerprint Simulation',
|
label: 'TLS Fingerprint Simulation',
|
||||||
hint: 'Simulate Node.js/Claude Code client TLS fingerprint'
|
hint: 'Simulate Node.js/Claude Code client TLS fingerprint',
|
||||||
|
defaultProfile: 'Built-in Default',
|
||||||
|
randomProfile: 'Random'
|
||||||
},
|
},
|
||||||
sessionIdMasking: {
|
sessionIdMasking: {
|
||||||
label: 'Session ID Masking',
|
label: 'Session ID Masking',
|
||||||
@ -4572,6 +4574,62 @@ export default {
|
|||||||
failedToSave: 'Failed to save rule',
|
failedToSave: 'Failed to save rule',
|
||||||
failedToDelete: 'Failed to delete rule',
|
failedToDelete: 'Failed to delete rule',
|
||||||
failedToToggle: 'Failed to toggle status'
|
failedToToggle: 'Failed to toggle status'
|
||||||
|
},
|
||||||
|
|
||||||
|
// TLS Fingerprint Profiles
|
||||||
|
tlsFingerprintProfiles: {
|
||||||
|
title: 'TLS Fingerprint Profiles',
|
||||||
|
description: 'Manage TLS fingerprint profiles for simulating specific client TLS handshake characteristics',
|
||||||
|
createProfile: 'Create Profile',
|
||||||
|
editProfile: 'Edit Profile',
|
||||||
|
deleteProfile: 'Delete Profile',
|
||||||
|
noProfiles: 'No profiles configured',
|
||||||
|
createFirstProfile: 'Create your first TLS fingerprint profile',
|
||||||
|
|
||||||
|
columns: {
|
||||||
|
name: 'Name',
|
||||||
|
description: 'Description',
|
||||||
|
grease: 'GREASE',
|
||||||
|
alpn: 'ALPN',
|
||||||
|
actions: 'Actions'
|
||||||
|
},
|
||||||
|
|
||||||
|
form: {
|
||||||
|
pasteYaml: 'Paste YAML Configuration',
|
||||||
|
pasteYamlPlaceholder: 'Paste YAML output from TLS Fingerprint Collector here...',
|
||||||
|
pasteYamlHint: 'Paste the YAML copied from TLS Fingerprint Collector to auto-fill all fields.',
|
||||||
|
openCollector: 'Open Collector',
|
||||||
|
parseYaml: 'Parse YAML',
|
||||||
|
yamlParsed: 'YAML parsed successfully, fields auto-filled',
|
||||||
|
yamlParseFailed: 'Failed to parse YAML: name field not found',
|
||||||
|
name: 'Profile Name',
|
||||||
|
namePlaceholder: 'e.g. macOS Node.js v24',
|
||||||
|
description: 'Description',
|
||||||
|
descriptionPlaceholder: 'Optional description for this profile',
|
||||||
|
enableGrease: 'Enable GREASE',
|
||||||
|
enableGreaseHint: 'Insert GREASE values in TLS ClientHello extensions',
|
||||||
|
cipherSuites: 'Cipher Suites',
|
||||||
|
cipherSuitesHint: 'Comma-separated hex values, e.g. 0x1301, 0x1302, 0xc02c',
|
||||||
|
curves: 'Elliptic Curves',
|
||||||
|
curvesHint: 'Comma-separated curve IDs',
|
||||||
|
pointFormats: 'Point Formats',
|
||||||
|
signatureAlgorithms: 'Signature Algorithms',
|
||||||
|
alpnProtocols: 'ALPN Protocols',
|
||||||
|
alpnProtocolsHint: 'Comma-separated, e.g. h2, http/1.1',
|
||||||
|
supportedVersions: 'Supported TLS Versions',
|
||||||
|
keyShareGroups: 'Key Share Groups',
|
||||||
|
pskModes: 'PSK Modes',
|
||||||
|
extensions: 'Extensions'
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteConfirm: 'Delete Profile',
|
||||||
|
deleteConfirmMessage: 'Are you sure you want to delete profile "{name}"? Accounts using this profile will fall back to the built-in default.',
|
||||||
|
createSuccess: 'Profile created successfully',
|
||||||
|
updateSuccess: 'Profile updated successfully',
|
||||||
|
deleteSuccess: 'Profile deleted successfully',
|
||||||
|
loadFailed: 'Failed to load profiles',
|
||||||
|
saveFailed: 'Failed to save profile',
|
||||||
|
deleteFailed: 'Failed to delete profile'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -2448,7 +2448,9 @@ export default {
|
|||||||
},
|
},
|
||||||
tlsFingerprint: {
|
tlsFingerprint: {
|
||||||
label: 'TLS 指纹模拟',
|
label: 'TLS 指纹模拟',
|
||||||
hint: '模拟 Node.js/Claude Code 客户端的 TLS 指纹'
|
hint: '模拟 Node.js/Claude Code 客户端的 TLS 指纹',
|
||||||
|
defaultProfile: '内置默认',
|
||||||
|
randomProfile: '随机'
|
||||||
},
|
},
|
||||||
sessionIdMasking: {
|
sessionIdMasking: {
|
||||||
label: '会话 ID 伪装',
|
label: '会话 ID 伪装',
|
||||||
@ -4736,6 +4738,62 @@ export default {
|
|||||||
failedToSave: '保存规则失败',
|
failedToSave: '保存规则失败',
|
||||||
failedToDelete: '删除规则失败',
|
failedToDelete: '删除规则失败',
|
||||||
failedToToggle: '切换状态失败'
|
failedToToggle: '切换状态失败'
|
||||||
|
},
|
||||||
|
|
||||||
|
// TLS 指纹模板
|
||||||
|
tlsFingerprintProfiles: {
|
||||||
|
title: 'TLS 指纹模板',
|
||||||
|
description: '管理 TLS 指纹模板,用于模拟特定客户端的 TLS 握手特征',
|
||||||
|
createProfile: '创建模板',
|
||||||
|
editProfile: '编辑模板',
|
||||||
|
deleteProfile: '删除模板',
|
||||||
|
noProfiles: '暂无模板',
|
||||||
|
createFirstProfile: '创建你的第一个 TLS 指纹模板',
|
||||||
|
|
||||||
|
columns: {
|
||||||
|
name: '名称',
|
||||||
|
description: '描述',
|
||||||
|
grease: 'GREASE',
|
||||||
|
alpn: 'ALPN',
|
||||||
|
actions: '操作'
|
||||||
|
},
|
||||||
|
|
||||||
|
form: {
|
||||||
|
pasteYaml: '粘贴 YAML 配置',
|
||||||
|
pasteYamlPlaceholder: '将 TLS 指纹采集器复制的 YAML 粘贴到这里...',
|
||||||
|
pasteYamlHint: '粘贴从 TLS 指纹采集器复制的 YAML 配置,自动填充所有字段。',
|
||||||
|
openCollector: '打开采集器',
|
||||||
|
parseYaml: '解析 YAML',
|
||||||
|
yamlParsed: 'YAML 解析成功,字段已自动填充',
|
||||||
|
yamlParseFailed: 'YAML 解析失败:未找到 name 字段',
|
||||||
|
name: '模板名称',
|
||||||
|
namePlaceholder: '例如 macOS Node.js v24',
|
||||||
|
description: '描述',
|
||||||
|
descriptionPlaceholder: '可选的模板描述',
|
||||||
|
enableGrease: '启用 GREASE',
|
||||||
|
enableGreaseHint: '在 TLS ClientHello 扩展中插入 GREASE 值',
|
||||||
|
cipherSuites: '密码套件',
|
||||||
|
cipherSuitesHint: '逗号分隔的十六进制值,例如 0x1301, 0x1302, 0xc02c',
|
||||||
|
curves: '椭圆曲线',
|
||||||
|
curvesHint: '逗号分隔的曲线 ID',
|
||||||
|
pointFormats: '点格式',
|
||||||
|
signatureAlgorithms: '签名算法',
|
||||||
|
alpnProtocols: 'ALPN 协议',
|
||||||
|
alpnProtocolsHint: '逗号分隔,例如 h2, http/1.1',
|
||||||
|
supportedVersions: '支持的 TLS 版本',
|
||||||
|
keyShareGroups: '密钥共享组',
|
||||||
|
pskModes: 'PSK 模式',
|
||||||
|
extensions: '扩展'
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteConfirm: '删除模板',
|
||||||
|
deleteConfirmMessage: '确定要删除模板 "{name}" 吗?使用此模板的账号将回退到内置默认值。',
|
||||||
|
createSuccess: '模板创建成功',
|
||||||
|
updateSuccess: '模板更新成功',
|
||||||
|
deleteSuccess: '模板删除成功',
|
||||||
|
loadFailed: '加载模板失败',
|
||||||
|
saveFailed: '保存模板失败',
|
||||||
|
deleteFailed: '删除模板失败'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -724,6 +724,7 @@ export interface Account {
|
|||||||
|
|
||||||
// TLS指纹伪装(仅 Anthropic OAuth/SetupToken 账号有效)
|
// TLS指纹伪装(仅 Anthropic OAuth/SetupToken 账号有效)
|
||||||
enable_tls_fingerprint?: boolean | null
|
enable_tls_fingerprint?: boolean | null
|
||||||
|
tls_fingerprint_profile_id?: number | null
|
||||||
|
|
||||||
// 会话ID伪装(仅 Anthropic OAuth/SetupToken 账号有效)
|
// 会话ID伪装(仅 Anthropic OAuth/SetupToken 账号有效)
|
||||||
// 启用后将在15分钟内固定 metadata.user_id 中的 session ID
|
// 启用后将在15分钟内固定 metadata.user_id 中的 session ID
|
||||||
|
|||||||
@ -73,6 +73,16 @@
|
|||||||
<span class="hidden md:inline">{{ t('admin.errorPassthrough.title') }}</span>
|
<span class="hidden md:inline">{{ t('admin.errorPassthrough.title') }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- TLS Fingerprint Profiles -->
|
||||||
|
<button
|
||||||
|
@click="showTLSFingerprintProfiles = true"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
:title="t('admin.tlsFingerprintProfiles.title')"
|
||||||
|
>
|
||||||
|
<Icon name="lock" size="md" class="mr-1.5" />
|
||||||
|
<span class="hidden md:inline">{{ t('admin.tlsFingerprintProfiles.title') }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- Column Settings Dropdown -->
|
<!-- Column Settings Dropdown -->
|
||||||
<div class="relative" ref="columnDropdownRef">
|
<div class="relative" ref="columnDropdownRef">
|
||||||
<button
|
<button
|
||||||
@ -289,6 +299,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
<ErrorPassthroughRulesModal :show="showErrorPassthrough" @close="showErrorPassthrough = false" />
|
<ErrorPassthroughRulesModal :show="showErrorPassthrough" @close="showErrorPassthrough = false" />
|
||||||
|
<TLSFingerprintProfilesModal :show="showTLSFingerprintProfiles" @close="showTLSFingerprintProfiles = false" />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -326,6 +337,7 @@ import AccountCapacityCell from '@/components/account/AccountCapacityCell.vue'
|
|||||||
import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue'
|
import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue'
|
||||||
import Icon from '@/components/icons/Icon.vue'
|
import Icon from '@/components/icons/Icon.vue'
|
||||||
import ErrorPassthroughRulesModal from '@/components/admin/ErrorPassthroughRulesModal.vue'
|
import ErrorPassthroughRulesModal from '@/components/admin/ErrorPassthroughRulesModal.vue'
|
||||||
|
import TLSFingerprintProfilesModal from '@/components/admin/TLSFingerprintProfilesModal.vue'
|
||||||
import { buildOpenAIUsageRefreshKey } from '@/utils/accountUsageRefresh'
|
import { buildOpenAIUsageRefreshKey } from '@/utils/accountUsageRefresh'
|
||||||
import { formatDateTime, formatRelativeTime } from '@/utils/format'
|
import { formatDateTime, formatRelativeTime } from '@/utils/format'
|
||||||
import type { Account, AccountPlatform, AccountType, Proxy as AccountProxy, AdminGroup, WindowStats, ClaudeModel } from '@/types'
|
import type { Account, AccountPlatform, AccountType, Proxy as AccountProxy, AdminGroup, WindowStats, ClaudeModel } from '@/types'
|
||||||
@ -366,6 +378,7 @@ const showReAuth = ref(false)
|
|||||||
const showTest = ref(false)
|
const showTest = ref(false)
|
||||||
const showStats = ref(false)
|
const showStats = ref(false)
|
||||||
const showErrorPassthrough = ref(false)
|
const showErrorPassthrough = ref(false)
|
||||||
|
const showTLSFingerprintProfiles = ref(false)
|
||||||
const edAcc = ref<Account | null>(null)
|
const edAcc = ref<Account | null>(null)
|
||||||
const tempUnschedAcc = ref<Account | null>(null)
|
const tempUnschedAcc = ref<Account | null>(null)
|
||||||
const deletingAcc = ref<Account | null>(null)
|
const deletingAcc = ref<Account | null>(null)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user