mirror of
https://github.com/Eugeny/tabby
synced 2024-11-14 17:07:15 +00:00
ssh: reworked keyboard-interactive auth UI - fixes #4540
This commit is contained in:
parent
8a514fff17
commit
d6fa3b02a9
9 changed files with 136 additions and 22 deletions
|
@ -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',
|
||||
(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 { BaseTerminalTabComponent } from 'tabby-terminal'
|
||||
import { SSHService } from '../services/ssh.service'
|
||||
import { SSHSession } from '../session/ssh'
|
||||
import { KeyboardInteractivePrompt, SSHSession } from '../session/ssh'
|
||||
import { SSHPortForwardingModalComponent } from './sshPortForwardingModal.component'
|
||||
import { SSHProfile } from '../api'
|
||||
|
||||
|
@ -24,6 +24,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
|||
sftpPanelVisible = false
|
||||
sftpPath = '/'
|
||||
enableToolbar = true
|
||||
activeKIPrompt: KeyboardInteractivePrompt|null = null
|
||||
private sessionStack: SSHSession[] = []
|
||||
private recentInputs = ''
|
||||
private reconnectOffered = false
|
||||
|
@ -35,6 +36,9 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
|||
private profilesService: ProfilesService,
|
||||
) {
|
||||
super(injector)
|
||||
this.sessionChanged$.subscribe(() => {
|
||||
this.activeKIPrompt = null
|
||||
})
|
||||
}
|
||||
|
||||
ngOnInit (): void {
|
||||
|
@ -126,6 +130,17 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
|||
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 {
|
||||
await this.ssh.connectSession(session)
|
||||
this.stopSpinner()
|
||||
|
|
|
@ -15,6 +15,7 @@ import { SSHSettingsTabComponent } from './components/sshSettingsTab.component'
|
|||
import { SSHTabComponent } from './components/sshTab.component'
|
||||
import { SFTPPanelComponent } from './components/sftpPanel.component'
|
||||
import { SFTPDeleteModalComponent } from './components/sftpDeleteModal.component'
|
||||
import { KeyboardInteractiveAuthComponent } from './components/keyboardInteractiveAuthPanel.component'
|
||||
|
||||
import { SSHConfigProvider } from './config'
|
||||
import { SSHSettingsTabProvider } from './settings'
|
||||
|
@ -60,6 +61,7 @@ import { CommonSFTPContextMenu } from './sftpContextMenu'
|
|||
SSHSettingsTabComponent,
|
||||
SSHTabComponent,
|
||||
SFTPPanelComponent,
|
||||
KeyboardInteractiveAuthComponent,
|
||||
],
|
||||
})
|
||||
// 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 { Duplex } from 'stream'
|
||||
import { Injectable, NgZone } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Client } from 'ssh2'
|
||||
import { spawn } from 'child_process'
|
||||
import { ChildProcess } from 'node:child_process'
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import { Logger, LogService, ConfigService, NotificationsService, HostAppService, Platform, PlatformService, PromptModalComponent } from 'tabby-core'
|
||||
import { SSHSession } from '../session/ssh'
|
||||
import { Logger, LogService, ConfigService, NotificationsService, HostAppService, Platform, PlatformService } from 'tabby-core'
|
||||
import { KeyboardInteractivePrompt, SSHSession } from '../session/ssh'
|
||||
import { ForwardedPort } from '../session/forwards'
|
||||
import { ALGORITHM_BLACKLIST, SSHAlgorithmType, SSHProfile } from '../api'
|
||||
import { PasswordStorageService } from './passwordStorage.service'
|
||||
|
@ -21,7 +20,6 @@ export class SSHService {
|
|||
private constructor (
|
||||
log: LogService,
|
||||
private zone: NgZone,
|
||||
private ngbModal: NgbModal,
|
||||
private passwordStorage: PasswordStorageService,
|
||||
private notifications: NotificationsService,
|
||||
private config: ConfigService,
|
||||
|
@ -83,22 +81,12 @@ export class SSHService {
|
|||
})
|
||||
|
||||
ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => this.zone.run(async () => {
|
||||
log(colors.bgBlackBright(' ') + ` Keyboard-interactive auth requested: ${name}`)
|
||||
this.logger.info('Keyboard-interactive auth:', name, instructions, instructionsLang)
|
||||
const results: string[] = []
|
||||
for (const prompt of prompts) {
|
||||
const modal = this.ngbModal.open(PromptModalComponent)
|
||||
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)
|
||||
session.emitKeyboardInteractivePrompt(new KeyboardInteractivePrompt(
|
||||
name,
|
||||
instructions,
|
||||
prompts.map(x => x.prompt),
|
||||
finish,
|
||||
))
|
||||
}))
|
||||
|
||||
ssh.on('greeting', greeting => {
|
||||
|
|
|
@ -27,6 +27,21 @@ interface AuthMethod {
|
|||
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 {
|
||||
shell?: ClientChannel
|
||||
ssh: Client
|
||||
|
@ -36,12 +51,14 @@ export class SSHSession extends BaseSession {
|
|||
proxyCommandStream: ProxyCommandStream|null = null
|
||||
savedPassword?: string
|
||||
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
|
||||
get keyboardInteractivePrompt$ (): Observable<KeyboardInteractivePrompt> { return this.keyboardInteractivePrompt }
|
||||
|
||||
agentPath?: string
|
||||
activePrivateKey: string|null = null
|
||||
|
||||
private remainingAuthMethods: AuthMethod[] = []
|
||||
private serviceMessage = new Subject<string>()
|
||||
private keyboardInteractivePrompt = new Subject<KeyboardInteractivePrompt>()
|
||||
private keychainPasswordUsed = false
|
||||
|
||||
private passwordStorage: PasswordStorageService
|
||||
|
@ -246,6 +263,17 @@ export class SSHSession extends BaseSession {
|
|||
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> {
|
||||
this.activePrivateKey = null
|
||||
|
||||
|
|
|
@ -723,7 +723,9 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||
this.spinner.text = text
|
||||
}
|
||||
this.spinner.setSpinnerString(6)
|
||||
this.spinner.start()
|
||||
this.zone.runOutsideAngular(() => {
|
||||
this.spinner.start()
|
||||
})
|
||||
this.spinnerActive = true
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue