From 732005d9bf92742f03dd26da7dbc0495f36a3515 Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Thu, 18 Jan 2018 21:17:57 -0800 Subject: [PATCH] Some cleanup in prep for possible io.ReadFull() fixed-block session-cmd header to resolve the eaten-byte issue handing Accept off to cmdRunner --- demo/client/client.go | 40 +++++++++----- demo/server/server.go | 123 +++++++++--------------------------------- 2 files changed, 53 insertions(+), 110 deletions(-) diff --git a/demo/client/client.go b/demo/client/client.go index 31db8b0..0b8cc11 100644 --- a/demo/client/client.go +++ b/demo/client/client.go @@ -10,6 +10,7 @@ import ( "sync" hkex "blitter.com/herradurakex" + isatty "github.com/mattn/go-isatty" "golang.org/x/sys/unix" ) @@ -31,6 +32,7 @@ func main() { var cAlg string var hAlg string var server string + isInteractive := false flag.StringVar(&cAlg, "c", "C_AES_256", "cipher [\"C_AES_256\" | \"C_TWOFISH_128\" | \"C_BLOWFISH_64\"]") flag.StringVar(&hAlg, "h", "H_SHA256", "hmac [\"H_SHA256\"]") @@ -46,20 +48,27 @@ func main() { } defer conn.Close() - // Set stdin in raw mode. - oldState, err := MakeRaw(int(os.Stdin.Fd())) - if err != nil { - panic(err) + // Set stdin in raw mode if it's an interactive session + if isatty.IsTerminal(os.Stdin.Fd()) { + isInteractive = true + oldState, err := MakeRaw(int(os.Stdin.Fd())) + if err != nil { + panic(err) + } + defer func() { _ = Restore(int(os.Stdin.Fd()), oldState) }() // Best effort. + } else { + fmt.Println("NOT A TTY") } - defer func() { _ = Restore(int(os.Stdin.Fd()), oldState) }() // Best effort. wg.Add(1) go func() { - // This will guarantee the side that closes first - // marks its direction's goroutine as finished. + // By deferring a call to wg.Done(), + // each goroutine guarantees that it marks + // its direction's stream as finished. + // // Whichever direction's goroutine finishes first - // will call wg.Done() once more explicitly to - // hang up on the other side so the client + // will call wg.Done() once more, explicitly, to + // hang up on the other side, so that this client // exits immediately on an EOF from either side. defer wg.Done() @@ -72,8 +81,10 @@ func main() { os.Exit(1) } } - log.Println("[Got Write EOF]") - wg.Done() // client hanging up, close server read goroutine + if isInteractive { + log.Println("[Got Write EOF]") + wg.Done() // client hanging up, close WaitGroup to exit client + } }() wg.Add(1) @@ -90,7 +101,7 @@ func main() { } } log.Println("[Got Read EOF]") - wg.Done() // server hung up, close client write goroutine + wg.Done() // server hung up, close WaitGroup to exit client }() // Wait until both stdin and stdout goroutines finish @@ -99,13 +110,16 @@ func main() { /* ------------- minimal terminal APIs brought in from ssh/terminal * (they have no real business being there as they aren't specific to - * ssh.) + * ssh, but as of v1.10, early 2018, core go stdlib hasn't yet done + * the planned terminal lib reorgs.) * ------------- */ +// From github.com/golang/crypto/blob/master/ssh/terminal/util_linux.go const ioctlReadTermios = unix.TCGETS const ioctlWriteTermios = unix.TCSETS +// From github.com/golang/crypto/blob/master/ssh/terminal/util.go // State contains the state of a terminal. type State struct { termios unix.Termios diff --git a/demo/server/server.go b/demo/server/server.go index 4b6655b..bb7c00b 100644 --- a/demo/server/server.go +++ b/demo/server/server.go @@ -16,6 +16,9 @@ import ( "github.com/kr/pty" ) +// Unused, probably obsolete. Once interactive session +// and piped I/O one-shot commands are working reconsider +// how Op might be used const ( OpR = 'r' // read(file) (binary mode) OpW = 'w' // (over)write @@ -39,78 +42,11 @@ 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 { - case OpR: - //Clean up r.cmd beforehand - r.arg = strings.TrimSpace(r.arg) - fmt.Printf("[cmd was:'%s']\n", r.arg) - runCmdAs(r.who, r.arg, nil) - fmt.Println(r.arg) - break - default: - fmt.Printf("[cmd %d ignored:%d]\n", int(r.op)) - break - } -} -*/ - // Run a command (via os.exec) as a specific user +// +// Uses ptys to support commands which expect a terminal. func runCmdAs(who string, cmd string, conn hkex.Conn) (err error) { u, _ := user.Lookup(who) var uid, gid uint32 @@ -183,6 +119,7 @@ func main() { // multiple connections may be served concurrently. go func(c hkex.Conn) (e error) { defer c.Close() + var connOp *byte = nil ch := make(chan []byte) chN := 0 eCh := make(chan error) @@ -204,8 +141,6 @@ func main() { }(ch, eCh) ticker := time.Tick(time.Second / 100) - //var r cmdRunner - var connOp *byte = nil Term: // continuously read from the connection for { @@ -216,39 +151,33 @@ func main() { fmt.Printf("Client sent %+v\n", data[0:chN]) if connOp == nil { // Initial xmit - get op byte - // (TODO: determine valid ops - // for now 'e' (echo), 'i' (interactive), 'x' (exec), ... ?) + // Have op here and first block of data[] connOp = new(byte) *connOp = data[0] + fmt.Printf("[* connOp '%c']\n", *connOp) + } + if len(data) > 1 { data = data[1:chN] chN -= 1 - // Have op here and first block of data[] - - fmt.Printf("[* connOp '%c']\n", *connOp) - // The CloseHandler typically handles the - // accumulated command data - //r = cmdRunner{op: Op(*connOp), - // who: "larissa", arg: string(data), - // authCookie: "c00ki3", - // status: 0} } - // From here, one could pass all subsequent data - // between client/server attached to an exec.Cmd, - // as data to/from a file, etc. - if *connOp == 's' { - fmt.Println("[Running shell]") - runCmdAs("larissa", "bash -l -i", conn) - // Returned hopefully via an EOF or exit/logout; - // Clear current op so user can enter next, or EOF - connOp = nil - fmt.Println("[Exiting shell]") - conn.Close() + if len(data) > 0 { + // From here, one could pass all subsequent data + // between client/server attached to an exec.Cmd, + // as data to/from a file, etc. + if connOp != nil && *connOp == 's' { + fmt.Println("[Running shell]") + runCmdAs("larissa", "bash -l -i", conn) + // Returned hopefully via an EOF or exit/logout; + // 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() + } } - if strings.Trim(string(data), "\r\n") == "exit" { - conn.Close() - } - //fmt.Printf("Client sent %s\n", string(data)) // This case means we got an error and the goroutine has finished case err := <-eCh: