Skip to content

feat(proxy): shorthand proxy formats + protocol header mode for bulk import - #6867

Merged
diegosouzapw merged 6 commits into
cheaper-inference:release/v3.8.47from
growab:feat/proxy-bulk-import-shorthand-formats
Jul 12, 2026
Merged

diegosouzapw merged 6 commits into
cheaper-inference:release/v3.8.47from
growab:feat/proxy-bulk-import-shorthand-formats

Conversation

@growab

@growab growab commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds support for 6 new shorthand proxy formats and a protocol-header mode to the proxy Bulk Import feature, alongside the existing pipe-delimited format (fully backward-compatible).

Problem

The proxy bulk import only accepted the pipe-delimited format (NAME|HOST|PORT|USERNAME|PASSWORD|TYPE|REGION|STATUS|NOTES). Users copying proxy lists from common proxy providers (which use ip:port:user:pass, user:pass@ip:port, protocol://user:pass@ip:port, etc.) had to manually reformat each line.

The en.json description already mentioned HOST:PORT shorthand support, but the inline parser in ProxyRegistryManager.tsx never implemented it — it only handled pipe-delimited lines.

Additionally, ProxyRegistryManager.tsx had a duplicate inline parser that shadowed the extracted parseBulkProxyImport.ts module. The inline copy only supported pipe-delimited format, so the shorthand parsing logic in the shared module was never called at runtime.

Changes

1. parseBulkProxyImport.ts — rewritten parser

Supports all formats (one proxy per line):

Format Example
Pipe-delimited proxy-us|138.99.147.218|50101|myuser|mypass|socks5|US-East|active|US proxy
ip:port 138.99.147.218:50101
ip:port:user:pass 138.99.147.218:50101:myuser:mypass
user:pass@ip:port myuser:mypass@138.99.147.218:50101
user:pass:ip:port myuser:mypass:138.99.147.218:50101
protocol://ip:port http://10.0.0.50:8080
protocol://user:pass@ip:port https://admin:secret@proxy.example.com:443

Protocol header mode: A bare protocol name (http, https, socks5) on its own line sets the default type for all subsequent shorthand lines without an explicit protocol:// prefix. The per-line protocol:// prefix always takes precedence over the header default.

socks5
138.99.147.218:50101:myuser:mypass      → type: socks5 (from header)
http://10.0.0.50:8080                    → type: http (explicit prefix overrides header)
200.234.177.62:50101:otheruser:otherpass → type: socks5 (from header)

4-colon disambiguation: For a:b:c:d format, the parser checks if parts[0] looks like an IP/hostname and parts[1] is a valid port → ip:port:user:pass. Otherwise if parts[2] looks like an IP/hostname and parts[3] is a valid port → user:pass:ip:port.

Default type change: Shorthand ip:port (no protocol) now defaults to socks5 instead of http for consistency with the pipe-delimited default.

2. ProxyRegistryManager.tsx — removed inline parser duplicate

  • Deleted the inline parseBulkImportText function, VALID_TYPES, VALID_STATUSES, and duplicate ParsedProxyEntry/ParseError types
  • Added import { parseBulkImportText, type ParsedProxyEntry, type ParseError } from "./parseBulkProxyImport"
  • Now the component uses the shared parser that supports all formats
  • Updated BULK_IMPORT_TEMPLATE comment block with full documentation and examples for all 3 format groups

3. en.json — updated description

Updated bulkImportDescription to list all supported formats.

4. Tests — proxy-registry-manager.test.ts

Expanded from 13 → 30 tests covering:

  • All 6 shorthand formats (positive + negative cases)
  • Protocol header mode (4 tests: default application, prefix override precedence, mixed with pipe, lines-before-header)
  • Pipe-delimited regression (5 tests — all existing behavior preserved)
  • Edge cases (ambiguous 4-colon, invalid port, bare text)
  • Full real-world mixed import block

Commands ran

npx c8 --output-dir coverage --exclude=open-sse --check-coverage --lines 60 --functions 60 --branches 60 node --import tsx/esm --test tests/unit/proxy-registry-manager.test.ts

Changed test files

  • tests/unit/proxy-registry-manager.test.ts — expanded from 13 to 30 tests

Coverage result

File                                   | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------------------------------|---------|----------|---------|---------|-------------------
All files                              |   97.01 |    91.01 |     100 |   97.01 |
 parseBulkProxyImport.ts               |   93.18 |    86.44 |     100 |   93.18 |
 proxy-registry-manager.test.ts         |     100 |      100 |     100 |     100 |
---------------------------------------|---------|----------|---------|---------|-------------------

All metrics exceed the 60% threshold (statements, lines, functions, branches).

diegosouzapw and others added 4 commits July 7, 2026 23:10
… (ABI 148) (cheaper-inference#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 cheaper-inference#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.)
…heaper-inference#6588)

deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (cheaper-inference#6605).
…7) + production deps bump (cheaper-inference#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 cheaper-inference#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 cheaper-inference#6605 which passed the smoke. Not this diff.)
…port

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
@growab
growab requested a review from diegosouzapw as a code owner July 11, 2026 10:02

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request expands the proxy bulk import feature by adding support for multiple shorthand formats (such as ip:port, ip:port:user:pass, user:pass@ip:port, user:pass:ip:port, and protocol prefixes) as well as a protocol header mode. It also updates the unit tests to cover these new formats. The code reviewer identified two critical issues in the new parsing logic: first, the looksLikeHost helper is too permissive and can lead to incorrect parsing of 4-colon shorthand formats when the password is a valid port number; second, splitting shorthand lines by : breaks support for IPv6 addresses, introducing a regression. Code suggestions were provided to resolve both issues.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +46 to +55
function looksLikeHost(s: string): boolean {
if (!s) return false;
// IPv4: four dot-separated octets, each 0–255
const ipParts = s.split(".");
if (ipParts.length === 4 && ipParts.every((o) => /^\d+$/.test(o) && Number(o) >= 0 && Number(o) <= 255)) {
return true;
}
// Hostname: alphanumeric + dots/hyphens, at least one char
return /^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$/.test(s);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of looksLikeHost is too permissive for hostname validation. Since it matches any alphanumeric string (e.g., "myuser"), it can cause incorrect parsing of 4-colon shorthand formats like user:pass:ip:port when the password is a valid port number (e.g., myuser:1234:1.2.3.4:8080). In this case, looksLikeHost("myuser") returns true and isPort1 is true, causing it to be incorrectly parsed as ip:port:user:pass with host "myuser" and port 1234.\n\nTo prevent this, looksLikeHost should require a dot in the hostname (or explicitly match localhost and bracketed IPv6 addresses).

function looksLikeHost(s: string): boolean {
  if (!s) return false;
  const lower = s.toLowerCase();
  if (lower === "localhost") return true;
  // Bracketed IPv6
  if (s.startsWith("[") && s.endsWith("]")) {
    return true;
  }
  // IPv4: four dot-separated octets, each 0–255
  const ipParts = s.split(".");
  if (ipParts.length === 4 && ipParts.every((o) => /^\d+$/.test(o) && Number(o) >= 0 && Number(o) <= 255)) {
    return true;
  }
  // Hostname: must contain at least one dot, and match alphanumeric + dots/hyphens
  return s.includes(".") && /^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$/.test(s);
}

Comment on lines +152 to +186
// No @ — split by colon to determine which colon-delimited format
const colonParts = working.split(":").map((p) => p.trim());

if (colonParts.length === 2) {
// host:port
const [host, portStr] = colonParts;
return pushShorthandEntry(entries, errors, lineNum, host, portStr, "", "", type);
}

if (colonParts.length === 4) {
// Two possibilities: ip:port:user:pass OR user:pass:ip:port
// Require the "host" slot to look like an IP/hostname AND the "port" slot to be a valid port.
const isPort1 = /^\d+$/.test(colonParts[1]) && Number(colonParts[1]) >= 1 && Number(colonParts[1]) <= 65535;
const isPort3 = /^\d+$/.test(colonParts[3]) && Number(colonParts[3]) >= 1 && Number(colonParts[3]) <= 65535;
const hostLooksLikePart0 = looksLikeHost(colonParts[0]);
const hostLooksLikePart2 = looksLikeHost(colonParts[2]);

if (hostLooksLikePart0 && isPort1) {
// ip:port:user:pass
const [host, portStr, username, password] = colonParts;
return pushShorthandEntry(entries, errors, lineNum, host, portStr, username, password, type);
}
if (hostLooksLikePart2 && isPort3) {
// user:pass:ip:port
const [username, password, host, portStr] = colonParts;
return pushShorthandEntry(entries, errors, lineNum, host, portStr, username, password, type);
}
// Can't determine format
errors.push({ line: lineNum, reason: "bulkImportErrorInvalidPort" });
return false;
}

// Unknown colon format
errors.push({ line: lineNum, reason: "bulkImportErrorMissingHost" });
return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Splitting the shorthand proxy line by : breaks support for IPv6 addresses (e.g., [2001:db8::1]:8080 or user:pass:[2001:db8::1]:8080), which are split into more than 4 parts and fail to parse. This is a regression from the previous simple HOST:PORT parser which used lastIndexOf(":").\n\nWe can handle bracketed IPv6 hosts explicitly before splitting by : to preserve full IPv6 compatibility.

  // No @ — check for bracketed IPv6 hosts first to avoid splitting on IPv6 colons
  if (working.startsWith("[")) {
    const closeBracketIdx = working.indexOf("]");
    if (closeBracketIdx > 0 && working.charAt(closeBracketIdx + 1) === ":") {
      const host = working.slice(0, closeBracketIdx + 1).trim();
      const rest = working.slice(closeBracketIdx + 2).trim();
      const restParts = rest.split(":").map((p) => p.trim());
      if (restParts.length === 1) {
        // [ipv6]:port
        return pushShorthandEntry(entries, errors, lineNum, host, restParts[0], "", "", type);
      }
      if (restParts.length === 3) {
        // [ipv6]:port:user:pass
        return pushShorthandEntry(entries, errors, lineNum, host, restParts[0], restParts[1], restParts[2], type);
      }
    }
  }

  const openBracketIdx = working.indexOf(":[");
  if (openBracketIdx > 0) {
    const closeBracketIdx = working.indexOf("]", openBracketIdx);
    if (closeBracketIdx > 0 && working.charAt(closeBracketIdx + 1) === ":") {
      const authPart = working.slice(0, openBracketIdx).trim();
      const host = working.slice(openBracketIdx + 1, closeBracketIdx + 1).trim();
      const portStr = working.slice(closeBracketIdx + 2).trim();
      const authParts = authPart.split(":").map((p) => p.trim());
      if (authParts.length === 2) {
        // user:pass:[ipv6]:port
        return pushShorthandEntry(entries, errors, lineNum, host, portStr, authParts[0], authParts[1], type);
      }
    }
  }

  const colonParts = working.split(":").map((p) => p.trim());

  if (colonParts.length === 2) {
    // host:port
    const [host, portStr] = colonParts;
    return pushShorthandEntry(entries, errors, lineNum, host, portStr, "", "", type);
  }

  if (colonParts.length === 4) {
    // Two possibilities: ip:port:user:pass OR user:pass:ip:port
    // Require the "host" slot to look like an IP/hostname AND the "port" slot to be a valid port.
    const isPort1 = /^\d+$/.test(colonParts[1]) && Number(colonParts[1]) >= 1 && Number(colonParts[1]) <= 65535;
    const isPort3 = /^\d+$/.test(colonParts[3]) && Number(colonParts[3]) >= 1 && Number(colonParts[3]) <= 65535;
    const hostLooksLikePart0 = looksLikeHost(colonParts[0]);
    const hostLooksLikePart2 = looksLikeHost(colonParts[2]);

    if (hostLooksLikePart0 && isPort1) {
      // ip:port:user:pass
      const [host, portStr, username, password] = colonParts;
      return pushShorthandEntry(entries, errors, lineNum, host, portStr, username, password, type);
    }
    if (hostLooksLikePart2 && isPort3) {
      // user:pass:ip:port
      const [username, password, host, portStr] = colonParts;
      return pushShorthandEntry(entries, errors, lineNum, host, portStr, username, password, type);
    }
    // Can't determine format
    errors.push({ line: lineNum, reason: "bulkImportErrorInvalidPort" });
    return false;
  }

  // Unknown colon format
  errors.push({ line: lineNum, reason: "bulkImportErrorMissingHost" });
  return false;

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Nice consolidation — removing the duplicate inline parser in ProxyRegistryManager.tsx so the shared parseBulkProxyImport.ts module (which already documented HOST:PORT shorthand in the i18n string) actually runs at all is a real bug fix on its own. The 6 new shorthand formats + protocol-header mode are well covered by 30 passing tests (verified locally: node --import tsx/esm --test tests/unit/proxy-registry-manager.test.ts -> 30/30 pass), lint is clean, and nothing here touches DB/routes/security surfaces. Good to merge.

@diegosouzapw
diegosouzapw changed the base branch from main to release/v3.8.47 July 11, 2026 21:48
diegosouzapw and others added 2 commits July 11, 2026 19:45
Resolve conflict in ProxyRegistryManager.tsx by keeping both imports
(bulk-import parser from this PR + pool-strategy options from release).
@diegosouzapw
diegosouzapw merged commit c4c8af3 into cheaper-inference:release/v3.8.47 Jul 12, 2026
3 checks passed
@diegosouzapw diegosouzapw mentioned this pull request Jul 13, 2026
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…import (cheaper-inference#6867)

* fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (cheaper-inference#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 cheaper-inference#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 (cheaper-inference#6588)

deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (cheaper-inference#6605).

* fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (cheaper-inference#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 cheaper-inference#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 cheaper-inference#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>
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…import (cheaper-inference#6867)

* fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (cheaper-inference#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 cheaper-inference#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 (cheaper-inference#6588)

deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (cheaper-inference#6605).

* fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (cheaper-inference#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 cheaper-inference#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 cheaper-inference#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants