mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-18 04:12:08 +03:00
fix(sub): address review on external X-HWID
- Serialize first-time id creation with a mutex so concurrent first fetches cannot mint two UUIDs. - Fix goimports grouping for the new third-party import. - Add externalSubSendHwid opt-out (default send); document it. - Cover header send/omit with httptest in TestFetchSendsStableHwid.
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
package sub
|
package sub
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -41,3 +43,43 @@ func TestServerHwidStableAcrossCalls(t *testing.T) {
|
|||||||
t.Fatalf("persisted hwid %q != returned %q", row.Value, first)
|
t.Fatalf("persisted hwid %q != returned %q", row.Value, first)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The fetch must carry the stable id by default so an HWID-limited donor
|
||||||
|
// lets it through, and must drop it when the operator opts out.
|
||||||
|
func TestFetchSendsStableHwid(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() })
|
||||||
|
|
||||||
|
var gotHwid string
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
gotHwid = r.Header.Get("X-HWID")
|
||||||
|
_, _ = w.Write([]byte("vless://uuid@host:443?security=none#x"))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
res := fetchSubscriptionLinks(srv.URL)
|
||||||
|
if res.err != nil {
|
||||||
|
t.Fatalf("fetch: %v", res.err)
|
||||||
|
}
|
||||||
|
if len(res.links) != 1 {
|
||||||
|
t.Fatalf("links = %v", res.links)
|
||||||
|
}
|
||||||
|
if gotHwid == "" {
|
||||||
|
t.Fatal("X-HWID header missing on fetch")
|
||||||
|
}
|
||||||
|
if gotHwid != serverHwid() {
|
||||||
|
t.Fatalf("sent %q != stable %q", gotHwid, serverHwid())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.GetDB().Create(
|
||||||
|
&model.Setting{Key: sendHwidKey, Value: "false"}).Error; err != nil {
|
||||||
|
t.Fatalf("opt out: %v", err)
|
||||||
|
}
|
||||||
|
gotHwid = "sentinel"
|
||||||
|
fetchSubscriptionLinks(srv.URL + "/other")
|
||||||
|
if gotHwid != "" {
|
||||||
|
t.Fatalf("X-HWID sent despite opt-out: %q", gotHwid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,15 +10,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// External subscription fetching: a "subscription" external link is a remote
|
// External subscription fetching: a remote URL whose body is a share-link
|
||||||
// URL whose body is a (often base64-encoded) newline list of share links. We
|
// list. Fetches are cached briefly and bounded so a dead provider can't stall.
|
||||||
// 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.
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
subscriptionCacheTTL = 5 * time.Minute
|
subscriptionCacheTTL = 5 * time.Minute
|
||||||
@@ -151,11 +150,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
|
// A 3x-ui donor with an HWID limit answers 404 when the header is
|
||||||
// (#6559). Identify this panel with a stable per-installation id so the
|
// empty (#6559). Send our stable id unless the operator opted out.
|
||||||
// donor registers exactly one device slot for it.
|
if sendServerHwid() {
|
||||||
if hwid := serverHwid(); hwid != "" {
|
if hwid := serverHwid(); hwid != "" {
|
||||||
req.Header.Set("X-HWID", hwid)
|
req.Header.Set("X-HWID", hwid)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
resp, err := subscriptionHTTPClient.Do(req)
|
resp, err := subscriptionHTTPClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -180,14 +180,38 @@ 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
|
// serverHwidKey is the settings row holding this panel's stable identity
|
||||||
// outbound external-subscription fetches.
|
// for outbound external-subscription fetches.
|
||||||
const serverHwidKey = "externalSubHwid"
|
const serverHwidKey = "externalSubHwid"
|
||||||
|
|
||||||
// serverHwid returns a stable per-installation id, creating and persisting it
|
// sendHwidKey toggles the X-HWID header on external fetches. Default on;
|
||||||
// on first use. A random-per-request value would burn one donor HWID slot per
|
// set to "false" to stop identifying this panel to third-party providers.
|
||||||
// fetch; empty means the DB is unreachable, in which case no header is sent.
|
const sendHwidKey = "externalSubSendHwid"
|
||||||
|
|
||||||
|
// sendServerHwid reports whether to attach our stable id. Missing row or
|
||||||
|
// parse failure keeps the default (send) so donor sync works out of box.
|
||||||
|
func sendServerHwid() bool {
|
||||||
|
db := database.GetDB()
|
||||||
|
if db == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var row model.Setting
|
||||||
|
if err := db.Where("key = ?", sendHwidKey).First(&row).Error; err != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
v := strings.TrimSpace(strings.ToLower(row.Value))
|
||||||
|
return v != "false" && v != "0" && v != "no" && v != "off"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
func serverHwid() string {
|
||||||
|
serverHwidMu.Lock()
|
||||||
|
defer serverHwidMu.Unlock()
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
if db == nil {
|
if db == nil {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
Reference in New Issue
Block a user