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=設定修改將在重新啟動後生效