Compare commits

..

16 Commits

Author SHA1 Message Date
allcontributors[bot]
5a33247027 update README.de-DE.md [skip ci] 2025-02-24 23:13:10 +00:00
allcontributors[bot]
0a22bb00d7 update README.it-IT.md [skip ci] 2025-02-24 23:13:09 +00:00
allcontributors[bot]
6ee4ba6ebc update README.ko-KR.md [skip ci] 2025-02-24 23:13:08 +00:00
allcontributors[bot]
ddaac158f7 update README.ru-RU.md [skip ci] 2025-02-24 23:13:07 +00:00
allcontributors[bot]
6fa4c0a09b update README.zh-CN.md [skip ci] 2025-02-24 23:13:06 +00:00
allcontributors[bot]
eeb366ce67 update README.md [skip ci] 2025-02-24 23:13:05 +00:00
Timo Schnaible
ab87099b8b add debian bullseye, bookworm and trixie deb package upload (#10314) 2025-02-25 00:12:45 +01:00
Eugene
47b4b54557 bump russh for agent RSA auth fixes 2025-02-21 10:48:51 +01:00
OpaqueGlass
15f4182e0e Fix: Unable to launch WinSCP for SSH sessions using private key (#10308) 2025-02-19 10:27:59 +01:00
aminelch
4be1e12559 Add Tokyonight color scheme (#10283) 2025-02-03 10:06:20 +01:00
Eugene
5d2d179677 prefer saved password to keyboard interactive auth 2025-01-29 10:37:28 +01:00
Eugene
4197cefdfd bump russh for events fix 2025-01-28 10:57:56 +01:00
Eugene
7c1421ffcf bump russh for async trait 2025-01-28 09:00:50 +01:00
Eugene
380c306d89 added warning when server disconnects during auth 2025-01-27 14:53:44 +01:00
Eugene
cf0da75224 bump russh for best hash selection in agent auth 2025-01-27 14:53:32 +01:00
Eugene
d1c1b48502 bump russh for rsa hash autoselection 2025-01-25 12:56:54 +01:00
14 changed files with 226 additions and 43 deletions

View File

@@ -258,7 +258,7 @@ jobs:
repo: 'eugeny/tabby'
dir: 'dist'
rpmvers: 'el/9 el/8 ol/6 ol/7'
debvers: 'ubuntu/bionic ubuntu/focal ubuntu/hirsute ubuntu/impish ubuntu/jammy ubuntu/kinetic ubuntu/noble ubuntu/oracular debian/jessie debian/stretch debian/buster'
debvers: 'ubuntu/bionic ubuntu/focal ubuntu/hirsute ubuntu/impish ubuntu/jammy ubuntu/kinetic ubuntu/noble ubuntu/oracular debian/jessie debian/stretch debian/buster debian/bullseye debian/bookworm debian/trixie'
- uses: actions/upload-artifact@master
name: Upload AppImage (${{matrix.arch}})

View File

@@ -347,6 +347,7 @@ Dank geht an diese wunderbaren Menschen ([emoji key](https://allcontributors.org
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://madebytimo.de"><img src="https://avatars.githubusercontent.com/u/44726248?v=4?s=100" width="100px;" alt="Timo Schnaible"/><br /><sub><b>Timo Schnaible</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=madebyTimo" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@@ -342,6 +342,7 @@ Grazie a queste persone meravigliose ([emoji key](https://allcontributors.org/do
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://madebytimo.de"><img src="https://avatars.githubusercontent.com/u/44726248?v=4?s=100" width="100px;" alt="Timo Schnaible"/><br /><sub><b>Timo Schnaible</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=madebyTimo" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@@ -341,6 +341,7 @@ Pull requests and plugins are welcome!
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://madebytimo.de"><img src="https://avatars.githubusercontent.com/u/44726248?v=4?s=100" width="100px;" alt="Timo Schnaible"/><br /><sub><b>Timo Schnaible</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=madebyTimo" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@@ -365,6 +365,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://madebytimo.de"><img src="https://avatars.githubusercontent.com/u/44726248?v=4?s=100" width="100px;" alt="Timo Schnaible"/><br /><sub><b>Timo Schnaible</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=madebyTimo" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@@ -342,6 +342,7 @@ Pull-запросы и плагины приветствуются!
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://madebytimo.de"><img src="https://avatars.githubusercontent.com/u/44726248?v=4?s=100" width="100px;" alt="Timo Schnaible"/><br /><sub><b>Timo Schnaible</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=madebyTimo" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@@ -341,6 +341,7 @@
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://madebytimo.de"><img src="https://avatars.githubusercontent.com/u/44726248?v=4?s=100" width="100px;" alt="Timo Schnaible"/><br /><sub><b>Timo Schnaible</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=madebyTimo" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@@ -30,7 +30,7 @@
"native-process-working-directory": "^1.0.2",
"npm": "6",
"rxjs": "^7.5.7",
"russh": "0.1.15",
"russh": "0.1.21",
"source-map-support": "^0.5.20",
"v8-compile-cache": "^2.3.0",
"yargs": "^17.7.2"

View File

@@ -3628,10 +3628,10 @@ run-queue@^1.0.0, run-queue@^1.0.3:
dependencies:
aproba "^1.1.1"
russh@0.1.15:
version "0.1.15"
resolved "https://registry.yarnpkg.com/russh/-/russh-0.1.15.tgz#121c0be876c6b70c43910a56fadefffe78588a35"
integrity sha512-md/72roZn8nmgq+U8rVrfqHS82aPancQM5VqrYh7wEMcnK/ll8vNJJcKH/YJv5/8n8Ovxrl8SMwDE5DlfXSL6Q==
russh@0.1.21:
version "0.1.21"
resolved "https://registry.yarnpkg.com/russh/-/russh-0.1.21.tgz#857b20c298a50a6657d1f1653ce9d149c68d6b5b"
integrity sha512-2zjOHTTDqaa3/pHUU+VCkoEqOXLpIpk9WATUaudtLGqy3n8Duz3WlhvyJzEmd+S+9eVGnQvyktpjtZziXLVHRA==
dependencies:
"@napi-rs/cli" "^2.18.3"

View File

@@ -0,0 +1,44 @@
!
! Generated with :
! XRDB2Xreources.py
!
*.foreground: #c0caf5
*.background: #1a1b26
*.cursorColor: #c0caf5
!
! Black
*.color0: #15161e
*.color8: #414868
!
! Red
*.color1: #f7768e
*.color9: #f7768e
!
! Green
*.color2: #9ece6a
*.color10: #9ece6a
!
! Yellow
*.color3: #e0af68
*.color11: #e0af68
!
! Blue
*.color4: #7aa2f7
*.color12: #7aa2f7
!
! Magenta
*.color5: #bb9af7
*.color13: #bb9af7
!
! Cyan
*.color6: #7dcfff
*.color14: #7dcfff
!
! White
*.color7: #a9b1d6
*.color15: #c0caf5
!
! Bold, Italic, Underline
*.colorBD: #eeeeee
!*.colorIT:
!*.colorUL:

View File

@@ -0,0 +1,44 @@
!
! Generated with :
! XRDB2Xreources.py
!
*.foreground: #3760bf
*.background: #e1e2e7
*.cursorColor: #3760bf
!
! Black
*.color0: #e9e9ed
*.color8: #a1a6c5
!
! Red
*.color1: #f52a65
*.color9: #f52a65
!
! Green
*.color2: #587539
*.color10: #587539
!
! Yellow
*.color3: #8c6c3e
*.color11: #8c6c3e
!
! Blue
*.color4: #2e7de9
*.color12: #2e7de9
!
! Magenta
*.color5: #9854f1
*.color13: #9854f1
!
! Cyan
*.color6: #007197
*.color14: #007197
!
! White
*.color7: #6172b0
*.color15: #3760bf
!
! Bold, Italic, Underline
*.colorBD: #eeeeee
!*.colorIT:
!*.colorUL:

View File

@@ -0,0 +1,44 @@
!
! Generated with :
! XRDB2Xreources.py
!
*.foreground: #c0caf5
*.background: #24283b
*.cursorColor: #c0caf5
!
! Black
*.color0: #1d202f
*.color8: #414868
!
! Red
*.color1: #f7768e
*.color9: #f7768e
!
! Green
*.color2: #9ece6a
*.color10: #9ece6a
!
! Yellow
*.color3: #e0af68
*.color11: #e0af68
!
! Blue
*.color4: #7aa2f7
*.color12: #7aa2f7
!
! Magenta
*.color5: #bb9af7
*.color13: #bb9af7
!
! Cyan
*.color6: #7dcfff
*.color14: #7dcfff
!
! White
*.color7: #a9b1d6
*.color15: #c0caf5
!
! Bold, Italic, Underline
*.colorBD: #eeeeee
!*.colorIT:
!*.colorUL:

View File

@@ -1,7 +1,8 @@
// import * as fs from 'fs/promises'
import * as fs from 'fs/promises'
import * as crypto from 'crypto'
import * as tmp from 'tmp-promise'
import { Injectable } from '@angular/core'
import { ConfigService, HostAppService, Platform, PlatformService } from 'tabby-core'
import { ConfigService, FileProvidersService, HostAppService, Platform, PlatformService } from 'tabby-core'
import { SSHSession } from '../session/ssh'
import { SSHProfile } from '../api'
import { PasswordStorageService } from './passwordStorage.service'
@@ -15,6 +16,7 @@ export class SSHService {
private config: ConfigService,
hostApp: HostAppService,
private platform: PlatformService,
private fileProviders: FileProvidersService,
) {
if (hostApp.platform === Platform.Windows) {
this.detectedWinSCPPath = platform.getWinSCPPath()
@@ -47,14 +49,35 @@ export class SSHService {
const args = [await this.getWinSCPURI(session.profile, undefined, session.authUsername ?? undefined)]
let tmpFile: tmp.FileResult|null = null
if (session.activePrivateKey) {
tmpFile = await tmp.file()
// await fs.writeFile(tmpFile.path, session.activePrivateKey)
const winSCPcom = path.slice(0, -3) + 'com'
await this.platform.exec(winSCPcom, ['/keygen', tmpFile.path, `/output=${tmpFile.path}`])
args.push(`/privatekey=${tmpFile.path}`)
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
}
args.push(`/privatekey=${tmpFile.path}`)
if (passphrase != null) {
args.push(`/passphrase=${passphrase}`)
}
}
await this.platform.exec(path, args)
} finally {
tmpFile?.cleanup()
}
await this.platform.exec(path, args)
tmpFile?.cleanup()
}
}

View File

@@ -111,7 +111,7 @@ export class SSHSession {
private logger: Logger
private refCount = 0
private remainingAuthMethods: AuthMethod[] = []
private allAuthMethods: AuthMethod[] = []
private serviceMessage = new Subject<string>()
private keyboardInteractivePrompt = new Subject<KeyboardInteractivePrompt>()
private willDestroy = new Subject<void>()
@@ -125,6 +125,7 @@ export class SSHSession {
private translate: TranslateService
private knownHosts: SSHKnownHostsService
private privateKeyImporters: AutoPrivateKeyLocator[]
private previouslyDisconnected = false
constructor (
private injector: Injector,
@@ -150,7 +151,7 @@ export class SSHSession {
}
private addPublicKeyAuthMethod (name: string, contents: Buffer) {
this.remainingAuthMethods.push({
this.allAuthMethods.push({
type: 'publickey',
name,
contents,
@@ -158,7 +159,7 @@ export class SSHSession {
}
async init (): Promise<void> {
this.remainingAuthMethods = [{ type: 'none' }]
this.allAuthMethods = [{ type: 'none' }]
if (!this.profile.options.auth || this.profile.options.auth === 'publicKey') {
if (this.profile.options.privateKeys?.length) {
for (const pk of this.profile.options.privateKeys) {
@@ -187,30 +188,32 @@ export class SSHSession {
if (!spec) {
this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Agent auth selected, but no running Agent process is found`)
} else {
this.remainingAuthMethods.push({
this.allAuthMethods.push({
type: 'agent',
...spec,
})
}
}
if (!this.profile.options.auth || this.profile.options.auth === 'keyboardInteractive') {
const savedPassword = this.profile.options.password ?? await this.passwordStorage.loadPassword(this.profile)
if (savedPassword) {
this.remainingAuthMethods.push({ type: 'keyboard-interactive', savedPassword })
}
this.remainingAuthMethods.push({ type: 'keyboard-interactive' })
}
if (!this.profile.options.auth || this.profile.options.auth === 'password') {
if (this.profile.options.password) {
this.remainingAuthMethods.push({ type: 'saved-password', password: this.profile.options.password })
this.allAuthMethods.push({ type: 'saved-password', password: this.profile.options.password })
}
const password = await this.passwordStorage.loadPassword(this.profile)
if (password) {
this.remainingAuthMethods.push({ type: 'saved-password', password })
this.allAuthMethods.push({ type: 'saved-password', password })
}
this.remainingAuthMethods.push({ type: 'prompt-password' })
}
this.remainingAuthMethods.push({ type: 'hostbased' })
if (!this.profile.options.auth || this.profile.options.auth === 'keyboardInteractive') {
const savedPassword = this.profile.options.password ?? await this.passwordStorage.loadPassword(this.profile)
if (savedPassword) {
this.allAuthMethods.push({ type: 'keyboard-interactive', savedPassword })
}
this.allAuthMethods.push({ type: 'keyboard-interactive' })
}
if (!this.profile.options.auth || this.profile.options.auth === 'password') {
this.allAuthMethods.push({ type: 'prompt-password' })
}
this.allAuthMethods.push({ type: 'hostbased' })
}
private async getAgentConnectionSpec (): Promise<russh.AgentConnectionSpec|null> {
@@ -323,9 +326,14 @@ export class SSHSession {
}
})
this.previouslyDisconnected = false
this.ssh.disconnect$.subscribe(() => {
if (this.open) {
this.destroy()
if (!this.previouslyDisconnected) {
this.previouslyDisconnected = true
// Let service messages drain
setTimeout(() => {
this.destroy()
})
}
})
@@ -508,6 +516,22 @@ export class SSHSession {
}
async handleAuth (): Promise<russh.AuthenticatedSSHClient|null> {
const subscription = this.ssh.disconnect$.subscribe(() => {
// Auto auth and >=3 keys found
if (!this.profile.options.auth && this.allAuthMethods.filter(x => x.type === 'publickey').length >= 3) {
this.emitServiceMessage('The server has disconnected during authentication.')
this.emitServiceMessage('This may happen if too many private key authentication attemps are made.')
this.emitServiceMessage('You can set the specific private key for authentication in the profile settings.')
}
})
try {
return await this._handleAuth()
} finally {
subscription.unsubscribe()
}
}
private async _handleAuth (): Promise<russh.AuthenticatedSSHClient|null> {
this.activePrivateKey = null
if (!(this.ssh instanceof russh.SSHClient)) {
@@ -523,6 +547,7 @@ export class SSHSession {
return noneResult
}
let remainingMethods = [...this.allAuthMethods]
let methodsLeft = noneResult.remainingMethods
function maybeSetRemainingMethods (r: russh.AuthFailure) {
@@ -533,13 +558,13 @@ export class SSHSession {
while (true) {
const m = methodsLeft
const method = this.remainingAuthMethods.find(x => m.length === 0 || m.includes(sshAuthTypeForMethod(x)))
const method = remainingMethods.find(x => m.length === 0 || m.includes(sshAuthTypeForMethod(x)))
if (!method) {
if (this.previouslyDisconnected || !method) {
return null
}
this.remainingAuthMethods = this.remainingAuthMethods.filter(x => x !== method)
remainingMethods = remainingMethods.filter(x => x !== method)
if (method.type === 'saved-password') {
this.emitServiceMessage(this.translate.instant('Using saved password'))
@@ -576,15 +601,12 @@ export class SSHSession {
if (method.type === 'publickey') {
try {
const key = await this.loadPrivateKey(method.name, method.contents)
const possibleHashAlgs = ['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512'].includes(key.algorithm) ? ['sha256', 'sha512', 'sha1'] as const : [null] as const
this.emitServiceMessage(`Trying private key: ${method.name}`)
for (const alg of possibleHashAlgs) {
const result = await this.ssh.authenticateWithKeyPair(this.authUsername, key, alg)
if (result instanceof russh.AuthenticatedSSHClient) {
return result
}
maybeSetRemainingMethods(result)
const result = await this.ssh.authenticateWithKeyPair(this.authUsername, key, null)
if (result instanceof russh.AuthenticatedSSHClient) {
return result
}
maybeSetRemainingMethods(result)
} catch (e) {
this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Failed to load private key ${method.name}: ${e}`)
continue