refactor: cleanup

This commit is contained in:
zhom committed 2026-09-09 10:09:14 +04:00
1 parent 598d3bd513
commit dd42d46753
249 files changed
+67417 -6659

No files matched your search

+119 -12
View File
@@ -15,21 +15,128 @@
import type { ParsedProxyLine, ProxyParseResult } from "@/types";
/** URL schemes the Rust parser accepts, mapped onto the stored proxy type. */
const PROXY_SCHEMES: Record<string, string> = {
http: "http",
https: "https",
socks: "socks5",
socks4: "socks4",
socks5: "socks5",
ss: "ss",
shadowsocks: "ss",
vless: "vless",
};
/**
* URL schemes the Rust parser accepts, mapped onto the stored proxy type.
*
* A Map rather than an object literal, and for the same reason as
* PROTOCOL_TOKENS below: the key comes from text the user pasted, and an object
* lookup answers for the whole prototype chain. `__proto__://a:1:b:2` resolved
* to Object.prototype and `constructor://...` to a function, both truthy, so
* `splitProxyScheme` handed a non-string `proxy_type` to the import preview.
*/
const PROXY_SCHEMES = new Map<string, string>([
["http", "http"],
["https", "https"],
["httpstls", "httpstls"],
["socks", "socks5"],
["socks4", "socks4"],
["socks5", "socks5"],
["ss", "ss"],
["shadowsocks", "ss"],
["vless", "vless"],
]);
/**
* Protocol identifiers, shortened where the raw stored type would not fit the
* management table's column. Not translated: these are wire protocol names,
* like the raw type that cell printed before. What the protocol MEANS for the
* user belongs in the tooltip, which is translated.
*/
const PROTOCOL_TOKENS = new Map<string, string>([["httpstls", "HTTP/TLS"]]);
/**
* Trim and case-fold a stored `proxy_type` before anything compares it.
*
* The first two steps of `canonicalProxyType` (`proxy-type.ts`), and every
* reader of a stored type in this file goes through it. Skipping the trim here
* while the form trimmed for its labels is what let one REST-stored `"ss "` be
* Shadowsocks to the cipher field and an unencrypted hop to the warning printed
* beside it, a false alarm about a proxy carrying a real cipher, and the
* cheapest way to teach a user to ignore this warning. The two files cannot
* import each other (both are loaded straight by `node --test`), so
* `proxy-string.test.mjs` imports both and pins them in lockstep.
*/
function normalizeProxyType(proxyType: string): string {
return proxyType.trim().toLowerCase();
}
/**
* The protocol label for a stored proxy: the short token when there is one,
* otherwise the stored type verbatim. Empty only when the stored type is blank,
* which is the one case the caller has to fill with a translated placeholder.
*
* A Map, not an object literal. `proxy_type` is free text, POST /v1/proxies
* and `import_proxies_json` store whatever they are given, unvalidated, so an
* object lookup would answer from Object.prototype: `__proto__` yielded an
* object, which React refuses as a child, and rendering it threw and unmounted
* the ENTIRE proxy table, hiding every proxy and every first-hop warning;
* `constructor`, `toString` and `valueOf` yielded functions, which React drops,
* leaving the cell blank. A Map only ever answers for keys actually put in it.
*/
export function proxyProtocolToken(proxyType: string): string {
return PROTOCOL_TOKENS.get(normalizeProxyType(proxyType)) ?? proxyType.trim();
}
/** What a line carrying no scheme is assumed to be. */
export const DEFAULT_PROXY_TYPE = "http";
/**
* Proxy types whose hop from this machine to the proxy is encrypted.
*
* `https` is deliberately absent. It is a provider label on a plaintext CONNECT
* endpoint, and Donut dials it byte-for-byte like `http`
* (`proxy_server.rs::connect_to_target_via_upstream`). Listing it here would
* make the UI state something untrue about the wire.
*/
const ENCRYPTED_FIRST_HOP = new Set([
"httpstls",
"ss",
// The REST API stores proxy_type verbatim, so a proxy created with
// "shadowsocks" reaches the UI with that spelling and the backend tunnels it
// (proxy_server.rs matches "ss" | "shadowsocks"). Without it here the guard
// below returned false and the UI told the user an ENCRYPTED hop was in the
// clear, the same class of wrong claim this feature exists to remove, only
// inverted.
"shadowsocks",
"vless",
]);
/**
* Whether the leg from this machine to the proxy is encrypted.
*
* Says nothing about what the proxy operator can see: they terminate the
* tunnel either way and still observe every hostname, port and timing, plus the
* full contents of anything not carried over end-to-end TLS.
*/
const NULL_CIPHERS = new Set(["none", "plain"]);
/**
* Whether the hop from this machine to the proxy is encrypted.
*
* Shadowsocks takes the cipher as free text, and shadowsocks-crypto maps both
* "none" and "plain" to CipherKind::NONE, a no-op. Keying off the type alone
* therefore labelled such a proxy "First hop encrypted", coloured it safe, and
* suppressed the credentials-in-clear warning, for a connection carrying
* everything in the clear. A claim of safety that is wrong is worse than no
* claim, so the cipher decides for Shadowsocks.
*/
export function isFirstHopEncrypted(
proxyType: string,
cipher?: string | null,
): boolean {
const type = normalizeProxyType(proxyType);
if (!ENCRYPTED_FIRST_HOP.has(type)) return false;
if (type === "ss" || type === "shadowsocks") {
// Fail closed. An absent cipher is not evidence of encryption, and the
// asymmetry matters: wrongly saying "in the clear" costs a needless
// warning, wrongly saying "encrypted" costs the user the one thing this
// label exists to tell them.
const normalized = (cipher ?? "").trim().toLowerCase();
return normalized.length > 0 && !NULL_CIPHERS.has(normalized);
}
return true;
}
export const HOST_FIRST_FORMAT = "host:port:username:password";
export const CREDENTIALS_FIRST_FORMAT = "username:password:host:port";
@@ -47,7 +154,7 @@ export function splitProxyScheme(line: string): {
return { proxyType: DEFAULT_PROXY_TYPE, rest: line };
}
const proxyType = PROXY_SCHEMES[line.slice(0, separator).toLowerCase()];
const proxyType = PROXY_SCHEMES.get(line.slice(0, separator).toLowerCase());
return proxyType
? { proxyType, rest: line.slice(separator + 3) }
: { proxyType: DEFAULT_PROXY_TYPE, rest: line };