From 5859131678a1f83521f97a665b4eb7ac49912f6e Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Mon, 6 Aug 2018 22:29:51 -0700 Subject: [PATCH] Continuing groundwork for cp mode - refactor main client code into shell/copy subroutines; -r option --- cp.cmd | 1 + hkexnet/hkexnet.go | 3 +- hkexsh/hkexsh.go | 235 +++++++++++++++++++++++++++------------------ hkexshd/hkexshd.go | 12 ++- 4 files changed, 156 insertions(+), 95 deletions(-) diff --git a/cp.cmd b/cp.cmd index 72f2db3..23da425 100644 --- a/cp.cmd +++ b/cp.cmd @@ -9,3 +9,4 @@ tar -cz -f - testdir/sub1/bar.txt | \ # (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 e17bcb7..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 } @@ -455,7 +454,7 @@ 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) - if *hc.closeStat >90 { + if *hc.closeStat > 90 { log.Println("[cannot verify HMAC]") } else { // Log alert if hmac didn't match, corrupted channel diff --git a/hkexsh/hkexsh.go b/hkexsh/hkexsh.go index 34db2b3..364dbe4 100755 --- a/hkexsh/hkexsh.go +++ b/hkexsh/hkexsh.go @@ -54,11 +54,9 @@ func GetSize() (cols, rows int, err error) { } func parseNonSwitchArgs(a []string, dp string) (user, host, port, path string, isDest bool, otherArgs []string) { - //TODO: Look for non-option fancyArg of syntax user@host:filespec to set -r,-t and -u - // Consider: 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 - // * throw error if >1 fancyArgs are found in flags.Args() + // 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, fancyPort, fancyPath string for i, arg := range a { if strings.Contains(arg, ":") || strings.Contains(arg, "@") { @@ -102,8 +100,82 @@ func parseNonSwitchArgs(a []string, dp string) (user, host, port, path string, i return fancyUser, fancyHost, fancyPort, fancyPath, isDest, otherArgs } -// Demo of a simple client that dials up to a simple test server to -// send data. +// doCopyMode begins a secure hkexsh local<->remote file copy operation. +func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, recurs bool, rec *cmdSpec) { + // TODO: Bring in runShellAs(), stripped down, from hkexshd + // and build either side of tar pipeline: names? + // runTarSrc(), runTarSink() ? + if remoteDest { + fmt.Println("local files:", files, "remote filepath:", string(rec.cmd)) + } else { + fmt.Println("remote filepath:", string(rec.cmd), "local files:", files) + } +} + +// 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() +} + +// 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, @@ -124,6 +196,7 @@ func main() { var server string var cmdStr string + var recursiveCopy bool var copySrc []byte var copyDst string @@ -156,12 +229,10 @@ func main() { // a srcpath (-r) or dstpath (-t) flag.StringVar(&cmdStr, "x", "", "command to run (default empty - interactive shell)") shellMode = true - } // else { - //// hkexcp accepts srcpath (-r) and dstpath (-t), but not - //// a command (-x) - //flag.StringVar(©Src, "r", "", "copy srcpath") - //flag.StringVar(©Dst, "t", "", "copy dstpath") - //} + } else { + // Note: only makes sense for client->server copies + flag.BoolVar(&recursiveCopy, "r", false, "recursive copy/preserve tree copy") + } flag.Parse() tmpUser, tmpHost, tmpPort, tmpPath, pathIsDest, otherArgs := @@ -176,21 +247,35 @@ func main() { server = tmpHost + ":" + tmpPort //fmt.Println("tmpHost sets server to", server) } - if tmpPath != "" { + + var fileArgs string + if !shellMode && tmpPath != "" { // -if pathIsSrc && len(otherArgs) > 1 ERROR // -else flatten otherArgs into space-delim list => copySrc if pathIsDest { - for _, v := range otherArgs { - copySrc = append(copySrc, ' ') - copySrc = append(copySrc, v...) + if len(otherArgs) == 0 { + log.Fatal("ERROR: Must specify at least one src path for copy") + } else { + for _, v := range otherArgs { + copySrc = append(copySrc, ' ') + copySrc = append(copySrc, v...) + } + copyDst = tmpPath + fileArgs = string(copySrc) } - fmt.Println(">> copySrc:", string(copySrc)) - copyDst = tmpPath - } else { - if len(otherArgs) > 1 { + } else { + if len(otherArgs) == 0 { + log.Fatal("ERROR: Must specify dest 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 } } @@ -222,20 +307,39 @@ func main() { log.SetOutput(ioutil.Discard) } - // We must make the decision about interactivity before Dial() - // as it affects chaffing behaviour. 20180805 - if len(cmdStr) == 0 { - op = []byte{'s'} - isInteractive = true + 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 { - 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) - //!DEBUG - //chaffEnabled = false + // 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) @@ -306,68 +410,15 @@ func main() { defer conn.ShutdownChaff() } - //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]") - }() + if shellMode { + doShellMode(isInteractive, conn, oldState, rec) + } else { + doCopyMode(conn, pathIsDest, fileArgs, recursiveCopy, rec) } - // Wait until both stdin and stdout goroutines finish - // ** IMPORTANT! This must come before the Restore() tty call below - // in order to maintain raw mode for interactive sessions. -rlm 20180805 - wg.Wait() - - _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. + if oldState != nil { + _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. + } os.Exit(rec.status) } diff --git a/hkexshd/hkexshd.go b/hkexshd/hkexshd.go index 67c0ae6..7c2324d 100755 --- a/hkexshd/hkexshd.go +++ b/hkexshd/hkexshd.go @@ -175,7 +175,7 @@ func runShellAs(who string, cmd string, interactive bool, conn hkexnet.Conn, cha }() if err := c.Wait(); err != nil { - fmt.Println("*** c.Wait() done ***") + fmt.Println("*** c.Wait() done ***") if exiterr, ok := err.(*exec.ExitError); ok { // The program has exited with an exit code != 0 @@ -369,6 +369,16 @@ 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 + } 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 ? } else { log.Println("[Bad cmdSpec]") }