fix(relay): honor the remote OpenSSH login shell for Windows SSH terminals (#9455)
* fix(relay): honor the remote OpenSSH login shell for Windows SSH terminals (#9327) * Prove OpenSSH DefaultShell fallback stays safe on bad registry reads
This commit is contained in:
parent
5a1ca24262
commit
26e48e4159
|
|
@ -1,11 +1,13 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { execFileMock } = vi.hoisted(() => ({
|
||||
execFileMock: vi.fn()
|
||||
const { execFileMock, execFileSyncMock } = vi.hoisted(() => ({
|
||||
execFileMock: vi.fn(),
|
||||
execFileSyncMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('child_process', () => ({
|
||||
execFile: execFileMock
|
||||
execFile: execFileMock,
|
||||
execFileSync: execFileSyncMock
|
||||
}))
|
||||
|
||||
import { resetWindowsProcessRowsSnapshotForTests } from '../main/providers/windows-foreground-process-rows'
|
||||
|
|
@ -49,7 +51,9 @@ async function withProcessPlatform<T>(
|
|||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
execFileMock.mockReset()
|
||||
execFileSyncMock.mockReset()
|
||||
resetProcessTableSnapshotForTests()
|
||||
resetWindowsProcessRowsSnapshotForTests()
|
||||
})
|
||||
|
|
@ -97,11 +101,124 @@ describe('resolveWindowsDefaultShell', () => {
|
|||
SystemRoot: 'C:\\Windows',
|
||||
ComSpec: 'C:\\Windows\\System32\\cmd.exe'
|
||||
},
|
||||
(path) => path === 'C:\\Tools\\pwsh.exe'
|
||||
(path) => path === 'C:\\Tools\\pwsh.exe',
|
||||
() => {
|
||||
throw new Error('DefaultShell should not be read when SHELL wins')
|
||||
}
|
||||
)
|
||||
).toBe('C:\\Tools\\pwsh.exe')
|
||||
})
|
||||
|
||||
it('uses an existing OpenSSH DefaultShell path', () => {
|
||||
const powershell7 = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'
|
||||
|
||||
expect(
|
||||
resolveWindowsDefaultShell(
|
||||
{
|
||||
SystemRoot: 'C:\\Windows',
|
||||
ComSpec: 'C:\\Windows\\System32\\cmd.exe'
|
||||
},
|
||||
(path) => path === powershell7,
|
||||
() => powershell7
|
||||
)
|
||||
).toBe(powershell7)
|
||||
})
|
||||
|
||||
it('reads and memoizes the OpenSSH DefaultShell registry value', async () => {
|
||||
execFileSyncMock.mockReturnValue(
|
||||
[
|
||||
'HKEY_LOCAL_MACHINE\\SOFTWARE\\OpenSSH',
|
||||
' DefaultShell REG_SZ C:\\Program Files\\PowerShell\\7\\pwsh.exe'
|
||||
].join('\n')
|
||||
)
|
||||
|
||||
const { readOpenSshDefaultShell } = await import('./pty-shell-utils')
|
||||
|
||||
expect(readOpenSshDefaultShell()).toBe('C:\\Program Files\\PowerShell\\7\\pwsh.exe')
|
||||
expect(readOpenSshDefaultShell()).toBe('C:\\Program Files\\PowerShell\\7\\pwsh.exe')
|
||||
expect(execFileSyncMock).toHaveBeenCalledTimes(1)
|
||||
expect(execFileSyncMock).toHaveBeenCalledWith(
|
||||
'reg.exe',
|
||||
['query', 'HKLM\\SOFTWARE\\OpenSSH', '/v', 'DefaultShell'],
|
||||
{ encoding: 'utf8', timeout: 3000, windowsHide: true }
|
||||
)
|
||||
})
|
||||
|
||||
it('treats malformed OpenSSH DefaultShell output as empty and preserves the fallback chain', async () => {
|
||||
execFileSyncMock.mockReturnValue(
|
||||
[
|
||||
'HKEY_LOCAL_MACHINE\\SOFTWARE\\OpenSSH',
|
||||
' DefaultShellCommandOption REG_SZ /c'
|
||||
].join('\n')
|
||||
)
|
||||
|
||||
const { readOpenSshDefaultShell } = await import('./pty-shell-utils')
|
||||
const powershell = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
|
||||
|
||||
expect(readOpenSshDefaultShell()).toBe('')
|
||||
expect(
|
||||
resolveWindowsDefaultShell(
|
||||
{
|
||||
SystemRoot: 'C:\\Windows',
|
||||
ComSpec: 'C:\\Windows\\System32\\cmd.exe'
|
||||
},
|
||||
(path) => path === powershell || path === 'C:\\Windows\\System32\\cmd.exe',
|
||||
readOpenSshDefaultShell
|
||||
)
|
||||
).toBe(powershell)
|
||||
})
|
||||
|
||||
it('treats reg.exe failures as empty and preserves the fallback chain', async () => {
|
||||
execFileSyncMock.mockImplementation(() => {
|
||||
throw new Error('reg.exe failed')
|
||||
})
|
||||
|
||||
const { readOpenSshDefaultShell } = await import('./pty-shell-utils')
|
||||
const powershell = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
|
||||
|
||||
expect(readOpenSshDefaultShell()).toBe('')
|
||||
expect(
|
||||
resolveWindowsDefaultShell(
|
||||
{
|
||||
SystemRoot: 'C:\\Windows',
|
||||
ComSpec: 'C:\\Windows\\System32\\cmd.exe'
|
||||
},
|
||||
(path) => path === powershell || path === 'C:\\Windows\\System32\\cmd.exe',
|
||||
readOpenSshDefaultShell
|
||||
)
|
||||
).toBe(powershell)
|
||||
})
|
||||
|
||||
it('preserves the fallback chain for an invalid OpenSSH DefaultShell', () => {
|
||||
const powershell = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
|
||||
|
||||
expect(
|
||||
resolveWindowsDefaultShell(
|
||||
{
|
||||
SystemRoot: 'C:\\Windows',
|
||||
ComSpec: 'C:\\Windows\\System32\\cmd.exe'
|
||||
},
|
||||
(path) => path === powershell,
|
||||
() => 'C:\\missing\\pwsh.exe'
|
||||
)
|
||||
).toBe(powershell)
|
||||
})
|
||||
|
||||
it('honors a deliberate OpenSSH PowerShell 5.1 DefaultShell value', () => {
|
||||
const powershell = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
|
||||
|
||||
expect(
|
||||
resolveWindowsDefaultShell(
|
||||
{
|
||||
SystemRoot: 'C:\\Windows',
|
||||
ComSpec: 'C:\\Windows\\System32\\cmd.exe'
|
||||
},
|
||||
(path) => path === powershell,
|
||||
() => powershell
|
||||
)
|
||||
).toBe(powershell)
|
||||
})
|
||||
|
||||
it('prefers inbox PowerShell before ComSpec for an interactive Windows PTY', () => {
|
||||
const powershell = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
|
||||
|
||||
|
|
@ -111,7 +228,8 @@ describe('resolveWindowsDefaultShell', () => {
|
|||
SystemRoot: 'C:\\Windows',
|
||||
ComSpec: 'C:\\Windows\\System32\\cmd.exe'
|
||||
},
|
||||
(path) => path === powershell || path === 'C:\\Windows\\System32\\cmd.exe'
|
||||
(path) => path === powershell || path === 'C:\\Windows\\System32\\cmd.exe',
|
||||
() => ''
|
||||
)
|
||||
).toBe(powershell)
|
||||
})
|
||||
|
|
@ -123,7 +241,8 @@ describe('resolveWindowsDefaultShell', () => {
|
|||
SystemRoot: 'C:\\Windows',
|
||||
ComSpec: 'C:\\Windows\\System32\\cmd.exe'
|
||||
},
|
||||
(path) => path === 'C:\\Windows\\System32\\cmd.exe'
|
||||
(path) => path === 'C:\\Windows\\System32\\cmd.exe',
|
||||
() => ''
|
||||
)
|
||||
).toBe('C:\\Windows\\System32\\cmd.exe')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { execFile as execFileCb } from 'node:child_process'
|
||||
import { execFile as execFileCb, execFileSync } from 'node:child_process'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { win32 as pathWin32 } from 'node:path'
|
||||
|
|
@ -23,15 +23,44 @@ import {
|
|||
|
||||
const execFile = promisify(execFileCb)
|
||||
|
||||
const OPENSSH_REGISTRY_KEY = 'HKLM\\SOFTWARE\\OpenSSH'
|
||||
let openSshDefaultShell: string | undefined
|
||||
|
||||
export function readOpenSshDefaultShell(): string {
|
||||
if (openSshDefaultShell !== undefined) {
|
||||
return openSshDefaultShell
|
||||
}
|
||||
|
||||
try {
|
||||
const output = execFileSync('reg.exe', ['query', OPENSSH_REGISTRY_KEY, '/v', 'DefaultShell'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 3000,
|
||||
windowsHide: true
|
||||
})
|
||||
const match = output.match(/^\s*DefaultShell\s+REG_\w+\s+(.+?)\s*$/im)
|
||||
openSshDefaultShell = match?.[1] ?? ''
|
||||
} catch {
|
||||
openSshDefaultShell = ''
|
||||
}
|
||||
|
||||
return openSshDefaultShell
|
||||
}
|
||||
|
||||
export function resolveWindowsDefaultShell(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
existsPath: (path: string) => boolean = existsSync
|
||||
existsPath: (path: string) => boolean = existsSync,
|
||||
readDefaultShell: () => string = readOpenSshDefaultShell
|
||||
): string {
|
||||
const envShell = env.SHELL
|
||||
if (envShell && existsPath(envShell)) {
|
||||
return envShell
|
||||
}
|
||||
|
||||
const configuredShell = readDefaultShell()
|
||||
if (configuredShell && existsPath(configuredShell)) {
|
||||
return configuredShell
|
||||
}
|
||||
|
||||
const systemRoot = env.SystemRoot || env.WINDIR || env.windir || 'C:\\Windows'
|
||||
const windowsPowerShell = pathWin32.join(
|
||||
systemRoot,
|
||||
|
|
|
|||
Loading…
Reference in New Issue