Skip to content

Commit ad5a0e3

Browse files
committed
feat: 支持订阅流量请求传入自定义 headers
- 增加订阅下载与合集下载链路对 flowHeaders 的透传(download、subscriptions、flow下载工具) - getFlowHeaders 支持从 flowHeaders/headers 参数读取自定义请求头,兼容 JSON 字符串与对象 - 自定义 headers 纳入缓存 key,避免不同请求头复用到错误的流量缓存 - 在 flow 信息获取时优先使用自定义 headers 发起 GET/HEAD 请求,并保留失败兜底行为 - 新增 flow.spec.js,覆盖 flowHeaders 从 URL/片段写入后生效的场景 - 未暂存变更里包含 backend/package.json 版本号升级到 2.22.20(若一起提交可补充到提交说明)
1 parent c5ab519 commit ad5a0e3

6 files changed

Lines changed: 228 additions & 21 deletions

File tree

backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sub-store",
3-
"version": "2.22.19",
3+
"version": "2.22.20",
44
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
55
"main": "src/main.js",
66
"scripts": {

backend/src/restful/download.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ async function downloadSubscription(req, res) {
280280
undefined,
281281
proxy || sub.proxy,
282282
$arguments.flowUrl,
283+
$arguments.flowHeaders,
283284
);
284285
if (flowInfo) {
285286
const headers = normalizeFlowHeader(flowInfo, true);
@@ -569,6 +570,7 @@ async function downloadCollection(req, res) {
569570
undefined,
570571
proxy || sub.proxy || collection.proxy,
571572
$arguments.flowUrl,
573+
$arguments.flowHeaders,
572574
);
573575
}
574576
} catch (err) {

backend/src/restful/subscriptions.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ async function getFlowInfo(req, res) {
162162
undefined,
163163
sub.proxy,
164164
$arguments.flowUrl,
165+
$arguments.flowHeaders,
165166
);
166167
if (!flowHeaders && !sub.subUserinfo) {
167168
failed(
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { expect } from 'chai';
2+
import { after, before, beforeEach, describe, it } from 'mocha';
3+
4+
import { HEADERS_RESOURCE_CACHE_KEY, SETTINGS_KEY } from '@/constants';
5+
6+
let $;
7+
let openApi;
8+
let getFlowHeaders;
9+
let headersResourceCache;
10+
let originalRead;
11+
let originalWrite;
12+
let originalInfo;
13+
let originalError;
14+
let originalHTTP;
15+
let originalENV;
16+
let state;
17+
let capturedRequests;
18+
let infoLogs;
19+
20+
describe('flow headers requests', function () {
21+
before(function () {
22+
({ default: $ } = require('@/core/app'));
23+
openApi = require('@/vendor/open-api');
24+
({ getFlowHeaders } = require('@/utils/flow'));
25+
({ default: headersResourceCache } = require(
26+
'@/utils/headers-resource-cache'
27+
));
28+
29+
originalRead = $.read.bind($);
30+
originalWrite = $.write.bind($);
31+
originalInfo = $.info.bind($);
32+
originalError = $.error.bind($);
33+
originalHTTP = openApi.HTTP;
34+
originalENV = openApi.ENV;
35+
});
36+
37+
after(function () {
38+
if ($) {
39+
$.read = originalRead;
40+
$.write = originalWrite;
41+
$.info = originalInfo;
42+
$.error = originalError;
43+
}
44+
45+
if (openApi) {
46+
openApi.HTTP = originalHTTP;
47+
openApi.ENV = originalENV;
48+
}
49+
});
50+
51+
beforeEach(function () {
52+
capturedRequests = [];
53+
infoLogs = [];
54+
state = {
55+
[SETTINGS_KEY]: {
56+
defaultFlowUserAgent: 'DefaultFlowUA',
57+
},
58+
[HEADERS_RESOURCE_CACHE_KEY]: '{}',
59+
};
60+
61+
$.read = (key) => state[key];
62+
$.write = (data, key) => {
63+
state[key] = data;
64+
return true;
65+
};
66+
$.info = (message) => {
67+
infoLogs.push(message);
68+
};
69+
$.error = () => {};
70+
71+
openApi.ENV = () => ({
72+
isNode: true,
73+
isStash: false,
74+
isLoon: false,
75+
isShadowRocket: false,
76+
isQX: false,
77+
isSurge: false,
78+
isGUIforCores: false,
79+
isEgern: false,
80+
isLanceX: false,
81+
});
82+
openApi.HTTP = () => ({
83+
head: async (options) => {
84+
capturedRequests.push({ method: 'HEAD', options });
85+
return {
86+
headers: {
87+
'subscription-userinfo':
88+
'upload=1; download=2; total=10',
89+
},
90+
};
91+
},
92+
get: async (options) => {
93+
capturedRequests.push({ method: 'GET', options });
94+
return {
95+
body: 'upload=1; download=2; total=10',
96+
headers: {},
97+
statusCode: 200,
98+
};
99+
},
100+
});
101+
102+
headersResourceCache.revokeAll();
103+
});
104+
105+
it('uses flowHeaders from the subscription URL for flow requests', async function () {
106+
await getFlowHeaders(
107+
'https://example.com/sub',
108+
undefined,
109+
undefined,
110+
undefined,
111+
undefined,
112+
JSON.stringify({
113+
'X-Flow-Token': 'token-1',
114+
'User-Agent': 'CustomFlowUA',
115+
}),
116+
);
117+
118+
expect(capturedRequests[0].method).to.equal('HEAD');
119+
expect(capturedRequests[0].options.headers).to.deep.include({
120+
'x-flow-token': 'token-1',
121+
'user-agent': 'CustomFlowUA',
122+
});
123+
expect(infoLogs[0]).to.include(
124+
'{"user-agent":"CustomFlowUA","x-flow-token":"token-1"}',
125+
);
126+
});
127+
128+
it('uses headers embedded in a flow URL such as subUserinfo links', async function () {
129+
await getFlowHeaders(
130+
undefined,
131+
undefined,
132+
undefined,
133+
undefined,
134+
`https://example.com/info#headers=${encodeURIComponent(
135+
JSON.stringify({ 'X-Sub-Token': 'token-2' }),
136+
)}`,
137+
);
138+
139+
expect(capturedRequests[0].method).to.equal('GET');
140+
expect(capturedRequests[0].options.url).to.equal(
141+
'https://example.com/info',
142+
);
143+
expect(capturedRequests[0].options.headers).to.deep.include({
144+
'x-sub-token': 'token-2',
145+
'user-agent': 'DefaultFlowUA',
146+
});
147+
expect(infoLogs[0]).to.include(
148+
'{"user-agent":"DefaultFlowUA","x-sub-token":"token-2"}',
149+
);
150+
});
151+
});

backend/src/utils/download.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ export default async function download(
395395
undefined,
396396
proxy,
397397
$arguments.flowUrl,
398+
$arguments.flowHeaders,
398399
),
399400
),
400401
);

backend/src/utils/flow.js

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export async function getFlowHeaders(
3131
timeout,
3232
customProxy,
3333
flowUrl,
34+
flowHeaders,
3435
) {
3536
let url = flowUrl || rawUrl || '';
3637
let $arguments = {};
@@ -69,25 +70,64 @@ export async function getFlowHeaders(
6970
}
7071
const userAgent = ua || defaultFlowUserAgent || 'clash.meta/v1.19.23';
7172
const requestTimeout = timeout || defaultTimeout || 8000;
72-
const id = hex_md5(userAgent + url);
73+
let customHeaders;
74+
const customHeadersArg =
75+
flowHeaders || $arguments?.flowHeaders || $arguments?.headers;
76+
if (customHeadersArg) {
77+
try {
78+
const parsed =
79+
typeof customHeadersArg === 'string'
80+
? JSON.parse(customHeadersArg)
81+
: customHeadersArg;
82+
if (
83+
parsed &&
84+
typeof parsed === 'object' &&
85+
!Array.isArray(parsed) &&
86+
Object.keys(parsed).length > 0
87+
) {
88+
const lowerCaseHeaders = { 'user-agent': userAgent };
89+
for (const key in parsed) {
90+
lowerCaseHeaders[key.toLowerCase()] = parsed[key];
91+
}
92+
customHeaders = lowerCaseHeaders;
93+
}
94+
} catch (e) {
95+
$.error(
96+
`解析自定义 ${
97+
flowHeaders || $arguments?.flowHeaders
98+
? 'flowHeaders'
99+
: 'headers'
100+
} 失败: ${e}`,
101+
);
102+
}
103+
}
104+
const id = hex_md5(
105+
`${customHeaders ? JSON.stringify(customHeaders) : userAgent}${url}`,
106+
);
73107
const cached = headersResourceCache.get(id);
74108
let flowInfo;
75109
if (!$arguments?.noCache && cached) {
76-
$.info(`使用缓存的流量信息: ${url}, ${userAgent}`);
110+
$.info(
111+
`使用缓存的流量信息: ${url}, ${
112+
customHeaders ? JSON.stringify(customHeaders) : userAgent
113+
}`,
114+
);
77115
flowInfo = cached;
78116
} else {
79117
const http = HTTP();
80118
if (flowUrl) {
81119
let flowUrlHeaders;
82120
try {
83121
$.info(
84-
`使用 GET 方法从响应体获取流量信息: ${flowUrl}, User-Agent: ${
85-
userAgent || ''
122+
`使用 GET 方法从响应体获取流量信息: ${flowUrl}, ${
123+
customHeaders
124+
? JSON.stringify(customHeaders)
125+
: `User-Agent: ${userAgent || ''}`
86126
}, Insecure: ${!!insecure}, Proxy: ${proxy}`,
87127
);
88128
const { headers, body, statusCode } = await http.get({
89-
url: flowUrl,
90-
headers: {
129+
url,
130+
headers: customHeaders || {
91131
'User-Agent': userAgent,
92132
},
93133
timeout: requestTimeout,
@@ -113,8 +153,10 @@ export async function getFlowHeaders(
113153
}
114154
} catch (e) {
115155
$.error(
116-
`使用 GET 方法从响应体获取流量信息失败: ${flowUrl}, User-Agent: ${
117-
userAgent || ''
156+
`使用 GET 方法从响应体获取流量信息失败: ${flowUrl}, ${
157+
customHeaders
158+
? JSON.stringify(customHeaders)
159+
: `User-Agent: ${userAgent || ''}`
118160
}, Insecure: ${!!insecure}, Proxy: ${proxy}: ${
119161
e.message ?? e
120162
}`,
@@ -129,8 +171,10 @@ export async function getFlowHeaders(
129171
Number.isFinite(parsed?.usage?.upload)
130172
) {
131173
$.info(
132-
`使用 GET 方法从响应头获取流量信息成功: ${flowUrl}, User-Agent: ${
133-
userAgent || ''
174+
`使用 GET 方法从响应头获取流量信息成功: ${flowUrl}, ${
175+
customHeaders
176+
? JSON.stringify(customHeaders)
177+
: `User-Agent: ${userAgent || ''}`
134178
}, Insecure: ${!!insecure}, Proxy: ${proxy}`,
135179
);
136180
flowInfo = flowField;
@@ -139,8 +183,10 @@ export async function getFlowHeaders(
139183
}
140184
} catch (e) {
141185
$.error(
142-
`使用 GET 方法从响应头获取流量信息失败: ${flowUrl}, User-Agent: ${
143-
userAgent || ''
186+
`使用 GET 方法从响应头获取流量信息失败: ${flowUrl}, ${
187+
customHeaders
188+
? JSON.stringify(customHeaders)
189+
: `User-Agent: ${userAgent || ''}`
144190
}, Insecure: ${!!insecure}, Proxy: ${proxy}: ${
145191
e.message ?? e
146192
}`,
@@ -151,8 +197,10 @@ export async function getFlowHeaders(
151197
} else {
152198
try {
153199
$.info(
154-
`使用 HEAD 方法从响应头获取流量信息: ${url}, User-Agent: ${
155-
userAgent || ''
200+
`使用 HEAD 方法从响应头获取流量信息: ${url}, ${
201+
customHeaders
202+
? JSON.stringify(customHeaders)
203+
: `User-Agent: ${userAgent || ''}`
156204
}, Insecure: ${!!insecure}, Proxy: ${proxy}`,
157205
);
158206
const { headers } = await http.head({
@@ -161,7 +209,7 @@ export async function getFlowHeaders(
161209
.map((i) => i.trim())
162210
.filter((i) => i.length)[0],
163211
headers: {
164-
'User-Agent': userAgent,
212+
...(customHeaders || { 'User-Agent': userAgent }),
165213
...(isStash && proxy
166214
? {
167215
'X-Stash-Selected-Proxy':
@@ -182,17 +230,21 @@ export async function getFlowHeaders(
182230
flowInfo = getFlowField(headers);
183231
} catch (e) {
184232
$.error(
185-
`使用 HEAD 方法从响应头获取流量信息失败: ${url}, User-Agent: ${
186-
userAgent || ''
233+
`使用 HEAD 方法从响应头获取流量信息失败: ${url}, ${
234+
customHeaders
235+
? JSON.stringify(customHeaders)
236+
: `User-Agent: ${userAgent || ''}`
187237
}, Insecure: ${!!insecure}, Proxy: ${proxy}: ${
188238
e.message ?? e
189239
}`,
190240
);
191241
}
192242
if (!flowInfo) {
193243
$.info(
194-
`使用 GET 方法获取流量信息: ${url}, User-Agent: ${
195-
userAgent || ''
244+
`使用 GET 方法获取流量信息: ${url}, ${
245+
customHeaders
246+
? JSON.stringify(customHeaders)
247+
: `User-Agent: ${userAgent || ''}`
196248
}, Insecure: ${!!insecure}, Proxy: ${proxy}`,
197249
);
198250
const { headers } = await http.get({
@@ -201,7 +253,7 @@ export async function getFlowHeaders(
201253
.map((i) => i.trim())
202254
.filter((i) => i.length)[0],
203255
headers: {
204-
'User-Agent': userAgent,
256+
...(customHeaders || { 'User-Agent': userAgent }),
205257
...(isStash && proxy
206258
? {
207259
'X-Stash-Selected-Proxy':

0 commit comments

Comments
 (0)