Update sshImporters.ts to parse Include directive (#10105)

This commit is contained in:
Hiroaki Ogasawara 2025-01-01 09:13:02 +09:00 committed by GitHub
parent d0dd09ad88
commit 39e3ba35c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1,6 +1,7 @@
import * as fs from 'fs/promises' import * as fs from 'fs/promises'
import * as fsSync from 'fs' import * as fsSync from 'fs'
import * as path from 'path' import * as path from 'path'
import * as glob from 'glob'
import slugify from 'slugify' import slugify from 'slugify'
import * as yaml from 'js-yaml' import * as yaml from 'js-yaml'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
@ -14,7 +15,7 @@ import {
} from 'tabby-ssh' } from 'tabby-ssh'
import { ElectronService } from './services/electron.service' import { ElectronService } from './services/electron.service'
import SSHConfig, { LineType } from 'ssh-config' import SSHConfig, { Directive, LineType } from 'ssh-config'
// Enum to delineate the properties in SSHProfile options // Enum to delineate the properties in SSHProfile options
enum SSHProfilePropertyNames { enum SSHProfilePropertyNames {
@ -90,15 +91,60 @@ function convertSSHConfigValuesToString (arg: string | string[] | object[]): str
.join(' ') .join(' ')
} }
// Function to read in the SSH config file and return it as a string // Function to read in the SSH config file recursively and parse any Include directives
async function readSSHConfigFile (filePath: string): Promise<string> { async function parseSSHConfigFile (
try { filePath: string,
return await fs.readFile(filePath, 'utf8') visited = new Set<string>(),
} catch (err) { ): Promise<SSHConfig> {
console.error('Error reading SSH config file:', err) // If we've already processed this file, return an empty config to avoid infinite recursion
return '' if (visited.has(filePath)) {
return SSHConfig.parse('')
} }
visited.add(filePath)
let raw = ''
try {
raw = await fs.readFile(filePath, 'utf8')
} catch (err) {
console.error(`Error reading SSH config file: ${filePath}`, err)
return SSHConfig.parse('')
}
const parsed = SSHConfig.parse(raw)
const merged = SSHConfig.parse('')
for (const entry of parsed) {
if (entry.type === LineType.DIRECTIVE && entry.param.toLowerCase() === 'include') {
const directive = entry as Directive
if (typeof directive.value !== 'string') {
continue
}
// ssh_config(5) says "Files without absolute paths are assumed to be in ~/.ssh if included in a user configuration file or /etc/ssh if included from the system configuration file."
let incPath = ''
if (path.isAbsolute(directive.value)) {
incPath = directive.value
} else if (directive.value.startsWith('~')) {
incPath = path.join(process.env.HOME ?? '~', directive.value.slice(1))
} else {
incPath = path.join(process.env.HOME ?? '~', '.ssh', directive.value)
}
const matches = glob.sync(incPath)
for (const match of matches) {
const stat = await fs.stat(match)
if (stat.isDirectory()) {
continue
}
const matchedConfig = await parseSSHConfigFile(match, visited)
merged.push(...matchedConfig)
}
} else {
merged.push(entry)
}
}
return merged
} }
// Function to take an ssh-config entry and convert it into an SSHProfile // Function to take an ssh-config entry and convert it into an SSHProfile
function convertHostToSSHProfile (host: string, settings: Record<string, string | string[] | object[] >): PartialProfile<SSHProfile> { function convertHostToSSHProfile (host: string, settings: Record<string, string | string[] | object[] >): PartialProfile<SSHProfile> {
@ -293,8 +339,7 @@ export class OpenSSHImporter extends SSHProfileImporter {
const configPath = path.join(process.env.HOME ?? '~', '.ssh', 'config') const configPath = path.join(process.env.HOME ?? '~', '.ssh', 'config')
try { try {
const sshConfigContent = await readSSHConfigFile(configPath) const config: SSHConfig = await parseSSHConfigFile(configPath)
const config: SSHConfig = SSHConfig.parse(sshConfigContent)
return convertToSSHProfiles(config) return convertToSSHProfiles(config)
} catch (e) { } catch (e) {
if (e.code === 'ENOENT') { if (e.code === 'ENOENT') {