1
0

client_hwid_test.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. package service
  2. import (
  3. "path/filepath"
  4. "testing"
  5. "github.com/mhsanaei/3x-ui/v3/internal/database"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  7. )
  8. func initClientHwidTestDB(t *testing.T) {
  9. t.Helper()
  10. dbDir := t.TempDir()
  11. t.Setenv("XUI_DB_FOLDER", dbDir)
  12. if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
  13. t.Fatalf("InitDB: %v", err)
  14. }
  15. t.Cleanup(func() { _ = database.CloseDB() })
  16. }
  17. func seedHwidClient(t *testing.T, limit int) *model.ClientRecord {
  18. t.Helper()
  19. rec := &model.ClientRecord{
  20. Email: "[email protected]",
  21. SubID: "sub-hwid",
  22. UUID: "11111111-2222-4333-8444-555555555555",
  23. Enable: true,
  24. LimitHwid: limit,
  25. }
  26. if err := database.GetDB().Create(rec).Error; err != nil {
  27. t.Fatalf("seed client: %v", err)
  28. }
  29. return rec
  30. }
  31. func TestClientHwidGate(t *testing.T) {
  32. initClientHwidTestDB(t)
  33. svc := &ClientService{}
  34. seedHwidClient(t, 0)
  35. res, err := svc.EnforceHwidForSubID("sub-hwid", HwidRequest{})
  36. if err != nil {
  37. t.Fatalf("no-limit gate: %v", err)
  38. }
  39. if !res.Allowed || res.Active {
  40. t.Fatalf("no limit should allow missing HWID without active headers: %+v", res)
  41. }
  42. }
  43. func TestClientHwidGateRegistersAndBlocks(t *testing.T) {
  44. initClientHwidTestDB(t)
  45. svc := &ClientService{}
  46. rec := seedHwidClient(t, 2)
  47. res, err := svc.EnforceHwidForSubID(rec.SubID, HwidRequest{})
  48. if err != nil {
  49. t.Fatalf("missing HWID gate: %v", err)
  50. }
  51. if res.Allowed || !res.Active || !res.NotSupported {
  52. t.Fatalf("missing HWID should be denied as not supported: %+v", res)
  53. }
  54. firstRaw := "device-one"
  55. for _, raw := range []string{firstRaw, "device-two"} {
  56. res, err = svc.EnforceHwidForSubID(rec.SubID, HwidRequest{
  57. Hwid: raw,
  58. UserAgent: "Happ/1.0",
  59. DeviceOS: "android",
  60. OsVersion: "15",
  61. DeviceModel: raw + "-model",
  62. })
  63. if err != nil {
  64. t.Fatalf("register %s: %v", raw, err)
  65. }
  66. if !res.Allowed {
  67. t.Fatalf("register %s denied: %+v", raw, res)
  68. }
  69. }
  70. res, err = svc.EnforceHwidForSubID(rec.SubID, HwidRequest{Hwid: "device-three"})
  71. if err != nil {
  72. t.Fatalf("third HWID gate: %v", err)
  73. }
  74. if res.Allowed || !res.MaxDevicesReached || !res.LimitReached {
  75. t.Fatalf("third unique HWID should be denied after limit: %+v", res)
  76. }
  77. res, err = svc.EnforceHwidForSubID(rec.SubID, HwidRequest{
  78. Hwid: firstRaw,
  79. UserAgent: "Karing/2.0",
  80. DeviceOS: "ios",
  81. OsVersion: "18",
  82. DeviceModel: "updated-model",
  83. })
  84. if err != nil {
  85. t.Fatalf("existing HWID after full limit: %v", err)
  86. }
  87. if !res.Allowed || !res.LimitReached {
  88. t.Fatalf("existing registered HWID should pass after limit: %+v", res)
  89. }
  90. var hashes []string
  91. if err := database.GetDB().Model(&model.ClientHwid{}).Pluck("hwid_hash", &hashes).Error; err != nil {
  92. t.Fatalf("pluck hashes: %v", err)
  93. }
  94. if len(hashes) != 2 {
  95. t.Fatalf("stored HWIDs = %d, want 2", len(hashes))
  96. }
  97. for _, h := range hashes {
  98. if h == firstRaw || h == "device-two" || len(h) != 64 {
  99. t.Fatalf("raw HWID leaked or invalid hash stored: %q", h)
  100. }
  101. }
  102. list, err := svc.ListClientHwids(rec.Email)
  103. if err != nil {
  104. t.Fatalf("list HWIDs: %v", err)
  105. }
  106. if len(list) != 2 {
  107. t.Fatalf("list count = %d, want 2", len(list))
  108. }
  109. foundUpdated := false
  110. for _, row := range list {
  111. if row.DeviceModel == "updated-model" && row.UserAgent == "Karing/2.0" && row.DeviceOS == "ios" && row.OsVersion == "18" {
  112. foundUpdated = true
  113. }
  114. }
  115. if !foundUpdated {
  116. t.Fatalf("updated HWID metadata missing: %#v", list)
  117. }
  118. if err := svc.setClientLimitHwidByEmail(nil, rec.Email, 1); err != nil {
  119. t.Fatalf("lower limit: %v", err)
  120. }
  121. var count int64
  122. if err := database.GetDB().Model(&model.ClientHwid{}).Where("sub_id = ?", rec.SubID).Count(&count).Error; err != nil {
  123. t.Fatalf("count after trim: %v", err)
  124. }
  125. if count != 1 {
  126. t.Fatalf("lowered limit should trim stored HWIDs to 1, got %d", count)
  127. }
  128. if err := svc.ClearClientHwids(rec.Email); err != nil {
  129. t.Fatalf("clear HWIDs: %v", err)
  130. }
  131. if err := database.GetDB().Model(&model.ClientHwid{}).Where("sub_id = ?", rec.SubID).Count(&count).Error; err != nil {
  132. t.Fatalf("count after clear: %v", err)
  133. }
  134. if count != 0 {
  135. t.Fatalf("clear should remove all HWIDs, got %d", count)
  136. }
  137. }
  138. func TestClientHwidGateSharedSubIdUsesMaxLimit(t *testing.T) {
  139. initClientHwidTestDB(t)
  140. svc := &ClientService{}
  141. db := database.GetDB()
  142. subID := "shared-sub"
  143. if err := db.Create(&model.ClientRecord{Email: "[email protected]", SubID: subID, UUID: "11111111-2222-4333-8444-555555555555", Enable: true, LimitHwid: 0}).Error; err != nil {
  144. t.Fatalf("seed anchor: %v", err)
  145. }
  146. if err := db.Create(&model.ClientRecord{Email: "[email protected]", SubID: subID, UUID: "22222222-2222-4333-8444-555555555555", Enable: true, LimitHwid: 2}).Error; err != nil {
  147. t.Fatalf("seed second: %v", err)
  148. }
  149. res, err := svc.EnforceHwidForSubID(subID, HwidRequest{})
  150. if err != nil || !res.Active || res.Limit != 2 {
  151. t.Fatalf("expected active gate limit 2 from max row, err=%v res=%+v", err, res)
  152. }
  153. if res.Allowed || !res.NotSupported {
  154. t.Fatalf("missing HWID should be denied: %+v", res)
  155. }
  156. }