Skip to content

Commit 3cf333f

Browse files
committed
feat: 文件链接参数新增 fakeFile, 此时不再使用 name 查询
1 parent 3b4838e commit 3cf333f

6 files changed

Lines changed: 356 additions & 8 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.24.20",
3+
"version": "2.24.21",
44
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
55
"main": "src/main.js",
66
"packageManager": "pnpm@11.0.9",

backend/src/restful/download.js

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { findByName } from '@/utils/database';
55
import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';
66
import $ from '@/core/app';
77
import { failed } from '@/restful/response';
8-
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
8+
import {
9+
InternalServerError,
10+
RequestInvalidError,
11+
ResourceNotFoundError,
12+
} from '@/restful/errors';
913
import { produceArtifact } from '@/restful/sync';
1014
// eslint-disable-next-line no-unused-vars
1115
import { isIPv4, isIPv6 } from '@/utils';
@@ -120,6 +124,7 @@ export default function register($app) {
120124

121125
async function downloadSubscription(req, res) {
122126
let { name, nezhaIndex } = req.params;
127+
const isShareRoute = req.path?.startsWith('/share/');
123128

124129
const { useMihomoExternal, mihomoMerge, mihomoMergeName, mihomoExternal } =
125130
getMihomoExternalOptions(req.query);
@@ -182,6 +187,45 @@ async function downloadSubscription(req, res) {
182187
$.info(`传入 $options: ${JSON.stringify(options)}`);
183188
Object.assign($options, options);
184189
}
190+
if (isShareRoute && _fakeSub) {
191+
$.warn(`分享链接禁止使用 fakeSub: ${name}`);
192+
failed(
193+
res,
194+
new RequestInvalidError(
195+
'UNSUPPORTED_SHARE_FAKE_SUB',
196+
'share/sub 不支持 fakeSub 参数',
197+
),
198+
400,
199+
);
200+
return;
201+
}
202+
if (
203+
isShareRoute &&
204+
((url != null && url !== '') || (content != null && content !== ''))
205+
) {
206+
$.warn(`分享链接禁止使用 url/content: ${name}`);
207+
failed(
208+
res,
209+
new RequestInvalidError(
210+
'UNSUPPORTED_SHARE_SUB_SOURCE_OVERRIDE',
211+
'share/sub 不支持 url 或 content 参数',
212+
),
213+
400,
214+
);
215+
return;
216+
}
217+
if (isShareRoute && mergeSources) {
218+
$.warn(`分享链接禁止使用 mergeSources: ${name}`);
219+
failed(
220+
res,
221+
new RequestInvalidError(
222+
'UNSUPPORTED_SHARE_SUB_MERGE_SOURCES',
223+
'share/sub 不支持 mergeSources 参数',
224+
),
225+
400,
226+
);
227+
return;
228+
}
185229
if (url) {
186230
$.info(`指定远程订阅 URL: ${maskAgeSecretInUrl(url)}`);
187231
if (!/^https?:\/\//.test(url)) {

backend/src/restful/file.js

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ function createFile(req, res) {
5454
async function getFile(req, res, next) {
5555
let { name } = req.params;
5656
const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
57+
const isShareRoute = req.path?.startsWith('/share/');
5758
$.info(`正在下载文件:${name}\n请求 User-Agent: ${reqUA}`);
5859
let {
5960
url,
@@ -66,6 +67,7 @@ async function getFile(req, res, next) {
6667
proxy,
6768
noCache,
6869
produceType,
70+
fakeFile: _fakeFile,
6971
} = req.query;
7072
let $options = {
7173
_req: {
@@ -100,9 +102,79 @@ async function getFile(req, res, next) {
100102
$.info(`传入 $options: ${JSON.stringify(options)}`);
101103
Object.assign($options, options);
102104
}
105+
if (isShareRoute && _fakeFile) {
106+
$.warn(`分享链接禁止使用 fakeFile: ${name}`);
107+
failed(
108+
res,
109+
new RequestInvalidError(
110+
'UNSUPPORTED_SHARE_FAKE_FILE',
111+
'share/file 不支持 fakeFile 参数',
112+
),
113+
400,
114+
);
115+
return;
116+
}
117+
if (
118+
_fakeFile &&
119+
(content == null || content === '') &&
120+
(url == null || url === '')
121+
) {
122+
$.warn(`fakeFile 缺少 content/url: ${name}`);
123+
failed(
124+
res,
125+
new RequestInvalidError(
126+
'INVALID_FAKE_FILE_SOURCE',
127+
'fakeFile 需要提供 content 或 url 参数',
128+
),
129+
400,
130+
);
131+
return;
132+
}
133+
if (
134+
isShareRoute &&
135+
((url != null && url !== '') || (content != null && content !== ''))
136+
) {
137+
$.warn(`分享链接禁止使用 url/content: ${name}`);
138+
failed(
139+
res,
140+
new RequestInvalidError(
141+
'UNSUPPORTED_SHARE_FILE_SOURCE_OVERRIDE',
142+
'share/file 不支持 url 或 content 参数',
143+
),
144+
400,
145+
);
146+
return;
147+
}
148+
if (isShareRoute && subInfoUrl != null && subInfoUrl !== '') {
149+
$.warn(`分享链接禁止使用 subInfoUrl: ${name}`);
150+
failed(
151+
res,
152+
new RequestInvalidError(
153+
'UNSUPPORTED_SHARE_FILE_SUB_INFO_URL',
154+
'share/file 不支持 subInfoUrl 参数',
155+
),
156+
400,
157+
);
158+
return;
159+
}
160+
if (isShareRoute && mergeSources) {
161+
$.warn(`分享链接禁止使用 mergeSources: ${name}`);
162+
failed(
163+
res,
164+
new RequestInvalidError(
165+
'UNSUPPORTED_SHARE_FILE_MERGE_SOURCES',
166+
'share/file 不支持 mergeSources 参数',
167+
),
168+
400,
169+
);
170+
return;
171+
}
103172
if (url) {
104173
$.info(`指定远程文件 URL: ${maskAgeSecretInUrl(url)}`);
105174
}
175+
if (_fakeFile) {
176+
$.info(`使用假文件, 不再通过单个文件名称 ${name} 查询`);
177+
}
106178
if (proxy) {
107179
$.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
108180
}
@@ -132,7 +204,12 @@ async function getFile(req, res, next) {
132204
}
133205

134206
const allFiles = $.read(FILES_KEY);
135-
const file = findByName(allFiles, name);
207+
const fakeFile = {
208+
name: 'fakeFile',
209+
source: 'remote',
210+
url: '',
211+
};
212+
const file = _fakeFile ? fakeFile : findByName(allFiles, name);
136213
if (file) {
137214
try {
138215
const output = await produceArtifact({
@@ -148,6 +225,7 @@ async function getFile(req, res, next) {
148225
noCache,
149226
produceType,
150227
all: true,
228+
file: _fakeFile ? fakeFile : undefined,
151229
});
152230

153231
try {

backend/src/restful/sync.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ async function produceArtifact({
5858
produceType,
5959
produceOpts = {},
6060
subscription,
61+
file: sourceFile,
6162
awaitCustomCache,
6263
$options,
6364
proxy,
@@ -579,7 +580,7 @@ async function produceArtifact({
579580
return RuleUtils.produce(rules, platform);
580581
} else if (type === 'file') {
581582
const allFiles = $.read(FILES_KEY);
582-
const file = findByName(allFiles, name);
583+
const file = sourceFile || findByName(allFiles, name);
583584
if (!file) throw new Error(`找不到文件 ${name}`);
584585
let raw = '';
585586
if (file.type !== 'mihomoProfile') {

backend/src/test/restful/download.spec.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ function createResponse(routePath) {
5656
this.sent = payload;
5757
return this;
5858
},
59+
json(payload) {
60+
this.sent = payload;
61+
return this;
62+
},
5963
set(key, value) {
6064
this.headers[key] = value;
6165
return this;
@@ -327,6 +331,98 @@ describe('download routes', function () {
327331
expect(res.sent).to.equal('local-vless');
328332
});
329333

334+
it('rejects fakeSub on share routes', async function () {
335+
const res = await requestShareSubscription({
336+
query: {
337+
fakeSub: '1',
338+
content: VLESS_WS,
339+
},
340+
shareToken: {
341+
type: 'sub',
342+
name: 'local-vless',
343+
},
344+
});
345+
346+
expect(res.statusCode).to.equal(400);
347+
expect(res.sent.status).to.equal('failed');
348+
expect(res.sent.error.code).to.equal('UNSUPPORTED_SHARE_FAKE_SUB');
349+
expect(res.sent.error.type).to.equal('RequestInvalidError');
350+
});
351+
352+
it('allows _fakeNode on share routes for invalid-share fallback', async function () {
353+
const res = await requestShareSubscription({
354+
query: {
355+
_fakeNode: '1',
356+
target: 'JSON',
357+
},
358+
shareToken: {
359+
type: 'sub',
360+
name: 'local-vless',
361+
},
362+
});
363+
364+
expect(res.statusCode).to.equal(200);
365+
expect(res.sent).to.be.a('string');
366+
expect(res.sent).to.include('fakeNodeInfo');
367+
});
368+
369+
it('rejects url on share routes', async function () {
370+
const res = await requestShareSubscription({
371+
query: {
372+
url: 'https://example.com/sub.txt',
373+
},
374+
shareToken: {
375+
type: 'sub',
376+
name: 'local-vless',
377+
},
378+
});
379+
380+
expect(res.statusCode).to.equal(400);
381+
expect(res.sent.status).to.equal('failed');
382+
expect(res.sent.error.code).to.equal(
383+
'UNSUPPORTED_SHARE_SUB_SOURCE_OVERRIDE',
384+
);
385+
expect(res.sent.error.type).to.equal('RequestInvalidError');
386+
});
387+
388+
it('rejects content on share routes', async function () {
389+
const res = await requestShareSubscription({
390+
query: {
391+
content: VLESS_WS,
392+
},
393+
shareToken: {
394+
type: 'sub',
395+
name: 'local-vless',
396+
},
397+
});
398+
399+
expect(res.statusCode).to.equal(400);
400+
expect(res.sent.status).to.equal('failed');
401+
expect(res.sent.error.code).to.equal(
402+
'UNSUPPORTED_SHARE_SUB_SOURCE_OVERRIDE',
403+
);
404+
expect(res.sent.error.type).to.equal('RequestInvalidError');
405+
});
406+
407+
it('rejects mergeSources on share routes', async function () {
408+
const res = await requestShareSubscription({
409+
query: {
410+
mergeSources: 'remoteFirst',
411+
},
412+
shareToken: {
413+
type: 'sub',
414+
name: 'local-vless',
415+
},
416+
});
417+
418+
expect(res.statusCode).to.equal(400);
419+
expect(res.sent.status).to.equal('failed');
420+
expect(res.sent.error.code).to.equal(
421+
'UNSUPPORTED_SHARE_SUB_MERGE_SOURCES',
422+
);
423+
expect(res.sent.error.type).to.equal('RequestInvalidError');
424+
});
425+
330426
it('encrypts transformed subscription output with source age-public-key', async function () {
331427
const pair = await ageUtils.generateKeyPair();
332428
state[SUBS_KEY][0]['age-public-key'] = pair['age-public-key'];

0 commit comments

Comments
 (0)