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:
Timur Chernykh
2026-09-11 16:32:04 +03:00
committed by GitHub
parent 89ee1242bd
commit 9f07951ba7
22 changed files with 134 additions and 9 deletions
@@ -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))