2018-04-07 20:04:10 +00:00
|
|
|
// hkexsh client
|
|
|
|
//
|
|
|
|
// Copyright (c) 2017-2018 Russell Magee
|
|
|
|
// Licensed under the terms of the MIT license (see LICENSE.mit in this
|
|
|
|
// distribution)
|
|
|
|
//
|
|
|
|
// golang implementation by Russ Magee (rmagee_at_gmail.com)
|
2018-01-06 15:30:56 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2018-09-06 04:58:55 +00:00
|
|
|
"bytes"
|
2018-09-06 20:50:56 +00:00
|
|
|
"encoding/binary"
|
2018-09-17 00:14:50 +00:00
|
|
|
"errors"
|
2018-01-12 07:01:39 +00:00
|
|
|
"flag"
|
2018-01-06 15:30:56 +00:00
|
|
|
"fmt"
|
2018-01-13 06:47:57 +00:00
|
|
|
"io"
|
2018-01-18 05:27:00 +00:00
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
2018-10-27 08:51:40 +00:00
|
|
|
"net"
|
2018-01-13 06:47:57 +00:00
|
|
|
"os"
|
2018-04-29 02:28:37 +00:00
|
|
|
"os/exec"
|
2018-01-21 23:46:40 +00:00
|
|
|
"os/user"
|
2018-08-31 03:06:42 +00:00
|
|
|
"path"
|
2018-09-01 17:20:33 +00:00
|
|
|
"path/filepath"
|
2018-05-05 06:25:26 +00:00
|
|
|
"runtime"
|
2018-01-21 23:46:40 +00:00
|
|
|
"strings"
|
2018-01-18 04:36:53 +00:00
|
|
|
"sync"
|
2018-08-25 01:50:45 +00:00
|
|
|
"syscall"
|
2018-01-06 20:26:08 +00:00
|
|
|
|
2018-04-28 23:05:33 +00:00
|
|
|
hkexsh "blitter.com/go/hkexsh"
|
2018-07-05 05:06:07 +00:00
|
|
|
"blitter.com/go/hkexsh/hkexnet"
|
2018-10-26 23:05:01 +00:00
|
|
|
"blitter.com/go/hkexsh/logger"
|
2018-10-27 08:51:40 +00:00
|
|
|
"blitter.com/go/hkexsh/spinsult"
|
2018-01-19 05:17:57 +00:00
|
|
|
isatty "github.com/mattn/go-isatty"
|
2018-01-06 15:30:56 +00:00
|
|
|
)
|
|
|
|
|
2018-05-26 20:43:09 +00:00
|
|
|
var (
|
2018-10-26 05:14:18 +00:00
|
|
|
wg sync.WaitGroup
|
2018-10-26 23:05:01 +00:00
|
|
|
Log *logger.Writer // reg. syslog output (no -d)
|
2018-05-26 20:43:09 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Get terminal size using 'stty' command
|
|
|
|
func GetSize() (cols, rows int, err error) {
|
2018-04-29 02:28:37 +00:00
|
|
|
cmd := exec.Command("stty", "size")
|
|
|
|
cmd.Stdin = os.Stdin
|
|
|
|
out, err := cmd.Output()
|
|
|
|
|
|
|
|
if err != nil {
|
2018-05-26 20:43:09 +00:00
|
|
|
log.Println(err)
|
2018-06-30 02:23:11 +00:00
|
|
|
cols, rows = 80, 24 //failsafe
|
2018-05-26 20:43:09 +00:00
|
|
|
} else {
|
|
|
|
fmt.Sscanf(string(out), "%d %d\n", &rows, &cols)
|
2018-04-29 02:28:37 +00:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-08-26 06:38:58 +00:00
|
|
|
func parseNonSwitchArgs(a []string) (user, host, path string, isDest bool, otherArgs []string) {
|
2018-08-07 05:29:51 +00:00
|
|
|
// 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
|
2018-08-26 06:38:58 +00:00
|
|
|
var fancyUser, fancyHost, fancyPath string
|
2018-07-29 19:47:44 +00:00
|
|
|
for i, arg := range a {
|
2018-07-29 07:48:42 +00:00
|
|
|
if strings.Contains(arg, ":") || strings.Contains(arg, "@") {
|
|
|
|
fancyArg := strings.Split(flag.Arg(i), "@")
|
2018-08-26 06:38:58 +00:00
|
|
|
var fancyHostPath []string
|
2018-07-29 07:48:42 +00:00
|
|
|
if len(fancyArg) < 2 {
|
|
|
|
//TODO: no user specified, use current
|
|
|
|
fancyUser = "[default:getUser]"
|
2018-08-26 06:38:58 +00:00
|
|
|
fancyHostPath = strings.Split(fancyArg[0], ":")
|
2018-07-29 07:48:42 +00:00
|
|
|
} else {
|
|
|
|
// user@....
|
|
|
|
fancyUser = fancyArg[0]
|
2018-08-26 06:38:58 +00:00
|
|
|
fancyHostPath = strings.Split(fancyArg[1], ":")
|
2018-07-29 07:48:42 +00:00
|
|
|
}
|
|
|
|
|
2018-08-26 06:38:58 +00:00
|
|
|
// [...@]host[:path]
|
|
|
|
if len(fancyHostPath) > 1 {
|
|
|
|
fancyPath = fancyHostPath[1]
|
2018-07-29 07:48:42 +00:00
|
|
|
}
|
2018-08-26 06:38:58 +00:00
|
|
|
fancyHost = fancyHostPath[0]
|
2018-07-29 07:48:42 +00:00
|
|
|
|
2018-08-06 04:43:21 +00:00
|
|
|
//if fancyPath == "" {
|
|
|
|
// fancyPath = "."
|
|
|
|
//}
|
2018-07-29 07:48:42 +00:00
|
|
|
|
2018-07-29 19:47:44 +00:00
|
|
|
if i == len(a)-1 {
|
|
|
|
isDest = true
|
2018-09-08 03:37:47 +00:00
|
|
|
//fmt.Println("remote path isDest")
|
2018-07-29 19:47:44 +00:00
|
|
|
}
|
2018-09-08 03:37:47 +00:00
|
|
|
//fmt.Println("fancyArgs: user:", fancyUser, "host:", fancyHost, "path:", fancyPath)
|
2018-07-29 19:47:44 +00:00
|
|
|
} else {
|
|
|
|
otherArgs = append(otherArgs, a[i])
|
2018-07-29 07:48:42 +00:00
|
|
|
}
|
|
|
|
}
|
2018-08-26 06:38:58 +00:00
|
|
|
return fancyUser, fancyHost, fancyPath, isDest, otherArgs
|
2018-07-29 07:48:42 +00:00
|
|
|
}
|
|
|
|
|
2018-08-07 05:29:51 +00:00
|
|
|
// doCopyMode begins a secure hkexsh local<->remote file copy operation.
|
2018-09-07 22:35:33 +00:00
|
|
|
func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, rec *hkexsh.Session) (err error, exitStatus uint32) {
|
2018-08-07 05:29:51 +00:00
|
|
|
if remoteDest {
|
2018-09-07 22:35:33 +00:00
|
|
|
log.Println("local files:", files, "remote filepath:", string(rec.Cmd()))
|
2018-08-25 01:50:45 +00:00
|
|
|
|
|
|
|
var c *exec.Cmd
|
|
|
|
|
|
|
|
//os.Clearenv()
|
|
|
|
//os.Setenv("HOME", u.HomeDir)
|
|
|
|
//os.Setenv("TERM", "vt102") // TODO: server or client option?
|
|
|
|
|
|
|
|
cmdName := "/bin/tar"
|
2018-09-06 23:37:17 +00:00
|
|
|
cmdArgs := []string{"-cz", "-f", "/dev/stdout"}
|
2018-08-26 06:38:58 +00:00
|
|
|
files = strings.TrimSpace(files)
|
2018-08-31 03:06:42 +00:00
|
|
|
// 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
|
2018-09-01 17:20:33 +00:00
|
|
|
// files and dirs to be extracted to a single dest dir server-side,
|
2018-09-03 02:58:13 +00:00
|
|
|
// whilst preserving the subtrees of dirs on the other side.
|
2018-08-31 03:06:42 +00:00
|
|
|
// 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 :)
|
2018-09-01 17:20:33 +00:00
|
|
|
//
|
|
|
|
// This is the 'scatter/gather' logic to allow specification of
|
|
|
|
// files and dirs in different trees to be deposited in a single
|
|
|
|
// remote destDir.
|
2018-08-26 06:38:58 +00:00
|
|
|
for _, v := range strings.Split(files, " ") {
|
2018-09-01 17:20:33 +00:00
|
|
|
v, _ = filepath.Abs(v)
|
2018-08-31 03:06:42 +00:00
|
|
|
dirTmp, fileTmp := path.Split(v)
|
2018-09-01 17:20:33 +00:00
|
|
|
if dirTmp == "" {
|
|
|
|
cmdArgs = append(cmdArgs, fileTmp)
|
|
|
|
} else {
|
|
|
|
cmdArgs = append(cmdArgs, "-C", dirTmp, fileTmp)
|
|
|
|
}
|
2018-08-26 06:38:58 +00:00
|
|
|
}
|
|
|
|
|
2018-09-06 23:37:17 +00:00
|
|
|
log.Printf("[%v %v]\n", cmdName, cmdArgs)
|
2018-08-25 01:50:45 +00:00
|
|
|
// 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...)
|
2018-08-25 06:22:07 +00:00
|
|
|
c.Dir, _ = os.Getwd()
|
2018-09-06 23:37:17 +00:00
|
|
|
log.Println("[wd:", c.Dir, "]")
|
2018-08-25 01:50:45 +00:00
|
|
|
c.Stdout = conn
|
2018-09-06 04:58:55 +00:00
|
|
|
stdErrBuffer := new(bytes.Buffer)
|
|
|
|
c.Stderr = stdErrBuffer
|
2018-08-25 06:22:07 +00:00
|
|
|
|
2018-08-25 01:50:45 +00:00
|
|
|
// Start the command (no pty)
|
|
|
|
err = c.Start() // returns immediately
|
2018-09-17 00:14:50 +00:00
|
|
|
/////////////
|
|
|
|
// NOTE: There is, apparently, a bug in Go stdlib here. Start()
|
|
|
|
// can actually return immediately, on a command which *does*
|
|
|
|
// start but exits quickly, with c.Wait() error
|
|
|
|
// "c.Wait status: exec: not started".
|
|
|
|
// As in this example, attempting a client->server copy to
|
|
|
|
// a nonexistent remote dir (it's tar exiting right away, exitStatus
|
|
|
|
// 2, stderr
|
|
|
|
// /bin/tar -xz -C /home/someuser/nosuchdir
|
|
|
|
// stderr: fork/exec /bin/tar: no such file or directory
|
|
|
|
//
|
|
|
|
// In this case, c.Wait() won't give us the real
|
|
|
|
// exit status (is it lost?).
|
|
|
|
/////////////
|
2018-08-25 01:50:45 +00:00
|
|
|
if err != nil {
|
2018-09-17 00:14:50 +00:00
|
|
|
fmt.Println("cmd exited immediately. Cannot get cmd.Wait().ExitStatus()")
|
|
|
|
err = errors.New("cmd exited prematurely")
|
|
|
|
exitStatus = uint32(2)
|
2018-08-25 01:50:45 +00:00
|
|
|
} 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 {
|
2018-09-06 20:50:56 +00:00
|
|
|
exitStatus = uint32(status.ExitStatus())
|
2018-09-06 04:58:55 +00:00
|
|
|
fmt.Print(stdErrBuffer)
|
2018-09-17 00:14:50 +00:00
|
|
|
fmt.Printf("Exit Status: %d\n", exitStatus) //#
|
2018-08-25 01:50:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-09-17 00:14:50 +00:00
|
|
|
// send CSOExitStatus to inform remote (server) end cp is done
|
|
|
|
log.Println("Sending local exitStatus:", exitStatus)
|
|
|
|
r := make([]byte, 4)
|
|
|
|
binary.BigEndian.PutUint32(r, exitStatus)
|
|
|
|
conn.WritePacket(r, hkexnet.CSOExitStatus)
|
|
|
|
|
|
|
|
// Do a final read for remote's exit status
|
2018-09-06 20:50:56 +00:00
|
|
|
s := make([]byte, 4)
|
2018-09-17 00:14:50 +00:00
|
|
|
_, remErr := conn.Read(s)
|
|
|
|
if remErr != io.EOF && !strings.Contains(remErr.Error(), "use of closed network") {
|
|
|
|
fmt.Printf("*** remote status Read() failed: %v\n", remErr)
|
|
|
|
}
|
|
|
|
|
|
|
|
// If local side status was OK, use remote side's status
|
|
|
|
if exitStatus == 0 {
|
2018-09-18 06:07:04 +00:00
|
|
|
exitStatus = uint32(conn.GetStatus())
|
2018-09-17 00:14:50 +00:00
|
|
|
log.Println("Received remote exitStatus:", exitStatus)
|
|
|
|
}
|
|
|
|
log.Printf("*** client->server cp finished , status %d ***\n", conn.GetStatus())
|
2018-08-25 01:50:45 +00:00
|
|
|
}
|
2018-08-07 05:29:51 +00:00
|
|
|
} else {
|
2018-09-07 22:35:33 +00:00
|
|
|
log.Println("remote filepath:", string(rec.Cmd()), "local files:", files)
|
2018-08-25 01:50:45 +00:00
|
|
|
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
|
|
|
|
|
2018-09-06 23:37:17 +00:00
|
|
|
cmdArgs := []string{"-xz", "-C", destPath}
|
|
|
|
log.Printf("[%v %v]\n", cmdName, cmdArgs)
|
2018-08-25 01:50:45 +00:00
|
|
|
// 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
|
2018-08-25 06:22:07 +00:00
|
|
|
|
2018-08-25 01:50:45 +00:00
|
|
|
// 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 {
|
2018-09-06 20:50:56 +00:00
|
|
|
exitStatus = uint32(status.ExitStatus())
|
2018-08-25 01:50:45 +00:00
|
|
|
log.Printf("Exit Status: %d", exitStatus)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-09-06 07:16:44 +00:00
|
|
|
// return local status, if nonzero;
|
|
|
|
// otherwise, return remote status if nonzero
|
|
|
|
if exitStatus == 0 {
|
2018-09-06 20:50:56 +00:00
|
|
|
exitStatus = uint32(conn.GetStatus())
|
2018-09-06 07:16:44 +00:00
|
|
|
}
|
2018-09-17 00:14:50 +00:00
|
|
|
fmt.Printf("*** server->client cp finished, status %d ***\n", conn.GetStatus())
|
2018-08-25 01:50:45 +00:00
|
|
|
}
|
2018-08-07 05:29:51 +00:00
|
|
|
}
|
2018-08-25 01:50:45 +00:00
|
|
|
return
|
2018-08-07 05:29:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// doShellMode begins an hkexsh shell session (one-shot command or interactive).
|
2018-09-07 22:35:33 +00:00
|
|
|
func doShellMode(isInteractive bool, conn *hkexnet.Conn, oldState *hkexsh.State, rec *hkexsh.Session) {
|
2018-08-07 05:29:51 +00:00
|
|
|
//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 {
|
|
|
|
_ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort.
|
2018-09-06 18:40:13 +00:00
|
|
|
// Copy operations and user logging off will cause
|
|
|
|
// a "use of closed network connection" so handle that
|
|
|
|
// gracefully here
|
2018-09-06 07:16:44 +00:00
|
|
|
if !strings.HasSuffix(inerr.Error(), "use of closed network connection") {
|
|
|
|
log.Println(inerr)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
2018-08-07 05:29:51 +00:00
|
|
|
}
|
|
|
|
|
2018-09-07 22:35:33 +00:00
|
|
|
rec.SetStatus(uint32(conn.GetStatus()))
|
|
|
|
log.Println("rec.status:", rec.Status)
|
2018-08-07 05:29:51 +00:00
|
|
|
|
|
|
|
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.
|
2018-09-06 23:23:57 +00:00
|
|
|
log.Println("[Hanging up]")
|
|
|
|
os.Exit(0)
|
2018-08-07 05:29:51 +00:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Wait until both stdin and stdout goroutines finish before returning
|
|
|
|
// (ensure client gets all data from server before closing)
|
|
|
|
wg.Wait()
|
|
|
|
}
|
|
|
|
|
2018-08-26 07:12:42 +00:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2018-09-06 23:23:57 +00:00
|
|
|
func rejectUserMsg() string {
|
|
|
|
return "Begone, " + spinsult.GetSentence() + "\r\n"
|
|
|
|
}
|
|
|
|
|
2018-10-29 02:17:47 +00:00
|
|
|
// Transmit request to server for it to set up the remote end of a tunnel
|
|
|
|
//
|
|
|
|
// Server responds with [CSOTunAck:rport] or [CSOTunRefused:rport]
|
2018-11-01 03:11:00 +00:00
|
|
|
// (handled in hkexnet.Read())
|
|
|
|
func reqTunnel(hc *hkexnet.Conn, lp uint16, p string /*net.Addr*/, rp uint16) {
|
|
|
|
// Write request to server so it can attempt to set up its end
|
2018-10-27 08:51:40 +00:00
|
|
|
var bTmp bytes.Buffer
|
2018-10-29 02:17:47 +00:00
|
|
|
binary.Write(&bTmp, binary.BigEndian, lp)
|
|
|
|
binary.Write(&bTmp, binary.BigEndian, rp)
|
2018-11-02 01:52:01 +00:00
|
|
|
fmt.Printf("bTmp:%x\n", bTmp.Bytes())
|
|
|
|
logger.LogDebug(fmt.Sprintln("[Client sending CSOTunSetup]"))
|
2018-11-01 03:11:00 +00:00
|
|
|
hc.WritePacket(bTmp.Bytes(), hkexnet.CSOTunSetup)
|
2018-11-02 05:14:01 +00:00
|
|
|
|
2018-11-01 03:11:00 +00:00
|
|
|
// Server should reply immediately with CSOTunSetupAck[lport:rport]
|
|
|
|
// hkexnet.Read() on server side handles server side tun setup.
|
2018-11-02 01:52:01 +00:00
|
|
|
resp := make([]byte, 4)
|
|
|
|
var lpResp, rpResp uint16
|
|
|
|
n, e := io.ReadFull(hc, resp)
|
|
|
|
if n < 4 || e != nil {
|
|
|
|
logger.LogErr(fmt.Sprintf("[Client tun response len %d, %s\n", n, e))
|
|
|
|
} else {
|
|
|
|
lpResp = binary.BigEndian.Uint16(resp[0:2])
|
|
|
|
rpResp = binary.BigEndian.Uint16(resp[2:4])
|
|
|
|
}
|
|
|
|
|
|
|
|
if lpResp == lp && rpResp == rp {
|
|
|
|
logger.LogDebug("[Client got tun setup ack OK]")
|
|
|
|
hc.StartClientTunnel(lp, rp)
|
|
|
|
} else {
|
|
|
|
logger.LogDebug(fmt.Sprintf("[Client tun response ports [%d:%d]\n", lpResp, rpResp))
|
|
|
|
logger.LogDebug(fmt.Sprintln("[Client tun setup FAILED]"))
|
|
|
|
}
|
2018-10-27 08:51:40 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-08-07 05:29:51 +00:00
|
|
|
// hkexsh - a client for secure shell and file copy operations.
|
2018-01-13 18:01:27 +00:00
|
|
|
//
|
|
|
|
// While conforming to the basic net.Conn interface HKex.Conn has extra
|
|
|
|
// capabilities designed to allow apps to define connection options,
|
|
|
|
// encryption/hmac settings and operations across the encrypted channel.
|
|
|
|
//
|
|
|
|
// Initial setup is the same as using plain net.Dial(), but one may
|
|
|
|
// specify extra extension tags (strings) to set the cipher and hmac
|
|
|
|
// setting desired; as well as the intended operation mode for the
|
|
|
|
// connection (app-specific, passed through to the server to use or
|
|
|
|
// ignore at its discretion).
|
2018-01-06 15:30:56 +00:00
|
|
|
func main() {
|
2018-09-18 00:27:13 +00:00
|
|
|
version := hkexsh.Version
|
2018-05-13 01:41:39 +00:00
|
|
|
var vopt bool
|
2018-09-14 18:58:10 +00:00
|
|
|
var gopt bool //login via password, asking server to generate authToken
|
2018-01-21 23:46:40 +00:00
|
|
|
var dbg bool
|
2018-08-06 04:43:21 +00:00
|
|
|
var shellMode bool // if true act as shell, else file copier
|
2018-10-11 04:12:38 +00:00
|
|
|
var cAlg string //cipher alg
|
|
|
|
var hAlg string //hmac alg
|
|
|
|
var kAlg string //KEX/KEM alg
|
2018-01-13 06:24:40 +00:00
|
|
|
var server string
|
2018-08-26 06:38:58 +00:00
|
|
|
var port uint
|
2018-01-21 23:46:40 +00:00
|
|
|
var cmdStr string
|
2018-10-27 08:51:40 +00:00
|
|
|
var tunSpecStr string // lport1:rport1[,lport2:rport2,...]
|
2018-07-19 05:32:49 +00:00
|
|
|
|
2018-07-29 20:22:35 +00:00
|
|
|
var copySrc []byte
|
2018-07-19 05:32:49 +00:00
|
|
|
var copyDst string
|
|
|
|
|
2018-01-22 06:02:08 +00:00
|
|
|
var authCookie string
|
2018-05-26 20:43:09 +00:00
|
|
|
var chaffEnabled bool
|
2018-05-07 00:41:09 +00:00
|
|
|
var chaffFreqMin uint
|
|
|
|
var chaffFreqMax uint
|
|
|
|
var chaffBytesMax uint
|
2018-05-07 01:20:12 +00:00
|
|
|
|
2018-07-19 05:32:49 +00:00
|
|
|
var op []byte
|
2018-01-19 05:17:57 +00:00
|
|
|
isInteractive := false
|
2018-01-13 06:47:57 +00:00
|
|
|
|
2018-05-13 01:41:39 +00:00
|
|
|
flag.BoolVar(&vopt, "v", false, "show version")
|
2018-07-19 05:32:49 +00:00
|
|
|
flag.BoolVar(&dbg, "d", false, "debug logging")
|
2018-10-24 07:15:33 +00:00
|
|
|
flag.StringVar(&cAlg, "c", "C_AES_256", "`cipher` [\"C_AES_256\" | \"C_TWOFISH_128\" | \"C_BLOWFISH_64\" | \"C_CRYPTMT1\"]")
|
2018-08-26 06:51:11 +00:00
|
|
|
flag.StringVar(&hAlg, "m", "H_SHA256", "`hmac` [\"H_SHA256\"]")
|
2018-10-12 23:16:49 +00:00
|
|
|
flag.StringVar(&kAlg, "k", "KEX_HERRADURA256", "`kex` [\"KEX_HERRADURA{256/512/1024/2048}\" | \"KEX_KYBER{512/768/1024}\"]")
|
2018-08-26 06:51:11 +00:00
|
|
|
flag.UintVar(&port, "p", 2000, "`port`")
|
2018-09-14 06:51:49 +00:00
|
|
|
//flag.StringVar(&authCookie, "a", "", "auth cookie")
|
2018-07-19 05:32:49 +00:00
|
|
|
flag.BoolVar(&chaffEnabled, "e", true, "enabled chaff pkts (default true)")
|
2018-10-27 08:51:40 +00:00
|
|
|
flag.UintVar(&chaffFreqMin, "f", 100, "`msecs-min` chaff pkt freq min (msecs)")
|
|
|
|
flag.UintVar(&chaffFreqMax, "F", 5000, "`msecs-max` chaff pkt freq max (msecs)")
|
|
|
|
flag.UintVar(&chaffBytesMax, "B", 64, "chaff pkt size max (bytes)")
|
2018-07-29 07:48:42 +00:00
|
|
|
|
2018-07-19 05:32:49 +00:00
|
|
|
// Find out what program we are (shell or copier)
|
|
|
|
myPath := strings.Split(os.Args[0], string(os.PathSeparator))
|
2018-07-29 07:48:42 +00:00
|
|
|
if myPath[len(myPath)-1] != "hkexcp" && myPath[len(myPath)-1] != "hkexcp.exe" {
|
2018-07-19 05:32:49 +00:00
|
|
|
// hkexsh accepts a command (-x) but not
|
|
|
|
// a srcpath (-r) or dstpath (-t)
|
2018-08-26 06:51:11 +00:00
|
|
|
flag.StringVar(&cmdStr, "x", "", "`command` to run (if not specified run interactive shell)")
|
2018-10-27 08:51:40 +00:00
|
|
|
flag.StringVar(&tunSpecStr, "t", "", "`tunnelspec` localPort:remotePort[,localPort:remotePort,...]")
|
2018-09-14 18:58:10 +00:00
|
|
|
flag.BoolVar(&gopt, "g", false, "ask server to generate authtoken")
|
2018-08-06 04:43:21 +00:00
|
|
|
shellMode = true
|
2018-08-26 07:12:42 +00:00
|
|
|
flag.Usage = UsageShell
|
2018-08-07 05:29:51 +00:00
|
|
|
} else {
|
2018-08-26 07:12:42 +00:00
|
|
|
flag.Usage = UsageCp
|
2018-08-07 05:29:51 +00:00
|
|
|
}
|
2018-07-29 07:48:42 +00:00
|
|
|
flag.Parse()
|
|
|
|
|
2018-09-14 08:13:14 +00:00
|
|
|
remoteUser, remoteHost, tmpPath, pathIsDest, otherArgs :=
|
2018-08-26 06:38:58 +00:00
|
|
|
parseNonSwitchArgs(flag.Args())
|
2018-09-08 03:37:47 +00:00
|
|
|
//fmt.Println("otherArgs:", otherArgs)
|
2018-08-26 06:38:58 +00:00
|
|
|
|
|
|
|
// 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
|
2018-07-29 07:48:42 +00:00
|
|
|
}
|
2018-08-26 06:38:58 +00:00
|
|
|
|
2018-09-14 08:13:14 +00:00
|
|
|
if remoteHost != "" {
|
|
|
|
server = remoteHost + ":" + fmt.Sprintf("%d", port)
|
2018-08-26 06:38:58 +00:00
|
|
|
}
|
|
|
|
if tmpPath == "" {
|
|
|
|
tmpPath = "."
|
2018-07-29 07:48:42 +00:00
|
|
|
}
|
2018-08-07 05:29:51 +00:00
|
|
|
|
|
|
|
var fileArgs string
|
2018-08-26 06:38:58 +00:00
|
|
|
if !shellMode /*&& tmpPath != ""*/ {
|
2018-07-29 20:22:35 +00:00
|
|
|
// -if pathIsSrc && len(otherArgs) > 1 ERROR
|
|
|
|
// -else flatten otherArgs into space-delim list => copySrc
|
2018-07-29 19:47:44 +00:00
|
|
|
if pathIsDest {
|
2018-08-07 05:29:51 +00:00
|
|
|
if len(otherArgs) == 0 {
|
2018-08-25 01:50:45 +00:00
|
|
|
log.Fatal("ERROR: Must specify at least one dest path for copy")
|
2018-08-07 05:29:51 +00:00
|
|
|
} else {
|
|
|
|
for _, v := range otherArgs {
|
|
|
|
copySrc = append(copySrc, ' ')
|
|
|
|
copySrc = append(copySrc, v...)
|
|
|
|
}
|
|
|
|
copyDst = tmpPath
|
|
|
|
fileArgs = string(copySrc)
|
2018-07-29 20:22:35 +00:00
|
|
|
}
|
2018-08-23 18:03:19 +00:00
|
|
|
} else {
|
2018-08-07 05:29:51 +00:00
|
|
|
if len(otherArgs) == 0 {
|
2018-08-25 01:50:45 +00:00
|
|
|
log.Fatal("ERROR: Must specify src path for copy")
|
2018-08-07 05:29:51 +00:00
|
|
|
} 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 {
|
2018-07-29 20:22:35 +00:00
|
|
|
log.Fatal("ERROR: cannot specify more than one dest path for copy")
|
|
|
|
}
|
2018-08-06 04:43:21 +00:00
|
|
|
copySrc = []byte(tmpPath)
|
2018-08-07 05:29:51 +00:00
|
|
|
fileArgs = copyDst
|
2018-07-29 19:47:44 +00:00
|
|
|
}
|
2018-07-29 07:48:42 +00:00
|
|
|
}
|
|
|
|
|
2018-08-06 04:43:21 +00:00
|
|
|
// Do some more option consistency checks
|
2018-07-19 05:32:49 +00:00
|
|
|
|
2018-08-06 04:43:21 +00:00
|
|
|
//fmt.Println("server finally is:", server)
|
2018-07-29 07:48:42 +00:00
|
|
|
if flag.NFlag() == 0 && server == "" {
|
2018-07-19 05:32:49 +00:00
|
|
|
flag.Usage()
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
2018-01-12 07:01:39 +00:00
|
|
|
|
2018-05-13 01:41:39 +00:00
|
|
|
if vopt {
|
|
|
|
fmt.Printf("version v%s\n", version)
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
|
2018-07-19 05:32:49 +00:00
|
|
|
if len(cmdStr) != 0 && (len(copySrc) != 0 || len(copyDst) != 0) {
|
2018-07-29 20:22:35 +00:00
|
|
|
log.Fatal("incompatible options -- either cmd (-x) or copy ops but not both")
|
2018-07-19 05:32:49 +00:00
|
|
|
}
|
|
|
|
|
2018-08-06 04:43:21 +00:00
|
|
|
//-------------------------------------------------------------------
|
|
|
|
// Here we have parsed all options and can now carry out
|
|
|
|
// either the shell session or copy operation.
|
|
|
|
_ = shellMode
|
|
|
|
|
2018-11-02 01:52:01 +00:00
|
|
|
Log, _ = logger.New(logger.LOG_USER|logger.LOG_DEBUG|logger.LOG_NOTICE|logger.LOG_ERR, "hkexsh")
|
|
|
|
hkexnet.Init(dbg, "hkexsh", logger.LOG_USER|logger.LOG_DEBUG|logger.LOG_NOTICE|logger.LOG_ERR)
|
2018-01-21 23:46:40 +00:00
|
|
|
if dbg {
|
2018-10-26 05:14:18 +00:00
|
|
|
log.SetOutput(Log)
|
2018-01-21 23:46:40 +00:00
|
|
|
} else {
|
|
|
|
log.SetOutput(ioutil.Discard)
|
|
|
|
}
|
2018-10-27 08:51:40 +00:00
|
|
|
|
2018-09-14 18:58:10 +00:00
|
|
|
if !gopt {
|
|
|
|
// See if we can log in via an auth token
|
|
|
|
u, _ := user.Current()
|
|
|
|
ab, aerr := ioutil.ReadFile(fmt.Sprintf("%s/.hkexsh_id", u.HomeDir))
|
|
|
|
if aerr == nil {
|
|
|
|
//authCookie = string(ab)
|
|
|
|
idx := strings.Index(string(ab), remoteHost)
|
2018-09-17 00:56:17 +00:00
|
|
|
//fmt.Printf("auth entry idx:%d\n", idx)
|
2018-09-14 18:58:10 +00:00
|
|
|
if idx >= 0 {
|
2018-10-19 03:44:23 +00:00
|
|
|
//fmt.Fprintln(os.Stderr, "[authtoken]")
|
2018-09-14 18:58:10 +00:00
|
|
|
ab = ab[idx:]
|
2018-09-17 00:30:02 +00:00
|
|
|
entries := strings.SplitN(string(ab), "\n", -1)
|
|
|
|
//if len(entries) > 0 {
|
|
|
|
//fmt.Println("entries[0]:", entries[0])
|
|
|
|
authCookie = strings.TrimSpace(entries[0])
|
|
|
|
//} else {
|
|
|
|
// fmt.Fprintln(os.Stderr, "ERROR: no matching authtoken")
|
|
|
|
// os.Exit(1)
|
|
|
|
//}
|
|
|
|
// Security scrub
|
|
|
|
ab = nil
|
|
|
|
runtime.GC()
|
2018-09-14 18:58:10 +00:00
|
|
|
} else {
|
2018-09-17 00:56:17 +00:00
|
|
|
fmt.Fprintln(os.Stderr, "[no authtoken, use -g to request one from server]")
|
2018-09-14 18:58:10 +00:00
|
|
|
}
|
2018-09-17 00:56:17 +00:00
|
|
|
} else {
|
|
|
|
log.Printf("[cannot read %s/.hkexsh_id]\n", u.HomeDir)
|
2018-09-14 18:58:10 +00:00
|
|
|
}
|
2018-09-14 06:51:49 +00:00
|
|
|
}
|
|
|
|
|
2018-08-07 05:29:51 +00:00
|
|
|
if shellMode {
|
|
|
|
// We must make the decision about interactivity before Dial()
|
|
|
|
// as it affects chaffing behaviour. 20180805
|
2018-09-14 18:58:10 +00:00
|
|
|
if gopt {
|
2018-09-17 00:56:17 +00:00
|
|
|
fmt.Fprintln(os.Stderr, "[requesting authtoken from server]")
|
2018-09-14 06:51:49 +00:00
|
|
|
op = []byte{'A'}
|
|
|
|
chaffFreqMin = 2
|
|
|
|
chaffFreqMax = 10
|
|
|
|
} else if len(cmdStr) == 0 {
|
2018-08-07 05:29:51 +00:00
|
|
|
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
|
|
|
|
}
|
2018-08-06 04:43:21 +00:00
|
|
|
} else {
|
2018-08-07 05:29:51 +00:00
|
|
|
// as copy mode is also non-interactive, set up chaffing
|
|
|
|
// just like the 'c' mode above
|
2018-08-06 04:43:21 +00:00
|
|
|
chaffFreqMin = 2
|
|
|
|
chaffFreqMax = 10
|
2018-08-07 05:29:51 +00:00
|
|
|
|
|
|
|
if pathIsDest {
|
|
|
|
// client->server file copy
|
|
|
|
// src file list is in copySrc
|
|
|
|
op = []byte{'D'}
|
2018-09-14 06:51:49 +00:00
|
|
|
//fmt.Println("client->server copy:", string(copySrc), "->", copyDst)
|
2018-08-07 05:29:51 +00:00
|
|
|
cmdStr = copyDst
|
|
|
|
} else {
|
|
|
|
// server->client file copy
|
|
|
|
// remote src file(s) in copyDsr
|
|
|
|
op = []byte{'S'}
|
2018-09-14 06:51:49 +00:00
|
|
|
//fmt.Println("server->client copy:", string(copySrc), "->", copyDst)
|
2018-08-07 05:29:51 +00:00
|
|
|
cmdStr = string(copySrc)
|
|
|
|
}
|
2018-08-06 04:43:21 +00:00
|
|
|
}
|
|
|
|
|
2018-10-11 04:12:38 +00:00
|
|
|
conn, err := hkexnet.Dial("tcp", server, cAlg, hAlg, kAlg)
|
2018-01-06 15:30:56 +00:00
|
|
|
if err != nil {
|
2018-09-14 06:51:49 +00:00
|
|
|
fmt.Println(err)
|
2018-01-13 06:47:57 +00:00
|
|
|
panic(err)
|
|
|
|
}
|
2018-01-18 04:36:53 +00:00
|
|
|
defer conn.Close()
|
2018-01-23 21:53:05 +00:00
|
|
|
// From this point on, conn is a secure encrypted channel
|
2018-01-18 04:36:53 +00:00
|
|
|
|
2018-01-19 05:17:57 +00:00
|
|
|
// Set stdin in raw mode if it's an interactive session
|
2018-01-21 23:46:40 +00:00
|
|
|
// TODO: send flag to server side indicating this
|
|
|
|
// affects shell command used
|
2018-04-15 19:58:24 +00:00
|
|
|
var oldState *hkexsh.State
|
2018-08-06 04:43:21 +00:00
|
|
|
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")
|
2018-01-19 05:17:57 +00:00
|
|
|
}
|
2018-01-19 02:57:37 +00:00
|
|
|
}
|
|
|
|
|
2018-01-22 06:02:08 +00:00
|
|
|
if len(authCookie) == 0 {
|
2018-09-14 07:40:20 +00:00
|
|
|
//No auth token, prompt for password
|
2018-01-22 06:02:08 +00:00
|
|
|
fmt.Printf("Gimme cookie:")
|
2018-04-04 22:43:27 +00:00
|
|
|
ab, err := hkexsh.ReadPassword(int(os.Stdin.Fd()))
|
2018-01-23 21:53:05 +00:00
|
|
|
fmt.Printf("\r\n")
|
2018-01-22 06:02:08 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
authCookie = string(ab)
|
2018-05-05 06:25:26 +00:00
|
|
|
// Security scrub
|
|
|
|
ab = nil
|
|
|
|
runtime.GC()
|
2018-01-22 06:02:08 +00:00
|
|
|
}
|
2018-09-14 06:51:49 +00:00
|
|
|
|
2018-09-08 03:37:47 +00:00
|
|
|
// Set up session params and send over to server
|
2018-09-14 08:13:14 +00:00
|
|
|
rec := hkexsh.NewSession(op, []byte(uname), []byte(remoteHost), []byte(os.Getenv("TERM")), []byte(cmdStr), []byte(authCookie), 0)
|
|
|
|
_, err = fmt.Fprintf(conn, "%d %d %d %d %d %d\n",
|
|
|
|
len(rec.Op()), len(rec.Who()), len(rec.ConnHost()), len(rec.TermType()), len(rec.Cmd()), len(rec.AuthCookie(true)))
|
2018-09-07 22:35:33 +00:00
|
|
|
_, err = conn.Write(rec.Op())
|
|
|
|
_, err = conn.Write(rec.Who())
|
2018-09-14 08:13:14 +00:00
|
|
|
_, err = conn.Write(rec.ConnHost())
|
2018-09-08 03:37:47 +00:00
|
|
|
_, err = conn.Write(rec.TermType())
|
2018-09-07 22:35:33 +00:00
|
|
|
_, err = conn.Write(rec.Cmd())
|
|
|
|
_, err = conn.Write(rec.AuthCookie(true))
|
2018-03-26 02:58:04 +00:00
|
|
|
|
2018-09-14 07:40:20 +00:00
|
|
|
//Security scrub
|
2018-09-14 08:13:14 +00:00
|
|
|
authCookie = ""
|
2018-09-14 07:40:20 +00:00
|
|
|
runtime.GC()
|
|
|
|
|
2018-09-06 23:23:57 +00:00
|
|
|
// Read auth reply from server
|
|
|
|
authReply := make([]byte, 1) // bool: 0 = fail, 1 = pass
|
|
|
|
_, err = conn.Read(authReply)
|
|
|
|
if authReply[0] == 0 {
|
|
|
|
fmt.Fprintln(os.Stderr, rejectUserMsg())
|
2018-09-07 22:35:33 +00:00
|
|
|
rec.SetStatus(255)
|
2018-08-07 05:29:51 +00:00
|
|
|
} else {
|
2018-01-18 04:36:53 +00:00
|
|
|
|
2018-09-06 23:23:57 +00:00
|
|
|
// Set up chaffing to server
|
|
|
|
conn.SetupChaff(chaffFreqMin, chaffFreqMax, chaffBytesMax) // enable client->server chaffing
|
|
|
|
if chaffEnabled {
|
2018-10-27 08:51:40 +00:00
|
|
|
conn.EnableChaff() // goroutine, returns immediately
|
2018-09-06 23:23:57 +00:00
|
|
|
defer conn.DisableChaff()
|
|
|
|
defer conn.ShutdownChaff()
|
|
|
|
}
|
2018-08-06 07:06:09 +00:00
|
|
|
|
2018-09-06 23:23:57 +00:00
|
|
|
if shellMode {
|
2018-10-27 08:51:40 +00:00
|
|
|
// TESTING - tunnel
|
|
|
|
remAddrs, _ := net.LookupHost(remoteHost)
|
2018-11-01 03:11:00 +00:00
|
|
|
reqTunnel(&conn, 6001, remAddrs[0], 7001)
|
2018-10-27 08:51:40 +00:00
|
|
|
// END TESTING - tunnel
|
|
|
|
|
2018-09-29 19:15:53 +00:00
|
|
|
doShellMode(isInteractive, &conn, oldState, rec)
|
2018-09-06 23:23:57 +00:00
|
|
|
} else { // copyMode
|
2018-09-29 19:15:53 +00:00
|
|
|
_, s := doCopyMode(&conn, pathIsDest, fileArgs, rec)
|
2018-09-14 06:51:49 +00:00
|
|
|
rec.SetStatus(s)
|
2018-09-06 20:50:56 +00:00
|
|
|
}
|
2018-09-07 22:35:33 +00:00
|
|
|
|
|
|
|
if rec.Status() != 0 {
|
2018-09-17 00:14:50 +00:00
|
|
|
fmt.Fprintln(os.Stderr, "Session exited with status:", rec.Status())
|
2018-09-06 23:23:57 +00:00
|
|
|
}
|
|
|
|
}
|
2018-09-06 20:50:56 +00:00
|
|
|
|
2018-09-06 23:23:57 +00:00
|
|
|
if oldState != nil {
|
|
|
|
_ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort.
|
2018-09-06 18:40:13 +00:00
|
|
|
}
|
2018-09-07 22:35:33 +00:00
|
|
|
os.Exit(int(rec.Status()))
|
2018-01-06 15:30:56 +00:00
|
|
|
}
|