outbound_subscription_test.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. package service
  2. import (
  3. "testing"
  4. "github.com/mhsanaei/3x-ui/v3/database/model"
  5. "github.com/mhsanaei/3x-ui/v3/util/link"
  6. )
  7. func TestDefaultPrefixNumber(t *testing.T) {
  8. mk := func(id int, prefix string) *model.OutboundSubscription {
  9. return &model.OutboundSubscription{Id: id, TagPrefix: prefix}
  10. }
  11. cases := []struct {
  12. name string
  13. subs []*model.OutboundSubscription
  14. excludeId int
  15. want int
  16. }{
  17. {"no subscriptions starts at 1", nil, 0, 1},
  18. {"sequential prefixes give the next", []*model.OutboundSubscription{mk(1, "sub1-"), mk(2, "sub2-")}, 0, 3},
  19. {"reuses the lowest freed number", []*model.OutboundSubscription{mk(2, "sub2-")}, 0, 1},
  20. {"legacy blank prefix reserves its id", []*model.OutboundSubscription{mk(1, ""), mk(5, "sub3-")}, 0, 2},
  21. {"custom prefixes are ignored", []*model.OutboundSubscription{mk(1, "hk-"), mk(2, "jp-")}, 0, 1},
  22. {"excludes the edited subscription", []*model.OutboundSubscription{mk(5, "sub2-")}, 5, 1},
  23. }
  24. for _, c := range cases {
  25. t.Run(c.name, func(t *testing.T) {
  26. if got := defaultPrefixNumber(c.subs, c.excludeId); got != c.want {
  27. t.Fatalf("got %d, want %d", got, c.want)
  28. }
  29. })
  30. }
  31. }
  32. func TestAssignStableTags(t *testing.T) {
  33. t.Run("reuses the tag mapped to a known identity", func(t *testing.T) {
  34. parsed := []link.Outbound{{"tag": "JP-Tokyo"}}
  35. prev := map[string]string{"id-abc": "sub1-keepme"}
  36. got := assignStableTags(parsed, []string{"id-abc"}, prev, nil, 1, "")
  37. if got[0] != "sub1-keepme" {
  38. t.Fatalf("got %q, want sub1-keepme", got[0])
  39. }
  40. if parsed[0]["tag"] != "sub1-keepme" {
  41. t.Fatalf("tag was not written back into the outbound: %v", parsed[0]["tag"])
  42. }
  43. })
  44. t.Run("falls back to the previous tag at the same position", func(t *testing.T) {
  45. parsed := []link.Outbound{{"tag": "JP-Tokyo"}}
  46. got := assignStableTags(parsed, []string{"id-new"}, map[string]string{}, map[int]string{0: "sub1-oldpos"}, 1, "")
  47. if got[0] != "sub1-oldpos" {
  48. t.Fatalf("got %q, want sub1-oldpos", got[0])
  49. }
  50. })
  51. t.Run("allocates a fresh tag with the default sub<id>- prefix", func(t *testing.T) {
  52. parsed := []link.Outbound{{"tag": "Tokyo"}}
  53. got := assignStableTags(parsed, []string{"id-x"}, nil, nil, 7, "")
  54. want := link.SuggestTag("sub7-", "Tokyo", 0)
  55. if got[0] != want {
  56. t.Fatalf("got %q, want %q", got[0], want)
  57. }
  58. })
  59. t.Run("uses a custom prefix for fresh tags", func(t *testing.T) {
  60. parsed := []link.Outbound{{"tag": "Tokyo"}}
  61. got := assignStableTags(parsed, []string{"id-x"}, nil, nil, 1, "hk-")
  62. want := link.SuggestTag("hk-", "Tokyo", 0)
  63. if got[0] != want {
  64. t.Fatalf("got %q, want %q", got[0], want)
  65. }
  66. })
  67. t.Run("disambiguates colliding tags with a -N suffix", func(t *testing.T) {
  68. parsed := []link.Outbound{{"tag": "Same"}, {"tag": "Same"}}
  69. got := assignStableTags(parsed, []string{"id1", "id2"}, nil, nil, 1, "p-")
  70. base := link.SuggestTag("p-", "Same", 0)
  71. if got[0] != base {
  72. t.Fatalf("got[0] = %q, want %q", got[0], base)
  73. }
  74. if got[1] != base+"-1" {
  75. t.Fatalf("got[1] = %q, want %q", got[1], base+"-1")
  76. }
  77. })
  78. }
  79. // TestSanitizePublicHTTPURLRejectsPrivateAndBadSchemes covers the SSRF guard used
  80. // when fetching subscription URLs. All rejected cases use literal IPs or bad
  81. // schemes so the test never performs real DNS resolution.
  82. func TestSanitizePublicHTTPURLRejectsPrivateAndBadSchemes(t *testing.T) {
  83. rejected := []string{
  84. "http://127.0.0.1/sub", // loopback
  85. "http://10.0.0.1/x", // private
  86. "http://192.168.1.1", // private
  87. "http://169.254.169.254/latest/meta-data", // link-local (cloud metadata)
  88. "http://[::1]:8080/sub", // IPv6 loopback
  89. "http://0.0.0.0", // unspecified
  90. "ftp://example.com/x", // unsupported scheme
  91. "file:///etc/passwd", // unsupported scheme
  92. }
  93. for _, raw := range rejected {
  94. if _, err := SanitizePublicHTTPURL(raw, false); err == nil {
  95. t.Errorf("expected %q to be rejected, got nil error", raw)
  96. }
  97. }
  98. t.Run("allows a public literal IP without DNS", func(t *testing.T) {
  99. got, err := SanitizePublicHTTPURL("http://8.8.8.8/sub", false)
  100. if err != nil {
  101. t.Fatalf("unexpected error: %v", err)
  102. }
  103. if got != "http://8.8.8.8/sub" {
  104. t.Fatalf("got %q, want http://8.8.8.8/sub", got)
  105. }
  106. })
  107. }