mirror of
https://github.com/Eugeny/tabby.git
synced 2025-01-06 13:44:36 +08:00
ssh: reworked keyboard-interactive auth UI - fixes #4540
This commit is contained in:
parent
8a514fff17
commit
d6fa3b02a9
@ -0,0 +1,26 @@
|
|||||||
|
.d-flex
|
||||||
|
strong Keyboard-interactive auth
|
||||||
|
.ml-2 {{prompt.name}}
|
||||||
|
|
||||||
|
.prompt-text {{prompt.prompts[step]}}
|
||||||
|
|
||||||
|
input.form-control.mt-2(
|
||||||
|
#input,
|
||||||
|
autofocus,
|
||||||
|
[type]='isPassword() ? "password": "text"',
|
||||||
|
placeholder='Response',
|
||||||
|
(keyup.enter)='next()',
|
||||||
|
[(ngModel)]='prompt.responses[step]'
|
||||||
|
)
|
||||||
|
|
||||||
|
.d-flex.mt-3
|
||||||
|
button.btn.btn-secondary(
|
||||||
|
*ngIf='step > 0',
|
||||||
|
(click)='previous()'
|
||||||
|
)
|
||||||
|
.ml-auto
|
||||||
|
button.btn.btn-primary(
|
||||||
|
(click)='next()'
|
||||||
|
)
|
||||||
|
span(*ngIf='step < prompt.prompts.length - 1') Next
|
||||||
|
span(*ngIf='step == prompt.prompts.length - 1') Finish
|
@ -0,0 +1,9 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 15px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-text {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, ChangeDetectionStrategy } from '@angular/core'
|
||||||
|
import { KeyboardInteractivePrompt } from '../session/ssh'
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'keyboard-interactive-auth-panel',
|
||||||
|
template: require('./keyboardInteractiveAuthPanel.component.pug'),
|
||||||
|
styles: [require('./keyboardInteractiveAuthPanel.component.scss')],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class KeyboardInteractiveAuthComponent {
|
||||||
|
@Input() prompt: KeyboardInteractivePrompt
|
||||||
|
@Input() step = 0
|
||||||
|
@Output() done = new EventEmitter()
|
||||||
|
@ViewChild('input') input: ElementRef
|
||||||
|
|
||||||
|
isPassword (): boolean {
|
||||||
|
return this.prompt.prompts[this.step].toLowerCase().includes('password')
|
||||||
|
}
|
||||||
|
|
||||||
|
previous (): void {
|
||||||
|
if (this.step > 0) {
|
||||||
|
this.step--
|
||||||
|
}
|
||||||
|
this.input.nativeElement.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
next (): void {
|
||||||
|
if (this.step === this.prompt.prompts.length - 1) {
|
||||||
|
this.prompt.respond()
|
||||||
|
this.done.emit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.step++
|
||||||
|
this.input.nativeElement.focus()
|
||||||
|
}
|
||||||
|
}
|
@ -45,3 +45,10 @@ sftp-panel.bg-dark(
|
|||||||
[session]='session',
|
[session]='session',
|
||||||
(closed)='sftpPanelVisible = false'
|
(closed)='sftpPanelVisible = false'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
keyboard-interactive-auth-panel.bg-dark(
|
||||||
|
*ngIf='activeKIPrompt',
|
||||||
|
[prompt]='activeKIPrompt',
|
||||||
|
(click)='$event.stopPropagation()',
|
||||||
|
(done)='activeKIPrompt = null; frontend.focus()'
|
||||||
|
)
|
||||||
|
@ -5,7 +5,7 @@ import { first } from 'rxjs'
|
|||||||
import { PartialProfile, Platform, ProfilesService, RecoveryToken } from 'tabby-core'
|
import { PartialProfile, Platform, ProfilesService, RecoveryToken } from 'tabby-core'
|
||||||
import { BaseTerminalTabComponent } from 'tabby-terminal'
|
import { BaseTerminalTabComponent } from 'tabby-terminal'
|
||||||
import { SSHService } from '../services/ssh.service'
|
import { SSHService } from '../services/ssh.service'
|
||||||
import { SSHSession } from '../session/ssh'
|
import { KeyboardInteractivePrompt, SSHSession } from '../session/ssh'
|
||||||
import { SSHPortForwardingModalComponent } from './sshPortForwardingModal.component'
|
import { SSHPortForwardingModalComponent } from './sshPortForwardingModal.component'
|
||||||
import { SSHProfile } from '../api'
|
import { SSHProfile } from '../api'
|
||||||
|
|
||||||
@ -24,6 +24,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
|||||||
sftpPanelVisible = false
|
sftpPanelVisible = false
|
||||||
sftpPath = '/'
|
sftpPath = '/'
|
||||||
enableToolbar = true
|
enableToolbar = true
|
||||||
|
activeKIPrompt: KeyboardInteractivePrompt|null = null
|
||||||
private sessionStack: SSHSession[] = []
|
private sessionStack: SSHSession[] = []
|
||||||
private recentInputs = ''
|
private recentInputs = ''
|
||||||
private reconnectOffered = false
|
private reconnectOffered = false
|
||||||
@ -35,6 +36,9 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
|||||||
private profilesService: ProfilesService,
|
private profilesService: ProfilesService,
|
||||||
) {
|
) {
|
||||||
super(injector)
|
super(injector)
|
||||||
|
this.sessionChanged$.subscribe(() => {
|
||||||
|
this.activeKIPrompt = null
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit (): void {
|
ngOnInit (): void {
|
||||||
@ -126,6 +130,17 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
|||||||
session.resize(this.size.columns, this.size.rows)
|
session.resize(this.size.columns, this.size.rows)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.attachSessionHandler(session.destroyed$, () => {
|
||||||
|
this.activeKIPrompt = null
|
||||||
|
})
|
||||||
|
|
||||||
|
this.attachSessionHandler(session.keyboardInteractivePrompt$, prompt => {
|
||||||
|
this.activeKIPrompt = prompt
|
||||||
|
setTimeout(() => {
|
||||||
|
this.frontend?.scrollToBottom()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.ssh.connectSession(session)
|
await this.ssh.connectSession(session)
|
||||||
this.stopSpinner()
|
this.stopSpinner()
|
||||||
|
@ -15,6 +15,7 @@ import { SSHSettingsTabComponent } from './components/sshSettingsTab.component'
|
|||||||
import { SSHTabComponent } from './components/sshTab.component'
|
import { SSHTabComponent } from './components/sshTab.component'
|
||||||
import { SFTPPanelComponent } from './components/sftpPanel.component'
|
import { SFTPPanelComponent } from './components/sftpPanel.component'
|
||||||
import { SFTPDeleteModalComponent } from './components/sftpDeleteModal.component'
|
import { SFTPDeleteModalComponent } from './components/sftpDeleteModal.component'
|
||||||
|
import { KeyboardInteractiveAuthComponent } from './components/keyboardInteractiveAuthPanel.component'
|
||||||
|
|
||||||
import { SSHConfigProvider } from './config'
|
import { SSHConfigProvider } from './config'
|
||||||
import { SSHSettingsTabProvider } from './settings'
|
import { SSHSettingsTabProvider } from './settings'
|
||||||
@ -60,6 +61,7 @@ import { CommonSFTPContextMenu } from './sftpContextMenu'
|
|||||||
SSHSettingsTabComponent,
|
SSHSettingsTabComponent,
|
||||||
SSHTabComponent,
|
SSHTabComponent,
|
||||||
SFTPPanelComponent,
|
SFTPPanelComponent,
|
||||||
|
KeyboardInteractiveAuthComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
||||||
|
@ -2,13 +2,12 @@ import colors from 'ansi-colors'
|
|||||||
import * as shellQuote from 'shell-quote'
|
import * as shellQuote from 'shell-quote'
|
||||||
import { Duplex } from 'stream'
|
import { Duplex } from 'stream'
|
||||||
import { Injectable, NgZone } from '@angular/core'
|
import { Injectable, NgZone } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
|
||||||
import { Client } from 'ssh2'
|
import { Client } from 'ssh2'
|
||||||
import { spawn } from 'child_process'
|
import { spawn } from 'child_process'
|
||||||
import { ChildProcess } from 'node:child_process'
|
import { ChildProcess } from 'node:child_process'
|
||||||
import { Subject, Observable } from 'rxjs'
|
import { Subject, Observable } from 'rxjs'
|
||||||
import { Logger, LogService, ConfigService, NotificationsService, HostAppService, Platform, PlatformService, PromptModalComponent } from 'tabby-core'
|
import { Logger, LogService, ConfigService, NotificationsService, HostAppService, Platform, PlatformService } from 'tabby-core'
|
||||||
import { SSHSession } from '../session/ssh'
|
import { KeyboardInteractivePrompt, SSHSession } from '../session/ssh'
|
||||||
import { ForwardedPort } from '../session/forwards'
|
import { ForwardedPort } from '../session/forwards'
|
||||||
import { ALGORITHM_BLACKLIST, SSHAlgorithmType, SSHProfile } from '../api'
|
import { ALGORITHM_BLACKLIST, SSHAlgorithmType, SSHProfile } from '../api'
|
||||||
import { PasswordStorageService } from './passwordStorage.service'
|
import { PasswordStorageService } from './passwordStorage.service'
|
||||||
@ -21,7 +20,6 @@ export class SSHService {
|
|||||||
private constructor (
|
private constructor (
|
||||||
log: LogService,
|
log: LogService,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private ngbModal: NgbModal,
|
|
||||||
private passwordStorage: PasswordStorageService,
|
private passwordStorage: PasswordStorageService,
|
||||||
private notifications: NotificationsService,
|
private notifications: NotificationsService,
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
@ -83,22 +81,12 @@ 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 () => {
|
||||||
log(colors.bgBlackBright(' ') + ` Keyboard-interactive auth requested: ${name}`)
|
session.emitKeyboardInteractivePrompt(new KeyboardInteractivePrompt(
|
||||||
this.logger.info('Keyboard-interactive auth:', name, instructions, instructionsLang)
|
name,
|
||||||
const results: string[] = []
|
instructions,
|
||||||
for (const prompt of prompts) {
|
prompts.map(x => x.prompt),
|
||||||
const modal = this.ngbModal.open(PromptModalComponent)
|
finish,
|
||||||
modal.componentInstance.prompt = prompt.prompt
|
))
|
||||||
modal.componentInstance.password = !prompt.echo
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await modal.result
|
|
||||||
results.push(result ? result.value : '')
|
|
||||||
} catch {
|
|
||||||
results.push('')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finish(results)
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
ssh.on('greeting', greeting => {
|
ssh.on('greeting', greeting => {
|
||||||
|
@ -27,6 +27,21 @@ interface AuthMethod {
|
|||||||
contents?: Buffer
|
contents?: Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class KeyboardInteractivePrompt {
|
||||||
|
responses: string[] = []
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
public name: string,
|
||||||
|
public instruction: string,
|
||||||
|
public prompts: string[],
|
||||||
|
private callback: (_: string[]) => void,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
respond (): void {
|
||||||
|
this.callback(this.responses)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class SSHSession extends BaseSession {
|
export class SSHSession extends BaseSession {
|
||||||
shell?: ClientChannel
|
shell?: ClientChannel
|
||||||
ssh: Client
|
ssh: Client
|
||||||
@ -36,12 +51,14 @@ export class SSHSession extends BaseSession {
|
|||||||
proxyCommandStream: ProxyCommandStream|null = null
|
proxyCommandStream: ProxyCommandStream|null = null
|
||||||
savedPassword?: string
|
savedPassword?: string
|
||||||
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
|
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
|
||||||
|
get keyboardInteractivePrompt$ (): Observable<KeyboardInteractivePrompt> { return this.keyboardInteractivePrompt }
|
||||||
|
|
||||||
agentPath?: string
|
agentPath?: string
|
||||||
activePrivateKey: string|null = null
|
activePrivateKey: string|null = null
|
||||||
|
|
||||||
private remainingAuthMethods: AuthMethod[] = []
|
private remainingAuthMethods: AuthMethod[] = []
|
||||||
private serviceMessage = new Subject<string>()
|
private serviceMessage = new Subject<string>()
|
||||||
|
private keyboardInteractivePrompt = new Subject<KeyboardInteractivePrompt>()
|
||||||
private keychainPasswordUsed = false
|
private keychainPasswordUsed = false
|
||||||
|
|
||||||
private passwordStorage: PasswordStorageService
|
private passwordStorage: PasswordStorageService
|
||||||
@ -246,6 +263,17 @@ export class SSHSession extends BaseSession {
|
|||||||
this.logger.info(stripAnsi(msg))
|
this.logger.info(stripAnsi(msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emitKeyboardInteractivePrompt (prompt: KeyboardInteractivePrompt): void {
|
||||||
|
this.logger.info('Keyboard-interactive auth:', prompt.name, prompt.instruction)
|
||||||
|
this.emitServiceMessage(colors.bgBlackBright(' ') + ` Keyboard-interactive auth requested: ${prompt.name}`)
|
||||||
|
if (prompt.instruction) {
|
||||||
|
for (const line of prompt.instruction.split('\n')) {
|
||||||
|
this.emitServiceMessage(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.keyboardInteractivePrompt.next(prompt)
|
||||||
|
}
|
||||||
|
|
||||||
async handleAuth (methodsLeft?: string[] | null): Promise<any> {
|
async handleAuth (methodsLeft?: string[] | null): Promise<any> {
|
||||||
this.activePrivateKey = null
|
this.activePrivateKey = null
|
||||||
|
|
||||||
|
@ -723,7 +723,9 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
this.spinner.text = text
|
this.spinner.text = text
|
||||||
}
|
}
|
||||||
this.spinner.setSpinnerString(6)
|
this.spinner.setSpinnerString(6)
|
||||||
this.spinner.start()
|
this.zone.runOutsideAngular(() => {
|
||||||
|
this.spinner.start()
|
||||||
|
})
|
||||||
this.spinnerActive = true
|
this.spinnerActive = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user