mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-15 18:02:58 +08:00
feat: SSH support ssh-agent (#433)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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" }
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ enum class AuthenticationType {
|
|||||||
No,
|
No,
|
||||||
Password,
|
Password,
|
||||||
PublicKey,
|
PublicKey,
|
||||||
|
SSHAgent,
|
||||||
KeyboardInteractive,
|
KeyboardInteractive,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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> ${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() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user