feat: support telnet

This commit is contained in:
hstyi
2025-07-05 13:58:36 +08:00
committed by hstyi
parent a32838dad6
commit 8cec835583
18 changed files with 701 additions and 5 deletions

View File

@@ -78,6 +78,7 @@ object Icons {
val import by lazy { DynamicIcon("icons/import.svg", "icons/import_dark.svg") } 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 export by lazy { DynamicIcon("icons/export.svg", "icons/export_dark.svg") }
val terminal by lazy { DynamicIcon("icons/terminal.svg", "icons/terminal_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 ssh by lazy { DynamicIcon("icons/ssh.svg", "icons/ssh_dark.svg") }
val ftp by lazy { DynamicIcon("icons/ftp.svg", "icons/ftp_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") } val minio by lazy { DynamicIcon("icons/minio.svg", "icons/minio_dark.svg") }

View File

@@ -11,6 +11,7 @@ import app.termora.plugin.internal.rdp.RDPInternalPlugin
import app.termora.plugin.internal.serial.SerialInternalPlugin import app.termora.plugin.internal.serial.SerialInternalPlugin
import app.termora.plugin.internal.sftppty.SFTPPtyInternalPlugin import app.termora.plugin.internal.sftppty.SFTPPtyInternalPlugin
import app.termora.plugin.internal.ssh.SSHInternalPlugin import app.termora.plugin.internal.ssh.SSHInternalPlugin
import app.termora.plugin.internal.telnet.TelnetInternalPlugin
import app.termora.plugin.internal.wsl.WSLInternalPlugin import app.termora.plugin.internal.wsl.WSLInternalPlugin
import app.termora.swingCoroutineScope import app.termora.swingCoroutineScope
import app.termora.terminal.panel.vw.FloatingToolbarPlugin import app.termora.terminal.panel.vw.FloatingToolbarPlugin
@@ -111,12 +112,14 @@ internal class PluginManager private constructor() {
// ssh plugin // ssh plugin
plugins.add(PluginDescriptor(SSHInternalPlugin(), origin = PluginOrigin.Internal, version = version)) plugins.add(PluginDescriptor(SSHInternalPlugin(), origin = PluginOrigin.Internal, version = version))
// serial plugin
plugins.add(PluginDescriptor(SerialInternalPlugin(), origin = PluginOrigin.Internal, version = version))
// local plugin // local plugin
plugins.add(PluginDescriptor(LocalInternalPlugin(), origin = PluginOrigin.Internal, version = version)) plugins.add(PluginDescriptor(LocalInternalPlugin(), origin = PluginOrigin.Internal, version = version))
// rdp plugin // rdp plugin
plugins.add(PluginDescriptor(RDPInternalPlugin(), origin = PluginOrigin.Internal, version = version)) 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 // wsl plugin
if (SystemUtils.IS_OS_WINDOWS) { if (SystemUtils.IS_OS_WINDOWS) {
plugins.add(PluginDescriptor(WSLInternalPlugin(), origin = PluginOrigin.Internal, version = version)) plugins.add(PluginDescriptor(WSLInternalPlugin(), origin = PluginOrigin.Internal, version = version))

View File

@@ -10,7 +10,10 @@ import java.awt.Component
import java.awt.event.ItemEvent import java.awt.event.ItemEvent
import javax.swing.* import javax.swing.*
class BasicProxyOption(private val proxyTypes: List<ProxyType> = listOf(ProxyType.HTTP, ProxyType.SOCKS5)) : class BasicProxyOption(
private val proxyTypes: List<ProxyType> = listOf(ProxyType.HTTP, ProxyType.SOCKS5),
private val authenticationTypes: List<AuthenticationType> = listOf(AuthenticationType.Password),
) :
JPanel(BorderLayout()), Option { JPanel(BorderLayout()), Option {
private val formMargin = "7dlu" private val formMargin = "7dlu"
@@ -67,7 +70,9 @@ class BasicProxyOption(private val proxyTypes: List<ProxyType> = listOf(ProxyTyp
} }
proxyAuthenticationTypeComboBox.addItem(AuthenticationType.No) proxyAuthenticationTypeComboBox.addItem(AuthenticationType.No)
proxyAuthenticationTypeComboBox.addItem(AuthenticationType.Password) for (type in authenticationTypes) {
proxyAuthenticationTypeComboBox.addItem(type)
}
proxyUsernameTextField.text = "root" proxyUsernameTextField.text = "root"

View File

@@ -18,4 +18,8 @@ internal class LocalProtocolHostPanelExtension private constructor() : ProtocolH
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel { override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return LocalProtocolHostPanel() return LocalProtocolHostPanel()
} }
override fun ordered(): Long {
return 1
}
} }

View File

@@ -18,4 +18,8 @@ internal class RDPProtocolHostPanelExtension private constructor() : ProtocolHos
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel { override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return RDPProtocolHostPanel() return RDPProtocolHostPanel()
} }
override fun ordered(): Long {
return 2
}
} }

View File

@@ -19,4 +19,7 @@ internal class SerialProtocolHostPanelExtension private constructor() : Protocol
return SerialProtocolHostPanel() return SerialProtocolHostPanel()
} }
override fun ordered(): Long {
return 5
}
} }

View File

@@ -19,4 +19,8 @@ internal class SSHProtocolHostPanelExtension private constructor() : ProtocolHos
return SSHProtocolHostPanel(accountOwner) return SSHProtocolHostPanel(accountOwner)
} }
override fun ordered(): Long {
return 0
}
} }

View File

@@ -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<String>()
val remarkTextArea = FixedLengthTextArea(512)
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
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<String>()
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
}
}
}

View File

@@ -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 <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -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()
}
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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<LoginScript>()
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
)
}
}

View File

@@ -10,7 +10,6 @@ interface ProtocolHostPanelExtension : Extension {
val extensions val extensions
get() = ExtensionManager.getInstance() get() = ExtensionManager.getInstance()
.getExtensions(ProtocolHostPanelExtension::class.java) .getExtensions(ProtocolHostPanelExtension::class.java)
.sortedBy { it.getProtocolProvider().ordered() }
} }
/** /**

View File

@@ -0,0 +1 @@
<svg t="1751694328543" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M889.5623525 160.37019636v579.50117625H134.4376475V160.31372511h755.124705z m-755.124705-56.47058813a56.47058813 56.47058813 0 0 0-56.47058906 56.47058813v579.50117625a56.47058813 56.47058813 0 0 0 56.47058906 56.47058812h755.124705a56.47058813 56.47058813 0 0 0 56.47058906-56.47058812V160.31372511a56.47058813 56.47058813 0 0 0-56.47058906-56.47058813H134.4376475zM733.53411781 915.26901917H290.46588219v-56.47058813h443.06823562v56.47058813z" p-id="1613" fill="#6C707E"></path><path d="M672.65882375 589.60313698v-278.96470593h43.87764656v241.46823562h118.08v37.49647031h-161.95764656zM445.70352969 589.60313698v-278.96470593h164.66823469v37.10117718H489.6376475v77.59058813h102.21176438V462.43137261h-102.21176438v89.67529406h124.85647031v37.49647031H445.70352969zM270.7576475 589.60313698V347.73960823H189.38352969v-37.10117719h207.81176437v37.10117719H315.36941187v241.86352875h-44.66823562z" p-id="1614" fill="#6C707E"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg t="1751694328543" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M889.5623525 160.37019636v579.50117625H134.4376475V160.31372511h755.124705z m-755.124705-56.47058813a56.47058813 56.47058813 0 0 0-56.47058906 56.47058813v579.50117625a56.47058813 56.47058813 0 0 0 56.47058906 56.47058812h755.124705a56.47058813 56.47058813 0 0 0 56.47058906-56.47058812V160.31372511a56.47058813 56.47058813 0 0 0-56.47058906-56.47058813H134.4376475zM733.53411781 915.26901917H290.46588219v-56.47058813h443.06823562v56.47058813z" p-id="1613" fill="#CED0D6"></path><path d="M672.65882375 589.60313698v-278.96470593h43.87764656v241.46823562h118.08v37.49647031h-161.95764656zM445.70352969 589.60313698v-278.96470593h164.66823469v37.10117718H489.6376475v77.59058813h102.21176438V462.43137261h-102.21176438v89.67529406h124.85647031v37.49647031H445.70352969zM270.7576475 589.60313698V347.73960823H189.38352969v-37.10117719h207.81176437v37.10117719H315.36941187v241.86352875h-44.66823562z" p-id="1614" fill="#CED0D6"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB