feat: 支持自定义订阅节点并兼容现有数据库
This commit is contained in:
+60
-25
@@ -1,14 +1,11 @@
|
||||
import { renderCustomProxyYaml } from "@/lib/custom-proxy";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { decryptText } from "@/lib/crypto";
|
||||
import { getEnv } from "@/lib/env";
|
||||
import type {
|
||||
Hy2NodeRow,
|
||||
Hy2UserRow,
|
||||
ProxyGroupConfig,
|
||||
} from "@/lib/store";
|
||||
import { buildNodeAuthUrl, formatBytes } from "@/lib/store";
|
||||
import type { Hy2NodeRow, Hy2UserRow, ProxyGroupConfig } from "@/lib/store";
|
||||
import { buildNodeAuthUrl, formatBytes, getNodeProxyName } from "@/lib/store";
|
||||
|
||||
type ClashSettings = {
|
||||
template: string;
|
||||
@@ -31,13 +28,13 @@ function indent(text: string, spaces: number) {
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function renderProxy(node: Hy2NodeRow, authString: string) {
|
||||
function renderHy2Proxy(node: Hy2NodeRow, authString: string) {
|
||||
const lines = [
|
||||
`- name: "${node.client_name || node.name}"`,
|
||||
`- name: ${JSON.stringify(node.client_name || node.name)}`,
|
||||
" type: hysteria2",
|
||||
` server: ${node.server_host}`,
|
||||
` port: ${node.server_port}`,
|
||||
` password: "${authString}"`,
|
||||
` password: ${JSON.stringify(authString)}`,
|
||||
];
|
||||
|
||||
if (node.sni) {
|
||||
@@ -55,10 +52,30 @@ function renderProxy(node: Hy2NodeRow, authString: string) {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function renderSubscriptionProxy(node: Hy2NodeRow, authString: string) {
|
||||
if (node.node_type === "custom") {
|
||||
return renderCustomProxyYaml(node.custom_config_yaml);
|
||||
}
|
||||
|
||||
return renderHy2Proxy(node, authString);
|
||||
}
|
||||
|
||||
function isEligibleSubscriptionNode(node: Hy2NodeRow) {
|
||||
if (!node.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.node_type === "custom") {
|
||||
return Boolean(node.custom_config_yaml.trim());
|
||||
}
|
||||
|
||||
return Boolean(node.server_host && node.server_port > 0);
|
||||
}
|
||||
|
||||
function renderGroupEntry(name: string, proxyNames: string[]) {
|
||||
const quoted = proxyNames.map((name) => `"${name}"`).join(", ");
|
||||
const quoted = proxyNames.map((name) => JSON.stringify(name)).join(", ");
|
||||
return [
|
||||
` - name: ${name}`,
|
||||
` - name: ${JSON.stringify(name)}`,
|
||||
" type: select",
|
||||
` proxies: [${quoted}]`,
|
||||
].join("\n");
|
||||
@@ -94,7 +111,10 @@ function orderNodesBySlugs(nodes: Hy2NodeRow[], orderedSlugs: string[]) {
|
||||
|
||||
export function encodeRFC5987ValueChars(value: string) {
|
||||
return encodeURIComponent(value)
|
||||
.replace(/['()]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`)
|
||||
.replace(
|
||||
/['()]/g,
|
||||
(char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`,
|
||||
)
|
||||
.replace(/\*/g, "%2A")
|
||||
.replace(/%(7C|60|5E)/g, (_, code) =>
|
||||
String.fromCharCode(Number.parseInt(code, 16)),
|
||||
@@ -112,7 +132,10 @@ function buildAsciiFilenameFallback(value: string) {
|
||||
return ascii || "subscription";
|
||||
}
|
||||
|
||||
export function buildSubscriptionHeaders(user: Hy2UserRow, profileName: string) {
|
||||
export function buildSubscriptionHeaders(
|
||||
user: Hy2UserRow,
|
||||
profileName: string,
|
||||
) {
|
||||
const expire = user.expires_at
|
||||
? Math.round(new Date(user.expires_at).getTime() / 1000)
|
||||
: user.is_admin
|
||||
@@ -137,7 +160,10 @@ export function buildSubscriptionUrl(token: string, origin = getEnv().APP_URL) {
|
||||
return `${origin}/api/subscription/${token}/clash`;
|
||||
}
|
||||
|
||||
export function buildNodeHy2Config(node: Hy2NodeRow, origin = getEnv().APP_URL) {
|
||||
export function buildNodeHy2Config(
|
||||
node: Hy2NodeRow,
|
||||
origin = getEnv().APP_URL,
|
||||
) {
|
||||
const authUrl = buildNodeAuthUrl(node.slug, node.auth_token, origin);
|
||||
|
||||
return [
|
||||
@@ -161,25 +187,34 @@ export function buildSubscriptionConfig(input: {
|
||||
nodes: Hy2NodeRow[];
|
||||
settings: ClashSettings;
|
||||
}) {
|
||||
const authPassword = decryptText(input.user.auth_secret_encrypted);
|
||||
if (!authPassword) {
|
||||
const eligibleNodes = input.nodes.filter((node) =>
|
||||
isEligibleSubscriptionNode(node),
|
||||
);
|
||||
const needsUserAuth = eligibleNodes.some((node) => node.node_type === "hy2");
|
||||
const authPassword = needsUserAuth
|
||||
? decryptText(input.user.auth_secret_encrypted)
|
||||
: null;
|
||||
|
||||
if (needsUserAuth && !authPassword) {
|
||||
throw new Error("user_secret_unavailable");
|
||||
}
|
||||
|
||||
const authString = `${input.user.username}:${authPassword}`;
|
||||
const eligibleNodes = input.nodes.filter(
|
||||
(node) => node.enabled && node.server_host && node.server_port > 0,
|
||||
);
|
||||
const authString = authPassword
|
||||
? `${input.user.username}:${authPassword}`
|
||||
: "";
|
||||
const orderedEligibleNodes = orderNodesBySlugs(
|
||||
eligibleNodes,
|
||||
input.settings.mainProxyGroupMemberSlugs,
|
||||
);
|
||||
const nodeNameBySlug = new Map(
|
||||
eligibleNodes.map((node) => [node.slug, node.client_name || node.name]),
|
||||
eligibleNodes.map((node) => [node.slug, getNodeProxyName(node)]),
|
||||
);
|
||||
const proxyNames = orderedEligibleNodes.map((node) => node.client_name || node.name);
|
||||
const proxiesBlock = orderedEligibleNodes.map((node) => renderProxy(node, authString)).join("\n");
|
||||
const defaultTemplate = input.settings.template.trimEnd() || readDefaultTemplate();
|
||||
const proxyNames = orderedEligibleNodes.map((node) => getNodeProxyName(node));
|
||||
const proxiesBlock = orderedEligibleNodes
|
||||
.map((node) => renderSubscriptionProxy(node, authString))
|
||||
.join("\n");
|
||||
const defaultTemplate =
|
||||
input.settings.template.trimEnd() || readDefaultTemplate();
|
||||
const renderedGroups = [
|
||||
renderGroupEntry(input.settings.profileName || "FeieProxy", proxyNames),
|
||||
...input.settings.proxyGroups
|
||||
@@ -202,7 +237,7 @@ export function buildSubscriptionConfig(input: {
|
||||
return [
|
||||
defaultTemplate,
|
||||
"",
|
||||
"proxies:",
|
||||
proxiesBlock ? "proxies:" : "proxies: []",
|
||||
indent(proxiesBlock, 2),
|
||||
"",
|
||||
"proxy-groups:",
|
||||
|
||||
Reference in New Issue
Block a user