mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 10:22:58 +08:00
feat: known_hosts (#206)
This commit is contained in:
@@ -6,8 +6,6 @@ import com.formdev.flatlaf.util.SystemInfo
|
|||||||
import com.jetbrains.JBR
|
import com.jetbrains.JBR
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import org.jdesktop.swingx.JXLabel
|
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.Desktop
|
import java.awt.Desktop
|
||||||
@@ -57,6 +55,7 @@ object OptionPane {
|
|||||||
pane.selectInitialValue()
|
pane.selectInitialValue()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
dialog.setLocationRelativeTo(parentComponent)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
dialog.dispose()
|
dialog.dispose()
|
||||||
val selectedValue = pane.value
|
val selectedValue = pane.value
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import org.apache.sshd.common.session.SessionListener.Event
|
|||||||
import org.apache.sshd.common.util.net.SshdSocketAddress
|
import org.apache.sshd.common.util.net.SshdSocketAddress
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.util.EventObject
|
import java.util.*
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
@@ -87,9 +87,11 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
terminal.write("SSH client is opening...\r\n")
|
terminal.write("SSH client is opening...\r\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val owner = SwingUtilities.getWindowAncestor(terminalPanel)
|
||||||
val client = SshClients.openClient(host).also { sshClient = it }
|
val client = SshClients.openClient(host).also { sshClient = it }
|
||||||
|
client.serverKeyVerifier = DialogServerKeyVerifier(owner)
|
||||||
// keyboard interactive
|
// keyboard interactive
|
||||||
client.userInteraction = TerminalUserInteraction(SwingUtilities.getWindowAncestor(terminalPanel))
|
client.userInteraction = TerminalUserInteraction(owner)
|
||||||
|
|
||||||
val sessionListener = MySessionListener()
|
val sessionListener = MySessionListener()
|
||||||
val channelListener = MyChannelListener()
|
val channelListener = MyChannelListener()
|
||||||
|
|||||||
@@ -2,14 +2,20 @@ package app.termora
|
|||||||
|
|
||||||
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||||
import app.termora.terminal.TerminalSize
|
import app.termora.terminal.TerminalSize
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.sshd.client.ClientBuilder
|
import org.apache.sshd.client.ClientBuilder
|
||||||
import org.apache.sshd.client.SshClient
|
import org.apache.sshd.client.SshClient
|
||||||
import org.apache.sshd.client.channel.ChannelShell
|
import org.apache.sshd.client.channel.ChannelShell
|
||||||
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver
|
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.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.client.session.ClientSession
|
||||||
import org.apache.sshd.common.SshException
|
import org.apache.sshd.common.SshException
|
||||||
import org.apache.sshd.common.channel.PtyChannelConfiguration
|
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.global.KeepAliveHandler
|
||||||
import org.apache.sshd.common.kex.BuiltinDHFactories
|
import org.apache.sshd.common.kex.BuiltinDHFactories
|
||||||
import org.apache.sshd.common.keyprovider.KeyIdentityProvider
|
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.IdentityPasswordProvider
|
||||||
import org.eclipse.jgit.transport.sshd.ProxyData
|
import org.eclipse.jgit.transport.sshd.ProxyData
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.Window
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Proxy
|
import java.net.Proxy
|
||||||
|
import java.net.SocketAddress
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.security.PublicKey
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import javax.swing.JOptionPane
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
object SshClients {
|
object SshClients {
|
||||||
@@ -192,3 +205,73 @@ object SshClients {
|
|||||||
return sshClient
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -115,8 +115,9 @@ class SftpFileSystemPanel(
|
|||||||
try {
|
try {
|
||||||
val client = SshClients.openClient(host).apply { client = this }
|
val client = SshClients.openClient(host).apply { client = this }
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
client.userInteraction =
|
val owner = SwingUtilities.getWindowAncestor(this@SftpFileSystemPanel)
|
||||||
TerminalUserInteraction(SwingUtilities.getWindowAncestor(this@SftpFileSystemPanel))
|
client.userInteraction = TerminalUserInteraction(owner)
|
||||||
|
client.serverKeyVerifier = DialogServerKeyVerifier(owner)
|
||||||
}
|
}
|
||||||
val session = SshClients.openSession(host, client).apply { session = this }
|
val session = SshClients.openSession(host, client).apply { session = this }
|
||||||
fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)
|
fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ termora.doorman.mnemonic.title=Enter 12 mnemonic words
|
|||||||
termora.doorman.mnemonic.incorrect=Incorrect mnemonic
|
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
|
# Settings
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ termora.doorman.mnemonic.title=输入 12 个助记词
|
|||||||
termora.doorman.mnemonic.incorrect=助记词错误
|
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.setting=设置
|
||||||
termora.settings.restart.title=重启
|
termora.settings.restart.title=重启
|
||||||
termora.settings.restart.message=设置修改将在重启后生效
|
termora.settings.restart.message=设置修改将在重启后生效
|
||||||
|
|||||||
@@ -35,6 +35,13 @@ termora.doorman.mnemonic-data-corrupted=無法從助記詞解密數據,資料
|
|||||||
termora.doorman.mnemonic.title=輸入 12 個助記詞
|
termora.doorman.mnemonic.title=輸入 12 個助記詞
|
||||||
termora.doorman.mnemonic.incorrect=助記詞錯誤
|
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.setting=設定
|
||||||
termora.settings.restart.title=重啟
|
termora.settings.restart.title=重啟
|
||||||
termora.settings.restart.message=設定修改將在重新啟動後生效
|
termora.settings.restart.message=設定修改將在重新啟動後生效
|
||||||
|
|||||||
Reference in New Issue
Block a user