setting_security_test.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. package service
  2. import (
  3. "path/filepath"
  4. "regexp"
  5. "testing"
  6. "time"
  7. "github.com/xlzd/gotp"
  8. "github.com/mhsanaei/3x-ui/v3/internal/database"
  9. "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
  10. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  11. )
  12. func TestResetSettingsRegeneratesSubscriptionPaths(t *testing.T) {
  13. setupSettingTestDB(t)
  14. s := &SettingService{}
  15. for key, value := range map[string]string{
  16. "subPath": "/sub/",
  17. "subJsonPath": "/json/",
  18. "subClashPath": "/clash/",
  19. "webPort": "8443",
  20. } {
  21. if err := s.saveSetting(key, value); err != nil {
  22. t.Fatalf("save %s: %v", key, err)
  23. }
  24. }
  25. if err := s.ResetSettings(); err != nil {
  26. t.Fatalf("ResetSettings: %v", err)
  27. }
  28. pathPattern := regexp.MustCompile(`^/[0-9a-z]{16}/$`)
  29. paths := map[string]string{}
  30. for _, key := range []string{"subPath", "subJsonPath", "subClashPath"} {
  31. value, err := s.getString(key)
  32. if err != nil {
  33. t.Fatalf("read %s: %v", key, err)
  34. }
  35. if !pathPattern.MatchString(value) {
  36. t.Errorf("%s = %q, want /<16 lowercase alphanumeric characters>/", key, value)
  37. }
  38. paths[key] = value
  39. }
  40. if paths["subPath"] == paths["subJsonPath"] || paths["subPath"] == paths["subClashPath"] || paths["subJsonPath"] == paths["subClashPath"] {
  41. t.Fatalf("subscription paths must be distinct: %v", paths)
  42. }
  43. if port, err := s.GetPort(); err != nil || port != 2053 {
  44. t.Fatalf("web port after reset = %d, %v; want 2053", port, err)
  45. }
  46. }
  47. func setupSettingTestDB(t *testing.T) {
  48. t.Helper()
  49. dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
  50. }
  51. func TestGetAllSettingViewRedactsSecrets(t *testing.T) {
  52. setupSettingTestDB(t)
  53. s := &SettingService{}
  54. if err := s.saveSetting("tgBotToken", "telegram-secret"); err != nil {
  55. t.Fatal(err)
  56. }
  57. if err := s.saveSetting("twoFactorToken", "totp-secret"); err != nil {
  58. t.Fatal(err)
  59. }
  60. if err := s.saveSetting("ldapPassword", "ldap-secret"); err != nil {
  61. t.Fatal(err)
  62. }
  63. if err := s.saveSetting("smtpPassword", "smtp-secret"); err != nil {
  64. t.Fatal(err)
  65. }
  66. if err := s.saveSetting("discordBotToken", "discord-secret"); err != nil {
  67. t.Fatal(err)
  68. }
  69. if err := database.GetDB().Create(&model.ApiToken{Name: "test", Token: "api-secret", Enabled: true}).Error; err != nil {
  70. t.Fatal(err)
  71. }
  72. view, err := s.GetAllSettingView()
  73. if err != nil {
  74. t.Fatal(err)
  75. }
  76. if view.TgBotToken != "" || view.TwoFactorToken != "" || view.LdapPassword != "" || view.SmtpPassword != "" || view.DiscordBotToken != "" {
  77. t.Fatalf("settings view leaked secrets: %#v", view)
  78. }
  79. if !view.HasTgBotToken || !view.HasTwoFactorToken || !view.HasLdapPassword || !view.HasApiToken || !view.HasSmtpPassword || !view.HasDiscordBotToken {
  80. t.Fatalf("settings view did not report configured secret flags: %#v", view)
  81. }
  82. }
  83. func TestUpdateAllSettingPreservesRedactedSecrets(t *testing.T) {
  84. setupSettingTestDB(t)
  85. s := &SettingService{}
  86. if err := s.saveSetting("tgBotToken", "telegram-secret"); err != nil {
  87. t.Fatal(err)
  88. }
  89. if err := s.saveSetting("ldapPassword", "ldap-secret"); err != nil {
  90. t.Fatal(err)
  91. }
  92. if err := s.saveSetting("twoFactorEnable", "true"); err != nil {
  93. t.Fatal(err)
  94. }
  95. if err := s.saveSetting("twoFactorToken", "totp-secret"); err != nil {
  96. t.Fatal(err)
  97. }
  98. if err := s.saveSetting("smtpPassword", "smtp-secret"); err != nil {
  99. t.Fatal(err)
  100. }
  101. if err := s.saveSetting("discordBotToken", "discord-secret"); err != nil {
  102. t.Fatal(err)
  103. }
  104. view, err := s.GetAllSettingView()
  105. if err != nil {
  106. t.Fatal(err)
  107. }
  108. settings := &view.AllSetting
  109. if err := s.UpdateAllSetting(settings, SecretClears{}); err != nil {
  110. t.Fatal(err)
  111. }
  112. if got, _ := s.GetTgBotToken(); got != "telegram-secret" {
  113. t.Fatalf("tg token = %q, want preserved secret", got)
  114. }
  115. if got, _ := s.GetLdapPassword(); got != "ldap-secret" {
  116. t.Fatalf("ldap password = %q, want preserved secret", got)
  117. }
  118. if got, _ := s.GetTwoFactorToken(); got != "totp-secret" {
  119. t.Fatalf("2fa token = %q, want preserved secret", got)
  120. }
  121. if got, _ := s.GetSmtpPassword(); got != "smtp-secret" {
  122. t.Fatalf("smtp password = %q, want preserved secret", got)
  123. }
  124. if got, _ := s.GetDiscordBotToken(); got != "discord-secret" {
  125. t.Fatalf("discord token = %q, want preserved secret", got)
  126. }
  127. }
  128. func TestUpdateAllSettingClearsFlaggedSecrets(t *testing.T) {
  129. setupSettingTestDB(t)
  130. s := &SettingService{}
  131. if err := s.saveSetting("tgBotToken", "telegram-secret"); err != nil {
  132. t.Fatal(err)
  133. }
  134. if err := s.saveSetting("ldapPassword", "ldap-secret"); err != nil {
  135. t.Fatal(err)
  136. }
  137. if err := s.saveSetting("smtpPassword", "smtp-secret"); err != nil {
  138. t.Fatal(err)
  139. }
  140. if err := s.saveSetting("discordBotToken", "discord-secret"); err != nil {
  141. t.Fatal(err)
  142. }
  143. view, err := s.GetAllSettingView()
  144. if err != nil {
  145. t.Fatal(err)
  146. }
  147. if err := s.UpdateAllSetting(&view.AllSetting, SecretClears{SmtpPassword: true}); err != nil {
  148. t.Fatal(err)
  149. }
  150. if got, _ := s.GetSmtpPassword(); got != "" {
  151. t.Fatalf("smtp password = %q, want cleared", got)
  152. }
  153. if got, _ := s.GetTgBotToken(); got != "telegram-secret" {
  154. t.Fatalf("tg token = %q, unflagged secret must stay preserved", got)
  155. }
  156. if got, _ := s.GetLdapPassword(); got != "ldap-secret" {
  157. t.Fatalf("ldap password = %q, unflagged secret must stay preserved", got)
  158. }
  159. if got, _ := s.GetDiscordBotToken(); got != "discord-secret" {
  160. t.Fatalf("discord token = %q, unflagged secret must stay preserved", got)
  161. }
  162. view, err = s.GetAllSettingView()
  163. if err != nil {
  164. t.Fatal(err)
  165. }
  166. if view.HasSmtpPassword {
  167. t.Fatal("hasSmtpPassword must report false after clearing")
  168. }
  169. if err := s.UpdateAllSetting(&view.AllSetting, SecretClears{TgBotToken: true, LdapPassword: true, DiscordBotToken: true}); err != nil {
  170. t.Fatal(err)
  171. }
  172. if got, _ := s.GetTgBotToken(); got != "" {
  173. t.Fatalf("tg token = %q, want cleared", got)
  174. }
  175. if got, _ := s.GetLdapPassword(); got != "" {
  176. t.Fatalf("ldap password = %q, want cleared", got)
  177. }
  178. if got, _ := s.GetDiscordBotToken(); got != "" {
  179. t.Fatalf("discord token = %q, want cleared", got)
  180. }
  181. view, err = s.GetAllSettingView()
  182. if err != nil {
  183. t.Fatal(err)
  184. }
  185. if view.HasDiscordBotToken {
  186. t.Fatal("hasDiscordBotToken must report false after clearing")
  187. }
  188. }
  189. func TestSanitizePublicHTTPURLBlocksPrivateAddressUnlessAllowed(t *testing.T) {
  190. if _, err := SanitizePublicHTTPURL("http://127.0.0.1:8080/hook", false); err == nil {
  191. t.Fatal("expected localhost URL to be blocked")
  192. }
  193. if got, err := SanitizePublicHTTPURL("http://127.0.0.1:8080/hook", true); err != nil || got != "http://127.0.0.1:8080/hook" {
  194. t.Fatalf("allowPrivate result = %q, %v", got, err)
  195. }
  196. }
  197. func TestVerifyTwoFactorCode(t *testing.T) {
  198. setupSettingTestDB(t)
  199. s := &SettingService{}
  200. if err := s.saveSetting("twoFactorEnable", "true"); err != nil {
  201. t.Fatal(err)
  202. }
  203. const token = "JBSWY3DPEHPK3PXP"
  204. if err := s.saveSetting("twoFactorToken", token); err != nil {
  205. t.Fatal(err)
  206. }
  207. if err := s.VerifyTwoFactorCode(gotp.NewDefaultTOTP(token).Now()); err != nil {
  208. t.Fatalf("valid code rejected: %v", err)
  209. }
  210. if err := s.VerifyTwoFactorCode(gotp.NewDefaultTOTP(token).AtTime(time.Now().Add(-30 * time.Second))); err != nil {
  211. t.Fatalf("previous window code rejected: %v", err)
  212. }
  213. if err := s.VerifyTwoFactorCode("000000"); err == nil {
  214. t.Fatal("invalid code accepted")
  215. }
  216. }
  217. func TestGetSecret_FallbacksOnEmptyDatabaseSetting(t *testing.T) {
  218. setupSettingTestDB(t)
  219. s := &SettingService{}
  220. if err := s.saveSetting("secret", ""); err != nil {
  221. t.Fatal(err)
  222. }
  223. secretBytes, err := s.GetSecret()
  224. if err != nil {
  225. t.Fatalf("unexpected error: %v", err)
  226. }
  227. secret := string(secretBytes)
  228. if len(secret) != 32 {
  229. t.Fatalf("secret length = %d, want 32-byte non-empty string", len(secret))
  230. }
  231. persisted, err := s.getString("secret")
  232. if err != nil {
  233. t.Fatalf("failed to retrieve persisted secret: %v", err)
  234. }
  235. if persisted != secret {
  236. t.Fatalf("persisted secret = %q, want %q", persisted, secret)
  237. }
  238. }