From 8a733379a33a4d190bd6c3a839977aeb17f4a8a8 Mon Sep 17 00:00:00 2001 From: hstyi Date: Wed, 12 Feb 2025 11:45:55 +0800 Subject: [PATCH] feat: known_hosts (#206) --- src/main/kotlin/app/termora/OptionPane.kt | 3 +- src/main/kotlin/app/termora/SSHTerminalTab.kt | 6 +- src/main/kotlin/app/termora/SshClients.kt | 85 ++++++++++++++++++- .../termora/transport/SftpFileSystemPanel.kt | 5 +- src/main/resources/i18n/messages.properties | 3 + .../resources/i18n/messages_zh_CN.properties | 5 ++ .../resources/i18n/messages_zh_TW.properties | 7 ++ 7 files changed, 107 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/app/termora/OptionPane.kt b/src/main/kotlin/app/termora/OptionPane.kt index a9ec26c..926077d 100644 --- a/src/main/kotlin/app/termora/OptionPane.kt +++ b/src/main/kotlin/app/termora/OptionPane.kt @@ -6,8 +6,6 @@ import com.formdev.flatlaf.util.SystemInfo import com.jetbrains.JBR import kotlinx.coroutines.* import kotlinx.coroutines.swing.Swing -import org.apache.commons.lang3.StringUtils -import org.jdesktop.swingx.JXLabel import java.awt.BorderLayout import java.awt.Component import java.awt.Desktop @@ -57,6 +55,7 @@ object OptionPane { pane.selectInitialValue() } }) + dialog.setLocationRelativeTo(parentComponent) dialog.isVisible = true dialog.dispose() val selectedValue = pane.value diff --git a/src/main/kotlin/app/termora/SSHTerminalTab.kt b/src/main/kotlin/app/termora/SSHTerminalTab.kt index c1a2347..e003fa1 100644 --- a/src/main/kotlin/app/termora/SSHTerminalTab.kt +++ b/src/main/kotlin/app/termora/SSHTerminalTab.kt @@ -29,7 +29,7 @@ import org.apache.sshd.common.session.SessionListener.Event import org.apache.sshd.common.util.net.SshdSocketAddress import org.slf4j.LoggerFactory import java.nio.charset.StandardCharsets -import java.util.EventObject +import java.util.* import javax.swing.JComponent import javax.swing.SwingUtilities @@ -87,9 +87,11 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) : terminal.write("SSH client is opening...\r\n") } + val owner = SwingUtilities.getWindowAncestor(terminalPanel) val client = SshClients.openClient(host).also { sshClient = it } + client.serverKeyVerifier = DialogServerKeyVerifier(owner) // keyboard interactive - client.userInteraction = TerminalUserInteraction(SwingUtilities.getWindowAncestor(terminalPanel)) + client.userInteraction = TerminalUserInteraction(owner) 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 27b6631..d655007 100644 --- a/src/main/kotlin/app/termora/SshClients.kt +++ b/src/main/kotlin/app/termora/SshClients.kt @@ -2,14 +2,20 @@ package app.termora import app.termora.keymgr.OhKeyPairKeyPairProvider import app.termora.terminal.TerminalSize +import org.apache.commons.lang3.StringUtils import org.apache.sshd.client.ClientBuilder import org.apache.sshd.client.SshClient import org.apache.sshd.client.channel.ChannelShell import org.apache.sshd.client.config.hosts.HostConfigEntryResolver +import org.apache.sshd.client.config.hosts.KnownHostEntry import org.apache.sshd.client.kex.DHGClient +import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier +import org.apache.sshd.client.keyverifier.ModifiedServerKeyAcceptor +import org.apache.sshd.client.keyverifier.ServerKeyVerifier import org.apache.sshd.client.session.ClientSession import org.apache.sshd.common.SshException import org.apache.sshd.common.channel.PtyChannelConfiguration +import org.apache.sshd.common.config.keys.KeyUtils import org.apache.sshd.common.global.KeepAliveHandler import org.apache.sshd.common.kex.BuiltinDHFactories import org.apache.sshd.common.keyprovider.KeyIdentityProvider @@ -22,9 +28,16 @@ import org.eclipse.jgit.transport.CredentialsProvider import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider import org.eclipse.jgit.transport.sshd.ProxyData import org.slf4j.LoggerFactory +import java.awt.Window import java.net.InetSocketAddress import java.net.Proxy +import java.net.SocketAddress +import java.nio.file.Paths +import java.security.PublicKey import java.time.Duration +import java.util.concurrent.atomic.AtomicBoolean +import javax.swing.JOptionPane +import javax.swing.SwingUtilities import kotlin.math.max object SshClients { @@ -191,4 +204,74 @@ object SshClients { sshClient.start() return sshClient } -} \ No newline at end of file +} + + +private class MyDialogServerKeyVerifier(private val owner: Window) : ServerKeyVerifier, ModifiedServerKeyAcceptor { + override fun verifyServerKey( + clientSession: ClientSession, + remoteAddress: SocketAddress, + serverKey: PublicKey + ): Boolean { + val result = AtomicBoolean(false) + + SwingUtilities.invokeAndWait { + result.set( + OptionPane.showConfirmDialog( + parentComponent = owner, + message = I18n.getString( + "termora.host.verify-server-key", + remoteAddress.toString().replace("/", StringUtils.EMPTY), + KeyUtils.getKeyType(serverKey), + KeyUtils.getFingerPrint(serverKey) + ), + optionType = JOptionPane.OK_CANCEL_OPTION, + messageType = JOptionPane.WARNING_MESSAGE, + ) == JOptionPane.OK_OPTION + ) + } + + return result.get() + } + + override fun acceptModifiedServerKey( + clientSession: ClientSession?, + remoteAddress: SocketAddress?, + entry: KnownHostEntry?, + expected: PublicKey?, + actual: PublicKey? + ): Boolean { + val result = AtomicBoolean(false) + + SwingUtilities.invokeAndWait { + result.set( + OptionPane.showConfirmDialog( + parentComponent = owner, + message = I18n.getString( + "termora.host.modified-server-key", + remoteAddress.toString().replace("/", StringUtils.EMPTY), + KeyUtils.getKeyType(expected), + KeyUtils.getFingerPrint(expected), + KeyUtils.getKeyType(actual), + KeyUtils.getFingerPrint(actual), + ), + optionType = JOptionPane.OK_CANCEL_OPTION, + messageType = JOptionPane.WARNING_MESSAGE, + ) == JOptionPane.OK_OPTION + ) + } + + return result.get() + } +} + +class DialogServerKeyVerifier( + owner: Window, +) : KnownHostsServerKeyVerifier( + MyDialogServerKeyVerifier(owner), + Paths.get(Application.getBaseDataDir().absolutePath, "known_hosts") +) { + init { + modifiedServerKeyAcceptor = delegateVerifier as ModifiedServerKeyAcceptor + } +} diff --git a/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt b/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt index ab33af4..c871900 100644 --- a/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt +++ b/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt @@ -115,8 +115,9 @@ class SftpFileSystemPanel( try { val client = SshClients.openClient(host).apply { client = this } withContext(Dispatchers.Swing) { - client.userInteraction = - TerminalUserInteraction(SwingUtilities.getWindowAncestor(this@SftpFileSystemPanel)) + val owner = SwingUtilities.getWindowAncestor(this@SftpFileSystemPanel) + client.userInteraction = TerminalUserInteraction(owner) + client.serverKeyVerifier = DialogServerKeyVerifier(owner) } val session = SshClients.openSession(host, client).apply { session = this } fileSystem = SftpClientFactory.instance().createSftpFileSystem(session) diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 65bc687..1e5d3cd 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -38,6 +38,9 @@ termora.doorman.mnemonic.title=Enter 12 mnemonic words termora.doorman.mnemonic.incorrect=Incorrect mnemonic +# Hosts +termora.host.verify-server-key=Host [{0}] key has been changed!

{1} key fingerprint is {2}

Are you sure you want to continue connecting? +termora.host.modified-server-key=HOST [{0}] IDENTIFICATION HAS CHANGED!

Expected: {1} key fingerprint is {2}

Actual: {3} key fingerprint is {4}

Are you sure you want to continue connecting? # Settings diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties index 5d258a7..f80974a 100644 --- a/src/main/resources/i18n/messages_zh_CN.properties +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -36,6 +36,11 @@ termora.doorman.mnemonic.title=输入 12 个助记词 termora.doorman.mnemonic.incorrect=助记词错误 +# Hosts +termora.host.verify-server-key=主机 [{0}] 密钥已经改变!

{1} 的指纹 {2}

你确定要继续连接吗? +termora.host.modified-server-key=主机 [{0}] 身份已发生变化!

期待: {1} 的指纹 {2}

实际: {3} 的指纹 {4}

你确定要继续连接吗? + + termora.setting=设置 termora.settings.restart.title=重启 termora.settings.restart.message=设置修改将在重启后生效 diff --git a/src/main/resources/i18n/messages_zh_TW.properties b/src/main/resources/i18n/messages_zh_TW.properties index 28d8b03..90db016 100644 --- a/src/main/resources/i18n/messages_zh_TW.properties +++ b/src/main/resources/i18n/messages_zh_TW.properties @@ -35,6 +35,13 @@ termora.doorman.mnemonic-data-corrupted=無法從助記詞解密數據,資料 termora.doorman.mnemonic.title=輸入 12 個助記詞 termora.doorman.mnemonic.incorrect=助記詞錯誤 + + +# Hosts +termora.host.verify-server-key=主機 [{0}] 金鑰已經改變!

{1} 的指紋 {2}

你確定要繼續連線嗎? +termora.host.modified-server-key=主機 [{0}] 身分已變更!

期待: {1} 的指紋 {2}

實際: {3} 的指紋 {4}

你確定要繼續連線嗎? + + termora.setting=設定 termora.settings.restart.title=重啟 termora.settings.restart.message=設定修改將在重新啟動後生效