diff --git a/src/main/kotlin/app/termora/PtyHostTerminalTab.kt b/src/main/kotlin/app/termora/PtyHostTerminalTab.kt index 40f71c1..e46a12a 100644 --- a/src/main/kotlin/app/termora/PtyHostTerminalTab.kt +++ b/src/main/kotlin/app/termora/PtyHostTerminalTab.kt @@ -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)) diff --git a/src/main/kotlin/app/termora/SSHTerminalTab.kt b/src/main/kotlin/app/termora/SSHTerminalTab.kt index 765cc54..8ec79a9 100644 --- a/src/main/kotlin/app/termora/SSHTerminalTab.kt +++ b/src/main/kotlin/app/termora/SSHTerminalTab.kt @@ -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() diff --git a/src/main/kotlin/app/termora/SshClients.kt b/src/main/kotlin/app/termora/SshClients.kt index 1b6d0e7..6ad4b28 100644 --- a/src/main/kotlin/app/termora/SshClients.kt +++ b/src/main/kotlin/app/termora/SshClients.kt @@ -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 } diff --git a/src/main/kotlin/app/termora/keyboardinteractive/KeyboardInteractiveDialog.kt b/src/main/kotlin/app/termora/keyboardinteractive/KeyboardInteractiveDialog.kt new file mode 100644 index 0000000..fc16903 --- /dev/null +++ b/src/main/kotlin/app/termora/keyboardinteractive/KeyboardInteractiveDialog.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/keyboardinteractive/TerminalUserInteraction.kt b/src/main/kotlin/app/termora/keyboardinteractive/TerminalUserInteraction.kt new file mode 100644 index 0000000..044a452 --- /dev/null +++ b/src/main/kotlin/app/termora/keyboardinteractive/TerminalUserInteraction.kt @@ -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, + echo: BooleanArray + ): Array { + 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() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt b/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt index a32b608..ab33af4 100644 --- a/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt +++ b/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt @@ -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() }