From 49c589ee8dd98787c7647c739fa4ed8140bc5bb5 Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Thu, 18 Jan 2018 18:57:37 -0800 Subject: [PATCH] Added pty lib to give true terminal capability. raw mode/restore for client working --- demo/client/client.go | 70 ++++++++++++++++++++++++++++++++++++++++-- demo/server/server.go | 71 ++++++++++++++++++++++++++++++++++++++++++- hkexnet.go | 8 ++--- 3 files changed, 142 insertions(+), 7 deletions(-) diff --git a/demo/client/client.go b/demo/client/client.go index ba71e5a..31db8b0 100644 --- a/demo/client/client.go +++ b/demo/client/client.go @@ -10,6 +10,7 @@ import ( "sync" hkex "blitter.com/herradurakex" + "golang.org/x/sys/unix" ) // Demo of a simple client that dials up to a simple test server to @@ -45,6 +46,13 @@ func main() { } defer conn.Close() + // Set stdin in raw mode. + oldState, err := MakeRaw(int(os.Stdin.Fd())) + if err != nil { + panic(err) + } + defer func() { _ = Restore(int(os.Stdin.Fd()), oldState) }() // Best effort. + wg.Add(1) go func() { // This will guarantee the side that closes first @@ -64,7 +72,7 @@ func main() { os.Exit(1) } } - fmt.Println("[Got Write EOF]") + log.Println("[Got Write EOF]") wg.Done() // client hanging up, close server read goroutine }() @@ -81,10 +89,68 @@ func main() { os.Exit(2) } } - fmt.Println("[Got Read EOF]") + log.Println("[Got Read EOF]") wg.Done() // server hung up, close client write goroutine }() // Wait until both stdin and stdout goroutines finish wg.Wait() } + +/* ------------- minimal terminal APIs brought in from ssh/terminal + * (they have no real business being there as they aren't specific to + * ssh.) + * ------------- + */ + +const ioctlReadTermios = unix.TCGETS +const ioctlWriteTermios = unix.TCSETS + +// State contains the state of a terminal. +type State struct { + termios unix.Termios +} + +// MakeRaw put the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd int) (*State, error) { + termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + if err != nil { + return nil, err + } + + oldState := State{termios: *termios} + + // This attempts to replicate the behaviour documented for cfmakeraw in + // the termios(3) manpage. + termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON + termios.Oflag &^= unix.OPOST + termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN + termios.Cflag &^= unix.CSIZE | unix.PARENB + termios.Cflag |= unix.CS8 + termios.Cc[unix.VMIN] = 1 + termios.Cc[unix.VTIME] = 0 + if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, termios); err != nil { + return nil, err + } + + return &oldState, nil +} + +// GetState returns the current state of a terminal which may be useful to +// restore the terminal after a signal. +func GetState(fd int) (*State, error) { + termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + if err != nil { + return nil, err + } + + return &State{termios: *termios}, nil +} + +// Restore restores the terminal connected to the given file descriptor to a +// previous state. +func Restore(fd int, state *State) error { + return unix.IoctlSetTermios(fd, ioctlWriteTermios, &state.termios) +} diff --git a/demo/server/server.go b/demo/server/server.go index 24c34a0..4b6655b 100644 --- a/demo/server/server.go +++ b/demo/server/server.go @@ -3,6 +3,7 @@ package main import ( "flag" "fmt" + "io" "io/ioutil" "log" "os/exec" @@ -12,6 +13,7 @@ import ( "time" hkex "blitter.com/herradurakex" + "github.com/kr/pty" ) const ( @@ -37,6 +39,60 @@ type cmdRunner struct { status int } +/* ------------- minimal terminal APIs brought in from ssh/terminal + * (they have no real business being there as they aren't specific to + * ssh.) + * ------------- + */ + +/* +// MakeRaw put the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd int) (*State, error) { + termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + if err != nil { + return nil, err + } + + oldState := State{termios: *termios} + + // This attempts to replicate the behaviour documented for cfmakeraw in + // the termios(3) manpage. + termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON + termios.Oflag &^= unix.OPOST + termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN + termios.Cflag &^= unix.CSIZE | unix.PARENB + termios.Cflag |= unix.CS8 + termios.Cc[unix.VMIN] = 1 + termios.Cc[unix.VTIME] = 0 + if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, termios); err != nil { + return nil, err + } + + return &oldState, nil +} + +// GetState returns the current state of a terminal which may be useful to +// restore the terminal after a signal. +func GetState(fd int) (*State, error) { + termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + if err != nil { + return nil, err + } + + return &State{termios: *termios}, nil +} + +// Restore restores the terminal connected to the given file descriptor to a +// previous state. +func Restore(fd int, state *State) error { + return unix.IoctlSetTermios(fd, ioctlWriteTermios, &state.termios) +} +*/ + +/* -------------------------------------------------------------- */ + /* func cmd(r *cmdRunner) { switch r.op { @@ -72,7 +128,19 @@ func runCmdAs(who string, cmd string, conn hkex.Conn) (err error) { c.Stdout = conn c.Stderr = conn - err = c.Run() + // Start the command with a pty. + ptmx, err := pty.Start(c) // returns immediately with ptmx file + if err != nil { + return err + } + // Make sure to close the pty at the end. + defer func() { _ = ptmx.Close() }() // Best effort. + // Copy stdin to the pty and the pty to stdout. + go func() { _, _ = io.Copy(ptmx, conn) }() + _, _ = io.Copy(conn, ptmx) + + //err = c.Run() // returns when c finishes. + if err != nil { log.Printf("Command finished with error: %v", err) log.Printf("[%s]\n", cmd) @@ -175,6 +243,7 @@ func main() { // Clear current op so user can enter next, or EOF connOp = nil fmt.Println("[Exiting shell]") + conn.Close() } if strings.Trim(string(data), "\r\n") == "exit" { conn.Close() diff --git a/hkexnet.go b/hkexnet.go index 210e90f..bab843d 100644 --- a/hkexnet.go +++ b/hkexnet.go @@ -175,7 +175,7 @@ func Dial(protocol string, ipport string, extensions ...string) (hc *Conn, err e // Close a hkex.Conn func (c Conn) Close() (err error) { err = c.c.Close() - fmt.Println("[Conn Closing]") + log.Println("[Conn Closing]") return } @@ -241,7 +241,7 @@ func Listen(protocol string, ipport string) (hl HKExListener, e error) { if err != nil { return HKExListener{nil}, err } - fmt.Println("[Listening]") + log.Println("[Listening]") hl.l = l return } @@ -250,7 +250,7 @@ func Listen(protocol string, ipport string) (hl HKExListener, e error) { // // See go doc io.Close func (hl HKExListener) Close() error { - fmt.Println("[Listener Closed]") + log.Println("[Listener Closed]") return hl.l.Close() } @@ -263,7 +263,7 @@ func (hl HKExListener) Accept() (hc Conn, err error) { return Conn{c: nil, h: nil, cipheropts: 0, opts: 0, r: nil, w: nil}, err } - fmt.Println("[Accepted]") + log.Println("[Accepted]") hc = Conn{c: c, h: New(0, 0), cipheropts: 0, opts: 0, op: 0, r: nil, w: nil}