From 8cec835583256a79e7cbbd5649b9e15e7f454e52 Mon Sep 17 00:00:00 2001 From: hstyi Date: Sat, 5 Jul 2025 13:58:36 +0800 Subject: [PATCH] feat: support telnet --- src/main/kotlin/app/termora/Icons.kt | 1 + .../app/termora/plugin/PluginManager.kt | 7 +- .../plugin/internal/BasicProxyOption.kt | 9 +- .../local/LocalProtocolHostPanelExtension.kt | 4 + .../rdp/RDPProtocolHostPanelExtension.kt | 4 + .../SerialProtocolHostPanelExtension.kt | 3 + .../ssh/SSHProtocolHostPanelExtension.kt | 4 + .../internal/telnet/TelnetHostOptionsPane.kt | 422 ++++++++++++++++++ .../internal/telnet/TelnetInternalPlugin.kt | 24 + .../telnet/TelnetProtocolHostPanel.kt | 38 ++ .../TelnetProtocolHostPanelExtension.kt | 25 ++ .../internal/telnet/TelnetProtocolProvider.kt | 27 ++ .../telnet/TelnetProtocolProviderExtension.kt | 14 + .../telnet/TelnetStreamPtyConnector.kt | 42 ++ .../internal/telnet/TelnetTerminalTab.kt | 79 ++++ .../protocol/ProtocolHostPanelExtension.kt | 1 - src/main/resources/icons/telnet.svg | 1 + src/main/resources/icons/telnet_dark.svg | 1 + 18 files changed, 701 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/app/termora/plugin/internal/telnet/TelnetHostOptionsPane.kt create mode 100644 src/main/kotlin/app/termora/plugin/internal/telnet/TelnetInternalPlugin.kt create mode 100644 src/main/kotlin/app/termora/plugin/internal/telnet/TelnetProtocolHostPanel.kt create mode 100644 src/main/kotlin/app/termora/plugin/internal/telnet/TelnetProtocolHostPanelExtension.kt create mode 100644 src/main/kotlin/app/termora/plugin/internal/telnet/TelnetProtocolProvider.kt create mode 100644 src/main/kotlin/app/termora/plugin/internal/telnet/TelnetProtocolProviderExtension.kt create mode 100644 src/main/kotlin/app/termora/plugin/internal/telnet/TelnetStreamPtyConnector.kt create mode 100644 src/main/kotlin/app/termora/plugin/internal/telnet/TelnetTerminalTab.kt create mode 100644 src/main/resources/icons/telnet.svg create mode 100644 src/main/resources/icons/telnet_dark.svg diff --git a/src/main/kotlin/app/termora/Icons.kt b/src/main/kotlin/app/termora/Icons.kt index 635ace2..1c7755c 100644 --- a/src/main/kotlin/app/termora/Icons.kt +++ b/src/main/kotlin/app/termora/Icons.kt @@ -78,6 +78,7 @@ object Icons { val import by lazy { DynamicIcon("icons/import.svg", "icons/import_dark.svg") } val export by lazy { DynamicIcon("icons/export.svg", "icons/export_dark.svg") } val terminal by lazy { DynamicIcon("icons/terminal.svg", "icons/terminal_dark.svg") } + val telnet by lazy { DynamicIcon("icons/telnet.svg", "icons/telnet_dark.svg") } val ssh by lazy { DynamicIcon("icons/ssh.svg", "icons/ssh_dark.svg") } val ftp by lazy { DynamicIcon("icons/ftp.svg", "icons/ftp_dark.svg") } val minio by lazy { DynamicIcon("icons/minio.svg", "icons/minio_dark.svg") } diff --git a/src/main/kotlin/app/termora/plugin/PluginManager.kt b/src/main/kotlin/app/termora/plugin/PluginManager.kt index 5225ede..5d2608a 100644 --- a/src/main/kotlin/app/termora/plugin/PluginManager.kt +++ b/src/main/kotlin/app/termora/plugin/PluginManager.kt @@ -11,6 +11,7 @@ import app.termora.plugin.internal.rdp.RDPInternalPlugin import app.termora.plugin.internal.serial.SerialInternalPlugin import app.termora.plugin.internal.sftppty.SFTPPtyInternalPlugin import app.termora.plugin.internal.ssh.SSHInternalPlugin +import app.termora.plugin.internal.telnet.TelnetInternalPlugin import app.termora.plugin.internal.wsl.WSLInternalPlugin import app.termora.swingCoroutineScope import app.termora.terminal.panel.vw.FloatingToolbarPlugin @@ -111,12 +112,14 @@ internal class PluginManager private constructor() { // ssh plugin plugins.add(PluginDescriptor(SSHInternalPlugin(), origin = PluginOrigin.Internal, version = version)) - // serial plugin - plugins.add(PluginDescriptor(SerialInternalPlugin(), origin = PluginOrigin.Internal, version = version)) // local plugin plugins.add(PluginDescriptor(LocalInternalPlugin(), origin = PluginOrigin.Internal, version = version)) // rdp plugin plugins.add(PluginDescriptor(RDPInternalPlugin(), origin = PluginOrigin.Internal, version = version)) + // telnet plugin + plugins.add(PluginDescriptor(TelnetInternalPlugin(), origin = PluginOrigin.Internal, version = version)) + // serial plugin + plugins.add(PluginDescriptor(SerialInternalPlugin(), origin = PluginOrigin.Internal, version = version)) // wsl plugin if (SystemUtils.IS_OS_WINDOWS) { plugins.add(PluginDescriptor(WSLInternalPlugin(), origin = PluginOrigin.Internal, version = version)) diff --git a/src/main/kotlin/app/termora/plugin/internal/BasicProxyOption.kt b/src/main/kotlin/app/termora/plugin/internal/BasicProxyOption.kt index 437e39b..d14b09b 100644 --- a/src/main/kotlin/app/termora/plugin/internal/BasicProxyOption.kt +++ b/src/main/kotlin/app/termora/plugin/internal/BasicProxyOption.kt @@ -10,7 +10,10 @@ import java.awt.Component import java.awt.event.ItemEvent import javax.swing.* -class BasicProxyOption(private val proxyTypes: List = listOf(ProxyType.HTTP, ProxyType.SOCKS5)) : +class BasicProxyOption( + private val proxyTypes: List = listOf(ProxyType.HTTP, ProxyType.SOCKS5), + private val authenticationTypes: List = listOf(AuthenticationType.Password), +) : JPanel(BorderLayout()), Option { private val formMargin = "7dlu" @@ -67,7 +70,9 @@ class BasicProxyOption(private val proxyTypes: List = listOf(ProxyTyp } proxyAuthenticationTypeComboBox.addItem(AuthenticationType.No) - proxyAuthenticationTypeComboBox.addItem(AuthenticationType.Password) + for (type in authenticationTypes) { + proxyAuthenticationTypeComboBox.addItem(type) + } proxyUsernameTextField.text = "root" diff --git a/src/main/kotlin/app/termora/plugin/internal/local/LocalProtocolHostPanelExtension.kt b/src/main/kotlin/app/termora/plugin/internal/local/LocalProtocolHostPanelExtension.kt index 7481278..635d7d2 100644 --- a/src/main/kotlin/app/termora/plugin/internal/local/LocalProtocolHostPanelExtension.kt +++ b/src/main/kotlin/app/termora/plugin/internal/local/LocalProtocolHostPanelExtension.kt @@ -18,4 +18,8 @@ internal class LocalProtocolHostPanelExtension private constructor() : ProtocolH override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel { return LocalProtocolHostPanel() } + + override fun ordered(): Long { + return 1 + } } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/plugin/internal/rdp/RDPProtocolHostPanelExtension.kt b/src/main/kotlin/app/termora/plugin/internal/rdp/RDPProtocolHostPanelExtension.kt index 79a2436..38ffad4 100644 --- a/src/main/kotlin/app/termora/plugin/internal/rdp/RDPProtocolHostPanelExtension.kt +++ b/src/main/kotlin/app/termora/plugin/internal/rdp/RDPProtocolHostPanelExtension.kt @@ -18,4 +18,8 @@ internal class RDPProtocolHostPanelExtension private constructor() : ProtocolHos override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel { return RDPProtocolHostPanel() } + + override fun ordered(): Long { + return 2 + } } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/plugin/internal/serial/SerialProtocolHostPanelExtension.kt b/src/main/kotlin/app/termora/plugin/internal/serial/SerialProtocolHostPanelExtension.kt index 326daaf..8b16e2f 100644 --- a/src/main/kotlin/app/termora/plugin/internal/serial/SerialProtocolHostPanelExtension.kt +++ b/src/main/kotlin/app/termora/plugin/internal/serial/SerialProtocolHostPanelExtension.kt @@ -19,4 +19,7 @@ internal class SerialProtocolHostPanelExtension private constructor() : Protocol return SerialProtocolHostPanel() } + override fun ordered(): Long { + return 5 + } } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/plugin/internal/ssh/SSHProtocolHostPanelExtension.kt b/src/main/kotlin/app/termora/plugin/internal/ssh/SSHProtocolHostPanelExtension.kt index a203960..3295ded 100644 --- a/src/main/kotlin/app/termora/plugin/internal/ssh/SSHProtocolHostPanelExtension.kt +++ b/src/main/kotlin/app/termora/plugin/internal/ssh/SSHProtocolHostPanelExtension.kt @@ -19,4 +19,8 @@ internal class SSHProtocolHostPanelExtension private constructor() : ProtocolHos return SSHProtocolHostPanel(accountOwner) } + override fun ordered(): Long { + return 0 + } + } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetHostOptionsPane.kt b/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetHostOptionsPane.kt new file mode 100644 index 0000000..11b2de4 --- /dev/null +++ b/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetHostOptionsPane.kt @@ -0,0 +1,422 @@ +package app.termora.plugin.internal.telnet + +import app.termora.* +import app.termora.account.AccountOwner +import app.termora.keymgr.KeyManager +import app.termora.plugin.internal.BasicProxyOption +import com.formdev.flatlaf.FlatClientProperties +import com.formdev.flatlaf.extras.components.FlatComboBox +import com.formdev.flatlaf.ui.FlatTextBorder +import com.jgoodies.forms.builder.FormBuilder +import com.jgoodies.forms.layout.FormLayout +import org.apache.commons.lang3.StringUtils +import java.awt.BorderLayout +import java.awt.Component +import java.awt.KeyboardFocusManager +import java.awt.Window +import java.awt.event.ComponentAdapter +import java.awt.event.ComponentEvent +import java.nio.charset.Charset +import javax.swing.* + +@Suppress("CascadeIf") +open class TelnetHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPane() { + protected val generalOption = GeneralOption() + + // telnet 不支持代理密码 + protected val proxyOption = BasicProxyOption(authenticationTypes = listOf()) + protected val terminalOption = TerminalOption() + protected val owner: Window get() = SwingUtilities.getWindowAncestor(this) + + init { + addOption(generalOption) + addOption(proxyOption) + addOption(terminalOption) + } + + + open fun getHost(): Host { + val name = generalOption.nameTextField.text + val protocol = TelnetProtocolProvider.PROTOCOL + val host = generalOption.hostTextField.text + val port = (generalOption.portTextField.value ?: 22) as Int + var authentication = Authentication.No + var proxy = Proxy.Companion.No + val authenticationType = generalOption.authenticationTypeComboBox.selectedItem as AuthenticationType + + if (authenticationType == AuthenticationType.Password) { + authentication = authentication.copy( + type = authenticationType, + password = String(generalOption.passwordTextField.password) + ) + } + + if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) { + proxy = proxy.copy( + type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType, + host = proxyOption.proxyHostTextField.text, + username = proxyOption.proxyUsernameTextField.text, + password = String(proxyOption.proxyPasswordTextField.password), + port = proxyOption.proxyPortTextField.value as Int, + authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType, + ) + } + + + val serialComm = SerialComm() + + val options = Options.Companion.Default.copy( + encoding = terminalOption.charsetComboBox.selectedItem as String, + env = terminalOption.environmentTextArea.text, + startupCommand = terminalOption.startupCommandTextField.text, + serialComm = serialComm, + ) + + return Host( + name = name, + protocol = protocol, + host = host, + port = port, + username = generalOption.usernameTextField.text, + authentication = authentication, + proxy = proxy, + sort = System.currentTimeMillis(), + remark = generalOption.remarkTextArea.text, + options = options, + ) + } + + fun setHost(host: Host) { + generalOption.portTextField.value = host.port + generalOption.nameTextField.text = host.name + generalOption.usernameTextField.text = host.username + generalOption.hostTextField.text = host.host + generalOption.remarkTextArea.text = host.remark + generalOption.authenticationTypeComboBox.selectedItem = host.authentication.type + if (host.authentication.type == AuthenticationType.Password) { + generalOption.passwordTextField.text = host.authentication.password + } + proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type + proxyOption.proxyHostTextField.text = host.proxy.host + proxyOption.proxyPasswordTextField.text = host.proxy.password + proxyOption.proxyUsernameTextField.text = host.proxy.username + proxyOption.proxyPortTextField.value = host.proxy.port + proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType + + terminalOption.charsetComboBox.selectedItem = host.options.encoding + terminalOption.environmentTextArea.text = host.options.env + terminalOption.startupCommandTextField.text = host.options.startupCommand + + } + + fun validateFields(): Boolean { + val host = getHost() + + // general + if (validateField(generalOption.nameTextField) + || validateField(generalOption.hostTextField) + ) { + return false + } + + if (StringUtils.equalsIgnoreCase(host.protocol, TelnetProtocolProvider.PROTOCOL)) { + if (validateField(generalOption.usernameTextField)) { + return false + } + } + + if (host.authentication.type == AuthenticationType.Password) { + if (validateField(generalOption.passwordTextField)) { + return false + } + } else if (host.authentication.type == AuthenticationType.PublicKey) { + if (validateField(generalOption.publicKeyComboBox)) { + return false + } + } + + // proxy + if (host.proxy.type != ProxyType.No) { + if (validateField(proxyOption.proxyHostTextField) + ) { + return false + } + + if (host.proxy.authenticationType != AuthenticationType.No) { + if (validateField(proxyOption.proxyUsernameTextField) + || validateField(proxyOption.proxyPasswordTextField) + ) { + return false + } + } + } + + return true + } + + /** + * 返回 true 表示有错误 + */ + private fun validateField(textField: JTextField): Boolean { + if (textField.isEnabled && textField.text.isBlank()) { + setOutlineError(textField) + return true + } + return false + } + + private fun setOutlineError(textField: JTextField) { + selectOptionJComponent(textField) + textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR) + textField.requestFocusInWindow() + } + + /** + * 返回 true 表示有错误 + */ + private fun validateField(comboBox: JComboBox<*>): Boolean { + val selectedItem = comboBox.selectedItem + if (comboBox.isEnabled && (selectedItem == null || (selectedItem is String && selectedItem.isBlank()))) { + selectOptionJComponent(comboBox) + comboBox.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR) + comboBox.requestFocusInWindow() + return true + } + return false + } + + protected inner class GeneralOption : JPanel(BorderLayout()), Option { + val portTextField = PortSpinner() + val nameTextField = OutlineTextField(128) + val usernameTextField = OutlineTextField(128) + val hostTextField = OutlineTextField(255) + val passwordTextField = OutlinePasswordField(255) + val publicKeyComboBox = OutlineComboBox() + val remarkTextArea = FixedLengthTextArea(512) + val authenticationTypeComboBox = FlatComboBox() + + init { + initView() + initEvents() + } + + private fun initView() { + add(getCenterComponent(), BorderLayout.CENTER) + + publicKeyComboBox.isEditable = false + + publicKeyComboBox.renderer = object : DefaultListCellRenderer() { + override fun getListCellRendererComponent( + list: JList<*>?, + value: Any?, + index: Int, + isSelected: Boolean, + cellHasFocus: Boolean + ): Component { + var text = StringUtils.EMPTY + if (value is String) { + text = KeyManager.getInstance().getOhKeyPair(value)?.name ?: text + } + return super.getListCellRendererComponent( + list, + text, + index, + isSelected, + cellHasFocus + ) + } + } + + authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() { + override fun getListCellRendererComponent( + list: JList<*>?, + value: Any?, + index: Int, + isSelected: Boolean, + cellHasFocus: Boolean + ): Component { + var text = value?.toString() ?: "" + when (value) { + AuthenticationType.Password -> { + text = "Password" + } + + AuthenticationType.PublicKey -> { + text = "Public Key" + } + + AuthenticationType.KeyboardInteractive -> { + text = "Keyboard Interactive" + } + } + return super.getListCellRendererComponent( + list, + text, + index, + isSelected, + cellHasFocus + ) + } + } + + authenticationTypeComboBox.addItem(AuthenticationType.No) + authenticationTypeComboBox.addItem(AuthenticationType.Password) + + authenticationTypeComboBox.selectedItem = AuthenticationType.Password + + } + + private fun initEvents() { + addComponentListener(object : ComponentAdapter() { + override fun componentResized(e: ComponentEvent) { + SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() } + removeComponentListener(this) + } + }) + } + + override fun getIcon(isSelected: Boolean): Icon { + return Icons.settings + } + + override fun getTitle(): String { + return I18n.getString("termora.new-host.general") + } + + override fun getJComponent(): JComponent { + return this + } + + private fun getCenterComponent(): JComponent { + val layout = FormLayout( + "left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default", + "pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref" + ) + remarkTextArea.setFocusTraversalKeys( + KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, + KeyboardFocusManager.getCurrentKeyboardFocusManager() + .getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS) + ) + remarkTextArea.setFocusTraversalKeys( + KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, + KeyboardFocusManager.getCurrentKeyboardFocusManager() + .getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS) + ) + + remarkTextArea.rows = 8 + remarkTextArea.lineWrap = true + remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4) + + + var rows = 1 + val step = 2 + val panel = FormBuilder.create().layout(layout) + .add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows) + .add(nameTextField).xyw(3, rows, 5).apply { rows += step } + + .add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows) + .add(hostTextField).xy(3, rows) + .add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows) + .add(portTextField).xy(7, rows).apply { rows += step } + + .add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows) + .add(usernameTextField).xyw(3, rows, 5).apply { rows += step } + + .add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, rows) + .add(authenticationTypeComboBox).xyw(3, rows, 5).apply { rows += step } + + .add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows) + .add(passwordTextField).xyw(3, rows, 5).apply { rows += step } + + .add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows) + .add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() }) + .xyw(3, rows, 5).apply { rows += step } + + .build() + + + return panel + } + + } + + + protected inner class TerminalOption : JPanel(BorderLayout()), Option { + val charsetComboBox = JComboBox() + val startupCommandTextField = OutlineTextField() + val environmentTextArea = FixedLengthTextArea(2048) + + + init { + initView() + initEvents() + } + + private fun initView() { + add(getCenterComponent(), BorderLayout.CENTER) + + + environmentTextArea.setFocusTraversalKeys( + KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, + KeyboardFocusManager.getCurrentKeyboardFocusManager() + .getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS) + ) + environmentTextArea.setFocusTraversalKeys( + KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, + KeyboardFocusManager.getCurrentKeyboardFocusManager() + .getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS) + ) + + environmentTextArea.rows = 8 + environmentTextArea.lineWrap = true + environmentTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4) + + for (e in Charset.availableCharsets()) { + charsetComboBox.addItem(e.key) + } + + charsetComboBox.selectedItem = "UTF-8" + + } + + private fun initEvents() { + + } + + + override fun getIcon(isSelected: Boolean): Icon { + return Icons.terminal + } + + override fun getTitle(): String { + return I18n.getString("termora.new-host.terminal") + } + + override fun getJComponent(): JComponent { + return this + } + + private fun getCenterComponent(): JComponent { + val layout = FormLayout( + "left:pref, $FORM_MARGIN, default:grow", + "pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref" + ) + + var rows = 1 + val step = 2 + val panel = FormBuilder.create().layout(layout) + .add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows) + .add(charsetComboBox).xy(3, rows).apply { rows += step } + .add("${I18n.getString("termora.new-host.terminal.startup-commands")}:").xy(1, rows) + .add(startupCommandTextField).xy(3, rows).apply { rows += step } + .add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows) + .add(JScrollPane(environmentTextArea).apply { border = FlatTextBorder() }).xy(3, rows) + .apply { rows += step } + .build() + + + return panel + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetInternalPlugin.kt b/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetInternalPlugin.kt new file mode 100644 index 0000000..edf263e --- /dev/null +++ b/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetInternalPlugin.kt @@ -0,0 +1,24 @@ +package app.termora.plugin.internal.telnet + +import app.termora.plugin.Extension +import app.termora.plugin.InternalPlugin +import app.termora.protocol.ProtocolHostPanelExtension +import app.termora.protocol.ProtocolProviderExtension + +internal class TelnetInternalPlugin : InternalPlugin() { + init { + support.addExtension(ProtocolProviderExtension::class.java) { TelnetProtocolProviderExtension.instance } + support.addExtension(ProtocolHostPanelExtension::class.java) { TelnetProtocolHostPanelExtension.instance } + } + + override fun getName(): String { + return "Telnet Protocol" + } + + + override fun getExtensions(clazz: Class): List { + return support.getExtensions(clazz) + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetProtocolHostPanel.kt b/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetProtocolHostPanel.kt new file mode 100644 index 0000000..3c2967b --- /dev/null +++ b/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetProtocolHostPanel.kt @@ -0,0 +1,38 @@ +package app.termora.plugin.internal.telnet + +import app.termora.Disposer +import app.termora.Host +import app.termora.account.AccountOwner +import app.termora.protocol.ProtocolHostPanel +import java.awt.BorderLayout + +class TelnetProtocolHostPanel(accountOwner: AccountOwner) : ProtocolHostPanel() { + + private val pane = TelnetHostOptionsPane(accountOwner) + + init { + initView() + initEvents() + } + + + private fun initView() { + add(pane, BorderLayout.CENTER) + Disposer.register(this, pane) + } + + private fun initEvents() {} + + + override fun getHost(): Host { + return pane.getHost() + } + + override fun setHost(host: Host) { + pane.setHost(host) + } + + override fun validateFields(): Boolean { + return pane.validateFields() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetProtocolHostPanelExtension.kt b/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetProtocolHostPanelExtension.kt new file mode 100644 index 0000000..8fa7062 --- /dev/null +++ b/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetProtocolHostPanelExtension.kt @@ -0,0 +1,25 @@ +package app.termora.plugin.internal.telnet + +import app.termora.account.AccountOwner +import app.termora.protocol.ProtocolHostPanel +import app.termora.protocol.ProtocolHostPanelExtension +import app.termora.protocol.ProtocolProvider + +internal class TelnetProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension { + companion object { + val instance = TelnetProtocolHostPanelExtension() + + } + + override fun getProtocolProvider(): ProtocolProvider { + return TelnetProtocolProvider.instance + } + + override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel { + return TelnetProtocolHostPanel(accountOwner) + } + + override fun ordered(): Long { + return 4 + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetProtocolProvider.kt b/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetProtocolProvider.kt new file mode 100644 index 0000000..9c8e3c1 --- /dev/null +++ b/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetProtocolProvider.kt @@ -0,0 +1,27 @@ +package app.termora.plugin.internal.telnet + +import app.termora.* +import app.termora.actions.DataProvider +import app.termora.protocol.GenericProtocolProvider +import app.termora.protocol.ProtocolTester + +internal class TelnetProtocolProvider private constructor() : GenericProtocolProvider, ProtocolTester { + companion object { + val instance by lazy { TelnetProtocolProvider() } + const val PROTOCOL = "Telnet" + } + + override fun getProtocol(): String { + return PROTOCOL + } + + override fun createTerminalTab(dataProvider: DataProvider, windowScope: WindowScope, host: Host): TerminalTab { + return TelnetTerminalTab(windowScope, host) + } + + override fun getIcon(width: Int, height: Int): DynamicIcon { + return Icons.telnet + } + + override fun ordered() = Int.MIN_VALUE +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetProtocolProviderExtension.kt b/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetProtocolProviderExtension.kt new file mode 100644 index 0000000..337ac0f --- /dev/null +++ b/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetProtocolProviderExtension.kt @@ -0,0 +1,14 @@ +package app.termora.plugin.internal.telnet + +import app.termora.protocol.ProtocolProvider +import app.termora.protocol.ProtocolProviderExtension + +internal class TelnetProtocolProviderExtension private constructor() : ProtocolProviderExtension { + companion object { + val instance = TelnetProtocolProviderExtension() + } + + override fun getProtocolProvider(): ProtocolProvider { + return TelnetProtocolProvider.instance + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetStreamPtyConnector.kt b/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetStreamPtyConnector.kt new file mode 100644 index 0000000..ca845d1 --- /dev/null +++ b/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetStreamPtyConnector.kt @@ -0,0 +1,42 @@ +package app.termora.plugin.internal.telnet + +import app.termora.terminal.StreamPtyConnector +import org.apache.commons.net.telnet.TelnetClient +import org.apache.commons.net.telnet.TelnetOption +import org.apache.commons.net.telnet.WindowSizeOptionHandler +import java.io.InputStreamReader +import java.nio.charset.Charset + +class TelnetStreamPtyConnector( + private val telnet: TelnetClient, + private val charset: Charset +) : + StreamPtyConnector(telnet.inputStream, telnet.outputStream) { + private val reader = InputStreamReader(telnet.inputStream, getCharset()) + + override fun read(buffer: CharArray): Int { + return reader.read(buffer) + } + + override fun write(buffer: ByteArray, offset: Int, len: Int) { + output.write(buffer, offset, len) + output.flush() + } + + override fun resize(rows: Int, cols: Int) { + telnet.deleteOptionHandler(TelnetOption.WINDOW_SIZE) + telnet.addOptionHandler(WindowSizeOptionHandler(cols, rows, true, false, true, false)) + } + + override fun waitFor(): Int { + return -1 + } + + override fun close() { + telnet.disconnect() + } + + override fun getCharset(): Charset { + return charset + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetTerminalTab.kt b/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetTerminalTab.kt new file mode 100644 index 0000000..c5f7f41 --- /dev/null +++ b/src/main/kotlin/app/termora/plugin/internal/telnet/TelnetTerminalTab.kt @@ -0,0 +1,79 @@ +package app.termora.plugin.internal.telnet + +import app.termora.* +import app.termora.terminal.PtyConnector +import org.apache.commons.net.telnet.* +import java.net.InetSocketAddress +import java.net.Proxy +import java.nio.charset.Charset + +class TelnetTerminalTab( + windowScope: WindowScope, host: Host, +) : PtyHostTerminalTab(windowScope, host) { + override suspend fun openPtyConnector(): PtyConnector { + val winSize = terminalPanel.winSize() + val telnet = TelnetClient() + telnet.charset = Charset.forName(host.options.encoding) + telnet.connectTimeout = 60 * 1000 + + if (host.proxy.type == ProxyType.HTTP) { + telnet.proxy = Proxy( + Proxy.Type.HTTP, + InetSocketAddress(host.proxy.host, host.proxy.port) + ) + } else if (host.proxy.type == ProxyType.SOCKS5) { + telnet.proxy = Proxy( + Proxy.Type.SOCKS, + InetSocketAddress(host.proxy.host, host.proxy.port) + ) + } + + val termtype = host.options.envs()["TERM"] ?: "xterm-256color" + val ttopt = TerminalTypeOptionHandler(termtype, false, false, true, false) + val echoopt = EchoOptionHandler(false, true, false, true) + val gaopt = SuppressGAOptionHandler(true, true, true, true) + val wsopt = WindowSizeOptionHandler(winSize.cols, winSize.rows, true, false, true, false) + + telnet.addOptionHandler(ttopt) + telnet.addOptionHandler(echoopt) + telnet.addOptionHandler(gaopt) + telnet.addOptionHandler(wsopt) + + telnet.connect(host.host, host.port) + telnet.keepAlive = true + + return ptyConnectorFactory.decorate(TelnetStreamPtyConnector(telnet, telnet.charset)) + } + + + override fun loginScriptsPtyConnector(host: Host, ptyConnector: PtyConnector): PtyConnector { + if (host.authentication.type != AuthenticationType.Password) { + return ptyConnector + } + + val scripts = mutableListOf() + scripts.add( + LoginScript( + expect = "login:", + send = host.username, + regex = false, + matchCase = false + ) + ) + + scripts.add( + LoginScript( + expect = "password:", + send = host.authentication.password, + regex = false, + matchCase = false + ) + ) + + return super.loginScriptsPtyConnector( + host.copy(options = host.options.copy(loginScripts = scripts)), + ptyConnector + ) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/protocol/ProtocolHostPanelExtension.kt b/src/main/kotlin/app/termora/protocol/ProtocolHostPanelExtension.kt index 3a51055..264df80 100644 --- a/src/main/kotlin/app/termora/protocol/ProtocolHostPanelExtension.kt +++ b/src/main/kotlin/app/termora/protocol/ProtocolHostPanelExtension.kt @@ -10,7 +10,6 @@ interface ProtocolHostPanelExtension : Extension { val extensions get() = ExtensionManager.getInstance() .getExtensions(ProtocolHostPanelExtension::class.java) - .sortedBy { it.getProtocolProvider().ordered() } } /** diff --git a/src/main/resources/icons/telnet.svg b/src/main/resources/icons/telnet.svg new file mode 100644 index 0000000..e66bb1e --- /dev/null +++ b/src/main/resources/icons/telnet.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/icons/telnet_dark.svg b/src/main/resources/icons/telnet_dark.svg new file mode 100644 index 0000000..b2532f2 --- /dev/null +++ b/src/main/resources/icons/telnet_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file