mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 12:22:14 +03:00
* fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605) fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148). Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148). Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.) * deps: bump the development group across 1 directory with 6 updates (#6588) deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605). * fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620) fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump. undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green. Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.) * feat(proxy): add shorthand formats + protocol header mode for bulk import Supports 6 new shorthand formats alongside the existing pipe-delimited parser: - ip:port - ip:port:user:pass - user:pass@ip:port - user:pass:ip:port - protocol://ip:port - protocol://user:pass@ip:port Protocol header mode: a bare protocol name (http/https/socks5) on its own line sets the default type for subsequent protocol-less shorthand lines. Explicit protocol:// prefix always takes precedence over the header default. Changes: - Rewrite parseBulkProxyImport.ts with parseShorthandLine helper - Use Record<string, true> for static lookup tables (VALID_PROXY_TYPES, VALID_PROXY_STATUSES) per project convention - Add looksLikeHost() heuristic to disambiguate 4-colon format (ip:port:user:pass vs user:pass:ip:port) - Update BULK_IMPORT_TEMPLATE in ProxyRegistryManager.tsx with full documentation and examples for all formats - Update en.json bulkImportDescription to list all supported formats - Add 30 unit tests covering every format, edge cases, and regressions for the existing pipe-delimited path --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
341 lines
13 KiB
TypeScript
341 lines
13 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { parseBulkImportText } from "../../src/app/(dashboard)/dashboard/settings/components/parseBulkProxyImport.ts";
|
|
|
|
// ── 2-part auth-less shorthand: host:port ─────────────────────────────────────
|
|
|
|
test("auth-less host:port produces socks5 entry with generated name (default type changed from http to socks5)", () => {
|
|
const { entries, errors } = parseBulkImportText("127.0.0.1:7897");
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 1);
|
|
const e = entries[0];
|
|
assert.equal(e.host, "127.0.0.1");
|
|
assert.equal(e.port, 7897);
|
|
assert.equal(e.type, "socks5");
|
|
assert.equal(e.username, "");
|
|
assert.equal(e.password, "");
|
|
assert.equal(e.status, "active");
|
|
assert.match(e.name, /127\.0\.0\.1:7897/);
|
|
});
|
|
|
|
test("auth-less host:port with hostname (not IP)", () => {
|
|
const { entries, errors } = parseBulkImportText("proxy.example.com:3128");
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 1);
|
|
assert.equal(entries[0].host, "proxy.example.com");
|
|
assert.equal(entries[0].port, 3128);
|
|
});
|
|
|
|
test("auth-less host:port with port 0 produces error", () => {
|
|
const { entries, errors } = parseBulkImportText("127.0.0.1:0");
|
|
assert.equal(entries.length, 0);
|
|
assert.equal(errors.length, 1);
|
|
assert.equal(errors[0].reason, "bulkImportErrorInvalidPort");
|
|
});
|
|
|
|
test("auth-less host:port with port > 65535 produces error", () => {
|
|
const { entries, errors } = parseBulkImportText("127.0.0.1:99999");
|
|
assert.equal(entries.length, 0);
|
|
assert.equal(errors.length, 1);
|
|
assert.equal(errors[0].reason, "bulkImportErrorInvalidPort");
|
|
});
|
|
|
|
test("auth-less host:port with non-numeric port produces error", () => {
|
|
const { entries, errors } = parseBulkImportText("127.0.0.1:abc");
|
|
assert.equal(entries.length, 0);
|
|
assert.equal(errors.length, 1);
|
|
assert.equal(errors[0].reason, "bulkImportErrorInvalidPort");
|
|
});
|
|
|
|
// ── 4-part shorthand: ip:port:user:pass ───────────────────────────────────────
|
|
|
|
test("ip:port:user:pass parses correctly", () => {
|
|
const { entries, errors } = parseBulkImportText("138.99.147.218:50101:myuser:mypass");
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 1);
|
|
const e = entries[0];
|
|
assert.equal(e.host, "138.99.147.218");
|
|
assert.equal(e.port, 50101);
|
|
assert.equal(e.username, "myuser");
|
|
assert.equal(e.password, "mypass");
|
|
assert.equal(e.type, "socks5");
|
|
assert.match(e.name, /138\.99\.147\.218:50101/);
|
|
});
|
|
|
|
test("ip:port:user:pass with hostname works", () => {
|
|
const { entries, errors } = parseBulkImportText("proxy.example.com:3128:user:pass");
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 1);
|
|
assert.equal(entries[0].host, "proxy.example.com");
|
|
assert.equal(entries[0].username, "user");
|
|
assert.equal(entries[0].password, "pass");
|
|
});
|
|
|
|
// ── @-style shorthand: user:pass@ip:port ─────────────────────────────────────
|
|
|
|
test("user:pass@ip:port parses correctly", () => {
|
|
const { entries, errors } = parseBulkImportText("myuser:mypass@138.99.147.218:50101");
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 1);
|
|
const e = entries[0];
|
|
assert.equal(e.host, "138.99.147.218");
|
|
assert.equal(e.port, 50101);
|
|
assert.equal(e.username, "myuser");
|
|
assert.equal(e.password, "mypass");
|
|
assert.equal(e.type, "socks5");
|
|
});
|
|
|
|
test("user:pass@hostname:port parses correctly", () => {
|
|
const { entries, errors } = parseBulkImportText("admin:secret@proxy.example.com:443");
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 1);
|
|
assert.equal(entries[0].host, "proxy.example.com");
|
|
assert.equal(entries[0].port, 443);
|
|
assert.equal(entries[0].username, "admin");
|
|
assert.equal(entries[0].password, "secret");
|
|
});
|
|
|
|
// ── user:pass:ip:port shorthand ───────────────────────────────────────────────
|
|
|
|
test("user:pass:ip:port parses correctly", () => {
|
|
const { entries, errors } = parseBulkImportText("myuser:mypass:138.99.147.218:50101");
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 1);
|
|
const e = entries[0];
|
|
assert.equal(e.host, "138.99.147.218");
|
|
assert.equal(e.port, 50101);
|
|
assert.equal(e.username, "myuser");
|
|
assert.equal(e.password, "mypass");
|
|
});
|
|
|
|
// ── protocol:// shorthand ──────────────────────────────────────────────────────
|
|
|
|
test("protocol://ip:port parses with explicit type", () => {
|
|
const { entries, errors } = parseBulkImportText("http://10.0.0.50:8080");
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 1);
|
|
const e = entries[0];
|
|
assert.equal(e.host, "10.0.0.50");
|
|
assert.equal(e.port, 8080);
|
|
assert.equal(e.type, "http");
|
|
assert.equal(e.username, "");
|
|
assert.equal(e.password, "");
|
|
});
|
|
|
|
test("socks5://ip:port parses with explicit type", () => {
|
|
const { entries, errors } = parseBulkImportText("socks5://1.2.3.4:1080");
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 1);
|
|
assert.equal(entries[0].type, "socks5");
|
|
assert.equal(entries[0].host, "1.2.3.4");
|
|
assert.equal(entries[0].port, 1080);
|
|
});
|
|
|
|
test("https://ip:port parses with explicit type", () => {
|
|
const { entries, errors } = parseBulkImportText("https://proxy.example.com:443");
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 1);
|
|
assert.equal(entries[0].type, "https");
|
|
assert.equal(entries[0].host, "proxy.example.com");
|
|
assert.equal(entries[0].port, 443);
|
|
});
|
|
|
|
test("protocol://user:pass@ip:port parses with auth + explicit type", () => {
|
|
const { entries, errors } = parseBulkImportText("https://admin:secret123@proxy.example.com:443");
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 1);
|
|
const e = entries[0];
|
|
assert.equal(e.type, "https");
|
|
assert.equal(e.host, "proxy.example.com");
|
|
assert.equal(e.port, 443);
|
|
assert.equal(e.username, "admin");
|
|
assert.equal(e.password, "secret123");
|
|
});
|
|
|
|
test("http://user:pass@ip:port parses correctly", () => {
|
|
const { entries, errors } = parseBulkImportText("http://user:pass@10.0.0.50:8080");
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 1);
|
|
assert.equal(entries[0].type, "http");
|
|
assert.equal(entries[0].username, "user");
|
|
assert.equal(entries[0].password, "pass");
|
|
});
|
|
|
|
// ── Protocol header mode ───────────────────────────────────────────────────────
|
|
|
|
test("protocol header sets default type for subsequent shorthand lines", () => {
|
|
const text = [
|
|
"http",
|
|
"1.2.3.4:8080",
|
|
"5.6.7.8:3128:user:pass",
|
|
"user:pass@9.10.11.12:443",
|
|
].join("\n");
|
|
const { entries, errors } = parseBulkImportText(text);
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 3);
|
|
assert.equal(entries[0].type, "http");
|
|
assert.equal(entries[1].type, "http");
|
|
assert.equal(entries[2].type, "http");
|
|
});
|
|
|
|
test("protocol:// prefix overrides protocol header default", () => {
|
|
const text = [
|
|
"socks5",
|
|
"http://1.2.3.4:8080",
|
|
"1.2.3.4:1080",
|
|
].join("\n");
|
|
const { entries, errors } = parseBulkImportText(text);
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 2);
|
|
assert.equal(entries[0].type, "http", "explicit protocol:// must override header");
|
|
assert.equal(entries[1].type, "socks5", "no-prefix line falls back to header default");
|
|
});
|
|
|
|
test("protocol header mode with mixed shorthand and pipe formats", () => {
|
|
const text = [
|
|
"https",
|
|
"1.2.3.4:443",
|
|
"named-proxy|5.6.7.8|8080|||socks5||active|pipe entry keeps own type",
|
|
].join("\n");
|
|
const { entries, errors } = parseBulkImportText(text);
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 2);
|
|
assert.equal(entries[0].type, "https", "shorthand inherits header");
|
|
assert.equal(entries[1].type, "socks5", "pipe entry keeps its own TYPE field");
|
|
});
|
|
|
|
test("protocol header only affects lines after it", () => {
|
|
const text = [
|
|
"1.2.3.4:1080",
|
|
"http",
|
|
"1.2.3.4:8080",
|
|
].join("\n");
|
|
const { entries, errors } = parseBulkImportText(text);
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 2);
|
|
assert.equal(entries[0].type, "socks5", "line before header gets default socks5");
|
|
assert.equal(entries[1].type, "http", "line after header gets http");
|
|
});
|
|
|
|
// ── Regression: pipe-delimited full format still works ────────────────────────
|
|
|
|
test("pipe-delimited NAME|HOST|PORT with all optional fields", () => {
|
|
const line = "my-proxy|10.0.0.1|8080|user|pass|http|US|active|notes here";
|
|
const { entries, errors } = parseBulkImportText(line);
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 1);
|
|
const e = entries[0];
|
|
assert.equal(e.name, "my-proxy");
|
|
assert.equal(e.host, "10.0.0.1");
|
|
assert.equal(e.port, 8080);
|
|
assert.equal(e.username, "user");
|
|
assert.equal(e.password, "pass");
|
|
assert.equal(e.type, "http");
|
|
assert.equal(e.region, "US");
|
|
assert.equal(e.status, "active");
|
|
assert.equal(e.notes, "notes here");
|
|
});
|
|
|
|
test("pipe-delimited minimal NAME|HOST|PORT defaults type to socks5", () => {
|
|
const { entries, errors } = parseBulkImportText("p|10.0.0.2|1080");
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 1);
|
|
assert.equal(entries[0].type, "socks5");
|
|
assert.equal(entries[0].status, "active");
|
|
});
|
|
|
|
test("pipe-delimited missing NAME produces error", () => {
|
|
const { errors } = parseBulkImportText("|10.0.0.1|8080");
|
|
assert.equal(errors.length, 1);
|
|
assert.equal(errors[0].reason, "bulkImportErrorMissingName");
|
|
});
|
|
|
|
test("pipe-delimited invalid port produces error", () => {
|
|
const { errors } = parseBulkImportText("proxy|10.0.0.1|notaport");
|
|
assert.equal(errors.length, 1);
|
|
assert.equal(errors[0].reason, "bulkImportErrorInvalidPort");
|
|
});
|
|
|
|
test("pipe-delimited invalid type produces error", () => {
|
|
const { errors } = parseBulkImportText("p|10.0.0.1|8080|||ftp");
|
|
assert.equal(errors.length, 1);
|
|
assert.equal(errors[0].reason, "bulkImportErrorInvalidType");
|
|
});
|
|
|
|
// ── Mixed lines ───────────────────────────────────────────────────────────────
|
|
|
|
test("mixed: comment lines and blank lines are skipped", () => {
|
|
const text = [
|
|
"# this is a comment",
|
|
"",
|
|
"127.0.0.1:7897",
|
|
"# another comment",
|
|
"proxy-us|10.0.0.1|3128",
|
|
].join("\n");
|
|
const { entries, errors, skipped } = parseBulkImportText(text);
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 2);
|
|
assert.equal(skipped, 3);
|
|
assert.equal(entries[0].host, "127.0.0.1");
|
|
assert.equal(entries[1].host, "10.0.0.1");
|
|
});
|
|
|
|
test("multiple auth-less entries in one block", () => {
|
|
const text = ["10.0.0.1:1080", "10.0.0.2:3128", "10.0.0.3:8888"].join("\n");
|
|
const { entries, errors } = parseBulkImportText(text);
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 3);
|
|
assert.equal(entries[0].port, 1080);
|
|
assert.equal(entries[1].port, 3128);
|
|
assert.equal(entries[2].port, 8888);
|
|
});
|
|
|
|
test("full real-world mixed import block", () => {
|
|
const text = [
|
|
"# My proxy list",
|
|
"socks5",
|
|
"138.99.147.218:50101:myuser:mypass",
|
|
"200.234.177.62:50101:otheruser:otherpass",
|
|
"http://10.0.0.50:8080",
|
|
"https://admin:secret@proxy.example.com:443",
|
|
"",
|
|
"named-proxy|5.6.7.8|3128|user|pass|http|US|active|via pipe",
|
|
].join("\n");
|
|
const { entries, errors, skipped } = parseBulkImportText(text);
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 5);
|
|
assert.equal(skipped, 2); // comment + blank line
|
|
assert.equal(entries[0].type, "socks5");
|
|
assert.equal(entries[1].type, "socks5");
|
|
assert.equal(entries[2].type, "http");
|
|
assert.equal(entries[3].type, "https");
|
|
assert.equal(entries[4].type, "http");
|
|
assert.equal(entries[4].name, "named-proxy");
|
|
});
|
|
|
|
// ── Edge cases ─────────────────────────────────────────────────────────────────
|
|
|
|
test("4-colon ambiguous line where part0 is not host-like defaults to user:pass:ip:port", () => {
|
|
const { entries, errors } = parseBulkImportText("myuser:mypass:1.2.3.4:1080");
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(entries.length, 1);
|
|
assert.equal(entries[0].host, "1.2.3.4");
|
|
assert.equal(entries[0].port, 1080);
|
|
assert.equal(entries[0].username, "myuser");
|
|
assert.equal(entries[0].password, "mypass");
|
|
});
|
|
|
|
test("single colon without port number produces error", () => {
|
|
const { entries, errors } = parseBulkImportText("justtext:nonsense");
|
|
assert.equal(entries.length, 0);
|
|
assert.equal(errors.length, 1);
|
|
assert.equal(errors[0].reason, "bulkImportErrorInvalidPort");
|
|
});
|
|
|
|
test("bare text with no colons or pipes produces error", () => {
|
|
const { entries, errors } = parseBulkImportText("justtext");
|
|
assert.equal(entries.length, 0);
|
|
assert.equal(errors.length, 1);
|
|
assert.equal(errors[0].reason, "bulkImportErrorMissingHost");
|
|
});
|