fix(ssh): extract proxy command helpers to pass max-lines

Move resolveEffectiveProxy/spawnProxyCommand out of ssh-connection-utils
so oxfmt line wrapping cannot push that file over the 300-line lint cap.
This commit is contained in:
Jinjing
2026-08-02 17:07:39 -07:00
parent ebb3072d95
commit 89bf31217e
4 changed files with 123 additions and 121 deletions
+2 -3
View File
@@ -39,13 +39,12 @@ import {
findDefaultKeyFile,
buildConnectConfig,
resolveAgentSocket,
resolveEffectiveProxy,
CONNECT_TIMEOUT_MS,
INITIAL_RETRY_ATTEMPTS,
INITIAL_RETRY_DELAY_MS,
RECONNECT_BACKOFF_MS,
spawnProxyCommand
RECONNECT_BACKOFF_MS
} from './ssh-connection-utils'
import { resolveEffectiveProxy, spawnProxyCommand } from './ssh-proxy-command'
import type { SshTarget } from '../../shared/ssh-types'
import type { SshResolvedConfig } from './ssh-config-parser'
-116
View File
@@ -1,6 +1,3 @@
import { spawn, type ChildProcess } from 'node:child_process'
import { Duplex } from 'node:stream'
import type { Socket as NetSocket } from 'node:net'
import type { ConnectConfig } from 'ssh2'
import type { SshTarget, SshConnectionState } from '../../shared/ssh-types'
import type { SshResolvedConfig } from './ssh-config-parser'
@@ -167,10 +164,6 @@ export function createSshOperationAbortError(): Error & { name: string } {
return error
}
function cmdEscape(s: string): string {
return `"${s.replace(/"/g, '""')}"`
}
type BuildConnectConfigOptions = {
includeAgent?: boolean
includePrivateKey?: boolean
@@ -253,112 +246,3 @@ function shouldUseResolvedEndpoint(target: SshTarget, resolved: SshResolvedConfi
const host = target.host.trim()
return host === '' || host === target.configHost || host === target.label
}
// Why: ProxyJump and jumpHost are syntactic sugar for ProxyCommand.
// OpenSSH internally converts `ProxyJump bastion` to
// `ProxyCommand ssh -W %h:%p bastion`. We do the same so that ssh2
// gets a single proxy spawn path regardless of how the tunnel was configured.
export type EffectiveProxy =
| { kind: 'proxy-command'; command: string }
| { kind: 'jump-host'; jumpHost: string }
export function resolveEffectiveProxy(
target: SshTarget,
resolved: SshResolvedConfig | null
): EffectiveProxy | undefined {
if (isOpenSshConfigBackedTarget(target) && resolved) {
if (resolved.proxyCommand) {
return { kind: 'proxy-command', command: resolved.proxyCommand }
}
return resolved.proxyJump ? { kind: 'jump-host', jumpHost: resolved.proxyJump } : undefined
}
if (target.proxyCommand) {
return { kind: 'proxy-command', command: target.proxyCommand }
}
if (resolved?.proxyCommand) {
return { kind: 'proxy-command', command: resolved.proxyCommand }
}
const jump = target.jumpHost || resolved?.proxyJump
if (jump) {
return { kind: 'jump-host', jumpHost: jump }
}
return undefined
}
// Why: ssh2 doesn't natively support ProxyCommand. When the SSH config
// specifies one (e.g. `cloudflared access ssh --hostname %h`), we spawn
// the command and bridge its stdin/stdout into a Duplex stream that ssh2
// uses as its transport socket via `config.sock`.
function getShellSpawnConfig(command: string): { file: string; args: string[] } {
if (process.platform === 'win32') {
const comspec = process.env.ComSpec || 'cmd.exe'
return { file: comspec, args: ['/d', '/s', '/c', command] }
}
return { file: '/bin/sh', args: ['-c', command] }
}
export function spawnProxyCommand(
proxy: EffectiveProxy,
host: string,
port: number,
user: string
): { process: ChildProcess; sock: NetSocket } {
const proc =
proxy.kind === 'jump-host'
? // Why: ProxyJump is structured input, not a shell snippet. Spawn ssh
// directly so jump-host values cannot escape through shell parsing.
spawn('ssh', ['-W', `${host}:${port}`, '--', proxy.jumpHost], {
stdio: ['pipe', 'pipe', 'pipe']
})
: (() => {
const escape = process.platform === 'win32' ? cmdEscape : shellEscape
const expanded = proxy.command
.replace(/%h/g, escape(host))
.replace(/%p/g, escape(String(port)))
.replace(/%r/g, escape(user))
const shell = getShellSpawnConfig(expanded)
return spawn(shell.file, shell.args, { stdio: ['pipe', 'pipe', 'pipe'] })
})()
// Why: a single PassThrough for both directions creates a feedback loop.
// Reads come from the proxy's stdout; writes go to its stdin.
let cleanedUp = false
const cleanup = (): void => {
if (cleanedUp) {
return
}
cleanedUp = true
proc.stdout!.off('data', onStdoutData)
proc.stdout!.off('end', onStdoutEnd)
proc.stdin!.off('error', onInputError)
proc.off('error', onProcessError)
}
const onStdoutData = (data: Buffer): void => {
stream.push(data)
}
const onStdoutEnd = (): void => {
stream.push(null)
}
const onInputError = (err: Error): void => {
stream.destroy(err)
}
const onProcessError = (err: Error): void => {
stream.destroy(err)
}
const stream = new Duplex({
read() {},
write(chunk, _encoding, cb) {
proc.stdin!.write(chunk, cb)
},
destroy(err, cb) {
cleanup()
cb(err)
}
})
proc.stdout!.on('data', onStdoutData)
proc.stdout!.on('end', onStdoutEnd)
proc.stdin!.on('error', onInputError)
proc.on('error', onProcessError)
return { process: proc, sock: stream as unknown as NetSocket }
}
+1 -2
View File
@@ -33,13 +33,12 @@ import {
isPassphraseError,
sleep,
buildConnectConfig,
resolveEffectiveProxy,
spawnProxyCommand,
wrapRemoteCommandForPosixShell,
createSshOperationAbortError,
type SshExecOptions,
type SshConnectionCallbacks
} from './ssh-connection-utils'
import { resolveEffectiveProxy, spawnProxyCommand } from './ssh-proxy-command'
import {
createCancelledConnectAttemptError,
isCancelledConnectAttemptError
+120
View File
@@ -0,0 +1,120 @@
import { spawn, type ChildProcess } from 'node:child_process'
import { Duplex } from 'node:stream'
import type { Socket as NetSocket } from 'node:net'
import type { SshTarget } from '../../shared/ssh-types'
import type { SshResolvedConfig } from './ssh-config-parser'
import { shellEscape } from './ssh-connection-utils'
import { isOpenSshConfigBackedTarget } from './system-ssh-args'
// Why: ProxyJump and jumpHost are syntactic sugar for ProxyCommand.
// OpenSSH internally converts `ProxyJump bastion` to
// `ProxyCommand ssh -W %h:%p bastion`. We do the same so that ssh2
// gets a single proxy spawn path regardless of how the tunnel was configured.
export type EffectiveProxy =
| { kind: 'proxy-command'; command: string }
| { kind: 'jump-host'; jumpHost: string }
export function resolveEffectiveProxy(
target: SshTarget,
resolved: SshResolvedConfig | null
): EffectiveProxy | undefined {
if (isOpenSshConfigBackedTarget(target) && resolved) {
if (resolved.proxyCommand) {
return { kind: 'proxy-command', command: resolved.proxyCommand }
}
return resolved.proxyJump ? { kind: 'jump-host', jumpHost: resolved.proxyJump } : undefined
}
if (target.proxyCommand) {
return { kind: 'proxy-command', command: target.proxyCommand }
}
if (resolved?.proxyCommand) {
return { kind: 'proxy-command', command: resolved.proxyCommand }
}
const jump = target.jumpHost || resolved?.proxyJump
if (jump) {
return { kind: 'jump-host', jumpHost: jump }
}
return undefined
}
function cmdEscape(s: string): string {
return `"${s.replace(/"/g, '""')}"`
}
// Why: ssh2 doesn't natively support ProxyCommand. When the SSH config
// specifies one (e.g. `cloudflared access ssh --hostname %h`), we spawn
// the command and bridge its stdin/stdout into a Duplex stream that ssh2
// uses as its transport socket via `config.sock`.
function getShellSpawnConfig(command: string): { file: string; args: string[] } {
if (process.platform === 'win32') {
const comspec = process.env.ComSpec || 'cmd.exe'
return { file: comspec, args: ['/d', '/s', '/c', command] }
}
return { file: '/bin/sh', args: ['-c', command] }
}
export function spawnProxyCommand(
proxy: EffectiveProxy,
host: string,
port: number,
user: string
): { process: ChildProcess; sock: NetSocket } {
const proc =
proxy.kind === 'jump-host'
? // Why: ProxyJump is structured input, not a shell snippet. Spawn ssh
// directly so jump-host values cannot escape through shell parsing.
spawn('ssh', ['-W', `${host}:${port}`, '--', proxy.jumpHost], {
stdio: ['pipe', 'pipe', 'pipe']
})
: (() => {
const escape = process.platform === 'win32' ? cmdEscape : shellEscape
const expanded = proxy.command
.replace(/%h/g, escape(host))
.replace(/%p/g, escape(String(port)))
.replace(/%r/g, escape(user))
const shell = getShellSpawnConfig(expanded)
return spawn(shell.file, shell.args, { stdio: ['pipe', 'pipe', 'pipe'] })
})()
// Why: a single PassThrough for both directions creates a feedback loop.
// Reads come from the proxy's stdout; writes go to its stdin.
let cleanedUp = false
const cleanup = (): void => {
if (cleanedUp) {
return
}
cleanedUp = true
proc.stdout!.off('data', onStdoutData)
proc.stdout!.off('end', onStdoutEnd)
proc.stdin!.off('error', onInputError)
proc.off('error', onProcessError)
}
const onStdoutData = (data: Buffer): void => {
stream.push(data)
}
const onStdoutEnd = (): void => {
stream.push(null)
}
const onInputError = (err: Error): void => {
stream.destroy(err)
}
const onProcessError = (err: Error): void => {
stream.destroy(err)
}
const stream = new Duplex({
read() {},
write(chunk, _encoding, cb) {
proc.stdin!.write(chunk, cb)
},
destroy(err, cb) {
cleanup()
cb(err)
}
})
proc.stdout!.on('data', onStdoutData)
proc.stdout!.on('end', onStdoutEnd)
proc.stdin!.on('error', onInputError)
proc.on('error', onProcessError)
return { process: proc, sock: stream as unknown as NetSocket }
}