harness_test.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. // Package nodee2e drives a real master panel and a real node panel, each its own
  2. // process, through the node-sync paths. Gated by XUI_NODE_E2E_BINARY.
  3. package nodee2e
  4. import (
  5. "bytes"
  6. "encoding/json"
  7. "fmt"
  8. "io"
  9. "net"
  10. "net/http"
  11. "os"
  12. "os/exec"
  13. "path/filepath"
  14. "regexp"
  15. "strconv"
  16. "strings"
  17. "testing"
  18. "time"
  19. "github.com/mhsanaei/3x-ui/v3/internal/database"
  20. )
  21. const settleTimeout = 30 * time.Second
  22. func panelBinary(t *testing.T) string {
  23. t.Helper()
  24. bin := os.Getenv("XUI_NODE_E2E_BINARY")
  25. if bin == "" {
  26. t.Skip("XUI_NODE_E2E_BINARY not set; run `make node-e2e`")
  27. }
  28. abs, err := filepath.Abs(bin)
  29. if err != nil {
  30. t.Fatalf("resolve %s: %v", bin, err)
  31. }
  32. return abs
  33. }
  34. type panel struct {
  35. t *testing.T
  36. name string
  37. bin string
  38. dir string
  39. port int
  40. token string
  41. cmd *exec.Cmd
  42. logOut *os.File
  43. }
  44. func freePort(t *testing.T) int {
  45. t.Helper()
  46. l, err := net.Listen("tcp", "127.0.0.1:0")
  47. if err != nil {
  48. t.Fatalf("free port: %v", err)
  49. }
  50. defer l.Close()
  51. return l.Addr().(*net.TCPAddr).Port
  52. }
  53. func (p *panel) env() []string {
  54. return append(os.Environ(),
  55. "XUI_DB_FOLDER="+filepath.Join(p.dir, "db"),
  56. "XUI_LOG_FOLDER="+filepath.Join(p.dir, "log"),
  57. "XUI_BIN_FOLDER="+filepath.Join(p.dir, "bin"),
  58. "XUI_ENABLE_FAIL2BAN=false",
  59. "MSYS_NO_PATHCONV=1",
  60. )
  61. }
  62. func (p *panel) cli(args ...string) string {
  63. p.t.Helper()
  64. cmd := exec.Command(p.bin, args...)
  65. cmd.Env = p.env()
  66. out, err := cmd.CombinedOutput()
  67. if err != nil {
  68. p.t.Fatalf("%s %v: %v\n%s", p.name, args, err, out)
  69. }
  70. return string(out)
  71. }
  72. var apiTokenLine = regexp.MustCompile(`(?m)^apiToken:\s*(\S+)`)
  73. func (p *panel) mintToken(name, scope string) string {
  74. p.t.Helper()
  75. out := p.cli("setting", "-getApiToken", "-tokenName", name, "-tokenScope", scope)
  76. m := apiTokenLine.FindStringSubmatch(out)
  77. if m == nil {
  78. p.t.Fatalf("%s: no apiToken in output:\n%s", p.name, out)
  79. }
  80. return m[1]
  81. }
  82. // newPanel prepares a panel's database: credentials, a private port, its own
  83. // sub-server port (two panels on one host would race for 2096) and an admin token.
  84. func newPanel(t *testing.T, bin, name string) *panel {
  85. t.Helper()
  86. p := &panel{t: t, name: name, bin: bin, dir: t.TempDir(), port: freePort(t)}
  87. for _, d := range []string{"db", "log", "bin"} {
  88. if err := os.MkdirAll(filepath.Join(p.dir, d), 0o755); err != nil {
  89. t.Fatal(err)
  90. }
  91. }
  92. p.cli("setting", "-username", "e2e", "-password", "e2e-pass", "-port", strconv.Itoa(p.port), "-webBasePath", "/")
  93. if err := database.InitDB(filepath.Join(p.dir, "db", "x-ui.db")); err != nil {
  94. t.Fatalf("%s: open db: %v", name, err)
  95. }
  96. db := database.GetDB()
  97. db.Exec("DELETE FROM settings WHERE key = ?", "subPort")
  98. if err := db.Exec("INSERT INTO settings(key, value) VALUES (?, ?)", "subPort", strconv.Itoa(freePort(t))).Error; err != nil {
  99. t.Fatalf("%s: set subPort: %v", name, err)
  100. }
  101. if err := database.CloseDB(); err != nil {
  102. t.Fatalf("%s: close db: %v", name, err)
  103. }
  104. p.token = p.mintToken("e2e-driver", "admin")
  105. t.Cleanup(p.stop)
  106. return p
  107. }
  108. func (p *panel) start() {
  109. p.t.Helper()
  110. logOut, err := os.OpenFile(filepath.Join(p.dir, "stdout.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
  111. if err != nil {
  112. p.t.Fatal(err)
  113. }
  114. p.logOut = logOut
  115. p.cmd = exec.Command(p.bin, "run")
  116. p.cmd.Env = p.env()
  117. p.cmd.Stdout = logOut
  118. p.cmd.Stderr = logOut
  119. if err := p.cmd.Start(); err != nil {
  120. p.t.Fatalf("%s: start: %v", p.name, err)
  121. }
  122. eventually(p.t, settleTimeout, p.name+" answers /server/status", func() (bool, string) {
  123. env, err := p.try(http.MethodGet, "/panel/api/server/status", nil)
  124. if err != nil {
  125. return false, err.Error()
  126. }
  127. return env.Success, env.Msg
  128. })
  129. }
  130. func (p *panel) stop() {
  131. if p.cmd == nil || p.cmd.Process == nil {
  132. return
  133. }
  134. _ = p.cmd.Process.Kill()
  135. _, _ = p.cmd.Process.Wait()
  136. p.cmd = nil
  137. if p.logOut != nil {
  138. _ = p.logOut.Close()
  139. p.logOut = nil
  140. }
  141. if p.t.Failed() {
  142. if b, err := os.ReadFile(filepath.Join(p.dir, "stdout.log")); err == nil {
  143. tail := string(b)
  144. if len(tail) > 6000 {
  145. tail = tail[len(tail)-6000:]
  146. }
  147. p.t.Logf("---- %s stdout tail ----\n%s", p.name, tail)
  148. }
  149. }
  150. }
  151. // deleteInboundRow simulates a node that lost an inbound (restore, reinstall)
  152. // while stopped; it must not run against a live panel.
  153. func (p *panel) deleteInboundRow(id int) {
  154. p.t.Helper()
  155. if p.cmd != nil {
  156. p.t.Fatalf("%s: deleteInboundRow on a running panel", p.name)
  157. }
  158. if err := database.InitDB(filepath.Join(p.dir, "db", "x-ui.db")); err != nil {
  159. p.t.Fatalf("%s: open db: %v", p.name, err)
  160. }
  161. defer func() { _ = database.CloseDB() }()
  162. db := database.GetDB()
  163. for _, q := range []string{"DELETE FROM client_inbounds WHERE inbound_id = ?", "DELETE FROM client_traffics WHERE inbound_id = ?", "DELETE FROM inbounds WHERE id = ?"} {
  164. if err := db.Exec(q, id).Error; err != nil {
  165. p.t.Fatalf("%s: %s: %v", p.name, q, err)
  166. }
  167. }
  168. }
  169. func (p *panel) url() string { return "http://127.0.0.1:" + strconv.Itoa(p.port) }
  170. type envelope struct {
  171. Success bool `json:"success"`
  172. Msg string `json:"msg"`
  173. Obj json.RawMessage `json:"obj"`
  174. }
  175. func (p *panel) try(method, path string, body any) (*envelope, error) {
  176. var rd io.Reader
  177. if body != nil {
  178. b, err := json.Marshal(body)
  179. if err != nil {
  180. return nil, err
  181. }
  182. rd = bytes.NewReader(b)
  183. }
  184. req, err := http.NewRequest(method, p.url()+path, rd)
  185. if err != nil {
  186. return nil, err
  187. }
  188. req.Header.Set("Authorization", "Bearer "+p.token)
  189. if body != nil {
  190. req.Header.Set("Content-Type", "application/json")
  191. }
  192. resp, err := (&http.Client{Timeout: 20 * time.Second}).Do(req)
  193. if err != nil {
  194. return nil, err
  195. }
  196. defer resp.Body.Close()
  197. raw, err := io.ReadAll(resp.Body)
  198. if err != nil {
  199. return nil, err
  200. }
  201. if resp.StatusCode != http.StatusOK {
  202. return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, raw)
  203. }
  204. var env envelope
  205. if err := json.Unmarshal(raw, &env); err != nil {
  206. return nil, fmt.Errorf("decode %s: %w (%s)", path, err, raw)
  207. }
  208. return &env, nil
  209. }
  210. // call fails the test on transport errors or success:false.
  211. func (p *panel) call(method, path string, body any) json.RawMessage {
  212. p.t.Helper()
  213. env, err := p.try(method, path, body)
  214. if err != nil {
  215. p.t.Fatalf("%s %s %s: %v", p.name, method, path, err)
  216. }
  217. if !env.Success {
  218. p.t.Fatalf("%s %s %s: success=false msg=%q", p.name, method, path, env.Msg)
  219. }
  220. return env.Obj
  221. }
  222. func eventually(t *testing.T, timeout time.Duration, what string, check func() (bool, string)) {
  223. t.Helper()
  224. deadline := time.Now().Add(timeout)
  225. last := ""
  226. for {
  227. ok, detail := check()
  228. if ok {
  229. return
  230. }
  231. last = detail
  232. if time.Now().After(deadline) {
  233. t.Fatalf("timed out after %s waiting for %s; last: %s", timeout, what, last)
  234. }
  235. time.Sleep(500 * time.Millisecond)
  236. }
  237. }
  238. // inboundView is the subset of an inbound row the scenarios assert on.
  239. type inboundView struct {
  240. Id int `json:"id"`
  241. Remark string `json:"remark"`
  242. Enable bool `json:"enable"`
  243. Port int `json:"port"`
  244. Tag string `json:"tag"`
  245. NodeID *int `json:"nodeId"`
  246. Settings json.RawMessage `json:"settings"`
  247. }
  248. type clientEntry map[string]any
  249. func (c clientEntry) email() string { s, _ := c["email"].(string); return s }
  250. func (ib inboundView) clients() []clientEntry {
  251. raw := ib.Settings
  252. var asString string
  253. if json.Unmarshal(raw, &asString) == nil {
  254. raw = json.RawMessage(asString)
  255. }
  256. var s struct {
  257. Clients []clientEntry `json:"clients"`
  258. }
  259. _ = json.Unmarshal(raw, &s)
  260. return s.Clients
  261. }
  262. func (ib inboundView) emails() []string {
  263. out := []string{}
  264. for _, c := range ib.clients() {
  265. out = append(out, c.email())
  266. }
  267. return out
  268. }
  269. func (ib inboundView) client(email string) clientEntry {
  270. for _, c := range ib.clients() {
  271. if strings.EqualFold(c.email(), email) {
  272. return c
  273. }
  274. }
  275. return nil
  276. }
  277. func (p *panel) inbounds() []inboundView {
  278. p.t.Helper()
  279. var list []inboundView
  280. if err := json.Unmarshal(p.call(http.MethodGet, "/panel/api/inbounds/list", nil), &list); err != nil {
  281. p.t.Fatalf("%s: decode inbound list: %v", p.name, err)
  282. }
  283. return list
  284. }
  285. func (p *panel) inboundOnPort(port int) (inboundView, bool) {
  286. p.t.Helper()
  287. for _, ib := range p.inbounds() {
  288. if ib.Port == port {
  289. return ib, true
  290. }
  291. }
  292. return inboundView{}, false
  293. }
  294. const tcpStream = `{"network":"tcp","security":"none","tcpSettings":{"header":{"type":"none"}}}`
  295. func vlessInbound(remark string, port int, nodeID *int, clients ...map[string]any) map[string]any {
  296. if clients == nil {
  297. clients = []map[string]any{}
  298. }
  299. settings, _ := json.Marshal(map[string]any{"clients": clients, "decryption": "none"})
  300. body := map[string]any{
  301. "remark": remark, "enable": true, "port": port, "protocol": "vless",
  302. "settings": string(settings), "streamSettings": tcpStream, "sniffing": `{}`,
  303. }
  304. if nodeID != nil {
  305. body["nodeId"] = *nodeID
  306. }
  307. return body
  308. }
  309. func vlessClient(email string) map[string]any {
  310. return map[string]any{"email": email, "enable": true, "id": newUUID(email)}
  311. }
  312. // newUUID derives a stable, valid UUID from a label so failures are reproducible.
  313. func newUUID(label string) string {
  314. var b [16]byte
  315. copy(b[:], []byte(label+"________________"))
  316. b[6] = (b[6] & 0x0f) | 0x40
  317. b[8] = (b[8] & 0x3f) | 0x80
  318. return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
  319. }