|
|
@@ -9,15 +9,15 @@ import (
|
|
|
"sync"
|
|
|
"time"
|
|
|
|
|
|
+ "github.com/google/uuid"
|
|
|
+
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
|
)
|
|
|
|
|
|
-// External subscription fetching: a "subscription" external link is a remote
|
|
|
-// URL whose body is a (often base64-encoded) newline list of share links. We
|
|
|
-// fetch it on demand, cache the decoded links briefly, and bound the request
|
|
|
-// with a short timeout so a slow/dead provider can't stall a client's sub.
|
|
|
+// External subscription fetching: a remote URL whose body is a share-link
|
|
|
+// list. Fetches are cached briefly and bounded so a dead provider can't stall.
|
|
|
|
|
|
const (
|
|
|
subscriptionCacheTTL = 5 * time.Minute
|
|
|
@@ -150,6 +150,10 @@ func doFetchSubscriptionLinks(rawURL string) ([]string, error) {
|
|
|
}
|
|
|
// Some providers gate the link body on a known client User-Agent.
|
|
|
req.Header.Set("User-Agent", "v2rayNG/1.8.5")
|
|
|
+ // A 3x-ui donor with an HWID limit answers 404 when the header is empty (#6559).
|
|
|
+ if hwid := serverHwid(); hwid != "" {
|
|
|
+ req.Header.Set("X-HWID", hwid)
|
|
|
+ }
|
|
|
resp, err := subscriptionHTTPClient.Do(req)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
@@ -173,6 +177,41 @@ var (
|
|
|
errSubscriptionBodyTooLarge = &subError{"subscription response body exceeds size limit"}
|
|
|
)
|
|
|
|
|
|
+// serverHwidKey is the settings row holding this panel's stable identity
|
|
|
+// for outbound external-subscription fetches.
|
|
|
+const serverHwidKey = "externalSubHwid"
|
|
|
+
|
|
|
+// serverHwidMu serializes first-time creation: without it, concurrent first
|
|
|
+// fetches of different URLs each mint and persist their own UUID.
|
|
|
+var serverHwidMu sync.Mutex
|
|
|
+
|
|
|
+// serverHwid returns a stable per-installation id, creating and persisting
|
|
|
+// it on first use. Empty means the DB is unreachable: send no header then.
|
|
|
+func serverHwid() string {
|
|
|
+ serverHwidMu.Lock()
|
|
|
+ defer serverHwidMu.Unlock()
|
|
|
+ db := database.GetDB()
|
|
|
+ if db == nil {
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+ var row model.Setting
|
|
|
+ if err := db.Where("key = ?", serverHwidKey).First(&row).Error; err == nil {
|
|
|
+ if strings.TrimSpace(row.Value) != "" {
|
|
|
+ return strings.TrimSpace(row.Value)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ hwid := "3x-ui-server-" + uuid.NewString()
|
|
|
+ row = model.Setting{Key: serverHwidKey, Value: hwid}
|
|
|
+ if err := db.Where(model.Setting{Key: serverHwidKey}).FirstOrCreate(&row).Error; err != nil {
|
|
|
+ logger.Warningf("sub: persisting server hwid failed: %v", err)
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+ if strings.TrimSpace(row.Value) == "" {
|
|
|
+ return hwid
|
|
|
+ }
|
|
|
+ return strings.TrimSpace(row.Value)
|
|
|
+}
|
|
|
+
|
|
|
type subError struct{ msg string }
|
|
|
|
|
|
func (e *subError) Error() string { return e.msg }
|