simplify argument parsing, hard code common ssh options

This commit is contained in:
sawka 2022-06-27 14:57:01 -07:00
parent 6574402691
commit dafe2b5a57
6 changed files with 101 additions and 52 deletions

1
go.mod
View File

@ -3,6 +3,7 @@ module github.com/scripthaus-dev/mshell
go 1.17
require (
github.com/alessio/shellescape v1.4.1 // indirect
github.com/creack/pty v1.1.18 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/google/uuid v1.3.0 // indirect

2
go.sum
View File

@ -1,3 +1,5 @@
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=

View File

@ -211,7 +211,7 @@ func doMain() {
}
}
func handleRemote() {
func handleSingle() {
packetParser := packet.MakePacketParser(os.Stdin)
sender := packet.MakePacketSender(os.Stdout)
defer func() {
@ -285,14 +285,35 @@ func parseClientOpts() (*shexec.ClientOpts, error) {
for iter.HasNext() {
argStr := iter.Next()
if argStr == "--ssh" {
if opts.IsSSH {
return nil, fmt.Errorf("duplicate '--ssh' option")
if !iter.IsNextPlain() {
return nil, fmt.Errorf("'--ssh [user@host]' missing host")
}
opts.IsSSH = true
break
opts.SSHHost = iter.Next()
continue
}
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()
continue
}
if argStr == "--cwd" {
if !iter.HasNext() {
if !iter.IsNextPlain() {
return nil, fmt.Errorf("'--cwd [dir]' missing directory")
}
opts.Cwd = iter.Next()
@ -316,7 +337,7 @@ func parseClientOpts() (*shexec.ClientOpts, error) {
continue
}
if argStr == "--sudo-with-passfile" {
if !iter.HasNext() {
if !iter.IsNextPlain() {
return nil, fmt.Errorf("'--sudo-with-passfile [file]', missing file")
}
opts.Sudo = true
@ -329,29 +350,12 @@ func parseClientOpts() (*shexec.ClientOpts, error) {
opts.SudoPw = string(contents)
continue
}
}
if opts.IsSSH {
// parse SSH opts
for iter.HasNext() {
argStr := iter.Next()
if argStr == "--" {
opts.SSHOptsTerm = true
break
if argStr == "--" {
if !iter.HasNext() {
return nil, fmt.Errorf("'--' should be followed by command")
}
if argStr == "-t" || argStr == "-tt" {
return nil, fmt.Errorf("mshell cannot run over ssh -t")
}
opts.SSHOpts = append(opts.SSHOpts, argStr)
}
if !opts.SSHOptsTerm {
return nil, fmt.Errorf("ssh options must be terminated with '--' followed by [command]")
}
if !iter.HasNext() {
return nil, fmt.Errorf("no command specified")
}
opts.Command = strings.Join(iter.Rest(), " ")
if strings.TrimSpace(opts.Command) == "" {
return nil, fmt.Errorf("no command or empty command specified")
opts.Command = strings.Join(iter.Rest(), " ")
break
}
}
return opts, nil
@ -365,9 +369,12 @@ func handleClient() (int, error) {
if opts.Debug {
packet.GlobalDebug = true
}
if !opts.IsSSH {
if opts.SSHHost == "" {
return 1, fmt.Errorf("when running in client mode '--ssh' option must be present")
}
if opts.Command == "" {
return 1, fmt.Errorf("no [command] specified. [command] follows '--' option (see usage)")
}
fds, err := detectOpenFds()
if err != nil {
return 1, err
@ -382,17 +389,20 @@ func handleClient() (int, error) {
func handleUsage() {
usage := `
Client Usage: mshell [mshell-opts] --ssh [ssh-opts] user@host -- [command]
Client Usage: mshell [opts] --ssh user@host -- [command]
mshell multiplexes input and output streams to a remote command over ssh.
Options:
--cwd [dir] - execute remote command in [dir]
[command] - a single argument (should be quoted)
-i [identity-file] - used to set '-i' option for ssh command
-l [user] - used to set '-l' option for ssh command
--cwd [dir] - execute remote command in [dir]
--ssh-opts [opts] - addition options to pass to ssh command
[command] - the remote command to execute
Sudo Options:
--sudo
--sudo-with-password [pw] (not recommended, use --sudo-with-passfile if possible)
--sudo - use only if sudo never requires a password
--sudo-with-password [pw] - not recommended, use --sudo-with-passfile if possible
--sudo-with-passfile [file]
Sudo options allow you to run the given command using "sudo". The first
@ -401,7 +411,7 @@ securely through a high numbered fd to "sudo -S". See full documentation for mo
Examples:
# execute a python script remotely, with stdin still hooked up correctly
mshell --cwd "~/work" --ssh -i key.pem ubuntu@somehost -- "python3 /dev/fd/4" 4< myscript.py
mshell --cwd "~/work" -i key.pem --ssh ubuntu@somehost -- "python3 /dev/fd/4" 4< myscript.py
# capture multiple outputs
mshell --ssh ubuntu@test -- "cat file1.txt > /dev/fd/3; cat file2.txt > /dev/fd/4" 3> file1.txt 4> file2.txt
@ -431,8 +441,8 @@ func main() {
} else if firstArg == "--version" {
fmt.Printf("mshell v%s\n", MShellVersion)
return
} else if firstArg == "--remote" {
handleRemote()
} else if firstArg == "--single" {
handleSingle()
return
} else if firstArg == "--server" {
handleServer()

View File

@ -25,6 +25,13 @@ func (iter *OptsIter) HasNext() bool {
return iter.Pos <= len(iter.Opts)-1
}
func (iter *OptsIter) IsNextPlain() bool {
if !iter.HasNext() {
return false
}
return !IsOption(iter.Opts[iter.Pos])
}
func (iter *OptsIter) Next() string {
if iter.Pos >= len(iter.Opts) {
return ""

View File

@ -340,6 +340,7 @@ type InitPacketType struct {
HomeDir string `json:"homedir,omitempty"`
Env []string `json:"env,omitempty"`
User string `json:"user,omitempty"`
NotFound bool `json:"notfound,omitempty"`
}
func (*InitPacketType) GetType() string {

View File

@ -16,6 +16,7 @@ import (
"syscall"
"time"
"github.com/alessio/shellescape"
"github.com/creack/pty"
"github.com/scripthaus-dev/mshell/pkg/base"
"github.com/scripthaus-dev/mshell/pkg/mpio"
@ -29,11 +30,20 @@ const MaxCols = 1024
const MaxFdNum = 1023
const FirstExtraFilesFdNum = 3
const SSHRemoteCommand = `PATH=$PATH:~/.mshell; mshell --remote`
const SSHRemoteCommand = `
PATH=$PATH:~/.mshell;
which mshell > /dev/null;
if [[ "$?" -ne 0 ]]
then
printf "\n##34{\"type\": \"init\", \"notfound\": true}\n"
else
mshell --single
fi
`
const RemoteCommandFmt = `%s`
const RemoteSudoCommandFmt = `sudo -C %d bash /dev/fd/%d`
const RemoteSudoPasswordCommandFmt = `cat /dev/fd/%d | sudo -S -C %d bash -c "echo '[from-mshell]'; bash /dev/fd/%d < /dev/fd/%d"`
const RunCommandFmt = `%s`
const RunSudoCommandFmt = `sudo -C %d bash /dev/fd/%d`
const RunSudoPasswordCommandFmt = `cat /dev/fd/%d | sudo -S -C %d bash -c "echo '[from-mshell]'; bash /dev/fd/%d < /dev/fd/%d"`
type ShExecType struct {
Lock *sync.Mutex
@ -205,9 +215,10 @@ func RunCommand(pk *packet.RunPacketType, sender *packet.PacketSender) (*ShExecT
}
type ClientOpts struct {
IsSSH bool
SSHOptsTerm bool
SSHOpts []string
SSHHost string
SSHOptsStr string
SSHIdentity string
SSHUser string
Command string
Fds []packet.RemoteFd
Cwd string
@ -218,13 +229,29 @@ type ClientOpts struct {
CommandStdinFdNum int
}
func (opts *ClientOpts) MakeSSHCommandString() string {
var moreSSHOpts []string
if opts.SSHIdentity != "" {
identityOpt := fmt.Sprintf("-i %s", shellescape.Quote(opts.SSHIdentity))
moreSSHOpts = append(moreSSHOpts, identityOpt)
}
if opts.SSHUser != "" {
userOpt := fmt.Sprintf("-l %s", shellescape.Quote(opts.SSHUser))
moreSSHOpts = append(moreSSHOpts, userOpt)
}
remoteCommand := strings.TrimSpace(SSHRemoteCommand)
// 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))
return sshCmd
}
func (opts *ClientOpts) MakeRunPacket() (*packet.RunPacketType, error) {
runPacket := packet.MakeRunPacket()
runPacket.Cwd = opts.Cwd
runPacket.Fds = opts.Fds
if !opts.Sudo {
// normal, non-sudo command
runPacket.Command = opts.Command
runPacket.Command = fmt.Sprintf(RunCommandFmt, opts.Command)
return runPacket, nil
}
if opts.SudoWithPass {
@ -248,7 +275,7 @@ func (opts *ClientOpts) MakeRunPacket() (*packet.RunPacketType, error) {
opts.Fds = append(opts.Fds, commandStdinRfd)
opts.CommandStdinFdNum = commandStdinFdNum
maxFdNum := opts.MaxFdNum()
runPacket.Command = fmt.Sprintf(RemoteSudoPasswordCommandFmt, pwFdNum, maxFdNum+1, commandFdNum, commandStdinFdNum)
runPacket.Command = fmt.Sprintf(RunSudoPasswordCommandFmt, pwFdNum, maxFdNum+1, commandFdNum, commandStdinFdNum)
runPacket.Fds = opts.Fds
return runPacket, nil
} else {
@ -259,7 +286,7 @@ func (opts *ClientOpts) MakeRunPacket() (*packet.RunPacketType, error) {
rfd := packet.RemoteFd{FdNum: commandFdNum, Read: true, Content: opts.Command}
opts.Fds = append(opts.Fds, rfd)
maxFdNum := opts.MaxFdNum()
runPacket.Command = fmt.Sprintf(RemoteSudoCommandFmt, maxFdNum+1, commandFdNum)
runPacket.Command = fmt.Sprintf(RunSudoCommandFmt, maxFdNum+1, commandFdNum)
runPacket.Fds = opts.Fds
return runPacket, nil
}
@ -324,10 +351,8 @@ func RunClientSSHCommandAndWait(opts *ClientOpts) (*packet.CmdDonePacketType, er
return nil, err
}
cmd := MakeShExec("")
var fullSshOpts []string
fullSshOpts = append(fullSshOpts, opts.SSHOpts...)
fullSshOpts = append(fullSshOpts, SSHRemoteCommand)
ecmd := exec.Command("ssh", fullSshOpts...)
sshCmdStr := opts.MakeSSHCommandString()
ecmd := exec.Command("bash", "-c", sshCmdStr)
cmd.Cmd = ecmd
inputWriter, err := ecmd.StdinPipe()
if err != nil {
@ -386,6 +411,9 @@ func RunClientSSHCommandAndWait(opts *ClientOpts) (*packet.CmdDonePacketType, er
}
if pk.GetType() == packet.InitPacketStr {
initPk := pk.(*packet.InitPacketType)
if initPk.NotFound {
return nil, fmt.Errorf("mshell command not found on remote server, can install with 'mshell --install'")
}
if initPk.Version != "0.1.0" {
return nil, fmt.Errorf("invalid remote mshell version 'v%s', must be v0.1.0", initPk.Version)
}