account_protection.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. package repository
  2. import (
  3. "context"
  4. "encoding/json"
  5. "strings"
  6. "time"
  7. dbent "github.com/Wei-Shaw/sub2api/ent"
  8. "github.com/Wei-Shaw/sub2api/internal/service"
  9. "github.com/lib/pq"
  10. )
  11. const accountProtectionEnabledSQL = "COALESCE(extra -> 'anti_degradation' = 'true'::jsonb, extra #> '{anti_degrade,enabled}' = 'true'::jsonb, false)"
  12. // An atomic expression preserves the committed state even when partial updates
  13. // come from imports or asynchronous credential refreshes with stale snapshots.
  14. func preserveProtectionExtraSQL(ctx context.Context, expression string) string {
  15. if service.ProtectionManagedWrite(ctx) {
  16. return expression
  17. }
  18. base := "ARRAY['anti_degradation','protection_scope','anti_degrade']::text[]"
  19. mode1 := "ARRAY['anti_degradation','protection_scope','anti_degrade','codex_fingerprint_mode','enable_tls_fingerprint','tls_fingerprint_builtin','tls_fingerprint_profile_id','proxy_mode']::text[]"
  20. modes := "'" + strings.Join(service.RegisteredProtectionModes(), "','") + "'"
  21. keys := "(CASE WHEN (COALESCE(extra #>> '{anti_degrade,enabled}', 'true') <> 'false' AND (extra #> '{anti_degrade,policy_version}' IS NOT NULL OR extra #>> '{anti_degrade,mode}' IN (" + modes + "))) THEN " + mode1 + " ELSE " + base + " END)"
  22. return "((" + expression + ") - " + keys + ") || COALESCE((SELECT jsonb_object_agg(key,value) FROM jsonb_each(COALESCE(extra,'{}'::jsonb)) WHERE key = ANY(" + keys + ")), '{}'::jsonb)"
  23. }
  24. func protectedConcurrencySQL(expression string) string {
  25. return "CASE WHEN " + accountProtectionEnabledSQL + " AND " + expression + " <= 0 THEN 16 ELSE " + expression + " END"
  26. }
  27. func preserveLockedAccountProtection(ctx context.Context, client *dbent.Client, a *service.Account) error {
  28. // lockAndMergeAccountProbeExtra already holds FOR NO KEY UPDATE in this
  29. // transaction. Re-read just extra under that lock to prevent stale writes.
  30. expected, compareVersion := service.GetProtectionWriteExpectation(ctx)
  31. query := "SELECT extra FROM accounts WHERE id = $1 AND deleted_at IS NULL"
  32. if compareVersion {
  33. query = "SELECT extra, updated_at FROM accounts WHERE id = $1 AND deleted_at IS NULL"
  34. }
  35. rows, err := client.QueryContext(ctx, query, a.ID)
  36. if err != nil {
  37. return err
  38. }
  39. defer rows.Close()
  40. if !rows.Next() {
  41. if err := rows.Err(); err != nil {
  42. return err
  43. }
  44. return service.ErrAccountNotFound
  45. }
  46. var raw []byte
  47. var revision time.Time
  48. var scanErr error
  49. if compareVersion {
  50. scanErr = rows.Scan(&raw, &revision)
  51. } else {
  52. scanErr = rows.Scan(&raw)
  53. }
  54. if scanErr != nil {
  55. return scanErr
  56. }
  57. if compareVersion && (expected.AccountID != a.ID || !expected.UpdatedAt.Equal(revision)) {
  58. return service.ErrProtectionConflict
  59. }
  60. var extra map[string]any
  61. if len(raw) > 0 {
  62. if err := json.Unmarshal(raw, &extra); err != nil {
  63. return err
  64. }
  65. }
  66. current := &service.Account{Platform: a.Platform, Type: a.Type, Extra: extra}
  67. if !service.ProtectionManagedWrite(ctx) && service.ProtectedProxyModeConflict(current, a.Extra) {
  68. return service.ErrProtectedProxyModeChange
  69. }
  70. a.Extra = service.PreserveAccountProtection(ctx, current, a.Extra)
  71. service.BoundAccountProtectionConcurrency(a)
  72. if err := service.ValidateAccountProtectionConfiguration(a); err != nil {
  73. return err
  74. }
  75. return rows.Err()
  76. }
  77. func validateLockedBulkProxyMode(ctx context.Context, exec sqlExecutor, ids []int64, incoming map[string]any) error {
  78. rows, err := exec.QueryContext(ctx, `SELECT platform,type,extra FROM accounts WHERE id=ANY($1) AND deleted_at IS NULL ORDER BY id FOR NO KEY UPDATE`, pq.Array(ids))
  79. if err != nil {
  80. return err
  81. }
  82. defer rows.Close()
  83. for rows.Next() {
  84. var a service.Account
  85. var raw []byte
  86. if err := rows.Scan(&a.Platform, &a.Type, &raw); err != nil {
  87. return err
  88. }
  89. if err := json.Unmarshal(raw, &a.Extra); err != nil {
  90. return err
  91. }
  92. if service.ProtectedProxyModeConflict(&a, incoming) {
  93. return service.ErrProtectedProxyModeChange
  94. }
  95. }
  96. return rows.Err()
  97. }