mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
feat: support RDP protocol (#524)
This commit is contained in:
@@ -25,6 +25,7 @@ enum class Protocol {
|
|||||||
SSH,
|
SSH,
|
||||||
Local,
|
Local,
|
||||||
Serial,
|
Serial,
|
||||||
|
RDP,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 交互式的 SFTP,此协议只在系统内部交互不应该暴露给用户也不应该持久化
|
* 交互式的 SFTP,此协议只在系统内部交互不应该暴露给用户也不应该持久化
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
protocolTypeComboBox.addItem(Protocol.SSH)
|
protocolTypeComboBox.addItem(Protocol.SSH)
|
||||||
protocolTypeComboBox.addItem(Protocol.Local)
|
protocolTypeComboBox.addItem(Protocol.Local)
|
||||||
protocolTypeComboBox.addItem(Protocol.Serial)
|
protocolTypeComboBox.addItem(Protocol.Serial)
|
||||||
|
protocolTypeComboBox.addItem(Protocol.RDP)
|
||||||
|
|
||||||
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||||
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class HostTreeNode(host: Host) : SimpleTreeNode<Host>(host) {
|
|||||||
return when (host.protocol) {
|
return when (host.protocol) {
|
||||||
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
||||||
Protocol.Serial -> if (selected && hasFocus) Icons.plugin.dark else Icons.plugin
|
Protocol.Serial -> if (selected && hasFocus) Icons.plugin.dark else Icons.plugin
|
||||||
|
Protocol.RDP -> if (selected && hasFocus) Icons.microsoftWindows.dark else Icons.microsoftWindows
|
||||||
else -> if (selected && hasFocus) Icons.terminal.dark else Icons.terminal
|
else -> if (selected && hasFocus) Icons.terminal.dark else Icons.terminal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ object Icons {
|
|||||||
val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") }
|
val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") }
|
||||||
val edit by lazy { DynamicIcon("icons/edit.svg", "icons/edit_dark.svg") }
|
val edit by lazy { DynamicIcon("icons/edit.svg", "icons/edit_dark.svg") }
|
||||||
val microsoft by lazy { DynamicIcon("icons/microsoft.svg", "icons/microsoft_dark.svg") }
|
val microsoft by lazy { DynamicIcon("icons/microsoft.svg", "icons/microsoft_dark.svg") }
|
||||||
|
val microsoftWindows by lazy { DynamicIcon("icons/microsoftWindows.svg", "icons/microsoftWindows_dark.svg") }
|
||||||
val tencent by lazy { DynamicIcon("icons/tencent.svg") }
|
val tencent by lazy { DynamicIcon("icons/tencent.svg") }
|
||||||
val google by lazy { DynamicIcon("icons/google-small.svg") }
|
val google by lazy { DynamicIcon("icons/google-small.svg") }
|
||||||
val aliyun by lazy { DynamicIcon("icons/aliyun.svg") }
|
val aliyun by lazy { DynamicIcon("icons/aliyun.svg") }
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import javax.xml.parsers.DocumentBuilderFactory
|
|||||||
import javax.xml.xpath.XPathConstants
|
import javax.xml.xpath.XPathConstants
|
||||||
import javax.xml.xpath.XPathFactory
|
import javax.xml.xpath.XPathFactory
|
||||||
|
|
||||||
|
@Suppress("CascadeIf")
|
||||||
class NewHostTree : SimpleTree() {
|
class NewHostTree : SimpleTree() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -48,7 +49,7 @@ class NewHostTree : SimpleTree() {
|
|||||||
private val properties get() = Database.getDatabase().properties
|
private val properties get() = Database.getDatabase().properties
|
||||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||||
private val openHostAction get() = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
|
private val openHostAction get() = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
|
||||||
private val sftpAction get() = ActionManager.getInstance().getAction(app.termora.Actions.SFTP)
|
private val sftpAction get() = ActionManager.getInstance().getAction(Actions.SFTP)
|
||||||
private var isShowMoreInfo
|
private var isShowMoreInfo
|
||||||
get() = properties.getString("HostTree.showMoreInfo", "false").toBoolean()
|
get() = properties.getString("HostTree.showMoreInfo", "false").toBoolean()
|
||||||
set(value) = properties.putString("HostTree.showMoreInfo", value.toString())
|
set(value) = properties.putString("HostTree.showMoreInfo", value.toString())
|
||||||
@@ -97,7 +98,7 @@ class NewHostTree : SimpleTree() {
|
|||||||
// 是否显示更多信息
|
// 是否显示更多信息
|
||||||
if (isShowMoreInfo) {
|
if (isShowMoreInfo) {
|
||||||
val color = if (sel) {
|
val color = if (sel) {
|
||||||
if (tree.hasFocus()) {
|
if (tree.hasFocus() || isPopupMenu) {
|
||||||
UIManager.getColor("textHighlightText")
|
UIManager.getColor("textHighlightText")
|
||||||
} else {
|
} else {
|
||||||
this.foreground
|
this.foreground
|
||||||
@@ -110,15 +111,15 @@ class NewHostTree : SimpleTree() {
|
|||||||
"""<font color=rgb(${color.red},${color.green},${color.blue})>${it}</font>"""
|
"""<font color=rgb(${color.red},${color.green},${color.blue})>${it}</font>"""
|
||||||
}
|
}
|
||||||
|
|
||||||
if (host.protocol == Protocol.SSH) {
|
// @formatter:off
|
||||||
text =
|
if (host.protocol == Protocol.SSH || host.protocol == Protocol.RDP) {
|
||||||
"<html>${host.name} ${fontTag.apply("${host.username}@${host.host}")}</html>"
|
text = "<html>${host.name} ${fontTag.apply("${host.username}@${host.host}")}</html>"
|
||||||
} else if (host.protocol == Protocol.Serial) {
|
} else if (host.protocol == Protocol.Serial) {
|
||||||
text =
|
text = "<html>${host.name} ${fontTag.apply(host.options.serialComm.port)}</html>"
|
||||||
"<html>${host.name} ${fontTag.apply(host.options.serialComm.port)}</html>"
|
|
||||||
} else if (host.protocol == Protocol.Folder) {
|
} else if (host.protocol == Protocol.Folder) {
|
||||||
text = "<html>${host.name}${fontTag.apply(" (${node.childCount})")}</html>"
|
text = "<html>${host.name}${fontTag.apply(" (${node.childCount})")}</html>"
|
||||||
}
|
}
|
||||||
|
// @formatter:on
|
||||||
}
|
}
|
||||||
|
|
||||||
val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus)
|
val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus)
|
||||||
@@ -810,7 +811,7 @@ class NewHostTree : SimpleTree() {
|
|||||||
var group = bookmarkGroups.find { it.bookmarkIds.contains(id) }
|
var group = bookmarkGroups.find { it.bookmarkIds.contains(id) }
|
||||||
while (group != null && group.id != "default") {
|
while (group != null && group.id != "default") {
|
||||||
folderNames.addFirst(group.title)
|
folderNames.addFirst(group.title)
|
||||||
group = bookmarkGroups.find { it.bookmarkGroupIds.contains(group?.id ?: StringUtils.EMPTY) }
|
group = bookmarkGroups.find { it.bookmarkGroupIds.contains(group.id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
printer.printRecord(
|
printer.printRecord(
|
||||||
|
|||||||
@@ -1,6 +1,19 @@
|
|||||||
package app.termora.actions
|
package app.termora.actions
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.awt.datatransfer.DataFlavor
|
||||||
|
import java.awt.datatransfer.StringSelection
|
||||||
|
import java.net.URI
|
||||||
|
import java.util.*
|
||||||
|
import javax.swing.JOptionPane
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class OpenHostAction : AnAction() {
|
class OpenHostAction : AnAction() {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -26,10 +39,70 @@ class OpenHostAction : AnAction() {
|
|||||||
Protocol.SSH -> SSHTerminalTab(windowScope, evt.host)
|
Protocol.SSH -> SSHTerminalTab(windowScope, evt.host)
|
||||||
Protocol.Serial -> SerialTerminalTab(windowScope, evt.host)
|
Protocol.Serial -> SerialTerminalTab(windowScope, evt.host)
|
||||||
Protocol.SFTPPty -> SFTPPtyTerminalTab(windowScope, evt.host)
|
Protocol.SFTPPty -> SFTPPtyTerminalTab(windowScope, evt.host)
|
||||||
|
Protocol.RDP -> openRDP(windowScope, evt.host)
|
||||||
else -> LocalTerminalTab(windowScope, evt.host)
|
else -> LocalTerminalTab(windowScope, evt.host)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tab is TerminalTab) {
|
||||||
terminalTabbedManager.addTerminalTab(tab)
|
terminalTabbedManager.addTerminalTab(tab)
|
||||||
|
if (tab is PtyHostTerminalTab) {
|
||||||
tab.start()
|
tab.start()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openRDP(windowScope: WindowScope, host: Host) {
|
||||||
|
if (SystemInfo.isLinux) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
windowScope.window,
|
||||||
|
"Linux cannot connect to Windows Remote Server, Supported only for macOS and Windows",
|
||||||
|
messageType = JOptionPane.WARNING_MESSAGE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SystemInfo.isMacOS) {
|
||||||
|
if (!FileUtils.getFile("/Applications/Windows App.app").exists()) {
|
||||||
|
val option = OptionPane.showConfirmDialog(
|
||||||
|
windowScope.window,
|
||||||
|
"If you want to connect to a Windows Remote Server, You have to install the Windows App",
|
||||||
|
optionType = JOptionPane.OK_CANCEL_OPTION
|
||||||
|
)
|
||||||
|
if (option == JOptionPane.OK_OPTION) {
|
||||||
|
Application.browse(URI.create("https://apps.apple.com/app/windows-app/id1295203466"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val sb = StringBuilder()
|
||||||
|
sb.append("full address:s:").append(host.host).append(':').append(host.port).appendLine()
|
||||||
|
sb.append("username:s:").append(host.username).appendLine()
|
||||||
|
|
||||||
|
val file = FileUtils.getFile(Application.getTemporaryDir(), UUID.randomUUID().toSimpleString() + ".rdp")
|
||||||
|
file.outputStream().use { IOUtils.write(sb.toString(), it, Charsets.UTF_8) }
|
||||||
|
|
||||||
|
if (host.authentication.type == AuthenticationType.Password) {
|
||||||
|
val systemClipboard = windowScope.window.toolkit.systemClipboard
|
||||||
|
val password = host.authentication.password
|
||||||
|
systemClipboard.setContents(StringSelection(password), null)
|
||||||
|
// clear password
|
||||||
|
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||||
|
delay(30.seconds)
|
||||||
|
if (systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||||
|
if (systemClipboard.getData(DataFlavor.stringFlavor) == password) {
|
||||||
|
systemClipboard.setContents(StringSelection(StringUtils.EMPTY), null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SystemInfo.isMacOS) {
|
||||||
|
ProcessBuilder("open", file.absolutePath).start()
|
||||||
|
} else if (SystemInfo.isWindows) {
|
||||||
|
ProcessBuilder("mstsc", file.absolutePath).start()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
4
src/main/resources/icons/microsoftWindows.svg
Normal file
4
src/main/resources/icons/microsoftWindows.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2.00098 2H7.00195L7.00098 7H2L2.00098 2ZM8.00293 2H13V7H8.00293V2ZM2 7.99902L7 8V13.001L2 13V7.99902ZM8.00195 8H12.999L12.998 13.001H8.00195" fill="#6C707E"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 399 B |
4
src/main/resources/icons/microsoftWindows_dark.svg
Normal file
4
src/main/resources/icons/microsoftWindows_dark.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2.00098 2H7.00195L7.00098 7H2L2.00098 2ZM8.00293 2H13V7H8.00293V2ZM2 7.99902L7 8V13.001L2 13V7.99902ZM8.00195 8H12.999L12.998 13.001H8.00195" fill="#CED0D6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 399 B |
Reference in New Issue
Block a user