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:
@@ -13569,6 +13569,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Prefix for generated outbound tags. Defaults to the lowest free \"sub<N>-\" prefix."
|
"description": "Prefix for generated outbound tags. Defaults to the lowest free \"sub<N>-\" prefix."
|
||||||
},
|
},
|
||||||
|
"userAgent": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "3x-ui-outbound-sub/1.0",
|
||||||
|
"description": "Custom User-Agent sent when fetching this subscription. Defaults to \"3x-ui-outbound-sub/1.0\"."
|
||||||
|
},
|
||||||
"updateInterval": {
|
"updateInterval": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"default": 600,
|
"default": 600,
|
||||||
@@ -13662,6 +13667,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Prefix for generated outbound tags. Defaults to the lowest free \"sub<N>-\" prefix."
|
"description": "Prefix for generated outbound tags. Defaults to the lowest free \"sub<N>-\" prefix."
|
||||||
},
|
},
|
||||||
|
"userAgent": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "3x-ui-outbound-sub/1.0",
|
||||||
|
"description": "Custom User-Agent sent when fetching this subscription. Defaults to \"3x-ui-outbound-sub/1.0\"."
|
||||||
|
},
|
||||||
"updateInterval": {
|
"updateInterval": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"default": 600,
|
"default": 600,
|
||||||
@@ -13914,6 +13924,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Subscription URL to preview (required)."
|
"description": "Subscription URL to preview (required)."
|
||||||
},
|
},
|
||||||
|
"userAgent": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Custom User-Agent sent while fetching the preview."
|
||||||
|
},
|
||||||
"allowPrivate": {
|
"allowPrivate": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Allow a private/internal/loopback URL. Default false."
|
"description": "Allow a private/internal/loopback URL. Default false."
|
||||||
|
|||||||
@@ -13569,6 +13569,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Prefix for generated outbound tags. Defaults to the lowest free \"sub<N>-\" prefix."
|
"description": "Prefix for generated outbound tags. Defaults to the lowest free \"sub<N>-\" prefix."
|
||||||
},
|
},
|
||||||
|
"userAgent": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "3x-ui-outbound-sub/1.0",
|
||||||
|
"description": "Custom User-Agent sent when fetching this subscription. Defaults to \"3x-ui-outbound-sub/1.0\"."
|
||||||
|
},
|
||||||
"updateInterval": {
|
"updateInterval": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"default": 600,
|
"default": 600,
|
||||||
@@ -13662,6 +13667,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Prefix for generated outbound tags. Defaults to the lowest free \"sub<N>-\" prefix."
|
"description": "Prefix for generated outbound tags. Defaults to the lowest free \"sub<N>-\" prefix."
|
||||||
},
|
},
|
||||||
|
"userAgent": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "3x-ui-outbound-sub/1.0",
|
||||||
|
"description": "Custom User-Agent sent when fetching this subscription. Defaults to \"3x-ui-outbound-sub/1.0\"."
|
||||||
|
},
|
||||||
"updateInterval": {
|
"updateInterval": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"default": 600,
|
"default": 600,
|
||||||
@@ -13914,6 +13924,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Subscription URL to preview (required)."
|
"description": "Subscription URL to preview (required)."
|
||||||
},
|
},
|
||||||
|
"userAgent": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Custom User-Agent sent while fetching the preview."
|
||||||
|
},
|
||||||
"allowPrivate": {
|
"allowPrivate": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Allow a private/internal/loopback URL. Default false."
|
"description": "Allow a private/internal/loopback URL. Default false."
|
||||||
|
|||||||
@@ -90,6 +90,14 @@ const outboundSubscriptionBodyParams: EndpointParam[] = [
|
|||||||
desc: 'Prefix for generated outbound tags. Defaults to the lowest free "sub<N>-" prefix.',
|
desc: 'Prefix for generated outbound tags. Defaults to the lowest free "sub<N>-" prefix.',
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'userAgent',
|
||||||
|
in: 'body (form)',
|
||||||
|
type: 'string',
|
||||||
|
desc: 'Custom User-Agent sent when fetching this subscription. Defaults to "3x-ui-outbound-sub/1.0".',
|
||||||
|
optional: true,
|
||||||
|
defaultValue: '3x-ui-outbound-sub/1.0',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'updateInterval',
|
name: 'updateInterval',
|
||||||
in: 'body (form)',
|
in: 'body (form)',
|
||||||
@@ -2502,6 +2510,13 @@ export const sections: readonly Section[] = [
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
desc: 'Subscription URL to preview (required).',
|
desc: 'Subscription URL to preview (required).',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'userAgent',
|
||||||
|
in: 'body (form)',
|
||||||
|
type: 'string',
|
||||||
|
desc: 'Custom User-Agent sent while fetching the preview.',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'allowPrivate',
|
name: 'allowPrivate',
|
||||||
in: 'body (form)',
|
in: 'body (form)',
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ import { useOutboundColumns } from './useOutboundColumns';
|
|||||||
import OutboundCardList from './OutboundCardList';
|
import OutboundCardList from './OutboundCardList';
|
||||||
import SubscriptionOutbounds from './SubscriptionOutbounds';
|
import SubscriptionOutbounds from './SubscriptionOutbounds';
|
||||||
|
|
||||||
|
const defaultOutboundSubscriptionUserAgent = '3x-ui-outbound-sub/1.0';
|
||||||
|
|
||||||
interface OutboundSub {
|
interface OutboundSub {
|
||||||
id: number;
|
id: number;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
@@ -69,6 +71,7 @@ interface OutboundSub {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
allowPrivate?: boolean;
|
allowPrivate?: boolean;
|
||||||
allowInsecure?: boolean;
|
allowInsecure?: boolean;
|
||||||
|
userAgent?: string;
|
||||||
prepend?: boolean;
|
prepend?: boolean;
|
||||||
priority?: number;
|
priority?: number;
|
||||||
tagPrefix?: string;
|
tagPrefix?: string;
|
||||||
@@ -136,6 +139,7 @@ export default function OutboundsTab({
|
|||||||
remark: '',
|
remark: '',
|
||||||
url: '',
|
url: '',
|
||||||
tagPrefix: '',
|
tagPrefix: '',
|
||||||
|
userAgent: '',
|
||||||
updateInterval: 600,
|
updateInterval: 600,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
allowPrivate: false,
|
allowPrivate: false,
|
||||||
@@ -334,6 +338,7 @@ export default function OutboundsTab({
|
|||||||
remark?: string;
|
remark?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
tagPrefix?: string;
|
tagPrefix?: string;
|
||||||
|
userAgent?: string;
|
||||||
updateInterval?: number;
|
updateInterval?: number;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
allowPrivate?: boolean;
|
allowPrivate?: boolean;
|
||||||
@@ -344,6 +349,7 @@ export default function OutboundsTab({
|
|||||||
remark: src.remark ?? '',
|
remark: src.remark ?? '',
|
||||||
url: src.url ?? '',
|
url: src.url ?? '',
|
||||||
tagPrefix: src.tagPrefix ?? '',
|
tagPrefix: src.tagPrefix ?? '',
|
||||||
|
userAgent: src.userAgent ?? '',
|
||||||
updateInterval: src.updateInterval ?? 600,
|
updateInterval: src.updateInterval ?? 600,
|
||||||
enabled: src.enabled ?? true,
|
enabled: src.enabled ?? true,
|
||||||
allowPrivate: src.allowPrivate ?? false,
|
allowPrivate: src.allowPrivate ?? false,
|
||||||
@@ -356,6 +362,7 @@ export default function OutboundsTab({
|
|||||||
remark: '',
|
remark: '',
|
||||||
url: '',
|
url: '',
|
||||||
tagPrefix: '',
|
tagPrefix: '',
|
||||||
|
userAgent: '',
|
||||||
updateInterval: 600,
|
updateInterval: 600,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
allowPrivate: false,
|
allowPrivate: false,
|
||||||
@@ -370,6 +377,7 @@ export default function OutboundsTab({
|
|||||||
remark: sub.remark ?? '',
|
remark: sub.remark ?? '',
|
||||||
url: sub.url ?? '',
|
url: sub.url ?? '',
|
||||||
tagPrefix: sub.tagPrefix ?? '',
|
tagPrefix: sub.tagPrefix ?? '',
|
||||||
|
userAgent: sub.userAgent ?? '',
|
||||||
updateInterval: sub.updateInterval ?? 600,
|
updateInterval: sub.updateInterval ?? 600,
|
||||||
enabled: sub.enabled ?? true,
|
enabled: sub.enabled ?? true,
|
||||||
allowPrivate: sub.allowPrivate ?? false,
|
allowPrivate: sub.allowPrivate ?? false,
|
||||||
@@ -423,7 +431,12 @@ export default function OutboundsTab({
|
|||||||
try {
|
try {
|
||||||
const r = await HttpUtil.post<{ tag?: string; protocol?: string }[]>(
|
const r = await HttpUtil.post<{ tag?: string; protocol?: string }[]>(
|
||||||
'/panel/api/xray/outbound-subs/parse',
|
'/panel/api/xray/outbound-subs/parse',
|
||||||
{ url: newSub.url, allowPrivate: newSub.allowPrivate },
|
{
|
||||||
|
url: newSub.url,
|
||||||
|
userAgent: newSub.userAgent,
|
||||||
|
allowPrivate: newSub.allowPrivate,
|
||||||
|
allowInsecure: newSub.allowInsecure,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
if (r?.success && Array.isArray(r.obj)) {
|
if (r?.success && Array.isArray(r.obj)) {
|
||||||
setPreviewData(r.obj);
|
setPreviewData(r.obj);
|
||||||
@@ -721,6 +734,13 @@ export default function OutboundsTab({
|
|||||||
placeholder={t('pages.xray.outboundSub.tagPrefixPlaceholder')}
|
placeholder={t('pages.xray.outboundSub.tagPrefixPlaceholder')}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label={t('pages.xray.outboundSub.userAgent')}>
|
||||||
|
<Input
|
||||||
|
value={newSub.userAgent}
|
||||||
|
onChange={(e) => setNewSub({ ...newSub, userAgent: e.target.value })}
|
||||||
|
placeholder={defaultOutboundSubscriptionUserAgent}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
<Form.Item label={t('pages.xray.outboundSub.interval')}>
|
<Form.Item label={t('pages.xray.outboundSub.interval')}>
|
||||||
<Space>
|
<Space>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
|||||||
@@ -96,10 +96,21 @@ func migrateClientTrafficLastSubFetchColumn() error {
|
|||||||
return migrator.AddColumn(&xray.ClientTraffic{}, "LastSubFetch")
|
return migrator.AddColumn(&xray.ClientTraffic{}, "LastSubFetch")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func migrateOutboundSubscriptionUserAgentColumn() error {
|
||||||
|
migrator := db.Migrator()
|
||||||
|
if !migrator.HasTable(&model.OutboundSubscription{}) || migrator.HasColumn(&model.OutboundSubscription{}, "user_agent") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return migrator.AddColumn(&model.OutboundSubscription{}, "UserAgent")
|
||||||
|
}
|
||||||
|
|
||||||
func initModels() error {
|
func initModels() error {
|
||||||
if err := migrateClientTrafficLastSubFetchColumn(); err != nil {
|
if err := migrateClientTrafficLastSubFetchColumn(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := migrateOutboundSubscriptionUserAgentColumn(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
models := allModels()
|
models := allModels()
|
||||||
for _, mdl := range models {
|
for _, mdl := range models {
|
||||||
if IsPostgres() && postgresModelSettled(mdl) {
|
if IsPostgres() && postgresModelSettled(mdl) {
|
||||||
|
|||||||
@@ -1248,6 +1248,7 @@ type OutboundSubscription struct {
|
|||||||
Enabled bool `json:"enabled" form:"enabled" gorm:"default:true"`
|
Enabled bool `json:"enabled" form:"enabled" gorm:"default:true"`
|
||||||
AllowPrivate bool `json:"allowPrivate" form:"allowPrivate" gorm:"default:false"`
|
AllowPrivate bool `json:"allowPrivate" form:"allowPrivate" gorm:"default:false"`
|
||||||
AllowInsecure bool `json:"allowInsecure" form:"allowInsecure" gorm:"default:false"`
|
AllowInsecure bool `json:"allowInsecure" form:"allowInsecure" gorm:"default:false"`
|
||||||
|
UserAgent string `json:"userAgent" form:"userAgent"`
|
||||||
TagPrefix string `json:"tagPrefix" form:"tagPrefix"`
|
TagPrefix string `json:"tagPrefix" form:"tagPrefix"`
|
||||||
UpdateInterval int `json:"updateInterval" form:"updateInterval" gorm:"default:600"` // seconds between refreshes
|
UpdateInterval int `json:"updateInterval" form:"updateInterval" gorm:"default:600"` // seconds between refreshes
|
||||||
Priority int `json:"priority" form:"priority" gorm:"default:0"` // order among subscriptions in the merged outbounds (lower = earlier)
|
Priority int `json:"priority" form:"priority" gorm:"default:0"` // order among subscriptions in the merged outbounds (lower = earlier)
|
||||||
|
|||||||
@@ -516,6 +516,7 @@ func (a *XraySettingController) createOutboundSub(c *gin.Context) {
|
|||||||
remark := c.PostForm("remark")
|
remark := c.PostForm("remark")
|
||||||
rawURL := c.PostForm("url")
|
rawURL := c.PostForm("url")
|
||||||
prefix := c.PostForm("tagPrefix")
|
prefix := c.PostForm("tagPrefix")
|
||||||
|
userAgent := c.PostForm("userAgent")
|
||||||
enabled := c.PostForm("enabled") != "false"
|
enabled := c.PostForm("enabled") != "false"
|
||||||
allowPrivate := c.PostForm("allowPrivate") == "true"
|
allowPrivate := c.PostForm("allowPrivate") == "true"
|
||||||
allowInsecure := c.PostForm("allowInsecure") == "true"
|
allowInsecure := c.PostForm("allowInsecure") == "true"
|
||||||
@@ -527,7 +528,7 @@ func (a *XraySettingController) createOutboundSub(c *gin.Context) {
|
|||||||
interval = v
|
interval = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sub, err := a.OutboundSubscriptionService.Create(remark, rawURL, prefix, enabled, interval, allowPrivate, prepend, allowInsecure)
|
sub, err := a.OutboundSubscriptionService.Create(remark, rawURL, prefix, userAgent, enabled, interval, allowPrivate, prepend, allowInsecure)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, "Failed to create outbound subscription", err)
|
jsonMsg(c, "Failed to create outbound subscription", err)
|
||||||
return
|
return
|
||||||
@@ -545,6 +546,7 @@ func (a *XraySettingController) updateOutboundSub(c *gin.Context) {
|
|||||||
remark := c.PostForm("remark")
|
remark := c.PostForm("remark")
|
||||||
rawURL := c.PostForm("url")
|
rawURL := c.PostForm("url")
|
||||||
prefix := c.PostForm("tagPrefix")
|
prefix := c.PostForm("tagPrefix")
|
||||||
|
userAgent := c.PostForm("userAgent")
|
||||||
enabled := c.PostForm("enabled") != "false"
|
enabled := c.PostForm("enabled") != "false"
|
||||||
allowPrivate := c.PostForm("allowPrivate") == "true"
|
allowPrivate := c.PostForm("allowPrivate") == "true"
|
||||||
allowInsecure := c.PostForm("allowInsecure") == "true"
|
allowInsecure := c.PostForm("allowInsecure") == "true"
|
||||||
@@ -556,7 +558,7 @@ func (a *XraySettingController) updateOutboundSub(c *gin.Context) {
|
|||||||
interval = v
|
interval = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := a.OutboundSubscriptionService.Update(subID, remark, rawURL, prefix, enabled, interval, allowPrivate, prepend, allowInsecure); err != nil {
|
if err := a.OutboundSubscriptionService.Update(subID, remark, rawURL, prefix, userAgent, enabled, interval, allowPrivate, prepend, allowInsecure); err != nil {
|
||||||
jsonMsg(c, "Failed to update outbound subscription", err)
|
jsonMsg(c, "Failed to update outbound subscription", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -624,12 +626,13 @@ func (a *XraySettingController) parseOutboundSubURL(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
allowPrivate := c.PostForm("allowPrivate") == "true"
|
allowPrivate := c.PostForm("allowPrivate") == "true"
|
||||||
allowInsecure := c.PostForm("allowInsecure") == "true"
|
allowInsecure := c.PostForm("allowInsecure") == "true"
|
||||||
|
userAgent := c.PostForm("userAgent")
|
||||||
// Use a throw-away service instance; it only needs the settingService for proxy.
|
// Use a throw-away service instance; it only needs the settingService for proxy.
|
||||||
svc := service.OutboundSubscriptionService{}
|
svc := service.OutboundSubscriptionService{}
|
||||||
// We don't have a direct "fetch once" that returns without storing, so we
|
// We don't have a direct "fetch once" that returns without storing, so we
|
||||||
// temporarily create a disabled row, refresh it, then delete. Cleaner would
|
// temporarily create a disabled row, refresh it, then delete. Cleaner would
|
||||||
// be to expose a pure ParseURL on the service, but this keeps the surface small.
|
// be to expose a pure ParseURL on the service, but this keeps the surface small.
|
||||||
tmp, err := svc.Create("preview", rawURL, "", false, 600, allowPrivate, false, allowInsecure)
|
tmp, err := svc.Create("preview", rawURL, "", userAgent, false, 600, allowPrivate, false, allowInsecure)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, "Failed to preview subscription", err)
|
jsonMsg(c, "Failed to preview subscription", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ func filterOutboundsRejectedByCore(label string, outbounds []any) ([]any, []stri
|
|||||||
// subscription may aggregate many upstream outbounds into one document.
|
// subscription may aggregate many upstream outbounds into one document.
|
||||||
const maxOutboundSubscriptionBytes int64 = 8 << 20
|
const maxOutboundSubscriptionBytes int64 = 8 << 20
|
||||||
|
|
||||||
|
const defaultOutboundSubscriptionUserAgent = "3x-ui-outbound-sub/1.0"
|
||||||
|
|
||||||
var errOutboundSubscriptionBodyTooLarge = errors.New("outbound subscription response body exceeds size limit")
|
var errOutboundSubscriptionBodyTooLarge = errors.New("outbound subscription response body exceeds size limit")
|
||||||
|
|
||||||
func readBoundedOutboundSubscriptionBody(r io.Reader) ([]byte, error) {
|
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
|
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)
|
cleanURL, err := SanitizePublicHTTPURL(rawURL, allowPrivate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, common.NewError("invalid subscription URL:", err)
|
return nil, common.NewError("invalid subscription URL:", err)
|
||||||
@@ -193,6 +195,7 @@ func (s *OutboundSubscriptionService) Create(remark, rawURL, tagPrefix string, e
|
|||||||
Enabled: enabled,
|
Enabled: enabled,
|
||||||
AllowPrivate: allowPrivate,
|
AllowPrivate: allowPrivate,
|
||||||
AllowInsecure: allowInsecure,
|
AllowInsecure: allowInsecure,
|
||||||
|
UserAgent: strings.TrimSpace(userAgent),
|
||||||
Prepend: prepend,
|
Prepend: prepend,
|
||||||
Priority: int(count),
|
Priority: int(count),
|
||||||
TagPrefix: prefix,
|
TagPrefix: prefix,
|
||||||
@@ -205,7 +208,7 @@ func (s *OutboundSubscriptionService) Create(remark, rawURL, tagPrefix string, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update updates editable fields.
|
// 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)
|
sub, err := s.Get(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -232,6 +235,7 @@ func (s *OutboundSubscriptionService) Update(id int, remark, rawURL, tagPrefix s
|
|||||||
sub.Enabled = enabled
|
sub.Enabled = enabled
|
||||||
sub.AllowPrivate = allowPrivate
|
sub.AllowPrivate = allowPrivate
|
||||||
sub.AllowInsecure = allowInsecure
|
sub.AllowInsecure = allowInsecure
|
||||||
|
sub.UserAgent = strings.TrimSpace(userAgent)
|
||||||
sub.Prepend = prepend
|
sub.Prepend = prepend
|
||||||
sub.TagPrefix = prefix
|
sub.TagPrefix = prefix
|
||||||
sub.UpdateInterval = updateInterval
|
sub.UpdateInterval = updateInterval
|
||||||
@@ -363,7 +367,11 @@ func (s *OutboundSubscriptionService) fetchAndStore(sub *model.OutboundSubscript
|
|||||||
s.recordError(sub, err)
|
s.recordError(sub, err)
|
||||||
return nil, 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)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package service
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -40,7 +42,7 @@ func TestOutboundSubscriptionCreatePropagatesAllocationDatabaseFailures(t *testi
|
|||||||
{name: "priority count query", tagPrefix: "custom-", operation: "priority allocation"},
|
{name: "priority count query", tagPrefix: "custom-", operation: "priority allocation"},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
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) {
|
if !errors.Is(err, errInjected) {
|
||||||
t.Fatalf("Create error = %v, want injected %s query failure", err, tc.operation)
|
t.Fatalf("Create error = %v, want injected %s query failure", err, tc.operation)
|
||||||
}
|
}
|
||||||
@@ -83,7 +85,7 @@ func TestOutboundSubscriptionUpdatePropagatesPrefixQueryFailureWithoutMutation(t
|
|||||||
})
|
})
|
||||||
|
|
||||||
err := (&OutboundSubscriptionService{}).Update(
|
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) {
|
if !errors.Is(err, errInjected) {
|
||||||
t.Fatalf("Update error = %v, want injected prefix query failure", err)
|
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) {
|
func TestReadBoundedOutboundSubscriptionBody(t *testing.T) {
|
||||||
t.Run("accepts body at the limit", func(t *testing.T) {
|
t.Run("accepts body at the limit", func(t *testing.T) {
|
||||||
want := bytes.Repeat([]byte("a"), int(maxOutboundSubscriptionBytes))
|
want := bytes.Repeat([]byte("a"), int(maxOutboundSubscriptionBytes))
|
||||||
|
|||||||
@@ -1788,6 +1788,7 @@
|
|||||||
"urlPlaceholder": "https://... (قائمة روابط بصيغة base64)",
|
"urlPlaceholder": "https://... (قائمة روابط بصيغة base64)",
|
||||||
"tagPrefix": "بادئة الوسم",
|
"tagPrefix": "بادئة الوسم",
|
||||||
"tagPrefixPlaceholder": "hk-",
|
"tagPrefixPlaceholder": "hk-",
|
||||||
|
"userAgent": "User-Agent",
|
||||||
"interval": "فاصل التحديث",
|
"interval": "فاصل التحديث",
|
||||||
"hours": "س",
|
"hours": "س",
|
||||||
"minutes": "د",
|
"minutes": "د",
|
||||||
|
|||||||
@@ -1906,6 +1906,7 @@
|
|||||||
"urlPlaceholder": "https://... (base64 list of links)",
|
"urlPlaceholder": "https://... (base64 list of links)",
|
||||||
"tagPrefix": "Tag prefix",
|
"tagPrefix": "Tag prefix",
|
||||||
"tagPrefixPlaceholder": "hk-",
|
"tagPrefixPlaceholder": "hk-",
|
||||||
|
"userAgent": "User-Agent",
|
||||||
"interval": "Update interval",
|
"interval": "Update interval",
|
||||||
"hours": "h",
|
"hours": "h",
|
||||||
"minutes": "min",
|
"minutes": "min",
|
||||||
|
|||||||
@@ -1788,6 +1788,7 @@
|
|||||||
"urlPlaceholder": "https://... (lista de enlaces en base64)",
|
"urlPlaceholder": "https://... (lista de enlaces en base64)",
|
||||||
"tagPrefix": "Prefijo de etiqueta",
|
"tagPrefix": "Prefijo de etiqueta",
|
||||||
"tagPrefixPlaceholder": "hk-",
|
"tagPrefixPlaceholder": "hk-",
|
||||||
|
"userAgent": "User-Agent",
|
||||||
"interval": "Intervalo de actualización",
|
"interval": "Intervalo de actualización",
|
||||||
"hours": "h",
|
"hours": "h",
|
||||||
"minutes": "min",
|
"minutes": "min",
|
||||||
|
|||||||
@@ -1788,6 +1788,7 @@
|
|||||||
"urlPlaceholder": "https://... (فهرست base64 از لینکها)",
|
"urlPlaceholder": "https://... (فهرست base64 از لینکها)",
|
||||||
"tagPrefix": "پیشوند تگ",
|
"tagPrefix": "پیشوند تگ",
|
||||||
"tagPrefixPlaceholder": "hk-",
|
"tagPrefixPlaceholder": "hk-",
|
||||||
|
"userAgent": "User-Agent",
|
||||||
"interval": "بازه بهروزرسانی",
|
"interval": "بازه بهروزرسانی",
|
||||||
"hours": "ساعت",
|
"hours": "ساعت",
|
||||||
"minutes": "دقیقه",
|
"minutes": "دقیقه",
|
||||||
|
|||||||
@@ -1788,6 +1788,7 @@
|
|||||||
"urlPlaceholder": "https://... (daftar tautan base64)",
|
"urlPlaceholder": "https://... (daftar tautan base64)",
|
||||||
"tagPrefix": "Awalan tag",
|
"tagPrefix": "Awalan tag",
|
||||||
"tagPrefixPlaceholder": "hk-",
|
"tagPrefixPlaceholder": "hk-",
|
||||||
|
"userAgent": "User-Agent",
|
||||||
"interval": "Interval pembaruan",
|
"interval": "Interval pembaruan",
|
||||||
"hours": "j",
|
"hours": "j",
|
||||||
"minutes": "mnt",
|
"minutes": "mnt",
|
||||||
|
|||||||
@@ -1788,6 +1788,7 @@
|
|||||||
"urlPlaceholder": "https://...(リンクのbase64リスト)",
|
"urlPlaceholder": "https://...(リンクのbase64リスト)",
|
||||||
"tagPrefix": "タグのプレフィックス",
|
"tagPrefix": "タグのプレフィックス",
|
||||||
"tagPrefixPlaceholder": "hk-",
|
"tagPrefixPlaceholder": "hk-",
|
||||||
|
"userAgent": "User-Agent",
|
||||||
"interval": "更新間隔",
|
"interval": "更新間隔",
|
||||||
"hours": "時間",
|
"hours": "時間",
|
||||||
"minutes": "分",
|
"minutes": "分",
|
||||||
|
|||||||
@@ -1788,6 +1788,7 @@
|
|||||||
"urlPlaceholder": "https://... (lista de links em base64)",
|
"urlPlaceholder": "https://... (lista de links em base64)",
|
||||||
"tagPrefix": "Prefixo da tag",
|
"tagPrefix": "Prefixo da tag",
|
||||||
"tagPrefixPlaceholder": "hk-",
|
"tagPrefixPlaceholder": "hk-",
|
||||||
|
"userAgent": "User-Agent",
|
||||||
"interval": "Intervalo de atualização",
|
"interval": "Intervalo de atualização",
|
||||||
"hours": "h",
|
"hours": "h",
|
||||||
"minutes": "min",
|
"minutes": "min",
|
||||||
|
|||||||
@@ -1788,6 +1788,7 @@
|
|||||||
"urlPlaceholder": "https://... (список ссылок в base64)",
|
"urlPlaceholder": "https://... (список ссылок в base64)",
|
||||||
"tagPrefix": "Префикс тега",
|
"tagPrefix": "Префикс тега",
|
||||||
"tagPrefixPlaceholder": "hk-",
|
"tagPrefixPlaceholder": "hk-",
|
||||||
|
"userAgent": "User-Agent",
|
||||||
"interval": "Интервал обновления",
|
"interval": "Интервал обновления",
|
||||||
"hours": "ч",
|
"hours": "ч",
|
||||||
"minutes": "мин",
|
"minutes": "мин",
|
||||||
|
|||||||
@@ -1788,6 +1788,7 @@
|
|||||||
"urlPlaceholder": "https://... (bağlantıların base64 listesi)",
|
"urlPlaceholder": "https://... (bağlantıların base64 listesi)",
|
||||||
"tagPrefix": "Etiket öneki",
|
"tagPrefix": "Etiket öneki",
|
||||||
"tagPrefixPlaceholder": "hk-",
|
"tagPrefixPlaceholder": "hk-",
|
||||||
|
"userAgent": "User-Agent",
|
||||||
"interval": "Güncelleme aralığı",
|
"interval": "Güncelleme aralığı",
|
||||||
"hours": "sa",
|
"hours": "sa",
|
||||||
"minutes": "dk",
|
"minutes": "dk",
|
||||||
|
|||||||
@@ -1788,6 +1788,7 @@
|
|||||||
"urlPlaceholder": "https://... (список посилань у base64)",
|
"urlPlaceholder": "https://... (список посилань у base64)",
|
||||||
"tagPrefix": "Префікс тегу",
|
"tagPrefix": "Префікс тегу",
|
||||||
"tagPrefixPlaceholder": "hk-",
|
"tagPrefixPlaceholder": "hk-",
|
||||||
|
"userAgent": "User-Agent",
|
||||||
"interval": "Інтервал оновлення",
|
"interval": "Інтервал оновлення",
|
||||||
"hours": "год",
|
"hours": "год",
|
||||||
"minutes": "хв",
|
"minutes": "хв",
|
||||||
|
|||||||
@@ -1788,6 +1788,7 @@
|
|||||||
"urlPlaceholder": "https://... (danh sách liên kết base64)",
|
"urlPlaceholder": "https://... (danh sách liên kết base64)",
|
||||||
"tagPrefix": "Tiền tố tag",
|
"tagPrefix": "Tiền tố tag",
|
||||||
"tagPrefixPlaceholder": "hk-",
|
"tagPrefixPlaceholder": "hk-",
|
||||||
|
"userAgent": "User-Agent",
|
||||||
"interval": "Khoảng cập nhật",
|
"interval": "Khoảng cập nhật",
|
||||||
"hours": "giờ",
|
"hours": "giờ",
|
||||||
"minutes": "phút",
|
"minutes": "phút",
|
||||||
|
|||||||
@@ -1788,6 +1788,7 @@
|
|||||||
"urlPlaceholder": "https://...(base64 编码的链接列表)",
|
"urlPlaceholder": "https://...(base64 编码的链接列表)",
|
||||||
"tagPrefix": "标签前缀",
|
"tagPrefix": "标签前缀",
|
||||||
"tagPrefixPlaceholder": "hk-",
|
"tagPrefixPlaceholder": "hk-",
|
||||||
|
"userAgent": "User-Agent",
|
||||||
"interval": "更新间隔",
|
"interval": "更新间隔",
|
||||||
"hours": "时",
|
"hours": "时",
|
||||||
"minutes": "分",
|
"minutes": "分",
|
||||||
|
|||||||
@@ -1788,6 +1788,7 @@
|
|||||||
"urlPlaceholder": "https://...(base64 連結清單)",
|
"urlPlaceholder": "https://...(base64 連結清單)",
|
||||||
"tagPrefix": "標籤前綴",
|
"tagPrefix": "標籤前綴",
|
||||||
"tagPrefixPlaceholder": "hk-",
|
"tagPrefixPlaceholder": "hk-",
|
||||||
|
"userAgent": "User-Agent",
|
||||||
"interval": "更新間隔",
|
"interval": "更新間隔",
|
||||||
"hours": "時",
|
"hours": "時",
|
||||||
"minutes": "分",
|
"minutes": "分",
|
||||||
|
|||||||
Reference in New Issue
Block a user