feat(sub): make external subscription fetch User-Agent configurable (#6613)

* feat(sub): make external subscription fetch User-Agent configurable

Some providers reject fetches that do not send a known client User-Agent.
Expose externalSubUserAgent as a panel setting (default v2rayNG/1.8.5)
and use it when fetching client external subscription URLs.

Fixes #6383

* ci: retrigger frontend after npm registry maintenance

The frontend job failed solely on `npm audit` while registry.npmjs.org
returned 503 (Service Under Maintenance). Lint, typecheck, vitest, vite
build, and storybook all passed. Local `npm audit --omit=dev
--audit-level=high` now reports 0 vulnerabilities.

* fix(sub): fall back to the default UA when the DB is not initialised

externalSubUserAgent read the setting through SettingService.getSetting,
which calls Model() on database.GetDB() and panics on a nil *gorm.DB.
The fetch path's other DB read, service.ExternalSubscriptionHwid, already
treats a nil DB as unreachable and sends no header; the new UA lookup
did not, so any fetch before InitDB panicked instead of sending the
historical v2rayNG/1.8.5.

Production initialises the DB before the sub server starts, but the
internal/sub fetch tests run without one: under make test-go's
-shuffle=on, whenever one of them ran before the first InitDB test the
panic aborted the whole package. Reproduced deterministically with
go test -run '^TestDoFetchSubscriptionLinks_RejectsOversizedBody$'.

---------

Co-authored-by: mrchatam <mrchatam@users.noreply.github.com>
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
mrchatam
2026-09-27 01:06:33 +03:30
committed by GitHub
parent 9672249edb
commit bd9ccde1f4
26 changed files with 161 additions and 1 deletions
+2
View File
@@ -13,6 +13,7 @@ export const EXAMPLES: Record<string, unknown> = {
"discordMemory": 0,
"discordRunTime": "",
"expireDiff": 0,
"externalSubUserAgent": "",
"externalTrafficInformEnable": false,
"externalTrafficInformURI": "",
"happLinkEnable": false,
@@ -163,6 +164,7 @@ export const EXAMPLES: Record<string, unknown> = {
"discordMemory": 0,
"discordRunTime": "",
"expireDiff": 0,
"externalSubUserAgent": "",
"externalTrafficInformEnable": false,
"externalTrafficInformURI": "",
"happLinkEnable": false,
+8
View File
@@ -43,6 +43,9 @@ export const SCHEMAS: Record<string, unknown> = {
"minimum": 0,
"type": "integer"
},
"externalSubUserAgent": {
"type": "string"
},
"externalTrafficInformEnable": {
"type": "boolean"
},
@@ -496,6 +499,7 @@ export const SCHEMAS: Record<string, unknown> = {
"discordMemory",
"discordRunTime",
"expireDiff",
"externalSubUserAgent",
"externalTrafficInformEnable",
"externalTrafficInformURI",
"happLinkEnable",
@@ -678,6 +682,9 @@ export const SCHEMAS: Record<string, unknown> = {
"minimum": 0,
"type": "integer"
},
"externalSubUserAgent": {
"type": "string"
},
"externalTrafficInformEnable": {
"type": "boolean"
},
@@ -1155,6 +1162,7 @@ export const SCHEMAS: Record<string, unknown> = {
"discordMemory",
"discordRunTime",
"expireDiff",
"externalSubUserAgent",
"externalTrafficInformEnable",
"externalTrafficInformURI",
"happLinkEnable",
+2
View File
@@ -21,6 +21,7 @@ export interface AllSetting {
discordMemory: number;
discordRunTime: string;
expireDiff: number;
externalSubUserAgent: string;
externalTrafficInformEnable: boolean;
externalTrafficInformURI: string;
happLinkEnable: boolean;
@@ -172,6 +173,7 @@ export interface AllSettingView {
discordMemory: number;
discordRunTime: string;
expireDiff: number;
externalSubUserAgent: string;
externalTrafficInformEnable: boolean;
externalTrafficInformURI: string;
happLinkEnable: boolean;
+2
View File
@@ -37,6 +37,7 @@ export const AllSettingSchema = z.object({
discordMemory: z.number().int().min(0).max(100),
discordRunTime: z.string(),
expireDiff: z.number().int().min(0),
externalSubUserAgent: z.string(),
externalTrafficInformEnable: z.boolean(),
externalTrafficInformURI: z.string(),
happLinkEnable: z.boolean(),
@@ -189,6 +190,7 @@ export const AllSettingViewSchema = z.object({
discordMemory: z.number().int().min(0).max(100),
discordRunTime: z.string(),
expireDiff: z.number().int().min(0),
externalSubUserAgent: z.string(),
externalTrafficInformEnable: z.boolean(),
externalTrafficInformURI: z.string(),
happLinkEnable: z.boolean(),
+1
View File
@@ -66,6 +66,7 @@ export class AllSetting {
restartXrayOnClientDisable = true;
subCertFile = '';
subKeyFile = '';
externalSubUserAgent = 'v2rayNG/1.8.5';
subUpdates = 12;
subEncrypt = true;
subURI = '';
@@ -247,6 +247,26 @@ export default function SubscriptionGeneralTab({
onChange={onNumber((v) => updateSetting({ subUpdates: v }))}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.externalSubUserAgent')}
badge={
<DefaultSettingTag
settingKey="externalSubUserAgent"
value={allSetting.externalSubUserAgent}
/>
}
description={t('pages.settings.externalSubUserAgentDesc')}
>
<Input
value={allSetting.externalSubUserAgent}
placeholder="v2rayNG/1.8.5"
maxLength={512}
allowClear
onChange={(e) => updateSetting({ externalSubUserAgent: e.target.value })}
/>
</SettingListItem>
</>
),
},
+1
View File
@@ -72,6 +72,7 @@ export const AllSettingSchema = z
restartXrayOnClientDisable: z.boolean().optional(),
subCertFile: z.string().optional(),
subKeyFile: z.string().optional(),
externalSubUserAgent: z.string().max(512).optional(),
subUpdates: z.number().int().min(0).max(525600).optional(),
subEncrypt: z.boolean().optional(),
subURI: z.string().optional(),