* fix(ssh): accept GitHub restricted-shell SSH probes (#6988) * fix: match first stderr line for GitHub restricted-shell probe (bug-bash takeover) Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
c1d2c4be08
commit
6d39e49480
|
|
@ -1099,6 +1099,228 @@ describe('SshConnection', () => {
|
|||
expect(conn.canRunConcurrentExecCommands()).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts GitHub restricted-shell SSH probes with resolved user fallback', async () => {
|
||||
vi.mocked(resolveWithSshG).mockResolvedValueOnce(
|
||||
createResolvedConfig({ hostname: 'github.com', user: 'git' })
|
||||
)
|
||||
spawnSystemSshCommandMock.mockImplementation(() =>
|
||||
createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK')
|
||||
)
|
||||
const conn = new SshConnection(
|
||||
createTarget({
|
||||
configHost: 'github.com',
|
||||
host: 'github.com',
|
||||
username: undefined
|
||||
}),
|
||||
createCallbacks()
|
||||
)
|
||||
|
||||
await conn.connect()
|
||||
|
||||
expect(conn.getState().status).toBe('connected')
|
||||
expect(conn.usesSystemSshTransport()).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts GitHub restricted-shell SSH probes with resolved host and target username', async () => {
|
||||
vi.mocked(resolveWithSshG).mockResolvedValueOnce(
|
||||
createResolvedConfig({ hostname: 'github.com', user: undefined })
|
||||
)
|
||||
spawnSystemSshCommandMock.mockImplementation(() =>
|
||||
createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK')
|
||||
)
|
||||
const conn = new SshConnection(
|
||||
createTarget({
|
||||
configHost: 'github.com',
|
||||
host: 'github.com',
|
||||
username: 'git'
|
||||
}),
|
||||
createCallbacks()
|
||||
)
|
||||
|
||||
await conn.connect()
|
||||
|
||||
expect(conn.getState().status).toBe('connected')
|
||||
expect(conn.usesSystemSshTransport()).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts ssh.github.com restricted-shell SSH probes', async () => {
|
||||
vi.mocked(resolveWithSshG).mockResolvedValueOnce(
|
||||
createResolvedConfig({ hostname: 'ssh.github.com', user: 'git' })
|
||||
)
|
||||
spawnSystemSshCommandMock.mockImplementation(() =>
|
||||
createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK')
|
||||
)
|
||||
const conn = new SshConnection(
|
||||
createTarget({
|
||||
configHost: 'ssh.github.com',
|
||||
host: 'ssh.github.com',
|
||||
username: 'git'
|
||||
}),
|
||||
createCallbacks()
|
||||
)
|
||||
|
||||
await conn.connect()
|
||||
|
||||
expect(conn.getState().status).toBe('connected')
|
||||
expect(conn.usesSystemSshTransport()).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts GitHub restricted-shell SSH probes with the real git:// advisory transcript', async () => {
|
||||
// Real 4-line stderr GitHub returns for an invalid command (issue #6988).
|
||||
vi.mocked(resolveWithSshG).mockResolvedValueOnce(
|
||||
createResolvedConfig({ hostname: 'github.com', user: 'git' })
|
||||
)
|
||||
spawnSystemSshCommandMock.mockImplementation(() =>
|
||||
createFailingSystemCommandChannel(
|
||||
1,
|
||||
'Invalid command: echo ORCA-SYSTEM-SSH-OK\n' +
|
||||
' You appear to be using ssh to clone a git:// URL.\n' +
|
||||
' Make sure your core.gitProxy config option and the\n' +
|
||||
' GIT_PROXY_COMMAND environment variable are NOT set.'
|
||||
)
|
||||
)
|
||||
const conn = new SshConnection(
|
||||
createTarget({
|
||||
configHost: 'github.com',
|
||||
host: 'github.com',
|
||||
username: 'git'
|
||||
}),
|
||||
createCallbacks()
|
||||
)
|
||||
|
||||
await conn.connect()
|
||||
|
||||
expect(conn.getState().status).toBe('connected')
|
||||
expect(conn.usesSystemSshTransport()).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts GitHub restricted-shell SSH probes when OpenSSH config resolution fails', async () => {
|
||||
vi.stubEnv('ORCA_SSH_FORCE_SYSTEM_TRANSPORT', '1')
|
||||
vi.mocked(resolveWithSshG).mockRejectedValueOnce(new Error('ssh -G failed'))
|
||||
spawnSystemSshCommandMock.mockImplementation(() =>
|
||||
createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK')
|
||||
)
|
||||
const conn = new SshConnection(
|
||||
createTarget({
|
||||
configHost: 'github.com',
|
||||
host: 'github.com',
|
||||
username: 'git'
|
||||
}),
|
||||
createCallbacks()
|
||||
)
|
||||
|
||||
await conn.connect()
|
||||
|
||||
expect(conn.getState().status).toBe('connected')
|
||||
expect(conn.usesSystemSshTransport()).toBe(true)
|
||||
expect(conn.getSystemSshResolvedConfig()).toBeNull()
|
||||
})
|
||||
|
||||
it('rejects non-GitHub SSH probes with GitHub invalid-command text', async () => {
|
||||
vi.mocked(resolveWithSshG).mockResolvedValueOnce(
|
||||
createResolvedConfig({ hostname: 'gitlab.com', user: 'git' })
|
||||
)
|
||||
spawnSystemSshCommandMock.mockImplementation(() =>
|
||||
createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK')
|
||||
)
|
||||
const conn = new SshConnection(
|
||||
createTarget({
|
||||
configHost: 'github.com',
|
||||
host: 'github.com',
|
||||
username: 'git'
|
||||
}),
|
||||
createCallbacks()
|
||||
)
|
||||
|
||||
await expect(conn.connect()).rejects.toThrow('System SSH probe failed (exit 1)')
|
||||
expect(conn.usesSystemSshTransport()).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts GitHub restricted-shell SSH probes when target username overrides resolved user', async () => {
|
||||
vi.mocked(resolveWithSshG).mockResolvedValueOnce(
|
||||
createResolvedConfig({ hostname: 'github.com', user: 'deploy' })
|
||||
)
|
||||
spawnSystemSshCommandMock.mockImplementation(() =>
|
||||
createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK')
|
||||
)
|
||||
const conn = new SshConnection(
|
||||
createTarget({
|
||||
configHost: 'github.com',
|
||||
host: 'github.com',
|
||||
username: 'git'
|
||||
}),
|
||||
createCallbacks()
|
||||
)
|
||||
|
||||
await conn.connect()
|
||||
|
||||
expect(conn.getState().status).toBe('connected')
|
||||
expect(conn.usesSystemSshTransport()).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects GitHub restricted-shell SSH probes when target username overrides resolved git user', async () => {
|
||||
vi.mocked(resolveWithSshG).mockResolvedValueOnce(
|
||||
createResolvedConfig({ hostname: 'github.com', user: 'git' })
|
||||
)
|
||||
spawnSystemSshCommandMock.mockImplementation(() =>
|
||||
createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK')
|
||||
)
|
||||
const conn = new SshConnection(
|
||||
createTarget({
|
||||
configHost: 'github.com',
|
||||
host: 'github.com',
|
||||
username: 'deploy'
|
||||
}),
|
||||
createCallbacks()
|
||||
)
|
||||
|
||||
await expect(conn.connect()).rejects.toThrow('System SSH probe failed (exit 1)')
|
||||
expect(conn.usesSystemSshTransport()).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects GitHub restricted-shell SSH probes with extra stderr text', async () => {
|
||||
vi.mocked(resolveWithSshG).mockResolvedValueOnce(
|
||||
createResolvedConfig({ hostname: 'github.com', user: 'git' })
|
||||
)
|
||||
spawnSystemSshCommandMock.mockImplementation(() =>
|
||||
createFailingSystemCommandChannel(
|
||||
1,
|
||||
'remote: rejected\nInvalid command: echo ORCA-SYSTEM-SSH-OK\ntry again'
|
||||
)
|
||||
)
|
||||
const conn = new SshConnection(
|
||||
createTarget({
|
||||
configHost: 'github.com',
|
||||
host: 'github.com',
|
||||
username: 'git'
|
||||
}),
|
||||
createCallbacks()
|
||||
)
|
||||
|
||||
await expect(conn.connect()).rejects.toThrow('System SSH probe failed (exit 1)')
|
||||
expect(conn.usesSystemSshTransport()).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects GitHub restricted-shell SSH probes for non-git users', async () => {
|
||||
vi.mocked(resolveWithSshG).mockResolvedValueOnce(
|
||||
createResolvedConfig({ hostname: 'github.com', user: 'deploy' })
|
||||
)
|
||||
spawnSystemSshCommandMock.mockImplementation(() =>
|
||||
createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK')
|
||||
)
|
||||
const conn = new SshConnection(
|
||||
createTarget({
|
||||
configHost: 'github.com',
|
||||
host: 'github.com',
|
||||
username: 'deploy'
|
||||
}),
|
||||
createCallbacks()
|
||||
)
|
||||
|
||||
await expect(conn.connect()).rejects.toThrow('System SSH probe failed (exit 1)')
|
||||
expect(conn.usesSystemSshTransport()).toBe(false)
|
||||
})
|
||||
|
||||
it('retries a failed system SSH probe without ControlMaster and disables mux for the session', async () => {
|
||||
getOrcaControlSocketPathMock.mockImplementation(
|
||||
(_target: SshTarget, options?: { disableControlMaster?: boolean }) =>
|
||||
|
|
|
|||
|
|
@ -66,6 +66,36 @@ function cloneResolvedConfig(config: SshResolvedConfig | null): SshResolvedConfi
|
|||
return { ...config, identityFile: [...config.identityFile] }
|
||||
}
|
||||
|
||||
function isGitHubRestrictedShellProbeSuccess(
|
||||
target: SshTarget,
|
||||
resolvedConfig: SshResolvedConfig | null,
|
||||
code: number | null,
|
||||
stderr: string
|
||||
): boolean {
|
||||
if (code !== 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
const effectiveUser = (target.username?.trim() || resolvedConfig?.user?.trim())?.toLowerCase()
|
||||
if (effectiveUser !== 'git') {
|
||||
return false
|
||||
}
|
||||
|
||||
// GitHub appends git:// advisory lines after the invalid-command line (issue #6988), so match the first line only.
|
||||
const firstLine = stderr.split('\n', 1)[0]?.trim()
|
||||
if (firstLine !== 'Invalid command: echo ORCA-SYSTEM-SSH-OK') {
|
||||
return false
|
||||
}
|
||||
|
||||
const resolvedHost = resolvedConfig?.hostname?.trim()
|
||||
const hostCandidates = resolvedHost ? [resolvedHost] : [target.host, target.configHost]
|
||||
|
||||
return hostCandidates.some((host) => {
|
||||
const normalizedHost = host?.trim().toLowerCase()
|
||||
return normalizedHost === 'github.com' || normalizedHost === 'ssh.github.com'
|
||||
})
|
||||
}
|
||||
|
||||
export class SshConnection {
|
||||
private client: SshClient | null = null
|
||||
private proxyProcess: ChildProcess | null = null
|
||||
|
|
@ -847,16 +877,24 @@ export class SshConnection {
|
|||
reject(new Error('SSH connection attempt was cancelled'))
|
||||
return
|
||||
}
|
||||
if (code !== 0 || !stdout.includes('ORCA-SYSTEM-SSH-OK')) {
|
||||
reject(
|
||||
new Error(
|
||||
`System SSH probe failed${code != null ? ` (exit ${code})` : ''}.${stderr ? ` stderr: ${stderr.trim()}` : ''}`
|
||||
)
|
||||
if (
|
||||
(code === 0 && stdout.includes('ORCA-SYSTEM-SSH-OK')) ||
|
||||
isGitHubRestrictedShellProbeSuccess(
|
||||
this.target,
|
||||
this.systemSshResolvedConfig,
|
||||
code,
|
||||
stderr
|
||||
)
|
||||
) {
|
||||
this.setState('connected')
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
this.setState('connected')
|
||||
resolve()
|
||||
reject(
|
||||
new Error(
|
||||
`System SSH probe failed${code != null ? ` (exit ${code})` : ''}.${stderr ? ` stderr: ${stderr.trim()}` : ''}`
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
const timeout = setTimeout(() => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue