mirror of https://gogs.blitter.com/RLabs/xs
				
				
				
			
		
			
				
	
	
		
			960 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			Go
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			960 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			Go
		
	
	
		
			Executable File
		
	
	
| // xsd server
 | |
| //
 | |
| // Copyright (c) 2017-2020 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)
 | |
| package main
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"crypto/rand"
 | |
| 	"encoding/binary"
 | |
| 	"encoding/hex"
 | |
| 	"errors"
 | |
| 	"flag"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"log"
 | |
| 	"net/http"
 | |
| 	"os"
 | |
| 	"os/exec"
 | |
| 	"os/signal"
 | |
| 	"os/user"
 | |
| 	"path"
 | |
| 	"runtime"
 | |
| 	"runtime/pprof"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"syscall"
 | |
| 	"time"
 | |
| 	"unsafe"
 | |
| 
 | |
| 	"blitter.com/go/goutmp"
 | |
| 	xs "blitter.com/go/xs"
 | |
| 	"blitter.com/go/xs/logger"
 | |
| 	"blitter.com/go/xs/xsnet"
 | |
| 	"github.com/creack/pty"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	version   string
 | |
| 	gitCommit string // set in -ldflags by build
 | |
| 
 | |
| 	useSysLogin bool
 | |
| 	kcpMode     string // set to a valid KCP BlockCrypt alg tag to use rather than TCP
 | |
| 
 | |
| 	// Log - syslog output (with no -d)
 | |
| 	Log *logger.Writer
 | |
| 
 | |
| 	cpuprofile string
 | |
| 	memprofile string
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	AuthTokenLen     = 64
 | |
| 	LoginTimeoutSecs = 30
 | |
| )
 | |
| 
 | |
| func ioctl(fd, request, argp uintptr) error {
 | |
| 	if _, _, e := syscall.Syscall6(syscall.SYS_IOCTL, fd, request, argp, 0, 0, 0); e != 0 {
 | |
| 		return e
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func ptsName(fd uintptr) (string, error) {
 | |
| 	var n uintptr
 | |
| 	err := ioctl(fd, syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n)))
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	return fmt.Sprintf("/dev/pts/%d", n), nil
 | |
| }
 | |
| 
 | |
| /* -------------------------------------------------------------- */
 | |
| // Perform a client->server copy
 | |
| func runClientToServerCopyAs(who, ttype string, conn *xsnet.Conn, fpath string, chaffing bool) (exitStatus uint32, err error) {
 | |
| 	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.
 | |
| 	os.Clearenv()
 | |
| 	os.Setenv("HOME", u.HomeDir)
 | |
| 	os.Setenv("TERM", ttype)
 | |
| 	os.Setenv("XS_SESSION", "1")
 | |
| 
 | |
| 	var c *exec.Cmd
 | |
| 	cmdName := xs.GetTool("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#`}
 | |
| 	fmt.Println(cmdName, cmdArgs)
 | |
| 	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 = os.Stdout
 | |
| 	c.Stderr = os.Stderr
 | |
| 
 | |
| 	// === Set up connection keepalive to client
 | |
| 	conn.StartupKeepAlive() // goroutine, returns immediately
 | |
| 	defer conn.ShutdownKeepAlive()
 | |
| 
 | |
| 	if chaffing {
 | |
| 		conn.StartupChaff()
 | |
| 	}
 | |
| 	defer conn.ShutdownChaff()
 | |
| 
 | |
| 	// Start the command (no pty)
 | |
| 	log.Printf("[%v %v]\n", cmdName, cmdArgs)
 | |
| 	err = c.Start() // returns immediately
 | |
| 	/////////////
 | |
| 	// 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?).
 | |
| 	/////////////
 | |
| 	if err != nil {
 | |
| 		log.Println("cmd exited immediately. Cannot get cmd.Wait().ExitStatus()")
 | |
| 		err = errors.New("cmd exited prematurely")
 | |
| 		//exitStatus = uint32(254)
 | |
| 		exitStatus = xsnet.CSEExecFail
 | |
| 	} 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 = uint32(status.ExitStatus())
 | |
| 					//err = errors.New("cmd returned nonzero status")
 | |
| 					log.Printf("Exit Status: %d\n", exitStatus)
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		log.Println("*** client->server cp finished ***")
 | |
| 	}
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // Perform a server->client copy
 | |
| func runServerToClientCopyAs(who, ttype string, conn *xsnet.Conn, srcPath string, chaffing bool) (exitStatus uint32, err error) {
 | |
| 	u, err := user.Lookup(who)
 | |
| 	if err != nil {
 | |
| 		exitStatus = 1
 | |
| 		return
 | |
| 	}
 | |
| 	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.
 | |
| 	os.Clearenv()
 | |
| 	_ = os.Setenv("HOME", u.HomeDir)
 | |
| 	_ = os.Setenv("TERM", ttype)
 | |
| 	_ = os.Setenv("XS_SESSION", "1")
 | |
| 
 | |
| 	var c *exec.Cmd
 | |
| 	cmdName := xs.GetTool("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 (or buffering to something other than stdout)
 | |
| 	// 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")
 | |
| 	stdErrBuffer := new(bytes.Buffer)
 | |
| 	c.Stderr = stdErrBuffer
 | |
| 	//c.Stderr = nil
 | |
| 
 | |
| 	// === Set up connection keepalive to client
 | |
| 	conn.StartupKeepAlive() // goroutine, returns immediately
 | |
| 	defer conn.ShutdownKeepAlive()
 | |
| 
 | |
| 	if chaffing {
 | |
| 		conn.StartupChaff()
 | |
| 	}
 | |
| 	//defer conn.Close()
 | |
| 	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 xsnet.CSEExecFail, err // !?
 | |
| 	}
 | |
| 	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 = uint32(status.ExitStatus())
 | |
| 				if len(stdErrBuffer.Bytes()) > 0 {
 | |
| 					log.Print(stdErrBuffer)
 | |
| 				}
 | |
| 				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. //nolint:gofmt
 | |
| func runShellAs(who, hname, ttype, cmd string, interactive bool, //nolint:funlen
 | |
| 	conn *xsnet.Conn, chaffing bool) (exitStatus uint32, err error) {
 | |
| 	var wg sync.WaitGroup
 | |
| 	u, err := user.Lookup(who)
 | |
| 	if err != nil {
 | |
| 		exitStatus = 1
 | |
| 		return
 | |
| 	}
 | |
| 	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.
 | |
| 	os.Clearenv()
 | |
| 	_ = os.Setenv("HOME", u.HomeDir)
 | |
| 	_ = os.Setenv("TERM", ttype)
 | |
| 	_ = os.Setenv("XS_SESSION", "1")
 | |
| 
 | |
| 	var c *exec.Cmd
 | |
| 
 | |
| 	if interactive {
 | |
| 		if useSysLogin {
 | |
| 			// Use the server's login binary (post-auth)
 | |
| 			//
 | |
| 			// Things UNIX login does, like print the 'motd',
 | |
| 			// and use the shell specified by /etc/passwd, will be done
 | |
| 			// automagically, at the cost of another external tool
 | |
| 			// dependency.
 | |
| 			//
 | |
| 			// One drawback of using 'login' is that the remote side
 | |
| 			// cannot give us back the shell's exit code, since it
 | |
| 			// exits back to 'login', which usually returns its own
 | |
| 			// 0 status back to us.
 | |
| 			//
 | |
| 			// Note login will drop privs to the intended user for us.
 | |
| 			//
 | |
| 			c = exec.Command(xs.GetTool("login"), "-f", "-p", who) //nolint:gosec
 | |
| 		} else {
 | |
| 			// Run shell directly (which allows nonzero exit codes back to
 | |
| 			// the local system upon shell exit, whereas 'login' does not.)
 | |
| 			//
 | |
| 			// Note we must drop privs ourselves for the user shell since
 | |
| 			// we aren't using 'login' on the remote end which would do it
 | |
| 			// for us.
 | |
| 			//
 | |
| 			c = exec.Command(xs.GetTool("bash"), "-i", "-l") //nolint:gosec
 | |
| 			c.SysProcAttr = &syscall.SysProcAttr{}
 | |
| 			c.SysProcAttr.Credential = &syscall.Credential{Uid: uid, Gid: gid}
 | |
| 		}
 | |
| 	} else {
 | |
| 		c = exec.Command(xs.GetTool("bash"), "-c", cmd) //nolint:gosec
 | |
| 		c.SysProcAttr = &syscall.SysProcAttr{}
 | |
| 		c.SysProcAttr.Credential = &syscall.Credential{Uid: uid, Gid: gid}
 | |
| 	}
 | |
| 	//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
 | |
| 
 | |
| 	// Start the command with a pty.
 | |
| 	ptmx, err := pty.Start(c) // returns immediately with ptmx file
 | |
| 	if err != nil {
 | |
| 		log.Println(err)
 | |
| 		return xsnet.CSEPtyExecFail, err
 | |
| 	}
 | |
| 	// Make sure to close the pty at the end.
 | |
| 	// #gv:s/label=\"runShellAs\$1\"/label=\"deferPtmxClose\"/
 | |
| 	defer func() {
 | |
| 		//logger.LogDebug(fmt.Sprintf("[Exited process was %d]", c.Process.Pid))
 | |
| 		_ = ptmx.Close()
 | |
| 	}()
 | |
| 
 | |
| 	// get pty info for system accounting (who, lastlog)
 | |
| 	pts, pe := ptsName(ptmx.Fd())
 | |
| 	if pe != nil {
 | |
| 		return xsnet.CSEPtyGetNameFail, err
 | |
| 	}
 | |
| 	utmpx := goutmp.Put_utmp(who, pts, hname)
 | |
| 	defer func() { goutmp.Unput_utmp(utmpx) }()
 | |
| 	goutmp.Put_lastlog_entry("xs", who, pts, hname)
 | |
| 
 | |
| 	conn.Pproc = c.Process.Pid
 | |
| 	//fmt.Printf("[process %d started]\n", c.Process.Pid)
 | |
| 
 | |
| 	log.Printf("[%s]\n", cmd)
 | |
| 	if err != nil {
 | |
| 		log.Printf("Command finished with error: %v", err)
 | |
| 	} else {
 | |
| 		// Watch for term resizes
 | |
| 		// #gv:s/label=\"runShellAs\$2\"/label=\"termResizeWatcher\"/
 | |
| 		go func() {
 | |
| 			for sz := range conn.WinCh {
 | |
| 				log.Printf("[Setting term size to: %v %v]\n", sz.Rows, sz.Cols)
 | |
| 				pty.Setsize(ptmx, &pty.Winsize{Rows: sz.Rows, Cols: sz.Cols}) //nolint:errcheck
 | |
| 			}
 | |
| 			log.Println("*** WinCh goroutine done ***")
 | |
| 		}()
 | |
| 
 | |
| 		// Copy stdin to the pty.. (bgnd goroutine)
 | |
| 		// #gv:s/label=\"runShellAs\$3\"/label=\"stdinToPtyWorker\"/
 | |
| 		go func() {
 | |
| 			_, e := io.Copy(ptmx, conn)
 | |
| 			if e != nil {
 | |
| 				log.Println("** stdin->pty ended **:", e.Error())
 | |
| 			} else {
 | |
| 				log.Println("*** stdin->pty goroutine done ***")
 | |
| 			}
 | |
| 		}()
 | |
| 
 | |
| 		// === Set up connection keepalive to client
 | |
| 		conn.StartupKeepAlive() // goroutine, returns immediately
 | |
| 		defer conn.ShutdownKeepAlive()
 | |
| 
 | |
| 		if chaffing {
 | |
| 			conn.StartupChaff()
 | |
| 		}
 | |
| 		// #gv:s/label=\"runShellAs\$4\"/label=\"deferChaffShutdown\"/
 | |
| 		defer func() {
 | |
| 			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)
 | |
| 		// #gv:s/label=\"runShellAs\$5\"/label=\"ptyToStdoutWorker\"/
 | |
| 		go func() {
 | |
| 			defer wg.Done()
 | |
| 			_, e := io.Copy(conn, ptmx)
 | |
| 			if e != nil {
 | |
| 				log.Println("** pty->stdout ended **:", e.Error())
 | |
| 			} else {
 | |
| 				// The above io.Copy() will exit when the command attached
 | |
| 				// to the pty exits
 | |
| 				log.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
 | |
| 
 | |
| 				// 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 = uint32(status.ExitStatus())
 | |
| 					log.Printf("Exit Status: %d", exitStatus)
 | |
| 				}
 | |
| 			}
 | |
| 			conn.SetStatus(xsnet.CSOType(exitStatus))
 | |
| 		} else {
 | |
| 			logger.LogDebug(fmt.Sprintf("*** Main proc has exited (%d) ***", c.ProcessState.ExitCode())) //nolint:errcheck
 | |
| 			// Background jobs still may be running; close the
 | |
| 			// pty anyway, so the client can return before
 | |
| 			// wg.Wait() below completes (Issue #18)
 | |
| 			if interactive {
 | |
| 				_ = ptmx.Close()
 | |
| 			}
 | |
| 		}
 | |
| 		wg.Wait() // Wait on pty->stdout completion to client
 | |
| 	}
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // GenAuthToken generates a pseudorandom auth token for a specific
 | |
| // user from a specific host to allow non-interactive logins.
 | |
| func GenAuthToken(who string, connhost string) string {
 | |
| 	//hname, e := os.Hostname()
 | |
| 	//if e != nil {
 | |
| 	//	hname = "#badhost#"
 | |
| 	//}
 | |
| 	hname := connhost
 | |
| 
 | |
| 	token := make([]byte, AuthTokenLen)
 | |
| 	_, _ = rand.Read(token)
 | |
| 	return fmt.Sprintf("%s:%s:%s", hname, who, hex.EncodeToString(token))
 | |
| }
 | |
| 
 | |
| var (
 | |
| 	aKEXAlgs    allowedKEXAlgs
 | |
| 	aCipherAlgs allowedCipherAlgs
 | |
| 	aHMACAlgs   allowedHMACAlgs
 | |
| )
 | |
| 
 | |
| type allowedKEXAlgs []string
 | |
| type allowedCipherAlgs []string
 | |
| type allowedHMACAlgs []string
 | |
| 
 | |
| func (a allowedKEXAlgs) allowed(k xsnet.KEXAlg) bool {
 | |
| 	for i := 0; i < len(a); i++ {
 | |
| 		if a[i] == "KEX_all" || a[i] == k.String() {
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| func (a *allowedKEXAlgs) String() string {
 | |
| 	return fmt.Sprintf("allowedKEXAlgs: %v", *a)
 | |
| }
 | |
| 
 | |
| func (a *allowedKEXAlgs) Set(value string) error {
 | |
| 	*a = append(*a, strings.TrimSpace(value))
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (a allowedCipherAlgs) allowed(c xsnet.CSCipherAlg) bool {
 | |
| 	for i := 0; i < len(a); i++ {
 | |
| 		if a[i] == "C_all" || a[i] == c.String() {
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| func (a *allowedCipherAlgs) String() string {
 | |
| 	return fmt.Sprintf("allowedCipherAlgs: %v", *a)
 | |
| }
 | |
| 
 | |
| func (a *allowedCipherAlgs) Set(value string) error {
 | |
| 	*a = append(*a, strings.TrimSpace(value))
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (a allowedHMACAlgs) allowed(h xsnet.CSHmacAlg) bool {
 | |
| 	for i := 0; i < len(a); i++ {
 | |
| 		if a[i] == "H_all" || a[i] == h.String() {
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| func (a *allowedHMACAlgs) String() string {
 | |
| 	return fmt.Sprintf("allowedHMACAlgs: %v", *a)
 | |
| }
 | |
| 
 | |
| func (a *allowedHMACAlgs) Set(value string) error {
 | |
| 	*a = append(*a, strings.TrimSpace(value))
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Main server that listens and spawns goroutines for each
 | |
| // connecting client to serve interactive or file copy sessions
 | |
| // and any requested tunnels.
 | |
| // Note that this server does not do UNIX forks of itself to give
 | |
| // each client its own separate manager process, so if the main
 | |
| // daemon dies, all clients will be rudely disconnected.
 | |
| // Consider this when planning to restart or upgrade in-place an installation.
 | |
| // TODO: reduce gocyclo
 | |
| func main() { //nolint:funlen,gocyclo
 | |
| 	var vopt bool
 | |
| 	var chaffEnabled bool
 | |
| 	var chaffFreqMin uint
 | |
| 	var chaffFreqMax uint
 | |
| 	var chaffBytesMax uint
 | |
| 	var dbg bool
 | |
| 	var laddr string
 | |
| 	var rekeySecs uint
 | |
| 	var remodSupported bool // true: when rekeying, switch to random cipher/hmac alg
 | |
| 
 | |
| 	var useSystemPasswd bool
 | |
| 
 | |
| 	flag.BoolVar(&vopt, "v", false, "show version")
 | |
| 	flag.UintVar(&rekeySecs, "r", 300, "rekey interval in `secs`")
 | |
| 	flag.BoolVar(&remodSupported, "R", false, "Borg Countermeasures (remodulate cipher/hmac alg on each rekey)")
 | |
| 	flag.StringVar(&laddr, "l", ":2000", "interface[:port] to listen")                                                                                                                                                                                                         //nolint:gomnd,lll
 | |
| 	flag.StringVar(&kcpMode, "K", "unused", `set to one of ["KCP_NONE","KCP_AES", "KCP_BLOWFISH", "KCP_CAST5", "KCP_SM4", "KCP_SALSA20", "KCP_SIMPLEXOR", "KCP_TEA", "KCP_3DES", "KCP_TWOFISH", "KCP_XTEA"] to use KCP (github.com/xtaci/kcp-go) reliable UDP instead of TCP`) //nolint:lll
 | |
| 	flag.BoolVar(&useSysLogin, "L", false, "use system login")
 | |
| 	flag.BoolVar(&chaffEnabled, "e", true, "enable chaff pkts")
 | |
| 	flag.UintVar(&chaffFreqMin, "f", 100, "chaff pkt freq min (msecs)")  //nolint:gomnd
 | |
| 	flag.UintVar(&chaffFreqMax, "F", 5000, "chaff pkt freq max (msecs)") //nolint:gomnd
 | |
| 	flag.UintVar(&chaffBytesMax, "B", 64, "chaff pkt size max (bytes)")  //nolint:gomnd
 | |
| 	flag.BoolVar(&useSystemPasswd, "s", true, "use system shadow passwds")
 | |
| 	flag.BoolVar(&dbg, "d", false, "debug logging")
 | |
| 	flag.Var(&aKEXAlgs, "aK", "Allowed KEX `alg`s (eg. '-aK KEXAlgA -aK KEXAlgB ...')"+`
 | |
|       KEX_all
 | |
|       KEX_HERRADURA256
 | |
|       KEX_HERRADURA512
 | |
|       KEX_HERRADURA1024
 | |
|       KEX_HERRADURA2048
 | |
|       KEX_KYBER512
 | |
|       KEX_KYBER768
 | |
|       KEX_KYBER1024
 | |
|       KEX_NEWHOPE
 | |
|       KEX_NEWHOPE_SIMPLE
 | |
|       KEX_FRODOKEM_1344AES
 | |
|       KEX_FRODOKEM_1344SHAKE
 | |
|       KEX_FRODOKEM_976AES
 | |
|       KEX_FRODOKEM_976SHAKE`)
 | |
| 	flag.Var(&aCipherAlgs, "aC", "Allowed `cipher`s (eg. '-aC CAlgA -aC CAlgB ...')"+`
 | |
|       C_all
 | |
|       C_AES_256
 | |
|       C_TWOFISH_128
 | |
|       C_BLOWFISH_64
 | |
|       C_CRYPTMT1
 | |
|       C_HOPSCOTCH
 | |
|       C_CHACHA20_12`)
 | |
| 	flag.Var(&aHMACAlgs, "aH", "Allowed `HMAC`s (eg. '-aH HMACAlgA -aH HMACAlgB ...')"+`
 | |
|       H_all
 | |
|       H_SHA256
 | |
|       H_SHA512`)
 | |
| 
 | |
| 	flag.StringVar(&cpuprofile, "cpuprofile", "", "write cpu profile to <`file`>")
 | |
| 	flag.StringVar(&memprofile, "memprofile", "", "write memory profile to <`file`>")
 | |
| 
 | |
| 	flag.Parse()
 | |
| 
 | |
| 	if vopt {
 | |
| 		fmt.Printf("version %s (%s)\n", version, gitCommit)
 | |
| 		os.Exit(0)
 | |
| 	}
 | |
| 
 | |
| 	{
 | |
| 		me, e := user.Current()
 | |
| 		if e != nil || me.Uid != "0" {
 | |
| 			log.Fatal("Must run as root.")
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// === Profiling instrumentation
 | |
| 
 | |
| 	if cpuprofile != "" {
 | |
| 		f, err := os.Create(cpuprofile)
 | |
| 		if err != nil {
 | |
| 			log.Fatal("could not create CPU profile: ", err)
 | |
| 		}
 | |
| 		defer f.Close()
 | |
| 		fmt.Println("StartCPUProfile()")
 | |
| 		if err := pprof.StartCPUProfile(f); err != nil {
 | |
| 			log.Fatal("could not start CPU profile: ", err) //nolint:gocritic
 | |
| 		} else {
 | |
| 			defer pprof.StopCPUProfile()
 | |
| 		}
 | |
| 
 | |
| 		go func() { http.ListenAndServe("localhost:6060", nil) }() //nolint:errcheck,gosec
 | |
| 	}
 | |
| 
 | |
| 	// Enforce some sane min/max vals on chaff flags
 | |
| 	if chaffFreqMin < 2 { //nolint:gomnd
 | |
| 		chaffFreqMin = 2
 | |
| 	}
 | |
| 	if chaffFreqMax == 0 {
 | |
| 		chaffFreqMax = chaffFreqMin + 1
 | |
| 	}
 | |
| 	if chaffBytesMax == 0 || chaffBytesMax > 4096 {
 | |
| 		chaffBytesMax = 64
 | |
| 	}
 | |
| 
 | |
| 	Log, _ = logger.New(logger.LOG_DAEMON|logger.LOG_DEBUG|logger.LOG_NOTICE|logger.LOG_ERR, "xsd")
 | |
| 	xsnet.Init(dbg, "xsd", logger.LOG_DAEMON|logger.LOG_DEBUG|logger.LOG_NOTICE|logger.LOG_ERR)
 | |
| 	if dbg {
 | |
| 		log.SetOutput(Log)
 | |
| 	} else {
 | |
| 		log.SetOutput(io.Discard)
 | |
| 	}
 | |
| 
 | |
| 	// Set up allowed algs, if specified (default allow all)
 | |
| 	if len(aKEXAlgs) == 0 {
 | |
| 		aKEXAlgs = []string{"none"}
 | |
| 	}
 | |
| 	logger.LogNotice(fmt.Sprintf("Allowed KEXAlgs: %v\n", aKEXAlgs)) //nolint:errcheck
 | |
| 
 | |
| 	if len(aCipherAlgs) == 0 {
 | |
| 		aCipherAlgs = []string{"none"}
 | |
| 	}
 | |
| 	logger.LogNotice(fmt.Sprintf("Allowed CipherAlgs: %v\n", aCipherAlgs)) //nolint:errcheck
 | |
| 
 | |
| 	if len(aHMACAlgs) == 0 {
 | |
| 		aHMACAlgs = []string{"none"}
 | |
| 	}
 | |
| 	logger.LogNotice(fmt.Sprintf("Allowed HMACAlgs: %v\n", aHMACAlgs)) //nolint:errcheck
 | |
| 
 | |
| 	// Set up handler for daemon signalling
 | |
| 	exitCh := make(chan os.Signal, 1)
 | |
| 	signal.Notify(exitCh, os.Signal(syscall.SIGTERM), os.Signal(syscall.SIGINT), os.Signal(syscall.SIGHUP), os.Signal(syscall.SIGUSR1), os.Signal(syscall.SIGUSR2)) //nolint:lll
 | |
| 	go func() {
 | |
| 		for {
 | |
| 			sig := <-exitCh
 | |
| 			switch sig {
 | |
| 			case syscall.SIGTERM: //"terminated":
 | |
| 				logger.LogNotice(fmt.Sprintf("[Got signal: %s]", sig.String())) //nolint:errcheck
 | |
| 				signal.Reset()
 | |
| 				syscall.Kill(0, syscall.SIGTERM) //nolint:errcheck
 | |
| 			case syscall.SIGINT: //"interrupt":
 | |
| 				logger.LogNotice(fmt.Sprintf("[Got signal: %s]", sig.String())) //nolint:errcheck
 | |
| 				signal.Reset()
 | |
| 				syscall.Kill(0, syscall.SIGINT) //nolint:errcheck
 | |
| 			case syscall.SIGHUP: //"hangup":
 | |
| 				logger.LogNotice(fmt.Sprintf("[Got signal: %s - nop]", sig.String())) //nolint:errcheck
 | |
| 				if cpuprofile != "" || memprofile != "" {
 | |
| 					dumpProf()
 | |
| 				}
 | |
| 			default:
 | |
| 				logger.LogNotice(fmt.Sprintf("[Got signal: %s - ignored]", sig.String())) //nolint:errcheck
 | |
| 			}
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	proto := "tcp"
 | |
| 	if kcpMode != "unused" {
 | |
| 		proto = "kcp"
 | |
| 	}
 | |
| 	l, err := xsnet.Listen(proto, laddr, kcpMode)
 | |
| 	if err != nil {
 | |
| 		log.Fatal(err)
 | |
| 	}
 | |
| 	defer l.Close()
 | |
| 
 | |
| 	log.Println("Serving on", laddr)
 | |
| 	for {
 | |
| 		// Wait for a connection.
 | |
| 		// Then check if client-proposed algs are allowed
 | |
| 		conn, err := l.Accept()
 | |
| 		//logger.LogDebug(fmt.Sprintf("l.Accept()\n"))
 | |
| 		if err != nil {
 | |
| 			log.Printf("Accept() got error(%v), hanging up.\n", err)
 | |
| 		} else {
 | |
| 			if !aKEXAlgs.allowed(conn.KEX()) {
 | |
| 				log.Printf("Accept() rejected for banned KEX alg %d, hanging up.\n", conn.KEX())
 | |
| 				conn.SetStatus(xsnet.CSEKEXAlgDenied)
 | |
| 				conn.Close()
 | |
| 			} else if !aCipherAlgs.allowed(conn.CAlg()) {
 | |
| 				log.Printf("Accept() rejected for banned Cipher alg %d, hanging up.\n", conn.CAlg())
 | |
| 				conn.SetStatus(xsnet.CSECipherAlgDenied)
 | |
| 				conn.Close()
 | |
| 			} else if !aHMACAlgs.allowed(conn.HAlg()) {
 | |
| 				log.Printf("Accept() rejected for banned HMAC alg %d, hanging up.\n", conn.HAlg())
 | |
| 				conn.SetStatus(xsnet.CSEHMACAlgDenied)
 | |
| 				conn.Close()
 | |
| 			} else {
 | |
| 				log.Println("Accepted client")
 | |
| 
 | |
| 				// Only enable cipher alg changes on re-key if we were told
 | |
| 				// to support it (launching xsd with -R), *and* the client
 | |
| 				// proposes to use it.
 | |
| 				if !remodSupported {
 | |
| 					if (conn.Opts() & xsnet.CORemodulateShields) != 0 {
 | |
| 						logger.LogDebug("[client proposed cipher/hmac remod, but we don't support it.]")
 | |
| 						conn.Close()
 | |
| 						continue
 | |
| 					}
 | |
| 				} else {
 | |
| 					if conn.Opts()&xsnet.CORemodulateShields != 0 {
 | |
| 						logger.LogDebug("[cipher/hmac remodulation active]")
 | |
| 					} else {
 | |
| 						logger.LogDebug("[cipher/hmac remodulation inactive]")
 | |
| 					}
 | |
| 				}
 | |
| 				conn.RekeyHelper(rekeySecs)
 | |
| 
 | |
| 				// Set up chaffing to client
 | |
| 				// Will only start when runShellAs() is called
 | |
| 				// after stdin/stdout are hooked up
 | |
| 				conn.SetupChaff(chaffFreqMin, chaffFreqMax, chaffBytesMax) // configure server->client chaffing
 | |
| 
 | |
| 				// Handle the connection in a new goroutine.
 | |
| 				// The loop then returns to accepting, so that
 | |
| 				// multiple connections may be served concurrently.
 | |
| 				go func(hc *xsnet.Conn) (e error) {
 | |
| 					defer hc.ShutdownRekey()
 | |
| 					defer hc.Close()
 | |
| 
 | |
| 					// Start login timeout here and disconnect if user/pass phase stalls
 | |
| 					loginTimeout := time.AfterFunc(LoginTimeoutSecs*time.Second, func() {
 | |
| 						logger.LogNotice(fmt.Sprintln("Login timed out")) //nolint:errcheck
 | |
| 						hc.Write([]byte{0})                               //nolint:errcheck
 | |
| 						hc.Close()
 | |
| 					})
 | |
| 
 | |
| 					//We use io.ReadFull() here to guarantee we consume
 | |
| 					//just the data we want for the xs.Session, and no more.
 | |
| 					//Otherwise data will be sitting in the channel that isn't
 | |
| 					//passed down to the command handlers.
 | |
| 					var rec xs.Session
 | |
| 					var len1, len2, len3, len4, len5, len6 uint32
 | |
| 
 | |
| 					n, err := fmt.Fscanf(hc, "%d %d %d %d %d %d\n", &len1, &len2, &len3, &len4, &len5, &len6)
 | |
| 					log.Printf("xs.Session read:%d %d %d %d %d %d\n", len1, len2, len3, len4, len5, len6)
 | |
| 
 | |
| 					if err != nil || n < 6 {
 | |
| 						log.Println("[Bad xs.Session fmt]")
 | |
| 						return err
 | |
| 					}
 | |
| 
 | |
| 					tmp := make([]byte, len1)
 | |
| 					_, err = io.ReadFull(hc, tmp)
 | |
| 					if err != nil {
 | |
| 						log.Println("[Bad xs.Session.Op]")
 | |
| 						return err
 | |
| 					}
 | |
| 					rec.SetOp(tmp)
 | |
| 
 | |
| 					tmp = make([]byte, len2)
 | |
| 					_, err = io.ReadFull(hc, tmp)
 | |
| 					if err != nil {
 | |
| 						log.Println("[Bad xs.Session.Who]")
 | |
| 						return err
 | |
| 					}
 | |
| 					rec.SetWho(tmp)
 | |
| 
 | |
| 					tmp = make([]byte, len3)
 | |
| 					_, err = io.ReadFull(hc, tmp)
 | |
| 					if err != nil {
 | |
| 						log.Println("[Bad xs.Session.ConnHost]")
 | |
| 						return err
 | |
| 					}
 | |
| 					rec.SetConnHost(tmp)
 | |
| 
 | |
| 					tmp = make([]byte, len4)
 | |
| 					_, err = io.ReadFull(hc, tmp)
 | |
| 					if err != nil {
 | |
| 						log.Println("[Bad xs.Session.TermType]")
 | |
| 						return err
 | |
| 					}
 | |
| 					rec.SetTermType(tmp)
 | |
| 
 | |
| 					tmp = make([]byte, len5)
 | |
| 					_, err = io.ReadFull(hc, tmp)
 | |
| 					if err != nil {
 | |
| 						log.Println("[Bad xs.Session.Cmd]")
 | |
| 						return err
 | |
| 					}
 | |
| 					rec.SetCmd(tmp)
 | |
| 
 | |
| 					tmp = make([]byte, len6)
 | |
| 					_, err = io.ReadFull(hc, tmp)
 | |
| 					if err != nil {
 | |
| 						log.Println("[Bad xs.Session.AuthCookie]")
 | |
| 						return err
 | |
| 					}
 | |
| 					rec.SetAuthCookie(tmp)
 | |
| 
 | |
| 					log.Printf("[xs.Session: op:%c who:%s connhost:%s cmd:%s auth:****]\n",
 | |
| 						rec.Op()[0], string(rec.Who()), string(rec.ConnHost()), string(rec.Cmd()))
 | |
| 
 | |
| 					var valid bool
 | |
| 					var allowedCmds string // Currently unused
 | |
| 					if xs.AuthUserByToken(xs.NewAuthCtx(), string(rec.Who()), string(rec.ConnHost()), string(rec.AuthCookie(true))) {
 | |
| 						valid = true
 | |
| 					} else {
 | |
| 						if useSystemPasswd {
 | |
| 							//var passErr error
 | |
| 							valid, _ /*passErr*/ = xs.VerifyPass(xs.NewAuthCtx(), string(rec.Who()), string(rec.AuthCookie(true)))
 | |
| 						} else {
 | |
| 							valid, allowedCmds = xs.AuthUserByPasswd(xs.NewAuthCtx(), string(rec.Who()), string(rec.AuthCookie(true)), "/etc/xs.passwd")
 | |
| 						}
 | |
| 					}
 | |
| 
 | |
| 					_ = loginTimeout.Stop()
 | |
| 					// Security scrub
 | |
| 					rec.ClearAuthCookie()
 | |
| 
 | |
| 					// Tell client if auth was valid
 | |
| 					if valid {
 | |
| 						hc.Write([]byte{1}) //nolint:errcheck
 | |
| 					} else {
 | |
| 						logger.LogNotice(fmt.Sprintln("Invalid user", string(rec.Who()))) //nolint:errcheck
 | |
| 						hc.Write([]byte{0})                                               //nolint:errcheck
 | |
| 						return
 | |
| 					}
 | |
| 
 | |
| 					log.Printf("[allowedCmds:%s]\n", allowedCmds)
 | |
| 
 | |
| 					if rec.Op()[0] == 'A' {
 | |
| 						// Generate automated login token
 | |
| 						addr := hc.RemoteAddr()
 | |
| 						hname := goutmp.GetHost(addr.String())
 | |
| 						logger.LogNotice(fmt.Sprintf("[Generating autologin token for [%s@%s]]\n", rec.Who(), hname)) //nolint:errcheck
 | |
| 						token := GenAuthToken(string(rec.Who()), string(rec.ConnHost()))
 | |
| 						tokenCmd := fmt.Sprintf("echo %q | tee -a ~/.xs_id", token)
 | |
| 						cmdStatus, runErr := runShellAs(string(rec.Who()), hname, string(rec.TermType()), tokenCmd, false, hc, chaffEnabled)
 | |
| 						// Returned hopefully via an EOF or exit/logout;
 | |
| 						// Clear current op so user can enter next, or EOF
 | |
| 						rec.SetOp([]byte{0})
 | |
| 						if runErr != nil {
 | |
| 							logger.LogErr(fmt.Sprintf("[Error generating autologin token for %s@%s]\n", rec.Who(), hname)) //nolint:errcheck
 | |
| 						} else {
 | |
| 							log.Printf("[Autologin token generation completed for %s@%s, status %d]\n", rec.Who(), hname, cmdStatus)
 | |
| 							hc.SetStatus(xsnet.CSOType(cmdStatus))
 | |
| 						}
 | |
| 					} else if rec.Op()[0] == 'c' {
 | |
| 						// Non-interactive command
 | |
| 						addr := hc.RemoteAddr()
 | |
| 						hname := goutmp.GetHost(addr.String())
 | |
| 						logger.LogNotice(fmt.Sprintf("[Running command for [%s@%s]]\n", rec.Who(), hname)) //nolint:errcheck
 | |
| 						cmdStatus, runErr := runShellAs(string(rec.Who()), hname, string(rec.TermType()), string(rec.Cmd()), false, hc, chaffEnabled)
 | |
| 						// Returned hopefully via an EOF or exit/logout;
 | |
| 						// Clear current op so user can enter next, or EOF
 | |
| 						rec.SetOp([]byte{0})
 | |
| 						if runErr != nil {
 | |
| 							logger.LogErr(fmt.Sprintf("[Error spawning cmd for %s@%s]\n", rec.Who(), hname)) //nolint:errcheck
 | |
| 						} else {
 | |
| 							logger.LogNotice(fmt.Sprintf("[Command completed for %s@%s, status %d]\n", rec.Who(), hname, cmdStatus)) //nolint:errcheck
 | |
| 							hc.SetStatus(xsnet.CSOType(cmdStatus))
 | |
| 						}
 | |
| 					} else if rec.Op()[0] == 's' {
 | |
| 						// Interactive session
 | |
| 						addr := hc.RemoteAddr()
 | |
| 						hname := goutmp.GetHost(addr.String())
 | |
| 						logger.LogNotice(fmt.Sprintf("[Running shell for [%s@%s]]\n", rec.Who(), hname)) //nolint:errcheck
 | |
| 
 | |
| 						cmdStatus, runErr := runShellAs(string(rec.Who()), hname, string(rec.TermType()), string(rec.Cmd()), true, hc, chaffEnabled)
 | |
| 						// Returned hopefully via an EOF or exit/logout;
 | |
| 						// Clear current op so user can enter next, or EOF
 | |
| 						rec.SetOp([]byte{0})
 | |
| 						if runErr != nil {
 | |
| 							Log.Err(fmt.Sprintf("[Error spawning shell for %s@%s]\n", rec.Who(), hname)) //nolint:errcheck
 | |
| 						} else {
 | |
| 							logger.LogNotice(fmt.Sprintf("[Shell completed for %s@%s, status %d]\n", rec.Who(), hname, cmdStatus)) //nolint:errcheck
 | |
| 							hc.SetStatus(xsnet.CSOType(cmdStatus))
 | |
| 						}
 | |
| 					} else if rec.Op()[0] == 'D' {
 | |
| 						// File copy (destination) operation - client copy to server
 | |
| 						log.Printf("[Client->Server copy]\n")
 | |
| 						addr := hc.RemoteAddr()
 | |
| 						hname := goutmp.GetHost(addr.String())
 | |
| 						logger.LogNotice(fmt.Sprintf("[c->s copy for %s@%s]\n", rec.Who(), hname)) //nolint:errcheck
 | |
| 						cmdStatus, runErr := runClientToServerCopyAs(string(rec.Who()), string(rec.TermType()), hc, string(rec.Cmd()), chaffEnabled)
 | |
| 						// Returned hopefully via an EOF or exit/logout;
 | |
| 						// Clear current op so user can enter next, or EOF
 | |
| 						rec.SetOp([]byte{0})
 | |
| 						if runErr != nil {
 | |
| 							logger.LogErr(fmt.Sprintf("[c->s copy error for %s@%s]\n", rec.Who(), hname)) //nolint:errcheck
 | |
| 						} else {
 | |
| 							logger.LogNotice(fmt.Sprintf("[c->s copy completed for %s@%s, status %d]\n", rec.Who(), hname, cmdStatus)) //nolint:errcheck
 | |
| 						}
 | |
| 						// TODO: Test this with huge files.. see Bug #22 - do we need to
 | |
| 						//   sync w/sender (client) that we've gotten all data?
 | |
| 						hc.SetStatus(xsnet.CSOType(cmdStatus))
 | |
| 
 | |
| 						// Send CSOExitStatus *before* client closes channel
 | |
| 						s := make([]byte, 4) //nolint:gomnd
 | |
| 						binary.BigEndian.PutUint32(s, cmdStatus)
 | |
| 						log.Printf("** cp writing closeStat %d at Close()\n", cmdStatus)
 | |
| 						hc.WritePacket(s, xsnet.CSOExitStatus) //nolint:errcheck
 | |
| 					} else if rec.Op()[0] == 'S' {
 | |
| 						// File copy (src) operation - server copy to client
 | |
| 						log.Printf("[Server->Client copy]\n")
 | |
| 						addr := hc.RemoteAddr()
 | |
| 						hname := goutmp.GetHost(addr.String())
 | |
| 						logger.LogNotice(fmt.Sprintf("[s->c copy for %s@%s]\n", rec.Who(), hname)) //nolint:errcheck
 | |
| 						cmdStatus, runErr := runServerToClientCopyAs(string(rec.Who()), string(rec.TermType()), hc, string(rec.Cmd()), chaffEnabled)
 | |
| 						if runErr != nil {
 | |
| 							logger.LogErr(fmt.Sprintf("[s->c copy error for %s@%s]\n", rec.Who(), hname)) //nolint:errcheck
 | |
| 						} else {
 | |
| 							// Returned hopefully via an EOF or exit/logout;
 | |
| 							logger.LogNotice(fmt.Sprintf("[s->c copy completed for %s@%s, status %d]\n", rec.Who(), hname, cmdStatus)) //nolint:errcheck
 | |
| 						}
 | |
| 
 | |
| 						// Clear current op so user can enter next, or EOF
 | |
| 						rec.SetOp([]byte{0})
 | |
| 						hc.SetStatus(xsnet.CSOType(cmdStatus))
 | |
| 						//fmt.Println("Waiting for EOF from other end.")
 | |
| 						//_, _ = hc.Read(nil /*ackByte*/)
 | |
| 						//fmt.Println("Got remote end ack.")
 | |
| 					} else {
 | |
| 						logger.LogErr(fmt.Sprintln("[Bad xs.Session]")) //nolint:errcheck
 | |
| 					}
 | |
| 					return
 | |
| 				}(&conn) //nolint:errcheck
 | |
| 			} // algs valid and not blacklisted
 | |
| 		} // Accept() success
 | |
| 	} //endfor
 | |
| 	//logger.LogNotice(fmt.Sprintln("[Exiting]")) //nolint:errcheck
 | |
| }
 | |
| 
 | |
| func dumpProf() {
 | |
| 	if cpuprofile != "" {
 | |
| 		pprof.StopCPUProfile()
 | |
| 	}
 | |
| 
 | |
| 	if memprofile != "" {
 | |
| 		f, err := os.Create(memprofile)
 | |
| 		if err != nil {
 | |
| 			log.Fatal("could not create memory profile: ", err)
 | |
| 		}
 | |
| 		defer f.Close()
 | |
| 		runtime.GC() // get up-to-date statistics
 | |
| 		if err := pprof.WriteHeapProfile(f); err != nil {
 | |
| 			log.Fatal("could not write memory profile: ", err) //nolint:gocritic
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	//os.Exit(status)
 | |
| }
 |