mirror of
https://github.com/eugeny/tabby
synced 2025-11-18 15:16:13 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cf6ef0036 | ||
|
|
7a7d3d2b77 | ||
|
|
4f14e92e6a | ||
|
|
52463555ab | ||
|
|
bbcb026433 | ||
|
|
34c0c780f4 |
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@@ -320,8 +320,10 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Code signing with Software Trust Manager
|
||||
uses: digicert/ssm-code-signing@v1.0.0
|
||||
uses: digicert/ssm-code-signing@v1.1.1
|
||||
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags'))
|
||||
env:
|
||||
FORCE_DOWNLOAD_TOOLS: 'true'
|
||||
|
||||
- name: Installing Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
@@ -372,6 +374,7 @@ jobs:
|
||||
# not used but necessary for electron-builder to run
|
||||
$env:WIN_CSC_LINK=$env:SM_CLIENT_CERT_FILE
|
||||
$env:WIN_CSC_KEY_PASSWORD=$env:SM_CLIENT_CERT_PASSWORD
|
||||
|
||||
node scripts/build-windows.mjs
|
||||
env:
|
||||
ARCH: ${{matrix.arch}}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "^2",
|
||||
"node-pty": "^1.1.0-beta34",
|
||||
"node-pty": "^1.1.0-beta39",
|
||||
"any-promise": "^1.3.0",
|
||||
"electron-config": "2.0.0",
|
||||
"electron-debug": "^3.2.0",
|
||||
|
||||
@@ -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-beta34:
|
||||
node-pty@^1.1.0-beta39:
|
||||
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==
|
||||
|
||||
@@ -36,15 +36,21 @@ builder({
|
||||
console.log('Signing', configuration)
|
||||
if (configuration.path) {
|
||||
try {
|
||||
const out = execSync(
|
||||
`smctl sign --keypair-alias=${keypair} --input "${String(configuration.path)}"`
|
||||
)
|
||||
const cmd = `smctl sign --keypair-alias=${keypair} --input "${String(configuration.path)}"`
|
||||
console.log(cmd)
|
||||
const out = execSync(cmd)
|
||||
if (out.toString().includes('FAILED')) {
|
||||
throw new Error(out.toString())
|
||||
}
|
||||
console.log(out.toString())
|
||||
} catch (e) {
|
||||
console.error(`Failed to sign ${configuration.path}`)
|
||||
if (e.stdout) {
|
||||
console.error('stdout:', e.stdout.toString())
|
||||
}
|
||||
if (e.stderr) {
|
||||
console.error('stderr:', e.stderr.toString())
|
||||
}
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ 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'
|
||||
@@ -145,15 +144,24 @@ 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
|
||||
function convertHostToSSHProfile (host: string, settings: Record<string, string | string[] | object[] >): PartialProfile<SSHProfile> {
|
||||
async function convertHostToSSHProfile (host: string, settings: Record<string, string | string[] | object[] >): Promise<PartialProfile<SSHProfile>> {
|
||||
|
||||
// inline function to generate an id for this profile
|
||||
const deriveID = (name: string) => 'openssh-config:' + slugify(name)
|
||||
const deriveID = async (name: string) => 'openssh-config:' + await hashSSHProfileName(name)
|
||||
|
||||
// Start point of the profile, with an ID, name, type and group
|
||||
const thisProfile: PartialProfile<SSHProfile> = {
|
||||
id: deriveID(host),
|
||||
id: await deriveID(host),
|
||||
name: `${host} (.ssh/config)`,
|
||||
type: 'ssh',
|
||||
group: 'Imported from .ssh/config',
|
||||
@@ -194,7 +202,7 @@ function convertHostToSSHProfile (host: string, settings: Record<string, string
|
||||
const basicString = settings[key]
|
||||
if (typeof basicString === 'string') {
|
||||
if (targetName === SSHProfilePropertyNames.JumpHost) {
|
||||
options[targetName] = deriveID(basicString)
|
||||
options[targetName] = await deriveID(basicString)
|
||||
} else {
|
||||
options[targetName] = basicString
|
||||
}
|
||||
@@ -205,9 +213,7 @@ function convertHostToSSHProfile (host: string, settings: Record<string, string
|
||||
|
||||
// The following have single integer values
|
||||
case SSHProfilePropertyNames.Port:
|
||||
case SSHProfilePropertyNames.KeepaliveInterval:
|
||||
case SSHProfilePropertyNames.KeepaliveCountMax:
|
||||
case SSHProfilePropertyNames.ReadyTimeout:
|
||||
const numberString = settings[key]
|
||||
if (typeof numberString === 'string') {
|
||||
options[targetName] = parseInt(numberString, 10)
|
||||
@@ -216,6 +222,22 @@ function convertHostToSSHProfile (host: string, settings: Record<string, string
|
||||
}
|
||||
break
|
||||
|
||||
// KeepaliveInterval and ReadyTimeout are in seconds in SSH config but milliseconds in Tabby
|
||||
case SSHProfilePropertyNames.KeepaliveInterval:
|
||||
case SSHProfilePropertyNames.ReadyTimeout:
|
||||
const secondsString = settings[key]
|
||||
if (typeof secondsString === 'string') {
|
||||
const parsedSeconds = parseInt(secondsString, 10)
|
||||
if (!isNaN(parsedSeconds) && parsedSeconds >= 0) {
|
||||
options[targetName] = parsedSeconds * 1000
|
||||
} else {
|
||||
console.log(`Invalid value for ${key}: "${secondsString}"`)
|
||||
}
|
||||
} else {
|
||||
console.log('Unexpected value in settings for ' + key)
|
||||
}
|
||||
break
|
||||
|
||||
// The following have single yes/no values
|
||||
case SSHProfilePropertyNames.X11:
|
||||
case SSHProfilePropertyNames.AgentForward:
|
||||
@@ -281,7 +303,7 @@ function convertHostToSSHProfile (host: string, settings: Record<string, string
|
||||
return thisProfile
|
||||
}
|
||||
|
||||
function convertToSSHProfiles (config: SSHConfig): PartialProfile<SSHProfile>[] {
|
||||
async function convertToSSHProfiles (config: SSHConfig): Promise<PartialProfile<SSHProfile>[]> {
|
||||
const myMap = new Map<string, PartialProfile<SSHProfile>>()
|
||||
|
||||
function noWildCardsInName (name: string) {
|
||||
@@ -319,7 +341,7 @@ function convertToSSHProfiles (config: SSHConfig): PartialProfile<SSHProfile>[]
|
||||
// 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] = convertHostToSSHProfile(host, configuration)
|
||||
myMap[host] = await convertHostToSSHProfile(host, configuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -340,7 +362,7 @@ export class OpenSSHImporter extends SSHProfileImporter {
|
||||
|
||||
try {
|
||||
const config: SSHConfig = await parseSSHConfigFile(configPath)
|
||||
return convertToSSHProfiles(config)
|
||||
return await convertToSSHProfiles(config)
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
return []
|
||||
@@ -362,7 +384,7 @@ export class StaticFileImporter extends SSHProfileImporter {
|
||||
}
|
||||
|
||||
async getProfiles (): Promise<PartialProfile<SSHProfile>[]> {
|
||||
const deriveID = name => 'file-config:' + slugify(name)
|
||||
const deriveID = async name => 'file-config:' + await hashSSHProfileName(name)
|
||||
|
||||
if (!fsSync.existsSync(this.configPath)) {
|
||||
return []
|
||||
@@ -373,11 +395,11 @@ export class StaticFileImporter extends SSHProfileImporter {
|
||||
return []
|
||||
}
|
||||
|
||||
return (yaml.load(content) as PartialProfile<SSHProfile>[]).map(item => ({
|
||||
return Promise.all((yaml.load(content) as PartialProfile<SSHProfile>[]).map(async item => ({
|
||||
...item,
|
||||
id: deriveID(item.name),
|
||||
id: await deriveID(item.name),
|
||||
type: 'ssh',
|
||||
}))
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,8 +45,6 @@ export const defaultAlgorithms = {
|
||||
'hmac-sha1',
|
||||
],
|
||||
[SSHAlgorithmType.COMPRESSION]: [
|
||||
'zlib@openssh.com',
|
||||
'zlib',
|
||||
'none',
|
||||
],
|
||||
}
|
||||
|
||||
@@ -27,18 +27,89 @@ export class SSHService {
|
||||
return this.detectedWinSCPPath ?? this.config.store.ssh.winSCPPath
|
||||
}
|
||||
|
||||
async getWinSCPURI (profile: SSHProfile, cwd?: string, username?: string): Promise<string> {
|
||||
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 }> {
|
||||
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
|
||||
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 }
|
||||
}
|
||||
|
||||
async launchWinSCP (session: SSHSession): Promise<void> {
|
||||
@@ -46,38 +117,26 @@ export class SSHService {
|
||||
if (!path) {
|
||||
return
|
||||
}
|
||||
const args = [await this.getWinSCPURI(session.profile, undefined, session.authUsername ?? undefined)]
|
||||
const winscpParms = await this.getWinSCPURI(session.profile, undefined, session.authUsername ?? undefined)
|
||||
const args = [winscpParms.uri]
|
||||
|
||||
let tmpFile: tmp.FileResult|null = null
|
||||
try {
|
||||
if (session.activePrivateKey && session.profile.options.privateKeys && session.profile.options.privateKeys.length > 0) {
|
||||
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
|
||||
const profile = session.profile
|
||||
const privateKeyPairs = await this.convertPrivateKeyFileToPuTTYFormat(profile)
|
||||
tmpFile = privateKeyPairs.privateKeyFile
|
||||
if (tmpFile) {
|
||||
args.push(`/privatekey=${tmpFile.path}`)
|
||||
}
|
||||
args.push(`/privatekey=${tmpFile.path}`)
|
||||
if (passphrase != null) {
|
||||
args.push(`/passphrase=${passphrase}`)
|
||||
if (privateKeyPairs.passphrase != null) {
|
||||
args.push(`/passphrase=${privateKeyPairs.passphrase}`)
|
||||
}
|
||||
}
|
||||
await this.platform.exec(path, args)
|
||||
} finally {
|
||||
tmpFile?.cleanup()
|
||||
winscpParms.privateKeyFile?.cleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user