feat: add yaml egress proxy benchmark
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { rootPath } from "./config";
|
||||
|
||||
export type EgressProxySourceType = "subscription-url" | "master-shadowsocks";
|
||||
export type EgressProxyPreferredOutbound = "vless-reality" | "hysteria2";
|
||||
|
||||
export interface MasterShadowsocksSourceSpec {
|
||||
serverHost: string;
|
||||
serverPort: number;
|
||||
method: string;
|
||||
}
|
||||
|
||||
export interface SharedEgressProxySourceSpec {
|
||||
id: string;
|
||||
configRef: string;
|
||||
sourceType: EgressProxySourceType;
|
||||
sourceRef: string;
|
||||
sourceKey: string;
|
||||
preferredOutbound: EgressProxyPreferredOutbound | null;
|
||||
masterShadowsocks: MasterShadowsocksSourceSpec | null;
|
||||
healthProbeUrl: string;
|
||||
fingerprint: string;
|
||||
valuesPrinted: false;
|
||||
}
|
||||
|
||||
export function resolveEgressProxySourceRef(ref: string, label: string): SharedEgressProxySourceSpec {
|
||||
const { file, path } = parseConfigRef(ref, label);
|
||||
if (!path.startsWith("sources.")) throw new Error(`${label} must reference sources.<id>`);
|
||||
const sourceId = path.slice("sources.".length);
|
||||
if (!/^[A-Za-z0-9._-]+$/u.test(sourceId)) throw new Error(`${label} has an unsupported source id`);
|
||||
const parsed = Bun.YAML.parse(readFileSync(rootPath(file), "utf8")) as unknown;
|
||||
const root = record(parsed, file);
|
||||
if (root.kind !== "platform-infra-egress-proxy-sources") throw new Error(`${file}.kind must be platform-infra-egress-proxy-sources`);
|
||||
const sources = record(root.sources, `${file}.sources`);
|
||||
const raw = record(sources[sourceId], `${file}#sources.${sourceId}`);
|
||||
return parseSharedEgressProxySource(sourceId, ref, raw, `${file}#sources.${sourceId}`);
|
||||
}
|
||||
|
||||
function parseSharedEgressProxySource(id: string, configRef: string, raw: Record<string, unknown>, label: string): SharedEgressProxySourceSpec {
|
||||
const sourceType = enumField(raw, "sourceType", label, ["subscription-url", "master-shadowsocks"] as const);
|
||||
const preferredOutbound = raw.preferredOutbound === undefined
|
||||
? null
|
||||
: enumField(raw, "preferredOutbound", label, ["vless-reality", "hysteria2"] as const);
|
||||
const masterShadowsocks = raw.masterShadowsocks === undefined
|
||||
? null
|
||||
: masterShadowsocksSpec(record(raw.masterShadowsocks, `${label}.masterShadowsocks`), `${label}.masterShadowsocks`);
|
||||
const spec: SharedEgressProxySourceSpec = {
|
||||
id,
|
||||
configRef,
|
||||
sourceType,
|
||||
sourceRef: sourceRefField(raw, "sourceRef", label),
|
||||
sourceKey: envKeyField(raw, "sourceKey", label),
|
||||
preferredOutbound,
|
||||
masterShadowsocks,
|
||||
healthProbeUrl: urlField(raw, "healthProbeUrl", label),
|
||||
fingerprint: "",
|
||||
valuesPrinted: false,
|
||||
};
|
||||
validateSharedEgressProxySource(spec, label);
|
||||
return {
|
||||
...spec,
|
||||
fingerprint: `sha256:${createHash("sha256").update(JSON.stringify({
|
||||
sourceType: spec.sourceType,
|
||||
sourceRef: spec.sourceRef,
|
||||
sourceKey: spec.sourceKey,
|
||||
preferredOutbound: spec.preferredOutbound,
|
||||
masterShadowsocks: spec.masterShadowsocks,
|
||||
healthProbeUrl: spec.healthProbeUrl,
|
||||
})).digest("hex").slice(0, 16)}`,
|
||||
};
|
||||
}
|
||||
|
||||
function validateSharedEgressProxySource(spec: SharedEgressProxySourceSpec, label: string): void {
|
||||
if (spec.sourceType === "subscription-url" && spec.preferredOutbound === null) {
|
||||
throw new Error(`${label}.preferredOutbound is required for sourceType=subscription-url`);
|
||||
}
|
||||
if (spec.sourceType === "master-shadowsocks") {
|
||||
if (spec.masterShadowsocks === null) throw new Error(`${label}.masterShadowsocks is required for sourceType=master-shadowsocks`);
|
||||
if (spec.preferredOutbound !== null) throw new Error(`${label}.preferredOutbound must be omitted for sourceType=master-shadowsocks`);
|
||||
}
|
||||
}
|
||||
|
||||
function masterShadowsocksSpec(raw: Record<string, unknown>, label: string): MasterShadowsocksSourceSpec {
|
||||
const serverHost = stringField(raw, "serverHost", label);
|
||||
if (!/^[A-Za-z0-9._:-]+$/u.test(serverHost)) throw new Error(`${label}.serverHost has an unsupported format`);
|
||||
const serverPort = integerField(raw, "serverPort", label);
|
||||
if (serverPort < 1 || serverPort > 65535) throw new Error(`${label}.serverPort must be a TCP port`);
|
||||
const method = stringField(raw, "method", label);
|
||||
if (!/^[A-Za-z0-9-]+$/u.test(method)) throw new Error(`${label}.method has an unsupported format`);
|
||||
return { serverHost, serverPort, method };
|
||||
}
|
||||
|
||||
function parseConfigRef(ref: string, label: string): { file: string; path: string } {
|
||||
const [file, path, extra] = ref.split("#");
|
||||
if (extra !== undefined || file === undefined || path === undefined || file.length === 0 || path.length === 0) {
|
||||
throw new Error(`${label} must use path/to/file.yaml#sources.<id> syntax`);
|
||||
}
|
||||
if (file.startsWith("/") || file.includes("..") || !file.startsWith("config/") || !file.endsWith(".yaml")) {
|
||||
throw new Error(`${label} must reference a repo-relative config/*.yaml file without ..`);
|
||||
}
|
||||
if (!/^[A-Za-z0-9_.-]+$/u.test(path)) throw new Error(`${label} has an unsupported YAML path`);
|
||||
return { file, path };
|
||||
}
|
||||
|
||||
function record(value: unknown, label: string): Record<string, unknown> {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} must be a YAML object`);
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function stringField(obj: Record<string, unknown>, key: string, label: string): string {
|
||||
const value = obj[key];
|
||||
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${label}.${key} must be a non-empty string`);
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function integerField(obj: Record<string, unknown>, key: string, label: string): number {
|
||||
const value = obj[key];
|
||||
if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`${label}.${key} must be an integer`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function enumField<const T extends readonly string[]>(obj: Record<string, unknown>, key: string, label: string, values: T): T[number] {
|
||||
const value = stringField(obj, key, label);
|
||||
if (!(values as readonly string[]).includes(value)) throw new Error(`${label}.${key} must be one of ${values.join(", ")}`);
|
||||
return value as T[number];
|
||||
}
|
||||
|
||||
function sourceRefField(obj: Record<string, unknown>, key: string, label: string): string {
|
||||
const value = stringField(obj, key, label);
|
||||
if (value.startsWith("/") || value.includes("..") || !/^[A-Za-z0-9_./-]+$/u.test(value)) throw new Error(`${label}.${key} has an unsupported sourceRef format`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function envKeyField(obj: Record<string, unknown>, key: string, label: string): string {
|
||||
const value = stringField(obj, key, label);
|
||||
if (!/^[A-Z_][A-Z0-9_]*$/u.test(value)) throw new Error(`${label}.${key} must be an env key`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function urlField(obj: Record<string, unknown>, key: string, label: string): string {
|
||||
const value = stringField(obj, key, label);
|
||||
const url = new URL(value);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`${label}.${key} must use http:// or https://`);
|
||||
if (url.username || url.password || url.search || url.hash) throw new Error(`${label}.${key} must not include credentials, query, or hash`);
|
||||
return value;
|
||||
}
|
||||
Reference in New Issue
Block a user