implement install command

This commit is contained in:
sawka 2022-06-27 22:39:16 -07:00
parent 26479f59c0
commit afd3bdb315
4 changed files with 229 additions and 52 deletions

View File

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

View File

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

View File

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

View File

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