external_subscription_test.go 10.0 KB

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