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)
|
||||
usageCache := service.NewUsageCache()
|
||||
identityCache := repository.NewIdentityCache(redisClient)
|
||||
accountUsageService := service.NewAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher, geminiQuotaService, antigravityQuotaFetcher, usageCache, identityCache)
|
||||
geminiTokenProvider := service.ProvideGeminiTokenProvider(accountRepository, geminiTokenCache, geminiOAuthService, oauthRefreshAPI)
|
||||
gatewayCache := repository.NewGatewayCache(redisClient)
|
||||
schedulerOutboxRepository := repository.NewSchedulerOutboxRepository(db)
|
||||
schedulerSnapshotService := service.ProvideSchedulerSnapshotService(schedulerCache, schedulerOutboxRepository, accountRepository, groupRepository, configConfig)
|
||||
antigravityTokenProvider := service.ProvideAntigravityTokenProvider(accountRepository, geminiTokenCache, antigravityOAuthService, oauthRefreshAPI, tempUnschedCache)
|
||||
antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, schedulerSnapshotService, antigravityTokenProvider, rateLimitService, httpUpstream, settingService)
|
||||
accountTestService := service.NewAccountTestService(accountRepository, geminiTokenProvider, antigravityGatewayService, httpUpstream, configConfig)
|
||||
tlsFingerprintProfileRepository := repository.NewTLSFingerprintProfileRepository(client)
|
||||
tlsFingerprintProfileCache := repository.NewTLSFingerprintProfileCache(redisClient)
|
||||
tlsFingerprintProfileService := service.NewTLSFingerprintProfileService(tlsFingerprintProfileRepository, tlsFingerprintProfileCache)
|
||||
accountUsageService := service.NewAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher, geminiQuotaService, antigravityQuotaFetcher, usageCache, identityCache, tlsFingerprintProfileService)
|
||||
accountTestService := service.NewAccountTestService(accountRepository, geminiTokenProvider, antigravityGatewayService, httpUpstream, configConfig, tlsFingerprintProfileService)
|
||||
crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService, geminiOAuthService, configConfig)
|
||||
sessionLimitCache := repository.ProvideSessionLimitCache(redisClient, configConfig)
|
||||
rpmCache := repository.NewRPMCache(redisClient)
|
||||
@ -176,7 +179,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
deferredService := service.ProvideDeferredService(accountRepository, timingWheelService)
|
||||
claudeTokenProvider := service.ProvideClaudeTokenProvider(accountRepository, geminiTokenCache, oAuthService, oauthRefreshAPI)
|
||||
digestSessionStore := service.NewDigestSessionStore()
|
||||
gatewayService := service.NewGatewayService(accountRepository, groupRepository, usageLogRepository, usageBillingRepository, userRepository, userSubscriptionRepository, userGroupRateRepository, gatewayCache, configConfig, schedulerSnapshotService, concurrencyService, billingService, rateLimitService, billingCacheService, identityService, httpUpstream, deferredService, claudeTokenProvider, sessionLimitCache, rpmCache, digestSessionStore, settingService)
|
||||
gatewayService := service.NewGatewayService(accountRepository, groupRepository, usageLogRepository, usageBillingRepository, userRepository, userSubscriptionRepository, userGroupRateRepository, gatewayCache, configConfig, schedulerSnapshotService, concurrencyService, billingService, rateLimitService, billingCacheService, identityService, httpUpstream, deferredService, claudeTokenProvider, sessionLimitCache, rpmCache, digestSessionStore, settingService, tlsFingerprintProfileService)
|
||||
openAITokenProvider := service.ProvideOpenAITokenProvider(accountRepository, geminiTokenCache, openAIOAuthService, oauthRefreshAPI)
|
||||
openAIGatewayService := service.NewOpenAIGatewayService(accountRepository, usageLogRepository, usageBillingRepository, userRepository, userSubscriptionRepository, userGroupRateRepository, gatewayCache, configConfig, schedulerSnapshotService, concurrencyService, billingService, rateLimitService, billingCacheService, httpUpstream, deferredService, openAITokenProvider)
|
||||
geminiMessagesCompatService := service.NewGeminiMessagesCompatService(accountRepository, groupRepository, gatewayCache, schedulerSnapshotService, geminiTokenProvider, rateLimitService, httpUpstream, antigravityGatewayService, configConfig)
|
||||
@ -208,12 +211,13 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
errorPassthroughCache := repository.NewErrorPassthroughCache(redisClient)
|
||||
errorPassthroughService := service.NewErrorPassthroughService(errorPassthroughRepository, errorPassthroughCache)
|
||||
errorPassthroughHandler := admin.NewErrorPassthroughHandler(errorPassthroughService)
|
||||
tlsFingerprintProfileHandler := admin.NewTLSFingerprintProfileHandler(tlsFingerprintProfileService)
|
||||
adminAPIKeyHandler := admin.NewAdminAPIKeyHandler(adminService)
|
||||
scheduledTestPlanRepository := repository.NewScheduledTestPlanRepository(db)
|
||||
scheduledTestResultRepository := repository.NewScheduledTestResultRepository(db)
|
||||
scheduledTestService := service.ProvideScheduledTestService(scheduledTestPlanRepository, scheduledTestResultRepository)
|
||||
scheduledTestHandler := admin.NewScheduledTestHandler(scheduledTestService)
|
||||
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, dataManagementHandler, backupHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, adminAPIKeyHandler, scheduledTestHandler)
|
||||
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, dataManagementHandler, backupHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, tlsFingerprintProfileHandler, adminAPIKeyHandler, scheduledTestHandler)
|
||||
usageRecordWorkerPool := service.NewUsageRecordWorkerPool(configConfig)
|
||||
userMsgQueueCache := repository.NewUserMsgQueueCache(redisClient)
|
||||
userMessageQueueService := service.ProvideUserMessageQueueService(userMsgQueueCache, rpmCache, configConfig)
|
||||
|
||||
@ -29,6 +29,7 @@ import (
|
||||
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
|
||||
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
||||
"github.com/Wei-Shaw/sub2api/ent/setting"
|
||||
"github.com/Wei-Shaw/sub2api/ent/tlsfingerprintprofile"
|
||||
"github.com/Wei-Shaw/sub2api/ent/usagecleanuptask"
|
||||
"github.com/Wei-Shaw/sub2api/ent/usagelog"
|
||||
"github.com/Wei-Shaw/sub2api/ent/user"
|
||||
@ -73,6 +74,8 @@ type Client struct {
|
||||
SecuritySecret *SecuritySecretClient
|
||||
// Setting is the client for interacting with the Setting builders.
|
||||
Setting *SettingClient
|
||||
// TLSFingerprintProfile is the client for interacting with the TLSFingerprintProfile builders.
|
||||
TLSFingerprintProfile *TLSFingerprintProfileClient
|
||||
// UsageCleanupTask is the client for interacting with the UsageCleanupTask builders.
|
||||
UsageCleanupTask *UsageCleanupTaskClient
|
||||
// UsageLog is the client for interacting with the UsageLog builders.
|
||||
@ -112,6 +115,7 @@ func (c *Client) init() {
|
||||
c.RedeemCode = NewRedeemCodeClient(c.config)
|
||||
c.SecuritySecret = NewSecuritySecretClient(c.config)
|
||||
c.Setting = NewSettingClient(c.config)
|
||||
c.TLSFingerprintProfile = NewTLSFingerprintProfileClient(c.config)
|
||||
c.UsageCleanupTask = NewUsageCleanupTaskClient(c.config)
|
||||
c.UsageLog = NewUsageLogClient(c.config)
|
||||
c.User = NewUserClient(c.config)
|
||||
@ -225,6 +229,7 @@ func (c *Client) Tx(ctx context.Context) (*Tx, error) {
|
||||
RedeemCode: NewRedeemCodeClient(cfg),
|
||||
SecuritySecret: NewSecuritySecretClient(cfg),
|
||||
Setting: NewSettingClient(cfg),
|
||||
TLSFingerprintProfile: NewTLSFingerprintProfileClient(cfg),
|
||||
UsageCleanupTask: NewUsageCleanupTaskClient(cfg),
|
||||
UsageLog: NewUsageLogClient(cfg),
|
||||
User: NewUserClient(cfg),
|
||||
@ -265,6 +270,7 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)
|
||||
RedeemCode: NewRedeemCodeClient(cfg),
|
||||
SecuritySecret: NewSecuritySecretClient(cfg),
|
||||
Setting: NewSettingClient(cfg),
|
||||
TLSFingerprintProfile: NewTLSFingerprintProfileClient(cfg),
|
||||
UsageCleanupTask: NewUsageCleanupTaskClient(cfg),
|
||||
UsageLog: NewUsageLogClient(cfg),
|
||||
User: NewUserClient(cfg),
|
||||
@ -304,8 +310,9 @@ func (c *Client) Use(hooks ...Hook) {
|
||||
c.APIKey, c.Account, c.AccountGroup, c.Announcement, c.AnnouncementRead,
|
||||
c.ErrorPassthroughRule, c.Group, c.IdempotencyRecord, c.PromoCode,
|
||||
c.PromoCodeUsage, c.Proxy, c.RedeemCode, c.SecuritySecret, c.Setting,
|
||||
c.UsageCleanupTask, c.UsageLog, c.User, c.UserAllowedGroup,
|
||||
c.UserAttributeDefinition, c.UserAttributeValue, c.UserSubscription,
|
||||
c.TLSFingerprintProfile, c.UsageCleanupTask, c.UsageLog, c.User,
|
||||
c.UserAllowedGroup, c.UserAttributeDefinition, c.UserAttributeValue,
|
||||
c.UserSubscription,
|
||||
} {
|
||||
n.Use(hooks...)
|
||||
}
|
||||
@ -318,8 +325,9 @@ func (c *Client) Intercept(interceptors ...Interceptor) {
|
||||
c.APIKey, c.Account, c.AccountGroup, c.Announcement, c.AnnouncementRead,
|
||||
c.ErrorPassthroughRule, c.Group, c.IdempotencyRecord, c.PromoCode,
|
||||
c.PromoCodeUsage, c.Proxy, c.RedeemCode, c.SecuritySecret, c.Setting,
|
||||
c.UsageCleanupTask, c.UsageLog, c.User, c.UserAllowedGroup,
|
||||
c.UserAttributeDefinition, c.UserAttributeValue, c.UserSubscription,
|
||||
c.TLSFingerprintProfile, c.UsageCleanupTask, c.UsageLog, c.User,
|
||||
c.UserAllowedGroup, c.UserAttributeDefinition, c.UserAttributeValue,
|
||||
c.UserSubscription,
|
||||
} {
|
||||
n.Intercept(interceptors...)
|
||||
}
|
||||
@ -356,6 +364,8 @@ func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) {
|
||||
return c.SecuritySecret.mutate(ctx, m)
|
||||
case *SettingMutation:
|
||||
return c.Setting.mutate(ctx, m)
|
||||
case *TLSFingerprintProfileMutation:
|
||||
return c.TLSFingerprintProfile.mutate(ctx, m)
|
||||
case *UsageCleanupTaskMutation:
|
||||
return c.UsageCleanupTask.mutate(ctx, m)
|
||||
case *UsageLogMutation:
|
||||
@ -2612,6 +2622,139 @@ func (c *SettingClient) mutate(ctx context.Context, m *SettingMutation) (Value,
|
||||
}
|
||||
}
|
||||
|
||||
// TLSFingerprintProfileClient is a client for the TLSFingerprintProfile schema.
|
||||
type TLSFingerprintProfileClient struct {
|
||||
config
|
||||
}
|
||||
|
||||
// NewTLSFingerprintProfileClient returns a client for the TLSFingerprintProfile from the given config.
|
||||
func NewTLSFingerprintProfileClient(c config) *TLSFingerprintProfileClient {
|
||||
return &TLSFingerprintProfileClient{config: c}
|
||||
}
|
||||
|
||||
// Use adds a list of mutation hooks to the hooks stack.
|
||||
// A call to `Use(f, g, h)` equals to `tlsfingerprintprofile.Hooks(f(g(h())))`.
|
||||
func (c *TLSFingerprintProfileClient) Use(hooks ...Hook) {
|
||||
c.hooks.TLSFingerprintProfile = append(c.hooks.TLSFingerprintProfile, hooks...)
|
||||
}
|
||||
|
||||
// Intercept adds a list of query interceptors to the interceptors stack.
|
||||
// A call to `Intercept(f, g, h)` equals to `tlsfingerprintprofile.Intercept(f(g(h())))`.
|
||||
func (c *TLSFingerprintProfileClient) Intercept(interceptors ...Interceptor) {
|
||||
c.inters.TLSFingerprintProfile = append(c.inters.TLSFingerprintProfile, interceptors...)
|
||||
}
|
||||
|
||||
// Create returns a builder for creating a TLSFingerprintProfile entity.
|
||||
func (c *TLSFingerprintProfileClient) Create() *TLSFingerprintProfileCreate {
|
||||
mutation := newTLSFingerprintProfileMutation(c.config, OpCreate)
|
||||
return &TLSFingerprintProfileCreate{config: c.config, hooks: c.Hooks(), mutation: mutation}
|
||||
}
|
||||
|
||||
// CreateBulk returns a builder for creating a bulk of TLSFingerprintProfile entities.
|
||||
func (c *TLSFingerprintProfileClient) CreateBulk(builders ...*TLSFingerprintProfileCreate) *TLSFingerprintProfileCreateBulk {
|
||||
return &TLSFingerprintProfileCreateBulk{config: c.config, builders: builders}
|
||||
}
|
||||
|
||||
// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates
|
||||
// a builder and applies setFunc on it.
|
||||
func (c *TLSFingerprintProfileClient) MapCreateBulk(slice any, setFunc func(*TLSFingerprintProfileCreate, int)) *TLSFingerprintProfileCreateBulk {
|
||||
rv := reflect.ValueOf(slice)
|
||||
if rv.Kind() != reflect.Slice {
|
||||
return &TLSFingerprintProfileCreateBulk{err: fmt.Errorf("calling to TLSFingerprintProfileClient.MapCreateBulk with wrong type %T, need slice", slice)}
|
||||
}
|
||||
builders := make([]*TLSFingerprintProfileCreate, rv.Len())
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
builders[i] = c.Create()
|
||||
setFunc(builders[i], i)
|
||||
}
|
||||
return &TLSFingerprintProfileCreateBulk{config: c.config, builders: builders}
|
||||
}
|
||||
|
||||
// Update returns an update builder for TLSFingerprintProfile.
|
||||
func (c *TLSFingerprintProfileClient) Update() *TLSFingerprintProfileUpdate {
|
||||
mutation := newTLSFingerprintProfileMutation(c.config, OpUpdate)
|
||||
return &TLSFingerprintProfileUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation}
|
||||
}
|
||||
|
||||
// UpdateOne returns an update builder for the given entity.
|
||||
func (c *TLSFingerprintProfileClient) UpdateOne(_m *TLSFingerprintProfile) *TLSFingerprintProfileUpdateOne {
|
||||
mutation := newTLSFingerprintProfileMutation(c.config, OpUpdateOne, withTLSFingerprintProfile(_m))
|
||||
return &TLSFingerprintProfileUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
|
||||
}
|
||||
|
||||
// UpdateOneID returns an update builder for the given id.
|
||||
func (c *TLSFingerprintProfileClient) UpdateOneID(id int64) *TLSFingerprintProfileUpdateOne {
|
||||
mutation := newTLSFingerprintProfileMutation(c.config, OpUpdateOne, withTLSFingerprintProfileID(id))
|
||||
return &TLSFingerprintProfileUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
|
||||
}
|
||||
|
||||
// Delete returns a delete builder for TLSFingerprintProfile.
|
||||
func (c *TLSFingerprintProfileClient) Delete() *TLSFingerprintProfileDelete {
|
||||
mutation := newTLSFingerprintProfileMutation(c.config, OpDelete)
|
||||
return &TLSFingerprintProfileDelete{config: c.config, hooks: c.Hooks(), mutation: mutation}
|
||||
}
|
||||
|
||||
// DeleteOne returns a builder for deleting the given entity.
|
||||
func (c *TLSFingerprintProfileClient) DeleteOne(_m *TLSFingerprintProfile) *TLSFingerprintProfileDeleteOne {
|
||||
return c.DeleteOneID(_m.ID)
|
||||
}
|
||||
|
||||
// DeleteOneID returns a builder for deleting the given entity by its id.
|
||||
func (c *TLSFingerprintProfileClient) DeleteOneID(id int64) *TLSFingerprintProfileDeleteOne {
|
||||
builder := c.Delete().Where(tlsfingerprintprofile.ID(id))
|
||||
builder.mutation.id = &id
|
||||
builder.mutation.op = OpDeleteOne
|
||||
return &TLSFingerprintProfileDeleteOne{builder}
|
||||
}
|
||||
|
||||
// Query returns a query builder for TLSFingerprintProfile.
|
||||
func (c *TLSFingerprintProfileClient) Query() *TLSFingerprintProfileQuery {
|
||||
return &TLSFingerprintProfileQuery{
|
||||
config: c.config,
|
||||
ctx: &QueryContext{Type: TypeTLSFingerprintProfile},
|
||||
inters: c.Interceptors(),
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns a TLSFingerprintProfile entity by its id.
|
||||
func (c *TLSFingerprintProfileClient) Get(ctx context.Context, id int64) (*TLSFingerprintProfile, error) {
|
||||
return c.Query().Where(tlsfingerprintprofile.ID(id)).Only(ctx)
|
||||
}
|
||||
|
||||
// GetX is like Get, but panics if an error occurs.
|
||||
func (c *TLSFingerprintProfileClient) GetX(ctx context.Context, id int64) *TLSFingerprintProfile {
|
||||
obj, err := c.Get(ctx, id)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
// Hooks returns the client hooks.
|
||||
func (c *TLSFingerprintProfileClient) Hooks() []Hook {
|
||||
return c.hooks.TLSFingerprintProfile
|
||||
}
|
||||
|
||||
// Interceptors returns the client interceptors.
|
||||
func (c *TLSFingerprintProfileClient) Interceptors() []Interceptor {
|
||||
return c.inters.TLSFingerprintProfile
|
||||
}
|
||||
|
||||
func (c *TLSFingerprintProfileClient) mutate(ctx context.Context, m *TLSFingerprintProfileMutation) (Value, error) {
|
||||
switch m.Op() {
|
||||
case OpCreate:
|
||||
return (&TLSFingerprintProfileCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
|
||||
case OpUpdate:
|
||||
return (&TLSFingerprintProfileUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
|
||||
case OpUpdateOne:
|
||||
return (&TLSFingerprintProfileUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
|
||||
case OpDelete, OpDeleteOne:
|
||||
return (&TLSFingerprintProfileDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx)
|
||||
default:
|
||||
return nil, fmt.Errorf("ent: unknown TLSFingerprintProfile mutation op: %q", m.Op())
|
||||
}
|
||||
}
|
||||
|
||||
// UsageCleanupTaskClient is a client for the UsageCleanupTask schema.
|
||||
type UsageCleanupTaskClient struct {
|
||||
config
|
||||
@ -3889,16 +4032,16 @@ type (
|
||||
hooks struct {
|
||||
APIKey, Account, AccountGroup, Announcement, AnnouncementRead,
|
||||
ErrorPassthroughRule, Group, IdempotencyRecord, PromoCode, PromoCodeUsage,
|
||||
Proxy, RedeemCode, SecuritySecret, Setting, UsageCleanupTask, UsageLog, User,
|
||||
UserAllowedGroup, UserAttributeDefinition, UserAttributeValue,
|
||||
UserSubscription []ent.Hook
|
||||
Proxy, RedeemCode, SecuritySecret, Setting, TLSFingerprintProfile,
|
||||
UsageCleanupTask, UsageLog, User, UserAllowedGroup, UserAttributeDefinition,
|
||||
UserAttributeValue, UserSubscription []ent.Hook
|
||||
}
|
||||
inters struct {
|
||||
APIKey, Account, AccountGroup, Announcement, AnnouncementRead,
|
||||
ErrorPassthroughRule, Group, IdempotencyRecord, PromoCode, PromoCodeUsage,
|
||||
Proxy, RedeemCode, SecuritySecret, Setting, UsageCleanupTask, UsageLog, User,
|
||||
UserAllowedGroup, UserAttributeDefinition, UserAttributeValue,
|
||||
UserSubscription []ent.Interceptor
|
||||
Proxy, RedeemCode, SecuritySecret, Setting, TLSFingerprintProfile,
|
||||
UsageCleanupTask, UsageLog, User, UserAllowedGroup, UserAttributeDefinition,
|
||||
UserAttributeValue, UserSubscription []ent.Interceptor
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ import (
|
||||
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
|
||||
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
||||
"github.com/Wei-Shaw/sub2api/ent/setting"
|
||||
"github.com/Wei-Shaw/sub2api/ent/tlsfingerprintprofile"
|
||||
"github.com/Wei-Shaw/sub2api/ent/usagecleanuptask"
|
||||
"github.com/Wei-Shaw/sub2api/ent/usagelog"
|
||||
"github.com/Wei-Shaw/sub2api/ent/user"
|
||||
@ -107,6 +108,7 @@ func checkColumn(t, c string) error {
|
||||
redeemcode.Table: redeemcode.ValidColumn,
|
||||
securitysecret.Table: securitysecret.ValidColumn,
|
||||
setting.Table: setting.ValidColumn,
|
||||
tlsfingerprintprofile.Table: tlsfingerprintprofile.ValidColumn,
|
||||
usagecleanuptask.Table: usagecleanuptask.ValidColumn,
|
||||
usagelog.Table: usagelog.ValidColumn,
|
||||
user.Table: user.ValidColumn,
|
||||
|
||||
@ -177,6 +177,18 @@ func (f SettingFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, err
|
||||
return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.SettingMutation", m)
|
||||
}
|
||||
|
||||
// The TLSFingerprintProfileFunc type is an adapter to allow the use of ordinary
|
||||
// function as TLSFingerprintProfile mutator.
|
||||
type TLSFingerprintProfileFunc func(context.Context, *ent.TLSFingerprintProfileMutation) (ent.Value, error)
|
||||
|
||||
// Mutate calls f(ctx, m).
|
||||
func (f TLSFingerprintProfileFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) {
|
||||
if mv, ok := m.(*ent.TLSFingerprintProfileMutation); ok {
|
||||
return f(ctx, mv)
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.TLSFingerprintProfileMutation", m)
|
||||
}
|
||||
|
||||
// The UsageCleanupTaskFunc type is an adapter to allow the use of ordinary
|
||||
// function as UsageCleanupTask mutator.
|
||||
type UsageCleanupTaskFunc func(context.Context, *ent.UsageCleanupTaskMutation) (ent.Value, error)
|
||||
|
||||
@ -23,6 +23,7 @@ import (
|
||||
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
|
||||
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
||||
"github.com/Wei-Shaw/sub2api/ent/setting"
|
||||
"github.com/Wei-Shaw/sub2api/ent/tlsfingerprintprofile"
|
||||
"github.com/Wei-Shaw/sub2api/ent/usagecleanuptask"
|
||||
"github.com/Wei-Shaw/sub2api/ent/usagelog"
|
||||
"github.com/Wei-Shaw/sub2api/ent/user"
|
||||
@ -466,6 +467,33 @@ func (f TraverseSetting) Traverse(ctx context.Context, q ent.Query) error {
|
||||
return fmt.Errorf("unexpected query type %T. expect *ent.SettingQuery", q)
|
||||
}
|
||||
|
||||
// The TLSFingerprintProfileFunc type is an adapter to allow the use of ordinary function as a Querier.
|
||||
type TLSFingerprintProfileFunc func(context.Context, *ent.TLSFingerprintProfileQuery) (ent.Value, error)
|
||||
|
||||
// Query calls f(ctx, q).
|
||||
func (f TLSFingerprintProfileFunc) Query(ctx context.Context, q ent.Query) (ent.Value, error) {
|
||||
if q, ok := q.(*ent.TLSFingerprintProfileQuery); ok {
|
||||
return f(ctx, q)
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected query type %T. expect *ent.TLSFingerprintProfileQuery", q)
|
||||
}
|
||||
|
||||
// The TraverseTLSFingerprintProfile type is an adapter to allow the use of ordinary function as Traverser.
|
||||
type TraverseTLSFingerprintProfile func(context.Context, *ent.TLSFingerprintProfileQuery) error
|
||||
|
||||
// Intercept is a dummy implementation of Intercept that returns the next Querier in the pipeline.
|
||||
func (f TraverseTLSFingerprintProfile) Intercept(next ent.Querier) ent.Querier {
|
||||
return next
|
||||
}
|
||||
|
||||
// Traverse calls f(ctx, q).
|
||||
func (f TraverseTLSFingerprintProfile) Traverse(ctx context.Context, q ent.Query) error {
|
||||
if q, ok := q.(*ent.TLSFingerprintProfileQuery); ok {
|
||||
return f(ctx, q)
|
||||
}
|
||||
return fmt.Errorf("unexpected query type %T. expect *ent.TLSFingerprintProfileQuery", q)
|
||||
}
|
||||
|
||||
// The UsageCleanupTaskFunc type is an adapter to allow the use of ordinary function as a Querier.
|
||||
type UsageCleanupTaskFunc func(context.Context, *ent.UsageCleanupTaskQuery) (ent.Value, error)
|
||||
|
||||
@ -686,6 +714,8 @@ func NewQuery(q ent.Query) (Query, error) {
|
||||
return &query[*ent.SecuritySecretQuery, predicate.SecuritySecret, securitysecret.OrderOption]{typ: ent.TypeSecuritySecret, tq: q}, nil
|
||||
case *ent.SettingQuery:
|
||||
return &query[*ent.SettingQuery, predicate.Setting, setting.OrderOption]{typ: ent.TypeSetting, tq: q}, nil
|
||||
case *ent.TLSFingerprintProfileQuery:
|
||||
return &query[*ent.TLSFingerprintProfileQuery, predicate.TLSFingerprintProfile, tlsfingerprintprofile.OrderOption]{typ: ent.TypeTLSFingerprintProfile, tq: q}, nil
|
||||
case *ent.UsageCleanupTaskQuery:
|
||||
return &query[*ent.UsageCleanupTaskQuery, predicate.UsageCleanupTask, usagecleanuptask.OrderOption]{typ: ent.TypeUsageCleanupTask, tq: q}, nil
|
||||
case *ent.UsageLogQuery:
|
||||
|
||||
@ -673,6 +673,30 @@ var (
|
||||
Columns: SettingsColumns,
|
||||
PrimaryKey: []*schema.Column{SettingsColumns[0]},
|
||||
}
|
||||
// TLSFingerprintProfilesColumns holds the columns for the "tls_fingerprint_profiles" table.
|
||||
TLSFingerprintProfilesColumns = []*schema.Column{
|
||||
{Name: "id", Type: field.TypeInt64, Increment: true},
|
||||
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||
{Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||
{Name: "name", Type: field.TypeString, Unique: true, Size: 100},
|
||||
{Name: "description", Type: field.TypeString, Nullable: true, Size: 2147483647},
|
||||
{Name: "enable_grease", Type: field.TypeBool, Default: false},
|
||||
{Name: "cipher_suites", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||
{Name: "curves", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||
{Name: "point_formats", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||
{Name: "signature_algorithms", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||
{Name: "alpn_protocols", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||
{Name: "supported_versions", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||
{Name: "key_share_groups", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||
{Name: "psk_modes", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||
{Name: "extensions", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||
}
|
||||
// TLSFingerprintProfilesTable holds the schema information for the "tls_fingerprint_profiles" table.
|
||||
TLSFingerprintProfilesTable = &schema.Table{
|
||||
Name: "tls_fingerprint_profiles",
|
||||
Columns: TLSFingerprintProfilesColumns,
|
||||
PrimaryKey: []*schema.Column{TLSFingerprintProfilesColumns[0]},
|
||||
}
|
||||
// UsageCleanupTasksColumns holds the columns for the "usage_cleanup_tasks" table.
|
||||
UsageCleanupTasksColumns = []*schema.Column{
|
||||
{Name: "id", Type: field.TypeInt64, Increment: true},
|
||||
@ -1111,6 +1135,7 @@ var (
|
||||
RedeemCodesTable,
|
||||
SecuritySecretsTable,
|
||||
SettingsTable,
|
||||
TLSFingerprintProfilesTable,
|
||||
UsageCleanupTasksTable,
|
||||
UsageLogsTable,
|
||||
UsersTable,
|
||||
@ -1175,6 +1200,9 @@ func init() {
|
||||
SettingsTable.Annotation = &entsql.Annotation{
|
||||
Table: "settings",
|
||||
}
|
||||
TLSFingerprintProfilesTable.Annotation = &entsql.Annotation{
|
||||
Table: "tls_fingerprint_profiles",
|
||||
}
|
||||
UsageCleanupTasksTable.Annotation = &entsql.Annotation{
|
||||
Table: "usage_cleanup_tasks",
|
||||
}
|
||||
|
||||
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.
|
||||
type Setting func(*sql.Selector)
|
||||
|
||||
// TLSFingerprintProfile is the predicate function for tlsfingerprintprofile builders.
|
||||
type TLSFingerprintProfile func(*sql.Selector)
|
||||
|
||||
// UsageCleanupTask is the predicate function for usagecleanuptask builders.
|
||||
type UsageCleanupTask func(*sql.Selector)
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ import (
|
||||
"github.com/Wei-Shaw/sub2api/ent/schema"
|
||||
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
||||
"github.com/Wei-Shaw/sub2api/ent/setting"
|
||||
"github.com/Wei-Shaw/sub2api/ent/tlsfingerprintprofile"
|
||||
"github.com/Wei-Shaw/sub2api/ent/usagecleanuptask"
|
||||
"github.com/Wei-Shaw/sub2api/ent/usagelog"
|
||||
"github.com/Wei-Shaw/sub2api/ent/user"
|
||||
@ -746,6 +747,43 @@ func init() {
|
||||
setting.DefaultUpdatedAt = settingDescUpdatedAt.Default.(func() time.Time)
|
||||
// setting.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field.
|
||||
setting.UpdateDefaultUpdatedAt = settingDescUpdatedAt.UpdateDefault.(func() time.Time)
|
||||
tlsfingerprintprofileMixin := schema.TLSFingerprintProfile{}.Mixin()
|
||||
tlsfingerprintprofileMixinFields0 := tlsfingerprintprofileMixin[0].Fields()
|
||||
_ = tlsfingerprintprofileMixinFields0
|
||||
tlsfingerprintprofileFields := schema.TLSFingerprintProfile{}.Fields()
|
||||
_ = tlsfingerprintprofileFields
|
||||
// tlsfingerprintprofileDescCreatedAt is the schema descriptor for created_at field.
|
||||
tlsfingerprintprofileDescCreatedAt := tlsfingerprintprofileMixinFields0[0].Descriptor()
|
||||
// tlsfingerprintprofile.DefaultCreatedAt holds the default value on creation for the created_at field.
|
||||
tlsfingerprintprofile.DefaultCreatedAt = tlsfingerprintprofileDescCreatedAt.Default.(func() time.Time)
|
||||
// tlsfingerprintprofileDescUpdatedAt is the schema descriptor for updated_at field.
|
||||
tlsfingerprintprofileDescUpdatedAt := tlsfingerprintprofileMixinFields0[1].Descriptor()
|
||||
// tlsfingerprintprofile.DefaultUpdatedAt holds the default value on creation for the updated_at field.
|
||||
tlsfingerprintprofile.DefaultUpdatedAt = tlsfingerprintprofileDescUpdatedAt.Default.(func() time.Time)
|
||||
// tlsfingerprintprofile.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field.
|
||||
tlsfingerprintprofile.UpdateDefaultUpdatedAt = tlsfingerprintprofileDescUpdatedAt.UpdateDefault.(func() time.Time)
|
||||
// tlsfingerprintprofileDescName is the schema descriptor for name field.
|
||||
tlsfingerprintprofileDescName := tlsfingerprintprofileFields[0].Descriptor()
|
||||
// tlsfingerprintprofile.NameValidator is a validator for the "name" field. It is called by the builders before save.
|
||||
tlsfingerprintprofile.NameValidator = func() func(string) error {
|
||||
validators := tlsfingerprintprofileDescName.Validators
|
||||
fns := [...]func(string) error{
|
||||
validators[0].(func(string) error),
|
||||
validators[1].(func(string) error),
|
||||
}
|
||||
return func(name string) error {
|
||||
for _, fn := range fns {
|
||||
if err := fn(name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
// tlsfingerprintprofileDescEnableGrease is the schema descriptor for enable_grease field.
|
||||
tlsfingerprintprofileDescEnableGrease := tlsfingerprintprofileFields[2].Descriptor()
|
||||
// tlsfingerprintprofile.DefaultEnableGrease holds the default value on creation for the enable_grease field.
|
||||
tlsfingerprintprofile.DefaultEnableGrease = tlsfingerprintprofileDescEnableGrease.Default.(bool)
|
||||
usagecleanuptaskMixin := schema.UsageCleanupTask{}.Mixin()
|
||||
usagecleanuptaskMixinFields0 := usagecleanuptaskMixin[0].Fields()
|
||||
_ = usagecleanuptaskMixinFields0
|
||||
|
||||
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
|
||||
// Setting is the client for interacting with the Setting builders.
|
||||
Setting *SettingClient
|
||||
// TLSFingerprintProfile is the client for interacting with the TLSFingerprintProfile builders.
|
||||
TLSFingerprintProfile *TLSFingerprintProfileClient
|
||||
// UsageCleanupTask is the client for interacting with the UsageCleanupTask builders.
|
||||
UsageCleanupTask *UsageCleanupTaskClient
|
||||
// UsageLog is the client for interacting with the UsageLog builders.
|
||||
@ -201,6 +203,7 @@ func (tx *Tx) init() {
|
||||
tx.RedeemCode = NewRedeemCodeClient(tx.config)
|
||||
tx.SecuritySecret = NewSecuritySecretClient(tx.config)
|
||||
tx.Setting = NewSettingClient(tx.config)
|
||||
tx.TLSFingerprintProfile = NewTLSFingerprintProfileClient(tx.config)
|
||||
tx.UsageCleanupTask = NewUsageCleanupTaskClient(tx.config)
|
||||
tx.UsageLog = NewUsageLogClient(tx.config)
|
||||
tx.User = NewUserClient(tx.config)
|
||||
|
||||
@ -673,17 +673,33 @@ type TLSFingerprintConfig struct {
|
||||
}
|
||||
|
||||
// TLSProfileConfig 单个TLS指纹模板的配置
|
||||
// 所有列表字段为空时使用内置默认值(Claude CLI 2.x / Node.js 20.x)
|
||||
// 建议通过 TLS 指纹采集工具 (tests/tls-fingerprint-web) 获取完整配置
|
||||
type TLSProfileConfig struct {
|
||||
// Name: 模板显示名称
|
||||
Name string `mapstructure:"name"`
|
||||
// EnableGREASE: 是否启用GREASE扩展(Chrome使用,Node.js不使用)
|
||||
EnableGREASE bool `mapstructure:"enable_grease"`
|
||||
// CipherSuites: TLS加密套件列表(空则使用内置默认值)
|
||||
// CipherSuites: TLS加密套件列表
|
||||
CipherSuites []uint16 `mapstructure:"cipher_suites"`
|
||||
// Curves: 椭圆曲线列表(空则使用内置默认值)
|
||||
// Curves: 椭圆曲线列表
|
||||
Curves []uint16 `mapstructure:"curves"`
|
||||
// PointFormats: 点格式列表(空则使用内置默认值)
|
||||
PointFormats []uint8 `mapstructure:"point_formats"`
|
||||
// PointFormats: 点格式列表
|
||||
PointFormats []uint16 `mapstructure:"point_formats"`
|
||||
// SignatureAlgorithms: 签名算法列表
|
||||
SignatureAlgorithms []uint16 `mapstructure:"signature_algorithms"`
|
||||
// ALPNProtocols: ALPN协议列表(如 ["h2", "http/1.1"])
|
||||
ALPNProtocols []string `mapstructure:"alpn_protocols"`
|
||||
// SupportedVersions: 支持的TLS版本列表(如 [0x0304, 0x0303] 即 TLS1.3, TLS1.2)
|
||||
SupportedVersions []uint16 `mapstructure:"supported_versions"`
|
||||
// KeyShareGroups: Key Share中发送的曲线组(如 [29] 即 X25519)
|
||||
KeyShareGroups []uint16 `mapstructure:"key_share_groups"`
|
||||
// PSKModes: PSK密钥交换模式(如 [1] 即 psk_dhe_ke)
|
||||
PSKModes []uint16 `mapstructure:"psk_modes"`
|
||||
// Extensions: TLS扩展类型ID列表,按发送顺序排列
|
||||
// 空则使用内置默认顺序 [0,11,10,35,16,22,23,13,43,45,51]
|
||||
// GREASE值(如0x0a0a)会自动插入GREASE扩展
|
||||
Extensions []uint16 `mapstructure:"extensions"`
|
||||
}
|
||||
|
||||
// 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
|
||||
out.EnableTLSFingerprint = &enabled
|
||||
}
|
||||
// TLS指纹模板ID
|
||||
if profileID := a.GetTLSFingerprintProfileID(); profileID > 0 {
|
||||
out.TLSFingerprintProfileID = &profileID
|
||||
}
|
||||
// 会话ID伪装开关
|
||||
if a.IsSessionIDMaskingEnabled() {
|
||||
enabled := true
|
||||
|
||||
@ -185,7 +185,8 @@ type Account struct {
|
||||
|
||||
// TLS指纹伪装(仅 Anthropic OAuth/SetupToken 账号有效)
|
||||
// 从 extra 字段提取,方便前端显示和编辑
|
||||
EnableTLSFingerprint *bool `json:"enable_tls_fingerprint,omitempty"`
|
||||
EnableTLSFingerprint *bool `json:"enable_tls_fingerprint,omitempty"`
|
||||
TLSFingerprintProfileID *int64 `json:"tls_fingerprint_profile_id,omitempty"`
|
||||
|
||||
// 会话ID伪装(仅 Anthropic OAuth/SetupToken 账号有效)
|
||||
// 启用后将在15分钟内固定 metadata.user_id 中的 session ID
|
||||
|
||||
@ -75,8 +75,10 @@ func (f *fakeGroupRepo) ListActive(context.Context) ([]service.Group, error) { r
|
||||
func (f *fakeGroupRepo) ListActiveByPlatform(context.Context, string) ([]service.Group, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakeGroupRepo) ExistsByName(context.Context, string) (bool, error) { return false, nil }
|
||||
func (f *fakeGroupRepo) GetAccountCount(context.Context, int64) (int64, int64, error) { return 0, 0, nil }
|
||||
func (f *fakeGroupRepo) ExistsByName(context.Context, string) (bool, error) { return false, nil }
|
||||
func (f *fakeGroupRepo) GetAccountCount(context.Context, int64) (int64, int64, error) {
|
||||
return 0, 0, nil
|
||||
}
|
||||
func (f *fakeGroupRepo) DeleteAccountGroupsByGroupID(context.Context, int64) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
@ -158,6 +160,7 @@ func newTestGatewayHandler(t *testing.T, group *service.Group, accounts []*servi
|
||||
nil, // rpmCache
|
||||
nil, // digestStore
|
||||
nil, // settingService
|
||||
nil, // tlsFPProfileService
|
||||
)
|
||||
|
||||
// RunModeSimple:跳过计费检查,避免引入 repo/cache 依赖。
|
||||
|
||||
@ -6,29 +6,30 @@ import (
|
||||
|
||||
// AdminHandlers contains all admin-related HTTP handlers
|
||||
type AdminHandlers struct {
|
||||
Dashboard *admin.DashboardHandler
|
||||
User *admin.UserHandler
|
||||
Group *admin.GroupHandler
|
||||
Account *admin.AccountHandler
|
||||
Announcement *admin.AnnouncementHandler
|
||||
DataManagement *admin.DataManagementHandler
|
||||
Backup *admin.BackupHandler
|
||||
OAuth *admin.OAuthHandler
|
||||
OpenAIOAuth *admin.OpenAIOAuthHandler
|
||||
GeminiOAuth *admin.GeminiOAuthHandler
|
||||
AntigravityOAuth *admin.AntigravityOAuthHandler
|
||||
Proxy *admin.ProxyHandler
|
||||
Redeem *admin.RedeemHandler
|
||||
Promo *admin.PromoHandler
|
||||
Setting *admin.SettingHandler
|
||||
Ops *admin.OpsHandler
|
||||
System *admin.SystemHandler
|
||||
Subscription *admin.SubscriptionHandler
|
||||
Usage *admin.UsageHandler
|
||||
UserAttribute *admin.UserAttributeHandler
|
||||
ErrorPassthrough *admin.ErrorPassthroughHandler
|
||||
APIKey *admin.AdminAPIKeyHandler
|
||||
ScheduledTest *admin.ScheduledTestHandler
|
||||
Dashboard *admin.DashboardHandler
|
||||
User *admin.UserHandler
|
||||
Group *admin.GroupHandler
|
||||
Account *admin.AccountHandler
|
||||
Announcement *admin.AnnouncementHandler
|
||||
DataManagement *admin.DataManagementHandler
|
||||
Backup *admin.BackupHandler
|
||||
OAuth *admin.OAuthHandler
|
||||
OpenAIOAuth *admin.OpenAIOAuthHandler
|
||||
GeminiOAuth *admin.GeminiOAuthHandler
|
||||
AntigravityOAuth *admin.AntigravityOAuthHandler
|
||||
Proxy *admin.ProxyHandler
|
||||
Redeem *admin.RedeemHandler
|
||||
Promo *admin.PromoHandler
|
||||
Setting *admin.SettingHandler
|
||||
Ops *admin.OpsHandler
|
||||
System *admin.SystemHandler
|
||||
Subscription *admin.SubscriptionHandler
|
||||
Usage *admin.UsageHandler
|
||||
UserAttribute *admin.UserAttributeHandler
|
||||
ErrorPassthrough *admin.ErrorPassthroughHandler
|
||||
TLSFingerprintProfile *admin.TLSFingerprintProfileHandler
|
||||
APIKey *admin.AdminAPIKeyHandler
|
||||
ScheduledTest *admin.ScheduledTestHandler
|
||||
}
|
||||
|
||||
// Handlers contains all HTTP handlers
|
||||
|
||||
@ -2224,7 +2224,7 @@ func (s *stubSoraClientForHandler) GetVideoTask(_ context.Context, _ *service.Ac
|
||||
func newMinimalGatewayService(accountRepo service.AccountRepository) *service.GatewayService {
|
||||
return service.NewGatewayService(
|
||||
accountRepo, nil, nil, nil, nil, nil, nil, nil, nil,
|
||||
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
||||
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -464,6 +464,7 @@ func TestSoraGatewayHandler_ChatCompletions(t *testing.T) {
|
||||
nil, // rpmCache
|
||||
nil, // digestStore
|
||||
nil, // settingService
|
||||
nil, // tlsFPProfileService
|
||||
)
|
||||
|
||||
soraClient := &stubSoraClient{imageURLs: []string{"https://example.com/a.png"}}
|
||||
|
||||
@ -30,33 +30,35 @@ func ProvideAdminHandlers(
|
||||
usageHandler *admin.UsageHandler,
|
||||
userAttributeHandler *admin.UserAttributeHandler,
|
||||
errorPassthroughHandler *admin.ErrorPassthroughHandler,
|
||||
tlsFingerprintProfileHandler *admin.TLSFingerprintProfileHandler,
|
||||
apiKeyHandler *admin.AdminAPIKeyHandler,
|
||||
scheduledTestHandler *admin.ScheduledTestHandler,
|
||||
) *AdminHandlers {
|
||||
return &AdminHandlers{
|
||||
Dashboard: dashboardHandler,
|
||||
User: userHandler,
|
||||
Group: groupHandler,
|
||||
Account: accountHandler,
|
||||
Announcement: announcementHandler,
|
||||
DataManagement: dataManagementHandler,
|
||||
Backup: backupHandler,
|
||||
OAuth: oauthHandler,
|
||||
OpenAIOAuth: openaiOAuthHandler,
|
||||
GeminiOAuth: geminiOAuthHandler,
|
||||
AntigravityOAuth: antigravityOAuthHandler,
|
||||
Proxy: proxyHandler,
|
||||
Redeem: redeemHandler,
|
||||
Promo: promoHandler,
|
||||
Setting: settingHandler,
|
||||
Ops: opsHandler,
|
||||
System: systemHandler,
|
||||
Subscription: subscriptionHandler,
|
||||
Usage: usageHandler,
|
||||
UserAttribute: userAttributeHandler,
|
||||
ErrorPassthrough: errorPassthroughHandler,
|
||||
APIKey: apiKeyHandler,
|
||||
ScheduledTest: scheduledTestHandler,
|
||||
Dashboard: dashboardHandler,
|
||||
User: userHandler,
|
||||
Group: groupHandler,
|
||||
Account: accountHandler,
|
||||
Announcement: announcementHandler,
|
||||
DataManagement: dataManagementHandler,
|
||||
Backup: backupHandler,
|
||||
OAuth: oauthHandler,
|
||||
OpenAIOAuth: openaiOAuthHandler,
|
||||
GeminiOAuth: geminiOAuthHandler,
|
||||
AntigravityOAuth: antigravityOAuthHandler,
|
||||
Proxy: proxyHandler,
|
||||
Redeem: redeemHandler,
|
||||
Promo: promoHandler,
|
||||
Setting: settingHandler,
|
||||
Ops: opsHandler,
|
||||
System: systemHandler,
|
||||
Subscription: subscriptionHandler,
|
||||
Usage: usageHandler,
|
||||
UserAttribute: userAttributeHandler,
|
||||
ErrorPassthrough: errorPassthroughHandler,
|
||||
TLSFingerprintProfile: tlsFingerprintProfileHandler,
|
||||
APIKey: apiKeyHandler,
|
||||
ScheduledTest: scheduledTestHandler,
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,6 +147,7 @@ var ProviderSet = wire.NewSet(
|
||||
admin.NewUsageHandler,
|
||||
admin.NewUserAttributeHandler,
|
||||
admin.NewErrorPassthroughHandler,
|
||||
admin.NewTLSFingerprintProfileHandler,
|
||||
admin.NewAdminAPIKeyHandler,
|
||||
admin.NewScheduledTestHandler,
|
||||
|
||||
|
||||
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.
|
||||
// All slice fields use built-in defaults when empty.
|
||||
type Profile struct {
|
||||
Name string // Profile name for identification
|
||||
CipherSuites []uint16
|
||||
Curves []uint16
|
||||
PointFormats []uint8
|
||||
EnableGREASE bool
|
||||
Name string // Profile name for identification
|
||||
CipherSuites []uint16
|
||||
Curves []uint16
|
||||
PointFormats []uint16
|
||||
EnableGREASE bool
|
||||
SignatureAlgorithms []uint16 // Empty uses defaultSignatureAlgorithms
|
||||
ALPNProtocols []string // Empty uses ["http/1.1"]
|
||||
SupportedVersions []uint16 // Empty uses [TLS1.3, TLS1.2]
|
||||
KeyShareGroups []uint16 // Empty uses [X25519]
|
||||
PSKModes []uint16 // Empty uses [psk_dhe_ke]
|
||||
Extensions []uint16 // Extension type IDs in order; empty uses default Node.js 24.x order
|
||||
}
|
||||
|
||||
// Dialer creates TLS connections with custom fingerprints.
|
||||
@ -45,154 +52,67 @@ type SOCKS5ProxyDialer struct {
|
||||
proxyURL *url.URL
|
||||
}
|
||||
|
||||
// Default TLS fingerprint values captured from Claude CLI 2.x (Node.js 20.x + OpenSSL 3.x)
|
||||
// Captured using: tshark -i lo -f "tcp port 8443" -Y "tls.handshake.type == 1" -V
|
||||
// JA3 Hash: 1a28e69016765d92e3b381168d68922c
|
||||
//
|
||||
// Note: JA3/JA4 may have slight variations due to:
|
||||
// - Session ticket presence/absence
|
||||
// - Extension negotiation state
|
||||
// Default TLS fingerprint values captured from Claude Code (Node.js 24.x)
|
||||
// Captured via tls-fingerprint-web capture server
|
||||
// JA3 Hash: 44f88fca027f27bab4bb08d4af15f23e
|
||||
// JA4: t13d1714h1_5b57614c22b0_7baf387fc6ff
|
||||
var (
|
||||
// defaultCipherSuites contains all 59 cipher suites from Claude CLI
|
||||
// defaultCipherSuites contains the 17 cipher suites from Node.js 24.x
|
||||
// Order is critical for JA3 fingerprint matching
|
||||
defaultCipherSuites = []uint16{
|
||||
// TLS 1.3 cipher suites (MUST be first)
|
||||
// TLS 1.3 cipher suites
|
||||
0x1301, // TLS_AES_128_GCM_SHA256
|
||||
0x1302, // TLS_AES_256_GCM_SHA384
|
||||
0x1303, // TLS_CHACHA20_POLY1305_SHA256
|
||||
0x1301, // TLS_AES_128_GCM_SHA256
|
||||
|
||||
// ECDHE + AES-GCM
|
||||
0xc02f, // TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
||||
0xc02b, // TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
||||
0xc030, // TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
|
||||
0xc02f, // TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
||||
0xc02c, // TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
|
||||
0xc030, // TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
|
||||
|
||||
// DHE + AES-GCM
|
||||
0x009e, // TLS_DHE_RSA_WITH_AES_128_GCM_SHA256
|
||||
|
||||
// ECDHE/DHE + AES-CBC-SHA256/384
|
||||
0xc027, // TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
|
||||
0x0067, // TLS_DHE_RSA_WITH_AES_128_CBC_SHA256
|
||||
0xc028, // TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
|
||||
0x006b, // TLS_DHE_RSA_WITH_AES_256_CBC_SHA256
|
||||
|
||||
// DHE-DSS/RSA + AES-GCM
|
||||
0x00a3, // TLS_DHE_DSS_WITH_AES_256_GCM_SHA384
|
||||
0x009f, // TLS_DHE_RSA_WITH_AES_256_GCM_SHA384
|
||||
|
||||
// ChaCha20-Poly1305
|
||||
// ECDHE + ChaCha20-Poly1305
|
||||
0xcca9, // TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
|
||||
0xcca8, // TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
|
||||
0xccaa, // TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256
|
||||
|
||||
// AES-CCM (256-bit)
|
||||
0xc0af, // TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8
|
||||
0xc0ad, // TLS_ECDHE_ECDSA_WITH_AES_256_CCM
|
||||
0xc0a3, // TLS_DHE_RSA_WITH_AES_256_CCM_8
|
||||
0xc09f, // TLS_DHE_RSA_WITH_AES_256_CCM
|
||||
|
||||
// ARIA (256-bit)
|
||||
0xc05d, // TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384
|
||||
0xc061, // TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384
|
||||
0xc057, // TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384
|
||||
0xc053, // TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384
|
||||
|
||||
// DHE-DSS + AES-GCM (128-bit)
|
||||
0x00a2, // TLS_DHE_DSS_WITH_AES_128_GCM_SHA256
|
||||
|
||||
// AES-CCM (128-bit)
|
||||
0xc0ae, // TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8
|
||||
0xc0ac, // TLS_ECDHE_ECDSA_WITH_AES_128_CCM
|
||||
0xc0a2, // TLS_DHE_RSA_WITH_AES_128_CCM_8
|
||||
0xc09e, // TLS_DHE_RSA_WITH_AES_128_CCM
|
||||
|
||||
// ARIA (128-bit)
|
||||
0xc05c, // TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256
|
||||
0xc060, // TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256
|
||||
0xc056, // TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256
|
||||
0xc052, // TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256
|
||||
|
||||
// ECDHE/DHE + AES-CBC-SHA384/256 (more)
|
||||
0xc024, // TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
|
||||
0x006a, // TLS_DHE_DSS_WITH_AES_256_CBC_SHA256
|
||||
0xc023, // TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
|
||||
0x0040, // TLS_DHE_DSS_WITH_AES_128_CBC_SHA256
|
||||
|
||||
// ECDHE/DHE + AES-CBC-SHA (legacy)
|
||||
0xc00a, // TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
|
||||
0xc014, // TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
|
||||
0x0039, // TLS_DHE_RSA_WITH_AES_256_CBC_SHA
|
||||
0x0038, // TLS_DHE_DSS_WITH_AES_256_CBC_SHA
|
||||
// ECDHE + AES-CBC-SHA (legacy fallback)
|
||||
0xc009, // TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
|
||||
0xc013, // TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
|
||||
0x0033, // TLS_DHE_RSA_WITH_AES_128_CBC_SHA
|
||||
0x0032, // TLS_DHE_DSS_WITH_AES_128_CBC_SHA
|
||||
0xc00a, // TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
|
||||
0xc014, // TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
|
||||
|
||||
// RSA + AES-GCM/CCM/ARIA (non-PFS, 256-bit)
|
||||
0x009d, // TLS_RSA_WITH_AES_256_GCM_SHA384
|
||||
0xc0a1, // TLS_RSA_WITH_AES_256_CCM_8
|
||||
0xc09d, // TLS_RSA_WITH_AES_256_CCM
|
||||
0xc051, // TLS_RSA_WITH_ARIA_256_GCM_SHA384
|
||||
|
||||
// RSA + AES-GCM/CCM/ARIA (non-PFS, 128-bit)
|
||||
// RSA + AES-GCM (non-PFS)
|
||||
0x009c, // TLS_RSA_WITH_AES_128_GCM_SHA256
|
||||
0xc0a0, // TLS_RSA_WITH_AES_128_CCM_8
|
||||
0xc09c, // TLS_RSA_WITH_AES_128_CCM
|
||||
0xc050, // TLS_RSA_WITH_ARIA_128_GCM_SHA256
|
||||
0x009d, // TLS_RSA_WITH_AES_256_GCM_SHA384
|
||||
|
||||
// RSA + AES-CBC (non-PFS, legacy)
|
||||
0x003d, // TLS_RSA_WITH_AES_256_CBC_SHA256
|
||||
0x003c, // TLS_RSA_WITH_AES_128_CBC_SHA256
|
||||
0x0035, // TLS_RSA_WITH_AES_256_CBC_SHA
|
||||
// RSA + AES-CBC-SHA (non-PFS, legacy)
|
||||
0x002f, // TLS_RSA_WITH_AES_128_CBC_SHA
|
||||
|
||||
// Renegotiation indication
|
||||
0x00ff, // TLS_EMPTY_RENEGOTIATION_INFO_SCSV
|
||||
0x0035, // TLS_RSA_WITH_AES_256_CBC_SHA
|
||||
}
|
||||
|
||||
// defaultCurves contains the 10 supported groups from Claude CLI (including FFDHE)
|
||||
// defaultCurves contains the 3 supported groups from Node.js 24.x
|
||||
defaultCurves = []utls.CurveID{
|
||||
utls.X25519, // 0x001d
|
||||
utls.CurveP256, // 0x0017 (secp256r1)
|
||||
utls.CurveID(0x001e), // x448
|
||||
utls.CurveP521, // 0x0019 (secp521r1)
|
||||
utls.CurveP384, // 0x0018 (secp384r1)
|
||||
utls.CurveID(0x0100), // ffdhe2048
|
||||
utls.CurveID(0x0101), // ffdhe3072
|
||||
utls.CurveID(0x0102), // ffdhe4096
|
||||
utls.CurveID(0x0103), // ffdhe6144
|
||||
utls.CurveID(0x0104), // ffdhe8192
|
||||
utls.X25519, // 0x001d
|
||||
utls.CurveP256, // 0x0017 (secp256r1)
|
||||
utls.CurveP384, // 0x0018 (secp384r1)
|
||||
}
|
||||
|
||||
// defaultPointFormats contains all 3 point formats from Claude CLI
|
||||
defaultPointFormats = []uint8{
|
||||
// defaultPointFormats contains point formats from Node.js 24.x
|
||||
defaultPointFormats = []uint16{
|
||||
0, // uncompressed
|
||||
1, // ansiX962_compressed_prime
|
||||
2, // ansiX962_compressed_char2
|
||||
}
|
||||
|
||||
// defaultSignatureAlgorithms contains the 20 signature algorithms from Claude CLI
|
||||
// defaultSignatureAlgorithms contains the 9 signature algorithms from Node.js 24.x
|
||||
defaultSignatureAlgorithms = []utls.SignatureScheme{
|
||||
0x0403, // ecdsa_secp256r1_sha256
|
||||
0x0503, // ecdsa_secp384r1_sha384
|
||||
0x0603, // ecdsa_secp521r1_sha512
|
||||
0x0807, // ed25519
|
||||
0x0808, // ed448
|
||||
0x0809, // rsa_pss_pss_sha256
|
||||
0x080a, // rsa_pss_pss_sha384
|
||||
0x080b, // rsa_pss_pss_sha512
|
||||
0x0804, // rsa_pss_rsae_sha256
|
||||
0x0805, // rsa_pss_rsae_sha384
|
||||
0x0806, // rsa_pss_rsae_sha512
|
||||
0x0401, // rsa_pkcs1_sha256
|
||||
0x0503, // ecdsa_secp384r1_sha384
|
||||
0x0805, // rsa_pss_rsae_sha384
|
||||
0x0501, // rsa_pkcs1_sha384
|
||||
0x0806, // rsa_pss_rsae_sha512
|
||||
0x0601, // rsa_pkcs1_sha512
|
||||
0x0303, // ecdsa_sha224
|
||||
0x0301, // rsa_pkcs1_sha224
|
||||
0x0302, // dsa_sha224
|
||||
0x0402, // dsa_sha256
|
||||
0x0502, // dsa_sha384
|
||||
0x0602, // dsa_sha512
|
||||
0x0201, // rsa_pkcs1_sha1
|
||||
}
|
||||
)
|
||||
|
||||
@ -256,49 +176,7 @@ func (d *SOCKS5ProxyDialer) DialTLSContext(ctx context.Context, network, addr st
|
||||
slog.Debug("tls_fingerprint_socks5_tunnel_established")
|
||||
|
||||
// Step 3: Perform TLS handshake on the tunnel with utls fingerprint
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
host = addr
|
||||
}
|
||||
slog.Debug("tls_fingerprint_socks5_starting_handshake", "host", host)
|
||||
|
||||
// Build ClientHello specification from profile (Node.js/Claude CLI fingerprint)
|
||||
spec := buildClientHelloSpecFromProfile(d.profile)
|
||||
slog.Debug("tls_fingerprint_socks5_clienthello_spec",
|
||||
"cipher_suites", len(spec.CipherSuites),
|
||||
"extensions", len(spec.Extensions),
|
||||
"compression_methods", spec.CompressionMethods,
|
||||
"tls_vers_max", spec.TLSVersMax,
|
||||
"tls_vers_min", spec.TLSVersMin)
|
||||
|
||||
if d.profile != nil {
|
||||
slog.Debug("tls_fingerprint_socks5_using_profile", "name", d.profile.Name, "grease", d.profile.EnableGREASE)
|
||||
}
|
||||
|
||||
// Create uTLS connection on the tunnel
|
||||
tlsConn := utls.UClient(conn, &utls.Config{
|
||||
ServerName: host,
|
||||
}, utls.HelloCustom)
|
||||
|
||||
if err := tlsConn.ApplyPreset(spec); err != nil {
|
||||
slog.Debug("tls_fingerprint_socks5_apply_preset_failed", "error", err)
|
||||
_ = conn.Close()
|
||||
return nil, fmt.Errorf("apply TLS preset: %w", err)
|
||||
}
|
||||
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
slog.Debug("tls_fingerprint_socks5_handshake_failed", "error", err)
|
||||
_ = conn.Close()
|
||||
return nil, fmt.Errorf("TLS handshake failed: %w", err)
|
||||
}
|
||||
|
||||
state := tlsConn.ConnectionState()
|
||||
slog.Debug("tls_fingerprint_socks5_handshake_success",
|
||||
"version", state.Version,
|
||||
"cipher_suite", state.CipherSuite,
|
||||
"alpn", state.NegotiatedProtocol)
|
||||
|
||||
return tlsConn, nil
|
||||
return performTLSHandshake(ctx, conn, d.profile, addr)
|
||||
}
|
||||
|
||||
// DialTLSContext establishes a TLS connection through HTTP proxy with the configured fingerprint.
|
||||
@ -358,7 +236,8 @@ func (d *HTTPProxyDialer) DialTLSContext(ctx context.Context, network, addr stri
|
||||
slog.Debug("tls_fingerprint_http_proxy_read_response_failed", "error", err)
|
||||
return nil, fmt.Errorf("read CONNECT response: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
// CONNECT response has no body; do not defer resp.Body.Close() as it wraps the
|
||||
// same conn that will be used for the TLS handshake.
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
_ = conn.Close()
|
||||
@ -368,47 +247,7 @@ func (d *HTTPProxyDialer) DialTLSContext(ctx context.Context, network, addr stri
|
||||
slog.Debug("tls_fingerprint_http_proxy_tunnel_established")
|
||||
|
||||
// Step 4: Perform TLS handshake on the tunnel with utls fingerprint
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
host = addr
|
||||
}
|
||||
slog.Debug("tls_fingerprint_http_proxy_starting_handshake", "host", host)
|
||||
|
||||
// Build ClientHello specification (reuse the shared method)
|
||||
spec := buildClientHelloSpecFromProfile(d.profile)
|
||||
slog.Debug("tls_fingerprint_http_proxy_clienthello_spec",
|
||||
"cipher_suites", len(spec.CipherSuites),
|
||||
"extensions", len(spec.Extensions))
|
||||
|
||||
if d.profile != nil {
|
||||
slog.Debug("tls_fingerprint_http_proxy_using_profile", "name", d.profile.Name, "grease", d.profile.EnableGREASE)
|
||||
}
|
||||
|
||||
// Create uTLS connection on the tunnel
|
||||
// Note: TLS 1.3 cipher suites are handled automatically by utls when TLS 1.3 is in SupportedVersions
|
||||
tlsConn := utls.UClient(conn, &utls.Config{
|
||||
ServerName: host,
|
||||
}, utls.HelloCustom)
|
||||
|
||||
if err := tlsConn.ApplyPreset(spec); err != nil {
|
||||
slog.Debug("tls_fingerprint_http_proxy_apply_preset_failed", "error", err)
|
||||
_ = conn.Close()
|
||||
return nil, fmt.Errorf("apply TLS preset: %w", err)
|
||||
}
|
||||
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
slog.Debug("tls_fingerprint_http_proxy_handshake_failed", "error", err)
|
||||
_ = conn.Close()
|
||||
return nil, fmt.Errorf("TLS handshake failed: %w", err)
|
||||
}
|
||||
|
||||
state := tlsConn.ConnectionState()
|
||||
slog.Debug("tls_fingerprint_http_proxy_handshake_success",
|
||||
"version", state.Version,
|
||||
"cipher_suite", state.CipherSuite,
|
||||
"alpn", state.NegotiatedProtocol)
|
||||
|
||||
return tlsConn, nil
|
||||
return performTLSHandshake(ctx, conn, d.profile, addr)
|
||||
}
|
||||
|
||||
// DialTLSContext establishes a TLS connection with the configured fingerprint.
|
||||
@ -423,53 +262,35 @@ func (d *Dialer) DialTLSContext(ctx context.Context, network, addr string) (net.
|
||||
}
|
||||
slog.Debug("tls_fingerprint_tcp_connected", "addr", addr)
|
||||
|
||||
// Extract hostname for SNI
|
||||
// Perform TLS handshake with utls fingerprint
|
||||
return performTLSHandshake(ctx, conn, d.profile, addr)
|
||||
}
|
||||
|
||||
// performTLSHandshake performs the uTLS handshake on an established connection.
|
||||
// It builds a ClientHello spec from the profile, applies it, and completes the handshake.
|
||||
// On failure, conn is closed and an error is returned.
|
||||
func performTLSHandshake(ctx context.Context, conn net.Conn, profile *Profile, addr string) (net.Conn, error) {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
host = addr
|
||||
}
|
||||
slog.Debug("tls_fingerprint_sni_hostname", "host", host)
|
||||
|
||||
// Build ClientHello specification
|
||||
spec := d.buildClientHelloSpec()
|
||||
slog.Debug("tls_fingerprint_clienthello_spec",
|
||||
"cipher_suites", len(spec.CipherSuites),
|
||||
"extensions", len(spec.Extensions))
|
||||
spec := buildClientHelloSpecFromProfile(profile)
|
||||
tlsConn := utls.UClient(conn, &utls.Config{ServerName: host}, utls.HelloCustom)
|
||||
|
||||
// Log profile info
|
||||
if d.profile != nil {
|
||||
slog.Debug("tls_fingerprint_using_profile", "name", d.profile.Name, "grease", d.profile.EnableGREASE)
|
||||
} else {
|
||||
slog.Debug("tls_fingerprint_using_default_profile")
|
||||
}
|
||||
|
||||
// Create uTLS connection
|
||||
// Note: TLS 1.3 cipher suites are handled automatically by utls when TLS 1.3 is in SupportedVersions
|
||||
tlsConn := utls.UClient(conn, &utls.Config{
|
||||
ServerName: host,
|
||||
}, utls.HelloCustom)
|
||||
|
||||
// Apply fingerprint
|
||||
if err := tlsConn.ApplyPreset(spec); err != nil {
|
||||
slog.Debug("tls_fingerprint_apply_preset_failed", "error", err)
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("apply TLS preset: %w", err)
|
||||
}
|
||||
slog.Debug("tls_fingerprint_preset_applied")
|
||||
|
||||
// Perform TLS handshake
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
slog.Debug("tls_fingerprint_handshake_failed",
|
||||
"error", err,
|
||||
"local_addr", conn.LocalAddr(),
|
||||
"remote_addr", conn.RemoteAddr())
|
||||
_ = conn.Close()
|
||||
return nil, fmt.Errorf("TLS handshake failed: %w", err)
|
||||
}
|
||||
|
||||
// Log successful handshake details
|
||||
state := tlsConn.ConnectionState()
|
||||
slog.Debug("tls_fingerprint_handshake_success",
|
||||
"host", host,
|
||||
"version", state.Version,
|
||||
"cipher_suite", state.CipherSuite,
|
||||
"alpn", state.NegotiatedProtocol)
|
||||
@ -477,11 +298,6 @@ func (d *Dialer) DialTLSContext(ctx context.Context, network, addr string) (net.
|
||||
return tlsConn, nil
|
||||
}
|
||||
|
||||
// buildClientHelloSpec constructs the ClientHello specification based on the profile.
|
||||
func (d *Dialer) buildClientHelloSpec() *utls.ClientHelloSpec {
|
||||
return buildClientHelloSpecFromProfile(d.profile)
|
||||
}
|
||||
|
||||
// toUTLSCurves converts uint16 slice to utls.CurveID slice.
|
||||
func toUTLSCurves(curves []uint16) []utls.CurveID {
|
||||
result := make([]utls.CurveID, len(curves))
|
||||
@ -491,70 +307,143 @@ func toUTLSCurves(curves []uint16) []utls.CurveID {
|
||||
return result
|
||||
}
|
||||
|
||||
// defaultExtensionOrder is the Node.js 24.x extension order.
|
||||
// Used when Profile.Extensions is empty.
|
||||
var defaultExtensionOrder = []uint16{
|
||||
0, // server_name
|
||||
65037, // encrypted_client_hello
|
||||
23, // extended_master_secret
|
||||
65281, // renegotiation_info
|
||||
10, // supported_groups
|
||||
11, // ec_point_formats
|
||||
35, // session_ticket
|
||||
16, // alpn
|
||||
5, // status_request
|
||||
13, // signature_algorithms
|
||||
18, // signed_certificate_timestamp
|
||||
51, // key_share
|
||||
45, // psk_key_exchange_modes
|
||||
43, // supported_versions
|
||||
}
|
||||
|
||||
// isGREASEValue checks if a uint16 value matches the TLS GREASE pattern (0x?a?a).
|
||||
func isGREASEValue(v uint16) bool {
|
||||
return v&0x0f0f == 0x0a0a && v>>8 == v&0xff
|
||||
}
|
||||
|
||||
// buildClientHelloSpecFromProfile constructs ClientHelloSpec from a Profile.
|
||||
// This is a standalone function that can be used by both Dialer and HTTPProxyDialer.
|
||||
func buildClientHelloSpecFromProfile(profile *Profile) *utls.ClientHelloSpec {
|
||||
// Get cipher suites
|
||||
var cipherSuites []uint16
|
||||
// Resolve effective values (profile overrides or built-in defaults)
|
||||
cipherSuites := defaultCipherSuites
|
||||
if profile != nil && len(profile.CipherSuites) > 0 {
|
||||
cipherSuites = profile.CipherSuites
|
||||
} else {
|
||||
cipherSuites = defaultCipherSuites
|
||||
}
|
||||
|
||||
// Get curves
|
||||
var curves []utls.CurveID
|
||||
curves := defaultCurves
|
||||
if profile != nil && len(profile.Curves) > 0 {
|
||||
curves = toUTLSCurves(profile.Curves)
|
||||
} else {
|
||||
curves = defaultCurves
|
||||
}
|
||||
|
||||
// Get point formats
|
||||
var pointFormats []uint8
|
||||
pointFormats := defaultPointFormats
|
||||
if profile != nil && len(profile.PointFormats) > 0 {
|
||||
pointFormats = profile.PointFormats
|
||||
} else {
|
||||
pointFormats = defaultPointFormats
|
||||
}
|
||||
|
||||
// Check if GREASE is enabled
|
||||
signatureAlgorithms := defaultSignatureAlgorithms
|
||||
if profile != nil && len(profile.SignatureAlgorithms) > 0 {
|
||||
signatureAlgorithms = make([]utls.SignatureScheme, len(profile.SignatureAlgorithms))
|
||||
for i, s := range profile.SignatureAlgorithms {
|
||||
signatureAlgorithms[i] = utls.SignatureScheme(s)
|
||||
}
|
||||
}
|
||||
|
||||
alpnProtocols := []string{"http/1.1"}
|
||||
if profile != nil && len(profile.ALPNProtocols) > 0 {
|
||||
alpnProtocols = profile.ALPNProtocols
|
||||
}
|
||||
|
||||
supportedVersions := []uint16{utls.VersionTLS13, utls.VersionTLS12}
|
||||
if profile != nil && len(profile.SupportedVersions) > 0 {
|
||||
supportedVersions = profile.SupportedVersions
|
||||
}
|
||||
|
||||
keyShareGroups := []utls.CurveID{utls.X25519}
|
||||
if profile != nil && len(profile.KeyShareGroups) > 0 {
|
||||
keyShareGroups = toUTLSCurves(profile.KeyShareGroups)
|
||||
}
|
||||
|
||||
pskModes := []uint16{uint16(utls.PskModeDHE)}
|
||||
if profile != nil && len(profile.PSKModes) > 0 {
|
||||
pskModes = profile.PSKModes
|
||||
}
|
||||
|
||||
enableGREASE := profile != nil && profile.EnableGREASE
|
||||
|
||||
extensions := make([]utls.TLSExtension, 0, 16)
|
||||
|
||||
if enableGREASE {
|
||||
extensions = append(extensions, &utls.UtlsGREASEExtension{})
|
||||
// Build key shares
|
||||
keyShares := make([]utls.KeyShare, len(keyShareGroups))
|
||||
for i, g := range keyShareGroups {
|
||||
keyShares[i] = utls.KeyShare{Group: g}
|
||||
}
|
||||
|
||||
// SNI extension - MUST be explicitly added for HelloCustom mode
|
||||
// utls will populate the server name from Config.ServerName
|
||||
extensions = append(extensions, &utls.SNIExtension{})
|
||||
// Determine extension order
|
||||
extOrder := defaultExtensionOrder
|
||||
if profile != nil && len(profile.Extensions) > 0 {
|
||||
extOrder = profile.Extensions
|
||||
}
|
||||
|
||||
// Claude CLI extension order (captured from tshark):
|
||||
// server_name(0), ec_point_formats(11), supported_groups(10), session_ticket(35),
|
||||
// alpn(16), encrypt_then_mac(22), extended_master_secret(23),
|
||||
// signature_algorithms(13), supported_versions(43),
|
||||
// psk_key_exchange_modes(45), key_share(51)
|
||||
extensions = append(extensions,
|
||||
&utls.SupportedPointsExtension{SupportedPoints: pointFormats},
|
||||
&utls.SupportedCurvesExtension{Curves: curves},
|
||||
&utls.SessionTicketExtension{},
|
||||
&utls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}},
|
||||
&utls.GenericExtension{Id: 22},
|
||||
&utls.ExtendedMasterSecretExtension{},
|
||||
&utls.SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: defaultSignatureAlgorithms},
|
||||
&utls.SupportedVersionsExtension{Versions: []uint16{
|
||||
utls.VersionTLS13,
|
||||
utls.VersionTLS12,
|
||||
}},
|
||||
&utls.PSKKeyExchangeModesExtension{Modes: []uint8{utls.PskModeDHE}},
|
||||
&utls.KeyShareExtension{KeyShares: []utls.KeyShare{
|
||||
{Group: utls.X25519},
|
||||
}},
|
||||
)
|
||||
// Build extensions list from the ordered IDs.
|
||||
// Parametric extensions (curves, sigalgs, etc.) are populated with resolved profile values.
|
||||
// Unknown IDs use GenericExtension (sends type ID with empty data).
|
||||
extensions := make([]utls.TLSExtension, 0, len(extOrder)+2)
|
||||
for _, id := range extOrder {
|
||||
if isGREASEValue(id) {
|
||||
extensions = append(extensions, &utls.UtlsGREASEExtension{})
|
||||
continue
|
||||
}
|
||||
switch id {
|
||||
case 0: // server_name
|
||||
extensions = append(extensions, &utls.SNIExtension{})
|
||||
case 5: // status_request (OCSP)
|
||||
extensions = append(extensions, &utls.StatusRequestExtension{})
|
||||
case 10: // supported_groups
|
||||
extensions = append(extensions, &utls.SupportedCurvesExtension{Curves: curves})
|
||||
case 11: // ec_point_formats
|
||||
extensions = append(extensions, &utls.SupportedPointsExtension{SupportedPoints: toUint8s(pointFormats)})
|
||||
case 13: // signature_algorithms
|
||||
extensions = append(extensions, &utls.SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: signatureAlgorithms})
|
||||
case 16: // alpn
|
||||
extensions = append(extensions, &utls.ALPNExtension{AlpnProtocols: alpnProtocols})
|
||||
case 18: // signed_certificate_timestamp
|
||||
extensions = append(extensions, &utls.SCTExtension{})
|
||||
case 23: // extended_master_secret
|
||||
extensions = append(extensions, &utls.ExtendedMasterSecretExtension{})
|
||||
case 35: // session_ticket
|
||||
extensions = append(extensions, &utls.SessionTicketExtension{})
|
||||
case 43: // supported_versions
|
||||
extensions = append(extensions, &utls.SupportedVersionsExtension{Versions: supportedVersions})
|
||||
case 45: // psk_key_exchange_modes
|
||||
extensions = append(extensions, &utls.PSKKeyExchangeModesExtension{Modes: toUint8s(pskModes)})
|
||||
case 50: // signature_algorithms_cert
|
||||
extensions = append(extensions, &utls.SignatureAlgorithmsCertExtension{SupportedSignatureAlgorithms: signatureAlgorithms})
|
||||
case 51: // key_share
|
||||
extensions = append(extensions, &utls.KeyShareExtension{KeyShares: keyShares})
|
||||
case 0xfe0d: // encrypted_client_hello (ECH, 65037)
|
||||
// Send GREASE ECH with random payload — mimics Node.js behavior when no real ECHConfig is available.
|
||||
// An empty GenericExtension causes "error decoding message" from servers that validate ECH format.
|
||||
extensions = append(extensions, &utls.GREASEEncryptedClientHelloExtension{})
|
||||
case 0xff01: // renegotiation_info
|
||||
extensions = append(extensions, &utls.RenegotiationInfoExtension{})
|
||||
default:
|
||||
// Unknown extension — send as GenericExtension (type ID + empty data).
|
||||
// This covers encrypt_then_mac(22) and any future extensions.
|
||||
extensions = append(extensions, &utls.GenericExtension{Id: id})
|
||||
}
|
||||
}
|
||||
|
||||
if enableGREASE {
|
||||
// For default extension order with EnableGREASE, wrap with GREASE bookends
|
||||
if enableGREASE && (profile == nil || len(profile.Extensions) == 0) {
|
||||
extensions = append([]utls.TLSExtension{&utls.UtlsGREASEExtension{}}, extensions...)
|
||||
extensions = append(extensions, &utls.UtlsGREASEExtension{})
|
||||
}
|
||||
|
||||
@ -566,3 +455,12 @@ func buildClientHelloSpecFromProfile(profile *Profile) *utls.ClientHelloSpec {
|
||||
TLSVersMin: utls.VersionTLS10,
|
||||
}
|
||||
}
|
||||
|
||||
// toUint8s converts []uint16 to []uint8 (for utls fields that require []uint8).
|
||||
func toUint8s(vals []uint16) []uint8 {
|
||||
out := make([]uint8, len(vals))
|
||||
for i, v := range vals {
|
||||
out[i] = uint8(v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
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.
|
||||
// This test uses tls.peet.ws to verify the fingerprint.
|
||||
// Expected JA3 hash: 1a28e69016765d92e3b381168d68922c (Claude CLI / Node.js 20.x)
|
||||
// Expected JA4: t13d5911h1_a33745022dd6_1f22a2ca17c4 (d=domain) or t13i5911h1_... (i=IP)
|
||||
// Expected JA3 hash: 44f88fca027f27bab4bb08d4af15f23e (Node.js 24.x)
|
||||
// Expected JA4: t13d1714h1_5b57614c22b0_7baf387fc6ff
|
||||
func TestJA3Fingerprint(t *testing.T) {
|
||||
// Skip if network is unavailable or if running in short mode
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
profile := &Profile{
|
||||
Name: "Claude CLI Test",
|
||||
Name: "Default Profile Test",
|
||||
EnableGREASE: false,
|
||||
}
|
||||
dialer := NewDialer(profile, nil)
|
||||
@ -61,7 +60,6 @@ func TestJA3Fingerprint(t *testing.T) {
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
// Use tls.peet.ws fingerprint detection API
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@ -69,7 +67,7 @@ func TestJA3Fingerprint(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Claude Code/2.0.0 Node.js/20.0.0")
|
||||
req.Header.Set("User-Agent", "Claude Code/2.0.0 Node.js/24.3.0")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
skipIfExternalServiceUnavailable(t, err)
|
||||
@ -86,71 +84,23 @@ func TestJA3Fingerprint(t *testing.T) {
|
||||
t.Fatalf("failed to parse fingerprint response: %v", err)
|
||||
}
|
||||
|
||||
// Log all fingerprint information
|
||||
t.Logf("JA3: %s", fpResp.TLS.JA3)
|
||||
t.Logf("JA3 Hash: %s", fpResp.TLS.JA3Hash)
|
||||
t.Logf("JA4: %s", fpResp.TLS.JA4)
|
||||
t.Logf("PeetPrint: %s", fpResp.TLS.PeetPrint)
|
||||
t.Logf("PeetPrint Hash: %s", fpResp.TLS.PeetPrintHash)
|
||||
|
||||
// Verify JA3 hash matches expected value
|
||||
expectedJA3Hash := "1a28e69016765d92e3b381168d68922c"
|
||||
expectedJA3Hash := "44f88fca027f27bab4bb08d4af15f23e"
|
||||
if fpResp.TLS.JA3Hash == expectedJA3Hash {
|
||||
t.Logf("✓ JA3 hash matches expected value: %s", expectedJA3Hash)
|
||||
t.Logf("✓ JA3 hash matches: %s", expectedJA3Hash)
|
||||
} else {
|
||||
t.Errorf("✗ JA3 hash mismatch: got %s, expected %s", fpResp.TLS.JA3Hash, expectedJA3Hash)
|
||||
}
|
||||
|
||||
// Verify JA4 fingerprint
|
||||
// JA4 format: t[version][sni][cipher_count][ext_count][alpn]_[cipher_hash]_[ext_hash]
|
||||
// Expected: t13d5910h1 (d=domain) or t13i5910h1 (i=IP)
|
||||
// The suffix _a33745022dd6_1f22a2ca17c4 should match
|
||||
expectedJA4Suffix := "_a33745022dd6_1f22a2ca17c4"
|
||||
if strings.HasSuffix(fpResp.TLS.JA4, expectedJA4Suffix) {
|
||||
t.Logf("✓ JA4 suffix matches expected value: %s", expectedJA4Suffix)
|
||||
expectedJA4CipherHash := "_5b57614c22b0_"
|
||||
if strings.Contains(fpResp.TLS.JA4, expectedJA4CipherHash) {
|
||||
t.Logf("✓ JA4 cipher hash matches: %s", expectedJA4CipherHash)
|
||||
} else {
|
||||
t.Errorf("✗ JA4 suffix mismatch: got %s, expected suffix %s", fpResp.TLS.JA4, expectedJA4Suffix)
|
||||
t.Errorf("✗ JA4 cipher hash mismatch: got %s, expected containing %s", fpResp.TLS.JA4, expectedJA4CipherHash)
|
||||
}
|
||||
|
||||
// Verify JA4 prefix (t13d5911h1 or t13i5911h1)
|
||||
// d = domain (SNI present), i = IP (no SNI)
|
||||
// Since we connect to tls.peet.ws (domain), we expect 'd'
|
||||
expectedJA4Prefix := "t13d5911h1"
|
||||
if strings.HasPrefix(fpResp.TLS.JA4, expectedJA4Prefix) {
|
||||
t.Logf("✓ JA4 prefix matches: %s (t13=TLS1.3, d=domain, 59=ciphers, 11=extensions, h1=HTTP/1.1)", expectedJA4Prefix)
|
||||
} else {
|
||||
// Also accept 'i' variant for IP connections
|
||||
altPrefix := "t13i5911h1"
|
||||
if strings.HasPrefix(fpResp.TLS.JA4, altPrefix) {
|
||||
t.Logf("✓ JA4 prefix matches (IP variant): %s", altPrefix)
|
||||
} else {
|
||||
t.Errorf("✗ JA4 prefix mismatch: got %s, expected %s or %s", fpResp.TLS.JA4, expectedJA4Prefix, altPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify JA3 contains expected cipher suites (TLS 1.3 ciphers at the beginning)
|
||||
if strings.Contains(fpResp.TLS.JA3, "4866-4867-4865") {
|
||||
t.Logf("✓ JA3 contains expected TLS 1.3 cipher suites")
|
||||
} else {
|
||||
t.Logf("Warning: JA3 does not contain expected TLS 1.3 cipher suites")
|
||||
}
|
||||
|
||||
// Verify extension list (should be 11 extensions including SNI)
|
||||
// Expected: 0-11-10-35-16-22-23-13-43-45-51
|
||||
expectedExtensions := "0-11-10-35-16-22-23-13-43-45-51"
|
||||
if strings.Contains(fpResp.TLS.JA3, expectedExtensions) {
|
||||
t.Logf("✓ JA3 contains expected extension list: %s", expectedExtensions)
|
||||
} else {
|
||||
t.Logf("Warning: JA3 extension list may differ")
|
||||
}
|
||||
}
|
||||
|
||||
// TestProfileExpectation defines expected fingerprint values for a profile.
|
||||
type TestProfileExpectation struct {
|
||||
Profile *Profile
|
||||
ExpectedJA3 string // Expected JA3 hash (empty = don't check)
|
||||
ExpectedJA4 string // Expected full JA4 (empty = don't check)
|
||||
JA4CipherHash string // Expected JA4 cipher hash - the stable middle part (empty = don't check)
|
||||
}
|
||||
|
||||
// TestAllProfiles tests multiple TLS fingerprint profiles against tls.peet.ws.
|
||||
@ -164,30 +114,24 @@ func TestAllProfiles(t *testing.T) {
|
||||
// These profiles are from config.yaml gateway.tls_fingerprint.profiles
|
||||
profiles := []TestProfileExpectation{
|
||||
{
|
||||
// Linux x64 Node.js v22.17.1
|
||||
// Expected JA3 Hash: 1a28e69016765d92e3b381168d68922c
|
||||
// Expected JA4: t13d5911h1_a33745022dd6_1f22a2ca17c4
|
||||
// Default profile (Node.js 24.x)
|
||||
Profile: &Profile{
|
||||
Name: "default_node_v24",
|
||||
EnableGREASE: false,
|
||||
},
|
||||
JA4CipherHash: "5b57614c22b0",
|
||||
},
|
||||
{
|
||||
// Linux x64 Node.js v22.17.1 (explicit profile with v22 extensions)
|
||||
Profile: &Profile{
|
||||
Name: "linux_x64_node_v22171",
|
||||
EnableGREASE: false,
|
||||
CipherSuites: []uint16{4866, 4867, 4865, 49199, 49195, 49200, 49196, 158, 49191, 103, 49192, 107, 163, 159, 52393, 52392, 52394, 49327, 49325, 49315, 49311, 49245, 49249, 49239, 49235, 162, 49326, 49324, 49314, 49310, 49244, 49248, 49238, 49234, 49188, 106, 49187, 64, 49162, 49172, 57, 56, 49161, 49171, 51, 50, 157, 49313, 49309, 49233, 156, 49312, 49308, 49232, 61, 60, 53, 47, 255},
|
||||
Curves: []uint16{29, 23, 30, 25, 24, 256, 257, 258, 259, 260},
|
||||
PointFormats: []uint8{0, 1, 2},
|
||||
PointFormats: []uint16{0, 1, 2},
|
||||
Extensions: []uint16{0, 11, 10, 35, 16, 22, 23, 13, 43, 45, 51},
|
||||
},
|
||||
JA4CipherHash: "a33745022dd6", // stable part
|
||||
},
|
||||
{
|
||||
// MacOS arm64 Node.js v22.18.0
|
||||
// Expected JA3 Hash: 70cb5ca646080902703ffda87036a5ea
|
||||
// Expected JA4: t13d5912h1_a33745022dd6_dbd39dd1d406
|
||||
Profile: &Profile{
|
||||
Name: "macos_arm64_node_v22180",
|
||||
EnableGREASE: false,
|
||||
CipherSuites: []uint16{4866, 4867, 4865, 49199, 49195, 49200, 49196, 158, 49191, 103, 49192, 107, 163, 159, 52393, 52392, 52394, 49327, 49325, 49315, 49311, 49245, 49249, 49239, 49235, 162, 49326, 49324, 49314, 49310, 49244, 49248, 49238, 49234, 49188, 106, 49187, 64, 49162, 49172, 57, 56, 49161, 49171, 51, 50, 157, 49313, 49309, 49233, 156, 49312, 49308, 49232, 61, 60, 53, 47, 255},
|
||||
Curves: []uint16{29, 23, 30, 25, 24, 256, 257, 258, 259, 260},
|
||||
PointFormats: []uint8{0, 1, 2},
|
||||
},
|
||||
JA4CipherHash: "a33745022dd6", // stable part (same cipher suites)
|
||||
JA4CipherHash: "a33745022dd6",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -55,13 +55,13 @@ func TestDialerBasicConnection(t *testing.T) {
|
||||
|
||||
// TestJA3Fingerprint verifies the JA3/JA4 fingerprint matches expected value.
|
||||
// This test uses tls.peet.ws to verify the fingerprint.
|
||||
// Expected JA3 hash: 1a28e69016765d92e3b381168d68922c (Claude CLI / Node.js 20.x)
|
||||
// Expected JA4: t13d5911h1_a33745022dd6_1f22a2ca17c4 (d=domain) or t13i5911h1_... (i=IP)
|
||||
// Expected JA3 hash: 44f88fca027f27bab4bb08d4af15f23e (Node.js 24.x)
|
||||
// Expected JA4: t13d1714h1_5b57614c22b0_7baf387fc6ff
|
||||
func TestJA3Fingerprint(t *testing.T) {
|
||||
skipNetworkTest(t)
|
||||
|
||||
profile := &Profile{
|
||||
Name: "Claude CLI Test",
|
||||
Name: "Default Profile Test",
|
||||
EnableGREASE: false,
|
||||
}
|
||||
dialer := NewDialer(profile, nil)
|
||||
@ -81,7 +81,7 @@ func TestJA3Fingerprint(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Claude Code/2.0.0 Node.js/20.0.0")
|
||||
req.Header.Set("User-Agent", "Claude Code/2.0.0 Node.js/24.3.0")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
@ -107,34 +107,28 @@ func TestJA3Fingerprint(t *testing.T) {
|
||||
t.Logf("PeetPrint: %s", fpResp.TLS.PeetPrint)
|
||||
t.Logf("PeetPrint Hash: %s", fpResp.TLS.PeetPrintHash)
|
||||
|
||||
// Verify JA3 hash matches expected value
|
||||
expectedJA3Hash := "1a28e69016765d92e3b381168d68922c"
|
||||
// Verify JA3 hash matches expected value (Node.js 24.x default)
|
||||
expectedJA3Hash := "44f88fca027f27bab4bb08d4af15f23e"
|
||||
if fpResp.TLS.JA3Hash == expectedJA3Hash {
|
||||
t.Logf("✓ JA3 hash matches expected value: %s", expectedJA3Hash)
|
||||
} else {
|
||||
t.Errorf("✗ JA3 hash mismatch: got %s, expected %s", fpResp.TLS.JA3Hash, expectedJA3Hash)
|
||||
}
|
||||
|
||||
// Verify JA4 fingerprint
|
||||
// JA4 format: t[version][sni][cipher_count][ext_count][alpn]_[cipher_hash]_[ext_hash]
|
||||
// Expected: t13d5910h1 (d=domain) or t13i5910h1 (i=IP)
|
||||
// The suffix _a33745022dd6_1f22a2ca17c4 should match
|
||||
expectedJA4Suffix := "_a33745022dd6_1f22a2ca17c4"
|
||||
if strings.HasSuffix(fpResp.TLS.JA4, expectedJA4Suffix) {
|
||||
t.Logf("✓ JA4 suffix matches expected value: %s", expectedJA4Suffix)
|
||||
// Verify JA4 cipher hash (stable middle part)
|
||||
expectedJA4CipherHash := "_5b57614c22b0_"
|
||||
if strings.Contains(fpResp.TLS.JA4, expectedJA4CipherHash) {
|
||||
t.Logf("✓ JA4 cipher hash matches: %s", expectedJA4CipherHash)
|
||||
} else {
|
||||
t.Errorf("✗ JA4 suffix mismatch: got %s, expected suffix %s", fpResp.TLS.JA4, expectedJA4Suffix)
|
||||
t.Errorf("✗ JA4 cipher hash mismatch: got %s, expected containing %s", fpResp.TLS.JA4, expectedJA4CipherHash)
|
||||
}
|
||||
|
||||
// Verify JA4 prefix (t13d5911h1 or t13i5911h1)
|
||||
// d = domain (SNI present), i = IP (no SNI)
|
||||
// Since we connect to tls.peet.ws (domain), we expect 'd'
|
||||
expectedJA4Prefix := "t13d5911h1"
|
||||
// Verify JA4 prefix (t13d1714h1 or t13i1714h1)
|
||||
expectedJA4Prefix := "t13d1714h1"
|
||||
if strings.HasPrefix(fpResp.TLS.JA4, expectedJA4Prefix) {
|
||||
t.Logf("✓ JA4 prefix matches: %s (t13=TLS1.3, d=domain, 59=ciphers, 11=extensions, h1=HTTP/1.1)", expectedJA4Prefix)
|
||||
t.Logf("✓ JA4 prefix matches: %s (t13=TLS1.3, d=domain, 17=ciphers, 14=extensions, h1=HTTP/1.1)", expectedJA4Prefix)
|
||||
} else {
|
||||
// Also accept 'i' variant for IP connections
|
||||
altPrefix := "t13i5911h1"
|
||||
altPrefix := "t13i1714h1"
|
||||
if strings.HasPrefix(fpResp.TLS.JA4, altPrefix) {
|
||||
t.Logf("✓ JA4 prefix matches (IP variant): %s", altPrefix)
|
||||
} else {
|
||||
@ -142,16 +136,15 @@ func TestJA3Fingerprint(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Verify JA3 contains expected cipher suites (TLS 1.3 ciphers at the beginning)
|
||||
if strings.Contains(fpResp.TLS.JA3, "4866-4867-4865") {
|
||||
// Verify JA3 contains expected TLS 1.3 cipher suites
|
||||
if strings.Contains(fpResp.TLS.JA3, "4865-4866-4867") {
|
||||
t.Logf("✓ JA3 contains expected TLS 1.3 cipher suites")
|
||||
} else {
|
||||
t.Logf("Warning: JA3 does not contain expected TLS 1.3 cipher suites")
|
||||
}
|
||||
|
||||
// Verify extension list (should be 11 extensions including SNI)
|
||||
// Expected: 0-11-10-35-16-22-23-13-43-45-51
|
||||
expectedExtensions := "0-11-10-35-16-22-23-13-43-45-51"
|
||||
// Verify extension list (14 extensions, Node.js 24.x order)
|
||||
expectedExtensions := "0-65037-23-65281-10-11-35-16-5-13-18-51-45-43"
|
||||
if strings.Contains(fpResp.TLS.JA3, expectedExtensions) {
|
||||
t.Logf("✓ JA3 contains expected extension list: %s", expectedExtensions)
|
||||
} else {
|
||||
@ -186,8 +179,8 @@ func TestDialerWithProfile(t *testing.T) {
|
||||
// Build specs and compare
|
||||
// Note: We can't directly compare JA3 without making network requests
|
||||
// but we can verify the specs are different
|
||||
spec1 := dialer1.buildClientHelloSpec()
|
||||
spec2 := dialer2.buildClientHelloSpec()
|
||||
spec1 := buildClientHelloSpecFromProfile(dialer1.profile)
|
||||
spec2 := buildClientHelloSpecFromProfile(dialer2.profile)
|
||||
|
||||
// Profile with GREASE should have more extensions
|
||||
if len(spec2.Extensions) <= len(spec1.Extensions) {
|
||||
@ -296,47 +289,33 @@ func mustParseURL(rawURL string) *url.URL {
|
||||
return u
|
||||
}
|
||||
|
||||
// TestProfileExpectation defines expected fingerprint values for a profile.
|
||||
type TestProfileExpectation struct {
|
||||
Profile *Profile
|
||||
ExpectedJA3 string // Expected JA3 hash (empty = don't check)
|
||||
ExpectedJA4 string // Expected full JA4 (empty = don't check)
|
||||
JA4CipherHash string // Expected JA4 cipher hash - the stable middle part (empty = don't check)
|
||||
}
|
||||
|
||||
// TestAllProfiles tests multiple TLS fingerprint profiles against tls.peet.ws.
|
||||
// Run with: go test -v -run TestAllProfiles ./internal/pkg/tlsfingerprint/...
|
||||
func TestAllProfiles(t *testing.T) {
|
||||
skipNetworkTest(t)
|
||||
|
||||
// Define all profiles to test with their expected fingerprints
|
||||
// These profiles are from config.yaml gateway.tls_fingerprint.profiles
|
||||
profiles := []TestProfileExpectation{
|
||||
{
|
||||
// Linux x64 Node.js v22.17.1
|
||||
// Expected JA3 Hash: 1a28e69016765d92e3b381168d68922c
|
||||
// Expected JA4: t13d5911h1_a33745022dd6_1f22a2ca17c4
|
||||
// Default profile (Node.js 24.x)
|
||||
// JA3 Hash: 44f88fca027f27bab4bb08d4af15f23e
|
||||
// JA4: t13d1714h1_5b57614c22b0_7baf387fc6ff
|
||||
Profile: &Profile{
|
||||
Name: "default_node_v24",
|
||||
EnableGREASE: false,
|
||||
},
|
||||
JA4CipherHash: "5b57614c22b0",
|
||||
},
|
||||
{
|
||||
// Linux x64 Node.js v22.17.1 (explicit profile)
|
||||
Profile: &Profile{
|
||||
Name: "linux_x64_node_v22171",
|
||||
EnableGREASE: false,
|
||||
CipherSuites: []uint16{4866, 4867, 4865, 49199, 49195, 49200, 49196, 158, 49191, 103, 49192, 107, 163, 159, 52393, 52392, 52394, 49327, 49325, 49315, 49311, 49245, 49249, 49239, 49235, 162, 49326, 49324, 49314, 49310, 49244, 49248, 49238, 49234, 49188, 106, 49187, 64, 49162, 49172, 57, 56, 49161, 49171, 51, 50, 157, 49313, 49309, 49233, 156, 49312, 49308, 49232, 61, 60, 53, 47, 255},
|
||||
Curves: []uint16{29, 23, 30, 25, 24, 256, 257, 258, 259, 260},
|
||||
PointFormats: []uint8{0, 1, 2},
|
||||
PointFormats: []uint16{0, 1, 2},
|
||||
Extensions: []uint16{0, 11, 10, 35, 16, 22, 23, 13, 43, 45, 51},
|
||||
},
|
||||
JA4CipherHash: "a33745022dd6", // stable part
|
||||
},
|
||||
{
|
||||
// MacOS arm64 Node.js v22.18.0
|
||||
// Expected JA3 Hash: 70cb5ca646080902703ffda87036a5ea
|
||||
// Expected JA4: t13d5912h1_a33745022dd6_dbd39dd1d406
|
||||
Profile: &Profile{
|
||||
Name: "macos_arm64_node_v22180",
|
||||
EnableGREASE: false,
|
||||
CipherSuites: []uint16{4866, 4867, 4865, 49199, 49195, 49200, 49196, 158, 49191, 103, 49192, 107, 163, 159, 52393, 52392, 52394, 49327, 49325, 49315, 49311, 49245, 49249, 49239, 49235, 162, 49326, 49324, 49314, 49310, 49244, 49248, 49238, 49234, 49188, 106, 49187, 64, 49162, 49172, 57, 56, 49161, 49171, 51, 50, 157, 49313, 49309, 49233, 156, 49312, 49308, 49232, 61, 60, 53, 47, 255},
|
||||
Curves: []uint16{29, 23, 30, 25, 24, 256, 257, 258, 259, 260},
|
||||
PointFormats: []uint8{0, 1, 2},
|
||||
},
|
||||
JA4CipherHash: "a33745022dd6", // stable part (same cipher suites)
|
||||
JA4CipherHash: "a33745022dd6",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -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"`
|
||||
}
|
||||
|
||||
// TestProfileExpectation defines expected fingerprint values for a profile.
|
||||
type TestProfileExpectation struct {
|
||||
Profile *Profile
|
||||
ExpectedJA3 string // Expected JA3 hash (empty = don't check)
|
||||
ExpectedJA4 string // Expected full JA4 (empty = don't check)
|
||||
JA4CipherHash string // Expected JA4 cipher hash - the stable middle part (empty = don't check)
|
||||
}
|
||||
|
||||
// TLSInfo contains TLS fingerprint details.
|
||||
type TLSInfo struct {
|
||||
JA3 string `json:"ja3"`
|
||||
|
||||
@ -68,10 +68,9 @@ func (s *claudeUsageService) FetchUsageWithOptions(ctx context.Context, opts *se
|
||||
|
||||
var resp *http.Response
|
||||
|
||||
// 如果启用 TLS 指纹且有 HTTPUpstream,使用 DoWithTLS
|
||||
if opts.EnableTLSFingerprint && s.httpUpstream != nil {
|
||||
// accountConcurrency 传 0 使用默认连接池配置,usage 请求不需要特殊的并发设置
|
||||
resp, err = s.httpUpstream.DoWithTLS(req, opts.ProxyURL, opts.AccountID, 0, true)
|
||||
// 如果有 TLS Profile 且有 HTTPUpstream,使用 DoWithTLS
|
||||
if opts.TLSProfile != nil && s.httpUpstream != nil {
|
||||
resp, err = s.httpUpstream.DoWithTLS(req, opts.ProxyURL, opts.AccountID, 0, opts.TLSProfile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request with TLS fingerprint failed: %w", err)
|
||||
}
|
||||
|
||||
@ -164,88 +164,59 @@ func (s *httpUpstreamService) Do(req *http.Request, proxyURL string, accountID i
|
||||
}
|
||||
|
||||
// DoWithTLS 执行带 TLS 指纹伪装的 HTTP 请求
|
||||
// 根据 enableTLSFingerprint 参数决定是否使用 TLS 指纹
|
||||
//
|
||||
// 参数:
|
||||
// - req: HTTP 请求对象
|
||||
// - proxyURL: 代理地址,空字符串表示直连
|
||||
// - accountID: 账户 ID,用于账户级隔离和 TLS 指纹模板选择
|
||||
// - accountConcurrency: 账户并发限制,用于动态调整连接池大小
|
||||
// - enableTLSFingerprint: 是否启用 TLS 指纹伪装
|
||||
//
|
||||
// TLS 指纹说明:
|
||||
// - 优先使用 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" {
|
||||
// mode 决定指纹策略:
|
||||
// - TLSModeOff / "": 不启用,行为与 Do 相同
|
||||
// - TLSModeNode: 走本地 Node.js TLS 代理(需 gateway.node_tls_proxy.enabled=true)
|
||||
// - TLSModeUTLS: 用 profile 模拟 TLS ClientHello(profile 为 nil 时降级为 Off)
|
||||
func (s *httpUpstreamService) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, mode service.TLSMode, profile *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
switch mode {
|
||||
case service.TLSModeNode:
|
||||
if s.isNodeTLSProxyEnabled() && req != nil && req.URL != nil && s.shouldRouteViaNodeProxy(req) {
|
||||
return s.doViaNodeTLSProxy(req, proxyURL, accountID, accountConcurrency)
|
||||
}
|
||||
}
|
||||
return s.Do(req, proxyURL, accountID, accountConcurrency)
|
||||
|
||||
// TLS 指纹已启用,记录调试日志
|
||||
targetHost := ""
|
||||
if req != nil && req.URL != nil {
|
||||
targetHost = req.URL.Host
|
||||
}
|
||||
proxyInfo := "direct"
|
||||
if proxyURL != "" {
|
||||
proxyInfo = proxyURL
|
||||
}
|
||||
slog.Debug("tls_fingerprint_enabled", "account_id", accountID, "target", targetHost, "proxy", proxyInfo)
|
||||
case service.TLSModeUTLS:
|
||||
if profile == nil {
|
||||
return s.Do(req, proxyURL, accountID, accountConcurrency)
|
||||
}
|
||||
targetHost := ""
|
||||
if req != nil && req.URL != nil {
|
||||
targetHost = req.URL.Host
|
||||
}
|
||||
proxyInfo := "direct"
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.validateRequestHost(req); err != nil {
|
||||
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
|
||||
registry := tlsfingerprint.GlobalRegistry()
|
||||
profile := registry.GetProfileByAccountID(accountID)
|
||||
if profile == nil {
|
||||
// 如果获取不到 profile,回退到普通请求
|
||||
slog.Debug("tls_fingerprint_no_profile", "account_id", accountID, "fallback", "standard_request")
|
||||
default: // TLSModeOff 或空字符串
|
||||
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,
|
||||
NewUserGroupRateRepository,
|
||||
NewErrorPassthroughRepository,
|
||||
NewTLSFingerprintProfileRepository,
|
||||
|
||||
// Cache implementations
|
||||
NewGatewayCache,
|
||||
@ -96,6 +97,7 @@ var ProviderSet = wire.NewSet(
|
||||
NewTotpCache,
|
||||
NewRefreshTokenCache,
|
||||
NewErrorPassthroughCache,
|
||||
NewTLSFingerprintProfileCache,
|
||||
|
||||
// Encryptors
|
||||
NewAESEncryptor,
|
||||
|
||||
@ -79,6 +79,9 @@ func RegisterAdminRoutes(
|
||||
// 错误透传规则管理
|
||||
registerErrorPassthroughRoutes(admin, h)
|
||||
|
||||
// TLS 指纹模板管理
|
||||
registerTLSFingerprintProfileRoutes(admin, h)
|
||||
|
||||
// API Key 管理
|
||||
registerAdminAPIKeyRoutes(admin, h)
|
||||
|
||||
@ -552,3 +555,14 @@ func registerErrorPassthroughRoutes(admin *gin.RouterGroup, h *handler.Handlers)
|
||||
rules.DELETE("/:id", h.Admin.ErrorPassthrough.Delete)
|
||||
}
|
||||
}
|
||||
|
||||
func registerTLSFingerprintProfileRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
||||
profiles := admin.Group("/tls-fingerprint-profiles")
|
||||
{
|
||||
profiles.GET("", h.Admin.TLSFingerprintProfile.List)
|
||||
profiles.GET("/:id", h.Admin.TLSFingerprintProfile.GetByID)
|
||||
profiles.POST("", h.Admin.TLSFingerprintProfile.Create)
|
||||
profiles.PUT("/:id", h.Admin.TLSFingerprintProfile.Update)
|
||||
profiles.DELETE("/:id", h.Admin.TLSFingerprintProfile.Delete)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1166,6 +1166,48 @@ func (a *Account) IsTLSFingerprintEnabled() bool {
|
||||
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 获取用户消息队列模式
|
||||
// "serialize" = 串行队列, "throttle" = 软性限速, "" = 未设置(使用全局配置)
|
||||
|
||||
@ -23,6 +23,7 @@ import (
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/geminicli"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/Wei-Shaw/sub2api/internal/util/soraerror"
|
||||
"github.com/Wei-Shaw/sub2api/internal/util/urlvalidator"
|
||||
"github.com/gin-gonic/gin"
|
||||
@ -69,6 +70,7 @@ type AccountTestService struct {
|
||||
antigravityGatewayService *AntigravityGatewayService
|
||||
httpUpstream HTTPUpstream
|
||||
cfg *config.Config
|
||||
tlsFPProfileService *TLSFingerprintProfileService
|
||||
soraTestGuardMu sync.Mutex
|
||||
soraTestLastRun map[int64]time.Time
|
||||
soraTestCooldown time.Duration
|
||||
@ -83,6 +85,7 @@ func NewAccountTestService(
|
||||
antigravityGatewayService *AntigravityGatewayService,
|
||||
httpUpstream HTTPUpstream,
|
||||
cfg *config.Config,
|
||||
tlsFPProfileService *TLSFingerprintProfileService,
|
||||
) *AccountTestService {
|
||||
return &AccountTestService{
|
||||
accountRepo: accountRepo,
|
||||
@ -90,6 +93,7 @@ func NewAccountTestService(
|
||||
antigravityGatewayService: antigravityGatewayService,
|
||||
httpUpstream: httpUpstream,
|
||||
cfg: cfg,
|
||||
tlsFPProfileService: tlsFPProfileService,
|
||||
soraTestLastRun: make(map[int64]time.Time),
|
||||
soraTestCooldown: defaultSoraTestCooldown,
|
||||
}
|
||||
@ -300,7 +304,7 @@ func (s *AccountTestService) testClaudeAccountConnection(c *gin.Context, account
|
||||
proxyURL = account.Proxy.URL()
|
||||
}
|
||||
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
|
||||
if err != nil {
|
||||
return s.sendErrorAndEnd(c, fmt.Sprintf("Request failed: %s", err.Error()))
|
||||
}
|
||||
@ -390,7 +394,7 @@ func (s *AccountTestService) testBedrockAccountConnection(c *gin.Context, ctx co
|
||||
proxyURL = account.Proxy.URL()
|
||||
}
|
||||
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, false)
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, nil)
|
||||
if err != nil {
|
||||
return s.sendErrorAndEnd(c, fmt.Sprintf("Request failed: %s", err.Error()))
|
||||
}
|
||||
@ -520,7 +524,7 @@ func (s *AccountTestService) testOpenAIAccountConnection(c *gin.Context, account
|
||||
proxyURL = account.Proxy.URL()
|
||||
}
|
||||
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
|
||||
if err != nil {
|
||||
return s.sendErrorAndEnd(c, fmt.Sprintf("Request failed: %s", err.Error()))
|
||||
}
|
||||
@ -610,7 +614,7 @@ func (s *AccountTestService) testGeminiAccountConnection(c *gin.Context, account
|
||||
proxyURL = account.Proxy.URL()
|
||||
}
|
||||
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
|
||||
if err != nil {
|
||||
return s.sendErrorAndEnd(c, fmt.Sprintf("Request failed: %s", err.Error()))
|
||||
}
|
||||
@ -881,9 +885,9 @@ func (s *AccountTestService) testSoraAccountConnection(c *gin.Context, account *
|
||||
if account.ProxyID != nil && account.Proxy != nil {
|
||||
proxyURL = account.Proxy.URL()
|
||||
}
|
||||
enableSoraTLSFingerprint := s.shouldEnableSoraTLSFingerprint()
|
||||
soraTLSProfile := s.resolveSoraTLSProfile()
|
||||
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, enableSoraTLSFingerprint)
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, soraTLSProfile)
|
||||
if err != nil {
|
||||
recorder.addStep("me", "failed", 0, "network_error", err.Error())
|
||||
s.emitSoraProbeSummary(c, recorder)
|
||||
@ -948,7 +952,7 @@ func (s *AccountTestService) testSoraAccountConnection(c *gin.Context, account *
|
||||
subReq.Header.Set("Origin", "https://sora.chatgpt.com")
|
||||
subReq.Header.Set("Referer", "https://sora.chatgpt.com/")
|
||||
|
||||
subResp, subErr := s.httpUpstream.DoWithTLS(subReq, proxyURL, account.ID, account.Concurrency, enableSoraTLSFingerprint)
|
||||
subResp, subErr := s.httpUpstream.DoWithTLS(subReq, proxyURL, account.ID, account.Concurrency, soraTLSProfile)
|
||||
if subErr != nil {
|
||||
recorder.addStep("subscription", "failed", 0, "network_error", subErr.Error())
|
||||
s.sendEvent(c, TestEvent{Type: "content", Text: fmt.Sprintf("Subscription check skipped: %s", subErr.Error())})
|
||||
@ -977,7 +981,7 @@ func (s *AccountTestService) testSoraAccountConnection(c *gin.Context, account *
|
||||
}
|
||||
|
||||
// 追加 Sora2 能力探测(对齐 sora2api 的测试思路):邀请码 + 剩余额度。
|
||||
s.testSora2Capabilities(c, ctx, account, authToken, proxyURL, enableSoraTLSFingerprint, recorder)
|
||||
s.testSora2Capabilities(c, ctx, account, authToken, proxyURL, soraTLSProfile, recorder)
|
||||
|
||||
s.emitSoraProbeSummary(c, recorder)
|
||||
s.sendEvent(c, TestEvent{Type: "test_complete", Success: true})
|
||||
@ -990,7 +994,7 @@ func (s *AccountTestService) testSora2Capabilities(
|
||||
account *Account,
|
||||
authToken string,
|
||||
proxyURL string,
|
||||
enableTLSFingerprint bool,
|
||||
tlsProfile *tlsfingerprint.Profile,
|
||||
recorder *soraProbeRecorder,
|
||||
) {
|
||||
inviteStatus, inviteHeader, inviteBody, err := s.fetchSoraTestEndpoint(
|
||||
@ -999,7 +1003,7 @@ func (s *AccountTestService) testSora2Capabilities(
|
||||
authToken,
|
||||
soraInviteMineURL,
|
||||
proxyURL,
|
||||
enableTLSFingerprint,
|
||||
tlsProfile,
|
||||
)
|
||||
if err != nil {
|
||||
if recorder != nil {
|
||||
@ -1016,7 +1020,7 @@ func (s *AccountTestService) testSora2Capabilities(
|
||||
authToken,
|
||||
soraBootstrapURL,
|
||||
proxyURL,
|
||||
enableTLSFingerprint,
|
||||
tlsProfile,
|
||||
)
|
||||
if bootstrapErr == nil && bootstrapStatus == http.StatusOK {
|
||||
if recorder != nil {
|
||||
@ -1029,7 +1033,7 @@ func (s *AccountTestService) testSora2Capabilities(
|
||||
authToken,
|
||||
soraInviteMineURL,
|
||||
proxyURL,
|
||||
enableTLSFingerprint,
|
||||
tlsProfile,
|
||||
)
|
||||
if err != nil {
|
||||
if recorder != nil {
|
||||
@ -1081,7 +1085,7 @@ func (s *AccountTestService) testSora2Capabilities(
|
||||
authToken,
|
||||
soraRemainingURL,
|
||||
proxyURL,
|
||||
enableTLSFingerprint,
|
||||
tlsProfile,
|
||||
)
|
||||
if remainingErr != nil {
|
||||
if recorder != nil {
|
||||
@ -1122,7 +1126,7 @@ func (s *AccountTestService) fetchSoraTestEndpoint(
|
||||
authToken string,
|
||||
url string,
|
||||
proxyURL string,
|
||||
enableTLSFingerprint bool,
|
||||
tlsProfile *tlsfingerprint.Profile,
|
||||
) (int, http.Header, []byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
@ -1135,7 +1139,7 @@ func (s *AccountTestService) fetchSoraTestEndpoint(
|
||||
req.Header.Set("Origin", "https://sora.chatgpt.com")
|
||||
req.Header.Set("Referer", "https://sora.chatgpt.com/")
|
||||
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, enableTLSFingerprint)
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, tlsProfile)
|
||||
if err != nil {
|
||||
return 0, nil, nil, err
|
||||
}
|
||||
@ -1224,11 +1228,12 @@ func parseSoraRemainingSummary(body []byte) string {
|
||||
return strings.Join(parts, " | ")
|
||||
}
|
||||
|
||||
func (s *AccountTestService) shouldEnableSoraTLSFingerprint() bool {
|
||||
if s == nil || s.cfg == nil {
|
||||
return true
|
||||
func (s *AccountTestService) resolveSoraTLSProfile() *tlsfingerprint.Profile {
|
||||
if s == nil || s.cfg == nil || !s.cfg.Sora.Client.DisableTLSFingerprint {
|
||||
// Sora TLS fingerprint enabled — use built-in default profile
|
||||
return &tlsfingerprint.Profile{Name: "Built-in Default (Sora)"}
|
||||
}
|
||||
return !s.cfg.Sora.Client.DisableTLSFingerprint
|
||||
return nil // disabled
|
||||
}
|
||||
|
||||
func isCloudflareChallengeResponse(statusCode int, headers http.Header, body []byte) bool {
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -24,9 +25,9 @@ func (u *queuedHTTPUpstream) Do(_ *http.Request, _ string, _ int64, _ int) (*htt
|
||||
return nil, fmt.Errorf("unexpected Do call")
|
||||
}
|
||||
|
||||
func (u *queuedHTTPUpstream) DoWithTLS(req *http.Request, _ string, _ int64, _ int, enableTLSFingerprint bool) (*http.Response, error) {
|
||||
func (u *queuedHTTPUpstream) DoWithTLS(req *http.Request, _ string, _ int64, _ int, profile *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
u.requests = append(u.requests, req)
|
||||
u.tlsFlags = append(u.tlsFlags, enableTLSFingerprint)
|
||||
u.tlsFlags = append(u.tlsFlags, profile != nil)
|
||||
if len(u.responses) == 0 {
|
||||
return nil, fmt.Errorf("no mocked response")
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ import (
|
||||
openaipkg "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/sync/singleflight"
|
||||
@ -241,11 +242,11 @@ type ClaudeUsageResponse struct {
|
||||
|
||||
// ClaudeUsageFetchOptions 包含获取 Claude 用量数据所需的所有选项
|
||||
type ClaudeUsageFetchOptions struct {
|
||||
AccessToken string // OAuth access token
|
||||
ProxyURL string // 代理 URL(可选)
|
||||
AccountID int64 // 账号 ID(用于 TLS 指纹选择)
|
||||
EnableTLSFingerprint bool // 是否启用 TLS 指纹伪装
|
||||
Fingerprint *Fingerprint // 缓存的指纹信息(User-Agent 等)
|
||||
AccessToken string // OAuth access token
|
||||
ProxyURL string // 代理 URL(可选)
|
||||
AccountID int64 // 账号 ID(用于连接池隔离)
|
||||
TLSProfile *tlsfingerprint.Profile // TLS 指纹 Profile(nil 表示不启用)
|
||||
Fingerprint *Fingerprint // 缓存的指纹信息(User-Agent 等)
|
||||
}
|
||||
|
||||
// ClaudeUsageFetcher fetches usage data from Anthropic OAuth API
|
||||
@ -264,6 +265,7 @@ type AccountUsageService struct {
|
||||
antigravityQuotaFetcher *AntigravityQuotaFetcher
|
||||
cache *UsageCache
|
||||
identityCache IdentityCache
|
||||
tlsFPProfileService *TLSFingerprintProfileService
|
||||
}
|
||||
|
||||
// NewAccountUsageService 创建AccountUsageService实例
|
||||
@ -275,6 +277,7 @@ func NewAccountUsageService(
|
||||
antigravityQuotaFetcher *AntigravityQuotaFetcher,
|
||||
cache *UsageCache,
|
||||
identityCache IdentityCache,
|
||||
tlsFPProfileService *TLSFingerprintProfileService,
|
||||
) *AccountUsageService {
|
||||
return &AccountUsageService{
|
||||
accountRepo: accountRepo,
|
||||
@ -284,6 +287,7 @@ func NewAccountUsageService(
|
||||
antigravityQuotaFetcher: antigravityQuotaFetcher,
|
||||
cache: cache,
|
||||
identityCache: identityCache,
|
||||
tlsFPProfileService: tlsFPProfileService,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1155,10 +1159,10 @@ func (s *AccountUsageService) fetchOAuthUsageRaw(ctx context.Context, account *A
|
||||
|
||||
// 构建完整的选项
|
||||
opts := &ClaudeUsageFetchOptions{
|
||||
AccessToken: accessToken,
|
||||
ProxyURL: proxyURL,
|
||||
AccountID: account.ID,
|
||||
EnableTLSFingerprint: account.IsTLSFingerprintEnabled(),
|
||||
AccessToken: accessToken,
|
||||
ProxyURL: proxyURL,
|
||||
AccountID: account.ID,
|
||||
TLSProfile: s.tlsFPProfileService.ResolveTLSProfile(account),
|
||||
}
|
||||
|
||||
// 尝试获取缓存的 Fingerprint(包含 User-Agent 等信息)
|
||||
|
||||
@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -130,7 +131,7 @@ func (s *httpUpstreamStub) Do(_ *http.Request, _ string, _ int64, _ int) (*http.
|
||||
return s.resp, s.err
|
||||
}
|
||||
|
||||
func (s *httpUpstreamStub) DoWithTLS(_ *http.Request, _ string, _ int64, _ int, _ bool) (*http.Response, error) {
|
||||
func (s *httpUpstreamStub) DoWithTLS(_ *http.Request, _ string, _ int64, _ int, _ *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
return s.resp, s.err
|
||||
}
|
||||
|
||||
@ -171,7 +172,7 @@ func (s *queuedHTTPUpstreamStub) Do(req *http.Request, _ string, _ int64, _ int)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (s *queuedHTTPUpstreamStub) DoWithTLS(req *http.Request, proxyURL string, accountID int64, concurrency int, _ bool) (*http.Response, error) {
|
||||
func (s *queuedHTTPUpstreamStub) DoWithTLS(req *http.Request, proxyURL string, accountID int64, concurrency int, _ *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
return s.Do(req, proxyURL, accountID, concurrency)
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -40,7 +41,7 @@ func (r *recordingOKUpstream) Do(req *http.Request, proxyURL string, accountID i
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *recordingOKUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) {
|
||||
func (r *recordingOKUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
return r.Do(req, proxyURL, accountID, accountConcurrency)
|
||||
}
|
||||
|
||||
@ -61,7 +62,7 @@ func (s *stubAntigravityUpstream) Do(req *http.Request, proxyURL string, account
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *stubAntigravityUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) {
|
||||
func (s *stubAntigravityUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
return s.Do(req, proxyURL, accountID, accountConcurrency)
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -93,7 +94,7 @@ func (m *mockSmartRetryUpstream) Do(req *http.Request, proxyURL string, accountI
|
||||
}, respErr
|
||||
}
|
||||
|
||||
func (m *mockSmartRetryUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) {
|
||||
func (m *mockSmartRetryUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
return m.Do(req, proxyURL, accountID, accountConcurrency)
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -35,7 +36,7 @@ func (u *epFixedUpstream) Do(req *http.Request, proxyURL string, accountID int64
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u *epFixedUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) {
|
||||
func (u *epFixedUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
return u.Do(req, proxyURL, accountID, accountConcurrency)
|
||||
}
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
@ -60,7 +61,7 @@ func (u *anthropicHTTPUpstreamRecorder) Do(req *http.Request, proxyURL string, a
|
||||
return u.resp, nil
|
||||
}
|
||||
|
||||
func (u *anthropicHTTPUpstreamRecorder) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) {
|
||||
func (u *anthropicHTTPUpstreamRecorder) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
return u.Do(req, proxyURL, accountID, accountConcurrency)
|
||||
}
|
||||
|
||||
|
||||
@ -120,7 +120,7 @@ func (s *GatewayService) ForwardAsChatCompletions(
|
||||
}
|
||||
|
||||
// 11. Send request
|
||||
resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
|
||||
if err != nil {
|
||||
if resp != nil && resp.Body != nil {
|
||||
_ = resp.Body.Close()
|
||||
|
||||
@ -117,7 +117,7 @@ func (s *GatewayService) ForwardAsResponses(
|
||||
}
|
||||
|
||||
// 11. Send request
|
||||
resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
|
||||
if err != nil {
|
||||
if resp != nil && resp.Body != nil {
|
||||
_ = resp.Body.Close()
|
||||
|
||||
@ -40,6 +40,7 @@ func newGatewayRecordUsageServiceForTest(usageRepo UsageLogRepository, userRepo
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -563,6 +563,8 @@ type GatewayService struct {
|
||||
responseHeaderFilter *responseheaders.CompiledHeaderFilter
|
||||
debugModelRouting atomic.Bool
|
||||
debugClaudeMimic atomic.Bool
|
||||
debugGatewayBodyFile atomic.Pointer[os.File] // non-nil when SUB2API_DEBUG_GATEWAY_BODY is set
|
||||
tlsFPProfileService *TLSFingerprintProfileService
|
||||
}
|
||||
|
||||
// NewGatewayService creates a new GatewayService
|
||||
@ -589,6 +591,7 @@ func NewGatewayService(
|
||||
rpmCache RPMCache,
|
||||
digestStore *DigestSessionStore,
|
||||
settingService *SettingService,
|
||||
tlsFPProfileService *TLSFingerprintProfileService,
|
||||
) *GatewayService {
|
||||
userGroupRateTTL := resolveUserGroupRateCacheTTL(cfg)
|
||||
modelsListTTL := resolveModelsListCacheTTL(cfg)
|
||||
@ -620,6 +623,7 @@ func NewGatewayService(
|
||||
modelsListCache: gocache.New(modelsListTTL, time.Minute),
|
||||
modelsListCacheTTL: modelsListTTL,
|
||||
responseHeaderFilter: compileResponseHeaderFilter(cfg),
|
||||
tlsFPProfileService: tlsFPProfileService,
|
||||
}
|
||||
svc.userGroupRateResolver = newUserGroupRateResolver(
|
||||
userGroupRateRepo,
|
||||
@ -4136,9 +4140,12 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
|
||||
proxyURL = account.Proxy.URL()
|
||||
}
|
||||
|
||||
// 解析 TLS 指纹 profile(同一请求生命周期内不变,避免重试循环中重复解析)
|
||||
tlsProfile := s.tlsFPProfileService.ResolveTLSProfile(account)
|
||||
|
||||
// 调试日志:记录即将转发的账号信息
|
||||
logger.LegacyPrintf("service.gateway", "[Forward] Using account: ID=%d Name=%s Platform=%s Type=%s TLSFingerprint=%v Proxy=%s",
|
||||
account.ID, account.Name, account.Platform, account.Type, account.IsTLSFingerprintEnabled(), proxyURL)
|
||||
account.ID, account.Name, account.Platform, account.Type, tlsProfile, proxyURL)
|
||||
// Pre-filter: strip empty text blocks (including nested in tool_result) to prevent upstream 400.
|
||||
body = StripEmptyTextBlocks(body)
|
||||
|
||||
@ -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 resp != nil && resp.Body != nil {
|
||||
_ = 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)
|
||||
releaseRetryCtx()
|
||||
if buildErr == nil {
|
||||
retryResp, retryErr := s.httpUpstream.DoWithTLS(retryReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
retryResp, retryErr := s.httpUpstream.DoWithTLS(retryReq, proxyURL, account.ID, account.Concurrency, tlsProfile)
|
||||
if retryErr == nil {
|
||||
if retryResp.StatusCode < 400 {
|
||||
logger.LegacyPrintf("service.gateway", "Account %d: thinking block retry succeeded (blocks downgraded)", account.ID)
|
||||
@ -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)
|
||||
releaseRetryCtx2()
|
||||
if buildErr2 == nil {
|
||||
retryResp2, retryErr2 := s.httpUpstream.DoWithTLS(retryReq2, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
retryResp2, retryErr2 := s.httpUpstream.DoWithTLS(retryReq2, proxyURL, account.ID, account.Concurrency, tlsProfile)
|
||||
if retryErr2 == nil {
|
||||
resp = retryResp2
|
||||
break
|
||||
@ -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)
|
||||
releaseBudgetRetryCtx()
|
||||
if buildErr == nil {
|
||||
budgetRetryResp, retryErr := s.httpUpstream.DoWithTLS(budgetRetryReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
budgetRetryResp, retryErr := s.httpUpstream.DoWithTLS(budgetRetryReq, proxyURL, account.ID, account.Concurrency, tlsProfile)
|
||||
if retryErr == nil {
|
||||
resp = budgetRetryResp
|
||||
break
|
||||
@ -4648,7 +4655,7 @@ func (s *GatewayService) forwardAnthropicAPIKeyPassthroughWithInput(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err = s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
resp, err = s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
|
||||
if err != nil {
|
||||
if resp != nil && resp.Body != nil {
|
||||
_ = resp.Body.Close()
|
||||
@ -5366,7 +5373,7 @@ func (s *GatewayService) executeBedrockUpstream(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err = s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, false)
|
||||
resp, err = s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, nil)
|
||||
if err != nil {
|
||||
if resp != nil && resp.Body != nil {
|
||||
_ = resp.Body.Close()
|
||||
@ -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 {
|
||||
setOpsUpstreamError(c, 0, sanitizeUpstreamErrorMessage(err.Error()), "")
|
||||
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)
|
||||
retryReq, buildErr := s.buildCountTokensRequest(ctx, c, account, filteredBody, token, tokenType, reqModel, shouldMimicClaudeCode)
|
||||
if buildErr == nil {
|
||||
retryResp, retryErr := s.httpUpstream.DoWithTLS(retryReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
retryResp, retryErr := s.httpUpstream.DoWithTLS(retryReq, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
|
||||
if retryErr == nil {
|
||||
resp = retryResp
|
||||
respBody, err = readUpstreamResponseBodyLimited(resp.Body, maxReadBytes)
|
||||
@ -8095,7 +8102,7 @@ func (s *GatewayService) forwardCountTokensAnthropicAPIKeyPassthrough(ctx contex
|
||||
proxyURL = account.Proxy.URL()
|
||||
}
|
||||
|
||||
resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
|
||||
resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
|
||||
if err != nil {
|
||||
setOpsUpstreamError(c, 0, sanitizeUpstreamErrorMessage(err.Error()), "")
|
||||
appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -36,7 +37,7 @@ func (s *geminiCompatHTTPUpstreamStub) Do(req *http.Request, proxyURL string, ac
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (s *geminiCompatHTTPUpstreamStub) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) {
|
||||
func (s *geminiCompatHTTPUpstreamStub) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
return s.Do(req, proxyURL, accountID, accountConcurrency)
|
||||
}
|
||||
|
||||
|
||||
@ -1,55 +1,24 @@
|
||||
package service
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
)
|
||||
|
||||
// HTTPUpstream 上游 HTTP 请求接口
|
||||
// 用于向上游 API(Claude、OpenAI、Gemini 等)发送请求
|
||||
// 这是一个通用接口,可用于任何基于 HTTP 的上游服务
|
||||
//
|
||||
// 设计说明:
|
||||
// - 支持可选代理配置
|
||||
// - 支持账户级连接池隔离
|
||||
// - 实现类负责连接池管理和复用
|
||||
// - 支持可选的 TLS 指纹伪装
|
||||
type HTTPUpstream interface {
|
||||
// Do 执行 HTTP 请求
|
||||
//
|
||||
// 参数:
|
||||
// - req: HTTP 请求对象,由调用方构建
|
||||
// - proxyURL: 代理服务器地址,空字符串表示直连
|
||||
// - accountID: 账户 ID,用于连接池隔离(隔离策略为 account 或 account_proxy 时生效)
|
||||
// - accountConcurrency: 账户并发限制,用于动态调整连接池大小
|
||||
//
|
||||
// 返回:
|
||||
// - *http.Response: HTTP 响应,调用方必须关闭 Body
|
||||
// - error: 请求错误(网络错误、超时等)
|
||||
//
|
||||
// 注意:
|
||||
// - 调用方必须关闭 resp.Body,否则会导致连接泄漏
|
||||
// - 响应体可能已被包装以跟踪请求生命周期
|
||||
// Do 执行 HTTP 请求(不启用 TLS 指纹)
|
||||
Do(req *http.Request, proxyURL string, accountID int64, accountConcurrency int) (*http.Response, error)
|
||||
|
||||
// DoWithTLS 执行带 TLS 指纹伪装的 HTTP 请求
|
||||
//
|
||||
// 参数:
|
||||
// - req: HTTP 请求对象,由调用方构建
|
||||
// - proxyURL: 代理服务器地址,空字符串表示直连
|
||||
// - accountID: 账户 ID,用于连接池隔离和 TLS 指纹模板选择
|
||||
// - accountConcurrency: 账户并发限制,用于动态调整连接池大小
|
||||
// - enableTLSFingerprint: 是否启用 TLS 指纹伪装
|
||||
// profile 参数:
|
||||
// - nil: 不启用 TLS 指纹,行为与 Do 方法相同
|
||||
// - non-nil: 使用指定的 Profile 进行 TLS 指纹伪装
|
||||
//
|
||||
// 返回:
|
||||
// - *http.Response: HTTP 响应,调用方必须关闭 Body
|
||||
// - error: 请求错误(网络错误、超时等)
|
||||
//
|
||||
// TLS 指纹说明:
|
||||
// - 当 enableTLSFingerprint=true 时,使用 utls 库模拟 Claude CLI 的 TLS 指纹
|
||||
// - TLS 指纹模板根据 accountID % len(profiles) 自动选择
|
||||
// - 支持直连、HTTP/HTTPS 代理、SOCKS5 代理三种场景
|
||||
// - 如果 enableTLSFingerprint=false,行为与 Do 方法相同
|
||||
//
|
||||
// 注意:
|
||||
// - 调用方必须关闭 resp.Body,否则会导致连接泄漏
|
||||
// - TLS 指纹客户端与普通客户端使用不同的缓存键,互不影响
|
||||
DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error)
|
||||
// Profile 由调用方通过 TLSFingerprintProfileService 解析后传入,
|
||||
// 支持按账号绑定的数据库 profile 或内置默认 profile。
|
||||
DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error)
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
@ -43,7 +44,7 @@ func (u *httpUpstreamRecorder) Do(req *http.Request, proxyURL string, accountID
|
||||
return u.resp, nil
|
||||
}
|
||||
|
||||
func (u *httpUpstreamRecorder) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) {
|
||||
func (u *httpUpstreamRecorder) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
return u.Do(req, proxyURL, accountID, accountConcurrency)
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -57,7 +58,7 @@ func (u *httpUpstreamSequenceRecorder) Do(req *http.Request, proxyURL string, ac
|
||||
return u.responses[len(u.responses)-1], nil
|
||||
}
|
||||
|
||||
func (u *httpUpstreamSequenceRecorder) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) {
|
||||
func (u *httpUpstreamSequenceRecorder) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) {
|
||||
return u.Do(req, proxyURL, accountID, accountConcurrency)
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
NewTotpService,
|
||||
NewErrorPassthroughService,
|
||||
NewTLSFingerprintProfileService,
|
||||
NewDigestSessionStore,
|
||||
ProvideIdempotencyCoordinator,
|
||||
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 scheduledTestsAPI from './scheduledTests'
|
||||
import backupAPI from './backup'
|
||||
import tlsFingerprintProfileAPI from './tlsFingerprintProfile'
|
||||
|
||||
/**
|
||||
* Unified admin API object for convenient access
|
||||
@ -49,7 +50,8 @@ export const adminAPI = {
|
||||
dataManagement: dataManagementAPI,
|
||||
apiKeys: apiKeysAPI,
|
||||
scheduledTests: scheduledTestsAPI,
|
||||
backup: backupAPI
|
||||
backup: backupAPI,
|
||||
tlsFingerprintProfiles: tlsFingerprintProfileAPI
|
||||
}
|
||||
|
||||
export {
|
||||
@ -73,7 +75,8 @@ export {
|
||||
dataManagementAPI,
|
||||
apiKeysAPI,
|
||||
scheduledTestsAPI,
|
||||
backupAPI
|
||||
backupAPI,
|
||||
tlsFingerprintProfileAPI
|
||||
}
|
||||
|
||||
export default adminAPI
|
||||
@ -82,3 +85,4 @@ export default adminAPI
|
||||
export type { BalanceHistoryItem } from './users'
|
||||
export type { ErrorPassthroughRule, CreateRuleRequest, UpdateRuleRequest } from './errorPassthrough'
|
||||
export type { BackupAgentHealth, DataManagementConfig } from './dataManagement'
|
||||
export type { TLSFingerprintProfile, CreateProfileRequest, UpdateProfileRequest } from './tlsFingerprintProfile'
|
||||
|
||||
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>
|
||||
</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>
|
||||
|
||||
<!-- Session ID Masking -->
|
||||
@ -3082,6 +3090,8 @@ const umqModeOptions = computed(() => [
|
||||
{ value: 'serialize', label: t('admin.accounts.quotaControl.rpmLimit.umqModeSerialize') },
|
||||
])
|
||||
const tlsFingerprintEnabled = ref(false)
|
||||
const tlsFingerprintProfileId = ref<number | null>(null)
|
||||
const tlsFingerprintProfiles = ref<{ id: number; name: string }[]>([])
|
||||
const sessionIdMaskingEnabled = ref(false)
|
||||
const cacheTTLOverrideEnabled = ref(false)
|
||||
const cacheTTLOverrideTarget = ref<string>('5m')
|
||||
@ -3247,6 +3257,10 @@ watch(
|
||||
() => props.show,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
// Load TLS fingerprint profiles
|
||||
adminAPI.tlsFingerprintProfiles.list()
|
||||
.then(profiles => { tlsFingerprintProfiles.value = profiles.map(p => ({ id: p.id, name: p.name })) })
|
||||
.catch(() => { tlsFingerprintProfiles.value = [] })
|
||||
// Modal opened - fill related models
|
||||
allowedModels.value = [...getModelsByPlatform(form.platform)]
|
||||
// Antigravity: 默认使用映射模式并填充默认映射
|
||||
@ -3747,6 +3761,7 @@ const resetForm = () => {
|
||||
rpmStickyBuffer.value = null
|
||||
userMsgQueueMode.value = ''
|
||||
tlsFingerprintEnabled.value = false
|
||||
tlsFingerprintProfileId.value = null
|
||||
sessionIdMaskingEnabled.value = false
|
||||
cacheTTLOverrideEnabled.value = false
|
||||
cacheTTLOverrideTarget.value = '5m'
|
||||
@ -4825,6 +4840,9 @@ const handleAnthropicExchange = async (authCode: string) => {
|
||||
// Add TLS fingerprint settings
|
||||
if (tlsFingerprintEnabled.value) {
|
||||
extra.enable_tls_fingerprint = true
|
||||
if (tlsFingerprintProfileId.value) {
|
||||
extra.tls_fingerprint_profile_id = tlsFingerprintProfileId.value
|
||||
}
|
||||
}
|
||||
|
||||
// Add session ID masking settings
|
||||
@ -4940,6 +4958,9 @@ const handleCookieAuth = async (sessionKey: string) => {
|
||||
// Add TLS fingerprint settings
|
||||
if (tlsFingerprintEnabled.value) {
|
||||
extra.enable_tls_fingerprint = true
|
||||
if (tlsFingerprintProfileId.value) {
|
||||
extra.tls_fingerprint_profile_id = tlsFingerprintProfileId.value
|
||||
}
|
||||
}
|
||||
|
||||
// Add session ID masking settings
|
||||
|
||||
@ -1504,6 +1504,14 @@
|
||||
/>
|
||||
</button>
|
||||
</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>
|
||||
|
||||
<!-- Session ID Masking -->
|
||||
@ -1841,6 +1849,8 @@ const umqModeOptions = computed(() => [
|
||||
{ value: 'serialize', label: t('admin.accounts.quotaControl.rpmLimit.umqModeSerialize') },
|
||||
])
|
||||
const tlsFingerprintEnabled = ref(false)
|
||||
const tlsFingerprintProfileId = ref<number | null>(null)
|
||||
const tlsFingerprintProfiles = ref<{ id: number; name: string }[]>([])
|
||||
const sessionIdMaskingEnabled = ref(false)
|
||||
const cacheTTLOverrideEnabled = ref(false)
|
||||
const cacheTTLOverrideTarget = ref<string>('5m')
|
||||
@ -2255,11 +2265,21 @@ watch(
|
||||
}
|
||||
if (!wasShow || newAccount !== previousAccount) {
|
||||
syncFormFromAccount(newAccount)
|
||||
loadTLSProfiles()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const loadTLSProfiles = async () => {
|
||||
try {
|
||||
const profiles = await adminAPI.tlsFingerprintProfiles.list()
|
||||
tlsFingerprintProfiles.value = profiles.map(p => ({ id: p.id, name: p.name }))
|
||||
} catch {
|
||||
tlsFingerprintProfiles.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// Model mapping helpers
|
||||
const addModelMapping = () => {
|
||||
modelMappings.value.push({ from: '', to: '' })
|
||||
@ -2458,6 +2478,7 @@ function loadQuotaControlSettings(account: Account) {
|
||||
rpmStickyBuffer.value = null
|
||||
userMsgQueueMode.value = ''
|
||||
tlsFingerprintEnabled.value = false
|
||||
tlsFingerprintProfileId.value = null
|
||||
sessionIdMaskingEnabled.value = false
|
||||
cacheTTLOverrideEnabled.value = false
|
||||
cacheTTLOverrideTarget.value = '5m'
|
||||
@ -2495,6 +2516,7 @@ function loadQuotaControlSettings(account: Account) {
|
||||
if (account.enable_tls_fingerprint === true) {
|
||||
tlsFingerprintEnabled.value = true
|
||||
}
|
||||
tlsFingerprintProfileId.value = account.tls_fingerprint_profile_id ?? null
|
||||
|
||||
// Load session ID masking setting
|
||||
if (account.session_id_masking_enabled === true) {
|
||||
@ -2932,8 +2954,14 @@ const handleSubmit = async () => {
|
||||
// TLS fingerprint setting
|
||||
if (tlsFingerprintEnabled.value) {
|
||||
newExtra.enable_tls_fingerprint = true
|
||||
if (tlsFingerprintProfileId.value) {
|
||||
newExtra.tls_fingerprint_profile_id = tlsFingerprintProfileId.value
|
||||
} else {
|
||||
delete newExtra.tls_fingerprint_profile_id
|
||||
}
|
||||
} else {
|
||||
delete newExtra.enable_tls_fingerprint
|
||||
delete newExtra.tls_fingerprint_profile_id
|
||||
}
|
||||
|
||||
// Session ID masking setting
|
||||
|
||||
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: {
|
||||
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: {
|
||||
label: 'Session ID Masking',
|
||||
@ -4572,6 +4574,62 @@ export default {
|
||||
failedToSave: 'Failed to save rule',
|
||||
failedToDelete: 'Failed to delete rule',
|
||||
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: {
|
||||
label: 'TLS 指纹模拟',
|
||||
hint: '模拟 Node.js/Claude Code 客户端的 TLS 指纹'
|
||||
hint: '模拟 Node.js/Claude Code 客户端的 TLS 指纹',
|
||||
defaultProfile: '内置默认',
|
||||
randomProfile: '随机'
|
||||
},
|
||||
sessionIdMasking: {
|
||||
label: '会话 ID 伪装',
|
||||
@ -4736,6 +4738,62 @@ export default {
|
||||
failedToSave: '保存规则失败',
|
||||
failedToDelete: '删除规则失败',
|
||||
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 账号有效)
|
||||
enable_tls_fingerprint?: boolean | null
|
||||
tls_fingerprint_profile_id?: number | null
|
||||
|
||||
// 会话ID伪装(仅 Anthropic OAuth/SetupToken 账号有效)
|
||||
// 启用后将在15分钟内固定 metadata.user_id 中的 session ID
|
||||
|
||||
@ -73,6 +73,16 @@
|
||||
<span class="hidden md:inline">{{ t('admin.errorPassthrough.title') }}</span>
|
||||
</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 -->
|
||||
<div class="relative" ref="columnDropdownRef">
|
||||
<button
|
||||
@ -289,6 +299,7 @@
|
||||
</label>
|
||||
</ConfirmDialog>
|
||||
<ErrorPassthroughRulesModal :show="showErrorPassthrough" @close="showErrorPassthrough = false" />
|
||||
<TLSFingerprintProfilesModal :show="showTLSFingerprintProfiles" @close="showTLSFingerprintProfiles = false" />
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
@ -326,6 +337,7 @@ import AccountCapacityCell from '@/components/account/AccountCapacityCell.vue'
|
||||
import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import ErrorPassthroughRulesModal from '@/components/admin/ErrorPassthroughRulesModal.vue'
|
||||
import TLSFingerprintProfilesModal from '@/components/admin/TLSFingerprintProfilesModal.vue'
|
||||
import { buildOpenAIUsageRefreshKey } from '@/utils/accountUsageRefresh'
|
||||
import { formatDateTime, formatRelativeTime } from '@/utils/format'
|
||||
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 showStats = ref(false)
|
||||
const showErrorPassthrough = ref(false)
|
||||
const showTLSFingerprintProfiles = ref(false)
|
||||
const edAcc = ref<Account | null>(null)
|
||||
const tempUnschedAcc = ref<Account | null>(null)
|
||||
const deletingAcc = ref<Account | null>(null)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user