mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-15 18:02:58 +08:00
feat: authentication support fallback (#431)
This commit is contained in:
69
src/main/java/app/termora/CombinedKeyIdentityProvider.java
Normal file
69
src/main/java/app/termora/CombinedKeyIdentityProvider.java
Normal file
@@ -0,0 +1,69 @@
|
||||
package app.termora;
|
||||
|
||||
import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
|
||||
import org.apache.sshd.common.session.SessionContext;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.util.*;
|
||||
|
||||
public class CombinedKeyIdentityProvider implements KeyIdentityProvider {
|
||||
|
||||
private final List<KeyIdentityProvider> providers = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public Iterable<KeyPair> loadKeys(SessionContext context) {
|
||||
return () -> new Iterator<>() {
|
||||
|
||||
private final Iterator<KeyIdentityProvider> factories = providers
|
||||
.iterator();
|
||||
private Iterator<KeyPair> current;
|
||||
|
||||
private Boolean hasElement;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
if (hasElement != null) {
|
||||
return hasElement;
|
||||
}
|
||||
while (current == null || !current.hasNext()) {
|
||||
if (factories.hasNext()) {
|
||||
try {
|
||||
current = factories.next().loadKeys(context)
|
||||
.iterator();
|
||||
} catch (IOException | GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else {
|
||||
current = null;
|
||||
hasElement = Boolean.FALSE;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
hasElement = Boolean.TRUE;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyPair next() {
|
||||
if ((hasElement == null && !hasNext()) || !hasElement) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
hasElement = null;
|
||||
KeyPair result;
|
||||
try {
|
||||
result = current.next();
|
||||
} catch (NoSuchElementException e) {
|
||||
result = null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
public void addKeyKeyIdentityProvider(KeyIdentityProvider provider) {
|
||||
providers.add(Objects.requireNonNull(provider));
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package app.termora
|
||||
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
@@ -103,8 +102,7 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
||||
var client: SshClient? = null
|
||||
var session: ClientSession? = null
|
||||
try {
|
||||
client = SshClients.openClient(host)
|
||||
client.userInteraction = TerminalUserInteraction(owner)
|
||||
client = SshClients.openClient(host, this)
|
||||
session = SshClients.openSession(host, client)
|
||||
} finally {
|
||||
session?.close()
|
||||
|
||||
@@ -34,8 +34,8 @@ class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(own
|
||||
pack()
|
||||
|
||||
size = Dimension(max(380, size.width), size.height)
|
||||
|
||||
setLocationRelativeTo(null)
|
||||
preferredSize = size
|
||||
minimumSize = size
|
||||
|
||||
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
@@ -65,6 +65,10 @@ class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(own
|
||||
}
|
||||
}
|
||||
|
||||
if (host.authentication.type != AuthenticationType.No) {
|
||||
authenticationTypeComboBox.selectedItem = host.authentication.type
|
||||
}
|
||||
|
||||
usernameTextField.text = host.username
|
||||
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
||||
private var lastPasswordReporterDataListener: PasswordReporterDataListener? = null
|
||||
private val sftpCommand get() = Database.getDatabase().sftp.sftpCommand
|
||||
private val defaultDirectory get() = Database.getDatabase().sftp.defaultDirectory
|
||||
private val owner get() = SwingUtilities.getWindowAncestor(terminalPanel)
|
||||
|
||||
init {
|
||||
terminalPanel.dropFiles = true
|
||||
@@ -67,7 +68,7 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
||||
)
|
||||
)
|
||||
|
||||
val sshClient = SshClients.openClient(host).apply { sshClient = this }
|
||||
val sshClient = SshClients.openClient(host, owner).apply { sshClient = this }
|
||||
val sshSession = SshClients.openSession(host, sshClient).apply { sshSession = this }
|
||||
|
||||
// 打开通道
|
||||
|
||||
@@ -4,7 +4,6 @@ import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.actions.TabReconnectAction
|
||||
import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
|
||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||
import app.termora.keymap.KeyShortcut
|
||||
import app.termora.keymap.KeymapManager
|
||||
import app.termora.terminal.ControlCharacters
|
||||
@@ -89,35 +88,8 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
||||
terminal.write("SSH client is opening...\r\n")
|
||||
}
|
||||
|
||||
var host =
|
||||
this.host.copy(authentication = this.host.authentication.copy(), updateDate = System.currentTimeMillis())
|
||||
val owner = SwingUtilities.getWindowAncestor(terminalPanel)
|
||||
val client = SshClients.openClient(host).also { sshClient = it }
|
||||
client.serverKeyVerifier = DialogServerKeyVerifier(owner)
|
||||
// keyboard interactive
|
||||
client.userInteraction = TerminalUserInteraction(owner)
|
||||
|
||||
if (host.authentication.type == AuthenticationType.No) {
|
||||
withContext(Dispatchers.Swing) {
|
||||
val dialog = RequestAuthenticationDialog(owner, host)
|
||||
val authentication = dialog.getAuthentication()
|
||||
host = host.copy(
|
||||
authentication = authentication,
|
||||
username = dialog.getUsername(),
|
||||
updateDate = System.currentTimeMillis(),
|
||||
)
|
||||
// save
|
||||
if (dialog.isRemembered()) {
|
||||
HostManager.getInstance().addHost(
|
||||
tab.host.copy(
|
||||
authentication = authentication,
|
||||
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val client = SshClients.openClient(host, owner).also { sshClient = it }
|
||||
val sessionListener = MySessionListener()
|
||||
val channelListener = MyChannelListener()
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||
import app.termora.keymgr.KeyManager
|
||||
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||
import app.termora.terminal.TerminalSize
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import kotlinx.coroutines.withContext
|
||||
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
|
||||
import org.apache.sshd.client.config.hosts.HostConfigEntry
|
||||
@@ -27,6 +27,7 @@ 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
|
||||
@@ -44,6 +45,7 @@ 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.*
|
||||
@@ -57,6 +59,7 @@ object SshClients {
|
||||
val HOST_KEY = AttributeRepository.AttributeKey<Host>()
|
||||
|
||||
private val timeout = Duration.ofSeconds(30)
|
||||
private val hostManager get() = HostManager.getInstance()
|
||||
private val log by lazy { LoggerFactory.getLogger(SshClients::class.java) }
|
||||
|
||||
/**
|
||||
@@ -119,16 +122,16 @@ object SshClients {
|
||||
* 打开一个会话
|
||||
*/
|
||||
fun openSession(host: Host, client: SshClient): ClientSession {
|
||||
|
||||
val h = hostManager.getHost(host.id) ?: host
|
||||
|
||||
// 如果没有跳板机直接连接
|
||||
if (host.options.jumpHosts.isEmpty()) {
|
||||
return doOpenSession(host, client)
|
||||
if (h.options.jumpHosts.isEmpty()) {
|
||||
return doOpenSession(h, client)
|
||||
}
|
||||
|
||||
val jumpHosts = mutableListOf<Host>()
|
||||
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
|
||||
for (jumpHostId in host.options.jumpHosts) {
|
||||
for (jumpHostId in h.options.jumpHosts) {
|
||||
val e = hosts[jumpHostId]
|
||||
if (e == null) {
|
||||
if (log.isWarnEnabled) {
|
||||
@@ -140,7 +143,7 @@ object SshClients {
|
||||
}
|
||||
|
||||
// 最后一跳是目标机器
|
||||
jumpHosts.add(host)
|
||||
jumpHosts.add(h)
|
||||
|
||||
val sessions = mutableListOf<ClientSession>()
|
||||
for (i in 0 until jumpHosts.size) {
|
||||
@@ -187,14 +190,25 @@ object SshClients {
|
||||
entry.hostName = host.host
|
||||
entry.setProperty("Middleware", middleware.toString())
|
||||
|
||||
val session = client.connect(entry)
|
||||
.verify(timeout).session
|
||||
val session = client.connect(entry).verify(timeout).session
|
||||
if (host.authentication.type == AuthenticationType.Password) {
|
||||
session.addPasswordIdentity(host.authentication.password)
|
||||
} else if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||
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)
|
||||
}
|
||||
combinedKeyIdentityProvider.addKeyKeyIdentityProvider(identityProvider)
|
||||
session.keyIdentityProvider = combinedKeyIdentityProvider
|
||||
}
|
||||
|
||||
val verifyTimeout = Duration.ofSeconds(timeout.seconds * 5)
|
||||
if (!session.auth().verify(verifyTimeout).await(verifyTimeout)) {
|
||||
throw SshException("Authentication failed")
|
||||
@@ -241,27 +255,13 @@ object SshClients {
|
||||
return sshdSocketAddress
|
||||
}
|
||||
|
||||
suspend fun openClient(host: Host, owner: Window): Pair<SshClient, Host> {
|
||||
val client = openClient(host)
|
||||
var myHost = host
|
||||
withContext(Dispatchers.Swing) {
|
||||
client.userInteraction = TerminalUserInteraction(owner)
|
||||
client.serverKeyVerifier = DialogServerKeyVerifier(owner)
|
||||
// 弹出授权框
|
||||
if (host.authentication.type == AuthenticationType.No) {
|
||||
val dialog = RequestAuthenticationDialog(owner, host)
|
||||
val authentication = dialog.getAuthentication()
|
||||
myHost = myHost.copy(
|
||||
authentication = authentication,
|
||||
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
|
||||
)
|
||||
// save
|
||||
if (dialog.isRemembered()) {
|
||||
HostManager.getInstance().addHost(myHost)
|
||||
}
|
||||
}
|
||||
}
|
||||
return client to myHost
|
||||
fun openClient(host: Host, owner: Window): SshClient {
|
||||
val h = hostManager.getHost(host.id) ?: host
|
||||
val client = openClient(h)
|
||||
client.userInteraction = TerminalUserInteraction(owner)
|
||||
client.serverKeyVerifier = DialogServerKeyVerifier(owner)
|
||||
client.properties["owner"] = owner
|
||||
return client
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -298,6 +298,28 @@ object SshClients {
|
||||
// JGit 会尝试读取本地的私钥或缓存的私钥
|
||||
sshClient.keyIdentityProvider = KeyIdentityProvider { mutableListOf() }
|
||||
|
||||
// 设置优先级
|
||||
if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||
CoreModuleProperties.PREFERRED_AUTHS.set(
|
||||
sshClient,
|
||||
listOf(
|
||||
UserAuthPasswordFactory.PUBLIC_KEY,
|
||||
UserAuthPasswordFactory.PASSWORD,
|
||||
UserAuthPasswordFactory.KB_INTERACTIVE
|
||||
).joinToString(",")
|
||||
)
|
||||
} else {
|
||||
CoreModuleProperties.PREFERRED_AUTHS.set(
|
||||
sshClient,
|
||||
listOf(
|
||||
UserAuthPasswordFactory.PASSWORD,
|
||||
UserAuthPasswordFactory.PUBLIC_KEY,
|
||||
UserAuthPasswordFactory.KB_INTERACTIVE
|
||||
).joinToString(",")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
val heartbeatInterval = max(host.options.heartbeatInterval, 3)
|
||||
CoreModuleProperties.HEARTBEAT_INTERVAL.set(sshClient, Duration.ofSeconds(heartbeatInterval.toLong()))
|
||||
CoreModuleProperties.ALLOW_DHG1_KEX_FALLBACK.set(sshClient, true)
|
||||
@@ -327,71 +349,123 @@ object SshClients {
|
||||
sshClient.start()
|
||||
return sshClient
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class MyDialogServerKeyVerifier(private val owner: Window) : ServerKeyVerifier, ModifiedServerKeyAcceptor {
|
||||
override fun verifyServerKey(
|
||||
clientSession: ClientSession,
|
||||
remoteAddress: SocketAddress,
|
||||
serverKey: PublicKey
|
||||
): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
private class MyDialogServerKeyVerifier(private val owner: Window) : ServerKeyVerifier, ModifiedServerKeyAcceptor {
|
||||
override fun verifyServerKey(
|
||||
clientSession: ClientSession,
|
||||
remoteAddress: SocketAddress,
|
||||
serverKey: PublicKey
|
||||
): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
return result.get()
|
||||
}
|
||||
}
|
||||
override fun acceptModifiedServerKey(
|
||||
clientSession: ClientSession?,
|
||||
remoteAddress: SocketAddress?,
|
||||
entry: KnownHostEntry?,
|
||||
expected: PublicKey?,
|
||||
actual: PublicKey?
|
||||
): Boolean {
|
||||
val result = AtomicBoolean(false)
|
||||
|
||||
class DialogServerKeyVerifier(
|
||||
owner: Window,
|
||||
) : KnownHostsServerKeyVerifier(
|
||||
MyDialogServerKeyVerifier(owner),
|
||||
Paths.get(Application.getBaseDataDir().absolutePath, "known_hosts")
|
||||
) {
|
||||
init {
|
||||
modifiedServerKeyAcceptor = delegateVerifier as ModifiedServerKeyAcceptor
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateKnownHostsFile(
|
||||
clientSession: ClientSession?,
|
||||
remoteAddress: SocketAddress?,
|
||||
serverKey: PublicKey?,
|
||||
file: Path?,
|
||||
knownHosts: Collection<HostEntryPair?>?
|
||||
): KnownHostEntry? {
|
||||
if (clientSession is JGitClientSession) {
|
||||
if (SshClients.isMiddleware(clientSession)) {
|
||||
return null
|
||||
private class DialogServerKeyVerifier(
|
||||
owner: Window,
|
||||
) : KnownHostsServerKeyVerifier(
|
||||
MyDialogServerKeyVerifier(owner),
|
||||
Paths.get(Application.getBaseDataDir().absolutePath, "known_hosts")
|
||||
) {
|
||||
init {
|
||||
modifiedServerKeyAcceptor = delegateVerifier as ModifiedServerKeyAcceptor
|
||||
}
|
||||
|
||||
override fun updateKnownHostsFile(
|
||||
clientSession: ClientSession?,
|
||||
remoteAddress: SocketAddress?,
|
||||
serverKey: PublicKey?,
|
||||
file: Path?,
|
||||
knownHosts: Collection<HostEntryPair?>?
|
||||
): KnownHostEntry? {
|
||||
if (clientSession is JGitClientSession) {
|
||||
if (isMiddleware(clientSession)) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return super.updateKnownHostsFile(clientSession, remoteAddress, serverKey, file, knownHosts)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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() }
|
||||
}
|
||||
}
|
||||
return super.updateKnownHostsFile(clientSession, remoteAddress, serverKey, file, knownHosts)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ class SSHCopyIdDialog(
|
||||
}
|
||||
|
||||
try {
|
||||
val client = SshClients.openClient(host).apply { myClient = this }
|
||||
val client = SshClients.openClient(host, this).apply { myClient = this }
|
||||
client.userInteraction = TerminalUserInteraction(owner)
|
||||
val session = SshClients.openSession(host, client).apply { mySession = this }
|
||||
val channel =
|
||||
|
||||
@@ -112,9 +112,9 @@ class SFTPFileSystemViewPanel(
|
||||
closeIO()
|
||||
|
||||
try {
|
||||
val (client, host) = SshClients.openClient(thisHost, SwingUtilities.getWindowAncestor(that))
|
||||
this.client = client
|
||||
val session = SshClients.openSession(host, client).apply { session = this }
|
||||
val owner = SwingUtilities.getWindowAncestor(that)
|
||||
val client = SshClients.openClient(thisHost, owner).apply { client = this }
|
||||
val session = SshClients.openSession(thisHost, client).apply { session = this }
|
||||
fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)
|
||||
session.addCloseFutureListener { onClose() }
|
||||
} catch (e: Exception) {
|
||||
|
||||
Reference in New Issue
Block a user