mirror of
https://github.com/eugeny/tabby
synced 2025-12-12 10:45:45 +00:00
ssh - show connection log while connecting
This commit is contained in:
@@ -23,10 +23,11 @@ export interface SSHConnection {
|
|||||||
|
|
||||||
export class SSHSession extends BaseSession {
|
export class SSHSession extends BaseSession {
|
||||||
scripts?: LoginScript[]
|
scripts?: LoginScript[]
|
||||||
|
shell: any
|
||||||
|
|
||||||
constructor (private shell: any, conn: SSHConnection) {
|
constructor (public connection: SSHConnection) {
|
||||||
super()
|
super()
|
||||||
this.scripts = conn.scripts || []
|
this.scripts = connection.scripts || []
|
||||||
}
|
}
|
||||||
|
|
||||||
start () {
|
start () {
|
||||||
@@ -87,15 +88,21 @@ export class SSHSession extends BaseSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resize (columns, rows) {
|
resize (columns, rows) {
|
||||||
this.shell.setWindow(rows, columns)
|
if (this.shell) {
|
||||||
|
this.shell.setWindow(rows, columns)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
write (data) {
|
write (data) {
|
||||||
this.shell.write(data)
|
if (this.shell) {
|
||||||
|
this.shell.write(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kill (signal?: string) {
|
kill (signal?: string) {
|
||||||
this.shell.signal(signal || 'TERM')
|
if (this.shell) {
|
||||||
|
this.shell.signal(signal || 'TERM')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChildProcesses (): Promise<any[]> {
|
async getChildProcesses (): Promise<any[]> {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export class SSHModalComponent {
|
|||||||
|
|
||||||
connect (connection: SSHConnection) {
|
connect (connection: SSHConnection) {
|
||||||
this.close()
|
this.close()
|
||||||
this.ssh.connect(connection).catch(error => {
|
this.ssh.openTab(connection).catch(error => {
|
||||||
this.toastr.error(`Could not connect: ${error}`)
|
this.toastr.error(`Could not connect: ${error}`)
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { TerminalTabComponent } from 'terminus-terminal'
|
import { first } from 'rxjs/operators'
|
||||||
|
import { BaseTerminalTabComponent } from 'terminus-terminal'
|
||||||
|
import { SSHService } from '../services/ssh.service'
|
||||||
|
import { SSHConnection, SSHSession } from '../api'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@@ -10,5 +13,44 @@ import { TerminalTabComponent } from 'terminus-terminal'
|
|||||||
`,
|
`,
|
||||||
styles: [require('./sshTab.component.scss')],
|
styles: [require('./sshTab.component.scss')],
|
||||||
})
|
})
|
||||||
export class SSHTabComponent extends TerminalTabComponent {
|
export class SSHTabComponent extends BaseTerminalTabComponent {
|
||||||
|
connection: SSHConnection
|
||||||
|
ssh: SSHService
|
||||||
|
session: SSHSession
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.logger = this.log.create('terminalTab')
|
||||||
|
this.ssh = this.injector.get(SSHService)
|
||||||
|
this.frontendReady$.pipe(first()).subscribe(() => {
|
||||||
|
this.initializeSession()
|
||||||
|
})
|
||||||
|
|
||||||
|
super.ngOnInit()
|
||||||
|
}
|
||||||
|
|
||||||
|
async initializeSession () {
|
||||||
|
if (!this.connection) {
|
||||||
|
this.logger.error('No SSH connection info supplied')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.session = new SSHSession(this.connection)
|
||||||
|
this.attachSessionHandlers()
|
||||||
|
this.write(`Connecting to ${this.connection.host}`)
|
||||||
|
let interval = setInterval(() => this.write('.'), 500)
|
||||||
|
try {
|
||||||
|
await this.ssh.connectSession(this.session, message => {
|
||||||
|
this.write('\r\n' + message)
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.write('\r\n')
|
||||||
|
this.write(e.message)
|
||||||
|
return
|
||||||
|
} finally {
|
||||||
|
clearInterval(interval)
|
||||||
|
this.write('\r\n')
|
||||||
|
}
|
||||||
|
this.session.resize(this.size.columns, this.size.rows)
|
||||||
|
this.session.start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,14 +33,31 @@ export class SSHService {
|
|||||||
this.logger = log.create('ssh')
|
this.logger = log.create('ssh')
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect (connection: SSHConnection): Promise<SSHTabComponent> {
|
async openTab (connection: SSHConnection): Promise<SSHTabComponent> {
|
||||||
|
return this.zone.run(() => this.app.openNewTab(
|
||||||
|
SSHTabComponent,
|
||||||
|
{ connection }
|
||||||
|
) as SSHTabComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectSession (session: SSHSession, logCallback?: (s: string) => void): Promise<void> {
|
||||||
let privateKey: string = null
|
let privateKey: string = null
|
||||||
let privateKeyPassphrase: string = null
|
let privateKeyPassphrase: string = null
|
||||||
let privateKeyPath = connection.privateKey
|
let privateKeyPath = session.connection.privateKey
|
||||||
|
|
||||||
|
if (!logCallback) {
|
||||||
|
logCallback = (s) => null
|
||||||
|
}
|
||||||
|
|
||||||
|
const log = s => {
|
||||||
|
logCallback(s)
|
||||||
|
this.logger.info(s)
|
||||||
|
}
|
||||||
|
|
||||||
if (!privateKeyPath) {
|
if (!privateKeyPath) {
|
||||||
let userKeyPath = path.join(process.env.HOME, '.ssh', 'id_rsa')
|
let userKeyPath = path.join(process.env.HOME, '.ssh', 'id_rsa')
|
||||||
if (await fs.exists(userKeyPath)) {
|
if (await fs.exists(userKeyPath)) {
|
||||||
this.logger.info('Using user\'s default private key:', userKeyPath)
|
log(`Using user's default private key: ${userKeyPath}`)
|
||||||
privateKeyPath = userKeyPath
|
privateKeyPath = userKeyPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,11 +66,12 @@ export class SSHService {
|
|||||||
try {
|
try {
|
||||||
privateKey = (await fs.readFile(privateKeyPath)).toString()
|
privateKey = (await fs.readFile(privateKeyPath)).toString()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
log('Could not read the private key file')
|
||||||
this.toastr.warning('Could not read the private key file')
|
this.toastr.warning('Could not read the private key file')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (privateKey) {
|
if (privateKey) {
|
||||||
this.logger.info('Loaded private key from', privateKeyPath)
|
log(`Loading private key from ${privateKeyPath}`)
|
||||||
|
|
||||||
let encrypted = privateKey.includes('ENCRYPTED')
|
let encrypted = privateKey.includes('ENCRYPTED')
|
||||||
if (privateKeyPath.toLowerCase().endsWith('.ppk')) {
|
if (privateKeyPath.toLowerCase().endsWith('.ppk')) {
|
||||||
@@ -61,6 +79,7 @@ export class SSHService {
|
|||||||
}
|
}
|
||||||
if (encrypted) {
|
if (encrypted) {
|
||||||
let modal = this.ngbModal.open(PromptModalComponent)
|
let modal = this.ngbModal.open(PromptModalComponent)
|
||||||
|
log('Key requires passphrase')
|
||||||
modal.componentInstance.prompt = 'Private key passphrase'
|
modal.componentInstance.prompt = 'Private key passphrase'
|
||||||
modal.componentInstance.password = true
|
modal.componentInstance.password = true
|
||||||
try {
|
try {
|
||||||
@@ -77,12 +96,12 @@ export class SSHService {
|
|||||||
ssh.on('ready', () => {
|
ssh.on('ready', () => {
|
||||||
connected = true
|
connected = true
|
||||||
if (savedPassword) {
|
if (savedPassword) {
|
||||||
this.passwordStorage.savePassword(connection, savedPassword)
|
this.passwordStorage.savePassword(session.connection, savedPassword)
|
||||||
}
|
}
|
||||||
this.zone.run(resolve)
|
this.zone.run(resolve)
|
||||||
})
|
})
|
||||||
ssh.on('error', error => {
|
ssh.on('error', error => {
|
||||||
this.passwordStorage.deletePassword(connection)
|
this.passwordStorage.deletePassword(session.connection)
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
this.toastr.error(error.toString())
|
this.toastr.error(error.toString())
|
||||||
@@ -92,7 +111,8 @@ export class SSHService {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => this.zone.run(async () => {
|
ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => this.zone.run(async () => {
|
||||||
console.log(name, instructions, instructionsLang)
|
log(`Keyboard-interactive auth requested: ${name}`)
|
||||||
|
this.logger.info('Keyboard-interactive auth:', name, instructions, instructionsLang)
|
||||||
let results = []
|
let results = []
|
||||||
for (let prompt of prompts) {
|
for (let prompt of prompts) {
|
||||||
let modal = this.ngbModal.open(PromptModalComponent)
|
let modal = this.ngbModal.open(PromptModalComponent)
|
||||||
@@ -103,6 +123,14 @@ export class SSHService {
|
|||||||
finish(results)
|
finish(results)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
ssh.on('greeting', greeting => {
|
||||||
|
log('Greeting: ' + greeting)
|
||||||
|
})
|
||||||
|
|
||||||
|
ssh.on('banner', banner => {
|
||||||
|
log('Banner: ' + banner)
|
||||||
|
})
|
||||||
|
|
||||||
let agent: string = null
|
let agent: string = null
|
||||||
if (this.hostApp.platform === Platform.Windows) {
|
if (this.hostApp.platform === Platform.Windows) {
|
||||||
let pageantRunning = new Promise<boolean>(resolve => {
|
let pageantRunning = new Promise<boolean>(resolve => {
|
||||||
@@ -119,50 +147,61 @@ export class SSHService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
ssh.connect({
|
ssh.connect({
|
||||||
host: connection.host,
|
host: session.connection.host,
|
||||||
port: connection.port || 22,
|
port: session.connection.port || 22,
|
||||||
username: connection.user,
|
username: session.connection.user,
|
||||||
password: connection.privateKey ? undefined : '',
|
password: session.connection.privateKey ? undefined : '',
|
||||||
privateKey,
|
privateKey,
|
||||||
passphrase: privateKeyPassphrase,
|
passphrase: privateKeyPassphrase,
|
||||||
tryKeyboard: true,
|
tryKeyboard: true,
|
||||||
agent,
|
agent,
|
||||||
agentForward: !!agent,
|
agentForward: !!agent,
|
||||||
keepaliveInterval: connection.keepaliveInterval,
|
keepaliveInterval: session.connection.keepaliveInterval,
|
||||||
keepaliveCountMax: connection.keepaliveCountMax,
|
keepaliveCountMax: session.connection.keepaliveCountMax,
|
||||||
readyTimeout: connection.readyTimeout,
|
readyTimeout: session.connection.readyTimeout,
|
||||||
|
debug: (...x) => console.log(...x),
|
||||||
|
hostVerifier: digest => {
|
||||||
|
log('SHA256 fingerprint: ' + digest)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
hostHash: 'sha256' as any,
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.toastr.error(e.message)
|
this.toastr.error(e.message)
|
||||||
|
reject(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
let keychainPasswordUsed = false
|
let keychainPasswordUsed = false
|
||||||
|
|
||||||
;(ssh as any).config.password = () => this.zone.run(async () => {
|
;(ssh as any).config.password = () => this.zone.run(async () => {
|
||||||
if (connection.password) {
|
if (session.connection.password) {
|
||||||
this.logger.info('Using preset password')
|
log('Using preset password')
|
||||||
return connection.password
|
return session.connection.password
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!keychainPasswordUsed) {
|
if (!keychainPasswordUsed) {
|
||||||
let password = await this.passwordStorage.loadPassword(connection)
|
let password = await this.passwordStorage.loadPassword(session.connection)
|
||||||
if (password) {
|
if (password) {
|
||||||
this.logger.info('Using saved password')
|
log('Trying saved password')
|
||||||
keychainPasswordUsed = true
|
keychainPasswordUsed = true
|
||||||
return password
|
return password
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let modal = this.ngbModal.open(PromptModalComponent)
|
let modal = this.ngbModal.open(PromptModalComponent)
|
||||||
modal.componentInstance.prompt = `Password for ${connection.user}@${connection.host}`
|
modal.componentInstance.prompt = `Password for ${session.connection.user}@${session.connection.host}`
|
||||||
modal.componentInstance.password = true
|
modal.componentInstance.password = true
|
||||||
savedPassword = await modal.result
|
try {
|
||||||
|
savedPassword = await modal.result
|
||||||
|
} catch (_) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
return savedPassword
|
return savedPassword
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let shell = await new Promise((resolve, reject) => {
|
let shell: any = await new Promise<any>((resolve, reject) => {
|
||||||
ssh.shell({ term: 'xterm-256color' }, (err, shell) => {
|
ssh.shell({ term: 'xterm-256color' }, (err, shell) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err)
|
reject(err)
|
||||||
@@ -172,14 +211,17 @@ export class SSHService {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
let session = new SSHSession(shell, connection)
|
session.shell = shell
|
||||||
|
|
||||||
return this.zone.run(() => this.app.openNewTab(
|
shell.on('greeting', greeting => {
|
||||||
SSHTabComponent,
|
log('Shell Greeting: ' + greeting)
|
||||||
{ session, sessionOptions: {} }
|
})
|
||||||
) as SSHTabComponent)
|
|
||||||
|
shell.on('banner', banner => {
|
||||||
|
log('Shell Banner: ' + banner)
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
this.toastr.error(error.message)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Observable, Subject, Subscription } from 'rxjs'
|
import { Observable, Subject, Subscription } from 'rxjs'
|
||||||
import { first } from 'rxjs/operators'
|
import { first } from 'rxjs/operators'
|
||||||
import { ToastrService } from 'ngx-toastr'
|
import { ToastrService } from 'ngx-toastr'
|
||||||
import { NgZone, OnInit, OnDestroy, Inject, Optional, ViewChild, HostBinding, Input } from '@angular/core'
|
import { NgZone, OnInit, OnDestroy, Inject, Injector, Optional, ViewChild, HostBinding, Input } from '@angular/core'
|
||||||
import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, Platform, LogService, Logger } from 'terminus-core'
|
import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, Platform, LogService, Logger } from 'terminus-core'
|
||||||
|
|
||||||
import { Session, SessionsService } from '../services/sessions.service'
|
import { BaseSession, SessionsService } from '../services/sessions.service'
|
||||||
import { TerminalFrontendService } from '../services/terminalFrontend.service'
|
import { TerminalFrontendService } from '../services/terminalFrontend.service'
|
||||||
|
|
||||||
import { TerminalDecorator, ResizeEvent, TerminalContextMenuItemProvider } from '../api'
|
import { TerminalDecorator, ResizeEvent, TerminalContextMenuItemProvider } from '../api'
|
||||||
@@ -20,7 +20,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
`
|
`
|
||||||
static styles = [require('./terminalTab.component.scss')]
|
static styles = [require('./terminalTab.component.scss')]
|
||||||
|
|
||||||
session: Session
|
session: BaseSession
|
||||||
@Input() zoom = 0
|
@Input() zoom = 0
|
||||||
@ViewChild('content') content
|
@ViewChild('content') content
|
||||||
@HostBinding('style.background-color') backgroundColor: string
|
@HostBinding('style.background-color') backgroundColor: string
|
||||||
@@ -43,6 +43,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
public config: ConfigService,
|
public config: ConfigService,
|
||||||
|
protected injector: Injector,
|
||||||
protected zone: NgZone,
|
protected zone: NgZone,
|
||||||
protected app: AppService,
|
protected app: AppService,
|
||||||
protected hostApp: HostAppService,
|
protected hostApp: HostAppService,
|
||||||
@@ -60,8 +61,6 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
this.decorators = this.decorators || []
|
this.decorators = this.decorators || []
|
||||||
this.setTitle('Terminal')
|
this.setTitle('Terminal')
|
||||||
|
|
||||||
this.session = new Session(this.config)
|
|
||||||
|
|
||||||
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
|
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
|
||||||
if (!this.hasFocus) {
|
if (!this.hasFocus) {
|
||||||
return
|
return
|
||||||
@@ -241,7 +240,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
this.logger.info(`Resizing to ${columns}x${rows}`)
|
this.logger.info(`Resizing to ${columns}x${rows}`)
|
||||||
this.size = { columns, rows }
|
this.size = { columns, rows }
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
if (this.session.open) {
|
if (this.session && this.session.open) {
|
||||||
this.session.resize(columns, rows)
|
this.session.resize(columns, rows)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -333,4 +332,19 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
await this.session.destroy()
|
await this.session.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected attachSessionHandlers () {
|
||||||
|
// this.session.output$.bufferTime(10).subscribe((datas) => {
|
||||||
|
this.session.output$.subscribe(data => {
|
||||||
|
this.zone.run(() => {
|
||||||
|
this.output.next(data)
|
||||||
|
this.write(data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.sessionCloseSubscription = this.session.closed$.subscribe(() => {
|
||||||
|
this.frontend.destroy()
|
||||||
|
this.app.closeTab(this)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { first } from 'rxjs/operators'
|
|||||||
import { BaseTabProcess } from 'terminus-core'
|
import { BaseTabProcess } from 'terminus-core'
|
||||||
import { BaseTerminalTabComponent } from './baseTerminalTab.component'
|
import { BaseTerminalTabComponent } from './baseTerminalTab.component'
|
||||||
import { SessionOptions } from '../api'
|
import { SessionOptions } from '../api'
|
||||||
|
import { Session } from '../services/sessions.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'terminalTab',
|
selector: 'terminalTab',
|
||||||
@@ -14,6 +15,7 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
|
|||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.logger = this.log.create('terminalTab')
|
this.logger = this.log.create('terminalTab')
|
||||||
|
this.session = new Session(this.config)
|
||||||
|
|
||||||
this.frontendReady$.pipe(first()).subscribe(() => {
|
this.frontendReady$.pipe(first()).subscribe(() => {
|
||||||
this.initializeSession(this.size.columns, this.size.rows)
|
this.initializeSession(this.size.columns, this.size.rows)
|
||||||
@@ -31,18 +33,7 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
// this.session.output$.bufferTime(10).subscribe((datas) => {
|
this.attachSessionHandlers()
|
||||||
this.session.output$.subscribe(data => {
|
|
||||||
this.zone.run(() => {
|
|
||||||
this.output.next(data)
|
|
||||||
this.write(data)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
this.sessionCloseSubscription = this.session.closed$.subscribe(() => {
|
|
||||||
this.frontend.destroy()
|
|
||||||
this.app.closeTab(this)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRecoveryToken (): Promise<any> {
|
async getRecoveryToken (): Promise<any> {
|
||||||
|
|||||||
@@ -11,13 +11,16 @@ export class TerminalFrontendService {
|
|||||||
|
|
||||||
constructor (private config: ConfigService) { }
|
constructor (private config: ConfigService) { }
|
||||||
|
|
||||||
getFrontend (session: BaseSession): Frontend {
|
getFrontend (session?: BaseSession): Frontend {
|
||||||
|
if (!session) {
|
||||||
|
return (this.config.store.terminal.frontend === 'xterm')
|
||||||
|
? new XTermFrontend()
|
||||||
|
: new HTermFrontend()
|
||||||
|
}
|
||||||
if (!this.containers.has(session)) {
|
if (!this.containers.has(session)) {
|
||||||
this.containers.set(
|
this.containers.set(
|
||||||
session,
|
session,
|
||||||
(this.config.store.terminal.frontend === 'xterm')
|
this.getFrontend(),
|
||||||
? new XTermFrontend()
|
|
||||||
: new HTermFrontend()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return this.containers.get(session)
|
return this.containers.get(session)
|
||||||
|
|||||||
Reference in New Issue
Block a user