external_subscription.go 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. package sub
  2. import (
  3. "encoding/base64"
  4. "io"
  5. "net/http"
  6. "strings"
  7. "sync"
  8. "time"
  9. )
  10. // External subscription fetching: a "subscription" external link is a remote
  11. // URL whose body is a (often base64-encoded) newline list of share links. We
  12. // fetch it on demand, cache the decoded links briefly, and bound the request
  13. // with a short timeout so a slow/dead provider can't stall a client's sub.
  14. const (
  15. subscriptionCacheTTL = 5 * time.Minute
  16. subscriptionMaxBytes = 2 << 20 // 2 MiB
  17. )
  18. var subscriptionHTTPClient = &http.Client{Timeout: 6 * time.Second}
  19. type subscriptionCacheEntry struct {
  20. links []string
  21. fetchedAt time.Time
  22. }
  23. var subscriptionCache = struct {
  24. sync.Mutex
  25. m map[string]subscriptionCacheEntry
  26. }{m: make(map[string]subscriptionCacheEntry)}
  27. // fetchSubscriptionLinks returns the share links contained in a remote
  28. // subscription URL, using a short-lived cache. On any failure it returns the
  29. // last cached value (if present) or nil — never an error, so the rest of the
  30. // client's subscription still renders.
  31. func fetchSubscriptionLinks(rawURL string) []string {
  32. rawURL = strings.TrimSpace(rawURL)
  33. if rawURL == "" {
  34. return nil
  35. }
  36. subscriptionCache.Lock()
  37. cached, ok := subscriptionCache.m[rawURL]
  38. subscriptionCache.Unlock()
  39. if ok && time.Since(cached.fetchedAt) < subscriptionCacheTTL {
  40. return cached.links
  41. }
  42. links, err := doFetchSubscriptionLinks(rawURL)
  43. if err != nil {
  44. // Serve stale on error rather than dropping the client's configs.
  45. if ok {
  46. return cached.links
  47. }
  48. return nil
  49. }
  50. subscriptionCache.Lock()
  51. subscriptionCache.m[rawURL] = subscriptionCacheEntry{links: links, fetchedAt: time.Now()}
  52. subscriptionCache.Unlock()
  53. return links
  54. }
  55. func doFetchSubscriptionLinks(rawURL string) ([]string, error) {
  56. req, err := http.NewRequest(http.MethodGet, rawURL, nil)
  57. if err != nil {
  58. return nil, err
  59. }
  60. // Some providers gate the link body on a known client User-Agent.
  61. req.Header.Set("User-Agent", "v2rayNG/1.8.5")
  62. resp, err := subscriptionHTTPClient.Do(req)
  63. if err != nil {
  64. return nil, err
  65. }
  66. defer resp.Body.Close()
  67. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  68. return nil, errBadStatus
  69. }
  70. body, err := io.ReadAll(io.LimitReader(resp.Body, subscriptionMaxBytes))
  71. if err != nil {
  72. return nil, err
  73. }
  74. return decodeSubscriptionBody(body), nil
  75. }
  76. var errBadStatus = &subError{"non-2xx subscription response"}
  77. type subError struct{ msg string }
  78. func (e *subError) Error() string { return e.msg }
  79. // decodeSubscriptionBody handles the common base64-encoded newline list as well
  80. // as a plain-text body, returning only the lines that look like share links.
  81. func decodeSubscriptionBody(body []byte) []string {
  82. text := strings.TrimSpace(string(body))
  83. if text == "" {
  84. return nil
  85. }
  86. if decoded, ok := tryDecodeBase64Body(text); ok {
  87. text = strings.TrimSpace(decoded)
  88. }
  89. lines := strings.FieldsFunc(text, func(r rune) bool { return r == '\n' || r == '\r' })
  90. out := make([]string, 0, len(lines))
  91. for _, ln := range lines {
  92. ln = strings.TrimSpace(ln)
  93. if ln == "" || strings.HasPrefix(ln, "#") {
  94. continue
  95. }
  96. if strings.Contains(ln, "://") {
  97. out = append(out, ln)
  98. }
  99. }
  100. return out
  101. }
  102. func tryDecodeBase64Body(s string) (string, bool) {
  103. clean := strings.Map(func(r rune) rune {
  104. switch r {
  105. case ' ', '\n', '\r', '\t':
  106. return -1
  107. }
  108. return r
  109. }, s)
  110. if b, err := base64.StdEncoding.DecodeString(padBase64Sub(clean)); err == nil {
  111. return string(b), true
  112. }
  113. if b, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(clean, "=")); err == nil {
  114. return string(b), true
  115. }
  116. return "", false
  117. }