client_hwid_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  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. for _, ua := range []string{"Happ/1.0", "Happ/2.0"} {
  43. res, err = svc.EnforceHwidForSubID("sub-hwid", HwidRequest{Hwid: "device-one", UserAgent: ua})
  44. if err != nil {
  45. t.Fatalf("no-limit gate with HWID: %v", err)
  46. }
  47. if res != (HwidGateResult{Allowed: true}) {
  48. t.Fatalf("no limit should allow HWID without active headers: %+v", res)
  49. }
  50. }
  51. list, err := svc.ListClientHwids("[email protected]")
  52. if err != nil {
  53. t.Fatalf("list HWIDs: %v", err)
  54. }
  55. if len(list) != 1 || list[0].UserAgent != "Happ/2.0" {
  56. t.Fatalf("no limit should still track one device with fresh metadata, got %+v", list)
  57. }
  58. }
  59. func TestClientHwidGateRegistersAndBlocks(t *testing.T) {
  60. initClientHwidTestDB(t)
  61. svc := &ClientService{}
  62. rec := seedHwidClient(t, 2)
  63. res, err := svc.EnforceHwidForSubID(rec.SubID, HwidRequest{})
  64. if err != nil {
  65. t.Fatalf("missing HWID gate: %v", err)
  66. }
  67. if res.Allowed || !res.Active || !res.NotSupported {
  68. t.Fatalf("missing HWID should be denied as not supported: %+v", res)
  69. }
  70. firstRaw := "device-one"
  71. for _, raw := range []string{firstRaw, "device-two"} {
  72. res, err = svc.EnforceHwidForSubID(rec.SubID, HwidRequest{
  73. Hwid: raw,
  74. UserAgent: "Happ/1.0",
  75. DeviceOS: "android",
  76. OsVersion: "15",
  77. DeviceModel: raw + "-model",
  78. })
  79. if err != nil {
  80. t.Fatalf("register %s: %v", raw, err)
  81. }
  82. if !res.Allowed {
  83. t.Fatalf("register %s denied: %+v", raw, res)
  84. }
  85. }
  86. res, err = svc.EnforceHwidForSubID(rec.SubID, HwidRequest{Hwid: "device-three"})
  87. if err != nil {
  88. t.Fatalf("third HWID gate: %v", err)
  89. }
  90. if res.Allowed || !res.MaxDevicesReached || !res.LimitReached {
  91. t.Fatalf("third unique HWID should be denied after limit: %+v", res)
  92. }
  93. res, err = svc.EnforceHwidForSubID(rec.SubID, HwidRequest{
  94. Hwid: firstRaw,
  95. UserAgent: "Karing/2.0",
  96. DeviceOS: "ios",
  97. OsVersion: "18",
  98. DeviceModel: "updated-model",
  99. })
  100. if err != nil {
  101. t.Fatalf("existing HWID after full limit: %v", err)
  102. }
  103. if !res.Allowed || !res.LimitReached {
  104. t.Fatalf("existing registered HWID should pass after limit: %+v", res)
  105. }
  106. var hashes []string
  107. if err := database.GetDB().Model(&model.ClientHwid{}).Pluck("hwid_hash", &hashes).Error; err != nil {
  108. t.Fatalf("pluck hashes: %v", err)
  109. }
  110. if len(hashes) != 2 {
  111. t.Fatalf("stored HWIDs = %d, want 2", len(hashes))
  112. }
  113. for _, h := range hashes {
  114. if h == firstRaw || h == "device-two" || len(h) != 64 {
  115. t.Fatalf("raw HWID leaked or invalid hash stored: %q", h)
  116. }
  117. }
  118. list, err := svc.ListClientHwids(rec.Email)
  119. if err != nil {
  120. t.Fatalf("list HWIDs: %v", err)
  121. }
  122. if len(list) != 2 {
  123. t.Fatalf("list count = %d, want 2", len(list))
  124. }
  125. foundUpdated := false
  126. for _, row := range list {
  127. if len(row.Fingerprint) != hwidFingerprintLength {
  128. t.Fatalf("fingerprint length = %d, want %d: %q", len(row.Fingerprint), hwidFingerprintLength, row.Fingerprint)
  129. }
  130. if row.DeviceModel == "updated-model" && row.UserAgent == "Karing/2.0" && row.DeviceOS == "ios" && row.OsVersion == "18" {
  131. foundUpdated = true
  132. want := hashHwid(firstRaw)[:hwidFingerprintLength]
  133. if row.Fingerprint != want {
  134. t.Fatalf("fingerprint = %q, want %q", row.Fingerprint, want)
  135. }
  136. }
  137. }
  138. if !foundUpdated {
  139. t.Fatalf("updated HWID metadata missing: %#v", list)
  140. }
  141. if err := svc.setClientLimitHwidByEmail(nil, rec.Email, 1); err != nil {
  142. t.Fatalf("lower limit: %v", err)
  143. }
  144. var count int64
  145. if err := database.GetDB().Model(&model.ClientHwid{}).Where("sub_id = ?", rec.SubID).Count(&count).Error; err != nil {
  146. t.Fatalf("count after trim: %v", err)
  147. }
  148. if count != 1 {
  149. t.Fatalf("lowered limit should trim stored HWIDs to 1, got %d", count)
  150. }
  151. if err := svc.ClearClientHwids(rec.Email); err != nil {
  152. t.Fatalf("clear HWIDs: %v", err)
  153. }
  154. if err := database.GetDB().Model(&model.ClientHwid{}).Where("sub_id = ?", rec.SubID).Count(&count).Error; err != nil {
  155. t.Fatalf("count after clear: %v", err)
  156. }
  157. if count != 0 {
  158. t.Fatalf("clear should remove all HWIDs, got %d", count)
  159. }
  160. }
  161. func TestDeleteClientHwid(t *testing.T) {
  162. initClientHwidTestDB(t)
  163. svc := &ClientService{}
  164. db := database.GetDB()
  165. rec := seedHwidClient(t, 5)
  166. if _, err := svc.EnforceHwidForSubID(rec.SubID, HwidRequest{Hwid: "device-own"}); err != nil {
  167. t.Fatalf("register own device: %v", err)
  168. }
  169. list, err := svc.ListClientHwids(rec.Email)
  170. if err != nil || len(list) != 1 {
  171. t.Fatalf("list own devices: err=%v list=%+v", err, list)
  172. }
  173. ownID := list[0].Id
  174. other := &model.ClientRecord{Email: "[email protected]", SubID: "sub-other", UUID: "33333333-2222-4333-8444-555555555555", Enable: true, LimitHwid: 5}
  175. if err := db.Create(other).Error; err != nil {
  176. t.Fatalf("seed other client: %v", err)
  177. }
  178. if _, err := svc.EnforceHwidForSubID(other.SubID, HwidRequest{Hwid: "device-foreign"}); err != nil {
  179. t.Fatalf("register foreign device: %v", err)
  180. }
  181. otherList, err := svc.ListClientHwids(other.Email)
  182. if err != nil || len(otherList) != 1 {
  183. t.Fatalf("list foreign devices: err=%v list=%+v", err, otherList)
  184. }
  185. foreignID := otherList[0].Id
  186. if err := svc.DeleteClientHwid(rec.Email, foreignID); err == nil {
  187. t.Fatalf("deleting a foreign sub_id's device id should fail")
  188. }
  189. if list, err := svc.ListClientHwids(other.Email); err != nil || len(list) != 1 {
  190. t.Fatalf("foreign device should survive a cross-sub_id delete attempt: err=%v list=%+v", err, list)
  191. }
  192. if err := svc.DeleteClientHwid(rec.Email, 999999); err == nil {
  193. t.Fatalf("deleting an unknown id should fail")
  194. }
  195. if err := svc.DeleteClientHwid(rec.Email, ownID); err != nil {
  196. t.Fatalf("delete own device: %v", err)
  197. }
  198. if list, err := svc.ListClientHwids(rec.Email); err != nil || len(list) != 0 {
  199. t.Fatalf("own device should be gone: err=%v list=%+v", err, list)
  200. }
  201. }
  202. func TestClientHwidGateSharedSubIdUsesMaxLimit(t *testing.T) {
  203. initClientHwidTestDB(t)
  204. svc := &ClientService{}
  205. db := database.GetDB()
  206. subID := "shared-sub"
  207. if err := db.Create(&model.ClientRecord{Email: "[email protected]", SubID: subID, UUID: "11111111-2222-4333-8444-555555555555", Enable: true, LimitHwid: 0}).Error; err != nil {
  208. t.Fatalf("seed anchor: %v", err)
  209. }
  210. if err := db.Create(&model.ClientRecord{Email: "[email protected]", SubID: subID, UUID: "22222222-2222-4333-8444-555555555555", Enable: true, LimitHwid: 2}).Error; err != nil {
  211. t.Fatalf("seed second: %v", err)
  212. }
  213. res, err := svc.EnforceHwidForSubID(subID, HwidRequest{})
  214. if err != nil || !res.Active || res.Limit != 2 {
  215. t.Fatalf("expected active gate limit 2 from max row, err=%v res=%+v", err, res)
  216. }
  217. if res.Allowed || !res.NotSupported {
  218. t.Fatalf("missing HWID should be denied: %+v", res)
  219. }
  220. }
  221. func TestClientHwidSlotStatus(t *testing.T) {
  222. initClientHwidTestDB(t)
  223. svc := &ClientService{}
  224. db := database.GetDB()
  225. rec := seedHwidClient(t, 1)
  226. status, found, err := svc.HwidSlotStatusForSubID("no-such-sub")
  227. if err != nil || found || status != (HwidSlotStatus{}) {
  228. t.Fatalf("unknown subId = (%+v, %v, %v), want zero status and found=false", status, found, err)
  229. }
  230. status, found, err = svc.HwidSlotStatusForSubID(" " + rec.SubID + " ")
  231. if err != nil || !found {
  232. t.Fatalf("padded subId = (%+v, %v, %v), want found=true", status, found, err)
  233. }
  234. if want := (HwidSlotStatus{Active: true, Limit: 1, Remaining: 1}); status != want {
  235. t.Fatalf("empty slots = %+v, want %+v", status, want)
  236. }
  237. // A shared sub_id takes the highest limit, matching the enforcement gate.
  238. if err := db.Create(&model.ClientRecord{Email: "[email protected]", SubID: rec.SubID, UUID: "22222222-2222-4333-8444-555555555555", Enable: true, LimitHwid: 3}).Error; err != nil {
  239. t.Fatalf("seed second client: %v", err)
  240. }
  241. for _, hwid := range []string{"device-one", "device-two", "device-three"} {
  242. if _, err := svc.EnforceHwidForSubID(rec.SubID, HwidRequest{Hwid: hwid}); err != nil {
  243. t.Fatalf("register %s: %v", hwid, err)
  244. }
  245. }
  246. status, found, err = svc.HwidSlotStatusForSubID(rec.SubID)
  247. if err != nil || !found {
  248. t.Fatalf("shared subId = (%+v, %v, %v), want found=true", status, found, err)
  249. }
  250. if want := (HwidSlotStatus{Active: true, Limit: 3, Registered: 3, Full: true}); status != want {
  251. t.Fatalf("full slots = %+v, want %+v", status, want)
  252. }
  253. // Deleting the highest-limit client drops the effective limit below the
  254. // registered count, and remaining must clamp at zero instead of going negative.
  255. if err := db.Where("email = ?", "[email protected]").Delete(&model.ClientRecord{}).Error; err != nil {
  256. t.Fatalf("delete second client: %v", err)
  257. }
  258. status, _, err = svc.HwidSlotStatusForSubID(rec.SubID)
  259. if err != nil {
  260. t.Fatalf("lowered limit: %v", err)
  261. }
  262. if want := (HwidSlotStatus{Active: true, Limit: 1, Registered: 3, Remaining: 0, Full: true}); status != want {
  263. t.Fatalf("over-limit slots = %+v, want %+v", status, want)
  264. }
  265. if err := db.Model(&model.ClientRecord{}).Where("sub_id = ?", rec.SubID).UpdateColumn("enable", false).Error; err != nil {
  266. t.Fatalf("disable clients: %v", err)
  267. }
  268. status, found, err = svc.HwidSlotStatusForSubID(rec.SubID)
  269. if err != nil || found || status != (HwidSlotStatus{}) {
  270. t.Fatalf("disabled subId = (%+v, %v, %v), want zero status and found=false", status, found, err)
  271. }
  272. }