tgbot_client_links_authz_test.go 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. package tgbot
  2. import (
  3. "path/filepath"
  4. "testing"
  5. "time"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
  8. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  9. "github.com/mhsanaei/3x-ui/v3/internal/web/global"
  10. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  11. "github.com/mymmrac/telego"
  12. )
  13. const (
  14. ownerTgID = int64(4242)
  15. ownerMail = "owner@x"
  16. )
  17. // newLinksCallbackTgbot seeds one inbound whose settings bind email to
  18. // ownerTgID, the traffic row the ownership lookup joins on, and a mocked API.
  19. func newLinksCallbackTgbot(t *testing.T, email string) (*Tgbot, func(string) int) {
  20. t.Helper()
  21. mock, calls := staleButtonServer(t, map[string]any{
  22. "answerCallbackQuery": map[string]any{"ok": true, "result": true},
  23. "sendMessage": map[string]any{"ok": true, "result": map[string]any{
  24. "message_id": 1,
  25. "date": 0,
  26. "chat": map[string]any{"id": ownerTgID, "type": "private"},
  27. }},
  28. })
  29. swapTestBot(t, mock.URL)
  30. t.Cleanup(mock.Close)
  31. dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
  32. inbound := &model.Inbound{
  33. UserId: 1,
  34. Remark: "in",
  35. Port: 443,
  36. Protocol: model.VLESS,
  37. Enable: true,
  38. Settings: `{"clients":[{"email":"` + email + `","tgId":4242,"subId":"sub-owned"}]}`,
  39. }
  40. if err := database.GetDB().Create(inbound).Error; err != nil {
  41. t.Fatalf("seed inbound: %v", err)
  42. }
  43. if err := database.GetDB().Create(&xray.ClientTraffic{
  44. InboundId: inbound.Id,
  45. Email: email,
  46. Enable: true,
  47. }).Error; err != nil {
  48. t.Fatalf("seed traffic: %v", err)
  49. }
  50. origRunning := isRunning
  51. t.Cleanup(func() { isRunning = origRunning })
  52. isRunning = true
  53. return &Tgbot{}, calls
  54. }
  55. func tapClientLinks(t *testing.T, tb *Tgbot, tgUserID int64, data string) {
  56. t.Helper()
  57. tb.answerCallback(&telego.CallbackQuery{
  58. ID: "q1",
  59. From: telego.User{ID: tgUserID},
  60. Data: data,
  61. Message: &telego.Message{Chat: telego.Chat{ID: tgUserID}},
  62. }, false)
  63. }
  64. // Regression test: a non-admin tapping a link callback carrying another
  65. // client's email must be refused; without the ownership check it is served.
  66. func TestClientLinkCallbackRefusesForeignClient(t *testing.T) {
  67. tb, calls := newLinksCallbackTgbot(t, ownerMail)
  68. tapClientLinks(t, tb, ownerTgID, "client_sub_links someone-else@x")
  69. if n := calls("sendMessage"); n != 0 {
  70. t.Errorf("sendMessage calls = %d, want 0: a non-admin received a foreign client's links", n)
  71. }
  72. if n := calls("answerCallbackQuery"); n != 1 {
  73. t.Errorf("answerCallbackQuery calls = %d, want 1: the refused tap must be answered", n)
  74. }
  75. }
  76. // The same guard must not lock the owner out of their own links.
  77. func TestClientLinkCallbackServesOwnClient(t *testing.T) {
  78. tb, calls := newLinksCallbackTgbot(t, ownerMail)
  79. tapClientLinks(t, tb, ownerTgID, "client_sub_links "+ownerMail)
  80. if n := calls("sendMessage"); n != 1 {
  81. t.Errorf("sendMessage calls = %d, want 1: the owner must still get its links", n)
  82. }
  83. if n := calls("answerCallbackQuery"); n != 0 {
  84. t.Errorf("answerCallbackQuery calls = %d, want 0: an allowed tap is not refused", n)
  85. }
  86. }
  87. // Regression test: a payload past 64 chars arrives as its hash, so an email long
  88. // enough to be hashed must still be decoded and served to its owner.
  89. func TestHashedLinkCallbackServesOwnClient(t *testing.T) {
  90. const longMail = "[email protected]"
  91. tb, calls := newLinksCallbackTgbot(t, longMail)
  92. origStorage := hashStorage
  93. hashStorage = global.NewHashStorage(20 * time.Minute)
  94. t.Cleanup(func() { hashStorage = origStorage })
  95. data := tb.encodeQuery("client_sub_links " + longMail)
  96. if data == "client_sub_links "+longMail {
  97. t.Fatalf("encodeQuery left %q unhashed; the test needs a hashed payload", data)
  98. }
  99. tapClientLinks(t, tb, ownerTgID, data)
  100. if n := calls("sendMessage"); n != 1 {
  101. t.Errorf("sendMessage calls = %d, want 1: the owner's hashed button must still be served", n)
  102. }
  103. }