|
@@ -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
|
|
|
|
|
-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 {
|
|
try {
|
|
|
- return await fs.readFile(filePath, 'utf8')
|
|
|
|
|
|
|
+ raw = await fs.readFile(filePath, 'utf8')
|
|
|
} catch (err) {
|
|
} 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 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 = SSHConfig.parse(sshConfigContent)
|
|
|
|
|
|
|
+ const config: SSHConfig = await parseSSHConfigFile(configPath)
|
|
|
return convertToSSHProfiles(config)
|
|
return convertToSSHProfiles(config)
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
if (e.code === 'ENOENT') {
|
|
if (e.code === 'ENOENT') {
|