diff --git a/Makefile b/Makefile index 67eb22d..fc5d4cd 100644 --- a/Makefile +++ b/Makefile @@ -108,10 +108,8 @@ reinstall: uninstall install install: echo "WIN_MSYS:" $(WIN_MSYS) ifdef WIN_MSYS - cp xs/mintty_wrapper.sh $(INSTPREFIX)/bin/xs - cp xs/mintty_wrapper.sh $(INSTPREFIX)/bin/xc - cp xs/xs $(INSTPREFIX)/bin/_xs - cp xs/xs $(INSTPREFIX)/bin/_xc + cp xs/xs $(INSTPREFIX)/bin/xs + cp xs/xs $(INSTPREFIX)/bin/xc echo "Install of xsd server for Windows not yet supported" else cp xs/xs $(INSTPREFIX)/bin @@ -120,8 +118,7 @@ else endif uninstall: - rm -f $(INSTPREFIX)/bin/xs $(INSTPREFIX)/bin/xc \ - $(INSTPREFIX)/bin/_xs $(INSTPREFIX)/bin/_xc + rm -f $(INSTPREFIX)/bin/xs $(INSTPREFIX)/bin/xc ifndef $(WIN_MSYS) rm -f $(INSTPREFIX)/sbin/xsd endif diff --git a/auth.go b/auth.go old mode 100755 new mode 100644 diff --git a/termmode_linux.go b/termmode_linux.go index 466742b..fbb8728 100644 --- a/termmode_linux.go +++ b/termmode_linux.go @@ -5,6 +5,7 @@ package xs import ( "errors" "io" + "os" unix "golang.org/x/sys/unix" ) @@ -30,7 +31,8 @@ type State struct { // 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 uintptr) (*State, error) { +func MakeRaw(f *os.File) (*State, error) { + fd := f.Fd() termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios) if err != nil { return nil, err @@ -56,8 +58,8 @@ func MakeRaw(fd uintptr) (*State, error) { // GetState returns the current state of a terminal which may be useful to // restore the terminal after a signal. -func GetState(fd uintptr) (*State, error) { - termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios) +func GetState(f *os.File) (*State, error) { + termios, err := unix.IoctlGetTermios(int(f.Fd()), ioctlReadTermios) if err != nil { return nil, err } @@ -67,9 +69,9 @@ func GetState(fd uintptr) (*State, error) { // Restore restores the terminal connected to the given file descriptor to a // previous state. -func Restore(fd uintptr, state *State) error { +func Restore(f *os.File, state *State) error { if state != nil { - return unix.IoctlSetTermios(int(fd), ioctlWriteTermios, &state.termios) + return unix.IoctlSetTermios(int(f.Fd()), ioctlWriteTermios, &state.termios) } else { return errors.New("nil State") } @@ -78,7 +80,8 @@ func Restore(fd uintptr, state *State) error { // ReadPassword reads a line of input from a terminal without local echo. This // is commonly used for inputting passwords and other sensitive data. The slice // returned does not include the \n. -func ReadPassword(fd uintptr) ([]byte, error) { +func ReadPassword(f *os.File) ([]byte, error) { + fd := f.Fd() termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios) if err != nil { return nil, err diff --git a/termmode_windows.go b/termmode_windows.go index c8931c9..18c89c0 100644 --- a/termmode_windows.go +++ b/termmode_windows.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows // Note the terminal manipulation functions herein are mostly stubs. They @@ -15,10 +16,12 @@ package xs import ( - "io" + "bufio" + "fmt" + "log" + "os" "os/exec" - - "golang.org/x/sys/windows" + "os/signal" ) type State struct { @@ -27,67 +30,84 @@ type State struct { // 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 uintptr) (*State, error) { - // This doesn't really work. The exec.Command() runs a sub-shell - // so the stty mods don't affect the client process. - cmd := exec.Command("stty", "-echo raw") - cmd.Run() +func MakeRaw(f *os.File) (*State, error) { + cmd := exec.Command("stty", "-echo", "raw") + cmd.Stdin = f + err := cmd.Run() + if err != nil { + log.Fatal(err) + return &State{}, err + } + + // MSYS2/CYGWIN: wintty needs CTRL-C caught + // ---------------------------------------- + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, os.Kill) + go func() { + for sig := range c { + _ = sig + //fmt.Println(sig) + } + }() + // ---------------------------------------- + return &State{}, nil } // GetState returns the current state of a terminal which may be useful to // restore the terminal after a signal. -func GetState(fd uintptr) (*State, error) { +func GetState(f *os.File) (*State, error) { return &State{}, nil } // Restore restores the terminal connected to the given file descriptor to a // previous state. -func Restore(fd uintptr, state *State) error { - cmd := exec.Command("stty", "echo cooked") - cmd.Run() +func Restore(f *os.File, state *State) error { + cmd := exec.Command("stty", "sane") + cmd.Stdin = f + err := cmd.Run() + if err != nil { + log.Fatal(err) + return nil + } return nil } // ReadPassword reads a line of input from a terminal without local echo. This // is commonly used for inputting passwords and other sensitive data. The slice // returned does not include the \n. -func ReadPassword(fd uintptr) ([]byte, error) { - return readPasswordLine(passwordReader(fd)) -} - -// passwordReader is an io.Reader that reads from a specific file descriptor. -type passwordReader windows.Handle - -func (r passwordReader) Read(buf []byte) (int, error) { - return windows.Read(windows.Handle(r), buf) -} - -// readPasswordLine reads from reader until it finds \n or io.EOF. -// The slice returned does not include the \n. -// readPasswordLine also ignores any \r it finds. -func readPasswordLine(reader io.Reader) ([]byte, error) { - var buf [1]byte - var ret []byte - - for { - n, err := reader.Read(buf[:]) - if n > 0 { - switch buf[0] { - case '\n': - return ret, nil - case '\r': - // remove \r from passwords on Windows - default: - ret = append(ret, buf[0]) - } - continue - } +func ReadPassword(f *os.File) (pw []byte, err error) { + sttycmd, err := exec.LookPath("stty") + if err != nil { + return nil, err + } else { + //fmt.Printf("stty found at: %v\n", sttycmd) + cmdOff := exec.Command(sttycmd, "-echo") + cmdOff.Stdin = f //os.Stdin + cmdOff.Stdout = nil //os.Stdout + cmdOff.Stderr = nil //os.Stderr + err = cmdOff.Run() if err != nil { - if err == io.EOF && len(ret) > 0 { - return ret, nil - } - return ret, err + return nil, err + } + + //fmt.Printf("Enter password:") + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + err = scanner.Err() + if err != nil { + return nil, err + } + pw = scanner.Bytes() + fmt.Println() + cmdOn := exec.Command(sttycmd, "echo") + cmdOn.Stdin = f //os.Stdin + cmdOn.Stdout = nil //os.Stdout + cmdOn.Stderr = nil //os.Stderr + err = cmdOn.Run() + if err != nil { + return nil, err } } + return } diff --git a/xs/mintty_wrapper.sh b/xs/mintty_wrapper.sh deleted file mode 100755 index dbc678a..0000000 --- a/xs/mintty_wrapper.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -# -## This wrapper may be used within the MSYS/mintty Windows -## shell environment to have a functioning xs client with -## working 'raw' mode and hidden password entry. -## -## mintty uses named pipes and ptys to get a more POSIX-like -## terminal (incl. VT/ANSI codes) rather than the dumb Windows -## console interface; however Go on Windows does not have functioning -## MSYS/mintty code to set raw, echo etc. modes. -## -## Someday it would be preferable to put native Windows term mode -## code into the client build, but this is 'good enough' for now -## (with the exception of tty rows/cols not being set based on -## info from the server). -## -## INSTALLATION -## -- -## Build the client, put it somewhere in your $PATH with this -## wrapper and edit the name of the client binary -## eg., -## $ cp hkexsh.exe /usr/bin/.hkexsh.exe -## $ cp mintty_wrapper.sh /usr/bin/hkexsh -#### -trap cleanup EXIT ERR - -cleanup() { - stty sane -} - -me="$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")" - -if [ ${1}x == "-hx" ]; then - _${me} -h -else - stty -echo raw icrnl - _${me} $@ -fi - diff --git a/xs/xs.go b/xs/xs.go index 018c542..e47c049 100755 --- a/xs/xs.go +++ b/xs/xs.go @@ -35,7 +35,7 @@ import ( "blitter.com/go/xs/logger" "blitter.com/go/xs/spinsult" "blitter.com/go/xs/xsnet" - isatty "github.com/mattn/go-isatty" + //isatty "github.com/mattn/go-isatty" ) var ( @@ -990,29 +990,13 @@ func main() { //nolint: funlen, gocyclo // === Shell terminal mode (Shell vs. Copy) setup - // Set stdin in raw mode if it's an interactive session - // TODO: send flag to server side indicating this - // affects shell command used - var oldState *xs.State defer conn.Close() // === From this point on, conn is a secure encrypted channel - if shellMode { - if isatty.IsTerminal(os.Stdin.Fd()) { - oldState, err = xs.MakeRaw(os.Stdin.Fd()) - if err != nil { - panic(err) - } - // #gv:s/label=\"main\$1\"/label=\"deferRestore\"/ - // TODO:.gv:main:1:deferRestore - defer restoreTermState(oldState) - } else { - log.Println("NOT A TTY") - } - } + // === BEGIN Login phase - // === Login phase + var oldState *xs.State // Start login timeout here and disconnect if user/pass phase stalls // iloginImpatience := time.AfterFunc(20*time.Second, func() { @@ -1029,7 +1013,7 @@ func main() { //nolint: funlen, gocyclo // No auth token, prompt for password fmt.Printf("Gimme cookie:") } - ab, e := xs.ReadPassword(os.Stdin.Fd()) + ab, e := xs.ReadPassword(os.Stdin) if !gopt { fmt.Printf("\r\n") } @@ -1044,6 +1028,25 @@ func main() { //nolint: funlen, gocyclo // Security scrub runtime.GC() + // === END Login phase + + + // === Terminal mode adjustment for session + + if shellMode { + if true { //isatty.IsTerminal(os.Stdin.Fd()) { + oldState, err = xs.MakeRaw(os.Stdin) + if err != nil { + panic(err) + } + // #gv:s/label=\"main\$1\"/label=\"deferRestore\"/ + // TODO:.gv:main:1:deferRestore + defer restoreTermState(oldState) + } else { + log.Println("NOT A TTY") + } + } + // === Session param and TERM setup // Set up session params and send over to server @@ -1147,7 +1150,7 @@ func localUserName(u *user.User) string { } func restoreTermState(oldState *xs.State) { - _ = xs.Restore(os.Stdin.Fd(), oldState) + _ = xs.Restore(os.Stdin, oldState) } // exitWithStatus wraps os.Exit() plus does any required pprof housekeeping diff --git a/xsnet/chan.go b/xsnet/chan.go old mode 100644 new mode 100755 diff --git a/xsnet/consts.go b/xsnet/consts.go old mode 100644 new mode 100755 diff --git a/xsnet/kcp.go b/xsnet/kcp.go old mode 100644 new mode 100755 diff --git a/xsnet/net.go b/xsnet/net.go old mode 100644 new mode 100755 index 8727437..4bfcff5 --- a/xsnet/net.go +++ b/xsnet/net.go @@ -39,7 +39,6 @@ import ( "net" "strings" "sync" - "syscall" "time" hkex "blitter.com/go/herradurakex" @@ -1762,7 +1761,9 @@ func (hc *Conn) keepaliveHelper() { hc.ShutdownKeepAlive() if hc.Pproc != 0 { //fmt.Printf("[pid %d needs to be killed]\n", hc.Pproc) - syscall.Kill(hc.Pproc, syscall.SIGABRT) //nolint:errcheck + //syscall.Kill(hc.Pproc, syscall.SIGABRT) //nolint:errcheck + //exec.Command("taskkill", "/f", "/pid", strconv.Itoa(hc.Pproc)).Run() + hc.kill() } break } diff --git a/xsnet/net_linux.go b/xsnet/net_linux.go new file mode 100755 index 0000000..bcd7501 --- /dev/null +++ b/xsnet/net_linux.go @@ -0,0 +1,13 @@ +//go:build linux +// +build linux + +package xsnet + +import ( + "syscall" + ) + +func (hc *Conn) kill() { + syscall.Kill(hc.Pproc, syscall.SIGABRT) //nolint:errcheck +} + diff --git a/xsnet/net_windows.go b/xsnet/net_windows.go new file mode 100755 index 0000000..a47836a --- /dev/null +++ b/xsnet/net_windows.go @@ -0,0 +1,13 @@ +//go:build windows +// +build windows + +package xsnet + +import ( + "os/exec" + "strconv" + ) + +func (hc *Conn) kill() { + exec.Command("taskkill", "/f", "/pid", strconv.Itoa(hc.Pproc)).Run() +} diff --git a/xsnet/tun.go b/xsnet/tun.go old mode 100644 new mode 100755