fix version
Some checks failed
Build, Test and Push Docker Image / e2e-test (push) Failing after 5s
Build, Test and Push Docker Image / build-and-push (push) Has been skipped

This commit is contained in:
2025-11-06 22:23:59 +01:00
parent 28ed749316
commit d37921cdd7
2 changed files with 82 additions and 32 deletions

2
go.mod
View File

@@ -5,7 +5,7 @@ go 1.24.0
require ( require (
github.com/creack/pty/v2 v2.0.1 github.com/creack/pty/v2 v2.0.1
golang.org/x/crypto v0.43.0 golang.org/x/crypto v0.43.0
golang.org/x/term v0.37.0 golang.org/x/term v0.36.0
) )
require golang.org/x/sys v0.37.0 // indirect require golang.org/x/sys v0.37.0 // indirect

112
main.go
View File

@@ -97,15 +97,32 @@ func handleChannel(channel ssh.Channel, requests <-chan *ssh.Request) {
defer channel.Close() defer channel.Close()
var ptmx *os.File var ptmx *os.File
var termWidth, termHeight uint32 = 80, 24 var termWidth, termHeight uint32 = 80, 24
var term string var clientTerm string
var ptyAllocated bool // Track if PTY is allocated
for req := range requests { for req := range requests {
switch req.Type { switch req.Type {
case "env":
// Handle environment requests (e.g., client TERM propagation)
if len(req.Payload) >= 7 && string(req.Payload[:7]) == "TERM=\x00" {
termLen := int(req.Payload[7])
if len(req.Payload) >= 8+termLen {
clientTerm = string(req.Payload[8 : 8+termLen])
log.Println("Client TERM from env:", clientTerm)
}
}
req.Reply(true, nil)
case "pty-req": case "pty-req":
// Allocate PTY early on pty-req, before shell/exec
if ptyAllocated {
req.Reply(false, nil)
continue
}
if len(req.Payload) >= 4 { if len(req.Payload) >= 4 {
termLen := binary.BigEndian.Uint32(req.Payload[0:4]) termLen := binary.BigEndian.Uint32(req.Payload[0:4])
if len(req.Payload) >= int(4+termLen+16) { if len(req.Payload) >= int(4+termLen+16) {
term = string(req.Payload[4 : 4+termLen]) clientTerm = string(req.Payload[4 : 4+termLen])
log.Println("Client TERM:", term) log.Println("Client TERM from pty-req:", clientTerm)
cols := binary.BigEndian.Uint32(req.Payload[4+termLen : 4+termLen+4]) cols := binary.BigEndian.Uint32(req.Payload[4+termLen : 4+termLen+4])
rows := binary.BigEndian.Uint32(req.Payload[4+termLen+4 : 4+termLen+8]) rows := binary.BigEndian.Uint32(req.Payload[4+termLen+4 : 4+termLen+8])
if cols > 0 { if cols > 0 {
@@ -116,50 +133,81 @@ func handleChannel(channel ssh.Channel, requests <-chan *ssh.Request) {
} }
} }
} }
req.Reply(true, nil) // Set default TERM if not provided (TUI-compatible)
case "window-change": if clientTerm == "" {
width := binary.BigEndian.Uint32(req.Payload) clientTerm = "xterm-256color"
height := binary.BigEndian.Uint32(req.Payload[4:])
if width > 0 {
termWidth = width
} }
if height > 0 {
termHeight = height
}
if ptmx != nil {
pty.Setsize(ptmx, &pty.Winsize{Cols: uint16(termWidth), Rows: uint16(termHeight)})
}
req.Reply(true, nil)
case "shell":
req.Reply(true, nil)
command := os.Getenv("COMMAND") command := os.Getenv("COMMAND")
if command == "" { if command == "" {
command = "/app/tui" command = "/app/tui"
} }
cmd := exec.Command(command) cmd := exec.Command(command)
envTerm := "TERM=xterm-256color" envTerm := "TERM=" + clientTerm
if term != "" {
envTerm = "TERM=" + term
}
cmd.Env = []string{"PATH=/bin", envTerm} cmd.Env = []string{"PATH=/bin", envTerm}
cmd.Dir = "/" cmd.Dir = "/"
var err error var err error
ptmx, err = pty.StartWithSize(cmd, &pty.Winsize{Cols: uint16(termWidth), Rows: uint16(termHeight)}) ptmx, err = pty.StartWithSize(cmd, &pty.Winsize{Cols: uint16(termWidth), Rows: uint16(termHeight)})
if err != nil { if err != nil {
log.Println("PTY start error:", err) log.Println("PTY start error:", err)
req.Reply(false, nil)
return return
} }
if err := term.MakeRaw(int(ptmx.Fd())); err != nil { // Make raw mode on master (server side)
if _, err := term.MakeRaw(int(ptmx.Fd())); err != nil {
log.Println("MakeRaw master error:", err) log.Println("MakeRaw master error:", err)
} }
// Note: Slave (cmd side) is already raw via pty.Start, but ensure via setsid if needed
ptyAllocated = true
// Start I/O bridging immediately
go func() { go func() {
defer ptmx.Close() defer func() {
go io.Copy(channel, ptmx) ptmx.Close()
go io.Copy(ptmx, channel) channel.Close()
cmd.Wait() }()
channel.Close() // Bidirectional copy with error handling
done := make(chan error, 2)
go func() { done <- io.Copy(channel, ptmx) }()
go func() { done <- io.Copy(ptmx, channel) }()
<-done // Wait for one to finish
cmd.Process.Signal(os.Interrupt) // Graceful shutdown
<-done
}() }()
req.Reply(true, nil)
// Wait for cmd to finish after PTY setup
go func() {
cmd.Wait()
channel.SendRequest("exit-status", false, []byte{0}) // Send exit status
}()
return // PTY session started, no more requests
case "window-change":
// Handle resizes post-PTY allocation
if ptyAllocated && ptmx != nil {
width := binary.BigEndian.Uint32(req.Payload)
height := binary.BigEndian.Uint32(req.Payload[4:])
if width > 0 {
termWidth = width
}
if height > 0 {
termHeight = height
}
pty.Setsize(ptmx, &pty.Winsize{Cols: uint16(termWidth), Rows: uint16(termHeight)})
}
req.Reply(true, nil)
case "shell":
// Only handle if no PTY (fallback, but PTY should be allocated first)
if !ptyAllocated {
req.Reply(true, nil)
// Fallback to original shell logic if needed
log.Println("Shell requested without PTY")
} else {
req.Reply(false, nil)
}
case "exec": case "exec":
// Handle exec separately if no PTY
if ptyAllocated {
req.Reply(false, nil)
continue
}
req.Reply(true, nil) req.Reply(true, nil)
command := string(req.Payload[4:]) command := string(req.Payload[4:])
runCommand(channel, command) runCommand(channel, command)
@@ -171,6 +219,7 @@ func handleChannel(channel ssh.Channel, requests <-chan *ssh.Request) {
} }
func runCommand(channel ssh.Channel, command string) { func runCommand(channel ssh.Channel, command string) {
defer channel.Close()
cmd := exec.Command("/bin/sh", "-c", command) cmd := exec.Command("/bin/sh", "-c", command)
cmd.Env = []string{"PATH=/bin"} cmd.Env = []string{"PATH=/bin"}
cmd.Dir = "/" cmd.Dir = "/"
@@ -193,8 +242,9 @@ func runCommand(channel ssh.Channel, command string) {
log.Println("Start error:", err) log.Println("Start error:", err)
return return
} }
go io.Copy(stdin, channel) go func() { io.Copy(stdin, channel); stdin.Close() }()
go io.Copy(channel, stdout) go func() { io.Copy(channel, stdout); stdout.Close() }()
go io.Copy(channel, stderr) go func() { io.Copy(channel, stderr); stderr.Close() }()
cmd.Wait() cmd.Wait()
} }