mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-08 21:49:45 +08:00
Adds browse/src/socks-bridge.ts: a 127.0.0.1-only SOCKS5 listener that
accepts unauthenticated connections from Chromium and relays them through
an authenticated upstream proxy. Chromium does not prompt for SOCKS5 auth
at launch, so this bridge is the workaround for using auth-required
residential SOCKS5 upstreams.
- startSocksBridge({ upstream, port: 0 }) → ephemeral 127.0.0.1 listener
- testUpstream({ upstream, retries: 3, backoffMs: 500, budgetMs: 5000 })
pre-flight that connects to a known endpoint (default 1.1.1.1:443)
- Stream-error policy: kill affected client + upstream sockets on any
error mid-stream; no transport retries (a transport-layer retry can
corrupt browser traffic)
Adds browse/src/proxy-redact.ts: single source of truth for redacting
credentials in any logged proxy URL or upstream config. Every code path
that prints proxy config goes through this helper.
Adds the socks npm dep (~30KB) and 16 tests covering: 127.0.0.1-only
bind, byte-for-byte round trip through the bridge, auth rejection,
mid-stream upstream drop kills client conn, listener teardown,
testUpstream success + retry-exhaust paths, redaction of every
credential shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
65 lines
2.2 KiB
TypeScript
65 lines
2.2 KiB
TypeScript
import { describe, test, expect } from 'bun:test';
|
|
import { redactProxyUrl, redactUpstream } from '../src/proxy-redact';
|
|
|
|
describe('redactProxyUrl', () => {
|
|
test('replaces user:pass with ***:*** in socks5 URL', () => {
|
|
const out = redactProxyUrl('socks5://alice:secret@host.example.com:1080');
|
|
expect(out).toContain('***:***');
|
|
expect(out).not.toContain('alice');
|
|
expect(out).not.toContain('secret');
|
|
expect(out).toContain('host.example.com:1080');
|
|
});
|
|
|
|
test('replaces creds in http URL', () => {
|
|
const out = redactProxyUrl('http://bob:hunter2@proxy.corp:3128');
|
|
expect(out).not.toContain('bob');
|
|
expect(out).not.toContain('hunter2');
|
|
expect(out).toContain('proxy.corp:3128');
|
|
});
|
|
|
|
test('returns URL unchanged when no creds present', () => {
|
|
const out = redactProxyUrl('http://proxy.corp:3128');
|
|
expect(out).toContain('proxy.corp:3128');
|
|
expect(out).not.toContain('***');
|
|
});
|
|
|
|
test('returns placeholder for malformed input', () => {
|
|
expect(redactProxyUrl('not-a-url')).toBe('<malformed proxy url>');
|
|
expect(redactProxyUrl('http://')).toBe('<malformed proxy url>');
|
|
});
|
|
|
|
test('returns placeholder for empty/null', () => {
|
|
expect(redactProxyUrl(null)).toBe('<no proxy>');
|
|
expect(redactProxyUrl(undefined)).toBe('<no proxy>');
|
|
expect(redactProxyUrl('')).toBe('<no proxy>');
|
|
});
|
|
|
|
test('does not echo cred bytes when URL is malformed but contains creds', () => {
|
|
// Defensive: if input has creds AND is malformed, we still don't echo.
|
|
const out = redactProxyUrl('socks5://leaked:password-bad-host');
|
|
expect(out).not.toContain('leaked');
|
|
expect(out).not.toContain('password');
|
|
});
|
|
});
|
|
|
|
describe('redactUpstream', () => {
|
|
test('redacts userId and password', () => {
|
|
const out = redactUpstream({
|
|
host: 'proxy.example.com',
|
|
port: 1080,
|
|
userId: 'realuser',
|
|
password: 'realpass',
|
|
});
|
|
expect(out.host).toBe('proxy.example.com');
|
|
expect(out.port).toBe(1080);
|
|
expect(out.userId).toBe('***');
|
|
expect(out.password).toBe('***');
|
|
});
|
|
|
|
test('omits userId/password when not present', () => {
|
|
const out = redactUpstream({ host: 'proxy.example.com', port: 1080 });
|
|
expect(out.userId).toBeUndefined();
|
|
expect(out.password).toBeUndefined();
|
|
});
|
|
});
|