diff --git a/go.mod b/go.mod index 02a9eb359..b6c3b3a72 100644 --- a/go.mod +++ b/go.mod @@ -10,4 +10,7 @@ require ( golang.org/x/sys v0.0.0-20220412211240-33da011f77ad ) -require github.com/Masterminds/semver/v3 v3.1.1 // indirect +require ( + github.com/Masterminds/semver/v3 v3.1.1 // indirect + golang.org/x/mod v0.5.1 // indirect +) diff --git a/go.sum b/go.sum index 32656449d..0757a9bde 100644 --- a/go.sum +++ b/go.sum @@ -8,5 +8,7 @@ github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwV github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/main-mshell.go b/main-mshell.go index 0902de23e..5010b0de0 100644 --- a/main-mshell.go +++ b/main-mshell.go @@ -417,14 +417,14 @@ func handleInstall() (int, error) { if !base.ValidGoArch(goos, goarch) { return 1, fmt.Errorf("invalid arch '%s' passed to mshell --install", fullArch) } - optName := base.GoArchOptFile(goos, goarch) + optName := base.GoArchOptFile(base.MShellVersion, 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) + err = shexec.RunInstallFromOpts(opts) if err != nil { return 1, err } diff --git a/pkg/base/base.go b/pkg/base/base.go index f8a386075..4a43a4edf 100644 --- a/pkg/base/base.go +++ b/pkg/base/base.go @@ -19,6 +19,7 @@ import ( "sync" "github.com/google/uuid" + "golang.org/x/mod/semver" ) const HomeVarName = "HOME" @@ -26,10 +27,12 @@ const DefaultMShellHome = "~/.mshell" const DefaultMShellName = "mshell" const MShellPathVarName = "MSHELL_PATH" const MShellHomeVarName = "MSHELL_HOME" +const MShellInstallBinVarName = "MSHELL_INSTALLBIN_PATH" const SSHCommandVarName = "SSH_COMMAND" const SessionsDirBaseName = "sessions" const MShellVersion = "v0.1.0" const RemoteIdFile = "remoteid" +const DefaultMShellInstallBinDir = "/opt/mshell/bin" var sessionDirCache = make(map[string]string) var baseLock = &sync.Mutex{} @@ -229,8 +232,17 @@ 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) +func GoArchOptFile(version string, goos string, goarch string) string { + installBinDir := os.Getenv(MShellInstallBinVarName) + if installBinDir == "" { + installBinDir = DefaultMShellInstallBinDir + } + versionStr := semver.MajorMinor(version) + if versionStr == "" { + versionStr = "unknown" + } + binBaseName := fmt.Sprintf("mshell-%s-%s.%s", versionStr, goos, goarch) + return fmt.Sprintf(path.Join(installBinDir, binBaseName)) } func GetRemoteId() (string, error) { diff --git a/pkg/shexec/client.go b/pkg/shexec/client.go index fadcb6442..30416ae37 100644 --- a/pkg/shexec/client.go +++ b/pkg/shexec/client.go @@ -9,6 +9,7 @@ import ( "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" + "golang.org/x/mod/semver" ) // TODO - track buffer sizes for sending input @@ -72,11 +73,11 @@ func MakeClientProc(ctx context.Context, ecmd *exec.Cmd) (*ClientProc, string, e initPk := pk.(*packet.InitPacketType) if initPk.NotFound { cproc.Close() - return nil, initPk.UName, fmt.Errorf("mshell command not found on local server") + return nil, initPk.UName, fmt.Errorf("mshell-%s command not found on local server", semver.MajorMinor(base.MShellVersion)) } - if initPk.Version != base.MShellVersion { + if semver.MajorMinor(initPk.Version) != semver.MajorMinor(base.MShellVersion) { cproc.Close() - return nil, initPk.UName, fmt.Errorf("invalid remote mshell version '%s', must be %s", initPk.Version, base.MShellVersion) + return nil, initPk.UName, fmt.Errorf("invalid remote mshell version '%s', must be '=%s'", initPk.Version, semver.MajorMinor(base.MShellVersion)) } cproc.InitPk = initPk } diff --git a/pkg/shexec/shexec.go b/pkg/shexec/shexec.go index dc52290d2..b272f3d3c 100644 --- a/pkg/shexec/shexec.go +++ b/pkg/shexec/shexec.go @@ -27,6 +27,7 @@ import ( "github.com/scripthaus-dev/mshell/pkg/cirfile" "github.com/scripthaus-dev/mshell/pkg/mpio" "github.com/scripthaus-dev/mshell/pkg/packet" + "golang.org/x/mod/semver" "golang.org/x/sys/unix" ) @@ -45,29 +46,37 @@ const MaxMaxPtySize = 100 * 1024 * 1024 const GetStateTimeout = 5 * time.Second -const ClientCommand = ` +const ClientCommandFmt = ` PATH=$PATH:~/.mshell; which mshell > /dev/null; if [[ "$?" -ne 0 ]] then printf "\n##N{\"type\": \"init\", \"notfound\": true, \"uname\": \"%s|%s\"}\n" "$(uname -s)" "$(uname -m)" else - mshell --single + mshell-[%VERSION%] --single fi ` -const InstallCommand = ` +func MakeClientCommandStr() string { + return strings.ReplaceAll(ClientCommandFmt, "[%VERSION%]", semver.MajorMinor(base.MShellVersion)) +} + +const InstallCommandFmt = ` printf "\n##N{\"type\": \"init\", \"notfound\": true, \"uname\": \"%s|%s\"}\n" "$(uname -s)" "$(uname -m)"; mkdir -p ~/.mshell/; cat > ~/.mshell/mshell.temp; if [[ -s ~/.mshell/mshell.temp ]] then - mv ~/.mshell/mshell.temp ~/.mshell/mshell; - chmod a+x ~/.mshell/mshell; - ~/.mshell/mshell --single --version + mv ~/.mshell/mshell.temp ~/.mshell/mshell-[%VERSION%]; + chmod a+x ~/.mshell/mshell-[%VERSION%]; + ~/.mshell/mshell-[%VERSION%] --single --version fi ` +func MakeInstallCommandStr() string { + return strings.ReplaceAll(InstallCommandFmt, "[%VERSION%]", semver.MajorMinor(base.MShellVersion)) +} + 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"` @@ -389,6 +398,7 @@ type InstallOpts struct { ArchStr string OptName string Detect bool + CmdPty *os.File } type ClientOpts struct { @@ -408,7 +418,8 @@ 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 + cmdStr := MakeInstallCommandStr() + return opts.MakeSSHExecCmd(cmdStr), nil } func (opts SSHOpts) MakeMShellServerCmd() (*exec.Cmd, error) { @@ -434,7 +445,8 @@ func (opts SSHOpts) MakeMShellSingleCmd(fromServer bool) (*exec.Cmd, error) { } return ecmd, nil } - return opts.MakeSSHExecCmd(ClientCommand), nil + cmdStr := MakeClientCommandStr() + return opts.MakeSSHExecCmd(cmdStr), nil } func (opts SSHOpts) MakeSSHExecCmd(remoteCommand string) *exec.Cmd { @@ -632,12 +644,7 @@ func sendOptFile(input io.WriteCloser, optName string) error { return nil } -func RunInstallSSHCommand(opts *InstallOpts) error { - tryDetect := opts.Detect - ecmd, err := opts.SSHOpts.MakeSSHInstallCmd() - if err != nil { - return err - } +func RunInstallFromCmd(ecmd *exec.Cmd, tryDetect bool, optName string, msgFn func(string)) error { inputWriter, err := ecmd.StdinPipe() if err != nil { return fmt.Errorf("creating stdin pipe: %v", err) @@ -653,8 +660,11 @@ func RunInstallSSHCommand(opts *InstallOpts) error { go func() { io.Copy(os.Stderr, stderrReader) }() - if opts.OptName != "" { - sendOptFile(inputWriter, opts.OptName) + if optName != "" { + err = sendOptFile(inputWriter, optName) + if err != nil { + return fmt.Errorf("cannot send mshell binary: %v", err) + } } packetParser := packet.MakePacketParser(stdoutReader) err = ecmd.Start() @@ -677,15 +687,19 @@ func RunInstallSSHCommand(opts *InstallOpts) error { if err != nil { return fmt.Errorf("arch cannot be detected (might be incompatible with mshell): %w", err) } - fmt.Printf("mshell detected remote architecture as '%s.%s'\n", goos, goarch) - optName := base.GoArchOptFile(goos, goarch) - sendOptFile(inputWriter, optName) + msgStr := fmt.Sprintf("mshell detected remote architecture as '%s.%s'\n", goos, goarch) + msgFn(msgStr) + optName := base.GoArchOptFile(base.MShellVersion, goos, goarch) + fmt.Printf("optname %s\n", optName) + err = sendOptFile(inputWriter, optName) + if err != nil { + return fmt.Errorf("cannot send mshell binary: %v", err) + } continue } if pk.GetType() == packet.InitPacketStr && !firstInit { 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) @@ -700,6 +714,23 @@ func RunInstallSSHCommand(opts *InstallOpts) error { return fmt.Errorf("did not receive version string from client, install not successful") } +func RunInstallFromOpts(opts *InstallOpts) error { + ecmd, err := opts.SSHOpts.MakeSSHInstallCmd() + if err != nil { + return err + } + msgFn := func(str string) { + fmt.Printf("%s", str) + } + err = RunInstallFromCmd(ecmd, opts.Detect, opts.OptName, msgFn) + if err != nil { + return err + } + mmVersion := semver.MajorMinor(base.MShellVersion) + fmt.Printf("mshell installed successfully at %s:~/.mshell/mshell%s\n", opts.SSHOpts.SSHHost, mmVersion) + return nil +} + func HasDupStdin(fds []packet.RemoteFd) bool { for _, rfd := range fds { if rfd.Read && rfd.DupStdin { @@ -764,22 +795,23 @@ func RunClientSSHCommandAndWait(runPacket *packet.RunPacketType, fdContext FdCon } if pk.GetType() == packet.InitPacketStr { initPk := pk.(*packet.InitPacketType) + mmVersion := semver.MajorMinor(base.MShellVersion) if initPk.NotFound { if sshOpts.SSHHost == "" { - return nil, fmt.Errorf("mshell command not found on local server") + return nil, fmt.Errorf("mshell-%s command not found on local server", mmVersion) } if initPk.UName == "" { - return nil, fmt.Errorf("mshell command not found on remote server, no uname detected") + return nil, fmt.Errorf("mshell-%s command not found on remote server, no uname detected", mmVersion) } goos, goarch, err := DetectGoArch(initPk.UName) if err != nil { - return nil, fmt.Errorf("mshell command not found on remote server, architecture cannot be detected (might be incompatible with mshell): %w", err) + return nil, fmt.Errorf("mshell-%s command not found on remote server, architecture cannot be detected (might be incompatible with mshell): %w", mmVersion, err) } sshOptsStr := sshOpts.MakeMShellSSHOpts() - return nil, fmt.Errorf("mshell command not found on remote server, can install with 'mshell --install %s %s.%s'", sshOptsStr, goos, goarch) + return nil, fmt.Errorf("mshell-%s command not found on remote server, can install with 'mshell --install %s %s.%s'", mmVersion, sshOptsStr, goos, goarch) } - if initPk.Version != base.MShellVersion { - return nil, fmt.Errorf("invalid remote mshell version '%s', must be %s", initPk.Version, base.MShellVersion) + if semver.MajorMinor(initPk.Version) != semver.MajorMinor(base.MShellVersion) { + return nil, fmt.Errorf("invalid remote mshell version '%s', must be '=%s'", initPk.Version, semver.MajorMinor(base.MShellVersion)) } versionOk = true if debug { diff --git a/scripthaus.md b/scripthaus.md index 6752bda67..3d7df182a 100644 --- a/scripthaus.md +++ b/scripthaus.md @@ -1,16 +1,16 @@ ```bash # @scripthaus command build -go build -ldflags="-s -w" -o /Users/mike/.mshell/mshell main-mshell.go +go build -ldflags="-s -w" -o /Users/mike/.mshell/mshell-v0.1 main-mshell.go ``` ```bash # @scripthaus command fullbuild -go build -ldflags="-s -w" -o /Users/mike/.mshell/mshell main-mshell.go -GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o /opt/mshell/bin/mshell.linux.amd64 main-mshell.go -GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o /opt/mshell/bin/mshell.linux.arm64 main-mshell.go -GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o /opt/mshell/bin/mshell.darwin.amd64 main-mshell.go -GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o /opt/mshell/bin/mshell.darwin.arm64 main-mshell.go +go build -ldflags="-s -w" -o /Users/mike/.mshell/mshell-v0.1 main-mshell.go +GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o /opt/mshell/bin/mshell-v0.1-linux.amd64 main-mshell.go +GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o /opt/mshell/bin/mshell-v0.1-linux.arm64 main-mshell.go +GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o /opt/mshell/bin/mshell-v0.1-darwin.amd64 main-mshell.go +GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o /opt/mshell/bin/mshell-v0.1-darwin.arm64 main-mshell.go ```