Fix SSH relay connect when default shell is fish (#2239)
This commit is contained in:
parent
db8af09f86
commit
fdc38bfc33
|
|
@ -72,6 +72,12 @@ export function shellEscape(s: string): string {
|
|||
return `'${s.replace(/'/g, "'\\''")}'`
|
||||
}
|
||||
|
||||
export function wrapRemoteCommandForPosixShell(command: string): string {
|
||||
// Why: sshd asks the user's login shell to parse exec commands. Orca emits
|
||||
// POSIX sh snippets; `exec` avoids leaving that shell around for relay bridges.
|
||||
return `exec /bin/sh -c ${shellEscape(command)}`
|
||||
}
|
||||
|
||||
function cmdEscape(s: string): string {
|
||||
return `"${s.replace(/"/g, '""')}"`
|
||||
}
|
||||
|
|
@ -200,7 +206,9 @@ export function spawnProxyCommand(
|
|||
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'] })
|
||||
spawn('ssh', ['-W', `${host}:${port}`, '--', proxy.jumpHost], {
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
})
|
||||
: (() => {
|
||||
const escape = process.platform === 'win32' ? cmdEscape : shellEscape
|
||||
const expanded = proxy.command
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ let connectErrorMessage = ''
|
|||
type MockSshClient = {
|
||||
setNoDelay: ReturnType<typeof vi.fn>
|
||||
_sock: Socket | undefined
|
||||
lastExecCommand?: string
|
||||
}
|
||||
let clientInstances: MockSshClient[] = []
|
||||
|
||||
|
|
@ -18,6 +19,7 @@ vi.mock('ssh2', () => {
|
|||
// to decide which log line to emit. A real Socket instance lets the test
|
||||
// exercise the "enabled" branch instead of the "skipped (proxy socket)" branch.
|
||||
_sock: Socket | undefined = new Socket()
|
||||
lastExecCommand?: string
|
||||
constructor() {
|
||||
clientInstances.push(this)
|
||||
}
|
||||
|
|
@ -35,7 +37,10 @@ vi.mock('ssh2', () => {
|
|||
}
|
||||
end() {}
|
||||
destroy() {}
|
||||
exec() {}
|
||||
exec(cmd: string, cb: (err: Error | undefined, channel: unknown) => void) {
|
||||
this.lastExecCommand = cmd
|
||||
cb(undefined, {})
|
||||
}
|
||||
sftp() {}
|
||||
}
|
||||
return { Client: MockSshClient }
|
||||
|
|
@ -205,6 +210,17 @@ describe('SshConnection', () => {
|
|||
|
||||
expect(resolveWithSshG).toHaveBeenCalledWith('ssh-alias')
|
||||
})
|
||||
|
||||
it('wraps exec commands in /bin/sh so non-POSIX login shells do not parse relay snippets', async () => {
|
||||
const conn = new SshConnection(createTarget(), createCallbacks())
|
||||
await conn.connect()
|
||||
|
||||
await conn.exec("cd '/tmp' && ('/usr/bin/node' -e 'console.log(1)' || echo MISSING)")
|
||||
|
||||
expect(clientInstances[0].lastExecCommand).toBe(
|
||||
"exec /bin/sh -c 'cd '\\''/tmp'\\'' && ('\\''/usr/bin/node'\\'' -e '\\''console.log(1)'\\'' || echo MISSING)'"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SshConnectionManager', () => {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
buildConnectConfig,
|
||||
resolveEffectiveProxy,
|
||||
spawnProxyCommand,
|
||||
wrapRemoteCommandForPosixShell,
|
||||
type SshConnectionCallbacks
|
||||
} from './ssh-connection-utils'
|
||||
export type { SshConnectionCallbacks } from './ssh-connection-utils'
|
||||
|
|
@ -70,7 +71,9 @@ export class SshConnection {
|
|||
if (!this.client) {
|
||||
throw new Error('Not connected')
|
||||
}
|
||||
return new Promise((res, rej) => this.client!.exec(cmd, (e, ch) => (e ? rej(e) : res(ch))))
|
||||
return new Promise((res, rej) =>
|
||||
this.client!.exec(wrapRemoteCommandForPosixShell(cmd), (e, ch) => (e ? rej(e) : res(ch)))
|
||||
)
|
||||
}
|
||||
|
||||
async sftp(): Promise<SFTPWrapper> {
|
||||
|
|
|
|||
Loading…
Reference in New Issue