node_credentials_writeonly_test.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. package service
  2. import (
  3. "encoding/json"
  4. "strings"
  5. "testing"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  8. )
  9. func TestNodeCredentialsNeverMarshal(t *testing.T) {
  10. raw, err := json.Marshal(&model.Node{
  11. Id: 7,
  12. Name: "node",
  13. ApiToken: "plain-secret-token",
  14. })
  15. if err != nil {
  16. t.Fatalf("marshal node: %v", err)
  17. }
  18. out := string(raw)
  19. if strings.Contains(out, "plain-secret-token") || strings.Contains(out, "apiToken") {
  20. t.Fatalf("model.Node JSON leaked api token field: %s", out)
  21. }
  22. }
  23. func TestNodeViewExposesOnlyCredentialPresence(t *testing.T) {
  24. setupConflictDB(t)
  25. svc := &NodeService{}
  26. reqToken := "write-only-secret"
  27. view, err := svc.CreateFromRequest(&NodeMutationRequest{
  28. Name: "node-view",
  29. Scheme: "https",
  30. Address: "127.0.0.1",
  31. Port: 2096,
  32. ApiToken: &reqToken,
  33. Enable: true,
  34. })
  35. if err != nil {
  36. t.Fatalf("create from request: %v", err)
  37. }
  38. if !view.HasApiToken {
  39. t.Fatal("create view should report credential presence")
  40. }
  41. got, err := svc.GetViewById(view.Id)
  42. if err != nil {
  43. t.Fatalf("get view: %v", err)
  44. }
  45. raw, err := json.Marshal(got)
  46. if err != nil {
  47. t.Fatalf("marshal view: %v", err)
  48. }
  49. out := string(raw)
  50. if !strings.Contains(out, `"hasApiToken":true`) {
  51. t.Fatalf("view does not report credential presence: %s", out)
  52. }
  53. if strings.Contains(out, reqToken) || strings.Contains(out, "apiToken") {
  54. t.Fatalf("NodeView leaked plaintext or apiToken key: %s", out)
  55. }
  56. }
  57. func TestNodeCredentialMutationSemantics(t *testing.T) {
  58. setupConflictDB(t)
  59. svc := &NodeService{}
  60. initial := "initial-token"
  61. view, err := svc.CreateFromRequest(&NodeMutationRequest{
  62. Name: "mut",
  63. Scheme: "https",
  64. Address: "127.0.0.1",
  65. Port: 2096,
  66. ApiToken: &initial,
  67. Enable: true,
  68. })
  69. if err != nil {
  70. t.Fatalf("create: %v", err)
  71. }
  72. before := rawStoredNodeToken(t, view.Id)
  73. if before != initial {
  74. t.Fatalf("stored token = %q, want %q", before, initial)
  75. }
  76. if err := svc.UpdateFromRequest(view.Id, &NodeMutationRequest{
  77. Name: "mut-renamed",
  78. Scheme: "https",
  79. Address: "127.0.0.1",
  80. Port: 2096,
  81. Enable: true,
  82. }); err != nil {
  83. t.Fatalf("keep-token update: %v", err)
  84. }
  85. if after := rawStoredNodeToken(t, view.Id); after != before {
  86. t.Fatalf("omitted token should keep existing token: %q -> %q", before, after)
  87. }
  88. blank := " "
  89. if err := svc.UpdateFromRequest(view.Id, &NodeMutationRequest{
  90. Name: "mut-blank",
  91. Scheme: "https",
  92. Address: "127.0.0.1",
  93. Port: 2096,
  94. ApiToken: &blank,
  95. Enable: true,
  96. }); err != nil {
  97. t.Fatalf("blank apiToken should keep existing token on update: %v", err)
  98. }
  99. if afterBlank := rawStoredNodeToken(t, view.Id); afterBlank != before {
  100. t.Fatalf("blank token should keep existing token: %q -> %q", before, afterBlank)
  101. }
  102. next := "next-token"
  103. if err := svc.UpdateFromRequest(view.Id, &NodeMutationRequest{
  104. Name: "mut",
  105. Scheme: "https",
  106. Address: "127.0.0.1",
  107. Port: 2096,
  108. ApiToken: &next,
  109. Enable: true,
  110. }); err != nil {
  111. t.Fatalf("replace token: %v", err)
  112. }
  113. if replaced := rawStoredNodeToken(t, view.Id); replaced != next {
  114. t.Fatalf("replace token stored %q, want %q", replaced, next)
  115. }
  116. if err := svc.UpdateFromRequest(view.Id, &NodeMutationRequest{
  117. Name: "mut",
  118. Scheme: "https",
  119. Address: "127.0.0.1",
  120. Port: 2096,
  121. ClearApiToken: true,
  122. Enable: true,
  123. }); err == nil {
  124. t.Fatal("enabled non-mtls node must not clear apiToken")
  125. }
  126. if err := svc.UpdateFromRequest(view.Id, &NodeMutationRequest{
  127. Name: "mut",
  128. Scheme: "https",
  129. Address: "127.0.0.1",
  130. Port: 2096,
  131. ClearApiToken: true,
  132. Enable: false,
  133. }); err != nil {
  134. t.Fatalf("clear disabled token: %v", err)
  135. }
  136. if cleared := rawStoredNodeToken(t, view.Id); cleared != "" {
  137. t.Fatalf("clear token left stored value %q", cleared)
  138. }
  139. if _, err := svc.CreateFromRequest(&NodeMutationRequest{
  140. Name: "mtls-only",
  141. Scheme: "https",
  142. Address: "127.0.0.1",
  143. Port: 2097,
  144. Enable: true,
  145. TlsVerifyMode: "mtls",
  146. }); err != nil {
  147. t.Fatalf("mtls create without token: %v", err)
  148. }
  149. }
  150. func TestNodeUpdateRequiresTokenWhenNoStoredTokenAndMtlsDisabled(t *testing.T) {
  151. setupConflictDB(t)
  152. svc := &NodeService{}
  153. view, err := svc.CreateFromRequest(&NodeMutationRequest{
  154. Name: "mtls-empty",
  155. Scheme: "https",
  156. Address: "127.0.0.1",
  157. Port: 2098,
  158. Enable: true,
  159. TlsVerifyMode: "mtls",
  160. })
  161. if err != nil {
  162. t.Fatalf("create mtls node: %v", err)
  163. }
  164. blank := ""
  165. if err := svc.UpdateFromRequest(view.Id, &NodeMutationRequest{
  166. Name: "mtls-empty",
  167. Scheme: "https",
  168. Address: "127.0.0.1",
  169. Port: 2098,
  170. ApiToken: &blank,
  171. Enable: true,
  172. BasePath: "/",
  173. OutboundTag: "",
  174. }); err == nil {
  175. t.Fatal("enabled non-mtls node without stored token must be rejected")
  176. }
  177. }
  178. func rawStoredNodeToken(t *testing.T, id int) string {
  179. t.Helper()
  180. var n model.Node
  181. if err := database.GetDB().Select("api_token").Where("id = ?", id).First(&n).Error; err != nil {
  182. t.Fatalf("load raw node token: %v", err)
  183. }
  184. return n.ApiToken
  185. }