mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-19 00:27:14 +00:00
fix(sub): send stable X-HWID on external subscription fetch
A Master panel fetching a donor subscription sent no X-HWID, so an HWID-limited donor rejected it with 404. Identify this panel with a stable per-installation id (persisted in settings), occupying exactly one donor device slot. Fixes MHSanaei/3x-ui#6559
This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
package sub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// #6559: the Master panel must send a stable X-HWID when fetching external
|
||||||
|
// subscriptions, otherwise an HWID-limited donor answers 404.
|
||||||
|
func TestServerHwidStableAcrossCalls(t *testing.T) {
|
||||||
|
if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
|
||||||
|
t.Fatalf("InitDB: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = database.CloseDB() })
|
||||||
|
|
||||||
|
first := serverHwid()
|
||||||
|
if first == "" {
|
||||||
|
t.Fatal("serverHwid returned empty")
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(first, "3x-ui-server-") {
|
||||||
|
t.Fatalf("unexpected hwid format: %q", first)
|
||||||
|
}
|
||||||
|
if len(first) < 6 {
|
||||||
|
t.Fatalf("hwid too short for donor minHwidLength: %q", first)
|
||||||
|
}
|
||||||
|
|
||||||
|
second := serverHwid()
|
||||||
|
if second != first {
|
||||||
|
t.Fatalf("hwid not stable: %q vs %q", first, second)
|
||||||
|
}
|
||||||
|
|
||||||
|
var row model.Setting
|
||||||
|
if err := database.GetDB().Where("key = ?", serverHwidKey).First(&row).Error; err != nil {
|
||||||
|
t.Fatalf("hwid not persisted: %v", err)
|
||||||
|
}
|
||||||
|
if row.Value != first {
|
||||||
|
t.Fatalf("persisted hwid %q != returned %q", row.Value, first)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||||
@@ -150,6 +151,12 @@ func doFetchSubscriptionLinks(rawURL string) ([]string, error) {
|
|||||||
}
|
}
|
||||||
// Some providers gate the link body on a known client User-Agent.
|
// Some providers gate the link body on a known client User-Agent.
|
||||||
req.Header.Set("User-Agent", "v2rayNG/1.8.5")
|
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). Identify this panel with a stable per-installation id so the
|
||||||
|
// donor registers exactly one device slot for it.
|
||||||
|
if hwid := serverHwid(); hwid != "" {
|
||||||
|
req.Header.Set("X-HWID", hwid)
|
||||||
|
}
|
||||||
resp, err := subscriptionHTTPClient.Do(req)
|
resp, err := subscriptionHTTPClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -173,6 +180,36 @@ var (
|
|||||||
errSubscriptionBodyTooLarge = &subError{"subscription response body exceeds size limit"}
|
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"
|
||||||
|
|
||||||
|
// serverHwid returns a stable per-installation id, creating and persisting it
|
||||||
|
// on first use. A random-per-request value would burn one donor HWID slot per
|
||||||
|
// fetch; empty means the DB is unreachable, in which case no header is sent.
|
||||||
|
func serverHwid() string {
|
||||||
|
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 }
|
type subError struct{ msg string }
|
||||||
|
|
||||||
func (e *subError) Error() string { return e.msg }
|
func (e *subError) Error() string { return e.msg }
|
||||||
|
|||||||
Reference in New Issue
Block a user