Skip to content

Commit 39e3ba3

Browse files
authored
Update sshImporters.ts to parse Include directive (#10105)
1 parent d0dd09a commit 39e3ba3

File tree

1 file changed

+53
-8
lines changed

1 file changed

+53
-8
lines changed

tabby-electron/src/sshImporters.ts

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as fs from 'fs/promises'
22
import * as fsSync from 'fs'
33
import * as path from 'path'
4+
import * as glob from 'glob'
45
import slugify from 'slugify'
56
import * as yaml from 'js-yaml'
67
import { Injectable } from '@angular/core'
@@ -14,7 +15,7 @@ import {
1415
} from 'tabby-ssh'
1516

1617
import { ElectronService } from './services/electron.service'
17-
import SSHConfig, { LineType } from 'ssh-config'
18+
import SSHConfig, { Directive, LineType } from 'ssh-config'
1819

1920
// Enum to delineate the properties in SSHProfile options
2021
enum SSHProfilePropertyNames {
@@ -90,15 +91,60 @@ function convertSSHConfigValuesToString (arg: string | string[] | object[]): str
9091
.join(' ')
9192
}
9293

93-
// Function to read in the SSH config file and return it as a string
94-
async function readSSHConfigFile (filePath: string): Promise<string> {
94+
// Function to read in the SSH config file recursively and parse any Include directives
95+
async function parseSSHConfigFile (
96+
filePath: string,
97+
visited = new Set<string>(),
98+
): Promise<SSHConfig> {
99+
// If we've already processed this file, return an empty config to avoid infinite recursion
100+
if (visited.has(filePath)) {
101+
return SSHConfig.parse('')
102+
}
103+
visited.add(filePath)
104+
105+
let raw = ''
95106
try {
96-
return await fs.readFile(filePath, 'utf8')
107+
raw = await fs.readFile(filePath, 'utf8')
97108
} catch (err) {
98-
console.error('Error reading SSH config file:', err)
99-
return ''
109+
console.error(`Error reading SSH config file: ${filePath}`, err)
110+
return SSHConfig.parse('')
111+
}
112+
113+
const parsed = SSHConfig.parse(raw)
114+
const merged = SSHConfig.parse('')
115+
for (const entry of parsed) {
116+
if (entry.type === LineType.DIRECTIVE && entry.param.toLowerCase() === 'include') {
117+
const directive = entry as Directive
118+
if (typeof directive.value !== 'string') {
119+
continue
120+
}
121+
122+
// 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."
123+
let incPath = ''
124+
if (path.isAbsolute(directive.value)) {
125+
incPath = directive.value
126+
} else if (directive.value.startsWith('~')) {
127+
incPath = path.join(process.env.HOME ?? '~', directive.value.slice(1))
128+
} else {
129+
incPath = path.join(process.env.HOME ?? '~', '.ssh', directive.value)
130+
}
131+
132+
const matches = glob.sync(incPath)
133+
for (const match of matches) {
134+
const stat = await fs.stat(match)
135+
if (stat.isDirectory()) {
136+
continue
137+
}
138+
const matchedConfig = await parseSSHConfigFile(match, visited)
139+
merged.push(...matchedConfig)
140+
}
141+
} else {
142+
merged.push(entry)
143+
}
100144
}
145+
return merged
101146
}
147+
102148
// Function to take an ssh-config entry and convert it into an SSHProfile
103149
function convertHostToSSHProfile (host: string, settings: Record<string, string | string[] | object[] >): PartialProfile<SSHProfile> {
104150

@@ -293,8 +339,7 @@ export class OpenSSHImporter extends SSHProfileImporter {
293339
const configPath = path.join(process.env.HOME ?? '~', '.ssh', 'config')
294340

295341
try {
296-
const sshConfigContent = await readSSHConfigFile(configPath)
297-
const config: SSHConfig = SSHConfig.parse(sshConfigContent)
342+
const config: SSHConfig = await parseSSHConfigFile(configPath)
298343
return convertToSSHProfiles(config)
299344
} catch (e) {
300345
if (e.code === 'ENOENT') {

0 commit comments

Comments
 (0)