incy_test.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. package sub
  2. import (
  3. "net/http"
  4. "net/http/httptest"
  5. "strings"
  6. "testing"
  7. "github.com/gin-gonic/gin"
  8. )
  9. func fullIncyConfig() IncyConfig {
  10. return IncyConfig{
  11. AutoDetect: true,
  12. ProfileDescription: "Fast and stable network",
  13. SortOrder: "ping",
  14. SupportEmail: "[email protected]",
  15. AnnounceUrl: "https://t.me/incy_news",
  16. PremiumUrl: "https://example.com/buy",
  17. BannerText: "Summer sale",
  18. BannerButtonText: "Buy now",
  19. BannerButtonUrl: "https://example.com/sale",
  20. BannerBgColor: "#E53E3E",
  21. BannerButtonColor: "#38A169",
  22. HideUrl: "1",
  23. HideCheck: "true",
  24. NoLimitEnabled: "0",
  25. PerAppProxyEnable: "1",
  26. PerAppProxyMode: "bypass",
  27. PerAppProxyList: "com.google.chrome,org.telegram.messenger",
  28. FragmentationEnable: "1",
  29. FragmentationLength: "10-30",
  30. FragmentationInterval: "20-40",
  31. FragmentationPackets: "tlshello",
  32. NoisesEnable: "1",
  33. NoisesType: "rand",
  34. NoisesPacket: "10-20",
  35. NoisesDelay: "10-50",
  36. ServerAddressResolveEnable: "1",
  37. ServerAddressResolveDnsDomain: "https://common.dot.dns.yandex.net/dns-query",
  38. ServerAddressResolveDnsIp: "77.88.8.8",
  39. }
  40. }
  41. func applyIncyToHeaders(t *testing.T, cfg IncyConfig, userAgent string) http.Header {
  42. t.Helper()
  43. gin.SetMode(gin.TestMode)
  44. recorder := httptest.NewRecorder()
  45. ctx, _ := gin.CreateTestContext(recorder)
  46. ctx.Request = httptest.NewRequest(http.MethodGet, "/sub/test", nil)
  47. if userAgent != "" {
  48. ctx.Request.Header.Set("User-Agent", userAgent)
  49. }
  50. ApplyIncyHeaders(ctx, cfg, cfg.AutoDetect && IsIncyClient(ctx.GetHeader("User-Agent")))
  51. return recorder.Header()
  52. }
  53. func TestIsIncyClient(t *testing.T) {
  54. for _, tc := range []struct {
  55. userAgent string
  56. want bool
  57. }{
  58. {"INCY/1.0.0/ios", true},
  59. {"incy/2.4/android", true},
  60. {"Mozilla/5.0 (Linux; Android 14) INCY/1.2.3/android", true},
  61. {"Happ/1.2.0 (iPhone; iOS 17.5)", false},
  62. {"v2rayNG/1.8.5", false},
  63. {"", false},
  64. } {
  65. if got := IsIncyClient(tc.userAgent); got != tc.want {
  66. t.Errorf("IsIncyClient(%q) = %v, want %v", tc.userAgent, got, tc.want)
  67. }
  68. }
  69. }
  70. func TestApplyIncyHeaders_AllDocumentedHeaders(t *testing.T) {
  71. cfg := fullIncyConfig()
  72. h := applyIncyToHeaders(t, cfg, "INCY/1.0.0/android")
  73. want := map[string]string{
  74. "Profile-Description": "Fast and stable network",
  75. "Sort-Order": "ping",
  76. "Support-Email": "[email protected]",
  77. "Announce-Url": "https://t.me/incy_news",
  78. "Premium-Url": "https://example.com/buy",
  79. "Banner-Text": "Summer sale",
  80. "Banner-Button-Text": "Buy now",
  81. "Banner-Button-Url": "https://example.com/sale",
  82. "Banner-Bg-Color": "#E53E3E",
  83. "Banner-Button-Color": "#38A169",
  84. "Hide-Url": "1",
  85. "Hide-Check": "1",
  86. "No-Limit-Enabled": "0",
  87. "Per-App-Proxy-Enable": "1",
  88. "Per-App-Proxy-Mode": "bypass",
  89. "Per-App-Proxy-List": "com.google.chrome,org.telegram.messenger",
  90. "Fragmentation-Enable": "1",
  91. "Fragmentation-Length": "10-30",
  92. "Fragmentation-Interval": "20-40",
  93. "Fragmentation-Packets": "tlshello",
  94. "Noises-Enable": "1",
  95. "Noises-Type": "rand",
  96. "Noises-Packet": "10-20",
  97. "Noises-Delay": "10-50",
  98. "Server-Address-Resolve-Enable": "1",
  99. "Server-Address-Resolve-Dns-Domain": "https://common.dot.dns.yandex.net/dns-query",
  100. "Server-Address-Resolve-Dns-Ip": "77.88.8.8",
  101. }
  102. for name, value := range want {
  103. if got := h.Get(name); got != value {
  104. t.Errorf("header %s = %q, want %q", name, got, value)
  105. }
  106. }
  107. }
  108. func TestApplyIncyHeaders_SkipsUnsetValues(t *testing.T) {
  109. cfg := IncyConfig{AutoDetect: true}
  110. cfg.HideUrl = ""
  111. cfg.SortOrder = ""
  112. h := applyIncyToHeaders(t, cfg, "INCY/1.0.0/ios")
  113. for _, name := range []string{"Hide-Url", "Sort-Order", "Fragmentation-Enable", "Banner-Text"} {
  114. if got := h.Get(name); got != "" {
  115. t.Errorf("unset header %s = %q, want omitted", name, got)
  116. }
  117. }
  118. }
  119. func TestApplyIncyHeaders_NotAppliedWithoutIncyClient(t *testing.T) {
  120. cfg := fullIncyConfig()
  121. for _, userAgent := range []string{"Happ/1.2.0 (iPhone)", "v2rayNG/1.8.5", ""} {
  122. h := applyIncyToHeaders(t, cfg, userAgent)
  123. if got := h.Get("Hide-Url"); got != "" {
  124. t.Errorf("user-agent %q: Hide-Url = %q, want omitted", userAgent, got)
  125. }
  126. }
  127. }
  128. func TestApplyIncyHeaders_NotAppliedWhenAutoDetectOff(t *testing.T) {
  129. cfg := fullIncyConfig()
  130. cfg.AutoDetect = false
  131. h := applyIncyToHeaders(t, cfg, "INCY/1.0.0/android")
  132. if got := h.Get("Hide-Url"); got != "" {
  133. t.Errorf("AutoDetect off: Hide-Url = %q, want omitted", got)
  134. }
  135. }
  136. func TestIncyHeaderText_Base64ForNonASCII(t *testing.T) {
  137. ascii := incyHeaderText("Plain banner")
  138. if ascii != "Plain banner" {
  139. t.Errorf("ASCII text = %q, want unchanged", ascii)
  140. }
  141. cyrillic := incyHeaderText("Здравствуйте")
  142. if !strings.HasPrefix(cyrillic, "base64:") {
  143. t.Fatalf("Cyrillic text = %q, want base64: prefix", cyrillic)
  144. }
  145. // The docs require base64 for anything outside the ASCII range, so the
  146. // raw value must not survive on the wire.
  147. if strings.Contains(cyrillic, "Здравствуйте") {
  148. t.Errorf("Cyrillic text = %q, want the raw value base64-encoded", cyrillic)
  149. }
  150. }
  151. func TestApplyIncyHeaders_RejectsUndocumentedValues(t *testing.T) {
  152. cfg := IncyConfig{
  153. AutoDetect: true,
  154. SortOrder: "alphabetical", // not none|ping|name
  155. PerAppProxyMode: "include", // Happ spelling, not bypass|proxy
  156. NoisesType: "uuid", // not rand|str|hex
  157. FragmentationLength: "10..30", // not min-max
  158. FragmentationPackets: "3", // documented only as tlshello|1-3|1|all
  159. BannerBgColor: "red", // not #RRGGBB
  160. HideUrl: "maybe", // not 1|0
  161. }
  162. h := applyIncyToHeaders(t, cfg, "INCY/1.0.0/android")
  163. for _, name := range []string{
  164. "Sort-Order", "Per-App-Proxy-Mode", "Noises-Type",
  165. "Fragmentation-Length", "Fragmentation-Packets", "Banner-Bg-Color", "Hide-Url",
  166. } {
  167. if got := h.Get(name); got != "" {
  168. t.Errorf("undocumented value accepted for %s: %q", name, got)
  169. }
  170. }
  171. }
  172. func TestApplyIncyHeaders_PerAppListKeepsSeparators(t *testing.T) {
  173. // The settings textarea takes one package per line, and a header cannot
  174. // carry a newline, so each line must stay a separate list entry.
  175. for _, tc := range []struct {
  176. name, list, want string
  177. }{
  178. {"newline", "com.google.chrome\norg.telegram.messenger", "com.google.chrome,org.telegram.messenger"},
  179. {"crlf and blank lines", "com.google.chrome\r\n\r\norg.telegram.messenger\r\n", "com.google.chrome,org.telegram.messenger"},
  180. {"url", " https://example.com/apps.txt ", "https://example.com/apps.txt"},
  181. } {
  182. t.Run(tc.name, func(t *testing.T) {
  183. cfg := IncyConfig{AutoDetect: true, PerAppProxyList: tc.list}
  184. h := applyIncyToHeaders(t, cfg, "INCY/1.0.0/android")
  185. if got := h.Get("Per-App-Proxy-List"); got != tc.want {
  186. t.Errorf("Per-App-Proxy-List = %q, want %q", got, tc.want)
  187. }
  188. })
  189. }
  190. }