feat: SSH support ssh-agent (#433)

This commit is contained in:
hstyi
2025-03-30 12:48:14 +08:00
committed by GitHub
parent c714f33a44
commit 283404b6b9
12 changed files with 183 additions and 103 deletions

View File

@@ -166,6 +166,10 @@ org.eclipse.jgit.ssh.apache
Eclipse Distribution License Eclipse Distribution License
https://www.eclipse.org/org/documents/edl-v10.php https://www.eclipse.org/org/documents/edl-v10.php
org.eclipse.jgit.ssh.apache.agent
Eclipse Distribution License
https://www.eclipse.org/org/documents/edl-v10.php
org.eclipse.jgit org.eclipse.jgit
Eclipse Distribution License Eclipse Distribution License
https://www.eclipse.org/org/documents/edl-v10.php https://www.eclipse.org/org/documents/edl-v10.php

View File

@@ -104,6 +104,7 @@ dependencies {
implementation(libs.commonmark) implementation(libs.commonmark)
implementation(libs.jgit) implementation(libs.jgit)
implementation(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") } implementation(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
implementation(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
implementation(libs.jnafilechooser) implementation(libs.jnafilechooser)
implementation(libs.xodus.vfs) implementation(libs.xodus.vfs)
implementation(libs.xodus.openAPI) implementation(libs.xodus.openAPI)

View File

@@ -80,6 +80,7 @@ sshd-core = { module = "org.apache.sshd:sshd-core", version.ref = "sshd-core" }
jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" } jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" }
commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" } commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" }
jgit-sshd = { module = "org.eclipse.jgit:org.eclipse.jgit.ssh.apache", version.ref = "jgit" } jgit-sshd = { module = "org.eclipse.jgit:org.eclipse.jgit.ssh.apache", version.ref = "jgit" }
jgit-agent = { module = "org.eclipse.jgit:org.eclipse.jgit.ssh.apache.agent", version.ref = "jgit" }
xodus-openAPI = { module = "org.jetbrains.xodus:xodus-openAPI", version.ref = "xodus" } xodus-openAPI = { module = "org.jetbrains.xodus:xodus-openAPI", version.ref = "xodus" }
xodus-environment = { module = "org.jetbrains.xodus:xodus-environment", version.ref = "xodus" } xodus-environment = { module = "org.jetbrains.xodus:xodus-environment", version.ref = "xodus" }
xodus-vfs = { module = "org.jetbrains.xodus:xodus-vfs", version.ref = "xodus" } xodus-vfs = { module = "org.jetbrains.xodus:xodus-vfs", version.ref = "xodus" }

View File

@@ -1,5 +1,6 @@
package app.termora package app.termora
@Suppress("CascadeIf")
class EditHostOptionsPane(private val host: Host) : HostOptionsPane() { class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
init { init {
generalOption.portTextField.value = host.port generalOption.portTextField.value = host.port
@@ -13,6 +14,8 @@ class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
generalOption.passwordTextField.text = host.authentication.password generalOption.passwordTextField.text = host.authentication.password
} else if (host.authentication.type == AuthenticationType.PublicKey) { } else if (host.authentication.type == AuthenticationType.PublicKey) {
generalOption.publicKeyComboBox.selectedItem = host.authentication.password generalOption.publicKeyComboBox.selectedItem = host.authentication.password
} else if (host.authentication.type == AuthenticationType.SSHAgent) {
generalOption.sshAgentComboBox.selectedItem = host.authentication.password
} }
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type

View File

@@ -38,6 +38,7 @@ enum class AuthenticationType {
No, No,
Password, Password,
PublicKey, PublicKey,
SSHAgent,
KeyboardInteractive, KeyboardInteractive,
} }

View File

@@ -6,6 +6,7 @@ import com.fazecast.jSerialComm.SerialPort
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatComboBox import com.formdev.flatlaf.extras.components.FlatComboBox
import com.formdev.flatlaf.ui.FlatTextBorder import com.formdev.flatlaf.ui.FlatTextBorder
import com.formdev.flatlaf.util.SystemInfo
import com.jgoodies.forms.builder.FormBuilder import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout import com.jgoodies.forms.layout.FormLayout
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -14,6 +15,9 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.eclipse.jgit.internal.transport.sshd.agent.connector.PageantConnector
import org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixDomainSocketConnector
import org.eclipse.jgit.internal.transport.sshd.agent.connector.WinPipeConnector
import java.awt.* import java.awt.*
import java.awt.event.* import java.awt.event.*
import java.nio.charset.Charset import java.nio.charset.Charset
@@ -21,7 +25,7 @@ import javax.swing.*
import javax.swing.table.DefaultTableCellRenderer import javax.swing.table.DefaultTableCellRenderer
import javax.swing.table.DefaultTableModel import javax.swing.table.DefaultTableModel
@Suppress("CascadeIf")
open class HostOptionsPane : OptionsPane() { open class HostOptionsPane : OptionsPane() {
protected val tunnelingOption = TunnelingOption() protected val tunnelingOption = TunnelingOption()
protected val generalOption = GeneralOption() protected val generalOption = GeneralOption()
@@ -52,18 +56,23 @@ open class HostOptionsPane : OptionsPane() {
val port = (generalOption.portTextField.value ?: 22) as Int val port = (generalOption.portTextField.value ?: 22) as Int
var authentication = Authentication.No var authentication = Authentication.No
var proxy = Proxy.No var proxy = Proxy.No
val authenticationType = generalOption.authenticationTypeComboBox.selectedItem as AuthenticationType
if (authenticationType == AuthenticationType.Password) {
if (generalOption.authenticationTypeComboBox.selectedItem == AuthenticationType.Password) {
authentication = authentication.copy( authentication = authentication.copy(
type = AuthenticationType.Password, type = authenticationType,
password = String(generalOption.passwordTextField.password) password = String(generalOption.passwordTextField.password)
) )
} else if (generalOption.authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) { } else if (authenticationType == AuthenticationType.PublicKey) {
authentication = authentication.copy( authentication = authentication.copy(
type = AuthenticationType.PublicKey, type = authenticationType,
password = generalOption.publicKeyComboBox.selectedItem?.toString() ?: StringUtils.EMPTY password = generalOption.publicKeyComboBox.selectedItem?.toString() ?: StringUtils.EMPTY
) )
} else if (authenticationType == AuthenticationType.SSHAgent) {
authentication = authentication.copy(
type = authenticationType,
password = generalOption.sshAgentComboBox.selectedItem?.toString() ?: StringUtils.EMPTY
)
} }
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) { if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
@@ -200,6 +209,7 @@ open class HostOptionsPane : OptionsPane() {
private val passwordPanel = JPanel(BorderLayout()) private val passwordPanel = JPanel(BorderLayout())
private val chooseKeyBtn = JButton(Icons.greyKey) private val chooseKeyBtn = JButton(Icons.greyKey)
val passwordTextField = OutlinePasswordField(255) val passwordTextField = OutlinePasswordField(255)
val sshAgentComboBox = OutlineComboBox<String>()
val publicKeyComboBox = OutlineComboBox<String>() val publicKeyComboBox = OutlineComboBox<String>()
val remarkTextArea = FixedLengthTextArea(512) val remarkTextArea = FixedLengthTextArea(512)
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>() val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
@@ -215,6 +225,10 @@ open class HostOptionsPane : OptionsPane() {
publicKeyComboBox.isEditable = false publicKeyComboBox.isEditable = false
chooseKeyBtn.isFocusable = false chooseKeyBtn.isFocusable = false
// 只有 Windows 允许修改
sshAgentComboBox.isEditable = SystemInfo.isWindows
sshAgentComboBox.isEnabled = SystemInfo.isWindows
protocolTypeComboBox.renderer = object : DefaultListCellRenderer() { protocolTypeComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent( override fun getListCellRendererComponent(
list: JList<*>?, list: JList<*>?,
@@ -294,6 +308,17 @@ open class HostOptionsPane : OptionsPane() {
authenticationTypeComboBox.addItem(AuthenticationType.No) authenticationTypeComboBox.addItem(AuthenticationType.No)
authenticationTypeComboBox.addItem(AuthenticationType.Password) authenticationTypeComboBox.addItem(AuthenticationType.Password)
authenticationTypeComboBox.addItem(AuthenticationType.PublicKey) authenticationTypeComboBox.addItem(AuthenticationType.PublicKey)
authenticationTypeComboBox.addItem(AuthenticationType.SSHAgent)
if (SystemInfo.isWindows) {
// 不要修改 addItem 的顺序,因为第一个是默认的
sshAgentComboBox.addItem(PageantConnector.DESCRIPTOR.identityAgent)
sshAgentComboBox.addItem(WinPipeConnector.DESCRIPTOR.identityAgent)
sshAgentComboBox.placeholderText = PageantConnector.DESCRIPTOR.identityAgent
} else {
sshAgentComboBox.addItem(UnixDomainSocketConnector.DESCRIPTOR.identityAgent)
sshAgentComboBox.placeholderText = UnixDomainSocketConnector.DESCRIPTOR.identityAgent
}
authenticationTypeComboBox.selectedItem = AuthenticationType.Password authenticationTypeComboBox.selectedItem = AuthenticationType.Password
@@ -457,6 +482,8 @@ open class HostOptionsPane : OptionsPane() {
.add(chooseKeyBtn).xy(3, 1) .add(chooseKeyBtn).xy(3, 1)
.build(), BorderLayout.CENTER .build(), BorderLayout.CENTER
) )
} else if (authenticationTypeComboBox.selectedItem == AuthenticationType.SSHAgent) {
passwordPanel.add(sshAgentComboBox, BorderLayout.CENTER)
} else { } else {
passwordPanel.add(passwordTextField, BorderLayout.CENTER) passwordPanel.add(passwordTextField, BorderLayout.CENTER)
} }

View File

@@ -16,7 +16,7 @@ import kotlin.math.max
class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(owner) { class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(owner) {
private val authenticationTypeComboBox = FlatComboBox<AuthenticationType>() private val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
private val rememberCheckBox = JCheckBox("Remember") private val rememberCheckBox = JCheckBox(I18n.getString("termora.new-host.general.remember"))
private val passwordPanel = JPanel(BorderLayout()) private val passwordPanel = JPanel(BorderLayout())
private val passwordPasswordField = OutlinePasswordField() private val passwordPasswordField = OutlinePasswordField()
private val usernameTextField = OutlineTextField() private val usernameTextField = OutlineTextField()

View File

@@ -1,14 +1,17 @@
package app.termora package app.termora
import app.termora.keyboardinteractive.TerminalUserInteraction import app.termora.keyboardinteractive.TerminalUserInteraction
import app.termora.keymgr.KeyManager
import app.termora.keymgr.OhKeyPairKeyPairProvider import app.termora.keymgr.OhKeyPairKeyPairProvider
import app.termora.terminal.TerminalSize import app.termora.terminal.TerminalSize
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.util.FontUtils
import com.formdev.flatlaf.util.SystemInfo
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils 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.auth.password.PasswordIdentityProvider
import org.apache.sshd.client.auth.password.UserAuthPasswordFactory import org.apache.sshd.client.auth.password.UserAuthPasswordFactory
import org.apache.sshd.client.channel.ChannelShell import org.apache.sshd.client.channel.ChannelShell
import org.apache.sshd.client.channel.ClientChannelEvent import org.apache.sshd.client.channel.ClientChannelEvent
@@ -21,23 +24,30 @@ import org.apache.sshd.client.keyverifier.ModifiedServerKeyAcceptor
import org.apache.sshd.client.keyverifier.ServerKeyVerifier 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.AttributeRepository import org.apache.sshd.common.AttributeRepository
import org.apache.sshd.common.SshConstants
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.KeyRandomArt
import org.apache.sshd.common.config.keys.KeyUtils 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
import org.apache.sshd.common.session.SessionContext
import org.apache.sshd.common.util.net.SshdSocketAddress import org.apache.sshd.common.util.net.SshdSocketAddress
import org.apache.sshd.core.CoreModuleProperties import org.apache.sshd.core.CoreModuleProperties
import org.apache.sshd.server.forward.AcceptAllForwardingFilter import org.apache.sshd.server.forward.AcceptAllForwardingFilter
import org.apache.sshd.server.forward.RejectAllForwardingFilter import org.apache.sshd.server.forward.RejectAllForwardingFilter
import org.eclipse.jgit.internal.transport.sshd.JGitClientSession import org.eclipse.jgit.internal.transport.sshd.JGitClientSession
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient import org.eclipse.jgit.internal.transport.sshd.JGitSshClient
import org.eclipse.jgit.internal.transport.sshd.agent.JGitSshAgentFactory
import org.eclipse.jgit.internal.transport.sshd.agent.connector.PageantConnector
import org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixDomainSocketConnector
import org.eclipse.jgit.transport.CredentialsProvider import org.eclipse.jgit.transport.CredentialsProvider
import org.eclipse.jgit.transport.SshConstants.IDENTITY_AGENT
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.eclipse.jgit.transport.sshd.agent.ConnectorFactory
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.awt.Font
import java.awt.Window import java.awt.Window
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.net.InetSocketAddress import java.net.InetSocketAddress
@@ -45,15 +55,15 @@ import java.net.Proxy
import java.net.SocketAddress import java.net.SocketAddress
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import java.security.KeyPair
import java.security.PublicKey import java.security.PublicKey
import java.time.Duration import java.time.Duration
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.JOptionPane import java.util.concurrent.atomic.AtomicReference
import javax.swing.SwingUtilities import javax.swing.*
import kotlin.math.max import kotlin.math.max
@Suppress("CascadeIf")
object SshClients { object SshClients {
val HOST_KEY = AttributeRepository.AttributeKey<Host>() val HOST_KEY = AttributeRepository.AttributeKey<Host>()
@@ -190,6 +200,16 @@ object SshClients {
entry.hostName = host.host entry.hostName = host.host
entry.setProperty("Middleware", middleware.toString()) entry.setProperty("Middleware", middleware.toString())
// ssh-agent
if (host.authentication.type == AuthenticationType.SSHAgent) {
if (host.authentication.password.isNotBlank())
entry.setProperty(IDENTITY_AGENT, host.authentication.password)
else if (SystemInfo.isWindows)
entry.setProperty(IDENTITY_AGENT, PageantConnector.DESCRIPTOR.identityAgent)
else
entry.setProperty(IDENTITY_AGENT, UnixDomainSocketConnector.DESCRIPTOR.identityAgent)
}
val session = client.connect(entry).verify(timeout).session val session = client.connect(entry).verify(timeout).session
if (host.authentication.type == AuthenticationType.Password) { if (host.authentication.type == AuthenticationType.Password) {
session.addPasswordIdentity(host.authentication.password) session.addPasswordIdentity(host.authentication.password)
@@ -197,21 +217,16 @@ object SshClients {
session.keyIdentityProvider = OhKeyPairKeyPairProvider(host.authentication.password) session.keyIdentityProvider = OhKeyPairKeyPairProvider(host.authentication.password)
} }
val owner = client.properties["owner"] as Window? try {
if (owner != null) { if (!session.auth().verify(timeout).await(timeout)) {
val identityProvider = IdentityProvider(host, owner) throw SshException("Authentication failed")
session.passwordIdentityProvider = identityProvider
val combinedKeyIdentityProvider = CombinedKeyIdentityProvider()
if (session.keyIdentityProvider != null) {
combinedKeyIdentityProvider.addKeyKeyIdentityProvider(session.keyIdentityProvider)
} }
combinedKeyIdentityProvider.addKeyKeyIdentityProvider(identityProvider) } catch (e: Exception) {
session.keyIdentityProvider = combinedKeyIdentityProvider if (e !is SshException || e.disconnectCode != SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) throw e
} val owner = client.properties["owner"] as Window? ?: throw e
val authentication = ask(host, owner) ?: throw e
val verifyTimeout = Duration.ofSeconds(timeout.seconds * 5) if (authentication.type == AuthenticationType.No) throw e
if (!session.auth().verify(verifyTimeout).await(verifyTimeout)) { return doOpenSession(host.copy(authentication = authentication), client)
throw SshException("Authentication failed")
} }
session.setAttribute(HOST_KEY, host) session.setAttribute(HOST_KEY, host)
@@ -299,7 +314,11 @@ object SshClients {
sshClient.keyIdentityProvider = KeyIdentityProvider { mutableListOf() } sshClient.keyIdentityProvider = KeyIdentityProvider { mutableListOf() }
// 设置优先级 // 设置优先级
if (host.authentication.type == AuthenticationType.PublicKey) { if (host.authentication.type == AuthenticationType.PublicKey || host.authentication.type == AuthenticationType.SSHAgent) {
if (host.authentication.type == AuthenticationType.SSHAgent) {
// ssh-agent
sshClient.agentFactory = JGitSshAgentFactory(ConnectorFactory.getDefault(), null)
}
CoreModuleProperties.PREFERRED_AUTHS.set( CoreModuleProperties.PREFERRED_AUTHS.set(
sshClient, sshClient,
listOf( listOf(
@@ -350,6 +369,24 @@ object SshClients {
return sshClient return sshClient
} }
private fun ask(host: Host, owner: Window): Authentication? {
val ref = AtomicReference<Authentication>(null)
SwingUtilities.invokeAndWait {
val dialog = RequestAuthenticationDialog(owner, host)
dialog.setLocationRelativeTo(owner)
val authentication = dialog.getAuthentication().apply { ref.set(this) }
// save
if (dialog.isRemembered()) {
hostManager.addHost(
host.copy(
authentication = authentication,
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
)
)
}
}
return ref.get()
}
private class MyDialogServerKeyVerifier(private val owner: Window) : ServerKeyVerifier, ModifiedServerKeyAcceptor { private class MyDialogServerKeyVerifier(private val owner: Window) : ServerKeyVerifier, ModifiedServerKeyAcceptor {
override fun verifyServerKey( override fun verifyServerKey(
@@ -368,27 +405,70 @@ object SshClients {
actual: PublicKey? actual: PublicKey?
): Boolean { ): Boolean {
val result = AtomicBoolean(false) val result = AtomicBoolean(false)
SwingUtilities.invokeAndWait { result.set(ask(remoteAddress, expected, actual) == JOptionPane.OK_OPTION) }
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() return result.get()
} }
private fun ask(
remoteAddress: SocketAddress?,
expected: PublicKey?,
actual: PublicKey?
): Int {
val formMargin = "7dlu"
val layout = FormLayout(
"default:grow",
"pref, 12dlu, pref, 4dlu, pref, 2dlu, pref, $formMargin, pref, $formMargin, pref, pref, 12dlu, pref"
)
val errorColor = if (FlatLaf.isLafDark()) UIManager.getColor("Component.warning.focusedBorderColor") else
UIManager.getColor("Component.error.focusedBorderColor")
val font = FontUtils.getCompositeFont("JetBrains Mono", Font.PLAIN, 12)
val artBox = Box.createHorizontalBox()
artBox.add(Box.createHorizontalGlue())
val expectedBox = Box.createVerticalBox()
for (line in KeyRandomArt(expected).toString().lines()) {
val label = JLabel(line)
label.font = font
expectedBox.add(label)
}
artBox.add(expectedBox)
artBox.add(Box.createHorizontalGlue())
val actualBox = Box.createVerticalBox()
for (line in KeyRandomArt(actual).toString().lines()) {
val label = JLabel(line)
label.foreground = errorColor
label.font = font
actualBox.add(label)
}
artBox.add(actualBox)
artBox.add(Box.createHorizontalGlue())
var rows = 1
val step = 2
// @formatter:off
val address = remoteAddress.toString().replace("/", StringUtils.EMPTY)
val panel = FormBuilder.create().layout(layout)
.add("<html><b>${I18n.getString("termora.host.modified-server-key.title", address)}</b></html>").xy(1, rows).apply { rows += step }
.add("${I18n.getString("termora.host.modified-server-key.thumbprint")}:").xy(1, rows).apply { rows += step }
.add(" ${I18n.getString("termora.host.modified-server-key.expected")}: ${KeyUtils.getFingerPrint(expected)}").xy(1, rows).apply { rows += step }
.add("<html>&nbsp;&nbsp;${I18n.getString("termora.host.modified-server-key.actual")}: <font color=rgb(${errorColor.red},${errorColor.green},${errorColor.blue})>${KeyUtils.getFingerPrint(actual)}</font></html>").xy(1, rows).apply { rows += step }
.addSeparator(StringUtils.EMPTY).xy(1, rows).apply { rows += step }
.add(artBox).xy(1, rows).apply { rows += step }
.addSeparator(StringUtils.EMPTY).xy(1, rows).apply { rows += 1 }
.add(I18n.getString("termora.host.modified-server-key.are-you-sure")).xy(1, rows).apply { rows += step }
.build()
// @formatter:on
return OptionPane.showConfirmDialog(
owner,
panel,
"SSH Security Warning",
messageType = JOptionPane.WARNING_MESSAGE,
optionType = JOptionPane.OK_CANCEL_OPTION
)
}
} }
private class DialogServerKeyVerifier( private class DialogServerKeyVerifier(
@@ -417,55 +497,5 @@ object SshClients {
} }
} }
private class IdentityProvider(private val host: Host, private val owner: Window) : PasswordIdentityProvider,
KeyIdentityProvider {
private val asked = AtomicBoolean(false)
private val hostManager get() = HostManager.getInstance()
private val keyManager get() = KeyManager.getInstance()
private var authentication = Authentication.No
override fun loadPasswords(session: SessionContext): MutableIterable<String> {
val authentication = ask()
if (authentication.type != AuthenticationType.Password) {
return mutableListOf()
}
return mutableListOf(authentication.password)
}
override fun loadKeys(session: SessionContext): MutableIterable<KeyPair> {
val authentication = ask()
if (authentication.type != AuthenticationType.PublicKey) {
return mutableListOf()
}
val ohKeyPair = keyManager.getOhKeyPair(authentication.password) ?: return mutableListOf()
return mutableListOf(OhKeyPairKeyPairProvider.generateKeyPair(ohKeyPair))
}
private fun ask(): Authentication {
if (asked.compareAndSet(false, true)) {
askNow()
}
return authentication
}
private fun askNow() {
if (SwingUtilities.isEventDispatchThread()) {
val dialog = RequestAuthenticationDialog(owner, host)
dialog.setLocationRelativeTo(owner)
authentication = dialog.getAuthentication()
// save
if (dialog.isRemembered()) {
val host = host.copy(
authentication = authentication,
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
)
hostManager.addHost(host)
}
} else {
SwingUtilities.invokeAndWait { askNow() }
}
}
}
} }

View File

@@ -51,7 +51,7 @@ class OutlineTextArea : FlatTextArea() {
} }
} }
class OutlineComboBox<T> : JComboBox<T>() { class OutlineComboBox<T> : FlatComboBox<T>() {
init { init {
addItemListener { addItemListener {
if (it.stateChange == ItemEvent.SELECTED) { if (it.stateChange == ItemEvent.SELECTED) {

View File

@@ -39,7 +39,11 @@ termora.doorman.mnemonic.incorrect=Incorrect mnemonic
# Hosts # Hosts
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? termora.host.modified-server-key.title=HOST [{0}] IDENTIFICATION HAS CHANGED
termora.host.modified-server-key.thumbprint=Host key thumbprint
termora.host.modified-server-key.expected=Expected
termora.host.modified-server-key.actual=Actual
termora.host.modified-server-key.are-you-sure=Are you sure you want to continue connecting?
# Settings # Settings
@@ -161,6 +165,7 @@ termora.new-host.general.username=Username
termora.new-host.general.authentication=Authentication termora.new-host.general.authentication=Authentication
termora.new-host.general.password=Password termora.new-host.general.password=Password
termora.new-host.general.remark=Comment termora.new-host.general.remark=Comment
termora.new-host.general.remember=Remember
termora.new-host.proxy=Proxy termora.new-host.proxy=Proxy

View File

@@ -37,8 +37,11 @@ termora.doorman.mnemonic.incorrect=助记词错误
# Hosts # Hosts
termora.host.modified-server-key=主机 [{0}] 身份已发生变化<br/><br/>期待: {1} 的指纹 {2}<br/><br/>实际: {3} 的指纹 {4}<br/><br/>你确定要继续连接吗? termora.host.modified-server-key.title=主机 [{0}] 身份已发生变化
termora.host.modified-server-key.thumbprint=主机密钥指纹
termora.host.modified-server-key.expected=期待
termora.host.modified-server-key.actual=实际
termora.host.modified-server-key.are-you-sure=你确定要继续连接吗?
termora.setting=设置 termora.setting=设置
termora.settings.restart.title=重启 termora.settings.restart.title=重启
@@ -148,6 +151,7 @@ termora.new-host.general.username=用户名
termora.new-host.general.authentication=认证类型 termora.new-host.general.authentication=认证类型
termora.new-host.general.password=密码 termora.new-host.general.password=密码
termora.new-host.general.remark=备注 termora.new-host.general.remark=备注
termora.new-host.general.remember=记住
termora.new-host.proxy=代理 termora.new-host.proxy=代理
termora.new-host.terminal=${termora.settings.terminal} termora.new-host.terminal=${termora.settings.terminal}

View File

@@ -38,8 +38,11 @@ termora.doorman.mnemonic.incorrect=助記詞錯誤
# Hosts # Hosts
termora.host.modified-server-key=主機 [{0}] 身分已變更<br/><br/>期待: {1} 的指紋 {2}<br/><br/>實際: {3} 的指紋 {4}<br/><br/>你確定要繼續連線嗎? termora.host.modified-server-key.title=主機 [{0}] 身分已變更
termora.host.modified-server-key.thumbprint=主機密鑰指紋
termora.host.modified-server-key.expected=期待
termora.host.modified-server-key.actual=實際
termora.host.modified-server-key.are-you-sure=你確定要繼續連線嗎?
termora.setting=設定 termora.setting=設定
termora.settings.restart.title=重啟 termora.settings.restart.title=重啟
@@ -147,6 +150,7 @@ termora.new-host.general.username=用戶名
termora.new-host.general.authentication=認證類型 termora.new-host.general.authentication=認證類型
termora.new-host.general.password=密碼 termora.new-host.general.password=密碼
termora.new-host.general.remark=備註 termora.new-host.general.remark=備註
termora.new-host.general.remember=記住
termora.new-host.proxy=代理 termora.new-host.proxy=代理
termora.new-host.terminal=${termora.settings.terminal} termora.new-host.terminal=${termora.settings.terminal}