mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
An SSH workspace's browser reaches sites from the SSH host: the host's sshd dials each destination over a direct-tcpip channel. sshd ignores the host's HTTPS_PROXY/HTTP_PROXY/ALL_PROXY, so on a host whose only way out is an HTTP proxy (a Docker sandbox, for example), every external site failed with ERR_SOCKS_CONNECTION_FAILED while localhost pages loaded. The SSH route now reads the proxy variables an exec session on the host gets (the environment orcad runs with) when it opens, and picks the upstream for each destination the same way a paired server's own tunnel does: environment proxy with its NO_PROXY list, never for loopback. An HTTP(S) proxy is reached over the SSH channel and asked to CONNECT; a no-auth SOCKS5 proxy gets a SOCKS handshake. This works for both the ssh2 and system OpenSSH transports. The CONNECT and SOCKS clients now run over any transport, so the server tunnel and the SSH route share them. Fixes #35217
141 lines
4.1 KiB
TypeScript
141 lines
4.1 KiB
TypeScript
export type NetworkProxySettings = {
|
|
httpProxyUrl?: string | null
|
|
httpProxyBypassRules?: string | null
|
|
}
|
|
|
|
// One English table: `message` stays English; the settings UI translates by code.
|
|
export const PROXY_URL_FAILURE_COPY = {
|
|
tooLong: 'Proxy URL is too long.',
|
|
invalid: 'Enter a valid proxy URL.',
|
|
unsupportedProtocol: 'Use an http, https, socks, socks4, or socks5 proxy URL.',
|
|
missingHost: 'Proxy URL must include a host.'
|
|
} as const
|
|
|
|
export type ProxyUrlFailureCode = keyof typeof PROXY_URL_FAILURE_COPY
|
|
|
|
export type ProxyUrlValidationResult =
|
|
| { ok: true; value: string; message?: undefined }
|
|
| { ok: false; value: ''; message: string; code: ProxyUrlFailureCode }
|
|
|
|
function proxyUrlFailure(code: ProxyUrlFailureCode): ProxyUrlValidationResult {
|
|
return { ok: false, value: '', message: PROXY_URL_FAILURE_COPY[code], code }
|
|
}
|
|
|
|
const PROXY_URL_MAX_LENGTH = 2048
|
|
const PROXY_BYPASS_RULES_MAX_LENGTH = 4096
|
|
const PROXY_PROTOCOLS = new Set(['http:', 'https:', 'socks:', 'socks4:', 'socks5:'])
|
|
const PROXY_ENV_KEYS = [
|
|
'HTTPS_PROXY',
|
|
'https_proxy',
|
|
'ALL_PROXY',
|
|
'all_proxy',
|
|
'HTTP_PROXY',
|
|
'http_proxy'
|
|
] as const
|
|
const NO_PROXY_ENV_KEYS = ['NO_PROXY', 'no_proxy'] as const
|
|
/** Every variable the environment proxy readers below consult. */
|
|
export const PROXY_ENVIRONMENT_KEYS: readonly string[] = [...PROXY_ENV_KEYS, ...NO_PROXY_ENV_KEYS]
|
|
|
|
function formatProxyUrl(url: URL): string {
|
|
const auth =
|
|
url.username || url.password ? `${url.username}${url.password ? `:${url.password}` : ''}@` : ''
|
|
return `${url.protocol}//${auth}${url.host}`
|
|
}
|
|
|
|
export function normalizeProxyUrl(value: unknown): ProxyUrlValidationResult {
|
|
if (typeof value !== 'string') {
|
|
return { ok: true, value: '' }
|
|
}
|
|
const trimmed = value.trim()
|
|
if (!trimmed) {
|
|
return { ok: true, value: '' }
|
|
}
|
|
if (trimmed.length > PROXY_URL_MAX_LENGTH) {
|
|
return proxyUrlFailure('tooLong')
|
|
}
|
|
|
|
let parsed: URL
|
|
try {
|
|
parsed = new URL(trimmed)
|
|
} catch {
|
|
return proxyUrlFailure('invalid')
|
|
}
|
|
if (!PROXY_PROTOCOLS.has(parsed.protocol)) {
|
|
return proxyUrlFailure('unsupportedProtocol')
|
|
}
|
|
if (!parsed.hostname) {
|
|
return proxyUrlFailure('missingHost')
|
|
}
|
|
return { ok: true, value: formatProxyUrl(parsed) }
|
|
}
|
|
|
|
export function normalizeProxyBypassRules(value: unknown): string {
|
|
if (typeof value !== 'string') {
|
|
return ''
|
|
}
|
|
return value
|
|
.slice(0, PROXY_BYPASS_RULES_MAX_LENGTH)
|
|
.split(/[;,\n]/)
|
|
.map((rule) => rule.trim())
|
|
.filter(Boolean)
|
|
.join(';')
|
|
}
|
|
|
|
export function getProxyUrlFromEnvironment(
|
|
env: Record<string, string | undefined>
|
|
): ProxyUrlValidationResult {
|
|
for (const key of PROXY_ENV_KEYS) {
|
|
if (env[key]) {
|
|
return normalizeProxyUrl(env[key])
|
|
}
|
|
}
|
|
return { ok: true, value: '' }
|
|
}
|
|
|
|
export function getProxyBypassRulesFromEnvironment(
|
|
env: Record<string, string | undefined>
|
|
): string {
|
|
for (const key of NO_PROXY_ENV_KEYS) {
|
|
if (env[key]) {
|
|
return normalizeProxyBypassRules(env[key])
|
|
}
|
|
}
|
|
return ''
|
|
}
|
|
|
|
export function buildConfiguredProxyEnv(
|
|
settings: NetworkProxySettings | null | undefined
|
|
): Record<string, string> {
|
|
const proxy = normalizeProxyUrl(settings?.httpProxyUrl)
|
|
if (!proxy.ok || !proxy.value) {
|
|
return {}
|
|
}
|
|
const env: Record<string, string> = {
|
|
HTTP_PROXY: proxy.value,
|
|
HTTPS_PROXY: proxy.value,
|
|
ALL_PROXY: proxy.value,
|
|
http_proxy: proxy.value,
|
|
https_proxy: proxy.value,
|
|
all_proxy: proxy.value
|
|
}
|
|
const bypassRules = normalizeProxyBypassRules(settings?.httpProxyBypassRules)
|
|
// Explicit proxy settings must not inherit shell NO_PROXY; only the configured bypass field controls child-process exclusions.
|
|
const noProxy = bypassRules ? bypassRules.replaceAll(';', ',') : ''
|
|
env.NO_PROXY = noProxy
|
|
env.no_proxy = noProxy
|
|
return env
|
|
}
|
|
|
|
export function redactProxyUrl(value: string): string {
|
|
const parsed = normalizeProxyUrl(value)
|
|
if (!parsed.ok || !parsed.value) {
|
|
return parsed.value
|
|
}
|
|
const url = new URL(parsed.value)
|
|
if (url.username || url.password) {
|
|
url.username = '***'
|
|
url.password = url.password ? '***' : ''
|
|
}
|
|
return formatProxyUrl(url)
|
|
}
|