From afd3bdb315aa14b33faca1695156ea39f499385f Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 27 Jun 2022 22:39:16 -0700 Subject: [PATCH] implement install command --- main-mshell.go | 132 ++++++++++++++++++++++++++++++++---------- pkg/base/base.go | 9 +++ pkg/base/optsiter.go | 7 +++ pkg/shexec/shexec.go | 133 +++++++++++++++++++++++++++++++++++-------- 4 files changed, 229 insertions(+), 52 deletions(-) diff --git a/main-mshell.go b/main-mshell.go index d83aee3cc..db6e34e58 100644 --- a/main-mshell.go +++ b/main-mshell.go @@ -23,8 +23,6 @@ import ( "golang.org/x/sys/unix" ) -const MShellVersion = "0.1.0" - // 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. @@ -220,8 +218,14 @@ func handleSingle() { close(sender.SendCh) <-sender.DoneCh }() + if len(os.Args) >= 3 && os.Args[2] == "--version" { + initPacket := packet.MakeInitPacket() + initPacket.Version = base.MShellVersion + sender.SendPacket(initPacket) + return + } initPacket := packet.MakeInitPacket() - initPacket.Version = MShellVersion + initPacket.Version = base.MShellVersion sender.SendPacket(initPacket) var runPacket *packet.RunPacketType for pk := range packetParser.MainCh { @@ -280,37 +284,70 @@ func detectOpenFds() ([]packet.RemoteFd, error) { return fds, nil } +func parseInstallOpts() (*shexec.InstallOpts, error) { + opts := &shexec.InstallOpts{} + iter := base.MakeOptsIter(os.Args[2:]) // first arg is --install + for iter.HasNext() { + argStr := iter.Next() + found, err := tryParseSSHOpt(iter, &opts.SSHOpts) + if err != nil { + return nil, err + } + if found { + continue + } + if base.IsOption(argStr) { + return nil, fmt.Errorf("invalid option '%s' passed to mshell --install", argStr) + } + opts.ArchStr = argStr + break + } + return opts, nil +} + +func tryParseSSHOpt(iter *base.OptsIter, sshOpts *shexec.SharedSSHOpts) (bool, error) { + argStr := iter.Current() + if argStr == "--ssh" { + if !iter.IsNextPlain() { + return false, fmt.Errorf("'--ssh [user@host]' missing host") + } + sshOpts.SSHHost = iter.Next() + return true, nil + } + if argStr == "--ssh-opts" { + if !iter.HasNext() { + return false, fmt.Errorf("'--ssh-opts [options]' missing options") + } + sshOpts.SSHOptsStr = iter.Next() + return true, nil + } + if argStr == "-i" { + if !iter.IsNextPlain() { + return false, fmt.Errorf("-i [identity-file]' missing file") + } + sshOpts.SSHIdentity = iter.Next() + return true, nil + } + if argStr == "-l" { + if !iter.IsNextPlain() { + return false, fmt.Errorf("-l [user]' missing user") + } + sshOpts.SSHUser = iter.Next() + return true, nil + } + return false, nil +} + func parseClientOpts() (*shexec.ClientOpts, error) { opts := &shexec.ClientOpts{} iter := base.MakeOptsIter(os.Args[1:]) for iter.HasNext() { argStr := iter.Next() - if argStr == "--ssh" { - if !iter.IsNextPlain() { - return nil, fmt.Errorf("'--ssh [user@host]' missing host") - } - opts.SSHHost = iter.Next() - continue + found, err := tryParseSSHOpt(iter, &opts.SSHOpts) + if err != nil { + return nil, err } - if argStr == "--ssh-opts" { - if !iter.HasNext() { - return nil, fmt.Errorf("'--ssh-opts [options]' missing options") - } - opts.SSHOptsStr = iter.Next() - continue - } - if argStr == "-i" { - if !iter.IsNextPlain() { - return nil, fmt.Errorf("-i [identity-file]' missing file") - } - opts.SSHIdentity = iter.Next() - continue - } - if argStr == "-l" { - if !iter.IsNextPlain() { - return nil, fmt.Errorf("-l [user]' missing user") - } - opts.SSHUser = iter.Next() + if found { continue } if argStr == "--cwd" { @@ -392,6 +429,36 @@ func handleClient() (int, error) { return donePacket.ExitCode, nil } +func handleInstall() (int, error) { + opts, err := parseInstallOpts() + if err != nil { + return 1, fmt.Errorf("parsing opts: %w", err) + } + if opts.SSHOpts.SSHHost == "" { + return 1, fmt.Errorf("cannot install without '--ssh user@host' option") + } + fullArch := opts.ArchStr + fields := strings.SplitN(fullArch, ".", 2) + if len(fields) != 2 { + return 1, fmt.Errorf("invalid arch format '%s' passed to mshell --install", fullArch) + } + goos, goarch := fields[0], fields[1] + if !base.ValidGoArch(goos, goarch) { + return 1, fmt.Errorf("invalid arch '%s' passed to mshell --install", fullArch) + } + optName := base.GoArchOptFile(goos, goarch) + _, err = os.Stat(optName) + if err != nil { + return 1, fmt.Errorf("cannot install mshell to remote host, cannot read '%s': %w", optName, err) + } + opts.OptName = optName + err = shexec.RunInstallSSHCommand(opts) + if err != nil { + return 1, err + } + return 0, nil +} + func handleUsage() { usage := ` Client Usage: mshell [opts] --ssh user@host -- [command] @@ -444,7 +511,7 @@ func main() { handleUsage() return } else if firstArg == "--version" { - fmt.Printf("mshell v%s\n", MShellVersion) + fmt.Printf("mshell v%s\n", base.MShellVersion) return } else if firstArg == "--single" { handleSingle() @@ -452,6 +519,13 @@ func main() { } else if firstArg == "--server" { handleServer() return + } else if firstArg == "--install" { + rtnCode, err := handleInstall() + if err != nil { + fmt.Printf("[error] %v\n", err) + } + os.Exit(rtnCode) + return } else { rtnCode, err := handleClient() if err != nil { diff --git a/pkg/base/base.go b/pkg/base/base.go index 39398e014..b2736c4c5 100644 --- a/pkg/base/base.go +++ b/pkg/base/base.go @@ -30,6 +30,7 @@ const SessionsDirBaseName = ".sessions" const RunnerBaseName = "runner" const SessionDBName = "session.db" const ScReadyString = "scripthaus runner ready" +const MShellVersion = "0.1.0" const OSCEscError = "error" @@ -253,3 +254,11 @@ func ExpandHomeDir(pathStr string) string { } return path.Join(homeDir, pathStr[2:]) } + +func ValidGoArch(goos string, goarch string) bool { + return (goos == "darwin" || goos == "linux") && (goarch == "amd64" || goarch == "arm64") +} + +func GoArchOptFile(goos string, goarch string) string { + return fmt.Sprintf("/opt/mshell/bin/mshell.%s.%s", goos, goarch) +} diff --git a/pkg/base/optsiter.go b/pkg/base/optsiter.go index 923d86d24..aee544e32 100644 --- a/pkg/base/optsiter.go +++ b/pkg/base/optsiter.go @@ -41,6 +41,13 @@ func (iter *OptsIter) Next() string { return rtn } +func (iter *OptsIter) Current() string { + if iter.Pos == 0 { + return "" + } + return iter.Opts[iter.Pos-1] +} + func (iter *OptsIter) Rest() []string { return iter.Opts[iter.Pos:] } diff --git a/pkg/shexec/shexec.go b/pkg/shexec/shexec.go index 78316a2d5..814cfac49 100644 --- a/pkg/shexec/shexec.go +++ b/pkg/shexec/shexec.go @@ -32,7 +32,7 @@ const FirstExtraFilesFdNum = 3 const ClientCommand = ` PATH=$PATH:~/.mshell; -which mshell2 > /dev/null; +which mshell > /dev/null; if [[ "$?" -ne 0 ]] then printf "\n##N{\"type\": \"init\", \"notfound\": true, \"uname\": \"%s | %s\"}\n" "$(uname -s)" "$(uname -m)" @@ -41,6 +41,14 @@ else fi ` +const InstallCommand = ` +mkdir -p ~/.mshell/; +cat > ~/.mshell/mshell.temp; +mv ~/.mshell/mshell.temp ~/.mshell/mshell; +chmod a+x ~/.mshell/mshell; +~/.mshell/mshell --single --version +` + const RunCommandFmt = `%s` const RunSudoCommandFmt = `sudo -n -C %d bash /dev/fd/%d` const RunSudoPasswordCommandFmt = `cat /dev/fd/%d | sudo -k -S -C %d bash -c "echo '[from-mshell]'; exec %d>&-; bash /dev/fd/%d < /dev/fd/%d"` @@ -227,11 +235,21 @@ func RunCommand(pk *packet.RunPacketType, sender *packet.PacketSender) (*ShExecT } } +type SharedSSHOpts struct { + SSHHost string + SSHOptsStr string + SSHIdentity string + SSHUser string +} + +type InstallOpts struct { + SSHOpts SharedSSHOpts + ArchStr string + OptName string +} + type ClientOpts struct { - SSHHost string - SSHOptsStr string - SSHIdentity string - SSHUser string + SSHOpts SharedSSHOpts Command string Fds []packet.RemoteFd Cwd string @@ -244,46 +262,63 @@ type ClientOpts struct { } func (opts *ClientOpts) MakeExecCmd() *exec.Cmd { - if opts.SSHHost == "" { + if opts.SSHOpts.SSHHost == "" { ecmd := exec.Command("bash", "-c", strings.TrimSpace(ClientCommand)) return ecmd } else { var moreSSHOpts []string - if opts.SSHIdentity != "" { - identityOpt := fmt.Sprintf("-i %s", shellescape.Quote(opts.SSHIdentity)) + if opts.SSHOpts.SSHIdentity != "" { + identityOpt := fmt.Sprintf("-i %s", shellescape.Quote(opts.SSHOpts.SSHIdentity)) moreSSHOpts = append(moreSSHOpts, identityOpt) } - if opts.SSHUser != "" { - userOpt := fmt.Sprintf("-l %s", shellescape.Quote(opts.SSHUser)) + if opts.SSHOpts.SSHUser != "" { + userOpt := fmt.Sprintf("-l %s", shellescape.Quote(opts.SSHOpts.SSHUser)) moreSSHOpts = append(moreSSHOpts, userOpt) } remoteCommand := strings.TrimSpace(ClientCommand) // note that SSHOptsStr is *not* escaped - sshCmd := fmt.Sprintf("ssh %s %s %s %s", strings.Join(moreSSHOpts, " "), opts.SSHOptsStr, shellescape.Quote(opts.SSHHost), shellescape.Quote(remoteCommand)) + sshCmd := fmt.Sprintf("ssh %s %s %s %s", strings.Join(moreSSHOpts, " "), opts.SSHOpts.SSHOptsStr, shellescape.Quote(opts.SSHOpts.SSHHost), shellescape.Quote(remoteCommand)) ecmd := exec.Command("bash", "-c", sshCmd) return ecmd } } -func (opts *ClientOpts) MakeInstallCommandString(goos string, goarch string) string { +func (opts *InstallOpts) MakeExecCmd() *exec.Cmd { var moreSSHOpts []string - if opts.SSHIdentity != "" { - identityOpt := fmt.Sprintf("-i %s", shellescape.Quote(opts.SSHIdentity)) + if opts.SSHOpts.SSHIdentity != "" { + identityOpt := fmt.Sprintf("-i %s", shellescape.Quote(opts.SSHOpts.SSHIdentity)) moreSSHOpts = append(moreSSHOpts, identityOpt) } - if opts.SSHUser != "" { - userOpt := fmt.Sprintf("-l %s", shellescape.Quote(opts.SSHUser)) + if opts.SSHOpts.SSHUser != "" { + userOpt := fmt.Sprintf("-l %s", shellescape.Quote(opts.SSHOpts.SSHUser)) moreSSHOpts = append(moreSSHOpts, userOpt) } - if opts.SSHOptsStr != "" { - optsOpt := fmt.Sprintf("--ssh-opts %s", shellescape.Quote(opts.SSHOptsStr)) + // note that SSHOptsStr is *not* escaped + installCommand := strings.TrimSpace(InstallCommand) + sshCmd := fmt.Sprintf("ssh %s %s %s %s", strings.Join(moreSSHOpts, " "), opts.SSHOpts.SSHOptsStr, shellescape.Quote(opts.SSHOpts.SSHHost), shellescape.Quote(installCommand)) + ecmd := exec.Command("bash", "-c", sshCmd) + return ecmd +} + +func (opts *ClientOpts) MakeInstallCommandString(goos string, goarch string) string { + var moreSSHOpts []string + if opts.SSHOpts.SSHIdentity != "" { + identityOpt := fmt.Sprintf("-i %s", shellescape.Quote(opts.SSHOpts.SSHIdentity)) + moreSSHOpts = append(moreSSHOpts, identityOpt) + } + if opts.SSHOpts.SSHUser != "" { + userOpt := fmt.Sprintf("-l %s", shellescape.Quote(opts.SSHOpts.SSHUser)) + moreSSHOpts = append(moreSSHOpts, userOpt) + } + if opts.SSHOpts.SSHOptsStr != "" { + optsOpt := fmt.Sprintf("--ssh-opts %s", shellescape.Quote(opts.SSHOpts.SSHOptsStr)) moreSSHOpts = append(moreSSHOpts, optsOpt) } - if opts.SSHHost != "" { - sshArg := fmt.Sprintf("--ssh %s", shellescape.Quote(opts.SSHHost)) + if opts.SSHOpts.SSHHost != "" { + sshArg := fmt.Sprintf("--ssh %s", shellescape.Quote(opts.SSHOpts.SSHHost)) moreSSHOpts = append(moreSSHOpts, sshArg) } - return fmt.Sprintf("mshell --install %s %s_%s", strings.Join(moreSSHOpts, " "), goos, goarch) + return fmt.Sprintf("mshell --install %s %s.%s", strings.Join(moreSSHOpts, " "), goos, goarch) } func (opts *ClientOpts) MakeRunPacket() (*packet.RunPacketType, error) { @@ -383,6 +418,55 @@ func ValidateRemoteFds(rfds []packet.RemoteFd) error { return nil } +func RunInstallSSHCommand(opts *InstallOpts) error { + ecmd := opts.MakeExecCmd() + inputWriter, err := ecmd.StdinPipe() + if err != nil { + return fmt.Errorf("creating stdin pipe: %v", err) + } + stdoutReader, err := ecmd.StdoutPipe() + if err != nil { + return fmt.Errorf("creating stdout pipe: %v", err) + } + stderrReader, err := ecmd.StderrPipe() + if err != nil { + return fmt.Errorf("creating stderr pipe: %v", err) + } + go func() { + io.Copy(os.Stderr, stderrReader) + }() + fd, err := os.Open(opts.OptName) + if err != nil { + return fmt.Errorf("cannot open '%s': %w", opts.OptName, err) + } + go func() { + defer inputWriter.Close() + io.Copy(inputWriter, fd) + }() + packetParser := packet.MakePacketParser(stdoutReader) + err = ecmd.Start() + if err != nil { + return fmt.Errorf("running ssh command: %w", err) + } + for pk := range packetParser.MainCh { + if pk.GetType() == packet.InitPacketStr { + initPacket := pk.(*packet.InitPacketType) + if initPacket.Version == base.MShellVersion { + fmt.Printf("mshell %s, installed successfully at %s:~/.mshell/mshell\n", initPacket.Version, opts.SSHOpts.SSHHost) + return nil + } + return fmt.Errorf("invalid version '%s' received from client, expecting '%s'", initPacket.Version, base.MShellVersion) + } + if pk.GetType() == packet.RawPacketStr { + rawPk := pk.(*packet.RawPacketType) + fmt.Printf("%s\n", rawPk.Data) + continue + } + return fmt.Errorf("invalid response packet '%s' received from client", pk.GetType()) + } + return fmt.Errorf("did not receive version string from client, install not successful") +} + func RunClientSSHCommandAndWait(opts *ClientOpts) (*packet.CmdDonePacketType, error) { err := ValidateRemoteFds(opts.Fds) if err != nil { @@ -464,8 +548,8 @@ func RunClientSSHCommandAndWait(opts *ClientOpts) (*packet.CmdDonePacketType, er installCmd := opts.MakeInstallCommandString(goos, goarch) return nil, fmt.Errorf("mshell command not found on remote server, can install with '%s'", installCmd) } - if initPk.Version != "0.1.0" { - return nil, fmt.Errorf("invalid remote mshell version 'v%s', must be v0.1.0", initPk.Version) + if initPk.Version != base.MShellVersion { + return nil, fmt.Errorf("invalid remote mshell version 'v%s', must be v%s", initPk.Version, base.MShellVersion) } versionOk = true if opts.Debug { @@ -509,6 +593,9 @@ func UNameStringToGoArch(uname string) (string, string, error) { if goarch == "" { return "", "", fmt.Errorf("invalid uname machine type '%s', mshell only supports aarch64 (amd64) and x86_64 (amd64)", archVal) } + if !base.ValidGoArch(goos, goarch) { + return "", "", fmt.Errorf("invalid arch detected %s.%s", goos, goarch) + } return goos, goarch, nil }