mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-17 15:47:14 +00:00
feat(outbounds): support custom subscription user agents (#6398)
Some subscription providers require a client-specific User-Agent before returning outbound links. Persist an optional value per subscription and use it for refreshes and previews while preserving the existing default for blank values.
This commit is contained in:
@@ -58,6 +58,8 @@ func filterOutboundsRejectedByCore(label string, outbounds []any) ([]any, []stri
|
||||
// subscription may aggregate many upstream outbounds into one document.
|
||||
const maxOutboundSubscriptionBytes int64 = 8 << 20
|
||||
|
||||
const defaultOutboundSubscriptionUserAgent = "3x-ui-outbound-sub/1.0"
|
||||
|
||||
var errOutboundSubscriptionBodyTooLarge = errors.New("outbound subscription response body exceeds size limit")
|
||||
|
||||
func readBoundedOutboundSubscriptionBody(r io.Reader) ([]byte, error) {
|
||||
@@ -164,7 +166,7 @@ func (s *OutboundSubscriptionService) nextDefaultSubPrefix(excludeId int) (strin
|
||||
return fmt.Sprintf("sub%d-", defaultPrefixNumber(subs, excludeId)), nil
|
||||
}
|
||||
|
||||
func (s *OutboundSubscriptionService) Create(remark, rawURL, tagPrefix string, enabled bool, updateInterval int, allowPrivate, prepend, allowInsecure bool) (*model.OutboundSubscription, error) {
|
||||
func (s *OutboundSubscriptionService) Create(remark, rawURL, tagPrefix, userAgent string, enabled bool, updateInterval int, allowPrivate, prepend, allowInsecure bool) (*model.OutboundSubscription, error) {
|
||||
cleanURL, err := SanitizePublicHTTPURL(rawURL, allowPrivate)
|
||||
if err != nil {
|
||||
return nil, common.NewError("invalid subscription URL:", err)
|
||||
@@ -193,6 +195,7 @@ func (s *OutboundSubscriptionService) Create(remark, rawURL, tagPrefix string, e
|
||||
Enabled: enabled,
|
||||
AllowPrivate: allowPrivate,
|
||||
AllowInsecure: allowInsecure,
|
||||
UserAgent: strings.TrimSpace(userAgent),
|
||||
Prepend: prepend,
|
||||
Priority: int(count),
|
||||
TagPrefix: prefix,
|
||||
@@ -205,7 +208,7 @@ func (s *OutboundSubscriptionService) Create(remark, rawURL, tagPrefix string, e
|
||||
}
|
||||
|
||||
// Update updates editable fields.
|
||||
func (s *OutboundSubscriptionService) Update(id int, remark, rawURL, tagPrefix string, enabled bool, updateInterval int, allowPrivate, prepend, allowInsecure bool) error {
|
||||
func (s *OutboundSubscriptionService) Update(id int, remark, rawURL, tagPrefix, userAgent string, enabled bool, updateInterval int, allowPrivate, prepend, allowInsecure bool) error {
|
||||
sub, err := s.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -232,6 +235,7 @@ func (s *OutboundSubscriptionService) Update(id int, remark, rawURL, tagPrefix s
|
||||
sub.Enabled = enabled
|
||||
sub.AllowPrivate = allowPrivate
|
||||
sub.AllowInsecure = allowInsecure
|
||||
sub.UserAgent = strings.TrimSpace(userAgent)
|
||||
sub.Prepend = prepend
|
||||
sub.TagPrefix = prefix
|
||||
sub.UpdateInterval = updateInterval
|
||||
@@ -363,7 +367,11 @@ func (s *OutboundSubscriptionService) fetchAndStore(sub *model.OutboundSubscript
|
||||
s.recordError(sub, err)
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "3x-ui-outbound-sub/1.0")
|
||||
userAgent := strings.TrimSpace(sub.UserAgent)
|
||||
if userAgent == "" {
|
||||
userAgent = defaultOutboundSubscriptionUserAgent
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,6 +3,8 @@ package service
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
@@ -40,7 +42,7 @@ func TestOutboundSubscriptionCreatePropagatesAllocationDatabaseFailures(t *testi
|
||||
{name: "priority count query", tagPrefix: "custom-", operation: "priority allocation"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
created, err := (&OutboundSubscriptionService{}).Create("test", "https://1.1.1.1/sub", tc.tagPrefix, true, 600, false, false, false)
|
||||
created, err := (&OutboundSubscriptionService{}).Create("test", "https://1.1.1.1/sub", tc.tagPrefix, "", true, 600, false, false, false)
|
||||
if !errors.Is(err, errInjected) {
|
||||
t.Fatalf("Create error = %v, want injected %s query failure", err, tc.operation)
|
||||
}
|
||||
@@ -83,7 +85,7 @@ func TestOutboundSubscriptionUpdatePropagatesPrefixQueryFailureWithoutMutation(t
|
||||
})
|
||||
|
||||
err := (&OutboundSubscriptionService{}).Update(
|
||||
original.Id, "after", "https://1.1.1.1/changed", "", false, 1200, false, false, false,
|
||||
original.Id, "after", "https://1.1.1.1/changed", "", "", false, 1200, false, false, false,
|
||||
)
|
||||
if !errors.Is(err, errInjected) {
|
||||
t.Fatalf("Update error = %v, want injected prefix query failure", err)
|
||||
@@ -102,6 +104,30 @@ func TestOutboundSubscriptionUpdatePropagatesPrefixQueryFailureWithoutMutation(t
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutboundSubscriptionRefreshUsesCustomUserAgent(t *testing.T) {
|
||||
setupSettingTestDB(t)
|
||||
const wantUserAgent = "ClashMetaForAndroid/2.11.13"
|
||||
var gotUserAgent string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotUserAgent = r.UserAgent()
|
||||
_, _ = w.Write([]byte("vless://00000000-0000-4000-8000-000000000000@1.1.1.1:443?security=tls&type=tcp#node"))
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
sub := &model.OutboundSubscription{
|
||||
Url: server.URL, AllowPrivate: true, UserAgent: wantUserAgent, TagPrefix: "test-",
|
||||
}
|
||||
if err := database.GetDB().Create(sub).Error; err != nil {
|
||||
t.Fatalf("seed subscription: %v", err)
|
||||
}
|
||||
if _, err := (&OutboundSubscriptionService{}).Refresh(sub.Id); err != nil {
|
||||
t.Fatalf("Refresh: %v", err)
|
||||
}
|
||||
if gotUserAgent != wantUserAgent {
|
||||
t.Fatalf("User-Agent = %q, want %q", gotUserAgent, wantUserAgent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadBoundedOutboundSubscriptionBody(t *testing.T) {
|
||||
t.Run("accepts body at the limit", func(t *testing.T) {
|
||||
want := bytes.Repeat([]byte("a"), int(maxOutboundSubscriptionBytes))
|
||||
|
||||
Reference in New Issue
Block a user