// Package eows is used to Execute commands Over WebSocket package eows import ( "fmt" "os" "os/exec" "strings" "syscall" "time" "unsafe" "github.com/Sirupsen/logrus" "github.com/googollee/go-socket.io" "github.com/kr/pty" ) // OnInputCB is the function callback used to receive data type OnInputCB func(e *ExecOverWS, stdin []byte) ([]byte, error) // EmitOutputCB is the function callback used to emit data type EmitOutputCB func(e *ExecOverWS, stdout, stderr []byte) // EmitExitCB is the function callback used to emit exit proc code type EmitExitCB func(e *ExecOverWS, code int, err error) // SplitType Type of spliting method to tokenize stdout/stderr type SplitType uint8 const ( // SplitLine Split line by line SplitLine SplitType = iota // SplitChar Split character by character SplitChar // SplitLineTime Split by line or until a timeout has passed SplitLineTime // SplitTime Split until a timeout has passed SplitTime ) // Inspired by : // https://github.com/gorilla/websocket/blob/master/examples/command/main.go // ExecOverWS . type ExecOverWS struct { Cmd string // command name to execute Args []string // command arguments SocketIO *socketio.Socket // websocket Sid string // websocket ID CmdID string // command ID // Optional fields Env []string // command environment variables CmdExecTimeout int // command execution time timeout Log *logrus.Logger // logger (nil if disabled) InputEvent string // websocket input event name InputCB OnInputCB // stdin callback OutputCB EmitOutputCB // stdout/stderr callback ExitCB EmitExitCB // exit proc callback UserData *map[string]interface{} // user data passed to callbacks OutSplit SplitType // split method to tokenize stdout/stderr LineTimeSpan int64 // time span (only used with SplitTime or SplitLineTime) PtyMode bool // Allocate a pseudo-terminal (allow to execute screen-based program) PtyTermEcho bool // Turn on/off terminal echo // Private fields proc *os.Process command *exec.Cmd ptmx *os.File procExited bool } var cmdIDMap = make(map[string]*ExecOverWS) // New creates a new instace of eows func New(cmd string, args []string, so *socketio.Socket, soID, cmdID string) *ExecOverWS { e := &ExecOverWS{ Cmd: cmd, Args: args, SocketIO: so, Sid: soID, CmdID: cmdID, CmdExecTimeout: -1, // default no timeout OutSplit: SplitLineTime, // default split by line with time LineTimeSpan: 500 * time.Millisecond.Nanoseconds(), PtyMode: false, PtyTermEcho: true, } cmdIDMap[cmdID] = e return e } // GetEows gets ExecOverWS object from command ID func GetEows(cmdID string) *ExecOverWS { if _, ok := cmdIDMap[cmdID]; !ok { return nil } return cmdIDMap[cmdID] } // Start executes the command and redirect stdout/stderr into a WebSocket func (e *ExecOverWS) Start() error { var err error var outr, outw, errr, errw, inr, inw *os.File bashArgs := []string{"/bin/bash", "-c", e.Cmd + " " + strings.Join(e.Args, " ")} // no timeout == 1 year if e.CmdExecTimeout == -1 { e.CmdExecTimeout = 365 * 24 * 60 * 60 } e.procExited = false if e.PtyMode { e.command = exec.Command(bashArgs[0], bashArgs[1:]...) e.command.Env = append(os.Environ(), e.Env...) e.ptmx, err = pty.Start(e.command) if err != nil { err = fmt.Errorf("Process start error: " + err.Error()) goto exitErr } e.proc = e.command.Process // Turn off terminal echo if !e.PtyTermEcho { e.terminalEcho(e.ptmx, false) } } else { // Create pipes outr, outw, err = os.Pipe() if err != nil { err = fmt.Errorf("Pipe stdout error: " + err.Error()) goto exitErr } errr, errw, err = os.Pipe() if err != nil { err = fmt.Errorf("Pipe stderr error: " + err.Error()) goto exitErr } inr, inw, err = os.Pipe() if err != nil { err = fmt.Errorf("Pipe stdin error: " + err.Error()) goto exitErr } e.proc, err = os.StartProcess(bashArgs[0], bashArgs, &os.ProcAttr{ Files: []*os.File{inr, outw, errw}, Env: append(os.Environ(), e.Env...), }) if err != nil { err = fmt.Errorf("Process start error: " + err.Error()) goto exitErr } } go func() { stdoutDone := make(chan struct{}) if e.PtyMode { // Make sure to close the pty at the end. defer e.ptmx.Close() // Handle both stdout mixed with stderr go e.ptsPumpStdout(e.ptmx, stdoutDone) // Blocking function that poll input or wait for end of process e.pumpStdin(e.ptmx) } else { // Make sure to close all pipes defer outr.Close() defer outw.Close() defer errr.Close() defer errw.Close() defer inr.Close() defer inw.Close() // Handle stdout + stderr go e.pipePumpStdout(outr, stdoutDone) go e.pipePumpStderr(errr) // Blocking function that poll input or wait for end of process e.pumpStdin(inw) } if status, err := e.proc.Wait(); err == nil { // Other commands need a bonk on the head. if !status.Exited() { if err := e.proc.Signal(os.Interrupt); err != nil { e.logError("Proc interrupt:", err) } select { case <-stdoutDone: case <-time.After(time.Second): // A bigger bonk on the head. if err := e.proc.Signal(os.Kill); err != nil { e.logError("Proc term:", err) } <-stdoutDone } } } delete(cmdIDMap, e.CmdID) }() return nil exitErr: for _, pf := range []*os.File{outr, outw, errr, errw, inr, inw} { pf.Close() } return err } // TerminalSetSize Set terminal size func (e *ExecOverWS) TerminalSetSize(rows, cols uint16) error { if !e.PtyMode || e.ptmx == nil { return fmt.Errorf("PtyMode not set") } w, err := pty.GetsizeFull(e.ptmx) if err != nil { return err } return e.TerminalSetSizePos(rows, cols, w.X, w.Y) } // TerminalSetSizePos Set terminal size and position func (e *ExecOverWS) TerminalSetSizePos(rows, cols, x, y uint16) error { if !e.PtyMode || e.ptmx == nil { return fmt.Errorf("PtyMode not set") } winSz := pty.Winsize{Rows: rows, Cols: cols, X: x, Y: y} return pty.Setsize(e.ptmx, &winSz) } /** * Private functions **/ // terminalEcho Enable or disable echoing terminal input. // This is useful specifically for when users enter passwords. func (e *ExecOverWS) terminalEcho(ff *os.File, show bool) { var termios = &syscall.Termios{} fd := ff.Fd() if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, syscall.TCGETS, uintptr(unsafe.Pointer(termios))); err != 0 { return } if show { termios.Lflag |= syscall.ECHO } else { termios.Lflag &^= syscall.ECHO } if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TCSETS), uintptr(unsafe.Pointer(termios))); err != 0 { return } } func (e *ExecOverWS) logDebug(format string, a ...interface{}) { if e.Log != nil { e.Log.Debugf(format, a...) } } func (e *ExecOverWS) logError(format string, a ...interface{}) { if e.Log != nil { e.Log.Errorf(format, a...) } }