mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-10-20 22:41:22 +02:00
refactor: cleanup
This commit is contained in:
1 parent
598d3bd513
commit
dd42d46753
249 files changed
+67417
-6659
No files matched your search
@@ -3,10 +3,13 @@ import test from "node:test";
|
||||
import {
|
||||
CREDENTIALS_FIRST_FORMAT,
|
||||
HOST_FIRST_FORMAT,
|
||||
isFirstHopEncrypted,
|
||||
pickParsedProxy,
|
||||
proxyProtocolToken,
|
||||
resolveAmbiguousProxyLine,
|
||||
splitProxyScheme,
|
||||
} from "./proxy-string.ts";
|
||||
import { canonicalProxyType } from "./proxy-type.ts";
|
||||
|
||||
/**
|
||||
* The formats themselves are exercised in Rust
|
||||
@@ -38,6 +41,68 @@ test("an unknown scheme is left in the body rather than guessed at", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("a scheme that names a prototype member is still an unknown scheme", () => {
|
||||
// The scheme comes from text the user pasted. An object-literal table
|
||||
// answered for the whole prototype chain, so `__proto__` matched with
|
||||
// Object.prototype and `constructor` with a function, both truthy, so the
|
||||
// body was stripped and a non-string proxy_type reached the import preview,
|
||||
// where React refuses an object as a child.
|
||||
for (const hostile of [
|
||||
"__proto__",
|
||||
"constructor",
|
||||
"toString",
|
||||
"valueOf",
|
||||
"hasOwnProperty",
|
||||
]) {
|
||||
const line = `${hostile}://1.2.3.4:8080`;
|
||||
assert.deepEqual(splitProxyScheme(line), { proxyType: "http", rest: line });
|
||||
}
|
||||
|
||||
// ...so the line still carries its five colon-parts and cannot be read as a
|
||||
// four-part ambiguous one. Before, the scheme was stripped and this minted a
|
||||
// proxy whose proxy_type was Object.prototype.
|
||||
assert.equal(
|
||||
resolveAmbiguousProxyLine(
|
||||
"__proto__://1.2.3.4:8080:u:p",
|
||||
HOST_FIRST_FORMAT,
|
||||
),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test("the protocol label shortens what needs it and prints the rest verbatim", () => {
|
||||
assert.equal(proxyProtocolToken("httpstls"), "HTTP/TLS");
|
||||
assert.equal(proxyProtocolToken("HTTPSTLS"), "HTTP/TLS");
|
||||
assert.equal(proxyProtocolToken("socks5"), "socks5");
|
||||
// An unrecognised type is the point: `proxy_type` is free text that
|
||||
// POST /v1/proxies and import_proxies_json store verbatim, and the column
|
||||
// must show what was actually stored rather than nothing.
|
||||
assert.equal(proxyProtocolToken("weird-vendor-type"), "weird-vendor-type");
|
||||
// Blank is the one empty answer, and it is the caller's cue to render a
|
||||
// translated placeholder instead of an empty cell.
|
||||
assert.equal(proxyProtocolToken(" "), "");
|
||||
});
|
||||
|
||||
test("the protocol label answers for protocols only, never for a prototype", () => {
|
||||
// `PROTOCOL_TOKENS[proxyType]` walked the prototype chain: "__proto__" gave
|
||||
// an object, which React refuses as a child, and the throw unmounted the
|
||||
// WHOLE proxy table, every proxy and every first-hop warning gone.
|
||||
// "constructor", "toString" and "valueOf" gave functions, which React drops,
|
||||
// leaving the protocol cell blank.
|
||||
for (const hostile of [
|
||||
"__proto__",
|
||||
"constructor",
|
||||
"toString",
|
||||
"valueOf",
|
||||
"hasOwnProperty",
|
||||
"isPrototypeOf",
|
||||
]) {
|
||||
const label = proxyProtocolToken(hostile);
|
||||
assert.equal(typeof label, "string", `${hostile} must render as text`);
|
||||
assert.equal(label, hostile);
|
||||
}
|
||||
});
|
||||
|
||||
test("host-first resolution keeps the scheme", () => {
|
||||
assert.deepEqual(
|
||||
resolveAmbiguousProxyLine(
|
||||
@@ -127,3 +192,130 @@ test("nothing usable yields null so the plain paste stands", () => {
|
||||
);
|
||||
assert.equal(pickParsedProxy([]), null);
|
||||
});
|
||||
|
||||
test("the TLS-wrapped scheme survives a paste", () => {
|
||||
assert.deepEqual(splitProxyScheme("httpstls://proxy.example.com:443"), {
|
||||
proxyType: "httpstls",
|
||||
rest: "proxy.example.com:443",
|
||||
});
|
||||
assert.equal(
|
||||
splitProxyScheme("HTTPSTLS://proxy.example.com:443").proxyType,
|
||||
"httpstls",
|
||||
);
|
||||
});
|
||||
|
||||
test("only the types that actually encrypt the first hop say they do", () => {
|
||||
// `ss` is deliberately NOT in this list: its answer depends on the cipher,
|
||||
// and a type-only query carries no cipher, so it cannot claim encryption.
|
||||
for (const encrypted of ["httpstls", "vless", "HTTPSTLS", "VLESS"]) {
|
||||
assert.equal(isFirstHopEncrypted(encrypted), true, encrypted);
|
||||
}
|
||||
assert.equal(isFirstHopEncrypted("ss", "aes-256-gcm"), true);
|
||||
assert.equal(isFirstHopEncrypted("SS", "chacha20-ietf-poly1305"), true);
|
||||
// The REST API stores the type verbatim, so both spellings arrive, and both
|
||||
// must answer identically or the UI contradicts the wire.
|
||||
assert.equal(isFirstHopEncrypted("shadowsocks", "aes-256-gcm"), true);
|
||||
assert.equal(isFirstHopEncrypted("shadowsocks", "none"), false);
|
||||
assert.equal(isFirstHopEncrypted("shadowsocks"), false);
|
||||
|
||||
// `https` is the one that matters. It is a provider label on a plaintext
|
||||
// CONNECT endpoint, dialed byte-for-byte like `http`, so the UI must not
|
||||
// present it as encrypted.
|
||||
for (const plaintext of ["http", "https", "HTTPS", "socks4", "socks5", ""]) {
|
||||
assert.equal(isFirstHopEncrypted(plaintext), false, plaintext);
|
||||
}
|
||||
|
||||
// Shadowsocks takes its cipher as free text and shadowsocks-crypto maps
|
||||
// "none"/"plain" to a no-op cipher. Such a proxy was labelled "First hop
|
||||
// encrypted", coloured safe, and had its credentials-in-clear warning
|
||||
// suppressed, while carrying everything in the clear.
|
||||
for (const nullCipher of ["none", "plain", "NONE", " None "]) {
|
||||
assert.equal(
|
||||
isFirstHopEncrypted("ss", nullCipher),
|
||||
false,
|
||||
`ss with cipher ${nullCipher} is not encrypted`,
|
||||
);
|
||||
}
|
||||
for (const real of ["aes-256-gcm", "chacha20-ietf-poly1305"]) {
|
||||
assert.equal(isFirstHopEncrypted("ss", real), true, real);
|
||||
}
|
||||
// An unspecified cipher is not a claim of safety either.
|
||||
assert.equal(isFirstHopEncrypted("ss", null), false);
|
||||
// Types whose encryption does not depend on a cipher are unaffected.
|
||||
assert.equal(isFirstHopEncrypted("vless", null), true);
|
||||
});
|
||||
|
||||
test("a padded stored type answers the same as an unpadded one", () => {
|
||||
// `proxy_type` is stored verbatim, so `"ss "` is a value a proxy really
|
||||
// arrives with from POST /v1/proxies or import_proxies_json. This helper
|
||||
// lowercased but did not trim, while canonicalProxyType trimmed, so one such
|
||||
// proxy was Shadowsocks to the form, which labelled its field "Cipher" -
|
||||
// and an UNENCRYPTED first hop to the warning printed beside that field,
|
||||
// about a proxy configured with aes-256-gcm. A warning that fires on a
|
||||
// correct configuration is how a user learns to ignore this warning.
|
||||
assert.equal(isFirstHopEncrypted("ss ", "aes-256-gcm"), true);
|
||||
assert.equal(isFirstHopEncrypted(" shadowsocks", "aes-256-gcm"), true);
|
||||
assert.equal(isFirstHopEncrypted("\thttpstls\n"), true);
|
||||
assert.equal(isFirstHopEncrypted(" VLESS "), true);
|
||||
// Padding does not buy encryption either: the cipher still decides, and the
|
||||
// types that never encrypt still never do.
|
||||
assert.equal(isFirstHopEncrypted(" ss ", ""), false);
|
||||
assert.equal(isFirstHopEncrypted(" ss ", " none "), false);
|
||||
assert.equal(isFirstHopEncrypted(" https "), false);
|
||||
assert.equal(proxyProtocolToken(" httpstls "), "HTTP/TLS");
|
||||
});
|
||||
|
||||
test("the first-hop answer never disagrees with the canonical type", () => {
|
||||
// The lockstep the two helpers cannot get from a shared import: both files
|
||||
// are loaded straight by `node --test`, which resolves neither `@/` nor an
|
||||
// extensionless relative specifier, so neither may import the other. This
|
||||
// test can import both, and it is what keeps their normalisation identical.
|
||||
// Any two spellings of one type must get one answer, and it must be the
|
||||
// answer the canonical spelling gets.
|
||||
const spellings = [
|
||||
"ss",
|
||||
"SS",
|
||||
" ss ",
|
||||
"shadowsocks",
|
||||
" Shadowsocks ",
|
||||
"httpstls",
|
||||
" HTTPSTLS ",
|
||||
"vless",
|
||||
" VLess ",
|
||||
"http",
|
||||
" HTTPS ",
|
||||
"socks5",
|
||||
" socks4 ",
|
||||
"__proto__",
|
||||
"",
|
||||
" ",
|
||||
];
|
||||
const answers = new Map();
|
||||
for (const spelling of spellings) {
|
||||
const canonical = canonicalProxyType(spelling);
|
||||
const answer = isFirstHopEncrypted(spelling, "aes-256-gcm");
|
||||
assert.equal(
|
||||
isFirstHopEncrypted(canonical, "aes-256-gcm"),
|
||||
answer,
|
||||
`${JSON.stringify(spelling)} and its canonical form ${JSON.stringify(
|
||||
canonical,
|
||||
)} describe different wires`,
|
||||
);
|
||||
if (answers.has(canonical)) {
|
||||
assert.equal(
|
||||
answer,
|
||||
answers.get(canonical),
|
||||
`${JSON.stringify(spelling)} answers differently from another ` +
|
||||
`spelling of ${JSON.stringify(canonical)}`,
|
||||
);
|
||||
} else {
|
||||
answers.set(canonical, answer);
|
||||
}
|
||||
}
|
||||
// The corpus has to have covered both answers, or the loop proves nothing.
|
||||
assert.equal(answers.get("ss"), true);
|
||||
assert.equal(answers.get("httpstls"), true);
|
||||
assert.equal(answers.get("http"), false);
|
||||
assert.equal(answers.get("https"), false);
|
||||
assert.equal(answers.get("__proto__"), false);
|
||||
});
|
||||
Reference in new issue
Block a user