mirror of
https://gogs.blitter.com/RLabs/xs
synced 2024-08-14 10:26:42 +00:00
Merge branch 'hkexcp-proto'
This commit is contained in:
commit
45d270b03e
5 changed files with 611 additions and 158 deletions
12
cp.cmd
Normal file
12
cp.cmd
Normal file
|
@ -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 -
|
|
@ -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 {
|
||||
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,12 +454,16 @@ 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 {
|
||||
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})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
retN := hc.dBuf.Len()
|
||||
if retN > len(b) {
|
||||
|
|
1
hkexsh/hkexcp
Symbolic link
1
hkexsh/hkexcp
Symbolic link
|
@ -0,0 +1 @@
|
|||
hkexsh
|
477
hkexsh/hkexsh.go
Normal file → Executable file
477
hkexsh/hkexsh.go
Normal file → Executable file
|
@ -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,6 +468,7 @@ func main() {
|
|||
// TODO: send flag to server side indicating this
|
||||
// affects shell command used
|
||||
var oldState *hkexsh.State
|
||||
if shellMode {
|
||||
if isatty.IsTerminal(os.Stdin.Fd()) {
|
||||
oldState, err = hkexsh.MakeRaw(int(os.Stdin.Fd()))
|
||||
if err != nil {
|
||||
|
@ -127,33 +478,6 @@ func main() {
|
|||
} 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()
|
||||
if shellMode {
|
||||
doShellMode(isInteractive, conn, oldState, rec)
|
||||
} else {
|
||||
doCopyMode(conn, pathIsDest, fileArgs, rec)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if oldState != nil {
|
||||
_ = 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
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait until both stdin and stdout goroutines finish
|
||||
wg.Wait()
|
||||
|
||||
_ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort.
|
||||
os.Exit(rec.status)
|
||||
}
|
||||
|
|
233
hkexshd/hkexshd.go
Normal file → Executable file
233
hkexshd/hkexshd.go
Normal file → Executable file
|
@ -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
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// 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]")
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue