diff --git a/cp.cmd b/cp.cmd new file mode 100644 index 0000000..23da425 --- /dev/null +++ b/cp.cmd @@ -0,0 +1,12 @@ +## Template for copying files from local to remote site, destdir DEST: +tar -cz -f - testdir/sub1/bar.txt | \ + tar -xzv -C DEST --xform="s#.*/\(.*\)#\1#" + +# Note the --xform= option will strip leading path components from the file +# on extraction (ie., throw away dirtree info when copying into remote DEST) +# +# Probably need to have a '-r' option ala 'scp -r' to control --xform= +# (in the absence of --xform=.. above, files and dirs will all be extracted +# to remote DEST preserving tree structure.) + +tar cf /dev/stdout ../*.txt | tar xf - diff --git a/hkexnet/hkexnet.go b/hkexnet/hkexnet.go index 85f92d5..302835c 100644 --- a/hkexnet/hkexnet.go +++ b/hkexnet/hkexnet.go @@ -202,7 +202,6 @@ func Dial(protocol string, ipport string, extensions ...string) (hc *Conn, err e hc.r, hc.rm, err = hc.getStream(hc.h.FA()) hc.w, hc.wm, err = hc.getStream(hc.h.FA()) - *hc.closeStat = 99 // open or prematurely-closed status return } @@ -439,7 +438,12 @@ func (hc Conn) Read(b []byte) (n int, err error) { log.Printf("[TermSize pkt: rows %v cols %v]\n", hc.Rows, hc.Cols) hc.WinCh <- WinSize{hc.Rows, hc.Cols} } else if ctrlStatOp == CSOExitStatus { - *hc.closeStat = uint8(payloadBytes[0]) + if len(payloadBytes) > 0 { + *hc.closeStat = uint8(payloadBytes[0]) + } else { + log.Println("[truncated payload, cannot determine CSOExitStatus]") + *hc.closeStat = 98 + } } else { hc.dBuf.Write(payloadBytes) //log.Printf("hc.dBuf: %s\n", hex.Dump(hc.dBuf.Bytes())) @@ -450,10 +454,14 @@ func (hc Conn) Read(b []byte) (n int, err error) { hTmp := hc.rm.Sum(nil)[0:4] log.Printf("<%04x) HMAC:(i)%s (c)%02x\r\n", decryptN, hex.EncodeToString([]byte(hmacIn[0:])), hTmp) - // Log alert if hmac didn't match, corrupted channel - if !bytes.Equal(hTmp, []byte(hmacIn[0:])) /*|| hmacIn[0] > 0xf8*/ { - fmt.Println("** ALERT - detected HMAC mismatch, possible channel tampering **") - _, _ = hc.c.Write([]byte{CSOHmacInvalid}) + if *hc.closeStat > 90 { + log.Println("[cannot verify HMAC]") + } else { + // Log alert if hmac didn't match, corrupted channel + if !bytes.Equal(hTmp, []byte(hmacIn[0:])) /*|| hmacIn[0] > 0xf8*/ { + fmt.Println("** ALERT - detected HMAC mismatch, possible channel tampering **") + _, _ = hc.c.Write([]byte{CSOHmacInvalid}) + } } } diff --git a/hkexsh/hkexcp b/hkexsh/hkexcp new file mode 120000 index 0000000..cf0eb1c --- /dev/null +++ b/hkexsh/hkexcp @@ -0,0 +1 @@ +hkexsh \ No newline at end of file diff --git a/hkexsh/hkexsh.go b/hkexsh/hkexsh.go old mode 100644 new mode 100755 index daca62b..0e37a1e --- a/hkexsh/hkexsh.go +++ b/hkexsh/hkexsh.go @@ -16,9 +16,11 @@ import ( "os" "os/exec" "os/user" + "path" "runtime" "strings" "sync" + "syscall" hkexsh "blitter.com/go/hkexsh" "blitter.com/go/hkexsh/hkexnet" @@ -30,7 +32,7 @@ type cmdSpec struct { who []byte cmd []byte authCookie []byte - status int // though UNIX shell exit status is uint8, os.Exit() wants int + status int // UNIX exit status is uint8, but os.Exit() wants int } var ( @@ -52,8 +54,237 @@ func GetSize() (cols, rows int, err error) { return } -// Demo of a simple client that dials up to a simple test server to -// send data. +func parseNonSwitchArgs(a []string) (user, host, path string, isDest bool, otherArgs []string) { + // Whether fancyArg is src or dst file depends on flag.Args() index; + // fancyArg as last flag.Args() element denotes dstFile + // fancyArg as not-last flag.Args() element denotes srcFile + var fancyUser, fancyHost, fancyPath string + for i, arg := range a { + if strings.Contains(arg, ":") || strings.Contains(arg, "@") { + fancyArg := strings.Split(flag.Arg(i), "@") + var fancyHostPath []string + if len(fancyArg) < 2 { + //TODO: no user specified, use current + fancyUser = "[default:getUser]" + fancyHostPath = strings.Split(fancyArg[0], ":") + } else { + // user@.... + fancyUser = fancyArg[0] + fancyHostPath = strings.Split(fancyArg[1], ":") + } + + // [...@]host[:path] + if len(fancyHostPath) > 1 { + fancyPath = fancyHostPath[1] + } + fancyHost = fancyHostPath[0] + + //if fancyPath == "" { + // fancyPath = "." + //} + + if i == len(a)-1 { + isDest = true + fmt.Println("remote path isDest") + } + fmt.Println("fancyArgs: user:", fancyUser, "host:", fancyHost, "path:", fancyPath) + } else { + otherArgs = append(otherArgs, a[i]) + } + } + return fancyUser, fancyHost, fancyPath, isDest, otherArgs +} + +// doCopyMode begins a secure hkexsh local<->remote file copy operation. +func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, rec *cmdSpec) (err error, exitStatus int) { + if remoteDest { + fmt.Println("local files:", files, "remote filepath:", string(rec.cmd)) + + var c *exec.Cmd + + //os.Clearenv() + //os.Setenv("HOME", u.HomeDir) + //os.Setenv("TERM", "vt102") // TODO: server or client option? + + cmdName := "/bin/tar" + cmdArgs := []string{"-cz", "-f", "/dev/stdout"} + files = strings.TrimSpace(files) + // Awesome fact: tar actually can take multiple -C args, and + // changes to the dest dir *as it sees each one*. This enables + // its use below, where clients can send scattered sets of source + // files and dirs to be extraced to a single dest dir server-side, + // whilst preserving the subtrees of dirs on the other side. :) + // Eg., tar -c -f /dev/stdout -C /dirA fileInA -C /some/where/dirB fileInB /foo/dirC + // packages fileInA, fileInB, and dirC at a single toplevel in the tar. + // The tar authors are/were real smarties :) + for _, v := range strings.Split(files, " ") { + dirTmp, fileTmp := path.Split(v) + cmdArgs = append(cmdArgs, "-C", dirTmp, fileTmp) + //cmdArgs = append(cmdArgs, v) + } + + fmt.Printf("[%v %v]\n", cmdName, cmdArgs) + // NOTE the lack of quotes around --xform option's sed expression. + // When args are passed in exec() format, no quoting is required + // (as this isn't input from a shell) (right? -rlm 20180823) + //cmdArgs := []string{"-xvz", "-C", files, `--xform=s#.*/\(.*\)#\1#`} + c = exec.Command(cmdName, cmdArgs...) + c.Dir, _ = os.Getwd() + fmt.Println("[wd:", c.Dir, "]") + c.Stdout = conn + // Stderr sinkholing is important. Any extraneous output to tarpipe + // messes up remote side as it's expecting pure tar data. + // (For example, if user specifies abs paths, tar outputs + // "Removing leading '/' from path names") + c.Stderr = nil + + // Start the command (no pty) + err = c.Start() // returns immediately + if err != nil { + fmt.Println(err) + //log.Fatal(err) + } else { + if err = c.Wait(); err != nil { + if exiterr, ok := err.(*exec.ExitError); ok { + // The program has exited with an exit code != 0 + + // This works on both Unix and Windows. Although package + // syscall is generally platform dependent, WaitStatus is + // defined for both Unix and Windows and in both cases has + // an ExitStatus() method with the same signature. + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { + exitStatus = status.ExitStatus() + log.Printf("Exit Status: %d", exitStatus) + } + } + } + fmt.Println("*** client->server cp finished ***") + } + } else { + fmt.Println("remote filepath:", string(rec.cmd), "local files:", files) + var c *exec.Cmd + + //os.Clearenv() + //os.Setenv("HOME", u.HomeDir) + //os.Setenv("TERM", "vt102") // TODO: server or client option? + + cmdName := "/bin/tar" + destPath := files + + cmdArgs := []string{"-xz", "-C", destPath} + fmt.Printf("[%v %v]\n", cmdName, cmdArgs) + // NOTE the lack of quotes around --xform option's sed expression. + // When args are passed in exec() format, no quoting is required + // (as this isn't input from a shell) (right? -rlm 20180823) + //cmdArgs := []string{"-xvz", "-C", destPath, `--xform=s#.*/\(.*\)#\1#`} + c = exec.Command(cmdName, cmdArgs...) + c.Stdin = conn + c.Stdout = os.Stdout + c.Stderr = os.Stderr + + // Start the command (no pty) + err = c.Start() // returns immediately + if err != nil { + fmt.Println(err) + //log.Fatal(err) + } else { + if err = c.Wait(); err != nil { + if exiterr, ok := err.(*exec.ExitError); ok { + // The program has exited with an exit code != 0 + + // This works on both Unix and Windows. Although package + // syscall is generally platform dependent, WaitStatus is + // defined for both Unix and Windows and in both cases has + // an ExitStatus() method with the same signature. + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { + exitStatus = status.ExitStatus() + log.Printf("Exit Status: %d", exitStatus) + } + } + } + fmt.Println("*** server->client cp finished ***") + } + } + return +} + +// doShellMode begins an hkexsh shell session (one-shot command or interactive). +func doShellMode(isInteractive bool, conn *hkexnet.Conn, oldState *hkexsh.State, rec *cmdSpec) { + //client reader (from server) goroutine + //Read remote end's stdout + wg.Add(1) + go func() { + defer wg.Done() + // By deferring a call to wg.Done(), + // each goroutine guarantees that it marks + // its direction's stream as finished. + + // io.Copy() expects EOF so normally this will + // exit with inerr == nil + _, inerr := io.Copy(os.Stdout, conn) + if inerr != nil { + fmt.Println(inerr) + _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. + os.Exit(1) + } + + rec.status = int(conn.GetStatus()) + log.Println("rec.status:", rec.status) + + if isInteractive { + log.Println("[* Got EOF *]") + _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. + } + }() + + // Only look for data from stdin to send to remote end + // for interactive sessions. + if isInteractive { + handleTermResizes(conn) + + // client writer (to server) goroutine + // Write local stdin to remote end + wg.Add(1) + go func() { + defer wg.Done() + //!defer wg.Done() + // Copy() expects EOF so this will + // exit with outerr == nil + //!_, outerr := io.Copy(conn, os.Stdin) + _, outerr := func(conn *hkexnet.Conn, r io.Reader) (w int64, e error) { + w, e = io.Copy(conn, r) + return w, e + }(conn, os.Stdin) + + if outerr != nil { + log.Println(outerr) + fmt.Println(outerr) + _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. + os.Exit(255) + } + log.Println("[Sent EOF]") + }() + } + + // Wait until both stdin and stdout goroutines finish before returning + // (ensure client gets all data from server before closing) + wg.Wait() +} + +func UsageShell() { + fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "%s [opts] [user]@server\n", os.Args[0]) + flag.PrintDefaults() +} + +func UsageCp() { + fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "%s [opts] srcFileOrDir [...] [user]@server[:dstpath]\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "%s [opts] [user]@server[:srcFileOrDir] dstPath\n", os.Args[0]) + flag.PrintDefaults() +} + +// hkexsh - a client for secure shell and file copy operations. // // While conforming to the basic net.Conn interface HKex.Conn has extra // capabilities designed to allow apps to define connection options, @@ -68,44 +299,163 @@ func main() { version := "0.1pre (NO WARRANTY)" var vopt bool var dbg bool + var shellMode bool // if true act as shell, else file copier var cAlg string var hAlg string var server string + var port uint var cmdStr string - var altUser string + + var copySrc []byte + var copyDst string + var authCookie string var chaffEnabled bool var chaffFreqMin uint var chaffFreqMax uint var chaffBytesMax uint + var op []byte isInteractive := false flag.BoolVar(&vopt, "v", false, "show version") - 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\"]") - flag.StringVar(&server, "s", "localhost:2000", "server hostname/address[:port]") - flag.StringVar(&cmdStr, "x", "", "command to run (default empty - interactive shell)") - flag.StringVar(&altUser, "u", "", "specify alternate user") - flag.StringVar(&authCookie, "a", "", "auth cookie") - flag.BoolVar(&chaffEnabled, "cE", true, "enabled chaff pkts (default true)") - flag.UintVar(&chaffFreqMin, "cfm", 100, "chaff pkt freq min (msecs)") - flag.UintVar(&chaffFreqMax, "cfM", 5000, "chaff pkt freq max (msecs)") - flag.UintVar(&chaffBytesMax, "cbM", 64, "chaff pkt size max (bytes)") flag.BoolVar(&dbg, "d", false, "debug logging") + flag.StringVar(&cAlg, "c", "C_AES_256", "`cipher` [\"C_AES_256\" | \"C_TWOFISH_128\" | \"C_BLOWFISH_64\"]") + flag.StringVar(&hAlg, "m", "H_SHA256", "`hmac` [\"H_SHA256\"]") + flag.UintVar(&port, "p", 2000, "`port`") + flag.StringVar(&authCookie, "a", "", "auth cookie") + flag.BoolVar(&chaffEnabled, "e", true, "enabled chaff pkts (default true)") + flag.UintVar(&chaffFreqMin, "f", 100, "chaff pkt `freq` min (msecs)") + flag.UintVar(&chaffFreqMax, "F", 5000, "chaff pkt `freq` max (msecs)") + flag.UintVar(&chaffBytesMax, "B", 64, "chaff pkt `size` max (bytes)") + + // Find out what program we are (shell or copier) + myPath := strings.Split(os.Args[0], string(os.PathSeparator)) + if myPath[len(myPath)-1] != "hkexcp" && myPath[len(myPath)-1] != "hkexcp.exe" { + // hkexsh accepts a command (-x) but not + // a srcpath (-r) or dstpath (-t) + flag.StringVar(&cmdStr, "x", "", "`command` to run (if not specified run interactive shell)") + shellMode = true + flag.Usage = UsageShell + } else { + flag.Usage = UsageCp + } flag.Parse() + remoteUser, tmpHost, tmpPath, pathIsDest, otherArgs := + parseNonSwitchArgs(flag.Args()) + fmt.Println("otherArgs:", otherArgs) + + // Set defaults if user doesn't specify user, path or port + var uname string + if remoteUser == "" { + u, _ := user.Current() + uname = u.Username + } else { + uname = remoteUser + } + + if tmpHost != "" { + server = tmpHost + ":" + fmt.Sprintf("%d", port) + } + if tmpPath == "" { + tmpPath = "." + } + + var fileArgs string + if !shellMode /*&& tmpPath != ""*/ { + // -if pathIsSrc && len(otherArgs) > 1 ERROR + // -else flatten otherArgs into space-delim list => copySrc + if pathIsDest { + if len(otherArgs) == 0 { + log.Fatal("ERROR: Must specify at least one dest path for copy") + } else { + for _, v := range otherArgs { + copySrc = append(copySrc, ' ') + copySrc = append(copySrc, v...) + } + copyDst = tmpPath + fileArgs = string(copySrc) + } + } else { + if len(otherArgs) == 0 { + log.Fatal("ERROR: Must specify src path for copy") + } else if len(otherArgs) == 1 { + copyDst = otherArgs[0] + if strings.Contains(copyDst, "*") || strings.Contains(copyDst, "?") { + log.Fatal("ERROR: wildcards not allowed in dest path for copy") + } + } else { + log.Fatal("ERROR: cannot specify more than one dest path for copy") + } + copySrc = []byte(tmpPath) + fileArgs = copyDst + } + } + + // Do some more option consistency checks + + //fmt.Println("server finally is:", server) + if flag.NFlag() == 0 && server == "" { + flag.Usage() + os.Exit(0) + } + if vopt { fmt.Printf("version v%s\n", version) os.Exit(0) } + if len(cmdStr) != 0 && (len(copySrc) != 0 || len(copyDst) != 0) { + log.Fatal("incompatible options -- either cmd (-x) or copy ops but not both") + } + + //------------------------------------------------------------------- + // Here we have parsed all options and can now carry out + // either the shell session or copy operation. + _ = shellMode + if dbg { log.SetOutput(os.Stdout) } else { log.SetOutput(ioutil.Discard) } + if shellMode { + // We must make the decision about interactivity before Dial() + // as it affects chaffing behaviour. 20180805 + if len(cmdStr) == 0 { + op = []byte{'s'} + isInteractive = true + } else { + op = []byte{'c'} + // non-interactive cmds may complete quickly, so chaff earlier/faster + // to help ensure there's some cover to the brief traffic. + // (ignoring cmdline values) + chaffFreqMin = 2 + chaffFreqMax = 10 + } + } else { + // as copy mode is also non-interactive, set up chaffing + // just like the 'c' mode above + chaffFreqMin = 2 + chaffFreqMax = 10 + + if pathIsDest { + // client->server file copy + // src file list is in copySrc + op = []byte{'D'} + fmt.Println("client->server copy:", string(copySrc), "->", copyDst) + cmdStr = copyDst + } else { + // server->client file copy + // remote src file(s) in copyDsr + op = []byte{'S'} + fmt.Println("server->client copy:", string(copySrc), "->", copyDst) + cmdStr = string(copySrc) + } + } + conn, err := hkexnet.Dial("tcp", server, cAlg, hAlg) if err != nil { fmt.Println("Err!") @@ -118,42 +468,16 @@ func main() { // TODO: send flag to server side indicating this // affects shell command used var oldState *hkexsh.State - if isatty.IsTerminal(os.Stdin.Fd()) { - oldState, err = hkexsh.MakeRaw(int(os.Stdin.Fd())) - if err != nil { - panic(err) + if shellMode { + if isatty.IsTerminal(os.Stdin.Fd()) { + oldState, err = hkexsh.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + panic(err) + } + defer func() { _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort. + } else { + log.Println("NOT A TTY") } - defer func() { _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort. - } else { - log.Println("NOT A TTY") - } - - var uname string - if len(altUser) == 0 { - u, _ := user.Current() - uname = u.Username - } else { - uname = altUser - } - - var op []byte - if len(cmdStr) == 0 { - op = []byte{'s'} - isInteractive = true - } else if cmdStr == "-" { - op = []byte{'c'} - cmdStdin, err := ioutil.ReadAll(os.Stdin) - if err != nil { - panic(err) - } - cmdStr = strings.Trim(string(cmdStdin), "\r\n") - } else { - op = []byte{'c'} - // non-interactive cmds may complete quickly, so chaff earlier/faster - // to help ensure there's some cover to the brief traffic. - // (ignoring cmdline values) - chaffFreqMin = 2 - chaffFreqMax = 10 } if len(authCookie) == 0 { @@ -188,76 +512,19 @@ func main() { conn.SetupChaff(chaffFreqMin, chaffFreqMax, chaffBytesMax) // enable client->server chaffing if chaffEnabled { conn.EnableChaff() - } - defer conn.DisableChaff() - defer conn.ShutdownChaff() - - //client reader (from server) goroutine - wg.Add(1) - go func() { - // 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 that this client - // exits immediately on an EOF from either side. - defer wg.Done() - - // io.Copy() expects EOF so this will - // exit with inerr == nil - _, inerr := io.Copy(os.Stdout, conn) - if inerr != nil { - if inerr.Error() != "EOF" { - fmt.Println(inerr) - _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. - os.Exit(1) - } - } - - rec.status = int(conn.GetStatus()) - log.Println("rec.status:", rec.status) - - if isInteractive { - log.Println("[* Got EOF *]") - _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. - wg.Done() - //os.Exit(rec.status) - } - }() - - if isInteractive { - handleTermResizes(conn) - - // client writer (to server) goroutine - wg.Add(1) - go func() { - defer wg.Done() - - // Copy() expects EOF so this will - // exit with outerr == nil - //!_, outerr := io.Copy(conn, os.Stdin) - _, outerr := func(conn *hkexnet.Conn, r io.Reader) (w int64, e error) { - return io.Copy(conn, r) - }(conn, os.Stdin) - - if outerr != nil { - log.Println(outerr) - if outerr.Error() != "EOF" { - fmt.Println(outerr) - _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. - os.Exit(255) - } - } - log.Println("[Sent EOF]") - wg.Done() // client hung up, close WaitGroup to exit client - }() + defer conn.DisableChaff() + defer conn.ShutdownChaff() } - // Wait until both stdin and stdout goroutines finish - wg.Wait() + if shellMode { + doShellMode(isInteractive, conn, oldState, rec) + } else { + doCopyMode(conn, pathIsDest, fileArgs, rec) + } + + if oldState != nil { + _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. + } - _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. os.Exit(rec.status) } diff --git a/hkexshd/hkexshd.go b/hkexshd/hkexshd.go old mode 100644 new mode 100755 index 97a624e..a0874c9 --- a/hkexshd/hkexshd.go +++ b/hkexshd/hkexshd.go @@ -16,8 +16,10 @@ import ( "os" "os/exec" "os/user" + "path" "runtime" "strings" + "sync" "syscall" "blitter.com/go/goutmp" @@ -36,53 +38,170 @@ type cmdSpec struct { } /* -------------------------------------------------------------- */ - -/* - // 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) { +// Perform a client->server copy +func runClientToServerCopyAs(who string, conn hkexnet.Conn, fpath string, chaffing bool) (err error, exitStatus int) { u, _ := user.Lookup(who) var uid, gid uint32 fmt.Sscanf(u.Uid, "%d", &uid) fmt.Sscanf(u.Gid, "%d", &gid) - fmt.Println("uid:", uid, "gid:", gid) + log.Println("uid:", uid, "gid:", gid) - args := strings.Split(cmd, " ") - arg0 := args[0] - args = args[1:] - c := exec.Command(arg0, args...) + // Need to clear server's env and set key vars of the + // target user. This isn't perfect (TERM doesn't seem to + // work 100%; ANSI/xterm colour isn't working even + // if we set "xterm" or "ansi" here; and line count + // reported by 'stty -a' defaults to 24 regardless + // of client shell window used to run client. + // Investigate -- rlm 2018-01-26) + os.Clearenv() + os.Setenv("HOME", u.HomeDir) + os.Setenv("TERM", "vt102") // TODO: server or client option? + + var c *exec.Cmd + cmdName := "/bin/tar" + + var destDir string + if path.IsAbs(fpath) { + destDir = fpath + } else { + destDir = path.Join(u.HomeDir, fpath) + } + + cmdArgs := []string{"-xz", "-C", destDir} + + // NOTE the lack of quotes around --xform option's sed expression. + // When args are passed in exec() format, no quoting is required + // (as this isn't input from a shell) (right? -rlm 20180823) + //cmdArgs := []string{"-x", "-C", destDir, `--xform=s#.*/\(.*\)#\1#`} + c = exec.Command(cmdName, cmdArgs...) + + c.Dir = destDir + + //If os.Clearenv() isn't called by server above these will be seen in the + //client's session env. + //c.Env = []string{"HOME=" + u.HomeDir, "SUDO_GID=", "SUDO_UID=", "SUDO_USER=", "SUDO_COMMAND=", "MAIL=", "LOGNAME="+who} + //c.Dir = u.HomeDir c.SysProcAttr = &syscall.SysProcAttr{} c.SysProcAttr.Credential = &syscall.Credential{Uid: uid, Gid: gid} c.Stdin = conn - c.Stdout = conn - c.Stderr = conn + c.Stdout = os.Stdout + c.Stderr = os.Stderr - // Start the command with a pty. - ptmx, err := pty.Start(c) // returns immediately with ptmx file - if err != nil { - return err + if chaffing { + conn.EnableChaff() } - // 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. + defer conn.DisableChaff() + defer conn.ShutdownChaff() + // Start the command (no pty) + log.Printf("[%v %v]\n", cmdName, cmdArgs) + err = c.Start() // returns immediately if err != nil { log.Printf("Command finished with error: %v", err) - log.Printf("[%s]\n", cmd) + return err, 253 // !? + } else { + if err := c.Wait(); err != nil { + fmt.Println("*** c.Wait() done ***") + if exiterr, ok := err.(*exec.ExitError); ok { + // The program has exited with an exit code != 0 + + // This works on both Unix and Windows. Although package + // syscall is generally platform dependent, WaitStatus is + // defined for both Unix and Windows and in both cases has + // an ExitStatus() method with the same signature. + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { + exitStatus = status.ExitStatus() + log.Printf("Exit Status: %d", exitStatus) + } + } + } + fmt.Println("*** client->server cp finished ***") + return + } +} + +// Perform a server->client copy +func runServerToClientCopyAs(who string, conn hkexnet.Conn, srcPath string, chaffing bool) (err error, exitStatus int) { + u, _ := user.Lookup(who) + var uid, gid uint32 + fmt.Sscanf(u.Uid, "%d", &uid) + fmt.Sscanf(u.Gid, "%d", &gid) + log.Println("uid:", uid, "gid:", gid) + + // Need to clear server's env and set key vars of the + // target user. This isn't perfect (TERM doesn't seem to + // work 100%; ANSI/xterm colour isn't working even + // if we set "xterm" or "ansi" here; and line count + // reported by 'stty -a' defaults to 24 regardless + // of client shell window used to run client. + // Investigate -- rlm 2018-01-26) + os.Clearenv() + os.Setenv("HOME", u.HomeDir) + os.Setenv("TERM", "vt102") // TODO: server or client option? + + var c *exec.Cmd + cmdName := "/bin/tar" + if !path.IsAbs(srcPath) { + srcPath = fmt.Sprintf("%s%c%s", u.HomeDir, os.PathSeparator, srcPath) + } + + srcDir, srcBase := path.Split(srcPath) + cmdArgs := []string{"-cz", "-C", srcDir, "-f", "-", srcBase} + + c = exec.Command(cmdName, cmdArgs...) + + //If os.Clearenv() isn't called by server above these will be seen in the + //client's session env. + //c.Env = []string{"HOME=" + u.HomeDir, "SUDO_GID=", "SUDO_UID=", "SUDO_USER=", "SUDO_COMMAND=", "MAIL=", "LOGNAME="+who} + c.Dir = u.HomeDir + c.SysProcAttr = &syscall.SysProcAttr{} + c.SysProcAttr.Credential = &syscall.Credential{Uid: uid, Gid: gid} + c.Stdout = conn + // Stderr sinkholing is important. Any extraneous output to tarpipe + // messes up remote side as it's expecting pure tar data. + // (For example, if user specifies abs paths, tar outputs + // "Removing leading '/' from path names") + c.Stderr = nil + + if chaffing { + conn.EnableChaff() + } + //defer conn.Close() + defer conn.DisableChaff() + defer conn.ShutdownChaff() + + // Start the command (no pty) + log.Printf("[%v %v]\n", cmdName, cmdArgs) + err = c.Start() // returns immediately + if err != nil { + log.Printf("Command finished with error: %v", err) + return err, 253 // !? + } else { + if err := c.Wait(); err != nil { + fmt.Println("*** c.Wait() done ***") + if exiterr, ok := err.(*exec.ExitError); ok { + // The program has exited with an exit code != 0 + + // This works on both Unix and Windows. Although package + // syscall is generally platform dependent, WaitStatus is + // defined for both Unix and Windows and in both cases has + // an ExitStatus() method with the same signature. + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { + exitStatus = status.ExitStatus() + log.Printf("Exit Status: %d", exitStatus) + } + } + } + fmt.Println("*** server->client cp finished ***") + return } - return } -*/ // Run a command (via default shell) as a specific user // // Uses ptys to support commands which expect a terminal. func runShellAs(who string, cmd string, interactive bool, conn hkexnet.Conn, chaffing bool) (err error, exitStatus int) { + var wg sync.WaitGroup u, _ := user.Lookup(who) var uid, gid uint32 fmt.Sscanf(u.Uid, "%d", &uid) @@ -135,15 +254,16 @@ func runShellAs(who string, cmd string, interactive bool, conn hkexnet.Conn, cha log.Printf("[Setting term size to: %v %v]\n", sz.Rows, sz.Cols) pty.Setsize(ptmx, &pty.Winsize{Rows: sz.Rows, Cols: sz.Cols}) } + fmt.Println("*** WinCh goroutine done ***") }() // Copy stdin to the pty.. (bgnd goroutine) go func() { _, e := io.Copy(ptmx, conn) if e != nil { - log.Printf("** std->pty ended **\n") - return + log.Println("** stdin->pty ended **:", e.Error()) } + fmt.Println("*** stdin->pty goroutine done ***") }() if chaffing { @@ -153,17 +273,25 @@ func runShellAs(who string, cmd string, interactive bool, conn hkexnet.Conn, cha defer conn.ShutdownChaff() // ..and the pty to stdout. + // This may take some time exceeding that of the + // actual command's lifetime, so the c.Wait() below + // must synchronize with the completion of this goroutine + // to ensure all stdout data gets to the client before + // connection is closed. + wg.Add(1) go func() { + defer wg.Done() _, e := io.Copy(conn, ptmx) if e != nil { - log.Printf("** pty->stdout ended **\n") - return + log.Println("** pty->stdout ended **:", e.Error()) } // The above io.Copy() will exit when the command attached // to the pty exits + fmt.Println("*** pty->stdout goroutine done ***") }() if err := c.Wait(); err != nil { + fmt.Println("*** c.Wait() done ***") if exiterr, ok := err.(*exec.ExitError); ok { // The program has exited with an exit code != 0 @@ -177,6 +305,7 @@ func runShellAs(who string, cmd string, interactive bool, conn hkexnet.Conn, cha } } } + wg.Wait() // Wait on pty->stdout completion to client } return } @@ -202,10 +331,10 @@ func main() { flag.BoolVar(&vopt, "v", false, "show version") flag.StringVar(&laddr, "l", ":2000", "interface[:port] to listen") - flag.BoolVar(&chaffEnabled, "cE", true, "enabled chaff pkts") - flag.UintVar(&chaffFreqMin, "cfm", 100, "chaff pkt freq min (msecs)") - flag.UintVar(&chaffFreqMax, "cfM", 5000, "chaff pkt freq max (msecs)") - flag.UintVar(&chaffBytesMax, "cbM", 64, "chaff pkt size max (bytes)") + flag.BoolVar(&chaffEnabled, "e", true, "enabled chaff pkts") + flag.UintVar(&chaffFreqMin, "f", 100, "chaff pkt freq min (msecs)") + flag.UintVar(&chaffFreqMax, "F", 5000, "chaff pkt freq max (msecs)") + flag.UintVar(&chaffBytesMax, "B", 64, "chaff pkt size max (bytes)") flag.BoolVar(&dbg, "d", false, "debug logging") flag.Parse() @@ -354,6 +483,42 @@ func main() { log.Printf("[Shell completed for %s@%s, status %d]\n", rec.who, hname, cmdStatus) hc.SetStatus(uint8(cmdStatus)) } + } else if rec.op[0] == 'D' { + // File copy (destination) operation - client copy to server + log.Printf("[Client->Server copy]\n") + // TODO: call function with hc, rec.cmd, chaffEnabled etc. + // func hooks tar cmd right-half of pipe to hc Reader + addr := hc.RemoteAddr() + hname := strings.Split(addr.String(), ":")[0] + log.Printf("[Running copy for [%s@%s]]\n", rec.who, hname) + runErr, cmdStatus := runClientToServerCopyAs(string(rec.who), hc, string(rec.cmd), chaffEnabled) + // Returned hopefully via an EOF or exit/logout; + // Clear current op so user can enter next, or EOF + rec.op[0] = 0 + if runErr != nil { + log.Printf("[Error spawning cp for %s@%s]\n", rec.who, hname) + } else { + log.Printf("[Command completed for %s@%s, status %d]\n", rec.who, hname, cmdStatus) + hc.SetStatus(uint8(cmdStatus)) + } + } else if rec.op[0] == 'S' { + // File copy (src) operation - server copy to client + log.Printf("[Server->Client copy]\n") + // TODO: call function to copy rec.cmd (file list) to + // tar cmd left-half of pipeline to hc.Writer ? + addr := hc.RemoteAddr() + hname := strings.Split(addr.String(), ":")[0] + log.Printf("[Running copy for [%s@%s]]\n", rec.who, hname) + runErr, cmdStatus := runServerToClientCopyAs(string(rec.who), hc, string(rec.cmd), chaffEnabled) + // Returned hopefully via an EOF or exit/logout; + // Clear current op so user can enter next, or EOF + rec.op[0] = 0 + if runErr != nil { + log.Printf("[Error spawning cp for %s@%s]\n", rec.who, hname) + } else { + log.Printf("[Command completed for %s@%s, status %d]\n", rec.who, hname, cmdStatus) + hc.SetStatus(uint8(cmdStatus)) + } } else { log.Println("[Bad cmdSpec]") }