Compare commits

..

No commits in common. "2cf6ef0036eb32f41ca43e0fa171e02b37346aee" and "bbcb026433177fff61c90018b026f7a144867d83" have entirely different histories.

5 changed files with 40 additions and 105 deletions

View File

@ -16,7 +16,7 @@
},
"dependencies": {
"@electron/remote": "^2",
"node-pty": "^1.1.0-beta39",
"node-pty": "^1.1.0-beta34",
"any-promise": "^1.3.0",
"electron-config": "2.0.0",
"electron-debug": "^3.2.0",

View File

@ -2813,7 +2813,7 @@ node-gyp@^10.0.0, node-gyp@^5.0.2, node-gyp@^5.1.0:
tar "^6.1.2"
which "^4.0.0"
node-pty@^1.1.0-beta39:
node-pty@^1.1.0-beta34:
version "1.1.0-beta9"
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.1.0-beta9.tgz#ed643cb3b398d031b4e31c216e8f3b0042435f1d"
integrity sha512-/Ue38pvXJdgRZ3+me1FgfglLd301GhJN0NStiotdt61tm43N5htUyR/IXOUzOKuNaFmCwIhy6nwb77Ky41LMbw==

View File

@ -2,6 +2,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'
import { PartialProfile } from 'tabby-core'
@ -144,24 +145,15 @@ async function parseSSHConfigFile (
return merged
}
// Function to convert an SSH Profile name into a sha256 hash-based ID
async function hashSSHProfileName (name: string) {
const textEncoder = new TextEncoder()
const encoded = textEncoder.encode(name)
const hash = await crypto.subtle.digest('SHA-256', encoded)
const hashArray = Array.from(new Uint8Array(hash))
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
}
// Function to take an ssh-config entry and convert it into an SSHProfile
async function convertHostToSSHProfile (host: string, settings: Record<string, string | string[] | object[] >): Promise<PartialProfile<SSHProfile>> {
function convertHostToSSHProfile (host: string, settings: Record<string, string | string[] | object[] >): PartialProfile<SSHProfile> {
// inline function to generate an id for this profile
const deriveID = async (name: string) => 'openssh-config:' + await hashSSHProfileName(name)
const deriveID = (name: string) => 'openssh-config:' + slugify(name)
// Start point of the profile, with an ID, name, type and group
const thisProfile: PartialProfile<SSHProfile> = {
id: await deriveID(host),
id: deriveID(host),
name: `${host} (.ssh/config)`,
type: 'ssh',
group: 'Imported from .ssh/config',
@ -202,7 +194,7 @@ async function convertHostToSSHProfile (host: string, settings: Record<string, s
const basicString = settings[key]
if (typeof basicString === 'string') {
if (targetName === SSHProfilePropertyNames.JumpHost) {
options[targetName] = await deriveID(basicString)
options[targetName] = deriveID(basicString)
} else {
options[targetName] = basicString
}
@ -303,7 +295,7 @@ async function convertHostToSSHProfile (host: string, settings: Record<string, s
return thisProfile
}
async function convertToSSHProfiles (config: SSHConfig): Promise<PartialProfile<SSHProfile>[]> {
function convertToSSHProfiles (config: SSHConfig): PartialProfile<SSHProfile>[] {
const myMap = new Map<string, PartialProfile<SSHProfile>>()
function noWildCardsInName (name: string) {
@ -341,7 +333,7 @@ async function convertToSSHProfiles (config: SSHConfig): Promise<PartialProfile<
// NOTE: SSHConfig.compute() lies about the return types
const configuration: Record<string, string | string[] | object[]> = config.compute(host)
if (Object.keys(configuration).map(key => key.toLowerCase()).includes('hostname')) {
myMap[host] = await convertHostToSSHProfile(host, configuration)
myMap[host] = convertHostToSSHProfile(host, configuration)
}
}
}
@ -362,7 +354,7 @@ export class OpenSSHImporter extends SSHProfileImporter {
try {
const config: SSHConfig = await parseSSHConfigFile(configPath)
return await convertToSSHProfiles(config)
return convertToSSHProfiles(config)
} catch (e) {
if (e.code === 'ENOENT') {
return []
@ -384,7 +376,7 @@ export class StaticFileImporter extends SSHProfileImporter {
}
async getProfiles (): Promise<PartialProfile<SSHProfile>[]> {
const deriveID = async name => 'file-config:' + await hashSSHProfileName(name)
const deriveID = name => 'file-config:' + slugify(name)
if (!fsSync.existsSync(this.configPath)) {
return []
@ -395,11 +387,11 @@ export class StaticFileImporter extends SSHProfileImporter {
return []
}
return Promise.all((yaml.load(content) as PartialProfile<SSHProfile>[]).map(async item => ({
return (yaml.load(content) as PartialProfile<SSHProfile>[]).map(item => ({
...item,
id: await deriveID(item.name),
id: deriveID(item.name),
type: 'ssh',
})))
}))
}
}

View File

@ -45,6 +45,8 @@ export const defaultAlgorithms = {
'hmac-sha1',
],
[SSHAlgorithmType.COMPRESSION]: [
'zlib@openssh.com',
'zlib',
'none',
],
}

View File

@ -27,89 +27,18 @@ export class SSHService {
return this.detectedWinSCPPath ?? this.config.store.ssh.winSCPPath
}
async generateWinSCPXTunnelURI (jumpHostProfile: SSHProfile|null): Promise<{ uri: string|null, privateKeyFile?: tmp.FileResult|null }> {
let uri = ''
let tmpFile: tmp.FileResult|null = null
if (jumpHostProfile) {
uri += ';x-tunnel=1'
const jumpHostname = jumpHostProfile.options.host
uri += `;x-tunnelhostname=${jumpHostname}`
const jumpPort = jumpHostProfile.options.port ?? 22
uri += `;x-tunnelportnumber=${jumpPort}`
const jumpUsername = jumpHostProfile.options.user
uri += `;x-tunnelusername=${jumpUsername}`
if (jumpHostProfile.options.auth === 'password') {
const jumpPassword = await this.passwordStorage.loadPassword(jumpHostProfile, jumpUsername)
if (jumpPassword) {
uri += `;x-tunnelpasswordplain=${encodeURIComponent(jumpPassword)}`
}
}
if (jumpHostProfile.options.auth === 'publicKey' && jumpHostProfile.options.privateKeys && jumpHostProfile.options.privateKeys.length > 0) {
const privateKeyPairs = await this.convertPrivateKeyFileToPuTTYFormat(jumpHostProfile)
tmpFile = privateKeyPairs.privateKeyFile
if (tmpFile) {
uri += `;x-tunnelpublickeyfile=${encodeURIComponent(tmpFile.path)}`
}
if (privateKeyPairs.passphrase != null) {
uri += `;x-tunnelpassphraseplain=${encodeURIComponent(privateKeyPairs.passphrase)}`
}
}
}
return { uri: uri, privateKeyFile: tmpFile?? null }
}
async getWinSCPURI (profile: SSHProfile, cwd?: string, username?: string): Promise<{ uri: string, privateKeyFile?: tmp.FileResult|null }> {
async getWinSCPURI (profile: SSHProfile, cwd?: string, username?: string): Promise<string> {
let uri = `scp://${username ?? profile.options.user}`
const password = await this.passwordStorage.loadPassword(profile, username)
if (password) {
uri += ':' + encodeURIComponent(password)
}
let tmpFile: tmp.FileResult|null = null
if (profile.options.jumpHost) {
const jumpHostProfile = this.config.store.profiles.find(x => x.id === profile.options.jumpHost) ?? null
const xTunnelParams = await this.generateWinSCPXTunnelURI(jumpHostProfile)
uri += xTunnelParams.uri ?? ''
tmpFile = xTunnelParams.privateKeyFile ?? null
}
if (profile.options.host.includes(':')) {
uri += `@[${profile.options.host}]:${profile.options.port}${cwd ?? '/'}`
}else {
uri += `@${profile.options.host}:${profile.options.port}${cwd ?? '/'}`
}
return { uri, privateKeyFile: tmpFile?? null }
}
async convertPrivateKeyFileToPuTTYFormat (profile: SSHProfile): Promise<{ passphrase: string|null, privateKeyFile: tmp.FileResult|null }> {
if (!profile.options.privateKeys || profile.options.privateKeys.length === 0) {
throw new Error('No private keys in profile')
}
const path = this.getWinSCPPath()
if (!path) {
throw new Error('WinSCP not found')
}
let tmpPrivateKeyFile: tmp.FileResult|null = null
let passphrase: string|null = null
const tmpFile: tmp.FileResult = await tmp.file()
for (const pk of profile.options.privateKeys) {
let privateKeyContent: string|null = null
const buffer = await this.fileProviders.retrieveFile(pk)
privateKeyContent = buffer.toString()
await fs.writeFile(tmpFile.path, privateKeyContent)
const keyHash = crypto.createHash('sha512').update(privateKeyContent).digest('hex')
// need to pass an default passphrase, otherwise it might get stuck at the passphrase input
const curPassphrase = await this.passwordStorage.loadPrivateKeyPassword(keyHash) ?? 'tabby'
const winSCPcom = path.slice(0, -3) + 'com'
try {
await this.platform.exec(winSCPcom, ['/keygen', tmpFile.path, '-o', tmpFile.path, '--old-passphrase', curPassphrase])
} catch (error) {
console.warn('Could not convert private key ', error)
continue
}
tmpPrivateKeyFile = tmpFile
passphrase = curPassphrase
break
}
return { passphrase, privateKeyFile: tmpPrivateKeyFile }
return uri
}
async launchWinSCP (session: SSHSession): Promise<void> {
@ -117,26 +46,38 @@ export class SSHService {
if (!path) {
return
}
const winscpParms = await this.getWinSCPURI(session.profile, undefined, session.authUsername ?? undefined)
const args = [winscpParms.uri]
const args = [await this.getWinSCPURI(session.profile, undefined, session.authUsername ?? undefined)]
let tmpFile: tmp.FileResult|null = null
try {
if (session.activePrivateKey && session.profile.options.privateKeys && session.profile.options.privateKeys.length > 0) {
const profile = session.profile
const privateKeyPairs = await this.convertPrivateKeyFileToPuTTYFormat(profile)
tmpFile = privateKeyPairs.privateKeyFile
if (tmpFile) {
args.push(`/privatekey=${tmpFile.path}`)
tmpFile = await tmp.file()
let passphrase: string|null = null
for (const pk of session.profile.options.privateKeys) {
let privateKeyContent: string|null = null
const buffer = await this.fileProviders.retrieveFile(pk)
privateKeyContent = buffer.toString()
await fs.writeFile(tmpFile.path, privateKeyContent)
const keyHash = crypto.createHash('sha512').update(privateKeyContent).digest('hex')
// need to pass an default passphrase, otherwise it might get stuck at the passphrase input
passphrase = await this.passwordStorage.loadPrivateKeyPassword(keyHash) ?? 'tabby'
const winSCPcom = path.slice(0, -3) + 'com'
try {
await this.platform.exec(winSCPcom, ['/keygen', tmpFile.path, '-o', tmpFile.path, '--old-passphrase', passphrase])
} catch (error) {
console.warn('Could not convert private key ', error)
continue
}
break
}
if (privateKeyPairs.passphrase != null) {
args.push(`/passphrase=${privateKeyPairs.passphrase}`)
args.push(`/privatekey=${tmpFile.path}`)
if (passphrase != null) {
args.push(`/passphrase=${passphrase}`)
}
}
await this.platform.exec(path, args)
} finally {
tmpFile?.cleanup()
winscpParms.privateKeyFile?.cleanup()
}
}
}