From b8d612f1d593039aa808590c55367d1432e567ce Mon Sep 17 00:00:00 2001 From: hstyi Date: Wed, 12 Feb 2025 17:13:30 +0800 Subject: [PATCH] feat: supports one-time authorised connection (#211) --- .../termora/RequestAuthenticationDialog.kt | 133 ++++++++++++++++++ src/main/kotlin/app/termora/SSHTerminalTab.kt | 13 ++ .../termora/transport/SftpFileSystemPanel.kt | 14 +- 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/app/termora/RequestAuthenticationDialog.kt diff --git a/src/main/kotlin/app/termora/RequestAuthenticationDialog.kt b/src/main/kotlin/app/termora/RequestAuthenticationDialog.kt new file mode 100644 index 0000000..e42b99f --- /dev/null +++ b/src/main/kotlin/app/termora/RequestAuthenticationDialog.kt @@ -0,0 +1,133 @@ +package app.termora + +import app.termora.keymgr.KeyManager +import app.termora.keymgr.OhKeyPair +import com.formdev.flatlaf.extras.components.FlatComboBox +import com.jgoodies.forms.builder.FormBuilder +import com.jgoodies.forms.layout.FormLayout +import java.awt.BorderLayout +import java.awt.Component +import java.awt.Dimension +import java.awt.Window +import java.awt.event.ItemEvent +import javax.swing.* +import kotlin.math.max + +class RequestAuthenticationDialog(owner: Window) : DialogWrapper(owner) { + + private val authenticationTypeComboBox = FlatComboBox() + private val rememberCheckBox = JCheckBox("Remember") + private val passwordPanel = JPanel(BorderLayout()) + private val passwordPasswordField = OutlinePasswordField() + private val publicKeyComboBox = FlatComboBox() + private val keyManager get() = KeyManager.getInstance() + private var authentication = Authentication.No + + init { + isModal = true + title = "SSH User Authentication" + controlsVisible = false + + init() + + pack() + + size = Dimension(max(380, size.width), size.height) + + setLocationRelativeTo(null) + + publicKeyComboBox.renderer = object : DefaultListCellRenderer() { + override fun getListCellRendererComponent( + list: JList<*>?, + value: Any?, + index: Int, + isSelected: Boolean, + cellHasFocus: Boolean + ): Component { + return super.getListCellRendererComponent( + list, + if (value is OhKeyPair) value.name else value, + index, + isSelected, + cellHasFocus + ) + } + } + + for (keyPair in keyManager.getOhKeyPairs()) { + publicKeyComboBox.addItem(keyPair) + } + + authenticationTypeComboBox.addItemListener { + if (it.stateChange == ItemEvent.SELECTED) { + switchPasswordComponent() + } + } + + } + + override fun createCenterPanel(): JComponent { + authenticationTypeComboBox.addItem(AuthenticationType.Password) + authenticationTypeComboBox.addItem(AuthenticationType.PublicKey) + val formMargin = "7dlu" + val layout = FormLayout( + "left:pref, $formMargin, default:grow", + "pref, $formMargin, pref" + ) + + switchPasswordComponent() + + return FormBuilder.create().padding("$formMargin, $formMargin, $formMargin, $formMargin") + .layout(layout) + .add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, 1) + .add(authenticationTypeComboBox).xy(3, 1) + .add("${I18n.getString("termora.new-host.general.password")}:").xy(1, 3) + .add(passwordPanel).xy(3, 3) + .build() + } + + private fun switchPasswordComponent() { + passwordPanel.removeAll() + if (authenticationTypeComboBox.selectedItem == AuthenticationType.Password) { + passwordPanel.add(passwordPasswordField, BorderLayout.CENTER) + } else if (authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) { + passwordPanel.add(publicKeyComboBox, BorderLayout.CENTER) + } + passwordPanel.revalidate() + passwordPanel.repaint() + } + + override fun createSouthPanel(): JComponent? { + val box = super.createSouthPanel() ?: return null + rememberCheckBox.isFocusable = false + box.add(rememberCheckBox, 0) + return box + } + + override fun doCancelAction() { + authentication = Authentication.No + super.doCancelAction() + } + + override fun doOKAction() { + val type = authenticationTypeComboBox.selectedItem as AuthenticationType + authentication = authentication.copy( + type = type, + password = if (type == AuthenticationType.Password) String(passwordPasswordField.password) + else (publicKeyComboBox.selectedItem as OhKeyPair).id + ) + super.doOKAction() + } + + fun getAuthentication(): Authentication { + isModal = true + SwingUtilities.invokeLater { passwordPasswordField.requestFocusInWindow() } + isVisible = true + return authentication + } + + fun isRemembered(): Boolean { + return rememberCheckBox.isSelected + } + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/SSHTerminalTab.kt b/src/main/kotlin/app/termora/SSHTerminalTab.kt index e003fa1..fcc9c1b 100644 --- a/src/main/kotlin/app/termora/SSHTerminalTab.kt +++ b/src/main/kotlin/app/termora/SSHTerminalTab.kt @@ -87,12 +87,25 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) : terminal.write("SSH client is opening...\r\n") } + var host = this.host.copy(authentication = this.host.authentication.copy()) val owner = SwingUtilities.getWindowAncestor(terminalPanel) val client = SshClients.openClient(host).also { sshClient = it } client.serverKeyVerifier = DialogServerKeyVerifier(owner) // keyboard interactive client.userInteraction = TerminalUserInteraction(owner) + if (host.authentication.type == AuthenticationType.No) { + withContext(Dispatchers.Swing) { + val dialog = RequestAuthenticationDialog(owner) + val authentication = dialog.getAuthentication() + host = host.copy(authentication = authentication) + // save + if (dialog.isRemembered()) { + HostManager.getInstance().addHost(this@SSHTerminalTab.host.copy(authentication = authentication)) + } + } + } + val sessionListener = MySessionListener() val channelListener = MyChannelListener() diff --git a/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt b/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt index 78b2d6a..76935ad 100644 --- a/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt +++ b/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt @@ -104,7 +104,8 @@ class SftpFileSystemPanel( private suspend fun doConnect() { - val host = this.host ?: return + val thisHost = this.host ?: return + var host = thisHost.copy(authentication = thisHost.authentication.copy()) closeIO() @@ -114,6 +115,17 @@ class SftpFileSystemPanel( val owner = SwingUtilities.getWindowAncestor(this@SftpFileSystemPanel) client.userInteraction = TerminalUserInteraction(owner) client.serverKeyVerifier = DialogServerKeyVerifier(owner) + // 弹出授权框 + if (host.authentication.type == AuthenticationType.No) { + val dialog = RequestAuthenticationDialog(owner) + val authentication = dialog.getAuthentication() + host = host.copy(authentication = authentication) + // save + if (dialog.isRemembered()) { + HostManager.getInstance() + .addHost(host.copy(authentication = authentication)) + } + } } val session = SshClients.openSession(host, client).apply { session = this } fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)