sanitize packets to be 7-bit ascii without control chars. dont send data/dataend when no rundata present. use os.Executable to locate mshell if running locally. more work on detached mode

This commit is contained in:
sawka 2022-06-29 14:29:38 -07:00
parent 4d8841a459
commit b6711e7428
4 changed files with 148 additions and 50 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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)

View File

@ -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)