mtproto_fake_test.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. package service
  2. import (
  3. "fmt"
  4. "net"
  5. "net/http"
  6. "net/url"
  7. "os"
  8. "path/filepath"
  9. "regexp"
  10. "slices"
  11. "strings"
  12. "testing"
  13. "time"
  14. "github.com/mhsanaei/3x-ui/v3/internal/database"
  15. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  16. "github.com/mhsanaei/3x-ui/v3/internal/mtproto"
  17. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  18. )
  19. const (
  20. mtprotoTestSecretA = "ee00112233445566778899aabbccddeeff6578616d706c652e636f6d"
  21. mtprotoTestSecretB = "ee101112131415161718191a1b1c1d1e1f6578616d706c652e636f6d"
  22. mtprotoTestSecretC = "ee202122232425262728292a2b2c2d2e2f6578616d706c652e636f6d"
  23. mtprotoTestSecretD = "ee303132333435363738393a3b3c3d3e3f6578616d706c652e636f6d"
  24. )
  25. func seedClientTraffic(t *testing.T, inboundId int, email string, enable bool) {
  26. t.Helper()
  27. row := xray.ClientTraffic{InboundId: inboundId, Email: email, Enable: enable}
  28. if err := database.GetDB().Create(&row).Error; err != nil {
  29. t.Fatalf("seed traffic %s: %v", email, err)
  30. }
  31. }
  32. func loadInboundByTag(t *testing.T, tag string) *model.Inbound {
  33. t.Helper()
  34. var ib model.Inbound
  35. if err := database.GetDB().Where("tag = ?", tag).First(&ib).Error; err != nil {
  36. t.Fatalf("load inbound %s: %v", tag, err)
  37. }
  38. return &ib
  39. }
  40. // fakeMtgChildMain is what the re-executed test binary runs when posing as an
  41. // mtg child process: it appends its pid to the file named by MTG_FAKE_PIDFILE
  42. // so tests can count spawns, then blocks until the manager kills it.
  43. func fakeMtgChildMain() {
  44. if f, err := os.OpenFile(os.Getenv("MTG_FAKE_PIDFILE"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644); err == nil {
  45. fmt.Fprintf(f, "%d\n", os.Getpid())
  46. f.Close()
  47. }
  48. if logPath := os.Getenv("MTG_FAKE_APILOG"); logPath != "" && len(os.Args) > 2 {
  49. go serveFakeMtgAPI(os.Args[len(os.Args)-1], logPath)
  50. }
  51. select {}
  52. }
  53. // serveFakeMtgAPI answers the management API on the config's api-bind-to and
  54. // logs each reset-quota call, so a test sees which sidecar quotas were zeroed.
  55. func serveFakeMtgAPI(configPath, logPath string) {
  56. cfg, err := os.ReadFile(configPath)
  57. if err != nil {
  58. return
  59. }
  60. m := regexp.MustCompile(`api-bind-to = "([^"]+)"`).FindSubmatch(cfg)
  61. if m == nil {
  62. return
  63. }
  64. ln, err := net.Listen("tcp", string(m[1]))
  65. if err != nil {
  66. return
  67. }
  68. appendFakeMtgLog(logPath, "ready")
  69. _ = http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  70. if name, ok := strings.CutSuffix(strings.TrimPrefix(r.URL.Path, "/secrets/"), "/reset-quota"); ok && r.Method == http.MethodPost {
  71. if unescaped, err := url.PathUnescape(name); err == nil {
  72. appendFakeMtgLog(logPath, "reset:"+unescaped)
  73. }
  74. }
  75. _, _ = w.Write([]byte("{}"))
  76. }))
  77. }
  78. func appendFakeMtgLog(path, line string) {
  79. if f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644); err == nil {
  80. fmt.Fprintln(f, line)
  81. f.Close()
  82. }
  83. }
  84. // installFakeMtgAPI is installFakeMtg whose children also serve the management
  85. // API; it returns the pid file and the API call log.
  86. func installFakeMtgAPI(t *testing.T) (string, string) {
  87. t.Helper()
  88. pidFile := installFakeMtg(t)
  89. logPath := filepath.Join(filepath.Dir(pidFile), "mtg-api.log")
  90. t.Setenv("MTG_FAKE_APILOG", logPath)
  91. return pidFile, logPath
  92. }
  93. func fakeMtgLog(t *testing.T, logPath string) []string {
  94. t.Helper()
  95. data, err := os.ReadFile(logPath)
  96. if os.IsNotExist(err) {
  97. return nil
  98. }
  99. if err != nil {
  100. t.Fatalf("read mtg api log: %v", err)
  101. }
  102. return strings.Fields(string(data))
  103. }
  104. // waitFakeMtgLog polls until the log holds want, failing on timeout.
  105. func waitFakeMtgLog(t *testing.T, logPath, want string) {
  106. t.Helper()
  107. deadline := time.Now().Add(5 * time.Second)
  108. for !slices.Contains(fakeMtgLog(t, logPath), want) {
  109. if time.Now().After(deadline) {
  110. t.Fatalf("mtg api log never recorded %q: %v", want, fakeMtgLog(t, logPath))
  111. }
  112. time.Sleep(20 * time.Millisecond)
  113. }
  114. }
  115. // installFakeMtg points the mtproto manager at a copy of the running test
  116. // binary posing as mtg (via the MTG_FAKE_CHILD gate in TestMain) and returns
  117. // the pid file whose line count equals the number of processes spawned so far.
  118. func installFakeMtg(t *testing.T) string {
  119. t.Helper()
  120. binDir := t.TempDir()
  121. self, err := os.Executable()
  122. if err != nil {
  123. t.Fatalf("locate test binary: %v", err)
  124. }
  125. payload, err := os.ReadFile(self)
  126. if err != nil {
  127. t.Fatalf("read test binary: %v", err)
  128. }
  129. if err := os.WriteFile(filepath.Join(binDir, mtproto.GetBinaryName()), payload, 0o755); err != nil {
  130. t.Fatalf("install fake mtg: %v", err)
  131. }
  132. pidFile := filepath.Join(binDir, "mtg-pids.txt")
  133. t.Setenv("XUI_BIN_FOLDER", binDir)
  134. t.Setenv("MTG_FAKE_CHILD", "1")
  135. t.Setenv("MTG_FAKE_PIDFILE", pidFile)
  136. return pidFile
  137. }
  138. func countSpawns(t *testing.T, pidFile string) int {
  139. t.Helper()
  140. data, err := os.ReadFile(pidFile)
  141. if os.IsNotExist(err) {
  142. return 0
  143. }
  144. if err != nil {
  145. t.Fatalf("read pid file: %v", err)
  146. }
  147. return len(strings.Fields(string(data)))
  148. }
  149. // waitForSpawns polls until exactly want processes have registered, failing
  150. // fast when the count overshoots and on timeout.
  151. func waitForSpawns(t *testing.T, pidFile string, want int) {
  152. t.Helper()
  153. deadline := time.Now().Add(5 * time.Second)
  154. for {
  155. got := countSpawns(t, pidFile)
  156. if got == want {
  157. return
  158. }
  159. if got > want {
  160. t.Fatalf("expected %d mtg spawn(s), got %d", want, got)
  161. }
  162. if time.Now().After(deadline) {
  163. t.Fatalf("expected %d mtg spawn(s), still %d after timeout", want, got)
  164. }
  165. time.Sleep(20 * time.Millisecond)
  166. }
  167. }
  168. // assertNoNewSpawns gives a wrongly spawned child time to register, then
  169. // asserts the spawn count is still exactly want.
  170. func assertNoNewSpawns(t *testing.T, pidFile string, want int) {
  171. t.Helper()
  172. time.Sleep(500 * time.Millisecond)
  173. if got := countSpawns(t, pidFile); got != want {
  174. t.Fatalf("expected the mtg process to be kept (%d spawn(s)), got %d", want, got)
  175. }
  176. }