feat: support keyboard-interactive

This commit is contained in:
hstyi
2025-01-07 20:18:57 +08:00
committed by hstyi
parent ffcb4d028e
commit 3e5df2161b
6 changed files with 143 additions and 1 deletions

View File

@@ -61,6 +61,10 @@ abstract class PtyHostTerminalTab(
if (log.isErrorEnabled) {
log.error(e.message, e)
}
// 失败关闭
stop()
withContext(Dispatchers.Swing) {
terminal.write("\r\n${ControlCharacters.ESC}[31m")
terminal.write(ExceptionUtils.getRootCauseMessage(e))

View File

@@ -1,6 +1,7 @@
package app.termora
import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
import app.termora.keyboardinteractive.TerminalUserInteraction
import app.termora.terminal.ControlCharacters
import app.termora.terminal.DataKey
import app.termora.terminal.PtyConnector
@@ -24,6 +25,7 @@ import org.apache.sshd.common.util.net.SshdSocketAddress
import org.slf4j.LoggerFactory
import java.nio.charset.StandardCharsets
import javax.swing.JComponent
import javax.swing.SwingUtilities
class SSHTerminalTab(host: Host) : PtyHostTerminalTab(host) {
@@ -76,6 +78,9 @@ class SSHTerminalTab(host: Host) : PtyHostTerminalTab(host) {
}
val client = SshClients.openClient(host).also { sshClient = it }
// keyboard interactive
client.userInteraction = TerminalUserInteraction(SwingUtilities.getWindowAncestor(terminalPanel))
val sessionListener = MySessionListener()
val channelListener = MyChannelListener()

View File

@@ -64,9 +64,12 @@ object SshClients {
} else if (host.authentication.type == AuthenticationType.PublicKey) {
session.keyIdentityProvider = OhKeyPairKeyPairProvider(host.authentication.password)
}
if (!session.auth().verify(timeout).await(timeout)) {
val verifyTimeout = Duration.ofSeconds(timeout.seconds * 5)
if (!session.auth().verify(verifyTimeout).await(verifyTimeout)) {
throw SshException("Authentication failed")
}
return session
}

View File

@@ -0,0 +1,72 @@
package app.termora.keyboardinteractive
import app.termora.DialogWrapper
import app.termora.I18n
import app.termora.OutlinePasswordField
import app.termora.OutlineTextField
import com.formdev.flatlaf.FlatClientProperties
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import org.apache.commons.lang3.StringUtils
import java.awt.Dimension
import java.awt.Window
import javax.swing.JComponent
import javax.swing.text.JTextComponent
class KeyboardInteractiveDialog(
owner: Window,
private val prompt: String,
echo: Boolean
) : DialogWrapper(owner) {
private val textField = (if (echo) OutlineTextField() else OutlinePasswordField()) as JTextComponent
init {
isModal = true
isResizable = true
controlsVisible = false
title = I18n.getString("termora.new-host.title")
init()
pack()
size = Dimension(300, size.height)
setLocationRelativeTo(null)
}
override fun createCenterPanel(): JComponent {
val formMargin = "4dlu"
val layout = FormLayout(
"left:pref, $formMargin, default:grow",
"pref, $formMargin, pref, $formMargin"
)
var rows = 1
val step = 2
return FormBuilder.create().layout(layout).padding("$formMargin, $formMargin, 0, $formMargin")
.add(prompt).xy(1, rows)
.add(textField).xy(3, rows).apply { rows += step }
.build()
}
override fun doCancelAction() {
textField.text = StringUtils.EMPTY
super.doCancelAction()
}
override fun doOKAction() {
if (textField.text.isBlank()) {
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
textField.requestFocusInWindow()
return
}
super.doOKAction()
}
fun getText(): String {
isModal = true
isVisible = true
return textField.text
}
}

View File

@@ -0,0 +1,53 @@
package app.termora.keyboardinteractive
import org.apache.commons.lang3.StringUtils
import org.apache.sshd.client.auth.keyboard.UserInteraction
import org.apache.sshd.client.session.ClientSession
import java.awt.Window
import javax.swing.SwingUtilities
class TerminalUserInteraction(
private val owner: Window
) : UserInteraction {
override fun interactive(
session: ClientSession?,
name: String?,
instruction: String?,
lang: String?,
prompt: Array<out String>,
echo: BooleanArray
): Array<String> {
val passwords = Array(prompt.size) { StringUtils.EMPTY }
SwingUtilities.invokeAndWait {
for (i in prompt.indices) {
val dialog = KeyboardInteractiveDialog(
owner,
prompt[i],
true
)
dialog.title = instruction ?: name ?: StringUtils.EMPTY
passwords[i] = dialog.getText()
if (passwords[i].isBlank()) {
break
}
}
}
if (passwords.last().isBlank()) {
throw IllegalStateException("User interaction was cancelled.")
}
if (passwords.all { it.isEmpty() }) {
return emptyArray()
}
return passwords
}
override fun getUpdatedPassword(session: ClientSession?, prompt: String?, lang: String?): String {
throw UnsupportedOperationException()
}
}

View File

@@ -1,6 +1,7 @@
package app.termora.transport
import app.termora.*
import app.termora.keyboardinteractive.TerminalUserInteraction
import com.formdev.flatlaf.icons.FlatOptionPaneErrorIcon
import com.formdev.flatlaf.icons.FlatOptionPaneInformationIcon
import com.jgoodies.forms.builder.FormBuilder
@@ -113,6 +114,10 @@ class SftpFileSystemPanel(
try {
val client = SshClients.openClient(host).apply { client = this }
withContext(Dispatchers.Swing) {
client.userInteraction =
TerminalUserInteraction(SwingUtilities.getWindowAncestor(this@SftpFileSystemPanel))
}
val session = SshClients.openSession(host, client).apply { session = this }
fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)
session.addCloseFutureListener { onClose() }