diff --git a/main-mshell.go b/main-mshell.go index cfb9fca98..a7487660f 100644 --- a/main-mshell.go +++ b/main-mshell.go @@ -10,10 +10,8 @@ import ( "bytes" "fmt" "os" - "os/signal" "os/user" "strings" - "syscall" "time" "github.com/scripthaus-dev/mshell/pkg/base" @@ -24,19 +22,6 @@ import ( "golang.org/x/sys/unix" ) -// in single run mode, we don't want mshell to die from signals -// since we want the single mshell to persist even if session / main mshell -// is terminated. -func setupSingleSignals(cmd *shexec.ShExecType) { - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) - go func() { - for range sigCh { - // do nothing - } - }() -} - func doSingle(ck base.CommandKey) { packetParser := packet.MakePacketParser(os.Stdin) sender := packet.MakePacketSender(os.Stdout) @@ -60,12 +45,12 @@ func doSingle(ck base.CommandKey) { sender.SendErrorPacket(fmt.Sprintf("run packet cmdid[%s] did not match arg[%s]", runPacket.CK, ck)) return } - cmd, err := shexec.RunCommand(runPacket, sender) + cmd, err := shexec.RunCommandDetached(runPacket, sender) if err != nil { sender.SendErrorPacket(fmt.Sprintf("error running command: %v", err)) return } - setupSingleSignals(cmd) + shexec.SetupSignalsForDetach() startPacket := cmd.MakeCmdStartPacket() sender.SendPacket(startPacket) donePacket := cmd.WaitForCommand() @@ -245,15 +230,30 @@ func handleSingle() { sender.SendCKErrorPacket(ck, err.Error()) return } - cmd, err := shexec.RunCommand(runPacket, sender) + err = shexec.ValidateRunPacket(runPacket) if err != nil { - sender.SendCKErrorPacket(runPacket.CK, fmt.Sprintf("error running command: %v", err)) + sender.SendCKErrorPacket(runPacket.CK, err.Error()) + return + } + if runPacket.Detached { + cmd, err := shexec.RunCommandDetached(runPacket, sender) + if err != nil { + sender.SendCKErrorPacket(runPacket.CK, err.Error()) + return + } + cmd.WaitForCommand() + } else { + cmd, err := shexec.RunCommandSimple(runPacket, sender) + if err != nil { + sender.SendCKErrorPacket(runPacket.CK, fmt.Sprintf("error running command: %v", err)) + return + } + defer cmd.Close() + startPacket := cmd.MakeCmdStartPacket() + sender.SendPacket(startPacket) + cmd.RunRemoteIOAndWait(packetParser, sender) return } - defer cmd.Close() - startPacket := cmd.MakeCmdStartPacket() - sender.SendPacket(startPacket) - cmd.RunRemoteIOAndWait(packetParser, sender) } func detectOpenFds() ([]packet.RemoteFd, error) { @@ -494,7 +494,10 @@ Sudo Options: Sudo options allow you to run the given command using "sudo". The first option only works when you can sudo without a password. Your password will be passed -securely through a high numbered fd to "sudo -S". See full documentation for more details. +securely through a high numbered fd to "sudo -S". Note that to use high numbered +file descriptors with sudo, you will need to add this line to your /etc/sudoers file: + Defaults closefrom_override +See full documentation for more details. Examples: # execute a python script remotely, with stdin still hooked up correctly diff --git a/pkg/packet/packet.go b/pkg/packet/packet.go index 17a5638e7..f49db0c47 100644 --- a/pkg/packet/packet.go +++ b/pkg/packet/packet.go @@ -548,6 +548,14 @@ func ParseJsonPacket(jsonBuf []byte) (PacketType, error) { return pk, nil } +func sanitizeBytes(buf []byte) { + for idx, b := range buf { + if b >= 127 || (b < 32 && b != 10 && b != 13) { + buf[idx] = '?' + } + } +} + func SendPacket(w io.Writer, packet PacketType) error { if packet == nil { return nil @@ -564,7 +572,9 @@ func SendPacket(w io.Writer, packet PacketType) error { if GlobalDebug { fmt.Printf("SEND> %s\n", AsString(packet)) } - _, err = w.Write(outBuf.Bytes()) + outBytes := outBuf.Bytes() + sanitizeBytes(outBytes) + _, err = w.Write(outBytes) if err != nil { return err } @@ -687,6 +697,9 @@ func MakeRunPacketBuilder() *RunPacketBuilder { func (b *RunPacketBuilder) ProcessPacket(pk PacketType) (bool, *RunPacketType) { if pk.GetType() == RunPacketStr { runPacket := pk.(*RunPacketType) + if len(runPacket.RunData) == 0 { + return true, runPacket + } b.RunMap[runPacket.CK] = runPacket return true, nil } diff --git a/pkg/server/server.go b/pkg/server/server.go index cc0c84f34..a04f5f980 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -55,6 +55,8 @@ func (c *serverFdContext) processDataPacket(pk *packet.DataPacketType) { } func (m *MServer) MakeServerFdContext(ck base.CommandKey) *serverFdContext { + m.Lock.Lock() + defer m.Lock.Unlock() rtn := &serverFdContext{ M: m, Lock: &sync.Mutex{}, @@ -62,6 +64,7 @@ func (m *MServer) MakeServerFdContext(ck base.CommandKey) *serverFdContext { CK: ck, Readers: make(map[int]*mpio.PacketReader), } + m.FdContextMap[ck] = rtn return rtn } @@ -103,16 +106,20 @@ func (c *serverFdContext) GetReader(fdNum int) io.ReadCloser { return reader } +func (m *MServer) RemoveFdContext(ck base.CommandKey) { + m.Lock.Lock() + defer m.Lock.Unlock() + delete(m.FdContextMap, ck) +} + func (m *MServer) runCommand(runPacket *packet.RunPacketType) { if err := runPacket.CK.Validate("packet"); err != nil { m.Sender.SendErrorPacket(fmt.Sprintf("server run packets require valid ck: %s", err)) return } fdContext := m.MakeServerFdContext(runPacket.CK) - m.Lock.Lock() - m.FdContextMap[runPacket.CK] = fdContext - m.Lock.Unlock() go func() { + defer m.RemoveFdContext(runPacket.CK) donePk, err := shexec.RunClientSSHCommandAndWait(runPacket, fdContext, shexec.SSHOpts{}, m, m.Debug) if donePk != nil { m.Sender.SendPacket(donePk) @@ -143,7 +150,6 @@ func RunServer() (int, error) { server.MainInput = packet.MakePacketParser(os.Stdin) server.Sender = packet.MakePacketSender(os.Stdout) defer server.Close() - defer fmt.Printf("runserver done\n") initPacket := packet.MakeInitPacket() initPacket.Version = base.MShellVersion server.Sender.SendPacket(initPacket) diff --git a/pkg/shexec/shexec.go b/pkg/shexec/shexec.go index 144f52a49..788de2cb4 100644 --- a/pkg/shexec/shexec.go +++ b/pkg/shexec/shexec.go @@ -12,6 +12,7 @@ import ( "io" "os" "os/exec" + "os/signal" "strings" "sync" "syscall" @@ -164,20 +165,59 @@ func UpdateCmdEnv(cmd *exec.Cmd, envVars map[string]string) { cmd.Env = newEnv } -func MakeExecCmd(pk *packet.RunPacketType, cmdTty *os.File) *exec.Cmd { +// returns (pr, err) +func MakeSimpleStaticWriterPipe(data []byte) (*os.File, error) { + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + go func() { + defer pw.Close() + pw.Write(data) + }() + return pr, err +} + +func MakeDetachedExecCmd(pk *packet.RunPacketType, cmdTty *os.File) (*exec.Cmd, error) { ecmd := exec.Command("bash", "-c", pk.Command) UpdateCmdEnv(ecmd, pk.Env) if pk.Cwd != "" { ecmd.Dir = base.ExpandHomeDir(pk.Cwd) } - ecmd.Stdin = cmdTty + if !HasDupStdin(pk.Fds) { + ecmd.Stdin = cmdTty + } ecmd.Stdout = cmdTty ecmd.Stderr = cmdTty ecmd.SysProcAttr = &syscall.SysProcAttr{ Setsid: true, Setctty: true, } - return ecmd + extraFiles := make([]*os.File, 0, MaxFdNum+1) + for _, rfd := range pk.Fds { + if rfd.FdNum >= len(extraFiles) { + extraFiles = extraFiles[:rfd.FdNum+1] + } + if rfd.Read && rfd.DupStdin { + extraFiles[rfd.FdNum] = cmdTty + continue + } + return nil, fmt.Errorf("invalid fd %d passed to detached command", rfd.FdNum) + } + for _, runData := range pk.RunData { + if runData.FdNum >= len(extraFiles) { + extraFiles = extraFiles[:runData.FdNum+1] + } + var err error + extraFiles[runData.FdNum], err = MakeSimpleStaticWriterPipe(runData.Data) + if err != nil { + return nil, err + } + } + if len(extraFiles) > FirstExtraFilesFdNum { + ecmd.ExtraFiles = extraFiles[FirstExtraFilesFdNum:] + } + return ecmd, nil } func MakeRunnerExec(ck base.CommandKey) (*exec.Cmd, error) { @@ -269,19 +309,6 @@ func GetWinsize(p *packet.RunPacketType) *pty.Winsize { return &pty.Winsize{Rows: uint16(rows), Cols: uint16(cols)} } -// when err is nil, the command will have already been started -func RunCommand(pk *packet.RunPacketType, sender *packet.PacketSender) (*ShExecType, error) { - err := ValidateRunPacket(pk) - if err != nil { - return nil, err - } - if !pk.Detached { - return runCommandSimple(pk, sender) - } else { - return runCommandDetached(pk, sender) - } -} - type SSHOpts struct { SSHHost string SSHOptsStr string @@ -308,6 +335,25 @@ type ClientOpts struct { Detach bool } +func (opts SSHOpts) MakeSSHInstallCmd() (*exec.Cmd, error) { + if opts.SSHHost == "" { + return nil, fmt.Errorf("no ssh host provided, can only install to a remote host") + } + return opts.MakeSSHExecCmd(InstallCommand), nil +} + +func (opts SSHOpts) MakeMShellSingleCmd() (*exec.Cmd, error) { + if opts.SSHHost == "" { + execFile, err := os.Executable() + if err != nil { + return nil, fmt.Errorf("cannot find local mshell executable: %w", err) + } + ecmd := exec.Command(execFile, "--single") + return ecmd, nil + } + return opts.MakeSSHExecCmd(ClientCommand), nil +} + func (opts SSHOpts) MakeSSHExecCmd(remoteCommand string) *exec.Cmd { remoteCommand = strings.TrimSpace(remoteCommand) if opts.SSHHost == "" { @@ -474,7 +520,10 @@ func sendOptFile(input io.WriteCloser, optName string) error { func RunInstallSSHCommand(opts *InstallOpts) error { tryDetect := opts.Detect - ecmd := opts.SSHOpts.MakeSSHExecCmd(InstallCommand) + ecmd, err := opts.SSHOpts.MakeSSHInstallCmd() + if err != nil { + return err + } inputWriter, err := ecmd.StdinPipe() if err != nil { return fmt.Errorf("creating stdin pipe: %v", err) @@ -548,7 +597,10 @@ func HasDupStdin(fds []packet.RemoteFd) bool { func RunClientSSHCommandAndWait(runPacket *packet.RunPacketType, fdContext FdContext, sshOpts SSHOpts, upr packet.UnknownPacketReporter, debug bool) (*packet.CmdDonePacketType, error) { cmd := MakeShExec(runPacket.CK, upr) - ecmd := sshOpts.MakeSSHExecCmd(ClientCommand) + ecmd, err := sshOpts.MakeMShellSingleCmd() + if err != nil { + return nil, err + } cmd.Cmd = ecmd inputWriter, err := ecmd.StdinPipe() if err != nil { @@ -646,6 +698,9 @@ func min(v1 int, v2 int) int { func SendRunPacketAndRunData(sender *packet.PacketSender, runPacket *packet.RunPacketType) { sender.SendPacket(runPacket) + if len(runPacket.RunData) == 0 { + return + } for _, runData := range runPacket.RunData { sendBuf := runData.Data for len(sendBuf) > 0 { @@ -696,7 +751,7 @@ func (cmd *ShExecType) RunRemoteIOAndWait(packetParser *packet.PacketParser, sen sender.SendPacket(donePacket) } -func runCommandSimple(pk *packet.RunPacketType, sender *packet.PacketSender) (*ShExecType, error) { +func RunCommandSimple(pk *packet.RunPacketType, sender *packet.PacketSender) (*ShExecType, error) { cmd := MakeShExec(pk.CK, nil) cmd.Cmd = exec.Command("bash", "-c", pk.Command) UpdateCmdEnv(cmd.Cmd, pk.Env) @@ -767,7 +822,19 @@ func runCommandSimple(pk *packet.RunPacketType, sender *packet.PacketSender) (*S return cmd, nil } -func runCommandDetached(pk *packet.RunPacketType, sender *packet.PacketSender) (*ShExecType, error) { +// in detached run mode, we don't want mshell to die from signals +// since we want mshell to persist even if the mshell --server is terminated +func SetupSignalsForDetach() { + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) + go func() { + for range sigCh { + // do nothing + } + }() +} + +func RunCommandDetached(pk *packet.RunPacketType, sender *packet.PacketSender) (*ShExecType, error) { fileNames, err := base.GetCommandFileNames(pk.CK) if err != nil { return nil, err @@ -788,11 +855,20 @@ func runCommandDetached(pk *packet.RunPacketType, sender *packet.PacketSender) ( cmdTty.Close() }() rtn := MakeShExec(pk.CK, nil) - ecmd := MakeExecCmd(pk, cmdTty) + ecmd, err := MakeDetachedExecCmd(pk, cmdTty) + if err != nil { + return nil, err + } + SetupSignalsForDetach() err = ecmd.Start() if err != nil { return nil, fmt.Errorf("starting command: %w", err) } + for _, fd := range ecmd.ExtraFiles { + if fd != cmdTty { + fd.Close() + } + } ptyOutFd, err := os.OpenFile(fileNames.PtyOutFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0600) if err != nil { return nil, fmt.Errorf("cannot open ptyout file '%s': %w", fileNames.PtyOutFile, err)