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
|
||||
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
|
||||
Eclipse Distribution License
|
||||
https://www.eclipse.org/org/documents/edl-v10.php
|
||||
|
||||
@@ -104,6 +104,7 @@ dependencies {
|
||||
implementation(libs.commonmark)
|
||||
implementation(libs.jgit)
|
||||
implementation(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
|
||||
implementation(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
|
||||
implementation(libs.jnafilechooser)
|
||||
implementation(libs.xodus.vfs)
|
||||
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" }
|
||||
commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" }
|
||||
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-environment = { module = "org.jetbrains.xodus:xodus-environment", version.ref = "xodus" }
|
||||
xodus-vfs = { module = "org.jetbrains.xodus:xodus-vfs", version.ref = "xodus" }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
||||
init {
|
||||
generalOption.portTextField.value = host.port
|
||||
@@ -13,6 +14,8 @@ class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
||||
generalOption.passwordTextField.text = host.authentication.password
|
||||
} else if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||
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
|
||||
|
||||
@@ -38,6 +38,7 @@ enum class AuthenticationType {
|
||||
No,
|
||||
Password,
|
||||
PublicKey,
|
||||
SSHAgent,
|
||||
KeyboardInteractive,
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.fazecast.jSerialComm.SerialPort
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -14,6 +15,9 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import kotlinx.coroutines.withContext
|
||||
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.event.*
|
||||
import java.nio.charset.Charset
|
||||
@@ -21,7 +25,7 @@ import javax.swing.*
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
import javax.swing.table.DefaultTableModel
|
||||
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
open class HostOptionsPane : OptionsPane() {
|
||||
protected val tunnelingOption = TunnelingOption()
|
||||
protected val generalOption = GeneralOption()
|
||||
@@ -52,18 +56,23 @@ open class HostOptionsPane : OptionsPane() {
|
||||
val port = (generalOption.portTextField.value ?: 22) as Int
|
||||
var authentication = Authentication.No
|
||||
var proxy = Proxy.No
|
||||
val authenticationType = generalOption.authenticationTypeComboBox.selectedItem as AuthenticationType
|
||||
|
||||
|
||||
if (generalOption.authenticationTypeComboBox.selectedItem == AuthenticationType.Password) {
|
||||
if (authenticationType == AuthenticationType.Password) {
|
||||
authentication = authentication.copy(
|
||||
type = AuthenticationType.Password,
|
||||
type = authenticationType,
|
||||
password = String(generalOption.passwordTextField.password)
|
||||
)
|
||||
} else if (generalOption.authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) {
|
||||
} else if (authenticationType == AuthenticationType.PublicKey) {
|
||||
authentication = authentication.copy(
|
||||
type = AuthenticationType.PublicKey,
|
||||
type = authenticationType,
|
||||
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) {
|
||||
@@ -200,6 +209,7 @@ open class HostOptionsPane : OptionsPane() {
|
||||
private val passwordPanel = JPanel(BorderLayout())
|
||||
private val chooseKeyBtn = JButton(Icons.greyKey)
|
||||
val passwordTextField = OutlinePasswordField(255)
|
||||
val sshAgentComboBox = OutlineComboBox<String>()
|
||||
val publicKeyComboBox = OutlineComboBox<String>()
|
||||
val remarkTextArea = FixedLengthTextArea(512)
|
||||
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||
@@ -215,6 +225,10 @@ open class HostOptionsPane : OptionsPane() {
|
||||
publicKeyComboBox.isEditable = false
|
||||
chooseKeyBtn.isFocusable = false
|
||||
|
||||
// 只有 Windows 允许修改
|
||||
sshAgentComboBox.isEditable = SystemInfo.isWindows
|
||||
sshAgentComboBox.isEnabled = SystemInfo.isWindows
|
||||
|
||||
protocolTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
@@ -294,6 +308,17 @@ open class HostOptionsPane : OptionsPane() {
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||
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
|
||||
|
||||
@@ -457,6 +482,8 @@ open class HostOptionsPane : OptionsPane() {
|
||||
.add(chooseKeyBtn).xy(3, 1)
|
||||
.build(), BorderLayout.CENTER
|
||||
)
|
||||
} else if (authenticationTypeComboBox.selectedItem == AuthenticationType.SSHAgent) {
|
||||
passwordPanel.add(sshAgentComboBox, BorderLayout.CENTER)
|
||||
} else {
|
||||
passwordPanel.add(passwordTextField, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import kotlin.math.max
|
||||
class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(owner) {
|
||||
|
||||
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 passwordPasswordField = OutlinePasswordField()
|
||||
private val usernameTextField = OutlineTextField()
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||
import app.termora.keymgr.KeyManager
|
||||
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||
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.lang3.StringUtils
|
||||
import org.apache.sshd.client.ClientBuilder
|
||||
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.channel.ChannelShell
|
||||
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.session.ClientSession
|
||||
import org.apache.sshd.common.AttributeRepository
|
||||
import org.apache.sshd.common.SshConstants
|
||||
import org.apache.sshd.common.SshException
|
||||
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.global.KeepAliveHandler
|
||||
import org.apache.sshd.common.kex.BuiltinDHFactories
|
||||
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.core.CoreModuleProperties
|
||||
import org.apache.sshd.server.forward.AcceptAllForwardingFilter
|
||||
import org.apache.sshd.server.forward.RejectAllForwardingFilter
|
||||
import org.eclipse.jgit.internal.transport.sshd.JGitClientSession
|
||||
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.SshConstants.IDENTITY_AGENT
|
||||
import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider
|
||||
import org.eclipse.jgit.transport.sshd.ProxyData
|
||||
import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Font
|
||||
import java.awt.Window
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.net.InetSocketAddress
|
||||
@@ -45,15 +55,15 @@ import java.net.Proxy
|
||||
import java.net.SocketAddress
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.SwingUtilities
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import javax.swing.*
|
||||
import kotlin.math.max
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
object SshClients {
|
||||
|
||||
val HOST_KEY = AttributeRepository.AttributeKey<Host>()
|
||||
@@ -190,6 +200,16 @@ object SshClients {
|
||||
entry.hostName = host.host
|
||||
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
|
||||
if (host.authentication.type == AuthenticationType.Password) {
|
||||
session.addPasswordIdentity(host.authentication.password)
|
||||
@@ -197,21 +217,16 @@ object SshClients {
|
||||
session.keyIdentityProvider = OhKeyPairKeyPairProvider(host.authentication.password)
|
||||
}
|
||||
|
||||
val owner = client.properties["owner"] as Window?
|
||||
if (owner != null) {
|
||||
val identityProvider = IdentityProvider(host, owner)
|
||||
session.passwordIdentityProvider = identityProvider
|
||||
val combinedKeyIdentityProvider = CombinedKeyIdentityProvider()
|
||||
if (session.keyIdentityProvider != null) {
|
||||
combinedKeyIdentityProvider.addKeyKeyIdentityProvider(session.keyIdentityProvider)
|
||||
try {
|
||||
if (!session.auth().verify(timeout).await(timeout)) {
|
||||
throw SshException("Authentication failed")
|
||||
}
|
||||
combinedKeyIdentityProvider.addKeyKeyIdentityProvider(identityProvider)
|
||||
session.keyIdentityProvider = combinedKeyIdentityProvider
|
||||
}
|
||||
|
||||
val verifyTimeout = Duration.ofSeconds(timeout.seconds * 5)
|
||||
if (!session.auth().verify(verifyTimeout).await(verifyTimeout)) {
|
||||
throw SshException("Authentication failed")
|
||||
} catch (e: Exception) {
|
||||
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
|
||||
if (authentication.type == AuthenticationType.No) throw e
|
||||
return doOpenSession(host.copy(authentication = authentication), client)
|
||||
}
|
||||
|
||||
session.setAttribute(HOST_KEY, host)
|
||||
@@ -299,7 +314,11 @@ object SshClients {
|
||||
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(
|
||||
sshClient,
|
||||
listOf(
|
||||
@@ -350,6 +369,24 @@ object SshClients {
|
||||
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 {
|
||||
override fun verifyServerKey(
|
||||
@@ -368,27 +405,70 @@ object SshClients {
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
SwingUtilities.invokeAndWait { result.set(ask(remoteAddress, expected, actual) == JOptionPane.OK_OPTION) }
|
||||
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(
|
||||
@@ -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 {
|
||||
addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
|
||||
@@ -39,7 +39,11 @@ termora.doorman.mnemonic.incorrect=Incorrect mnemonic
|
||||
|
||||
|
||||
# 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
|
||||
@@ -161,6 +165,7 @@ termora.new-host.general.username=Username
|
||||
termora.new-host.general.authentication=Authentication
|
||||
termora.new-host.general.password=Password
|
||||
termora.new-host.general.remark=Comment
|
||||
termora.new-host.general.remember=Remember
|
||||
|
||||
termora.new-host.proxy=Proxy
|
||||
|
||||
|
||||
@@ -37,8 +37,11 @@ termora.doorman.mnemonic.incorrect=助记词错误
|
||||
|
||||
|
||||
# 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.settings.restart.title=重启
|
||||
@@ -148,6 +151,7 @@ termora.new-host.general.username=用户名
|
||||
termora.new-host.general.authentication=认证类型
|
||||
termora.new-host.general.password=密码
|
||||
termora.new-host.general.remark=备注
|
||||
termora.new-host.general.remember=记住
|
||||
termora.new-host.proxy=代理
|
||||
|
||||
termora.new-host.terminal=${termora.settings.terminal}
|
||||
|
||||
@@ -38,8 +38,11 @@ termora.doorman.mnemonic.incorrect=助記詞錯誤
|
||||
|
||||
|
||||
# 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.settings.restart.title=重啟
|
||||
@@ -147,6 +150,7 @@ termora.new-host.general.username=用戶名
|
||||
termora.new-host.general.authentication=認證類型
|
||||
termora.new-host.general.password=密碼
|
||||
termora.new-host.general.remark=備註
|
||||
termora.new-host.general.remember=記住
|
||||
termora.new-host.proxy=代理
|
||||
|
||||
termora.new-host.terminal=${termora.settings.terminal}
|
||||
|
||||
Reference in New Issue
Block a user