external_subscription_test.go 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. package sub
  2. import (
  3. "errors"
  4. "net/http"
  5. "net/http/httptest"
  6. "path/filepath"
  7. "strconv"
  8. "strings"
  9. "sync"
  10. "sync/atomic"
  11. "testing"
  12. "time"
  13. "github.com/mhsanaei/3x-ui/v3/internal/database"
  14. "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
  15. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  16. )
  17. func resetSubscriptionCache(t *testing.T) {
  18. t.Helper()
  19. subscriptionCache.Lock()
  20. previousEntries := subscriptionCache.m
  21. previousInflight := subscriptionCache.inflight
  22. subscriptionCache.m = make(map[string]subscriptionCacheEntry)
  23. subscriptionCache.inflight = make(map[string]*subscriptionFetch)
  24. subscriptionCache.Unlock()
  25. t.Cleanup(func() {
  26. subscriptionCache.Lock()
  27. subscriptionCache.m = previousEntries
  28. subscriptionCache.inflight = previousInflight
  29. subscriptionCache.Unlock()
  30. })
  31. }
  32. func TestFetchSubscriptionLinksSharesConcurrentRefresh(t *testing.T) {
  33. resetSubscriptionCache(t)
  34. var requests atomic.Int32
  35. release := make(chan struct{})
  36. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  37. requests.Add(1)
  38. <-release
  39. _, _ = w.Write([]byte("vless://[email protected]:443"))
  40. }))
  41. defer srv.Close()
  42. const callers = 16
  43. results := make(chan []string, callers)
  44. var wg sync.WaitGroup
  45. for range callers {
  46. wg.Go(func() {
  47. results <- fetchSubscriptionLinks(srv.URL).links
  48. })
  49. }
  50. time.Sleep(100 * time.Millisecond)
  51. close(release)
  52. wg.Wait()
  53. close(results)
  54. for links := range results {
  55. if len(links) != 1 || links[0] != "vless://[email protected]:443" {
  56. t.Fatalf("links = %#v", links)
  57. }
  58. }
  59. if got := requests.Load(); got != 1 {
  60. t.Fatalf("requests = %d, want 1", got)
  61. }
  62. }
  63. func TestFetchSubscriptionLinksBoundsCacheSize(t *testing.T) {
  64. resetSubscriptionCache(t)
  65. var requests atomic.Int32
  66. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  67. requests.Add(1)
  68. _, _ = w.Write([]byte("vless://[email protected]:443"))
  69. }))
  70. defer srv.Close()
  71. for i := range subscriptionCacheCapacity + 1 {
  72. links := fetchSubscriptionLinks(srv.URL + "?id=" + strconv.Itoa(i)).links
  73. if len(links) != 1 {
  74. t.Fatalf("links at %d = %#v", i, links)
  75. }
  76. }
  77. subscriptionCache.Lock()
  78. entries := len(subscriptionCache.m)
  79. subscriptionCache.Unlock()
  80. if entries != subscriptionCacheCapacity {
  81. t.Fatalf("cache entries = %d, want %d", entries, subscriptionCacheCapacity)
  82. }
  83. if got := requests.Load(); got != subscriptionCacheCapacity+1 {
  84. t.Fatalf("requests = %d, want %d", got, subscriptionCacheCapacity+1)
  85. }
  86. }
  87. func TestFetchSubscriptionLinksSharesStaleResultAfterRefreshFailure(t *testing.T) {
  88. resetSubscriptionCache(t)
  89. stale := []string{"vless://[email protected]:443"}
  90. release := make(chan struct{})
  91. var staleRequests atomic.Int32
  92. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  93. if r.URL.Path == "/stale" {
  94. staleRequests.Add(1)
  95. <-release
  96. w.WriteHeader(http.StatusBadGateway)
  97. return
  98. }
  99. _, _ = w.Write([]byte("vless://[email protected]:443"))
  100. }))
  101. defer srv.Close()
  102. staleURL := srv.URL + "/stale"
  103. subscriptionCache.Lock()
  104. subscriptionCache.m[staleURL] = subscriptionCacheEntry{
  105. links: stale,
  106. fetchedAt: time.Now().Add(-subscriptionCacheTTL),
  107. }
  108. for i := range subscriptionCacheCapacity - 1 {
  109. subscriptionCache.m["cached-"+strconv.Itoa(i)] = subscriptionCacheEntry{fetchedAt: time.Now()}
  110. }
  111. subscriptionCache.Unlock()
  112. const callers = 16
  113. results := make(chan []string, callers)
  114. var wg sync.WaitGroup
  115. for range callers {
  116. wg.Go(func() {
  117. results <- fetchSubscriptionLinks(staleURL).links
  118. })
  119. }
  120. time.Sleep(100 * time.Millisecond)
  121. if links := fetchSubscriptionLinks(srv.URL + "/fresh").links; len(links) != 1 || links[0] != "vless://[email protected]:443" {
  122. t.Fatalf("fresh links = %#v", links)
  123. }
  124. close(release)
  125. wg.Wait()
  126. close(results)
  127. for links := range results {
  128. if len(links) != 1 || links[0] != stale[0] {
  129. t.Fatalf("links = %#v, want %#v", links, stale)
  130. }
  131. }
  132. if got := staleRequests.Load(); got != 1 {
  133. t.Fatalf("requests = %d, want 1", got)
  134. }
  135. }
  136. func TestDoFetchSubscriptionLinks_RejectsOversizedBody(t *testing.T) {
  137. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  138. _, _ = w.Write([]byte(strings.Repeat("a", subscriptionMaxBytes+1)))
  139. }))
  140. defer srv.Close()
  141. links, err := doFetchSubscriptionLinks(srv.URL)
  142. if !errors.Is(err, errSubscriptionBodyTooLarge) {
  143. t.Fatalf("err = %v, want errSubscriptionBodyTooLarge", err)
  144. }
  145. if links != nil {
  146. t.Fatalf("links = %v, want nil", links)
  147. }
  148. }
  149. func TestDoFetchSubscriptionLinks_AcceptsBodyAtLimit(t *testing.T) {
  150. link := "vless://example"
  151. body := link + "\n" + strings.Repeat("#", subscriptionMaxBytes-len(link)-1)
  152. if len(body) != subscriptionMaxBytes {
  153. t.Fatalf("fixture size = %d, want %d", len(body), subscriptionMaxBytes)
  154. }
  155. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  156. _, _ = w.Write([]byte(body))
  157. }))
  158. defer srv.Close()
  159. links, err := doFetchSubscriptionLinks(srv.URL)
  160. if err != nil {
  161. t.Fatalf("unexpected err: %v", err)
  162. }
  163. if len(links) != 1 || links[0] != link {
  164. t.Fatalf("links = %v, want [%q]", links, link)
  165. }
  166. }
  167. func TestRecordExternalSubscriptionFetchStampsEveryRowForTheURL(t *testing.T) {
  168. initMutDB(t)
  169. resetSubscriptionCache(t)
  170. db := database.GetDB()
  171. var failing atomic.Bool
  172. failing.Store(true)
  173. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  174. if failing.Load() {
  175. w.WriteHeader(http.StatusBadGateway)
  176. return
  177. }
  178. _, _ = w.Write([]byte("vless://[email protected]:443#Node"))
  179. }))
  180. defer srv.Close()
  181. owners := []model.ClientRecord{
  182. {Email: "[email protected]", SubID: "sub-fetch", UUID: "uuid-1", Enable: true},
  183. {Email: "[email protected]", SubID: "sub-fetch", UUID: "uuid-2", Enable: true},
  184. }
  185. for i := range owners {
  186. if err := db.Create(&owners[i]).Error; err != nil {
  187. t.Fatalf("seed client %d: %v", i, err)
  188. }
  189. row := model.ClientExternalLink{
  190. ClientId: owners[i].Id,
  191. Kind: model.ExternalLinkKindSubscription,
  192. Value: srv.URL,
  193. }
  194. if err := db.Create(&row).Error; err != nil {
  195. t.Fatalf("seed external link %d: %v", i, err)
  196. }
  197. }
  198. svc := NewSubService("")
  199. entries, err := svc.getClientExternalLinksBySubId("sub-fetch")
  200. if err != nil {
  201. t.Fatalf("getClientExternalLinksBySubId: %v", err)
  202. }
  203. if len(entries) != 2 {
  204. t.Fatalf("entries = %d, want 2", len(entries))
  205. }
  206. for _, e := range entries {
  207. expandEntry(e)
  208. }
  209. var rows []model.ClientExternalLink
  210. if err := db.Where("value = ?", srv.URL).Find(&rows).Error; err != nil {
  211. t.Fatalf("read rows: %v", err)
  212. }
  213. if len(rows) != 2 {
  214. t.Fatalf("rows = %d, want 2", len(rows))
  215. }
  216. for _, row := range rows {
  217. if row.LastFetchAt <= 0 {
  218. t.Fatalf("row %d lastFetchAt = %d, want a stamped timestamp", row.Id, row.LastFetchAt)
  219. }
  220. if row.LastFetchError != errBadStatus.Error() {
  221. t.Fatalf("row %d lastFetchError = %q, want %q", row.Id, row.LastFetchError, errBadStatus)
  222. }
  223. }
  224. failing.Store(false)
  225. resetSubscriptionCache(t)
  226. for _, e := range entries {
  227. expandEntry(e)
  228. }
  229. if err := db.Where("value = ?", srv.URL).Find(&rows).Error; err != nil {
  230. t.Fatalf("re-read rows: %v", err)
  231. }
  232. for _, row := range rows {
  233. if row.LastFetchError != "" {
  234. t.Fatalf("row %d lastFetchError = %q, want cleared after a good fetch", row.Id, row.LastFetchError)
  235. }
  236. if row.LastFetchAt <= 0 {
  237. t.Fatalf("row %d lastFetchAt = %d, want a stamped timestamp", row.Id, row.LastFetchAt)
  238. }
  239. }
  240. }
  241. func TestExpandEntryCacheHitWritesNothing(t *testing.T) {
  242. initMutDB(t)
  243. resetSubscriptionCache(t)
  244. db := database.GetDB()
  245. const subURL = "https://provider.example/cached"
  246. rec := model.ClientRecord{Email: "[email protected]", SubID: "sub-cached", UUID: "uuid", Enable: true}
  247. if err := db.Create(&rec).Error; err != nil {
  248. t.Fatalf("seed client: %v", err)
  249. }
  250. row := model.ClientExternalLink{ClientId: rec.Id, Kind: model.ExternalLinkKindSubscription, Value: subURL}
  251. if err := db.Create(&row).Error; err != nil {
  252. t.Fatalf("seed external link: %v", err)
  253. }
  254. subscriptionCache.Lock()
  255. subscriptionCache.m[subURL] = subscriptionCacheEntry{
  256. links: []string{"vless://[email protected]:443#Node"},
  257. fetchedAt: time.Now(),
  258. }
  259. subscriptionCache.Unlock()
  260. if got := expandEntry(externalLinkEntry{Kind: model.ExternalLinkKindSubscription, Value: subURL}); len(got) != 1 {
  261. t.Fatalf("expandEntry = %#v, want the cached link", got)
  262. }
  263. var after model.ClientExternalLink
  264. if err := db.First(&after, row.Id).Error; err != nil {
  265. t.Fatalf("read row: %v", err)
  266. }
  267. if after.LastFetchAt != 0 || after.LastFetchError != "" {
  268. t.Fatalf("cache hit wrote fetch status: %#v", after)
  269. }
  270. }
  271. func TestFetchUsesConfiguredExternalSubUserAgent(t *testing.T) {
  272. resetSubscriptionCache(t)
  273. dbtest.InitDB(t, filepath.Join(t.TempDir(), "ua.db"))
  274. const customUA = "Happ/4.2.1"
  275. if err := database.GetDB().Create(&model.Setting{
  276. Key: "externalSubUserAgent",
  277. Value: customUA,
  278. }).Error; err != nil {
  279. t.Fatalf("save setting: %v", err)
  280. }
  281. var gotUA string
  282. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  283. gotUA = r.Header.Get("User-Agent")
  284. _, _ = w.Write([]byte("vless://uuid@host:443?security=none#x"))
  285. }))
  286. defer srv.Close()
  287. res := fetchSubscriptionLinks(srv.URL)
  288. if res.err != nil {
  289. t.Fatalf("fetch: %v", res.err)
  290. }
  291. if gotUA != customUA {
  292. t.Fatalf("User-Agent = %q, want %q", gotUA, customUA)
  293. }
  294. }
  295. func TestFetchFallsBackToDefaultExternalSubUserAgent(t *testing.T) {
  296. resetSubscriptionCache(t)
  297. dbtest.InitDB(t, filepath.Join(t.TempDir(), "ua-default.db"))
  298. var gotUA string
  299. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  300. gotUA = r.Header.Get("User-Agent")
  301. _, _ = w.Write([]byte("vless://uuid@host:443?security=none#x"))
  302. }))
  303. defer srv.Close()
  304. res := fetchSubscriptionLinks(srv.URL)
  305. if res.err != nil {
  306. t.Fatalf("fetch: %v", res.err)
  307. }
  308. if gotUA != "v2rayNG/1.8.5" {
  309. t.Fatalf("User-Agent = %q, want default v2rayNG/1.8.5", gotUA)
  310. }
  311. }