mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-08-29 14:10:53 +02:00
refactor: better proxy clipboard autofill
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
CREDENTIALS_FIRST_FORMAT,
|
||||
HOST_FIRST_FORMAT,
|
||||
pickParsedProxy,
|
||||
resolveAmbiguousProxyLine,
|
||||
splitProxyScheme,
|
||||
} from "./proxy-string.ts";
|
||||
|
||||
/**
|
||||
* The formats themselves are exercised in Rust
|
||||
* (`proxy_manager::tests::test_proxy_txt_parsing_various_formats`). What is
|
||||
* pinned here is the frontend's half: the scheme survives an ambiguous line,
|
||||
* and a resolution that doesn't fit the line is refused rather than turned into
|
||||
* a proxy pointing at somebody's password.
|
||||
*/
|
||||
|
||||
test("a bare line is HTTP", () => {
|
||||
assert.deepEqual(splitProxyScheme("1.2.3.4:8080"), {
|
||||
proxyType: "http",
|
||||
rest: "1.2.3.4:8080",
|
||||
});
|
||||
});
|
||||
|
||||
test("known schemes are recognised and normalised", () => {
|
||||
assert.deepEqual(splitProxyScheme("SOCKS://1.2.3.4:1080"), {
|
||||
proxyType: "socks5",
|
||||
rest: "1.2.3.4:1080",
|
||||
});
|
||||
assert.equal(splitProxyScheme("shadowsocks://host:8388").proxyType, "ss");
|
||||
});
|
||||
|
||||
test("an unknown scheme is left in the body rather than guessed at", () => {
|
||||
assert.deepEqual(splitProxyScheme("ftp://1.2.3.4:21"), {
|
||||
proxyType: "http",
|
||||
rest: "ftp://1.2.3.4:21",
|
||||
});
|
||||
});
|
||||
|
||||
test("host-first resolution keeps the scheme", () => {
|
||||
assert.deepEqual(
|
||||
resolveAmbiguousProxyLine(
|
||||
"socks5://1234:5678:9012:3456",
|
||||
HOST_FIRST_FORMAT,
|
||||
),
|
||||
{
|
||||
proxy_type: "socks5",
|
||||
host: "1234",
|
||||
port: 5678,
|
||||
username: "9012",
|
||||
password: "3456",
|
||||
original_line: "socks5://1234:5678:9012:3456",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("credentials-first resolution reads the tail as the endpoint", () => {
|
||||
assert.deepEqual(
|
||||
resolveAmbiguousProxyLine("1234:5678:9012:3456", CREDENTIALS_FIRST_FORMAT),
|
||||
{
|
||||
proxy_type: "http",
|
||||
host: "9012",
|
||||
port: 3456,
|
||||
username: "1234",
|
||||
password: "5678",
|
||||
original_line: "1234:5678:9012:3456",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("a format that doesn't fit the line resolves to nothing", () => {
|
||||
// 70000 is past the port range, so this ordering cannot be the right one.
|
||||
assert.equal(
|
||||
resolveAmbiguousProxyLine("host:70000:user:pass", HOST_FIRST_FORMAT),
|
||||
null,
|
||||
);
|
||||
assert.equal(resolveAmbiguousProxyLine("host:8080", HOST_FIRST_FORMAT), null);
|
||||
assert.equal(
|
||||
resolveAmbiguousProxyLine("a:1:b:2", "host:port:user:password"),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test("the first parsed line of a multi-line paste wins", () => {
|
||||
const parsed = pickParsedProxy([
|
||||
{ status: "invalid", line: "notaproxy", reason: "nope" },
|
||||
{
|
||||
status: "parsed",
|
||||
proxy_type: "socks5",
|
||||
host: "1.2.3.4",
|
||||
port: 1080,
|
||||
username: "u",
|
||||
password: "p",
|
||||
original_line: "socks5://u:p@1.2.3.4:1080",
|
||||
},
|
||||
{
|
||||
status: "parsed",
|
||||
proxy_type: "http",
|
||||
host: "5.6.7.8",
|
||||
port: 80,
|
||||
original_line: "5.6.7.8:80",
|
||||
},
|
||||
]);
|
||||
assert.equal(parsed?.host, "1.2.3.4");
|
||||
assert.equal(parsed?.proxy_type, "socks5");
|
||||
});
|
||||
|
||||
test("an ambiguous paste falls back to host:port:username:password", () => {
|
||||
const parsed = pickParsedProxy([
|
||||
{
|
||||
status: "ambiguous",
|
||||
line: "1234:5678:9012:3456",
|
||||
possible_formats: [HOST_FIRST_FORMAT, CREDENTIALS_FIRST_FORMAT],
|
||||
},
|
||||
]);
|
||||
assert.equal(parsed?.host, "1234");
|
||||
assert.equal(parsed?.port, 5678);
|
||||
});
|
||||
|
||||
test("nothing usable yields null so the plain paste stands", () => {
|
||||
assert.equal(
|
||||
pickParsedProxy([
|
||||
{ status: "invalid", line: "proxy.example.com", reason: "" },
|
||||
]),
|
||||
null,
|
||||
);
|
||||
assert.equal(pickParsedProxy([]), null);
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Reading a proxy out of a pasted line.
|
||||
*
|
||||
* The parser itself is Rust's `parse_txt_proxies`
|
||||
* (`src-tauri/src/proxy_manager.rs`); both the import dialog and the add/edit
|
||||
* form hand their clipboard text to it rather than re-implementing the format
|
||||
* zoo. What is left for the frontend is the part the backend deliberately
|
||||
* refuses to decide: `a:b:c:d` is either `host:port:username:password` or
|
||||
* `username:password:host:port`, and when both middle fields parse as a port
|
||||
* only the user knows which. The backend reports that as `ambiguous`; the
|
||||
* functions below turn the user's answer back into a proxy.
|
||||
*
|
||||
* Kept free of runtime imports so `proxy-string.test.mjs` can load it directly.
|
||||
*/
|
||||
|
||||
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",
|
||||
};
|
||||
|
||||
/** What a line carrying no scheme is assumed to be. */
|
||||
export const DEFAULT_PROXY_TYPE = "http";
|
||||
|
||||
export const HOST_FIRST_FORMAT = "host:port:username:password";
|
||||
export const CREDENTIALS_FIRST_FORMAT = "username:password:host:port";
|
||||
|
||||
/**
|
||||
* Separates `socks5://1.2.3.4:1080` into its scheme and body. An unknown or
|
||||
* absent scheme leaves the body untouched and falls back to HTTP, which is what
|
||||
* the backend does with a bare `host:port`.
|
||||
*/
|
||||
export function splitProxyScheme(line: string): {
|
||||
proxyType: string;
|
||||
rest: string;
|
||||
} {
|
||||
const separator = line.indexOf("://");
|
||||
if (separator === -1) {
|
||||
return { proxyType: DEFAULT_PROXY_TYPE, rest: line };
|
||||
}
|
||||
|
||||
const proxyType = PROXY_SCHEMES[line.slice(0, separator).toLowerCase()];
|
||||
return proxyType
|
||||
? { proxyType, rest: line.slice(separator + 3) }
|
||||
: { proxyType: DEFAULT_PROXY_TYPE, rest: line };
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a proxy from a four-part line once the user has said which of the two
|
||||
* orderings it uses. Returns null when the chosen ordering doesn't actually fit
|
||||
* the line, so a stale selection can't produce a proxy pointing at a password.
|
||||
*/
|
||||
export function resolveAmbiguousProxyLine(
|
||||
line: string,
|
||||
format: string,
|
||||
): ParsedProxyLine | null {
|
||||
const trimmed = line.trim();
|
||||
const { proxyType, rest } = splitProxyScheme(trimmed);
|
||||
const parts = rest.split(":");
|
||||
if (parts.length !== 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hostFirst = format === HOST_FIRST_FORMAT;
|
||||
if (!hostFirst && format !== CREDENTIALS_FIRST_FORMAT) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const host = hostFirst ? parts[0] : parts[2];
|
||||
const port = Number.parseInt(hostFirst ? parts[1] : parts[3], 10);
|
||||
if (!host || !Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
proxy_type: proxyType,
|
||||
host,
|
||||
port,
|
||||
username: hostFirst ? parts[2] : parts[0],
|
||||
password: hostFirst ? parts[3] : parts[1],
|
||||
original_line: trimmed,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks the proxy to use out of a parse of pasted text. Only the first usable
|
||||
* line matters: the form holds one proxy, and a paste that happens to carry a
|
||||
* whole list should still fill it in rather than do nothing.
|
||||
*
|
||||
* Ambiguous lines resolve as `host:port:username:password`, the ordering the
|
||||
* import dialog offers first and the one vendors overwhelmingly ship.
|
||||
*/
|
||||
export function pickParsedProxy(
|
||||
results: ProxyParseResult[],
|
||||
): ParsedProxyLine | null {
|
||||
for (const result of results) {
|
||||
if (result.status === "parsed") {
|
||||
return {
|
||||
proxy_type: result.proxy_type,
|
||||
host: result.host,
|
||||
port: result.port,
|
||||
username: result.username,
|
||||
password: result.password,
|
||||
vless_uri: result.vless_uri,
|
||||
original_line: result.original_line,
|
||||
};
|
||||
}
|
||||
if (result.status === "ambiguous") {
|
||||
const resolved = resolveAmbiguousProxyLine(
|
||||
result.line,
|
||||
HOST_FIRST_FORMAT,
|
||||
);
|
||||
if (resolved) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user