mirror of
https://github.com/Eugeny/tabby.git
synced 2025-06-14 16:40:05 +00:00
wip
This commit is contained in:
parent
bba72b4bb8
commit
a01d693eec
@ -76,7 +76,6 @@
|
|||||||
"source-code-pro": "^2.38.0",
|
"source-code-pro": "^2.38.0",
|
||||||
"source-map-loader": "^4.0.1",
|
"source-map-loader": "^4.0.1",
|
||||||
"source-sans-pro": "3.6.0",
|
"source-sans-pro": "3.6.0",
|
||||||
"ssh2": "^1.14.0",
|
|
||||||
"style-loader": "^3.3.1",
|
"style-loader": "^3.3.1",
|
||||||
"svg-inline-loader": "^0.8.2",
|
"svg-inline-loader": "^0.8.2",
|
||||||
"thenby": "^1.3.4",
|
"thenby": "^1.3.4",
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
diff --git a/node_modules/ssh2/lib/protocol/keyParser.js b/node_modules/ssh2/lib/protocol/keyParser.js
|
|
||||||
index 9860e3f..ee82e51 100644
|
|
||||||
--- a/node_modules/ssh2/lib/protocol/keyParser.js
|
|
||||||
+++ b/node_modules/ssh2/lib/protocol/keyParser.js
|
|
||||||
@@ -15,6 +15,7 @@ const {
|
|
||||||
sign: sign_,
|
|
||||||
verify: verify_,
|
|
||||||
} = require('crypto');
|
|
||||||
+const { createVerify: createVerifyDSS } = require('browserify-sign')
|
|
||||||
const supportedOpenSSLCiphers = getCiphers();
|
|
||||||
|
|
||||||
const { Ber } = require('asn1');
|
|
||||||
@@ -404,6 +405,17 @@ const BaseKey = {
|
|
||||||
return new Error('No public key available');
|
|
||||||
if (!algo || typeof algo !== 'string')
|
|
||||||
algo = this[SYM_HASH_ALGO];
|
|
||||||
+
|
|
||||||
+ if (algo === 'dss1') {
|
|
||||||
+ const verifier = createVerifyDSS('DSA-SHA1');
|
|
||||||
+ verifier.update(data);
|
|
||||||
+ try {
|
|
||||||
+ return verifier.verify(pem, signature);
|
|
||||||
+ } catch (ex) {
|
|
||||||
+ return ex;
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
try {
|
|
||||||
return verify_(algo, data, pem, signature);
|
|
||||||
} catch (ex) {
|
|
||||||
@@ -1343,7 +1355,7 @@ function parseDER(data, baseType, comment, fullType) {
|
|
||||||
return new Error('Malformed OpenSSH public key');
|
|
||||||
pubPEM = genOpenSSLDSAPub(p, q, g, y);
|
|
||||||
pubSSH = genOpenSSHDSAPub(p, q, g, y);
|
|
||||||
- algo = 'sha1';
|
|
||||||
+ algo = 'dss1';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'ssh-ed25519': {
|
|
@ -23,7 +23,6 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "20.3.1",
|
"@types/node": "20.3.1",
|
||||||
"@types/ssh2": "^0.5.46",
|
|
||||||
"ansi-colors": "^4.1.1",
|
"ansi-colors": "^4.1.1",
|
||||||
"diffie-hellman": "^5.0.3",
|
"diffie-hellman": "^5.0.3",
|
||||||
"sshpk": "Eugeny/node-sshpk#c2b71d1243714d2daf0988f84c3323d180817136",
|
"sshpk": "Eugeny/node-sshpk#c2b71d1243714d2daf0988f84c3323d180817136",
|
||||||
|
@ -1,20 +1,44 @@
|
|||||||
import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
|
import * as russh from 'russh'
|
||||||
import { ALGORITHM_BLACKLIST, SSHAlgorithmType } from './api'
|
import { SSHAlgorithmType } from './api'
|
||||||
|
|
||||||
// Counteracts https://github.com/mscdex/ssh2/commit/f1b5ac3c81734c194740016eab79a699efae83d8
|
export const supportedAlgorithms = {
|
||||||
ALGORITHMS.DEFAULT_CIPHER.push('aes128-gcm')
|
[SSHAlgorithmType.KEX]: russh.getSupportedKexAlgorithms().filter(x => x !== 'none'),
|
||||||
ALGORITHMS.DEFAULT_CIPHER.push('aes256-gcm')
|
[SSHAlgorithmType.HOSTKEY]: russh.getSupportedKeyTypes().filter(x => x !== 'none'),
|
||||||
ALGORITHMS.SUPPORTED_CIPHER.push('aes128-gcm')
|
[SSHAlgorithmType.CIPHER]: russh.getSupportedCiphers().filter(x => x !== 'clear'),
|
||||||
ALGORITHMS.SUPPORTED_CIPHER.push('aes256-gcm')
|
[SSHAlgorithmType.HMAC]: russh.getSupportedMACs().filter(x => x !== 'none'),
|
||||||
|
}
|
||||||
export const supportedAlgorithms: Record<string, string> = {}
|
|
||||||
|
export const defaultAlgorithms = {
|
||||||
for (const k of Object.values(SSHAlgorithmType)) {
|
[SSHAlgorithmType.KEX]: [
|
||||||
const supportedAlg = {
|
'curve25519-sha256',
|
||||||
[SSHAlgorithmType.KEX]: 'SUPPORTED_KEX',
|
'curve25519-sha256@libssh.org',
|
||||||
[SSHAlgorithmType.HOSTKEY]: 'SUPPORTED_SERVER_HOST_KEY',
|
'diffie-hellman-group16-sha512',
|
||||||
[SSHAlgorithmType.CIPHER]: 'SUPPORTED_CIPHER',
|
'diffie-hellman-group14-sha256',
|
||||||
[SSHAlgorithmType.HMAC]: 'SUPPORTED_MAC',
|
'ext-info-c',
|
||||||
}[k]
|
'ext-info-s',
|
||||||
supportedAlgorithms[k] = ALGORITHMS[supportedAlg].filter(x => !ALGORITHM_BLACKLIST.includes(x)).sort()
|
'kex-strict-c-v00@openssh.com',
|
||||||
|
'kex-strict-s-v00@openssh.com',
|
||||||
|
],
|
||||||
|
[SSHAlgorithmType.HOSTKEY]: [
|
||||||
|
'ssh-ed25519',
|
||||||
|
'ecdsa-sha2-nistp256',
|
||||||
|
'ecdsa-sha2-nistp521',
|
||||||
|
'rsa-sha2-256',
|
||||||
|
'rsa-sha2-512',
|
||||||
|
],
|
||||||
|
[SSHAlgorithmType.CIPHER]: [
|
||||||
|
'chacha20-poly1305@openssh.com',
|
||||||
|
'aes256-gcm@openssh.com',
|
||||||
|
'aes256-ctr',
|
||||||
|
'aes192-ctr',
|
||||||
|
'aes128-ctr',
|
||||||
|
],
|
||||||
|
[SSHAlgorithmType.HMAC]: [
|
||||||
|
'hmac-sha2-512-etm@openssh.com',
|
||||||
|
'hmac-sha2-256-etm@openssh.com',
|
||||||
|
'hmac-sha2-512',
|
||||||
|
'hmac-sha2-256',
|
||||||
|
'hmac-sha1-etm@openssh.com',
|
||||||
|
'hmac-sha1',
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
@ -51,13 +51,3 @@ export interface ForwardedPortConfig {
|
|||||||
targetPort: number
|
targetPort: number
|
||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export let ALGORITHM_BLACKLIST = [
|
|
||||||
// cause native crashes in node crypto, use EC instead
|
|
||||||
'diffie-hellman-group-exchange-sha256',
|
|
||||||
'diffie-hellman-group-exchange-sha1',
|
|
||||||
]
|
|
||||||
|
|
||||||
if (!process.env.TABBY_ENABLE_SSH_ALG_BLACKLIST) {
|
|
||||||
ALGORITHM_BLACKLIST = []
|
|
||||||
}
|
|
||||||
|
@ -94,17 +94,17 @@ export class SSHTabComponent extends ConnectableTerminalTabComponent<SSHProfile>
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
session.jumpStream = await new Promise((resolve, reject) => jumpSession.ssh.forwardOut(
|
// session.jumpStream = await new Promise((resolve, reject) => jumpSession.ssh.forwardOut(
|
||||||
'127.0.0.1', 0, profile.options.host, profile.options.port ?? 22,
|
// '127.0.0.1', 0, profile.options.host, profile.options.port ?? 22,
|
||||||
(err, stream) => {
|
// (err, stream) => {
|
||||||
if (err) {
|
// if (err) {
|
||||||
jumpSession.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not set up port forward on ${jumpConnection.name}`)
|
// jumpSession.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not set up port forward on ${jumpConnection.name}`)
|
||||||
reject(err)
|
// reject(err)
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
resolve(stream)
|
// resolve(stream)
|
||||||
},
|
// },
|
||||||
))
|
// ))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { Injectable, InjectFlags, Injector } from '@angular/core'
|
import { Injectable, InjectFlags, Injector } from '@angular/core'
|
||||||
import { NewTabParameters, PartialProfile, TranslateService, QuickConnectProfileProvider } from 'tabby-core'
|
import { NewTabParameters, PartialProfile, TranslateService, QuickConnectProfileProvider } from 'tabby-core'
|
||||||
import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
|
|
||||||
import { SSHProfileSettingsComponent } from './components/sshProfileSettings.component'
|
import { SSHProfileSettingsComponent } from './components/sshProfileSettings.component'
|
||||||
import { SSHTabComponent } from './components/sshTab.component'
|
import { SSHTabComponent } from './components/sshTab.component'
|
||||||
import { PasswordStorageService } from './services/passwordStorage.service'
|
import { PasswordStorageService } from './services/passwordStorage.service'
|
||||||
import { ALGORITHM_BLACKLIST, SSHAlgorithmType, SSHProfile } from './api'
|
import { SSHAlgorithmType, SSHProfile } from './api'
|
||||||
import { SSHProfileImporter } from './api/importer'
|
import { SSHProfileImporter } from './api/importer'
|
||||||
|
import { defaultAlgorithms } from './algorithms'
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile> {
|
export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile> {
|
||||||
@ -29,10 +29,10 @@ export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile>
|
|||||||
agentForward: false,
|
agentForward: false,
|
||||||
warnOnClose: null,
|
warnOnClose: null,
|
||||||
algorithms: {
|
algorithms: {
|
||||||
hmac: [],
|
hmac: [] as string[],
|
||||||
kex: [],
|
kex: [] as string[],
|
||||||
cipher: [],
|
cipher: [] as string[],
|
||||||
serverHostKey: [],
|
serverHostKey: [] as string[],
|
||||||
},
|
},
|
||||||
proxyCommand: null,
|
proxyCommand: null,
|
||||||
forwardedPorts: [],
|
forwardedPorts: [],
|
||||||
@ -54,13 +54,7 @@ export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile>
|
|||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
for (const k of Object.values(SSHAlgorithmType)) {
|
for (const k of Object.values(SSHAlgorithmType)) {
|
||||||
const defaultAlg = {
|
this.configDefaults.options.algorithms[k] = [...defaultAlgorithms[k]]
|
||||||
[SSHAlgorithmType.KEX]: 'DEFAULT_KEX',
|
|
||||||
[SSHAlgorithmType.HOSTKEY]: 'DEFAULT_SERVER_HOST_KEY',
|
|
||||||
[SSHAlgorithmType.CIPHER]: 'DEFAULT_CIPHER',
|
|
||||||
[SSHAlgorithmType.HMAC]: 'DEFAULT_MAC',
|
|
||||||
}[k]
|
|
||||||
this.configDefaults.options.algorithms[k] = ALGORITHMS[defaultAlg].filter(x => !ALGORITHM_BLACKLIST.includes(x))
|
|
||||||
this.configDefaults.options.algorithms[k].sort()
|
this.configDefaults.options.algorithms[k].sort()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as shellQuote from 'shell-quote'
|
import * as shellQuote from 'shell-quote'
|
||||||
import * as net from 'net'
|
import * as net from 'net'
|
||||||
import * as fs from 'fs/promises'
|
// import * as fs from 'fs/promises'
|
||||||
import * as tmp from 'tmp-promise'
|
import * as tmp from 'tmp-promise'
|
||||||
import socksv5 from '@luminati-io/socksv5'
|
import socksv5 from '@luminati-io/socksv5'
|
||||||
import { Duplex } from 'stream'
|
import { Duplex } from 'stream'
|
||||||
@ -55,7 +55,7 @@ export class SSHService {
|
|||||||
let tmpFile: tmp.FileResult|null = null
|
let tmpFile: tmp.FileResult|null = null
|
||||||
if (session.activePrivateKey) {
|
if (session.activePrivateKey) {
|
||||||
tmpFile = await tmp.file()
|
tmpFile = await tmp.file()
|
||||||
await fs.writeFile(tmpFile.path, session.activePrivateKey)
|
// await fs.writeFile(tmpFile.path, session.activePrivateKey)
|
||||||
const winSCPcom = path.slice(0, -3) + 'com'
|
const winSCPcom = path.slice(0, -3) + 'com'
|
||||||
await this.platform.exec(winSCPcom, ['/keygen', tmpFile.path, `/output=${tmpFile.path}`])
|
await this.platform.exec(winSCPcom, ['/keygen', tmpFile.path, `/output=${tmpFile.path}`])
|
||||||
args.push(`/privatekey=${tmpFile.path}`)
|
args.push(`/privatekey=${tmpFile.path}`)
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import { Observable, Subject } from 'rxjs'
|
import { Observable, Subject } from 'rxjs'
|
||||||
import stripAnsi from 'strip-ansi'
|
import stripAnsi from 'strip-ansi'
|
||||||
import { ClientChannel } from 'ssh2'
|
|
||||||
import { Injector } from '@angular/core'
|
import { Injector } from '@angular/core'
|
||||||
import { LogService } from 'tabby-core'
|
import { LogService } from 'tabby-core'
|
||||||
import { BaseSession, UTF8SplitterMiddleware, InputProcessor } from 'tabby-terminal'
|
import { BaseSession, UTF8SplitterMiddleware, InputProcessor } from 'tabby-terminal'
|
||||||
import { SSHSession } from './ssh'
|
import { SSHSession } from './ssh'
|
||||||
import { SSHProfile } from '../api'
|
import { SSHProfile } from '../api'
|
||||||
|
import * as russh from 'russh'
|
||||||
|
|
||||||
|
|
||||||
export class SSHShellSession extends BaseSession {
|
export class SSHShellSession extends BaseSession {
|
||||||
shell?: ClientChannel
|
shell?: russh.Channel
|
||||||
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
|
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
|
||||||
private serviceMessage = new Subject<string>()
|
private serviceMessage = new Subject<string>()
|
||||||
private ssh: SSHSession|null
|
private ssh: SSHSession|null
|
||||||
@ -53,19 +53,19 @@ export class SSHShellSession extends BaseSession {
|
|||||||
|
|
||||||
this.loginScriptProcessor?.executeUnconditionalScripts()
|
this.loginScriptProcessor?.executeUnconditionalScripts()
|
||||||
|
|
||||||
this.shell.on('greeting', greeting => {
|
// this.shell.on('greeting', greeting => {
|
||||||
this.emitServiceMessage(`Shell greeting: ${greeting}`)
|
// this.emitServiceMessage(`Shell greeting: ${greeting}`)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// this.shell.on('banner', banner => {
|
||||||
|
// this.emitServiceMessage(`Shell banner: ${banner}`)
|
||||||
|
// })
|
||||||
|
|
||||||
|
this.shell.data$.subscribe(data => {
|
||||||
|
this.emitOutput(Buffer.from(data))
|
||||||
})
|
})
|
||||||
|
|
||||||
this.shell.on('banner', banner => {
|
this.shell.eof$.subscribe(() => {
|
||||||
this.emitServiceMessage(`Shell banner: ${banner}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.shell.on('data', data => {
|
|
||||||
this.emitOutput(data)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.shell.on('end', () => {
|
|
||||||
this.logger.info('Shell session ended')
|
this.logger.info('Shell session ended')
|
||||||
if (this.open) {
|
if (this.open) {
|
||||||
this.destroy()
|
this.destroy()
|
||||||
@ -79,19 +79,22 @@ export class SSHShellSession extends BaseSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resize (columns: number, rows: number): void {
|
resize (columns: number, rows: number): void {
|
||||||
if (this.shell) {
|
this.shell?.resizePTY({
|
||||||
this.shell.setWindow(rows, columns, rows, columns)
|
columns,
|
||||||
}
|
rows,
|
||||||
|
pixHeight: 0,
|
||||||
|
pixWidth: 0,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
write (data: Buffer): void {
|
write (data: Buffer): void {
|
||||||
if (this.shell) {
|
if (this.shell) {
|
||||||
this.shell.write(data)
|
this.shell.write(new Uint8Array(data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kill (signal?: string): void {
|
kill (_signal?: string): void {
|
||||||
this.shell?.signal(signal ?? 'TERM')
|
// this.shell?.signal(signal ?? 'TERM')
|
||||||
}
|
}
|
||||||
|
|
||||||
async destroy (): Promise<void> {
|
async destroy (): Promise<void> {
|
||||||
|
@ -3,22 +3,22 @@ import * as crypto from 'crypto'
|
|||||||
import * as sshpk from 'sshpk'
|
import * as sshpk from 'sshpk'
|
||||||
import colors from 'ansi-colors'
|
import colors from 'ansi-colors'
|
||||||
import stripAnsi from 'strip-ansi'
|
import stripAnsi from 'strip-ansi'
|
||||||
import { Injector, NgZone } from '@angular/core'
|
import { Injector } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { ConfigService, FileProvidersService, HostAppService, NotificationsService, Platform, PlatformService, wrapPromise, PromptModalComponent, LogService, Logger, TranslateService } from 'tabby-core'
|
import { ConfigService, FileProvidersService, HostAppService, NotificationsService, Platform, PlatformService, PromptModalComponent, LogService, Logger, TranslateService } from 'tabby-core'
|
||||||
import { Socket } from 'net'
|
// import { Socket } from 'net'
|
||||||
import { Client, ClientChannel, SFTPWrapper } from 'ssh2'
|
// import { Client, ClientChannel, SFTPWrapper } from 'ssh2'
|
||||||
import { Subject, Observable } from 'rxjs'
|
import { Subject, Observable } from 'rxjs'
|
||||||
import { HostKeyPromptModalComponent } from '../components/hostKeyPromptModal.component'
|
import { HostKeyPromptModalComponent } from '../components/hostKeyPromptModal.component'
|
||||||
import { HTTPProxyStream, ProxyCommandStream, SocksProxyStream } from '../services/ssh.service'
|
// import { HTTPProxyStream, ProxyCommandStream, SocksProxyStream } from '../services/ssh.service'
|
||||||
import { PasswordStorageService } from '../services/passwordStorage.service'
|
import { PasswordStorageService } from '../services/passwordStorage.service'
|
||||||
import { SSHKnownHostsService } from '../services/sshKnownHosts.service'
|
import { SSHKnownHostsService } from '../services/sshKnownHosts.service'
|
||||||
import { promisify } from 'util'
|
|
||||||
import { SFTPSession } from './sftp'
|
import { SFTPSession } from './sftp'
|
||||||
import { SSHAlgorithmType, PortForwardType, SSHProfile, SSHProxyStream, AutoPrivateKeyLocator } from '../api'
|
import { SSHAlgorithmType, SSHProfile, SSHProxyStream, AutoPrivateKeyLocator } from '../api'
|
||||||
import { ForwardedPort } from './forwards'
|
import { ForwardedPort } from './forwards'
|
||||||
import { X11Socket } from './x11'
|
// import { X11Socket } from './x11'
|
||||||
import { supportedAlgorithms } from '../algorithms'
|
import { supportedAlgorithms } from '../algorithms'
|
||||||
|
import * as russh from 'russh'
|
||||||
|
|
||||||
const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent'
|
const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent'
|
||||||
|
|
||||||
@ -33,32 +33,42 @@ interface AuthMethod {
|
|||||||
contents?: Buffer
|
contents?: Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Handshake {
|
// interface Handshake {
|
||||||
kex: string
|
// kex: string
|
||||||
serverHostKey: string
|
// serverHostKey: string
|
||||||
}
|
// }
|
||||||
|
|
||||||
export class KeyboardInteractivePrompt {
|
export class KeyboardInteractivePrompt {
|
||||||
responses: string[] = []
|
readonly responses: string[] = []
|
||||||
|
|
||||||
|
private _resolve: (value: string[]) => void
|
||||||
|
private _reject: (reason: any) => void
|
||||||
|
readonly promise = new Promise<string[]>((resolve, reject) => {
|
||||||
|
this._resolve = resolve
|
||||||
|
this._reject = reject
|
||||||
|
})
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
public name: string,
|
public name: string,
|
||||||
public instruction: string,
|
public instruction: string,
|
||||||
public prompts: Prompt[],
|
public prompts: Prompt[],
|
||||||
private callback: (_: string[]) => void,
|
|
||||||
) {
|
) {
|
||||||
this.responses = new Array(this.prompts.length).fill('')
|
this.responses = new Array(this.prompts.length).fill('')
|
||||||
}
|
}
|
||||||
|
|
||||||
respond (): void {
|
respond (): void {
|
||||||
this.callback(this.responses)
|
this._resolve(this.responses)
|
||||||
|
}
|
||||||
|
|
||||||
|
reject (): void {
|
||||||
|
this._reject(new Error('Keyboard-interactive auth rejected'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SSHSession {
|
export class SSHSession {
|
||||||
shell?: ClientChannel
|
shell?: russh.Channel
|
||||||
ssh: Client
|
ssh: russh.SSHClient|russh.AuthenticatedSSHClient
|
||||||
sftp?: SFTPWrapper
|
// sftp?: SFTPWrapper
|
||||||
forwardedPorts: ForwardedPort[] = []
|
forwardedPorts: ForwardedPort[] = []
|
||||||
jumpStream: any
|
jumpStream: any
|
||||||
proxyCommandStream: SSHProxyStream|null = null
|
proxyCommandStream: SSHProxyStream|null = null
|
||||||
@ -68,7 +78,7 @@ export class SSHSession {
|
|||||||
get willDestroy$ (): Observable<void> { return this.willDestroy }
|
get willDestroy$ (): Observable<void> { return this.willDestroy }
|
||||||
|
|
||||||
agentPath?: string
|
agentPath?: string
|
||||||
activePrivateKey: string|null = null
|
activePrivateKey: russh.KeyPair|null = null
|
||||||
authUsername: string|null = null
|
authUsername: string|null = null
|
||||||
|
|
||||||
open = false
|
open = false
|
||||||
@ -80,14 +90,13 @@ export class SSHSession {
|
|||||||
private keyboardInteractivePrompt = new Subject<KeyboardInteractivePrompt>()
|
private keyboardInteractivePrompt = new Subject<KeyboardInteractivePrompt>()
|
||||||
private willDestroy = new Subject<void>()
|
private willDestroy = new Subject<void>()
|
||||||
private keychainPasswordUsed = false
|
private keychainPasswordUsed = false
|
||||||
private hostKeyDigest = ''
|
|
||||||
|
|
||||||
private passwordStorage: PasswordStorageService
|
private passwordStorage: PasswordStorageService
|
||||||
private ngbModal: NgbModal
|
private ngbModal: NgbModal
|
||||||
private hostApp: HostAppService
|
private hostApp: HostAppService
|
||||||
private platform: PlatformService
|
private platform: PlatformService
|
||||||
private notifications: NotificationsService
|
private notifications: NotificationsService
|
||||||
private zone: NgZone
|
// private zone: NgZone
|
||||||
private fileProviders: FileProvidersService
|
private fileProviders: FileProvidersService
|
||||||
private config: ConfigService
|
private config: ConfigService
|
||||||
private translate: TranslateService
|
private translate: TranslateService
|
||||||
@ -95,7 +104,7 @@ export class SSHSession {
|
|||||||
private privateKeyImporters: AutoPrivateKeyLocator[]
|
private privateKeyImporters: AutoPrivateKeyLocator[]
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private injector: Injector,
|
injector: Injector,
|
||||||
public profile: SSHProfile,
|
public profile: SSHProfile,
|
||||||
) {
|
) {
|
||||||
this.logger = injector.get(LogService).create(`ssh-${profile.options.host}-${profile.options.port}`)
|
this.logger = injector.get(LogService).create(`ssh-${profile.options.host}-${profile.options.port}`)
|
||||||
@ -105,7 +114,7 @@ export class SSHSession {
|
|||||||
this.hostApp = injector.get(HostAppService)
|
this.hostApp = injector.get(HostAppService)
|
||||||
this.platform = injector.get(PlatformService)
|
this.platform = injector.get(PlatformService)
|
||||||
this.notifications = injector.get(NotificationsService)
|
this.notifications = injector.get(NotificationsService)
|
||||||
this.zone = injector.get(NgZone)
|
// this.zone = injector.get(NgZone)
|
||||||
this.fileProviders = injector.get(FileProvidersService)
|
this.fileProviders = injector.get(FileProvidersService)
|
||||||
this.config = injector.get(ConfigService)
|
this.config = injector.get(ConfigService)
|
||||||
this.translate = injector.get(TranslateService)
|
this.translate = injector.get(TranslateService)
|
||||||
@ -184,248 +193,245 @@ export class SSHSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async openSFTP (): Promise<SFTPSession> {
|
async openSFTP (): Promise<SFTPSession> {
|
||||||
if (!this.sftp) {
|
throw new Error('Not implemented')
|
||||||
this.sftp = await wrapPromise(this.zone, promisify<SFTPWrapper>(f => this.ssh.sftp(f))())
|
// if (!this.sftp) {
|
||||||
}
|
// this.sftp = await wrapPromise(this.zone, promisify<SFTPWrapper>(f => this.ssh.sftp(f))())
|
||||||
return new SFTPSession(this.sftp, this.injector)
|
// }
|
||||||
|
// return new SFTPSession(this.sftp, this.injector)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async start (): Promise<void> {
|
async start (): Promise<void> {
|
||||||
const log = (s: any) => this.emitServiceMessage(s)
|
// const log = (s: any) => this.emitServiceMessage(s)
|
||||||
|
|
||||||
const ssh = new Client()
|
|
||||||
this.ssh = ssh
|
|
||||||
await this.init()
|
await this.init()
|
||||||
|
|
||||||
let connected = false
|
|
||||||
const algorithms = {}
|
const algorithms = {}
|
||||||
for (const key of Object.values(SSHAlgorithmType)) {
|
for (const key of Object.values(SSHAlgorithmType)) {
|
||||||
algorithms[key] = this.profile.options.algorithms![key].filter(x => supportedAlgorithms[key].includes(x))
|
algorithms[key] = this.profile.options.algorithms![key].filter(x => supportedAlgorithms[key].includes(x))
|
||||||
}
|
}
|
||||||
|
|
||||||
const hostVerifiedPromise: Promise<void> = new Promise((resolve, reject) => {
|
// todo migrate connection opts
|
||||||
ssh.on('handshake', async handshake => {
|
this.ssh = await russh.SSHClient.connect(
|
||||||
if (!await this.verifyHostKey(handshake)) {
|
`${this.profile.options.host.trim()}:${this.profile.options.port ?? 22}`,
|
||||||
this.ssh.end()
|
async key => {
|
||||||
reject(new Error('Host key verification failed'))
|
if (!await this.verifyHostKey(key)) {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
this.logger.info('Handshake complete:', handshake)
|
this.logger.info('Host key verified')
|
||||||
resolve()
|
return true
|
||||||
})
|
},
|
||||||
|
{
|
||||||
|
preferred: {
|
||||||
|
ciphers: this.profile.options.algorithms?.[SSHAlgorithmType.CIPHER]?.filter(x => supportedAlgorithms[SSHAlgorithmType.CIPHER].includes(x)),
|
||||||
|
kex: this.profile.options.algorithms?.[SSHAlgorithmType.KEX]?.filter(x => supportedAlgorithms[SSHAlgorithmType.KEX].includes(x)),
|
||||||
|
mac: this.profile.options.algorithms?.[SSHAlgorithmType.HMAC]?.filter(x => supportedAlgorithms[SSHAlgorithmType.HMAC].includes(x)),
|
||||||
|
key: this.profile.options.algorithms?.[SSHAlgorithmType.HOSTKEY]?.filter(x => supportedAlgorithms[SSHAlgorithmType.HOSTKEY].includes(x)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
this.ssh.disconnect$.subscribe(() => {
|
||||||
|
if (this.open) {
|
||||||
|
this.destroy()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const resultPromise: Promise<void> = new Promise(async (resolve, reject) => {
|
// auth
|
||||||
ssh.on('ready', () => {
|
|
||||||
connected = true
|
|
||||||
if (this.savedPassword) {
|
|
||||||
this.passwordStorage.savePassword(this.profile, this.savedPassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.zone.run(resolve)
|
this.authUsername ??= this.profile.options.user
|
||||||
})
|
if (!this.authUsername) {
|
||||||
ssh.on('error', error => {
|
const modal = this.ngbModal.open(PromptModalComponent)
|
||||||
if (error.message === 'All configured authentication methods failed') {
|
modal.componentInstance.prompt = `Username for ${this.profile.options.host}`
|
||||||
this.passwordStorage.deletePassword(this.profile)
|
try {
|
||||||
}
|
const result = await modal.result.catch(() => null)
|
||||||
this.zone.run(() => {
|
this.authUsername = result?.value ?? null
|
||||||
if (connected) {
|
} catch {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
this.authUsername = 'root'
|
||||||
this.notifications.error(error.toString())
|
}
|
||||||
} else {
|
}
|
||||||
reject(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
ssh.on('close', () => {
|
|
||||||
if (this.open) {
|
|
||||||
this.destroy()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => this.zone.run(async () => {
|
if (this.authUsername?.startsWith('$')) {
|
||||||
this.emitKeyboardInteractivePrompt(new KeyboardInteractivePrompt(
|
try {
|
||||||
name,
|
const result = process.env[this.authUsername.slice(1)]
|
||||||
instructions,
|
this.authUsername = result ?? this.authUsername
|
||||||
prompts,
|
} catch {
|
||||||
finish,
|
this.authUsername = 'root'
|
||||||
))
|
}
|
||||||
}))
|
}
|
||||||
|
|
||||||
ssh.on('greeting', greeting => {
|
const authenticatedClient = await this.handleAuth()
|
||||||
if (!this.profile.options.skipBanner) {
|
if (authenticatedClient) {
|
||||||
log('Greeting: ' + greeting)
|
this.ssh = authenticatedClient
|
||||||
}
|
} else {
|
||||||
})
|
this.ssh.disconnect()
|
||||||
|
this.passwordStorage.deletePassword(this.profile)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
|
throw new Error('Authentication rejected')
|
||||||
|
}
|
||||||
|
|
||||||
ssh.on('banner', banner => {
|
// auth success
|
||||||
if (!this.profile.options.skipBanner) {
|
|
||||||
log(banner)
|
if (this.savedPassword) {
|
||||||
}
|
this.passwordStorage.savePassword(this.profile, this.savedPassword)
|
||||||
})
|
}
|
||||||
})
|
|
||||||
|
//zone ?
|
||||||
|
|
||||||
|
// const resultPromise: Promise<void> = new Promise(async (resolve, reject) => {
|
||||||
|
|
||||||
|
|
||||||
|
// ssh.on('greeting', greeting => {
|
||||||
|
// if (!this.profile.options.skipBanner) {
|
||||||
|
// log('Greeting: ' + greeting)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
// ssh.on('banner', banner => {
|
||||||
|
// if (!this.profile.options.skipBanner) {
|
||||||
|
// log(banner)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.profile.options.socksProxyHost) {
|
// if (this.profile.options.socksProxyHost) {
|
||||||
this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ` Using ${this.profile.options.socksProxyHost}:${this.profile.options.socksProxyPort}`)
|
// this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ` Using ${this.profile.options.socksProxyHost}:${this.profile.options.socksProxyPort}`)
|
||||||
this.proxyCommandStream = new SocksProxyStream(this.profile)
|
// this.proxyCommandStream = new SocksProxyStream(this.profile)
|
||||||
}
|
// }
|
||||||
if (this.profile.options.httpProxyHost) {
|
// if (this.profile.options.httpProxyHost) {
|
||||||
this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ` Using ${this.profile.options.httpProxyHost}:${this.profile.options.httpProxyPort}`)
|
// this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ` Using ${this.profile.options.httpProxyHost}:${this.profile.options.httpProxyPort}`)
|
||||||
this.proxyCommandStream = new HTTPProxyStream(this.profile)
|
// this.proxyCommandStream = new HTTPProxyStream(this.profile)
|
||||||
}
|
// }
|
||||||
if (this.profile.options.proxyCommand) {
|
// if (this.profile.options.proxyCommand) {
|
||||||
this.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ` Using ${this.profile.options.proxyCommand}`)
|
// this.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ` Using ${this.profile.options.proxyCommand}`)
|
||||||
this.proxyCommandStream = new ProxyCommandStream(this.profile.options.proxyCommand)
|
// this.proxyCommandStream = new ProxyCommandStream(this.profile.options.proxyCommand)
|
||||||
}
|
// }
|
||||||
if (this.proxyCommandStream) {
|
// if (this.proxyCommandStream) {
|
||||||
this.proxyCommandStream.destroyed$.subscribe(err => {
|
// this.proxyCommandStream.destroyed$.subscribe(err => {
|
||||||
if (err) {
|
// if (err) {
|
||||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ` ${err.message}`)
|
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` ${err.message}`)
|
||||||
this.destroy()
|
// this.destroy()
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
|
|
||||||
this.proxyCommandStream.message$.subscribe(message => {
|
// this.proxyCommandStream.message$.subscribe(message => {
|
||||||
this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ' ' + message.trim())
|
// this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ' ' + message.trim())
|
||||||
})
|
// })
|
||||||
|
|
||||||
await this.proxyCommandStream.start()
|
// await this.proxyCommandStream.start()
|
||||||
}
|
// }
|
||||||
|
|
||||||
this.authUsername ??= this.profile.options.user
|
|
||||||
if (!this.authUsername) {
|
|
||||||
const modal = this.ngbModal.open(PromptModalComponent)
|
|
||||||
modal.componentInstance.prompt = `Username for ${this.profile.options.host}`
|
|
||||||
try {
|
|
||||||
const result = await modal.result.catch(() => null)
|
|
||||||
this.authUsername = result?.value ?? null
|
|
||||||
} catch {
|
|
||||||
this.authUsername = 'root'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.authUsername?.startsWith('$')) {
|
|
||||||
try {
|
|
||||||
const result = process.env[this.authUsername.slice(1)]
|
|
||||||
this.authUsername = result ?? this.authUsername
|
|
||||||
} catch {
|
|
||||||
this.authUsername = 'root'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ssh.connect({
|
// ssh.connect({
|
||||||
host: this.profile.options.host.trim(),
|
// host: this.profile.options.host.trim(),
|
||||||
port: this.profile.options.port ?? 22,
|
// port: this.profile.options.port ?? 22,
|
||||||
sock: this.proxyCommandStream?.socket ?? this.jumpStream,
|
// sock: this.proxyCommandStream?.socket ?? this.jumpStream,
|
||||||
username: this.authUsername ?? undefined,
|
// username: this.authUsername ?? undefined,
|
||||||
tryKeyboard: true,
|
// tryKeyboard: true,
|
||||||
agent: this.agentPath,
|
// agent: this.agentPath,
|
||||||
agentForward: this.profile.options.agentForward && !!this.agentPath,
|
// agentForward: this.profile.options.agentForward && !!this.agentPath,
|
||||||
keepaliveInterval: this.profile.options.keepaliveInterval ?? 15000,
|
// keepaliveInterval: this.profile.options.keepaliveInterval ?? 15000,
|
||||||
keepaliveCountMax: this.profile.options.keepaliveCountMax,
|
// keepaliveCountMax: this.profile.options.keepaliveCountMax,
|
||||||
readyTimeout: this.profile.options.readyTimeout,
|
// readyTimeout: this.profile.options.readyTimeout,
|
||||||
hostVerifier: (key: any) => {
|
// algorithms,
|
||||||
this.hostKeyDigest = crypto.createHash('sha256').update(key).digest('base64')
|
// authHandler: (methodsLeft, partialSuccess, callback) => {
|
||||||
return true
|
// this.zone.run(async () => {
|
||||||
},
|
// callback(await this.handleAuth(methodsLeft))
|
||||||
algorithms,
|
// })
|
||||||
authHandler: (methodsLeft, partialSuccess, callback) => {
|
// },
|
||||||
this.zone.run(async () => {
|
// })
|
||||||
callback(await this.handleAuth(methodsLeft))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.notifications.error(e.message)
|
this.notifications.error(e.message)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
await resultPromise
|
// for (const fw of this.profile.options.forwardedPorts ?? []) {
|
||||||
await hostVerifiedPromise
|
// this.addPortForward(Object.assign(new ForwardedPort(), fw))
|
||||||
|
// }
|
||||||
for (const fw of this.profile.options.forwardedPorts ?? []) {
|
|
||||||
this.addPortForward(Object.assign(new ForwardedPort(), fw))
|
|
||||||
}
|
|
||||||
|
|
||||||
this.open = true
|
this.open = true
|
||||||
|
|
||||||
this.ssh.on('tcp connection', (details, accept, reject) => {
|
// this.ssh.on('tcp connection', (details, accept, reject) => {
|
||||||
this.logger.info(`Incoming forwarded connection: (remote) ${details.srcIP}:${details.srcPort} -> (local) ${details.destIP}:${details.destPort}`)
|
// this.logger.info(`Incoming forwarded connection: (remote) ${details.srcIP}:${details.srcPort} -> (local) ${details.destIP}:${details.destPort}`)
|
||||||
const forward = this.forwardedPorts.find(x => x.port === details.destPort)
|
// const forward = this.forwardedPorts.find(x => x.port === details.destPort)
|
||||||
if (!forward) {
|
// if (!forward) {
|
||||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Rejected incoming forwarded connection for unrecognized port ${details.destPort}`)
|
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` Rejected incoming forwarded connection for unrecognized port ${details.destPort}`)
|
||||||
reject()
|
// reject()
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
const socket = new Socket()
|
// const socket = new Socket()
|
||||||
socket.connect(forward.targetPort, forward.targetAddress)
|
// socket.connect(forward.targetPort, forward.targetAddress)
|
||||||
socket.on('error', e => {
|
// socket.on('error', e => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
// // eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not forward the remote connection to ${forward.targetAddress}:${forward.targetPort}: ${e}`)
|
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not forward the remote connection to ${forward.targetAddress}:${forward.targetPort}: ${e}`)
|
||||||
reject()
|
// reject()
|
||||||
})
|
// })
|
||||||
socket.on('connect', () => {
|
// socket.on('connect', () => {
|
||||||
this.logger.info('Connection forwarded')
|
// this.logger.info('Connection forwarded')
|
||||||
const stream = accept()
|
// const stream = accept()
|
||||||
stream.pipe(socket)
|
// stream.pipe(socket)
|
||||||
socket.pipe(stream)
|
// socket.pipe(stream)
|
||||||
stream.on('close', () => {
|
// stream.on('close', () => {
|
||||||
socket.destroy()
|
// socket.destroy()
|
||||||
})
|
// })
|
||||||
socket.on('close', () => {
|
// socket.on('close', () => {
|
||||||
stream.close()
|
// stream.close()
|
||||||
})
|
// })
|
||||||
})
|
// })
|
||||||
})
|
// })
|
||||||
|
|
||||||
this.ssh.on('x11', async (details, accept, reject) => {
|
// this.ssh.on('x11', async (details, accept, reject) => {
|
||||||
this.logger.info(`Incoming X11 connection from ${details.srcIP}:${details.srcPort}`)
|
// this.logger.info(`Incoming X11 connection from ${details.srcIP}:${details.srcPort}`)
|
||||||
const displaySpec = (this.config.store.ssh.x11Display || process.env.DISPLAY) ?? 'localhost:0'
|
// const displaySpec = (this.config.store.ssh.x11Display || process.env.DISPLAY) ?? 'localhost:0'
|
||||||
this.logger.debug(`Trying display ${displaySpec}`)
|
// this.logger.debug(`Trying display ${displaySpec}`)
|
||||||
|
|
||||||
const socket = new X11Socket()
|
// const socket = new X11Socket()
|
||||||
try {
|
// try {
|
||||||
const x11Stream = await socket.connect(displaySpec)
|
// const x11Stream = await socket.connect(displaySpec)
|
||||||
this.logger.info('Connection forwarded')
|
// this.logger.info('Connection forwarded')
|
||||||
const stream = accept()
|
// const stream = accept()
|
||||||
stream.pipe(x11Stream)
|
// stream.pipe(x11Stream)
|
||||||
x11Stream.pipe(stream)
|
// x11Stream.pipe(stream)
|
||||||
stream.on('close', () => {
|
// stream.on('close', () => {
|
||||||
socket.destroy()
|
// socket.destroy()
|
||||||
})
|
// })
|
||||||
x11Stream.on('close', () => {
|
// x11Stream.on('close', () => {
|
||||||
stream.close()
|
// stream.close()
|
||||||
})
|
// })
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
// // eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not connect to the X server: ${e}`)
|
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not connect to the X server: ${e}`)
|
||||||
this.emitServiceMessage(` Tabby tried to connect to ${JSON.stringify(X11Socket.resolveDisplaySpec(displaySpec))} based on the DISPLAY environment var (${displaySpec})`)
|
// this.emitServiceMessage(` Tabby tried to connect to ${JSON.stringify(X11Socket.resolveDisplaySpec(displaySpec))} based on the DISPLAY environment var (${displaySpec})`)
|
||||||
if (process.platform === 'win32') {
|
// if (process.platform === 'win32') {
|
||||||
this.emitServiceMessage(' To use X forwarding, you need a local X server, e.g.:')
|
// this.emitServiceMessage(' To use X forwarding, you need a local X server, e.g.:')
|
||||||
this.emitServiceMessage(' * VcXsrv: https://sourceforge.net/projects/vcxsrv/')
|
// this.emitServiceMessage(' * VcXsrv: https://sourceforge.net/projects/vcxsrv/')
|
||||||
this.emitServiceMessage(' * Xming: https://sourceforge.net/projects/xming/')
|
// this.emitServiceMessage(' * Xming: https://sourceforge.net/projects/xming/')
|
||||||
}
|
// }
|
||||||
reject()
|
// reject()
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
private async verifyHostKey (handshake: Handshake): Promise<boolean> {
|
private async verifyHostKey (key: russh.SshPublicKey): Promise<boolean> {
|
||||||
this.emitServiceMessage('Host key fingerprint:')
|
this.emitServiceMessage('Host key fingerprint:')
|
||||||
this.emitServiceMessage(colors.white.bgBlack(` ${handshake.serverHostKey} `) + colors.bgBlackBright(' ' + this.hostKeyDigest + ' '))
|
this.emitServiceMessage(colors.white.bgBlack(` ${key.algorithm()} `) + colors.bgBlackBright(' ' + key.fingerprint() + ' '))
|
||||||
if (!this.config.store.ssh.verifyHostKeys) {
|
if (!this.config.store.ssh.verifyHostKeys) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
const selector = {
|
const selector = {
|
||||||
host: this.profile.options.host,
|
host: this.profile.options.host,
|
||||||
port: this.profile.options.port ?? 22,
|
port: this.profile.options.port ?? 22,
|
||||||
type: handshake.serverHostKey,
|
type: key.algorithm(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const keyDigest = crypto.createHash('sha256').update(key.bytes()).digest('base64')
|
||||||
|
|
||||||
const knownHost = this.knownHosts.getFor(selector)
|
const knownHost = this.knownHosts.getFor(selector)
|
||||||
if (!knownHost || knownHost.digest !== this.hostKeyDigest) {
|
if (!knownHost || knownHost.digest !== keyDigest) {
|
||||||
const modal = this.ngbModal.open(HostKeyPromptModalComponent)
|
const modal = this.ngbModal.open(HostKeyPromptModalComponent)
|
||||||
modal.componentInstance.selector = selector
|
modal.componentInstance.selector = selector
|
||||||
modal.componentInstance.digest = this.hostKeyDigest
|
modal.componentInstance.digest = keyDigest
|
||||||
return modal.result.catch(() => false)
|
return modal.result.catch(() => false)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@ -447,13 +453,21 @@ export class SSHSession {
|
|||||||
this.keyboardInteractivePrompt.next(prompt)
|
this.keyboardInteractivePrompt.next(prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleAuth (methodsLeft?: string[] | null): Promise<any> {
|
async handleAuth (methodsLeft?: string[] | null): Promise<russh.AuthenticatedSSHClient|null> {
|
||||||
this.activePrivateKey = null
|
this.activePrivateKey = null
|
||||||
|
|
||||||
|
if (!(this.ssh instanceof russh.SSHClient)) {
|
||||||
|
throw new Error('Wrong state for auth handling')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.authUsername) {
|
||||||
|
throw new Error('No username')
|
||||||
|
}
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const method = this.remainingAuthMethods.shift()
|
const method = this.remainingAuthMethods.shift()
|
||||||
if (!method) {
|
if (!method) {
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
if (methodsLeft && !methodsLeft.includes(method.type) && method.type !== 'agent') {
|
if (methodsLeft && !methodsLeft.includes(method.type) && method.type !== 'agent') {
|
||||||
// Agent can still be used even if not in methodsLeft
|
// Agent can still be used even if not in methodsLeft
|
||||||
@ -463,10 +477,9 @@ export class SSHSession {
|
|||||||
if (method.type === 'password') {
|
if (method.type === 'password') {
|
||||||
if (this.profile.options.password) {
|
if (this.profile.options.password) {
|
||||||
this.emitServiceMessage(this.translate.instant('Using preset password'))
|
this.emitServiceMessage(this.translate.instant('Using preset password'))
|
||||||
return {
|
const result = await this.ssh.authenticateWithPassword(this.authUsername, this.profile.options.password)
|
||||||
type: 'password',
|
if (result) {
|
||||||
username: this.authUsername,
|
return result
|
||||||
password: this.profile.options.password,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -475,10 +488,9 @@ export class SSHSession {
|
|||||||
if (password) {
|
if (password) {
|
||||||
this.emitServiceMessage(this.translate.instant('Trying saved password'))
|
this.emitServiceMessage(this.translate.instant('Trying saved password'))
|
||||||
this.keychainPasswordUsed = true
|
this.keychainPasswordUsed = true
|
||||||
return {
|
const result = await this.ssh.authenticateWithPassword(this.authUsername, password)
|
||||||
type: 'password',
|
if (result) {
|
||||||
username: this.authUsername,
|
return result
|
||||||
password,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -489,15 +501,14 @@ export class SSHSession {
|
|||||||
modal.componentInstance.showRememberCheckbox = true
|
modal.componentInstance.showRememberCheckbox = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await modal.result.catch(() => null)
|
const promptResult = await modal.result.catch(() => null)
|
||||||
if (result) {
|
if (promptResult) {
|
||||||
if (result.remember) {
|
if (promptResult.remember) {
|
||||||
this.savedPassword = result.value
|
this.savedPassword = promptResult.value
|
||||||
}
|
}
|
||||||
return {
|
const result = await this.ssh.authenticateWithPassword(this.authUsername, promptResult.value)
|
||||||
type: 'password',
|
if (result) {
|
||||||
username: this.authUsername,
|
return result
|
||||||
password: result.value,
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
continue
|
continue
|
||||||
@ -509,81 +520,116 @@ export class SSHSession {
|
|||||||
if (method.type === 'publickey' && method.contents) {
|
if (method.type === 'publickey' && method.contents) {
|
||||||
try {
|
try {
|
||||||
const key = await this.loadPrivateKey(method.name!, method.contents)
|
const key = await this.loadPrivateKey(method.name!, method.contents)
|
||||||
return {
|
const result = await this.ssh.authenticateWithKeyPair(this.authUsername, key)
|
||||||
type: 'publickey',
|
if (result) {
|
||||||
username: this.authUsername,
|
return result
|
||||||
key,
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Failed to load private key ${method.name}: ${e}`)
|
this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Failed to load private key ${method.name}: ${e}`)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return method.type
|
if (method.type === 'keyboard-interactive') {
|
||||||
|
let state: russh.AuthenticatedSSHClient|russh.KeyboardInteractiveAuthenticationState = await this.ssh.startKeyboardInteractiveAuthentication(this.authUsername)
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (state.state === 'failure') {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompts = await state.prompts()
|
||||||
|
|
||||||
|
let responses: string[] = []
|
||||||
|
// OpenSSH can send a k-i request without prompts
|
||||||
|
// just respond ok to it
|
||||||
|
if (prompts.length > 0) {
|
||||||
|
const prompt = new KeyboardInteractivePrompt(
|
||||||
|
state.name,
|
||||||
|
state.instructions,
|
||||||
|
await state.prompts(),
|
||||||
|
)
|
||||||
|
this.emitKeyboardInteractivePrompt(prompt)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||||
|
responses = await prompt.promise
|
||||||
|
} catch {
|
||||||
|
break // this loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state = await this.ssh .continueKeyboardInteractiveAuthentication(responses)
|
||||||
|
|
||||||
|
if (state instanceof russh.AuthenticatedSSHClient) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
async addPortForward (fw: ForwardedPort): Promise<void> {
|
async addPortForward (_fw: ForwardedPort): Promise<void> {
|
||||||
if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) {
|
// if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) {
|
||||||
await fw.startLocalListener((accept, reject, sourceAddress, sourcePort, targetAddress, targetPort) => {
|
// await fw.startLocalListener((accept, reject, sourceAddress, sourcePort, targetAddress, targetPort) => {
|
||||||
this.logger.info(`New connection on ${fw}`)
|
// this.logger.info(`New connection on ${fw}`)
|
||||||
this.ssh.forwardOut(
|
// this.ssh.forwardOut(
|
||||||
sourceAddress ?? '127.0.0.1',
|
// sourceAddress ?? '127.0.0.1',
|
||||||
sourcePort ?? 0,
|
// sourcePort ?? 0,
|
||||||
targetAddress,
|
// targetAddress,
|
||||||
targetPort,
|
// targetPort,
|
||||||
(err, stream) => {
|
// (err, stream) => {
|
||||||
if (err) {
|
// if (err) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
// // eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote has rejected the forwarded connection to ${targetAddress}:${targetPort} via ${fw}: ${err}`)
|
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote has rejected the forwarded connection to ${targetAddress}:${targetPort} via ${fw}: ${err}`)
|
||||||
reject()
|
// reject()
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
const socket = accept()
|
// const socket = accept()
|
||||||
stream.pipe(socket)
|
// stream.pipe(socket)
|
||||||
socket.pipe(stream)
|
// socket.pipe(stream)
|
||||||
stream.on('close', () => {
|
// stream.on('close', () => {
|
||||||
socket.destroy()
|
// socket.destroy()
|
||||||
})
|
// })
|
||||||
socket.on('close', () => {
|
// socket.on('close', () => {
|
||||||
stream.close()
|
// stream.close()
|
||||||
})
|
// })
|
||||||
},
|
// },
|
||||||
)
|
// )
|
||||||
}).then(() => {
|
// }).then(() => {
|
||||||
this.emitServiceMessage(colors.bgGreen.black(' -> ') + ` Forwarded ${fw}`)
|
// this.emitServiceMessage(colors.bgGreen.black(' -> ') + ` Forwarded ${fw}`)
|
||||||
this.forwardedPorts.push(fw)
|
// this.forwardedPorts.push(fw)
|
||||||
}).catch(e => {
|
// }).catch(e => {
|
||||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Failed to forward port ${fw}: ${e}`)
|
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` Failed to forward port ${fw}: ${e}`)
|
||||||
throw e
|
// throw e
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
if (fw.type === PortForwardType.Remote) {
|
// if (fw.type === PortForwardType.Remote) {
|
||||||
await new Promise<void>((resolve, reject) => {
|
// await new Promise<void>((resolve, reject) => {
|
||||||
this.ssh.forwardIn(fw.host, fw.port, err => {
|
// this.ssh.forwardIn(fw.host, fw.port, err => {
|
||||||
if (err) {
|
// if (err) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
// // eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected port forwarding for ${fw}: ${err}`)
|
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected port forwarding for ${fw}: ${err}`)
|
||||||
reject(err)
|
// reject(err)
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
resolve()
|
// resolve()
|
||||||
})
|
// })
|
||||||
})
|
// })
|
||||||
this.emitServiceMessage(colors.bgGreen.black(' <- ') + ` Forwarded ${fw}`)
|
// this.emitServiceMessage(colors.bgGreen.black(' <- ') + ` Forwarded ${fw}`)
|
||||||
this.forwardedPorts.push(fw)
|
// this.forwardedPorts.push(fw)
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
async removePortForward (fw: ForwardedPort): Promise<void> {
|
async removePortForward (fw: ForwardedPort): Promise<void> {
|
||||||
if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) {
|
// if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) {
|
||||||
fw.stopLocalListener()
|
// fw.stopLocalListener()
|
||||||
this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
|
// this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
|
||||||
}
|
// }
|
||||||
if (fw.type === PortForwardType.Remote) {
|
// if (fw.type === PortForwardType.Remote) {
|
||||||
this.ssh.unforwardIn(fw.host, fw.port)
|
// this.ssh.unforwardIn(fw.host, fw.port)
|
||||||
this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
|
// this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
|
||||||
}
|
// }
|
||||||
this.emitServiceMessage(`Stopped forwarding ${fw}`)
|
this.emitServiceMessage(`Stopped forwarding ${fw}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -593,25 +639,36 @@ export class SSHSession {
|
|||||||
this.willDestroy.complete()
|
this.willDestroy.complete()
|
||||||
this.serviceMessage.complete()
|
this.serviceMessage.complete()
|
||||||
this.proxyCommandStream?.stop()
|
this.proxyCommandStream?.stop()
|
||||||
this.ssh.end()
|
this.ssh.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
openShellChannel (options: { x11: boolean }): Promise<ClientChannel> {
|
async openShellChannel (options: { x11: boolean }): Promise<russh.Channel> {
|
||||||
return new Promise<ClientChannel>((resolve, reject) => {
|
if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
|
||||||
this.ssh.shell({ term: 'xterm-256color' }, options, (err, shell) => {
|
throw new Error('Cannot open shell channel before auth')
|
||||||
if (err) {
|
}
|
||||||
reject(err)
|
const ch = await this.ssh.openSessionChannel()
|
||||||
} else {
|
await ch.requestPTY('xterm-256color', {
|
||||||
resolve(shell)
|
columns: 80,
|
||||||
}
|
rows: 24,
|
||||||
})
|
pixHeight: 0,
|
||||||
|
pixWidth: 0,
|
||||||
})
|
})
|
||||||
|
if (options.x11) {
|
||||||
|
await ch.requestX11Forwarding({
|
||||||
|
singleConnection: false,
|
||||||
|
authProtocol: 'MIT-MAGIC-COOKIE-1',
|
||||||
|
authCookie: crypto.randomBytes(16).toString('hex'),
|
||||||
|
screenNumber: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await ch.requestShell()
|
||||||
|
return ch
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadPrivateKey (name: string, privateKeyContents: Buffer): Promise<string|null> {
|
async loadPrivateKey (name: string, privateKeyContents: Buffer): Promise<russh.KeyPair> {
|
||||||
this.emitServiceMessage(`Loading private key: ${name}`)
|
this.emitServiceMessage(`Loading private key: ${name}`)
|
||||||
const parsedKey = await this.parsePrivateKey(privateKeyContents.toString())
|
//todo passphrase handling
|
||||||
this.activePrivateKey = parsedKey.toString('openssh')
|
this.activePrivateKey = await russh.KeyPair.parse(privateKeyContents.toString())
|
||||||
return this.activePrivateKey
|
return this.activePrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,9 +7,4 @@ import config from '../webpack.plugin.config.mjs'
|
|||||||
export default () => config({
|
export default () => config({
|
||||||
name: 'ssh',
|
name: 'ssh',
|
||||||
dirname: __dirname,
|
dirname: __dirname,
|
||||||
alias: {
|
|
||||||
'cpu-features': false,
|
|
||||||
'./crypto/build/Release/sshcrypto.node': false,
|
|
||||||
'../build/Release/cpufeatures.node': false,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
@ -157,6 +157,7 @@ export default options => {
|
|||||||
'os',
|
'os',
|
||||||
'path',
|
'path',
|
||||||
'readline',
|
'readline',
|
||||||
|
'russh',
|
||||||
'@luminati-io/socksv5',
|
'@luminati-io/socksv5',
|
||||||
'stream',
|
'stream',
|
||||||
'windows-native-registry',
|
'windows-native-registry',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user