Skip to content

Commit b63a907

Browse files
authored
Merge pull request #3 from SubBoost/ryan/dev
Fix subscription import behavior before refreshing v2.4.0 self-host assets.
2 parents 9a96c5b + ff473d0 commit b63a907

4 files changed

Lines changed: 73 additions & 8 deletions

File tree

packages/server-core/src/subscription/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ export * from "./response-headers";
1212
export * from "./saved-sources";
1313
export * from "./source-import";
1414
export * from "./ssrf-ip";
15+
export * from "./user-agents";

packages/server-core/src/subscription/source-import.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ proxies:
1515
password: secret
1616
`;
1717

18+
function ssLink(name = "SS Node"): string {
19+
const credential = Buffer.from("aes-128-gcm:secret").toString("base64url");
20+
return `ss://${credential}@ss.example.com:8388#${encodeURIComponent(name)}`;
21+
}
22+
1823
describe("importSubscriptionFromUrl", () => {
1924
it("tries client user agents and keeps supplemental userinfo headers", async () => {
2025
const fetchText = vi.fn(async (request: SourceImportTransportRequest): Promise<SourceImportTransportResult> => {
@@ -55,6 +60,39 @@ describe("importSubscriptionFromUrl", () => {
5560
expect(fetchText).toHaveBeenCalledWith(expect.objectContaining({ userAgent: "mihomo/1.19.24" }));
5661
});
5762

63+
it("continues to Mihomo when the v2rayN response is link-only and may be incomplete", async () => {
64+
const fetchText = vi.fn(async (request: SourceImportTransportRequest): Promise<SourceImportTransportResult> => {
65+
if (request.userAgent.startsWith("v2rayN/")) {
66+
return { ok: true, content: ssLink("v2rayn"), headers: { "content-type": "text/plain" } };
67+
}
68+
return {
69+
ok: true,
70+
content: [
71+
"proxies:",
72+
" - name: clash-a",
73+
" type: trojan",
74+
" server: clash-a.example.com",
75+
" port: 443",
76+
" password: secret",
77+
" - name: clash-b",
78+
" type: trojan",
79+
" server: clash-b.example.com",
80+
" port: 443",
81+
" password: secret",
82+
].join("\n"),
83+
headers: { "content-type": "text/yaml" },
84+
};
85+
});
86+
87+
const result = await importSubscriptionFromUrl({ url: "https://example.com/sub.yaml" }, { fetchText });
88+
89+
expect(result.ok).toBe(true);
90+
if (!result.ok) return;
91+
expect(result.parsedNodes.map((node) => node.name)).toEqual(["clash-a", "clash-b"]);
92+
expect(fetchText).toHaveBeenCalledWith(expect.objectContaining({ userAgent: "v2rayN/7.20.4" }));
93+
expect(fetchText).toHaveBeenCalledWith(expect.objectContaining({ userAgent: "mihomo/1.19.24" }));
94+
});
95+
5896
it("returns structured network failures", async () => {
5997
const result = await importSubscriptionFromUrl(
6098
{ url: "https://example.com/sub.yaml" },

packages/server-core/src/subscription/source-import.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,8 @@ import {
1111
} from "@subboost/core/subscription/import-error";
1212
import { tryNormalizeSubscriptionUrlInput } from "@subboost/core/subscription/url-input";
1313
import type { ParseResult, ParsedNode } from "@subboost/core/types/node";
14-
15-
export const SUBSCRIPTION_IMPORT_USER_AGENTS = [
16-
"v2rayN/7.20.4",
17-
"mihomo/1.19.24",
18-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
19-
] as const;
14+
import { shouldTryClashMetaForV2raynPayload } from "./fetch-profile-heuristics";
15+
import { SUBSCRIPTION_IMPORT_USER_AGENTS } from "./user-agents";
2016

2117
export type SourceImportPurpose = "content" | "userinfo";
2218

@@ -105,6 +101,22 @@ function isUsableParsedAttempt(attempt: ParsedAttempt): attempt is Extract<Parse
105101
return attempt.ok && attempt.parsed.nodes.length > 0 && !looksLikeClientUpdatePlaceholderNodes(attempt.parsed.nodes);
106102
}
107103

104+
function shouldContinueAfterCleanAttempt(params: {
105+
attempt: ParsedAttempt;
106+
currentUserAgent: string;
107+
nextUserAgent?: string;
108+
}): boolean {
109+
const { attempt, currentUserAgent, nextUserAgent } = params;
110+
if (!isUsableParsedAttempt(attempt) || attempt.parsed.errors.length > 0) return true;
111+
if (
112+
currentUserAgent === SUBSCRIPTION_IMPORT_USER_AGENTS[0] &&
113+
nextUserAgent === SUBSCRIPTION_IMPORT_USER_AGENTS[1]
114+
) {
115+
return shouldTryClashMetaForV2raynPayload(attempt.content, attempt.parsed);
116+
}
117+
return false;
118+
}
119+
108120
function toFailure(attempt: ParsedAttempt | null, fallback = "获取 url 失败"): SourceImportFailure {
109121
if (!attempt) {
110122
return {
@@ -264,14 +276,23 @@ export async function importSubscriptionFromUrl(
264276
const userAgents = options.userAgents?.length ? options.userAgents : SUBSCRIPTION_IMPORT_USER_AGENTS;
265277
let best: ParsedAttempt | null = null;
266278

267-
for (const userAgent of userAgents) {
279+
for (let index = 0; index < userAgents.length; index += 1) {
280+
const userAgent = userAgents[index];
268281
const attempt = await fetchAndParseWithUserAgent(url, userAgent, {
269282
timeoutMs,
270283
maxBytes,
271284
fetchText: options.fetchText,
272285
});
273286
best = pickBetterAttempt(best, attempt);
274-
if (isUsableParsedAttempt(attempt) && attempt.parsed.errors.length === 0) break;
287+
if (
288+
!shouldContinueAfterCleanAttempt({
289+
attempt,
290+
currentUserAgent: userAgent,
291+
nextUserAgent: userAgents[index + 1],
292+
})
293+
) {
294+
break;
295+
}
275296
}
276297

277298
if (!best || !best.ok || !isUsableParsedAttempt(best)) {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const SUBSCRIPTION_IMPORT_USER_AGENTS = [
2+
"v2rayN/7.20.4",
3+
"mihomo/1.19.24",
4+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
5+
] as const;

0 commit comments

Comments
 (0)