feat: known_hosts (#206)

This commit is contained in:
hstyi
2025-02-12 11:45:55 +08:00
committed by GitHub
parent e5f854dfcd
commit 8a733379a3
7 changed files with 107 additions and 7 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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
}
}
}
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
}
}

View File

@@ -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)

View File

@@ -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!<br/><br/>{1} key fingerprint is {2}<br/><br/>Are you sure you want to continue connecting?
termora.host.modified-server-key=HOST [{0}] IDENTIFICATION HAS CHANGED!<br/><br/>Expected: {1} key fingerprint is {2}<br/><br/>Actual: {3} key fingerprint is {4}<br/><br/>Are you sure you want to continue connecting?
# Settings

View File

@@ -36,6 +36,11 @@ termora.doorman.mnemonic.title=输入 12 个助记词
termora.doorman.mnemonic.incorrect=助记词错误
# Hosts
termora.host.verify-server-key=主机 [{0}] 密钥已经改变!<br/><br/>{1} 的指纹 {2}<br/><br/>你确定要继续连接吗?
termora.host.modified-server-key=主机 [{0}] 身份已发生变化!<br/><br/>期待: {1} 的指纹 {2}<br/><br/>实际: {3} 的指纹 {4}<br/><br/>你确定要继续连接吗?
termora.setting=设置
termora.settings.restart.title=重启
termora.settings.restart.message=设置修改将在重启后生效

View File

@@ -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}] 金鑰已經改變!<br/><br/>{1} 的指紋 {2}<br/><br/>你確定要繼續連線嗎?
termora.host.modified-server-key=主機 [{0}] 身分已變更!<br/><br/>期待: {1} 的指紋 {2}<br/><br/>實際: {3} 的指紋 {4}<br/><br/>你確定要繼續連線嗎?
termora.setting=設定
termora.settings.restart.title=重啟
termora.settings.restart.message=設定修改將在重新啟動後生效