Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update sshImporters.ts to parse Include directive #10105

Merged
merged 2 commits into from
Jan 1, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 53 additions & 8 deletions tabby-electron/src/sshImporters.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as fs from 'fs/promises'
import * as fsSync from 'fs'
import * as path from 'path'
import * as glob from 'glob'
import slugify from 'slugify'
import * as yaml from 'js-yaml'
import { Injectable } from '@angular/core'
Expand All @@ -14,7 +15,7 @@ import {
} from 'tabby-ssh'

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 SSHProfilePropertyNames {
Expand Down Expand Up @@ -90,15 +91,60 @@ function convertSSHConfigValuesToString (arg: string | string[] | object[]): str
.join(' ')
}

// Function to read in the SSH config file and return it as a string
async function readSSHConfigFile (filePath: string): Promise<string> {
// Function to read in the SSH config file recursively and parse any Include directives
async function parseSSHConfigFile (
filePath: string,
visited = new Set<string>(),
): Promise<SSHConfig> {
// If we've already processed this file, return an empty config to avoid infinite recursion
if (visited.has(filePath)) {
return SSHConfig.parse('')
}
visited.add(filePath)

let raw = ''
try {
return await fs.readFile(filePath, 'utf8')
raw = await fs.readFile(filePath, 'utf8')
} catch (err) {
console.error('Error reading SSH config file:', err)
return ''
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 convertHostToSSHProfile (host: string, settings: Record<string, string | string[] | object[] >): PartialProfile<SSHProfile> {

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

try {
const sshConfigContent = await readSSHConfigFile(configPath)
const config: SSHConfig = SSHConfig.parse(sshConfigContent)
const config: SSHConfig = await parseSSHConfigFile(configPath)
return convertToSSHProfiles(config)
} catch (e) {
if (e.code === 'ENOENT') {
Expand Down
Loading