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
|
||||
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
||||
init {
|
||||
@@ -31,6 +33,8 @@ class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
||||
terminalOption.heartbeatIntervalTextField.value = host.options.heartbeatInterval
|
||||
|
||||
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()) {
|
||||
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
|
||||
|
||||
@@ -138,6 +138,16 @@ data class Options(
|
||||
* SFTP 默认目录
|
||||
*/
|
||||
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 {
|
||||
val Default = Options()
|
||||
|
||||
@@ -103,7 +103,9 @@ open class HostOptionsPane : OptionsPane() {
|
||||
heartbeatInterval = (terminalOption.heartbeatIntervalTextField.value ?: 30) as Int,
|
||||
jumpHosts = jumpHostsOption.jumpHosts.map { it.id },
|
||||
serialComm = serialComm,
|
||||
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text
|
||||
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||
enableX11Forwarding = tunnelingOption.x11ForwardingCheckBox.isSelected,
|
||||
x11Forwarding = tunnelingOption.x11ServerTextField.text,
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -178,14 +191,18 @@ open class HostOptionsPane : OptionsPane() {
|
||||
*/
|
||||
private fun validateField(textField: JTextField): Boolean {
|
||||
if (textField.isEnabled && textField.text.isBlank()) {
|
||||
selectOptionJComponent(textField)
|
||||
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||
textField.requestFocusInWindow()
|
||||
setOutlineError(textField)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun setOutlineError(textField: JTextField) {
|
||||
selectOptionJComponent(textField)
|
||||
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||
textField.requestFocusInWindow()
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 true 表示有错误
|
||||
*/
|
||||
@@ -749,6 +766,8 @@ open class HostOptionsPane : OptionsPane() {
|
||||
|
||||
protected inner class TunnelingOption : JPanel(BorderLayout()), Option {
|
||||
val tunnelings = mutableListOf<Tunneling>()
|
||||
val x11ForwardingCheckBox = JCheckBox("X DISPLAY:")
|
||||
val x11ServerTextField = OutlineTextField(255)
|
||||
|
||||
private val model = object : DefaultTableModel() {
|
||||
override fun getRowCount(): Int {
|
||||
@@ -823,13 +842,36 @@ open class HostOptionsPane : OptionsPane() {
|
||||
box.add(Box.createHorizontalStrut(4))
|
||||
box.add(deleteBtn)
|
||||
|
||||
add(JLabel("TCP/IP Forwarding:"), BorderLayout.NORTH)
|
||||
add(scrollPane, BorderLayout.CENTER)
|
||||
add(box, BorderLayout.SOUTH)
|
||||
x11ForwardingCheckBox.isFocusable = false
|
||||
|
||||
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() {
|
||||
x11ForwardingCheckBox.addChangeListener { x11ServerTextField.isEnabled = x11ForwardingCheckBox.isSelected }
|
||||
|
||||
addBtn.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent?) {
|
||||
val dialog = PortForwardingDialog(SwingUtilities.getWindowAncestor(this@HostOptionsPane))
|
||||
|
||||
@@ -3,6 +3,8 @@ package app.termora
|
||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||
import app.termora.terminal.TerminalSize
|
||||
import app.termora.x11.ChannelX11
|
||||
import app.termora.x11.X11ChannelFactory
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import com.formdev.flatlaf.util.FontUtils
|
||||
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.SshConstants
|
||||
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.PtyChannelConfigurationHolder
|
||||
import org.apache.sshd.common.cipher.CipherNone
|
||||
import org.apache.sshd.common.config.keys.KeyRandomArt
|
||||
import org.apache.sshd.common.config.keys.KeyUtils
|
||||
import org.apache.sshd.common.future.CloseFuture
|
||||
@@ -75,6 +80,7 @@ import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import javax.swing.*
|
||||
import kotlin.math.max
|
||||
import kotlin.random.Random
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
object SshClients {
|
||||
@@ -234,6 +240,18 @@ object SshClients {
|
||||
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 {
|
||||
if (!session.auth().verify(timeout).await(timeout)) {
|
||||
throw SshException("Authentication failed")
|
||||
@@ -325,6 +343,11 @@ object SshClients {
|
||||
|
||||
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
|
||||
|
||||
// https://github.com/TermoraDev/termora/issues/180
|
||||
@@ -533,6 +556,21 @@ object SshClients {
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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) :
|
||||
IoConnector {
|
||||
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
|
||||
|
||||
import org.apache.sshd.sftp.client.impl.DefaultSftpClientFactory
|
||||
import org.testcontainers.containers.GenericContainer
|
||||
import java.nio.file.Files
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class SFTPTest {
|
||||
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)
|
||||
class SFTPTest : SSHDTest() {
|
||||
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
sftpContainer.start()
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun teardown() {
|
||||
sftpContainer.stop()
|
||||
}
|
||||
|
||||
@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 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
|
||||
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 \
|
||||
&& 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
|
||||
|
||||
Reference in New Issue
Block a user