2022-06-10 09:35:24 +02:00
|
|
|
// Copyright 2022 Dashborg Inc
|
|
|
|
//
|
|
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
|
|
|
|
package shexec
|
|
|
|
|
|
|
|
import (
|
2022-07-07 03:59:46 +02:00
|
|
|
"context"
|
2022-06-29 06:57:30 +02:00
|
|
|
"encoding/base64"
|
2022-06-10 09:35:24 +02:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
2022-06-29 23:29:38 +02:00
|
|
|
"os/signal"
|
2022-07-02 02:37:37 +02:00
|
|
|
"os/user"
|
2022-06-10 09:35:24 +02:00
|
|
|
"strings"
|
2022-06-23 21:48:45 +02:00
|
|
|
"sync"
|
2022-06-10 09:35:24 +02:00
|
|
|
"syscall"
|
|
|
|
"time"
|
|
|
|
|
2022-06-27 23:57:01 +02:00
|
|
|
"github.com/alessio/shellescape"
|
2022-06-10 09:35:24 +02:00
|
|
|
"github.com/creack/pty"
|
2022-06-23 19:16:54 +02:00
|
|
|
"github.com/scripthaus-dev/mshell/pkg/base"
|
2022-08-20 00:28:32 +02:00
|
|
|
"github.com/scripthaus-dev/mshell/pkg/cirfile"
|
2022-06-24 19:24:02 +02:00
|
|
|
"github.com/scripthaus-dev/mshell/pkg/mpio"
|
2022-06-23 19:16:54 +02:00
|
|
|
"github.com/scripthaus-dev/mshell/pkg/packet"
|
2022-07-02 02:37:37 +02:00
|
|
|
"golang.org/x/sys/unix"
|
2022-06-10 09:35:24 +02:00
|
|
|
)
|
|
|
|
|
2022-06-21 02:51:28 +02:00
|
|
|
const DefaultRows = 25
|
|
|
|
const DefaultCols = 80
|
|
|
|
const MaxRows = 1024
|
|
|
|
const MaxCols = 1024
|
2022-06-24 03:23:30 +02:00
|
|
|
const MaxFdNum = 1023
|
|
|
|
const FirstExtraFilesFdNum = 3
|
2022-07-06 23:06:58 +02:00
|
|
|
const DefaultTermType = "xterm-256color"
|
2022-08-20 00:28:32 +02:00
|
|
|
const DefaultMaxPtySize = 1024 * 1024
|
2022-06-21 02:51:28 +02:00
|
|
|
|
2022-06-28 00:10:17 +02:00
|
|
|
const ClientCommand = `
|
2022-06-27 23:57:01 +02:00
|
|
|
PATH=$PATH:~/.mshell;
|
2022-06-28 07:39:16 +02:00
|
|
|
which mshell > /dev/null;
|
2022-06-27 23:57:01 +02:00
|
|
|
if [[ "$?" -ne 0 ]]
|
|
|
|
then
|
2022-06-28 03:42:56 +02:00
|
|
|
printf "\n##N{\"type\": \"init\", \"notfound\": true, \"uname\": \"%s | %s\"}\n" "$(uname -s)" "$(uname -m)"
|
2022-06-27 23:57:01 +02:00
|
|
|
else
|
|
|
|
mshell --single
|
|
|
|
fi
|
|
|
|
`
|
2022-06-26 10:41:58 +02:00
|
|
|
|
2022-06-28 07:39:16 +02:00
|
|
|
const InstallCommand = `
|
2022-06-28 08:14:53 +02:00
|
|
|
printf "\n##N{\"type\": \"init\", \"notfound\": true, \"uname\": \"%s | %s\"}\n" "$(uname -s)" "$(uname -m)";
|
2022-06-28 07:39:16 +02:00
|
|
|
mkdir -p ~/.mshell/;
|
|
|
|
cat > ~/.mshell/mshell.temp;
|
|
|
|
mv ~/.mshell/mshell.temp ~/.mshell/mshell;
|
|
|
|
chmod a+x ~/.mshell/mshell;
|
|
|
|
~/.mshell/mshell --single --version
|
|
|
|
`
|
|
|
|
|
2022-06-27 23:57:01 +02:00
|
|
|
const RunCommandFmt = `%s`
|
2022-06-28 00:59:14 +02:00
|
|
|
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"`
|
2022-06-26 10:41:58 +02:00
|
|
|
|
2022-06-10 09:35:24 +02:00
|
|
|
type ShExecType struct {
|
2022-07-02 02:37:37 +02:00
|
|
|
Lock *sync.Mutex
|
|
|
|
StartTs time.Time
|
|
|
|
CK base.CommandKey
|
|
|
|
FileNames *base.CommandFileNames
|
|
|
|
Cmd *exec.Cmd
|
|
|
|
CmdPty *os.File
|
2022-08-20 00:28:32 +02:00
|
|
|
MaxPtySize int64
|
2022-07-02 02:37:37 +02:00
|
|
|
Multiplexer *mpio.Multiplexer
|
|
|
|
Detached bool
|
|
|
|
DetachedOutput *packet.PacketSender
|
|
|
|
RunnerOutFd *os.File
|
2022-06-23 21:48:45 +02:00
|
|
|
}
|
|
|
|
|
2022-06-29 02:20:01 +02:00
|
|
|
type StdContext struct{}
|
|
|
|
|
|
|
|
func (StdContext) GetWriter(fdNum int) io.WriteCloser {
|
|
|
|
if fdNum == 0 {
|
|
|
|
return os.Stdin
|
|
|
|
}
|
|
|
|
if fdNum == 1 {
|
|
|
|
return os.Stdout
|
|
|
|
}
|
|
|
|
if fdNum == 2 {
|
|
|
|
return os.Stderr
|
|
|
|
}
|
|
|
|
fd := os.NewFile(uintptr(fdNum), fmt.Sprintf("/dev/fd/%d", fdNum))
|
|
|
|
return fd
|
|
|
|
}
|
|
|
|
|
|
|
|
func (StdContext) GetReader(fdNum int) io.ReadCloser {
|
|
|
|
if fdNum == 0 {
|
|
|
|
return os.Stdin
|
|
|
|
}
|
|
|
|
if fdNum == 1 {
|
|
|
|
return os.Stdout
|
|
|
|
}
|
|
|
|
if fdNum == 2 {
|
|
|
|
return os.Stdout
|
|
|
|
}
|
|
|
|
fd := os.NewFile(uintptr(fdNum), fmt.Sprintf("/dev/fd/%d", fdNum))
|
|
|
|
return fd
|
|
|
|
}
|
|
|
|
|
|
|
|
type FdContext interface {
|
|
|
|
GetWriter(fdNum int) io.WriteCloser
|
|
|
|
GetReader(fdNum int) io.ReadCloser
|
|
|
|
}
|
|
|
|
|
2022-06-29 04:01:33 +02:00
|
|
|
func MakeShExec(ck base.CommandKey, upr packet.UnknownPacketReporter) *ShExecType {
|
2022-06-23 21:48:45 +02:00
|
|
|
return &ShExecType{
|
2022-06-24 19:24:02 +02:00
|
|
|
Lock: &sync.Mutex{},
|
|
|
|
StartTs: time.Now(),
|
2022-06-27 21:03:47 +02:00
|
|
|
CK: ck,
|
2022-06-29 04:01:33 +02:00
|
|
|
Multiplexer: mpio.MakeMultiplexer(ck, upr),
|
2022-06-23 21:48:45 +02:00
|
|
|
}
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *ShExecType) Close() {
|
2022-06-23 21:48:45 +02:00
|
|
|
if c.CmdPty != nil {
|
|
|
|
c.CmdPty.Close()
|
|
|
|
}
|
2022-06-24 19:24:02 +02:00
|
|
|
c.Multiplexer.Close()
|
2022-07-02 02:37:37 +02:00
|
|
|
if c.DetachedOutput != nil {
|
|
|
|
c.DetachedOutput.Close()
|
|
|
|
c.DetachedOutput.WaitForDone()
|
|
|
|
}
|
|
|
|
if c.RunnerOutFd != nil {
|
|
|
|
c.RunnerOutFd.Close()
|
|
|
|
}
|
2022-06-23 21:48:45 +02:00
|
|
|
}
|
|
|
|
|
2022-07-06 08:14:14 +02:00
|
|
|
func (c *ShExecType) MakeCmdStartPacket(reqId string) *packet.CmdStartPacketType {
|
|
|
|
startPacket := packet.MakeCmdStartPacket(reqId)
|
2022-06-23 21:48:45 +02:00
|
|
|
startPacket.Ts = time.Now().UnixMilli()
|
2022-06-27 21:03:47 +02:00
|
|
|
startPacket.CK = c.CK
|
2022-06-23 21:48:45 +02:00
|
|
|
startPacket.Pid = c.Cmd.Process.Pid
|
|
|
|
startPacket.MShellPid = os.Getpid()
|
|
|
|
return startPacket
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func getEnvStrKey(envStr string) string {
|
|
|
|
eqIdx := strings.Index(envStr, "=")
|
|
|
|
if eqIdx == -1 {
|
|
|
|
return envStr
|
|
|
|
}
|
|
|
|
return envStr[0:eqIdx]
|
|
|
|
}
|
|
|
|
|
|
|
|
func UpdateCmdEnv(cmd *exec.Cmd, envVars map[string]string) {
|
|
|
|
if len(envVars) == 0 {
|
|
|
|
return
|
|
|
|
}
|
2022-07-06 21:16:37 +02:00
|
|
|
if cmd.Env == nil {
|
2022-06-10 09:35:24 +02:00
|
|
|
cmd.Env = os.Environ()
|
|
|
|
}
|
|
|
|
found := make(map[string]bool)
|
|
|
|
var newEnv []string
|
|
|
|
for _, envStr := range cmd.Env {
|
|
|
|
envKey := getEnvStrKey(envStr)
|
|
|
|
newEnvVal, ok := envVars[envKey]
|
|
|
|
if ok {
|
|
|
|
if newEnvVal == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
newEnv = append(newEnv, envKey+"="+newEnvVal)
|
|
|
|
found[envKey] = true
|
|
|
|
} else {
|
|
|
|
newEnv = append(newEnv, envStr)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for envKey, envVal := range envVars {
|
|
|
|
if found[envKey] {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
newEnv = append(newEnv, envKey+"="+envVal)
|
|
|
|
}
|
|
|
|
cmd.Env = newEnv
|
|
|
|
}
|
|
|
|
|
2022-06-29 23:29:38 +02:00
|
|
|
// 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) {
|
2022-06-10 09:35:24 +02:00
|
|
|
ecmd := exec.Command("bash", "-c", pk.Command)
|
|
|
|
UpdateCmdEnv(ecmd, pk.Env)
|
2022-07-06 23:06:58 +02:00
|
|
|
UpdateCmdEnv(ecmd, map[string]string{"TERM": getTermType(pk)})
|
2022-06-23 19:16:54 +02:00
|
|
|
if pk.Cwd != "" {
|
2022-06-25 08:42:00 +02:00
|
|
|
ecmd.Dir = base.ExpandHomeDir(pk.Cwd)
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
2022-07-06 20:21:15 +02:00
|
|
|
if HasDupStdin(pk.Fds) {
|
|
|
|
return nil, fmt.Errorf("cannot detach command with dup stdin")
|
2022-06-29 23:29:38 +02:00
|
|
|
}
|
2022-07-06 20:21:15 +02:00
|
|
|
ecmd.Stdin = cmdTty
|
2022-06-10 09:35:24 +02:00
|
|
|
ecmd.Stdout = cmdTty
|
|
|
|
ecmd.Stderr = cmdTty
|
|
|
|
ecmd.SysProcAttr = &syscall.SysProcAttr{
|
|
|
|
Setsid: true,
|
|
|
|
Setctty: true,
|
|
|
|
}
|
2022-06-29 23:29:38 +02:00
|
|
|
extraFiles := make([]*os.File, 0, MaxFdNum+1)
|
2022-07-06 20:21:15 +02:00
|
|
|
if len(pk.Fds) > 0 {
|
|
|
|
return nil, fmt.Errorf("invalid fd %d passed to detached command", pk.Fds[0].FdNum)
|
2022-06-29 23:29:38 +02:00
|
|
|
}
|
|
|
|
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
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
|
|
|
|
2022-06-27 21:03:47 +02:00
|
|
|
func MakeRunnerExec(ck base.CommandKey) (*exec.Cmd, error) {
|
2022-06-23 21:48:45 +02:00
|
|
|
msPath, err := base.GetMShellPath()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-06-27 21:03:47 +02:00
|
|
|
ecmd := exec.Command(msPath, string(ck))
|
2022-06-11 06:37:21 +02:00
|
|
|
return ecmd, nil
|
|
|
|
}
|
|
|
|
|
2022-06-10 09:35:24 +02:00
|
|
|
// this will never return (unless there is an error creating/opening the file), as fifoFile will never EOF
|
|
|
|
func MakeAndCopyStdinFifo(dst *os.File, fifoName string) error {
|
|
|
|
os.Remove(fifoName)
|
|
|
|
err := syscall.Mkfifo(fifoName, 0600) // only read/write from user for security
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("cannot make stdin-fifo '%s': %v", fifoName, err)
|
|
|
|
}
|
|
|
|
// rw is non-blocking, will keep the fifo "open" for the blocking reader
|
|
|
|
rwfd, err := os.OpenFile(fifoName, os.O_RDWR, 0600)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("cannot open stdin-fifo(1) '%s': %v", fifoName, err)
|
|
|
|
}
|
|
|
|
defer rwfd.Close()
|
|
|
|
fifoReader, err := os.Open(fifoName) // blocking open/reads (open won't block because of rwfd)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("cannot open stdin-fifo(2) '%s': %w", fifoName, err)
|
|
|
|
}
|
|
|
|
defer fifoReader.Close()
|
|
|
|
io.Copy(dst, fifoReader)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func ValidateRunPacket(pk *packet.RunPacketType) error {
|
|
|
|
if pk.Type != packet.RunPacketStr {
|
|
|
|
return fmt.Errorf("run packet has wrong type: %s", pk.Type)
|
|
|
|
}
|
2022-06-23 21:48:45 +02:00
|
|
|
if pk.Detached {
|
2022-06-27 21:03:47 +02:00
|
|
|
err := pk.CK.Validate("run packet")
|
2022-06-23 21:48:45 +02:00
|
|
|
if err != nil {
|
2022-06-27 21:03:47 +02:00
|
|
|
return err
|
2022-06-23 21:48:45 +02:00
|
|
|
}
|
2022-06-28 03:42:56 +02:00
|
|
|
for _, rfd := range pk.Fds {
|
|
|
|
if rfd.Write {
|
|
|
|
return fmt.Errorf("cannot detach command with writable remote files fd=%d", rfd.FdNum)
|
|
|
|
}
|
2022-07-06 20:21:15 +02:00
|
|
|
if rfd.Read && rfd.DupStdin {
|
|
|
|
return fmt.Errorf("cannot detach command with dup stdin fd=%d", rfd.FdNum)
|
|
|
|
}
|
|
|
|
if rfd.Read {
|
2022-06-29 06:57:30 +02:00
|
|
|
return fmt.Errorf("cannot detach command with readable remote files fd=%d", rfd.FdNum)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
totalRunData := 0
|
|
|
|
for _, rd := range pk.RunData {
|
|
|
|
if rd.DataLen > mpio.ReadBufSize {
|
|
|
|
return fmt.Errorf("cannot detach command, constant rundata input too large fd=%d, len=%d, max=%d", rd.FdNum, rd.DataLen, mpio.ReadBufSize)
|
2022-06-28 03:42:56 +02:00
|
|
|
}
|
2022-06-29 06:57:30 +02:00
|
|
|
totalRunData += rd.DataLen
|
|
|
|
}
|
|
|
|
if totalRunData > mpio.MaxTotalRunDataSize {
|
|
|
|
return fmt.Errorf("cannot detach command, constant rundata input too large len=%d, max=%d", totalRunData, mpio.MaxTotalRunDataSize)
|
2022-06-28 03:42:56 +02:00
|
|
|
}
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
2022-06-23 19:16:54 +02:00
|
|
|
if pk.Cwd != "" {
|
2022-06-25 08:42:00 +02:00
|
|
|
realCwd := base.ExpandHomeDir(pk.Cwd)
|
|
|
|
dirInfo, err := os.Stat(realCwd)
|
2022-06-10 09:35:24 +02:00
|
|
|
if err != nil {
|
2022-06-25 08:42:00 +02:00
|
|
|
return fmt.Errorf("invalid cwd '%s' for command: %v", realCwd, err)
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
|
|
|
if !dirInfo.IsDir() {
|
2022-06-25 08:42:00 +02:00
|
|
|
return fmt.Errorf("invalid cwd '%s' for command, not a directory", realCwd)
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
|
|
|
}
|
2022-06-29 06:57:30 +02:00
|
|
|
for _, runData := range pk.RunData {
|
|
|
|
if runData.DataLen != len(runData.Data) {
|
|
|
|
return fmt.Errorf("rundata length mismatch, fd=%d, datalen=%d, expected=%d", runData.FdNum, len(runData.Data), runData.DataLen)
|
|
|
|
}
|
|
|
|
}
|
2022-07-06 20:21:15 +02:00
|
|
|
if pk.UsePty && HasDupStdin(pk.Fds) {
|
|
|
|
return fmt.Errorf("cannot use pty with command that has dup stdin")
|
|
|
|
}
|
2022-06-10 09:35:24 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-06-21 02:51:28 +02:00
|
|
|
func GetWinsize(p *packet.RunPacketType) *pty.Winsize {
|
|
|
|
rows := DefaultRows
|
|
|
|
cols := DefaultCols
|
2022-07-06 23:06:58 +02:00
|
|
|
if p.TermOpts != nil {
|
|
|
|
if p.TermOpts.Rows > 0 && p.TermOpts.Rows <= MaxRows {
|
|
|
|
rows = p.TermOpts.Rows
|
2022-07-02 02:37:37 +02:00
|
|
|
}
|
2022-07-06 23:06:58 +02:00
|
|
|
if p.TermOpts.Cols > 0 && p.TermOpts.Cols <= MaxCols {
|
|
|
|
cols = p.TermOpts.Cols
|
2022-07-02 02:37:37 +02:00
|
|
|
}
|
2022-06-21 02:51:28 +02:00
|
|
|
}
|
|
|
|
return &pty.Winsize{Rows: uint16(rows), Cols: uint16(cols)}
|
|
|
|
}
|
|
|
|
|
2022-06-29 00:04:08 +02:00
|
|
|
type SSHOpts struct {
|
2022-06-29 02:20:01 +02:00
|
|
|
SSHHost string
|
|
|
|
SSHOptsStr string
|
|
|
|
SSHIdentity string
|
|
|
|
SSHUser string
|
2022-06-28 07:39:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type InstallOpts struct {
|
2022-06-29 00:04:08 +02:00
|
|
|
SSHOpts SSHOpts
|
2022-06-28 07:39:16 +02:00
|
|
|
ArchStr string
|
|
|
|
OptName string
|
2022-06-28 08:14:53 +02:00
|
|
|
Detect bool
|
2022-06-28 07:39:16 +02:00
|
|
|
}
|
|
|
|
|
2022-06-24 22:25:09 +02:00
|
|
|
type ClientOpts struct {
|
2022-06-29 06:57:30 +02:00
|
|
|
SSHOpts SSHOpts
|
|
|
|
Command string
|
|
|
|
Fds []packet.RemoteFd
|
|
|
|
Cwd string
|
|
|
|
Debug bool
|
|
|
|
Sudo bool
|
|
|
|
SudoWithPass bool
|
|
|
|
SudoPw string
|
|
|
|
Detach bool
|
2022-07-06 20:21:15 +02:00
|
|
|
UsePty bool
|
2022-06-24 22:25:09 +02:00
|
|
|
}
|
|
|
|
|
2022-06-29 23:29:38 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-07-07 03:59:46 +02:00
|
|
|
func (opts SSHOpts) MakeMShellServerCmd() (*exec.Cmd, error) {
|
|
|
|
msPath, err := base.GetMShellPath()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
ecmd := exec.Command(msPath, "--server")
|
|
|
|
return ecmd, nil
|
|
|
|
}
|
|
|
|
|
2022-06-29 23:29:38 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-06-29 00:04:08 +02:00
|
|
|
func (opts SSHOpts) MakeSSHExecCmd(remoteCommand string) *exec.Cmd {
|
|
|
|
remoteCommand = strings.TrimSpace(remoteCommand)
|
|
|
|
if opts.SSHHost == "" {
|
|
|
|
ecmd := exec.Command("bash", "-c", remoteCommand)
|
2022-06-28 00:10:17 +02:00
|
|
|
return ecmd
|
|
|
|
} else {
|
|
|
|
var moreSSHOpts []string
|
2022-06-29 00:04:08 +02:00
|
|
|
if opts.SSHIdentity != "" {
|
|
|
|
identityOpt := fmt.Sprintf("-i %s", shellescape.Quote(opts.SSHIdentity))
|
2022-06-28 00:10:17 +02:00
|
|
|
moreSSHOpts = append(moreSSHOpts, identityOpt)
|
|
|
|
}
|
2022-06-29 00:04:08 +02:00
|
|
|
if opts.SSHUser != "" {
|
|
|
|
userOpt := fmt.Sprintf("-l %s", shellescape.Quote(opts.SSHUser))
|
2022-06-28 00:10:17 +02:00
|
|
|
moreSSHOpts = append(moreSSHOpts, userOpt)
|
|
|
|
}
|
|
|
|
// note that SSHOptsStr is *not* escaped
|
2022-06-29 00:04:08 +02:00
|
|
|
sshCmd := fmt.Sprintf("ssh %s %s %s %s", strings.Join(moreSSHOpts, " "), opts.SSHOptsStr, shellescape.Quote(opts.SSHHost), shellescape.Quote(remoteCommand))
|
2022-06-28 00:10:17 +02:00
|
|
|
ecmd := exec.Command("bash", "-c", sshCmd)
|
|
|
|
return ecmd
|
|
|
|
}
|
2022-06-27 23:57:01 +02:00
|
|
|
}
|
|
|
|
|
2022-06-29 00:04:08 +02:00
|
|
|
func (opts SSHOpts) MakeMShellSSHOpts() string {
|
2022-06-28 07:39:16 +02:00
|
|
|
var moreSSHOpts []string
|
2022-06-29 00:04:08 +02:00
|
|
|
if opts.SSHIdentity != "" {
|
|
|
|
identityOpt := fmt.Sprintf("-i %s", shellescape.Quote(opts.SSHIdentity))
|
2022-06-28 07:39:16 +02:00
|
|
|
moreSSHOpts = append(moreSSHOpts, identityOpt)
|
|
|
|
}
|
2022-06-29 00:04:08 +02:00
|
|
|
if opts.SSHUser != "" {
|
|
|
|
userOpt := fmt.Sprintf("-l %s", shellescape.Quote(opts.SSHUser))
|
2022-06-28 07:39:16 +02:00
|
|
|
moreSSHOpts = append(moreSSHOpts, userOpt)
|
|
|
|
}
|
2022-06-29 00:04:08 +02:00
|
|
|
if opts.SSHOptsStr != "" {
|
|
|
|
optsOpt := fmt.Sprintf("--ssh-opts %s", shellescape.Quote(opts.SSHOptsStr))
|
2022-06-28 03:42:56 +02:00
|
|
|
moreSSHOpts = append(moreSSHOpts, optsOpt)
|
|
|
|
}
|
2022-06-29 00:04:08 +02:00
|
|
|
if opts.SSHHost != "" {
|
|
|
|
sshArg := fmt.Sprintf("--ssh %s", shellescape.Quote(opts.SSHHost))
|
2022-06-28 03:42:56 +02:00
|
|
|
moreSSHOpts = append(moreSSHOpts, sshArg)
|
|
|
|
}
|
2022-06-29 00:04:08 +02:00
|
|
|
return strings.Join(moreSSHOpts, " ")
|
2022-06-28 03:42:56 +02:00
|
|
|
}
|
|
|
|
|
2022-07-06 23:06:58 +02:00
|
|
|
func GetTerminalSize() (int, int, error) {
|
|
|
|
fd, err := os.Open("/dev/tty")
|
|
|
|
if err != nil {
|
|
|
|
return 0, 0, err
|
|
|
|
}
|
|
|
|
defer fd.Close()
|
|
|
|
return pty.Getsize(fd)
|
|
|
|
}
|
|
|
|
|
2022-06-26 10:41:58 +02:00
|
|
|
func (opts *ClientOpts) MakeRunPacket() (*packet.RunPacketType, error) {
|
2022-06-24 22:25:09 +02:00
|
|
|
runPacket := packet.MakeRunPacket()
|
2022-06-28 03:42:56 +02:00
|
|
|
runPacket.Detached = opts.Detach
|
2022-06-24 22:25:09 +02:00
|
|
|
runPacket.Cwd = opts.Cwd
|
|
|
|
runPacket.Fds = opts.Fds
|
2022-07-06 23:06:58 +02:00
|
|
|
if opts.UsePty {
|
|
|
|
runPacket.UsePty = true
|
|
|
|
runPacket.TermOpts = &packet.TermOpts{}
|
|
|
|
rows, cols, err := GetTerminalSize()
|
|
|
|
if err == nil {
|
|
|
|
runPacket.TermOpts.Rows = rows
|
|
|
|
runPacket.TermOpts.Cols = cols
|
|
|
|
}
|
|
|
|
term := os.Getenv("TERM")
|
|
|
|
if term != "" {
|
|
|
|
runPacket.TermOpts.Term = term
|
|
|
|
}
|
|
|
|
}
|
2022-06-26 10:41:58 +02:00
|
|
|
if !opts.Sudo {
|
|
|
|
// normal, non-sudo command
|
2022-06-27 23:57:01 +02:00
|
|
|
runPacket.Command = fmt.Sprintf(RunCommandFmt, opts.Command)
|
2022-06-26 10:41:58 +02:00
|
|
|
return runPacket, nil
|
|
|
|
}
|
2022-06-29 02:20:01 +02:00
|
|
|
if opts.SudoWithPass {
|
2022-06-29 06:57:30 +02:00
|
|
|
pwFdNum, err := AddRunData(runPacket, opts.SudoPw, "sudo pw")
|
2022-06-26 10:41:58 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-06-29 06:57:30 +02:00
|
|
|
commandFdNum, err := AddRunData(runPacket, opts.Command, "command")
|
2022-06-26 10:41:58 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-06-29 06:57:30 +02:00
|
|
|
commandStdinFdNum, err := NextFreeFdNum(runPacket)
|
2022-06-26 10:41:58 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
commandStdinRfd := packet.RemoteFd{FdNum: commandStdinFdNum, Read: true, DupStdin: true}
|
2022-06-29 06:57:30 +02:00
|
|
|
runPacket.Fds = append(runPacket.Fds, commandStdinRfd)
|
|
|
|
maxFdNum := MaxFdNumInPacket(runPacket)
|
2022-06-28 00:59:14 +02:00
|
|
|
runPacket.Command = fmt.Sprintf(RunSudoPasswordCommandFmt, pwFdNum, maxFdNum+1, pwFdNum, commandFdNum, commandStdinFdNum)
|
2022-06-26 10:41:58 +02:00
|
|
|
return runPacket, nil
|
|
|
|
} else {
|
2022-06-29 06:57:30 +02:00
|
|
|
commandFdNum, err := AddRunData(runPacket, opts.Command, "command")
|
2022-06-26 10:41:58 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-06-29 06:57:30 +02:00
|
|
|
maxFdNum := MaxFdNumInPacket(runPacket)
|
2022-06-27 23:57:01 +02:00
|
|
|
runPacket.Command = fmt.Sprintf(RunSudoCommandFmt, maxFdNum+1, commandFdNum)
|
2022-06-26 10:41:58 +02:00
|
|
|
return runPacket, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-29 06:57:30 +02:00
|
|
|
func AddRunData(pk *packet.RunPacketType, data string, dataType string) (int, error) {
|
|
|
|
if len(data) > mpio.ReadBufSize {
|
|
|
|
return 0, fmt.Errorf("%s too large, exceeds read buffer size", dataType)
|
|
|
|
}
|
|
|
|
fdNum, err := NextFreeFdNum(pk)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
runData := packet.RunDataType{FdNum: fdNum, DataLen: len(data), Data: []byte(data)}
|
|
|
|
pk.RunData = append(pk.RunData, runData)
|
|
|
|
return fdNum, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func NextFreeFdNum(pk *packet.RunPacketType) (int, error) {
|
2022-06-26 10:41:58 +02:00
|
|
|
fdMap := make(map[int]bool)
|
2022-06-29 06:57:30 +02:00
|
|
|
for _, fd := range pk.Fds {
|
2022-06-26 10:41:58 +02:00
|
|
|
fdMap[fd.FdNum] = true
|
|
|
|
}
|
2022-06-29 06:57:30 +02:00
|
|
|
for _, rd := range pk.RunData {
|
|
|
|
fdMap[rd.FdNum] = true
|
|
|
|
}
|
2022-06-26 10:41:58 +02:00
|
|
|
for i := 3; i <= MaxFdNum; i++ {
|
|
|
|
if !fdMap[i] {
|
|
|
|
return i, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0, fmt.Errorf("reached maximum number of fds, all fds between 3-%d are in use", MaxFdNum)
|
|
|
|
}
|
|
|
|
|
2022-06-29 06:57:30 +02:00
|
|
|
func MaxFdNumInPacket(pk *packet.RunPacketType) int {
|
2022-06-26 10:41:58 +02:00
|
|
|
maxFdNum := 3
|
2022-06-29 06:57:30 +02:00
|
|
|
for _, fd := range pk.Fds {
|
2022-06-26 10:41:58 +02:00
|
|
|
if fd.FdNum > maxFdNum {
|
|
|
|
maxFdNum = fd.FdNum
|
|
|
|
}
|
|
|
|
}
|
2022-06-29 06:57:30 +02:00
|
|
|
for _, rd := range pk.RunData {
|
|
|
|
if rd.FdNum > maxFdNum {
|
|
|
|
maxFdNum = rd.FdNum
|
|
|
|
}
|
|
|
|
}
|
2022-06-26 10:41:58 +02:00
|
|
|
return maxFdNum
|
2022-06-24 22:25:09 +02:00
|
|
|
}
|
|
|
|
|
2022-06-25 08:42:00 +02:00
|
|
|
func ValidateRemoteFds(rfds []packet.RemoteFd) error {
|
|
|
|
dupMap := make(map[int]bool)
|
|
|
|
for _, rfd := range rfds {
|
|
|
|
if rfd.FdNum < 0 {
|
|
|
|
return fmt.Errorf("mshell negative fd numbers fd=%d", rfd.FdNum)
|
|
|
|
}
|
|
|
|
if rfd.FdNum < FirstExtraFilesFdNum {
|
|
|
|
return fmt.Errorf("mshell does not support re-opening fd=%d (0, 1, and 2, are always open)", rfd.FdNum)
|
|
|
|
}
|
|
|
|
if rfd.FdNum > MaxFdNum {
|
|
|
|
return fmt.Errorf("mshell does not support opening fd numbers above %d", MaxFdNum)
|
|
|
|
}
|
|
|
|
if dupMap[rfd.FdNum] {
|
|
|
|
return fmt.Errorf("mshell got duplicate entries for fd=%d", rfd.FdNum)
|
|
|
|
}
|
|
|
|
if rfd.Read && rfd.Write {
|
|
|
|
return fmt.Errorf("mshell does not support opening fd numbers for reading and writing, fd=%d", rfd.FdNum)
|
|
|
|
}
|
|
|
|
if !rfd.Read && !rfd.Write {
|
|
|
|
return fmt.Errorf("invalid fd=%d, neither reading or writing mode specified", rfd.FdNum)
|
|
|
|
}
|
|
|
|
dupMap[rfd.FdNum] = true
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-06-28 08:14:53 +02:00
|
|
|
func sendOptFile(input io.WriteCloser, optName string) error {
|
|
|
|
fd, err := os.Open(optName)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("cannot open '%s': %w", optName, err)
|
|
|
|
}
|
|
|
|
go func() {
|
|
|
|
defer input.Close()
|
|
|
|
io.Copy(input, fd)
|
|
|
|
}()
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-06-28 07:39:16 +02:00
|
|
|
func RunInstallSSHCommand(opts *InstallOpts) error {
|
2022-06-28 08:14:53 +02:00
|
|
|
tryDetect := opts.Detect
|
2022-06-29 23:29:38 +02:00
|
|
|
ecmd, err := opts.SSHOpts.MakeSSHInstallCmd()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-06-28 07:39:16 +02:00
|
|
|
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)
|
|
|
|
}()
|
2022-06-28 08:14:53 +02:00
|
|
|
if opts.OptName != "" {
|
|
|
|
sendOptFile(inputWriter, opts.OptName)
|
2022-06-28 07:39:16 +02:00
|
|
|
}
|
|
|
|
packetParser := packet.MakePacketParser(stdoutReader)
|
|
|
|
err = ecmd.Start()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("running ssh command: %w", err)
|
|
|
|
}
|
2022-06-28 08:14:53 +02:00
|
|
|
firstInit := true
|
2022-06-28 07:39:16 +02:00
|
|
|
for pk := range packetParser.MainCh {
|
2022-06-28 08:14:53 +02:00
|
|
|
if pk.GetType() == packet.InitPacketStr && firstInit {
|
|
|
|
firstInit = false
|
|
|
|
initPacket := pk.(*packet.InitPacketType)
|
|
|
|
if !tryDetect {
|
|
|
|
continue // ignore
|
|
|
|
}
|
|
|
|
tryDetect = false
|
|
|
|
if initPacket.UName == "" {
|
|
|
|
return fmt.Errorf("cannot detect arch, no uname received from remote server")
|
|
|
|
}
|
|
|
|
goos, goarch, err := DetectGoArch(initPacket.UName)
|
|
|
|
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)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if pk.GetType() == packet.InitPacketStr && !firstInit {
|
2022-06-28 07:39:16 +02:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2022-06-29 02:20:01 +02:00
|
|
|
func HasDupStdin(fds []packet.RemoteFd) bool {
|
|
|
|
for _, rfd := range fds {
|
|
|
|
if rfd.Read && rfd.DupStdin {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2022-06-29 04:01:33 +02:00
|
|
|
func RunClientSSHCommandAndWait(runPacket *packet.RunPacketType, fdContext FdContext, sshOpts SSHOpts, upr packet.UnknownPacketReporter, debug bool) (*packet.CmdDonePacketType, error) {
|
|
|
|
cmd := MakeShExec(runPacket.CK, upr)
|
2022-06-29 23:29:38 +02:00
|
|
|
ecmd, err := sshOpts.MakeMShellSingleCmd()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-06-24 22:25:09 +02:00
|
|
|
cmd.Cmd = ecmd
|
|
|
|
inputWriter, err := ecmd.StdinPipe()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("creating stdin pipe: %v", err)
|
|
|
|
}
|
|
|
|
stdoutReader, err := ecmd.StdoutPipe()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("creating stdout pipe: %v", err)
|
|
|
|
}
|
|
|
|
stderrReader, err := ecmd.StderrPipe()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("creating stderr pipe: %v", err)
|
|
|
|
}
|
2022-06-29 02:20:01 +02:00
|
|
|
if !HasDupStdin(runPacket.Fds) {
|
2022-07-06 21:16:37 +02:00
|
|
|
cmd.Multiplexer.MakeRawFdReader(0, fdContext.GetReader(0), false, false)
|
2022-06-26 10:41:58 +02:00
|
|
|
}
|
2022-06-29 02:20:01 +02:00
|
|
|
cmd.Multiplexer.MakeRawFdWriter(1, fdContext.GetWriter(1), false)
|
|
|
|
cmd.Multiplexer.MakeRawFdWriter(2, fdContext.GetWriter(2), false)
|
2022-06-26 10:41:58 +02:00
|
|
|
for _, rfd := range runPacket.Fds {
|
|
|
|
if rfd.Read && rfd.DupStdin {
|
2022-07-06 21:16:37 +02:00
|
|
|
cmd.Multiplexer.MakeRawFdReader(rfd.FdNum, fdContext.GetReader(0), false, false)
|
2022-06-26 10:41:58 +02:00
|
|
|
continue
|
|
|
|
}
|
2022-06-25 08:42:00 +02:00
|
|
|
if rfd.Read {
|
2022-06-29 02:20:01 +02:00
|
|
|
fd := fdContext.GetReader(rfd.FdNum)
|
2022-07-06 21:16:37 +02:00
|
|
|
cmd.Multiplexer.MakeRawFdReader(rfd.FdNum, fd, false, false)
|
2022-06-25 08:42:00 +02:00
|
|
|
} else if rfd.Write {
|
2022-06-29 02:20:01 +02:00
|
|
|
fd := fdContext.GetWriter(rfd.FdNum)
|
2022-06-25 08:42:00 +02:00
|
|
|
cmd.Multiplexer.MakeRawFdWriter(rfd.FdNum, fd, true)
|
|
|
|
}
|
|
|
|
}
|
2022-06-24 22:25:09 +02:00
|
|
|
err = ecmd.Start()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("running ssh command: %w", err)
|
|
|
|
}
|
|
|
|
defer cmd.Close()
|
2022-06-27 21:14:07 +02:00
|
|
|
stdoutPacketParser := packet.MakePacketParser(stdoutReader)
|
|
|
|
stderrPacketParser := packet.MakePacketParser(stderrReader)
|
|
|
|
packetParser := packet.CombinePacketParsers(stdoutPacketParser, stderrPacketParser)
|
2022-06-24 22:25:09 +02:00
|
|
|
sender := packet.MakePacketSender(inputWriter)
|
2022-06-25 09:33:18 +02:00
|
|
|
versionOk := false
|
2022-06-27 21:14:07 +02:00
|
|
|
for pk := range packetParser.MainCh {
|
2022-06-24 22:25:09 +02:00
|
|
|
if pk.GetType() == packet.RawPacketStr {
|
|
|
|
rawPk := pk.(*packet.RawPacketType)
|
|
|
|
fmt.Printf("%s\n", rawPk.Data)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if pk.GetType() == packet.InitPacketStr {
|
|
|
|
initPk := pk.(*packet.InitPacketType)
|
2022-06-27 23:57:01 +02:00
|
|
|
if initPk.NotFound {
|
2022-06-29 00:04:08 +02:00
|
|
|
if sshOpts.SSHHost == "" {
|
|
|
|
return nil, fmt.Errorf("mshell command not found on local server")
|
|
|
|
}
|
2022-06-28 03:42:56 +02:00
|
|
|
if initPk.UName == "" {
|
|
|
|
return nil, fmt.Errorf("mshell command not found on remote server, no uname detected")
|
|
|
|
}
|
2022-06-28 08:14:53 +02:00
|
|
|
goos, goarch, err := DetectGoArch(initPk.UName)
|
2022-06-28 03:42:56 +02:00
|
|
|
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)
|
|
|
|
}
|
2022-06-29 00:04:08 +02:00
|
|
|
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)
|
2022-06-27 23:57:01 +02:00
|
|
|
}
|
2022-06-28 07:39:16 +02:00
|
|
|
if initPk.Version != base.MShellVersion {
|
|
|
|
return nil, fmt.Errorf("invalid remote mshell version 'v%s', must be v%s", initPk.Version, base.MShellVersion)
|
2022-06-24 22:25:09 +02:00
|
|
|
}
|
2022-06-25 09:33:18 +02:00
|
|
|
versionOk = true
|
2022-06-29 00:04:08 +02:00
|
|
|
if debug {
|
2022-06-28 00:59:14 +02:00
|
|
|
fmt.Printf("VERSION> %s\n", initPk.Version)
|
|
|
|
}
|
2022-06-24 22:25:09 +02:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2022-06-25 09:33:18 +02:00
|
|
|
if !versionOk {
|
|
|
|
return nil, fmt.Errorf("did not receive version from remote mshell")
|
|
|
|
}
|
2022-07-07 03:59:46 +02:00
|
|
|
SendRunPacketAndRunData(context.Background(), sender, runPacket)
|
2022-06-29 00:04:08 +02:00
|
|
|
if debug {
|
2022-06-25 09:22:03 +02:00
|
|
|
cmd.Multiplexer.Debug = true
|
|
|
|
}
|
2022-06-27 21:14:07 +02:00
|
|
|
remoteDonePacket := cmd.Multiplexer.RunIOAndWait(packetParser, sender, false, true, true)
|
2022-06-24 22:25:09 +02:00
|
|
|
donePacket := cmd.WaitForCommand()
|
|
|
|
if remoteDonePacket != nil {
|
|
|
|
donePacket = remoteDonePacket
|
|
|
|
}
|
|
|
|
return donePacket, nil
|
|
|
|
}
|
|
|
|
|
2022-06-29 06:57:30 +02:00
|
|
|
func min(v1 int, v2 int) int {
|
|
|
|
if v1 <= v2 {
|
|
|
|
return v1
|
|
|
|
}
|
|
|
|
return v2
|
|
|
|
}
|
|
|
|
|
2022-07-07 03:59:46 +02:00
|
|
|
func SendRunPacketAndRunData(ctx context.Context, sender *packet.PacketSender, runPacket *packet.RunPacketType) error {
|
|
|
|
err := sender.SendPacketCtx(ctx, runPacket)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-06-29 23:29:38 +02:00
|
|
|
if len(runPacket.RunData) == 0 {
|
2022-07-07 03:59:46 +02:00
|
|
|
return nil
|
2022-06-29 23:29:38 +02:00
|
|
|
}
|
2022-06-29 06:57:30 +02:00
|
|
|
for _, runData := range runPacket.RunData {
|
|
|
|
sendBuf := runData.Data
|
|
|
|
for len(sendBuf) > 0 {
|
|
|
|
chunkSize := min(len(sendBuf), mpio.MaxSingleWriteSize)
|
|
|
|
chunk := sendBuf[0:chunkSize]
|
|
|
|
dataPk := packet.MakeDataPacket()
|
|
|
|
dataPk.CK = runPacket.CK
|
|
|
|
dataPk.FdNum = runData.FdNum
|
|
|
|
dataPk.Data64 = base64.StdEncoding.EncodeToString(chunk)
|
|
|
|
dataPk.Eof = (len(chunk) == len(sendBuf))
|
|
|
|
sendBuf = sendBuf[chunkSize:]
|
2022-07-07 03:59:46 +02:00
|
|
|
err = sender.SendPacketCtx(ctx, dataPk)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-06-29 06:57:30 +02:00
|
|
|
}
|
|
|
|
}
|
2022-07-07 03:59:46 +02:00
|
|
|
err = sender.SendPacketCtx(ctx, packet.MakeDataEndPacket(runPacket.CK))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
2022-06-29 06:57:30 +02:00
|
|
|
}
|
|
|
|
|
2022-06-28 08:14:53 +02:00
|
|
|
func DetectGoArch(uname string) (string, string, error) {
|
2022-06-28 03:42:56 +02:00
|
|
|
fields := strings.SplitN(uname, "|", 2)
|
|
|
|
if len(fields) != 2 {
|
|
|
|
return "", "", fmt.Errorf("invalid uname string returned")
|
|
|
|
}
|
|
|
|
osVal := strings.TrimSpace(strings.ToLower(fields[0]))
|
|
|
|
archVal := strings.TrimSpace(strings.ToLower(fields[1]))
|
|
|
|
if osVal != "darwin" && osVal != "linux" {
|
|
|
|
return "", "", fmt.Errorf("invalid uname OS '%s', mshell only supports OS X (darwin) and linux", osVal)
|
|
|
|
}
|
|
|
|
goos := osVal
|
|
|
|
goarch := ""
|
|
|
|
if archVal == "x86_64" || archVal == "i686" || archVal == "amd64" {
|
|
|
|
goarch = "amd64"
|
|
|
|
} else if archVal == "aarch64" || archVal == "amd64" {
|
|
|
|
goarch = "arm64"
|
|
|
|
}
|
|
|
|
if goarch == "" {
|
|
|
|
return "", "", fmt.Errorf("invalid uname machine type '%s', mshell only supports aarch64 (amd64) and x86_64 (amd64)", archVal)
|
|
|
|
}
|
2022-06-28 07:39:16 +02:00
|
|
|
if !base.ValidGoArch(goos, goarch) {
|
|
|
|
return "", "", fmt.Errorf("invalid arch detected %s.%s", goos, goarch)
|
|
|
|
}
|
2022-06-28 03:42:56 +02:00
|
|
|
return goos, goarch, nil
|
|
|
|
}
|
|
|
|
|
2022-06-27 21:14:07 +02:00
|
|
|
func (cmd *ShExecType) RunRemoteIOAndWait(packetParser *packet.PacketParser, sender *packet.PacketSender) {
|
2022-06-25 08:42:00 +02:00
|
|
|
defer cmd.Close()
|
2022-06-27 21:14:07 +02:00
|
|
|
cmd.Multiplexer.RunIOAndWait(packetParser, sender, true, false, false)
|
2022-06-23 21:48:45 +02:00
|
|
|
donePacket := cmd.WaitForCommand()
|
|
|
|
sender.SendPacket(donePacket)
|
|
|
|
}
|
|
|
|
|
2022-07-06 23:06:58 +02:00
|
|
|
func getTermType(pk *packet.RunPacketType) string {
|
|
|
|
termType := DefaultTermType
|
|
|
|
if pk.TermOpts != nil && pk.TermOpts.Term != "" {
|
|
|
|
termType = pk.TermOpts.Term
|
|
|
|
}
|
|
|
|
return termType
|
|
|
|
}
|
|
|
|
|
2022-06-29 23:29:38 +02:00
|
|
|
func RunCommandSimple(pk *packet.RunPacketType, sender *packet.PacketSender) (*ShExecType, error) {
|
2022-06-29 04:01:33 +02:00
|
|
|
cmd := MakeShExec(pk.CK, nil)
|
2022-06-23 21:48:45 +02:00
|
|
|
cmd.Cmd = exec.Command("bash", "-c", pk.Command)
|
|
|
|
UpdateCmdEnv(cmd.Cmd, pk.Env)
|
|
|
|
if pk.Cwd != "" {
|
2022-06-25 08:42:00 +02:00
|
|
|
cmd.Cmd.Dir = base.ExpandHomeDir(pk.Cwd)
|
|
|
|
}
|
|
|
|
err := ValidateRemoteFds(pk.Fds)
|
|
|
|
if err != nil {
|
|
|
|
cmd.Close()
|
|
|
|
return nil, err
|
2022-06-23 21:48:45 +02:00
|
|
|
}
|
2022-07-06 20:21:15 +02:00
|
|
|
var cmdPty *os.File
|
|
|
|
var cmdTty *os.File
|
|
|
|
if pk.UsePty {
|
|
|
|
cmdPty, cmdTty, err = pty.Open()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("opening new pty: %w", err)
|
|
|
|
}
|
|
|
|
pty.Setsize(cmdPty, GetWinsize(pk))
|
|
|
|
defer func() {
|
|
|
|
cmdTty.Close()
|
|
|
|
}()
|
|
|
|
cmd.CmdPty = cmdPty
|
2022-07-06 23:06:58 +02:00
|
|
|
UpdateCmdEnv(cmd.Cmd, map[string]string{"TERM": getTermType(pk)})
|
2022-07-06 20:21:15 +02:00
|
|
|
}
|
|
|
|
if cmdTty != nil {
|
|
|
|
cmd.Cmd.Stdin = cmdTty
|
|
|
|
cmd.Cmd.Stdout = cmdTty
|
|
|
|
cmd.Cmd.Stderr = cmdTty
|
|
|
|
cmd.Cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
|
|
Setsid: true,
|
|
|
|
Setctty: true,
|
|
|
|
}
|
|
|
|
cmd.Multiplexer.MakeRawFdWriter(0, cmdPty, false)
|
2022-07-06 21:16:37 +02:00
|
|
|
cmd.Multiplexer.MakeRawFdReader(1, cmdPty, false, true)
|
2022-07-06 20:21:15 +02:00
|
|
|
nullFd, err := os.Open("/dev/null")
|
|
|
|
if err != nil {
|
|
|
|
cmd.Close()
|
|
|
|
return nil, fmt.Errorf("cannot open /dev/null: %w", err)
|
|
|
|
}
|
2022-07-06 21:16:37 +02:00
|
|
|
cmd.Multiplexer.MakeRawFdReader(2, nullFd, true, false)
|
2022-07-06 20:21:15 +02:00
|
|
|
} else {
|
|
|
|
cmd.Cmd.Stdin, err = cmd.Multiplexer.MakeWriterPipe(0)
|
|
|
|
if err != nil {
|
|
|
|
cmd.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
cmd.Cmd.Stdout, err = cmd.Multiplexer.MakeReaderPipe(1)
|
|
|
|
if err != nil {
|
|
|
|
cmd.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
cmd.Cmd.Stderr, err = cmd.Multiplexer.MakeReaderPipe(2)
|
|
|
|
if err != nil {
|
|
|
|
cmd.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-06-23 21:48:45 +02:00
|
|
|
}
|
2022-06-24 03:23:30 +02:00
|
|
|
extraFiles := make([]*os.File, 0, MaxFdNum+1)
|
2022-06-29 06:57:30 +02:00
|
|
|
for _, runData := range pk.RunData {
|
|
|
|
if runData.FdNum >= len(extraFiles) {
|
|
|
|
extraFiles = extraFiles[:runData.FdNum+1]
|
|
|
|
}
|
|
|
|
extraFiles[runData.FdNum], err = cmd.Multiplexer.MakeStaticWriterPipe(runData.FdNum, runData.Data)
|
|
|
|
if err != nil {
|
|
|
|
cmd.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
2022-06-24 03:23:30 +02:00
|
|
|
for _, rfd := range pk.Fds {
|
|
|
|
if rfd.FdNum >= len(extraFiles) {
|
|
|
|
extraFiles = extraFiles[:rfd.FdNum+1]
|
|
|
|
}
|
|
|
|
if rfd.Read {
|
|
|
|
// client file is open for reading, so we make a writer pipe
|
2022-06-24 19:24:02 +02:00
|
|
|
extraFiles[rfd.FdNum], err = cmd.Multiplexer.MakeWriterPipe(rfd.FdNum)
|
2022-06-24 03:23:30 +02:00
|
|
|
if err != nil {
|
|
|
|
cmd.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if rfd.Write {
|
|
|
|
// client file is open for writing, so we make a reader pipe
|
2022-06-24 19:24:02 +02:00
|
|
|
extraFiles[rfd.FdNum], err = cmd.Multiplexer.MakeReaderPipe(rfd.FdNum)
|
2022-06-24 03:23:30 +02:00
|
|
|
if err != nil {
|
|
|
|
cmd.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(extraFiles) > FirstExtraFilesFdNum {
|
|
|
|
cmd.Cmd.ExtraFiles = extraFiles[FirstExtraFilesFdNum:]
|
|
|
|
}
|
|
|
|
|
2022-06-23 21:48:45 +02:00
|
|
|
err = cmd.Cmd.Start()
|
|
|
|
if err != nil {
|
|
|
|
cmd.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return cmd, nil
|
|
|
|
}
|
|
|
|
|
2022-06-29 23:29:38 +02:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
2022-08-20 00:28:32 +02:00
|
|
|
func copyToCirFile(dest *cirfile.File, src io.Reader) error {
|
|
|
|
buf := make([]byte, 64*1024)
|
|
|
|
for {
|
|
|
|
var appendErr error
|
|
|
|
nr, readErr := src.Read(buf)
|
|
|
|
if nr > 0 {
|
|
|
|
appendErr = dest.AppendData(context.Background(), buf[0:nr])
|
|
|
|
}
|
|
|
|
if readErr != nil && readErr != io.EOF {
|
|
|
|
return readErr
|
|
|
|
}
|
|
|
|
if appendErr != nil {
|
|
|
|
return appendErr
|
|
|
|
}
|
|
|
|
if readErr == io.EOF {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-06 08:14:14 +02:00
|
|
|
func (cmd *ShExecType) DetachedWait(startPacket *packet.CmdStartPacketType) {
|
|
|
|
// after Start(), any output/errors must go to DetachedOutput
|
2022-07-06 09:21:44 +02:00
|
|
|
// close stdin, redirect stdout/stderr to /dev/null, but wait for cmdstart packet to get sent
|
|
|
|
cmd.DetachedOutput.SendPacket(startPacket)
|
|
|
|
err := os.Stdin.Close()
|
2022-07-06 08:14:14 +02:00
|
|
|
if err != nil {
|
2022-07-06 09:21:44 +02:00
|
|
|
cmd.DetachedOutput.SendCmdError(cmd.CK, fmt.Errorf("cannot close stdin: %w", err))
|
2022-07-06 08:14:14 +02:00
|
|
|
}
|
2022-07-06 09:21:44 +02:00
|
|
|
err = unix.Dup2(int(cmd.RunnerOutFd.Fd()), int(os.Stdout.Fd()))
|
|
|
|
if err != nil {
|
|
|
|
cmd.DetachedOutput.SendCmdError(cmd.CK, fmt.Errorf("cannot dup2 stdin to runout: %w", err))
|
|
|
|
}
|
|
|
|
err = unix.Dup2(int(cmd.RunnerOutFd.Fd()), int(os.Stderr.Fd()))
|
|
|
|
if err != nil {
|
|
|
|
cmd.DetachedOutput.SendCmdError(cmd.CK, fmt.Errorf("cannot dup2 stdin to runout: %w", err))
|
2022-07-06 08:14:14 +02:00
|
|
|
}
|
2022-08-20 00:28:32 +02:00
|
|
|
ptyOutFile, err := cirfile.CreateCirFile(cmd.FileNames.PtyOutFile, cmd.MaxPtySize)
|
2022-07-06 08:14:14 +02:00
|
|
|
if err != nil {
|
|
|
|
cmd.DetachedOutput.SendCmdError(cmd.CK, fmt.Errorf("cannot open ptyout file '%s': %w", cmd.FileNames.PtyOutFile, err))
|
|
|
|
// don't return (command is already running)
|
|
|
|
}
|
2022-07-06 09:21:44 +02:00
|
|
|
ptyCopyDone := make(chan bool)
|
2022-07-06 08:14:14 +02:00
|
|
|
go func() {
|
|
|
|
// copy pty output to .ptyout file
|
2022-07-06 09:21:44 +02:00
|
|
|
defer close(ptyCopyDone)
|
2022-08-20 00:28:32 +02:00
|
|
|
defer ptyOutFile.Close()
|
|
|
|
copyErr := copyToCirFile(ptyOutFile, cmd.CmdPty)
|
2022-07-06 08:14:14 +02:00
|
|
|
if copyErr != nil {
|
|
|
|
cmd.DetachedOutput.SendCmdError(cmd.CK, fmt.Errorf("copying pty output to ptyout file: %w", copyErr))
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
go func() {
|
|
|
|
// copy .stdin fifo contents to pty input
|
|
|
|
copyFifoErr := MakeAndCopyStdinFifo(cmd.CmdPty, cmd.FileNames.StdinFifo)
|
|
|
|
if copyFifoErr != nil {
|
|
|
|
cmd.DetachedOutput.SendCmdError(cmd.CK, fmt.Errorf("reading from stdin fifo: %w", copyFifoErr))
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
donePacket := cmd.WaitForCommand()
|
|
|
|
cmd.DetachedOutput.SendPacket(donePacket)
|
2022-07-06 09:21:44 +02:00
|
|
|
<-ptyCopyDone
|
|
|
|
cmd.Close()
|
2022-07-06 08:14:14 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func RunCommandDetached(pk *packet.RunPacketType, sender *packet.PacketSender) (*ShExecType, *packet.CmdStartPacketType, error) {
|
2022-06-27 21:03:47 +02:00
|
|
|
fileNames, err := base.GetCommandFileNames(pk.CK)
|
2022-06-10 09:35:24 +02:00
|
|
|
if err != nil {
|
2022-07-06 08:14:14 +02:00
|
|
|
return nil, nil, err
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
2022-07-06 20:21:15 +02:00
|
|
|
runOutInfo, err := os.Stat(fileNames.RunnerOutFile)
|
2022-06-17 21:27:29 +02:00
|
|
|
if err == nil { // non-nil error will be caught by regular OpenFile below
|
|
|
|
// must have size 0
|
2022-07-06 20:21:15 +02:00
|
|
|
if runOutInfo.Size() != 0 {
|
|
|
|
return nil, nil, fmt.Errorf("cmdkey '%s' was already used (runout len=%d)", pk.CK, runOutInfo.Size())
|
2022-06-17 21:27:29 +02:00
|
|
|
}
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
|
|
|
cmdPty, cmdTty, err := pty.Open()
|
|
|
|
if err != nil {
|
2022-07-06 08:14:14 +02:00
|
|
|
return nil, nil, fmt.Errorf("opening new pty: %w", err)
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
2022-06-21 02:51:28 +02:00
|
|
|
pty.Setsize(cmdPty, GetWinsize(pk))
|
2022-06-10 09:35:24 +02:00
|
|
|
defer func() {
|
|
|
|
cmdTty.Close()
|
|
|
|
}()
|
2022-07-02 02:37:37 +02:00
|
|
|
cmd := MakeShExec(pk.CK, nil)
|
|
|
|
cmd.FileNames = fileNames
|
|
|
|
cmd.CmdPty = cmdPty
|
|
|
|
cmd.Detached = true
|
2022-08-20 00:28:32 +02:00
|
|
|
if pk.TermOpts != nil && pk.TermOpts.MaxPtySize != 0 {
|
|
|
|
cmd.MaxPtySize = pk.TermOpts.MaxPtySize
|
|
|
|
} else {
|
|
|
|
cmd.MaxPtySize = DefaultMaxPtySize
|
|
|
|
}
|
2022-07-02 02:37:37 +02:00
|
|
|
cmd.RunnerOutFd, err = os.OpenFile(fileNames.RunnerOutFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
|
|
|
|
if err != nil {
|
2022-07-06 08:14:14 +02:00
|
|
|
return nil, nil, fmt.Errorf("cannot open runout file '%s': %w", fileNames.RunnerOutFile, err)
|
2022-07-02 02:37:37 +02:00
|
|
|
}
|
|
|
|
cmd.DetachedOutput = packet.MakePacketSender(cmd.RunnerOutFd)
|
2022-06-29 23:29:38 +02:00
|
|
|
ecmd, err := MakeDetachedExecCmd(pk, cmdTty)
|
|
|
|
if err != nil {
|
2022-07-06 08:14:14 +02:00
|
|
|
return nil, nil, err
|
2022-06-29 23:29:38 +02:00
|
|
|
}
|
2022-07-02 02:37:37 +02:00
|
|
|
cmd.Cmd = ecmd
|
2022-06-29 23:29:38 +02:00
|
|
|
SetupSignalsForDetach()
|
2022-06-10 09:35:24 +02:00
|
|
|
err = ecmd.Start()
|
|
|
|
if err != nil {
|
2022-07-06 08:14:14 +02:00
|
|
|
return nil, nil, fmt.Errorf("starting command: %w", err)
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
2022-06-29 23:29:38 +02:00
|
|
|
for _, fd := range ecmd.ExtraFiles {
|
|
|
|
if fd != cmdTty {
|
|
|
|
fd.Close()
|
|
|
|
}
|
|
|
|
}
|
2022-07-06 08:14:14 +02:00
|
|
|
startPacket := cmd.MakeCmdStartPacket(pk.ReqId)
|
|
|
|
return cmd, startPacket, nil
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
|
|
|
|
2022-06-16 01:29:39 +02:00
|
|
|
func GetExitCode(err error) int {
|
|
|
|
if err == nil {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
|
|
return exitErr.ExitCode()
|
|
|
|
} else {
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-23 21:48:45 +02:00
|
|
|
func (c *ShExecType) WaitForCommand() *packet.CmdDonePacketType {
|
2022-06-16 01:29:39 +02:00
|
|
|
exitErr := c.Cmd.Wait()
|
2022-06-11 06:37:21 +02:00
|
|
|
endTs := time.Now()
|
|
|
|
cmdDuration := endTs.Sub(c.StartTs)
|
2022-06-16 01:29:39 +02:00
|
|
|
exitCode := GetExitCode(exitErr)
|
2022-07-06 08:14:14 +02:00
|
|
|
donePacket := packet.MakeCmdDonePacket(c.CK)
|
2022-06-11 06:37:21 +02:00
|
|
|
donePacket.Ts = endTs.UnixMilli()
|
|
|
|
donePacket.ExitCode = exitCode
|
|
|
|
donePacket.DurationMs = int64(cmdDuration / time.Millisecond)
|
2022-06-23 21:48:45 +02:00
|
|
|
if c.FileNames != nil {
|
|
|
|
os.Remove(c.FileNames.StdinFifo) // best effort (no need to check error)
|
|
|
|
}
|
2022-06-11 06:37:21 +02:00
|
|
|
return donePacket
|
2022-06-10 09:35:24 +02:00
|
|
|
}
|
2022-07-02 02:37:37 +02:00
|
|
|
|
|
|
|
func MakeInitPacket() *packet.InitPacketType {
|
|
|
|
initPacket := packet.MakeInitPacket()
|
|
|
|
initPacket.Version = base.MShellVersion
|
|
|
|
initPacket.HomeDir = base.GetHomeDir()
|
|
|
|
initPacket.MShellHomeDir = base.GetMShellHomeDir()
|
|
|
|
if user, _ := user.Current(); user != nil {
|
|
|
|
initPacket.User = user.Username
|
|
|
|
}
|
2022-07-07 22:25:42 +02:00
|
|
|
initPacket.HostName, _ = os.Hostname()
|
2022-07-02 02:37:37 +02:00
|
|
|
return initPacket
|
|
|
|
}
|
|
|
|
|
|
|
|
func MakeServerInitPacket() (*packet.InitPacketType, error) {
|
|
|
|
var err error
|
|
|
|
initPacket := MakeInitPacket()
|
|
|
|
initPacket.Env = os.Environ()
|
|
|
|
initPacket.RemoteId, err = base.GetRemoteId()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return initPacket, nil
|
|
|
|
}
|