mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
feat: support X11 forwarding (#443)
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
|
||||||
@Suppress("CascadeIf")
|
@Suppress("CascadeIf")
|
||||||
class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
||||||
init {
|
init {
|
||||||
@@ -31,6 +33,8 @@ class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
|||||||
terminalOption.heartbeatIntervalTextField.value = host.options.heartbeatInterval
|
terminalOption.heartbeatIntervalTextField.value = host.options.heartbeatInterval
|
||||||
|
|
||||||
tunnelingOption.tunnelings.addAll(host.tunnelings)
|
tunnelingOption.tunnelings.addAll(host.tunnelings)
|
||||||
|
tunnelingOption.x11ForwardingCheckBox.isSelected = host.options.enableX11Forwarding
|
||||||
|
tunnelingOption.x11ServerTextField.text = StringUtils.defaultIfBlank(host.options.x11Forwarding, "localhost:0")
|
||||||
|
|
||||||
if (host.options.jumpHosts.isNotEmpty()) {
|
if (host.options.jumpHosts.isNotEmpty()) {
|
||||||
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
|
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
|
||||||
|
|||||||
@@ -138,6 +138,16 @@ data class Options(
|
|||||||
* SFTP 默认目录
|
* SFTP 默认目录
|
||||||
*/
|
*/
|
||||||
val sftpDefaultDirectory: String = StringUtils.EMPTY,
|
val sftpDefaultDirectory: String = StringUtils.EMPTY,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* X11 Forwarding
|
||||||
|
*/
|
||||||
|
val enableX11Forwarding: Boolean = false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* X11 Server,Format: host.port. default: localhost:0
|
||||||
|
*/
|
||||||
|
val x11Forwarding: String = StringUtils.EMPTY,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val Default = Options()
|
val Default = Options()
|
||||||
|
|||||||
@@ -103,7 +103,9 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
heartbeatInterval = (terminalOption.heartbeatIntervalTextField.value ?: 30) as Int,
|
heartbeatInterval = (terminalOption.heartbeatIntervalTextField.value ?: 30) as Int,
|
||||||
jumpHosts = jumpHostsOption.jumpHosts.map { it.id },
|
jumpHosts = jumpHostsOption.jumpHosts.map { it.id },
|
||||||
serialComm = serialComm,
|
serialComm = serialComm,
|
||||||
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text
|
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||||
|
enableX11Forwarding = tunnelingOption.x11ForwardingCheckBox.isSelected,
|
||||||
|
x11Forwarding = tunnelingOption.x11ServerTextField.text,
|
||||||
)
|
)
|
||||||
|
|
||||||
return Host(
|
return Host(
|
||||||
@@ -169,6 +171,17 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tunnel
|
||||||
|
if (tunnelingOption.x11ForwardingCheckBox.isSelected) {
|
||||||
|
if (validateField(tunnelingOption.x11ServerTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val segments = tunnelingOption.x11ServerTextField.text.split(":")
|
||||||
|
if (segments.size != 2 || segments[1].toIntOrNull() == null) {
|
||||||
|
setOutlineError(tunnelingOption.x11ServerTextField)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -178,14 +191,18 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
*/
|
*/
|
||||||
private fun validateField(textField: JTextField): Boolean {
|
private fun validateField(textField: JTextField): Boolean {
|
||||||
if (textField.isEnabled && textField.text.isBlank()) {
|
if (textField.isEnabled && textField.text.isBlank()) {
|
||||||
selectOptionJComponent(textField)
|
setOutlineError(textField)
|
||||||
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
|
||||||
textField.requestFocusInWindow()
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setOutlineError(textField: JTextField) {
|
||||||
|
selectOptionJComponent(textField)
|
||||||
|
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||||
|
textField.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 返回 true 表示有错误
|
* 返回 true 表示有错误
|
||||||
*/
|
*/
|
||||||
@@ -749,6 +766,8 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
protected inner class TunnelingOption : JPanel(BorderLayout()), Option {
|
protected inner class TunnelingOption : JPanel(BorderLayout()), Option {
|
||||||
val tunnelings = mutableListOf<Tunneling>()
|
val tunnelings = mutableListOf<Tunneling>()
|
||||||
|
val x11ForwardingCheckBox = JCheckBox("X DISPLAY:")
|
||||||
|
val x11ServerTextField = OutlineTextField(255)
|
||||||
|
|
||||||
private val model = object : DefaultTableModel() {
|
private val model = object : DefaultTableModel() {
|
||||||
override fun getRowCount(): Int {
|
override fun getRowCount(): Int {
|
||||||
@@ -823,13 +842,36 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
box.add(Box.createHorizontalStrut(4))
|
box.add(Box.createHorizontalStrut(4))
|
||||||
box.add(deleteBtn)
|
box.add(deleteBtn)
|
||||||
|
|
||||||
add(JLabel("TCP/IP Forwarding:"), BorderLayout.NORTH)
|
x11ForwardingCheckBox.isFocusable = false
|
||||||
add(scrollPane, BorderLayout.CENTER)
|
|
||||||
add(box, BorderLayout.SOUTH)
|
if (x11ServerTextField.text.isBlank()) {
|
||||||
|
x11ServerTextField.text = "localhost:0"
|
||||||
|
}
|
||||||
|
|
||||||
|
val x11Forwarding = Box.createHorizontalBox()
|
||||||
|
x11Forwarding.border = BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createTitledBorder("X11 Forwarding"),
|
||||||
|
BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||||
|
)
|
||||||
|
x11Forwarding.add(x11ForwardingCheckBox)
|
||||||
|
x11Forwarding.add(x11ServerTextField)
|
||||||
|
|
||||||
|
x11ServerTextField.isEnabled = x11ForwardingCheckBox.isSelected
|
||||||
|
|
||||||
|
val panel = JPanel(BorderLayout())
|
||||||
|
panel.add(JLabel("TCP/IP Forwarding:"), BorderLayout.NORTH)
|
||||||
|
panel.add(scrollPane, BorderLayout.CENTER)
|
||||||
|
panel.add(box, BorderLayout.SOUTH)
|
||||||
|
panel.border = BorderFactory.createEmptyBorder(0, 0, 8, 0)
|
||||||
|
|
||||||
|
add(panel, BorderLayout.CENTER)
|
||||||
|
add(x11Forwarding, BorderLayout.SOUTH)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
|
x11ForwardingCheckBox.addChangeListener { x11ServerTextField.isEnabled = x11ForwardingCheckBox.isSelected }
|
||||||
|
|
||||||
addBtn.addActionListener(object : AbstractAction() {
|
addBtn.addActionListener(object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent?) {
|
override fun actionPerformed(e: ActionEvent?) {
|
||||||
val dialog = PortForwardingDialog(SwingUtilities.getWindowAncestor(this@HostOptionsPane))
|
val dialog = PortForwardingDialog(SwingUtilities.getWindowAncestor(this@HostOptionsPane))
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package app.termora
|
|||||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||||
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||||
import app.termora.terminal.TerminalSize
|
import app.termora.terminal.TerminalSize
|
||||||
|
import app.termora.x11.ChannelX11
|
||||||
|
import app.termora.x11.X11ChannelFactory
|
||||||
import com.formdev.flatlaf.FlatLaf
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import com.formdev.flatlaf.util.FontUtils
|
import com.formdev.flatlaf.util.FontUtils
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
@@ -29,7 +31,10 @@ import org.apache.sshd.client.session.SessionFactory
|
|||||||
import org.apache.sshd.common.AttributeRepository
|
import org.apache.sshd.common.AttributeRepository
|
||||||
import org.apache.sshd.common.SshConstants
|
import org.apache.sshd.common.SshConstants
|
||||||
import org.apache.sshd.common.SshException
|
import org.apache.sshd.common.SshException
|
||||||
|
import org.apache.sshd.common.channel.ChannelFactory
|
||||||
import org.apache.sshd.common.channel.PtyChannelConfiguration
|
import org.apache.sshd.common.channel.PtyChannelConfiguration
|
||||||
|
import org.apache.sshd.common.channel.PtyChannelConfigurationHolder
|
||||||
|
import org.apache.sshd.common.cipher.CipherNone
|
||||||
import org.apache.sshd.common.config.keys.KeyRandomArt
|
import org.apache.sshd.common.config.keys.KeyRandomArt
|
||||||
import org.apache.sshd.common.config.keys.KeyUtils
|
import org.apache.sshd.common.config.keys.KeyUtils
|
||||||
import org.apache.sshd.common.future.CloseFuture
|
import org.apache.sshd.common.future.CloseFuture
|
||||||
@@ -75,6 +80,7 @@ import java.util.concurrent.atomic.AtomicBoolean
|
|||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
@Suppress("CascadeIf")
|
@Suppress("CascadeIf")
|
||||||
object SshClients {
|
object SshClients {
|
||||||
@@ -234,6 +240,18 @@ object SshClients {
|
|||||||
session.keyIdentityProvider = OhKeyPairKeyPairProvider(host.authentication.password)
|
session.keyIdentityProvider = OhKeyPairKeyPairProvider(host.authentication.password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (host.options.enableX11Forwarding) {
|
||||||
|
val segments = host.options.x11Forwarding.split(":")
|
||||||
|
if (segments.size == 2) {
|
||||||
|
val x11Host = segments[0]
|
||||||
|
val x11Port = segments[1].toIntOrNull()
|
||||||
|
if (x11Port != null) {
|
||||||
|
CoreModuleProperties.X11_BIND_HOST.set(session, x11Host)
|
||||||
|
CoreModuleProperties.X11_BASE_PORT.set(session, 6000 + x11Port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!session.auth().verify(timeout).await(timeout)) {
|
if (!session.auth().verify(timeout).await(timeout)) {
|
||||||
throw SshException("Authentication failed")
|
throw SshException("Authentication failed")
|
||||||
@@ -325,6 +343,11 @@ object SshClients {
|
|||||||
|
|
||||||
builder.hostConfigEntryResolver(HostConfigEntryResolver.EMPTY)
|
builder.hostConfigEntryResolver(HostConfigEntryResolver.EMPTY)
|
||||||
|
|
||||||
|
val channelFactories = mutableListOf<ChannelFactory>()
|
||||||
|
channelFactories.addAll(ClientBuilder.DEFAULT_CHANNEL_FACTORIES)
|
||||||
|
channelFactories.add(X11ChannelFactory.INSTANCE)
|
||||||
|
builder.channelFactories(channelFactories)
|
||||||
|
|
||||||
val sshClient = builder.build() as JGitSshClient
|
val sshClient = builder.build() as JGitSshClient
|
||||||
|
|
||||||
// https://github.com/TermoraDev/termora/issues/180
|
// https://github.com/TermoraDev/termora/issues/180
|
||||||
@@ -533,6 +556,21 @@ object SshClients {
|
|||||||
|
|
||||||
return clientProxyConnector
|
return clientProxyConnector
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun createShellChannel(
|
||||||
|
ptyConfig: PtyChannelConfigurationHolder?,
|
||||||
|
env: MutableMap<String, *>?
|
||||||
|
): ChannelShell {
|
||||||
|
if (inCipher is CipherNone || outCipher is CipherNone)
|
||||||
|
throw IllegalStateException("Interactive channels are not supported with none cipher")
|
||||||
|
val channel = MyChannelShell(ptyConfig, env)
|
||||||
|
val id = connectionService.registerChannel(channel)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("createShellChannel({}) created id={} - PTY={}", this, id, ptyConfig)
|
||||||
|
}
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -542,6 +580,63 @@ object SshClients {
|
|||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class MyChannelShell(
|
||||||
|
configHolder: PtyChannelConfigurationHolder?,
|
||||||
|
env: MutableMap<String, *>?
|
||||||
|
) : ChannelShell(configHolder, env) {
|
||||||
|
|
||||||
|
override fun doOpenPty() {
|
||||||
|
val session = super.getSession()
|
||||||
|
val x11Host = CoreModuleProperties.X11_BIND_HOST.getOrNull(session)
|
||||||
|
val x11Port = CoreModuleProperties.X11_BASE_PORT.getOrNull(session)
|
||||||
|
|
||||||
|
if (x11Port == null || x11Host == null) {
|
||||||
|
super.doOpenPty()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val buffer = session.createBuffer(SshConstants.SSH_MSG_CHANNEL_REQUEST)
|
||||||
|
buffer.putInt(super.getRecipient())
|
||||||
|
buffer.putString("x11-req")
|
||||||
|
buffer.putBoolean(false) // want-reply
|
||||||
|
buffer.putBoolean(false)
|
||||||
|
buffer.putString("MIT-MAGIC-COOKIE-1")
|
||||||
|
buffer.putBytes(getFakedCookie())
|
||||||
|
buffer.putInt(0)
|
||||||
|
|
||||||
|
writePacket(buffer)
|
||||||
|
|
||||||
|
super.doOpenPty()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFakedCookie(): ByteArray {
|
||||||
|
val session = super.getSession()
|
||||||
|
var cookie = ChannelX11.X11_COOKIE_HEX.getOrNull(session)
|
||||||
|
if (cookie != null) {
|
||||||
|
return cookie as ByteArray
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(session) {
|
||||||
|
cookie = ChannelX11.X11_COOKIE_HEX.getOrNull(session)
|
||||||
|
if (cookie != null) {
|
||||||
|
return cookie as ByteArray
|
||||||
|
}
|
||||||
|
|
||||||
|
val foo = Random.nextBytes(16)
|
||||||
|
ChannelX11.X11_COOKIE.set(session, foo)
|
||||||
|
|
||||||
|
val bar = foo.copyOf(32)
|
||||||
|
for (i in 0..15) {
|
||||||
|
bar[2 * i] = ChannelX11.COOKIE_TABLE[(foo[i].toInt() ushr 4) and 0xf]
|
||||||
|
bar[2 * i + 1] = ChannelX11.COOKIE_TABLE[foo[i].toInt() and 0xf]
|
||||||
|
}
|
||||||
|
ChannelX11.X11_COOKIE_HEX.set(session, bar)
|
||||||
|
|
||||||
|
return bar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class MyIoConnector(private val sshClient: MyJGitSshClient, private val ioConnector: IoConnector) :
|
private class MyIoConnector(private val sshClient: MyJGitSshClient, private val ioConnector: IoConnector) :
|
||||||
IoConnector {
|
IoConnector {
|
||||||
override fun close(immediately: Boolean): CloseFuture {
|
override fun close(immediately: Boolean): CloseFuture {
|
||||||
|
|||||||
117
src/main/kotlin/app/termora/x11/ChannelX11.kt
Normal file
117
src/main/kotlin/app/termora/x11/ChannelX11.kt
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package app.termora.x11
|
||||||
|
|
||||||
|
import org.apache.sshd.client.channel.AbstractClientChannel
|
||||||
|
import org.apache.sshd.client.future.DefaultOpenFuture
|
||||||
|
import org.apache.sshd.client.future.OpenFuture
|
||||||
|
import org.apache.sshd.client.session.ClientConnectionService
|
||||||
|
import org.apache.sshd.common.Property
|
||||||
|
import org.apache.sshd.common.SshConstants
|
||||||
|
import org.apache.sshd.common.channel.ChannelOutputStream
|
||||||
|
import org.apache.sshd.common.io.IoConnectFuture
|
||||||
|
import org.apache.sshd.common.io.IoSession
|
||||||
|
import org.apache.sshd.common.util.buffer.Buffer
|
||||||
|
import org.apache.sshd.common.util.buffer.ByteArrayBuffer
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
class ChannelX11(
|
||||||
|
private val host: String,
|
||||||
|
private val port: Int,
|
||||||
|
) : AbstractClientChannel("x11") {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val X11_COOKIE: Property<Any> = Property.`object`("x11-cookie")
|
||||||
|
val X11_COOKIE_HEX: Property<Any> = Property.`object`("x11-cookie-hex")
|
||||||
|
val COOKIE_TABLE = byteArrayOf(
|
||||||
|
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x61,
|
||||||
|
0x62, 0x63, 0x64, 0x65, 0x66
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var x11: IoSession
|
||||||
|
private val isInitialized = AtomicBoolean(false)
|
||||||
|
|
||||||
|
override fun open(recipient: Long, rwSize: Long, packetSize: Long, buffer: Buffer): OpenFuture {
|
||||||
|
val openFuture = DefaultOpenFuture(this, futureLock).apply { openFuture = this }
|
||||||
|
|
||||||
|
connectX11Server().addListener {
|
||||||
|
if (it.isConnected) {
|
||||||
|
this.x11 = it.session
|
||||||
|
handleOpenSuccess(recipient, rwSize, packetSize, buffer)
|
||||||
|
} else {
|
||||||
|
if (it.exception != null) {
|
||||||
|
openFuture.exception = it.exception
|
||||||
|
} else {
|
||||||
|
openFuture.value = false
|
||||||
|
}
|
||||||
|
unregisterSelf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return openFuture
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doOpen() {
|
||||||
|
this.out = ChannelOutputStream(
|
||||||
|
this, remoteWindow, log,
|
||||||
|
SshConstants.SSH_MSG_CHANNEL_DATA, true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connectX11Server(): IoConnectFuture {
|
||||||
|
val connector = session.factoryManager.ioServiceFactory.createConnector(X11IoHandler(this))
|
||||||
|
val future = connector.connect(InetSocketAddress(host, port), session, null)
|
||||||
|
addCloseFutureListener { if (it.isClosed) connector.close(true) }
|
||||||
|
return future
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun doWriteData(data: ByteArray, off: Int, len: Long) {
|
||||||
|
if (isInitialized.compareAndSet(false, true)) {
|
||||||
|
val cookie = X11_COOKIE.getOrNull(session) ?: return
|
||||||
|
val foo = data.copyOfRange(off, off + len.toInt())
|
||||||
|
val s = 0
|
||||||
|
val l = foo.size
|
||||||
|
if (l < 9) return
|
||||||
|
|
||||||
|
var plen = (foo[s + 6].toInt() and 0xff) * 256 + (foo[s + 7].toInt() and 0xff)
|
||||||
|
var dlen = (foo[s + 8].toInt() and 0xff) * 256 + (foo[s + 9].toInt() and 0xff)
|
||||||
|
if ((foo[s].toInt() and 0xff) == 0x6c) {
|
||||||
|
plen = ((plen ushr 8) and 0xff) or ((plen shl 8) and 0xff00)
|
||||||
|
dlen = ((dlen ushr 8) and 0xff) or ((dlen shl 8) and 0xff00)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (l < 12 + plen + ((-plen) and 3) + dlen) return
|
||||||
|
|
||||||
|
val bar = ByteArray(dlen)
|
||||||
|
System.arraycopy(foo, s + 12 + plen + ((-plen) and 3), bar, 0, dlen)
|
||||||
|
|
||||||
|
if (Objects.deepEquals(cookie, bar) && x11.isOpen) {
|
||||||
|
x11.writeBuffer(ByteArrayBuffer(foo, s, l))
|
||||||
|
} else {
|
||||||
|
sendEof()
|
||||||
|
}
|
||||||
|
} else if (x11.isOpen) {
|
||||||
|
x11.writeBuffer(ByteArrayBuffer(data, off, len.toInt()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleEof() {
|
||||||
|
super.handleEof()
|
||||||
|
close(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unregisterSelf() {
|
||||||
|
try {
|
||||||
|
session.getService(ClientConnectionService::class.java)
|
||||||
|
.unregisterChannel(this)
|
||||||
|
close(true)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
24
src/main/kotlin/app/termora/x11/X11ChannelFactory.kt
Normal file
24
src/main/kotlin/app/termora/x11/X11ChannelFactory.kt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package app.termora.x11
|
||||||
|
|
||||||
|
import org.apache.sshd.common.channel.Channel
|
||||||
|
import org.apache.sshd.common.channel.ChannelFactory
|
||||||
|
import org.apache.sshd.common.session.Session
|
||||||
|
import org.apache.sshd.core.CoreModuleProperties
|
||||||
|
|
||||||
|
class X11ChannelFactory private constructor() : ChannelFactory {
|
||||||
|
companion object {
|
||||||
|
val INSTANCE = X11ChannelFactory()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "x11"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createChannel(session: Session): Channel? {
|
||||||
|
val x11Host = CoreModuleProperties.X11_BIND_HOST.getOrNull(session)
|
||||||
|
val x11Port = CoreModuleProperties.X11_BASE_PORT.getOrNull(session)
|
||||||
|
if (x11Port == null || x11Host == null) return null
|
||||||
|
return ChannelX11(x11Host, x11Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
33
src/main/kotlin/app/termora/x11/X11IoHandler.kt
Normal file
33
src/main/kotlin/app/termora/x11/X11IoHandler.kt
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package app.termora.x11
|
||||||
|
|
||||||
|
import org.apache.sshd.common.io.IoSession
|
||||||
|
import org.apache.sshd.common.session.helpers.AbstractSession
|
||||||
|
import org.apache.sshd.common.session.helpers.AbstractSessionIoHandler
|
||||||
|
import org.apache.sshd.common.util.Readable
|
||||||
|
import org.apache.sshd.common.util.io.IoUtils
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class X11IoHandler(private val x11: ChannelX11) : AbstractSessionIoHandler() {
|
||||||
|
|
||||||
|
private val out get() = x11.out
|
||||||
|
|
||||||
|
override fun sessionClosed(ioSession: IoSession) {
|
||||||
|
x11.close(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun messageReceived(session: IoSession, message: Readable) {
|
||||||
|
val bytes = ByteArray(min(IoUtils.DEFAULT_COPY_SIZE, message.available()))
|
||||||
|
if (bytes.isEmpty()) return
|
||||||
|
while (message.available() > 0) {
|
||||||
|
val available = min(message.available(), bytes.size)
|
||||||
|
message.getRawBytes(bytes, 0, available)
|
||||||
|
out.write(bytes, 0, available)
|
||||||
|
}
|
||||||
|
out.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createSession(ioSession: IoSession): AbstractSession {
|
||||||
|
return x11.session as AbstractSession
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,45 +1,15 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import org.apache.sshd.sftp.client.impl.DefaultSftpClientFactory
|
import org.apache.sshd.sftp.client.impl.DefaultSftpClientFactory
|
||||||
import org.testcontainers.containers.GenericContainer
|
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import kotlin.test.AfterTest
|
|
||||||
import kotlin.test.BeforeTest
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class SFTPTest {
|
class SFTPTest : SSHDTest() {
|
||||||
private val sftpContainer = GenericContainer("linuxserver/openssh-server")
|
|
||||||
.withEnv("PUID", "1000")
|
|
||||||
.withEnv("PGID", "1000")
|
|
||||||
.withEnv("TZ", "Etc/UTC")
|
|
||||||
.withEnv("SUDO_ACCESS", "true")
|
|
||||||
.withEnv("PASSWORD_ACCESS", "true")
|
|
||||||
.withEnv("USER_NAME", "foo")
|
|
||||||
.withEnv("USER_PASSWORD", "pass")
|
|
||||||
.withEnv("SUDO_ACCESS", "true")
|
|
||||||
.withExposedPorts(2222)
|
|
||||||
|
|
||||||
@BeforeTest
|
|
||||||
fun setup() {
|
|
||||||
sftpContainer.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterTest
|
|
||||||
fun teardown() {
|
|
||||||
sftpContainer.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun test() {
|
fun test() {
|
||||||
val host = Host(
|
|
||||||
name = sftpContainer.containerName,
|
|
||||||
protocol = Protocol.SSH,
|
|
||||||
host = "127.0.0.1",
|
|
||||||
port = sftpContainer.getMappedPort(2222),
|
|
||||||
username = "foo",
|
|
||||||
authentication = Authentication.No.copy(type = AuthenticationType.Password, password = "pass"),
|
|
||||||
)
|
|
||||||
|
|
||||||
val client = SshClients.openClient(host)
|
val client = SshClients.openClient(host)
|
||||||
val session = SshClients.openSession(host, client)
|
val session = SshClients.openSession(host, client)
|
||||||
|
|||||||
41
src/test/kotlin/app/termora/SSHDTest.kt
Normal file
41
src/test/kotlin/app/termora/SSHDTest.kt
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import org.testcontainers.containers.GenericContainer
|
||||||
|
import kotlin.test.AfterTest
|
||||||
|
import kotlin.test.BeforeTest
|
||||||
|
|
||||||
|
|
||||||
|
abstract class SSHDTest {
|
||||||
|
protected val sshd: GenericContainer<*> = GenericContainer("sshd")
|
||||||
|
.withEnv("PUID", "1000")
|
||||||
|
.withEnv("PGID", "1000")
|
||||||
|
.withEnv("TZ", "Etc/UTC")
|
||||||
|
.withEnv("SUDO_ACCESS", "true")
|
||||||
|
.withEnv("PASSWORD_ACCESS", "true")
|
||||||
|
.withEnv("USER_NAME", "foo")
|
||||||
|
.withEnv("USER_PASSWORD", "pass")
|
||||||
|
.withEnv("SUDO_ACCESS", "true")
|
||||||
|
.withExposedPorts(2222)
|
||||||
|
|
||||||
|
protected val host by lazy {
|
||||||
|
Host(
|
||||||
|
name = sshd.containerName,
|
||||||
|
protocol = Protocol.SSH,
|
||||||
|
host = "127.0.0.1",
|
||||||
|
port = sshd.getMappedPort(2222),
|
||||||
|
username = "foo",
|
||||||
|
authentication = Authentication.No.copy(type = AuthenticationType.Password, password = "pass"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@BeforeTest
|
||||||
|
fun setup() {
|
||||||
|
sshd.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterTest
|
||||||
|
fun teardown() {
|
||||||
|
sshd.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
FROM linuxserver/openssh-server
|
FROM linuxserver/openssh-server
|
||||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||||
&& apk update && apk add wget gcc g++ git make zsh htop stress-ng inetutils-telnet xclock xorg-server xinit && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \
|
&& apk update && apk add wget gcc g++ git make zsh htop stress-ng inetutils-telnet xclock xcalc xorg-server xinit && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \
|
||||||
&& tar -xf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20 && ./configure && make && make install \
|
&& tar -xf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20 && ./configure && make && make install \
|
||||||
&& ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz
|
&& ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz
|
||||||
RUN sed -i 's/#AllowAgentForwarding yes/AllowAgentForwarding yes/g' /etc/ssh/sshd_config
|
RUN sed -i 's/#AllowAgentForwarding yes/AllowAgentForwarding yes/g' /etc/ssh/sshd_config
|
||||||
|
|||||||
Reference in New Issue
Block a user