happ_test.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. package sub
  2. import (
  3. "net/http"
  4. "net/http/httptest"
  5. "testing"
  6. "github.com/gin-gonic/gin"
  7. )
  8. func TestNormalizeHappRouting_Off(t *testing.T) {
  9. got, err := normalizeHappRouting([]byte("happ://routing/off"))
  10. if err != nil {
  11. t.Fatalf("normalizeHappRouting(off) error: %v", err)
  12. }
  13. if got != "happ://routing/off" {
  14. t.Fatalf("normalizeHappRouting(off) = %q, want happ://routing/off", got)
  15. }
  16. }
  17. func TestApplyCommonHeaders_HappClientHeaders(t *testing.T) {
  18. gin.SetMode(gin.TestMode)
  19. cfg := HappConfig{
  20. AutoDetect: true,
  21. ProviderId: "pid-test-123",
  22. NewUrl: "https://new.example.com/sub",
  23. FallbackUrl: "https://backup.example.com/sub",
  24. SubInfoColor: "primary",
  25. SubInfoText: "Welcome to VIP Network",
  26. SubInfoButtonText: "Telegram",
  27. SubInfoButtonLink: "https://t.me/example",
  28. SubExpire: true,
  29. SubExpireButtonLink: "https://renew.example.com",
  30. NotificationExpire: true,
  31. NoLimit: true,
  32. AlwaysHwid: true,
  33. TunMode: "gvisor",
  34. TunType: "singbox",
  35. ExcludeRoutes: "192.168.1.0/24, 10.0.0.0/8",
  36. ExcludeApns: true,
  37. ColorProfile: "{\"serverRowBackgroundColor\":\n\"#21003D67\"}",
  38. PingType: "http",
  39. AutoConnect: true,
  40. AutoConnectType: "fastest",
  41. PerAppMode: "include",
  42. PerAppList: "com.google.chrome,com.meta.instagram",
  43. }
  44. controller := &SUBController{happConfig: cfg}
  45. recorder := httptest.NewRecorder()
  46. ctx, _ := gin.CreateTestContext(recorder)
  47. ctx.Request = httptest.NewRequest(http.MethodGet, "/sub/test", nil)
  48. ctx.Request.Header.Set("User-Agent", "Happ/1.2.0 (iPhone; iOS 17.5)")
  49. controller.ApplyCommonHeaders(ctx, "upload=0; download=100; total=1000; expire=1800000000", "12", "MyTitle", "", "", "", false, "", false)
  50. h := recorder.Header()
  51. if h.Get("Routing-Enable") != "0" {
  52. t.Fatalf("Routing-Enable = %q, want 0 for Happ with disabled routing", h.Get("Routing-Enable"))
  53. }
  54. if h.Get("Hide-Settings") != "0" {
  55. t.Fatalf("Hide-Settings = %q, want 0 for Happ with disabled hideSettings", h.Get("Hide-Settings"))
  56. }
  57. if h.Get("ProviderID") != "pid-test-123" {
  58. t.Fatalf("ProviderID = %q, want pid-test-123", h.Get("ProviderID"))
  59. }
  60. if h.Get("New-Url") != "https://new.example.com/sub" {
  61. t.Fatalf("New-Url = %q", h.Get("New-Url"))
  62. }
  63. if h.Get("Fallback-Url") != "https://backup.example.com/sub" {
  64. t.Fatalf("Fallback-Url = %q", h.Get("Fallback-Url"))
  65. }
  66. if h.Get("Sub-Info-Color") != "blue" || h.Get("Sub-Info-Text") != "Welcome to VIP Network" {
  67. t.Fatalf("Sub-Info = %s / %s, want blue / Welcome to VIP Network", h.Get("Sub-Info-Color"), h.Get("Sub-Info-Text"))
  68. }
  69. if h.Get("Sub-Info-Button-Text") != "Telegram" || h.Get("Sub-Info-Button-Link") != "https://t.me/example" {
  70. t.Fatalf("Sub-Info button = %s / %s", h.Get("Sub-Info-Button-Text"), h.Get("Sub-Info-Button-Link"))
  71. }
  72. if h.Get("Sub-Expire") != "1" || h.Get("Sub-Expire-Button-Link") != "https://renew.example.com" {
  73. t.Fatalf("Sub-Expire = %s / %s", h.Get("Sub-Expire"), h.Get("Sub-Expire-Button-Link"))
  74. }
  75. if h.Get("Notification-Subs-Expire") != "1" {
  76. t.Fatalf("Notification-Subs-Expire = %q", h.Get("Notification-Subs-Expire"))
  77. }
  78. if h.Get("No-Limit-Enabled") != "1" {
  79. t.Fatalf("No-Limit-Enabled = %q", h.Get("No-Limit-Enabled"))
  80. }
  81. if h.Get("Subscription-Always-Hwid-Enable") != "1" {
  82. t.Fatalf("Subscription-Always-Hwid-Enable = %q", h.Get("Subscription-Always-Hwid-Enable"))
  83. }
  84. if h.Get("Tun-Mode") != "gvisor" || h.Get("Tun-Type") != "singbox" {
  85. t.Fatalf("Tun mode/type = %s / %s", h.Get("Tun-Mode"), h.Get("Tun-Type"))
  86. }
  87. if h.Get("Exclude-Routes") != "192.168.1.0/24, 10.0.0.0/8" || h.Get("Exclude-Apns-Enable") != "true" {
  88. t.Fatalf("Exclude routes/apns = %s / %s", h.Get("Exclude-Routes"), h.Get("Exclude-Apns-Enable"))
  89. }
  90. if wantProfile := "{\"serverRowBackgroundColor\":\"#21003D67\"}"; h.Get("Color-Profile") != wantProfile {
  91. t.Fatalf("Color-Profile = %q, want %q", h.Get("Color-Profile"), wantProfile)
  92. }
  93. if h.Get("Ping-Type") != "proxy" {
  94. t.Fatalf("Ping-Type = %q, want proxy for http alias", h.Get("Ping-Type"))
  95. }
  96. if h.Get("Subscription-Autoconnect") != "1" || h.Get("Subscription-Autoconnect-Type") != "lowestdelay" {
  97. t.Fatalf("Autoconnect = %s / %s, want 1 / lowestdelay for fastest alias", h.Get("Subscription-Autoconnect"), h.Get("Subscription-Autoconnect-Type"))
  98. }
  99. if h.Get("Per-App-Proxy-Mode") != "on" || h.Get("Per-App-Proxy-List") != "com.google.chrome,com.meta.instagram" {
  100. t.Fatalf("Per-App = %s / %s, want on / com.google.chrome,com.meta.instagram", h.Get("Per-App-Proxy-Mode"), h.Get("Per-App-Proxy-List"))
  101. }
  102. }
  103. func TestApplyHappHeaders_Gating(t *testing.T) {
  104. gin.SetMode(gin.TestMode)
  105. cfg := HappConfig{
  106. AutoDetect: true,
  107. ProviderId: "pid-secret",
  108. SubInfoText: "Banner",
  109. TunMode: "system",
  110. }
  111. t.Run("non-Happ User-Agent receives no headers", func(t *testing.T) {
  112. recorder := httptest.NewRecorder()
  113. ctx, _ := gin.CreateTestContext(recorder)
  114. ctx.Request = httptest.NewRequest(http.MethodGet, "/sub/test", nil)
  115. ctx.Request.Header.Set("User-Agent", "v2rayNG/1.8.5")
  116. controller := &SUBController{happConfig: cfg}
  117. controller.ApplyCommonHeaders(ctx, "", "", "Title", "", "", "", false, "", false)
  118. if got := recorder.Header().Get("ProviderID"); got != "" {
  119. t.Fatalf("ProviderID emitted to non-Happ client: %q", got)
  120. }
  121. if got := recorder.Header().Get("Sub-Info-Text"); got != "" {
  122. t.Fatalf("Sub-Info-Text emitted to non-Happ client: %q", got)
  123. }
  124. if got := recorder.Header().Get("Tun-Mode"); got != "" {
  125. t.Fatalf("Tun-Mode emitted to non-Happ client: %q", got)
  126. }
  127. })
  128. t.Run("AutoDetect disabled suppresses Happ headers", func(t *testing.T) {
  129. recorder := httptest.NewRecorder()
  130. ctx, _ := gin.CreateTestContext(recorder)
  131. ctx.Request = httptest.NewRequest(http.MethodGet, "/sub/test", nil)
  132. ctx.Request.Header.Set("User-Agent", "Happ/1.2.0 (Android)")
  133. disabledCfg := cfg
  134. disabledCfg.AutoDetect = false
  135. controller := &SUBController{happConfig: disabledCfg}
  136. controller.ApplyCommonHeaders(ctx, "", "", "Title", "", "", "", false, "", false)
  137. if got := recorder.Header().Get("ProviderID"); got != "" {
  138. t.Fatalf("ProviderID emitted when AutoDetect is false: %q", got)
  139. }
  140. if got := recorder.Header().Get("Sub-Info-Text"); got != "" {
  141. t.Fatalf("Sub-Info-Text emitted when AutoDetect is false: %q", got)
  142. }
  143. // happ.su documents routing-enable 0/false as "disables routing
  144. // globally", so it must stay behind the same opt-in as the rest.
  145. if got := recorder.Header().Get("Routing-Enable"); got != "" {
  146. t.Fatalf("Routing-Enable emitted when AutoDetect is false: %q", got)
  147. }
  148. if got := recorder.Header().Get("Hide-Settings"); got != "" {
  149. t.Fatalf("Hide-Settings emitted when AutoDetect is false: %q", got)
  150. }
  151. })
  152. }
  153. func TestApplyHappHeaders_Aliases(t *testing.T) {
  154. gin.SetMode(gin.TestMode)
  155. tests := []struct {
  156. name string
  157. cfg HappConfig
  158. wantHeader string
  159. wantValue string
  160. }{
  161. {
  162. name: "color warning maps to red",
  163. cfg: HappConfig{AutoDetect: true, SubInfoText: "Alert", SubInfoColor: "warning"},
  164. wantHeader: "Sub-Info-Color",
  165. wantValue: "red",
  166. },
  167. {
  168. name: "color danger maps to red",
  169. cfg: HappConfig{AutoDetect: true, SubInfoText: "Alert", SubInfoColor: "danger"},
  170. wantHeader: "Sub-Info-Color",
  171. wantValue: "red",
  172. },
  173. {
  174. name: "color success maps to green",
  175. cfg: HappConfig{AutoDetect: true, SubInfoText: "Ok", SubInfoColor: "success"},
  176. wantHeader: "Sub-Info-Color",
  177. wantValue: "green",
  178. },
  179. {
  180. name: "autoconnect last maps to lastused",
  181. cfg: HappConfig{AutoDetect: true, AutoConnect: true, AutoConnectType: "last"},
  182. wantHeader: "Subscription-Autoconnect-Type",
  183. wantValue: "lastused",
  184. },
  185. {
  186. name: "per-app exclude maps to bypass",
  187. cfg: HappConfig{AutoDetect: true, PerAppMode: "exclude", PerAppList: "app.id"},
  188. wantHeader: "Per-App-Proxy-Mode",
  189. wantValue: "bypass",
  190. },
  191. }
  192. for _, tc := range tests {
  193. t.Run(tc.name, func(t *testing.T) {
  194. recorder := httptest.NewRecorder()
  195. ctx, _ := gin.CreateTestContext(recorder)
  196. ctx.Request = httptest.NewRequest(http.MethodGet, "/sub/test", nil)
  197. ctx.Request.Header.Set("User-Agent", "Happ/1.2.0 (iOS)")
  198. controller := &SUBController{happConfig: tc.cfg}
  199. controller.ApplyCommonHeaders(ctx, "", "", "Title", "", "", "", false, "", false)
  200. if got := recorder.Header().Get(tc.wantHeader); got != tc.wantValue {
  201. t.Fatalf("%s = %q, want %q", tc.wantHeader, got, tc.wantValue)
  202. }
  203. })
  204. }
  205. }