From 03cfabd9b6f3df4361b463f84901e4f56f4c6699 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 19 Aug 2022 13:23:00 -0700 Subject: [PATCH] convert ptyout files to CF files (fixed size circular buffer files). connect to remotes with their own controlling terminal and capture that terminal output. POC to send password to controlling terminal to login. --- cmd/main-server.go | 35 +++---------- db/migrations/000001_init.up.sql | 1 + pkg/remote/remote.go | 84 ++++++++++++++++++++++++++++---- pkg/scbase/scbase.go | 4 +- pkg/sstore/dbops.go | 4 +- pkg/sstore/fileops.go | 45 +++++++++++------ pkg/sstore/sstore.go | 53 ++++++++++++++++++-- 7 files changed, 167 insertions(+), 59 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index cfc23754d..e723e6895 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "io" "io/fs" "net/http" "os" @@ -221,11 +220,6 @@ func HandleGetWindow(w http.ResponseWriter, r *http.Request) { return } -func GetPtyOutFile(sessionId string, cmdId string) string { - pathStr := fmt.Sprintf("/Users/mike/scripthaus/.sessions/%s/%s.ptyout", sessionId, cmdId) - return pathStr -} - func HandleGetPtyOut(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) w.Header().Set("Access-Control-Allow-Credentials", "true") @@ -249,25 +243,18 @@ func HandleGetPtyOut(w http.ResponseWriter, r *http.Request) { w.Write([]byte(fmt.Sprintf("invalid cmdid: %v", err))) return } - pathStr, err := scbase.PtyOutFile(sessionId, cmdId) - if err != nil { - w.WriteHeader(500) - w.Write([]byte(fmt.Sprintf("cannot get ptyout file name: %v", err))) - return - } - fd, err := os.Open(pathStr) + _, data, err := sstore.ReadFullPtyOutFile(r.Context(), sessionId, cmdId) if err != nil { if errors.Is(err, fs.ErrNotExist) { w.WriteHeader(http.StatusOK) return } w.WriteHeader(500) - w.Write([]byte(fmt.Sprintf("cannot open file '%s': %v", pathStr, err))) + w.Write([]byte(fmt.Sprintf("error reading ptyout file: %v", err))) return } - defer fd.Close() w.WriteHeader(http.StatusOK) - io.Copy(w, fd) + w.Write(data) } func WriteJsonError(w http.ResponseWriter, errVal error) { @@ -398,17 +385,6 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { // cmd-type = comment // cmd-type = command, commandid=ABC -// how to know if command is still executing? is command done? - -// local -- .ptyout, .stdin -// remote -- transfer controller program -// controller-startcmd -- start command (with options) => returns cmdid -// controller-watchsession [sessionid] -// transfer [cmdid:pos] pairs. streams back anything new written to ptyout on stdout -// stdin-packet [cmdid:user:data] -// startcmd will figure out the correct -// - func runWebSocketServer() { gr := mux.NewRouter() gr.HandleFunc("/ws", HandleWs) @@ -456,6 +432,11 @@ func main() { fmt.Printf("[error] ensuring test01 remote: %v\n", err) return } + err = sstore.AddTest02Remote(context.Background()) + if err != nil { + fmt.Printf("[error] ensuring test02 remote: %v\n", err) + return + } _, err = sstore.EnsureDefaultSession(context.Background()) if err != nil { fmt.Printf("[error] ensuring default session: %v\n", err) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 4854b7bf3..eedc7bf41 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -81,6 +81,7 @@ CREATE TABLE remote ( autoconnect boolean NOT NULL, initpk json NOT NULL, sshopts json NOT NULL, + remoteopts json NOT NULL, lastconnectts bigint NOT NULL ); diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index e993fedcf..3bd5ac215 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -5,10 +5,16 @@ import ( "encoding/base64" "errors" "fmt" + "io" + "os" + "os/exec" "path" "strings" "sync" + "syscall" + "time" + "github.com/creack/pty" "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/shexec" @@ -19,6 +25,7 @@ const RemoteTypeMShell = "mshell" const DefaultTermRows = 25 const DefaultTermCols = 80 const DefaultTerm = "xterm-256color" +const DefaultMaxPtySize = 1024 * 1024 const MShellServerCommand = ` PATH=$PATH:~/.mshell; @@ -50,11 +57,13 @@ type RemoteState struct { RemoteType string `json:"remotetype"` RemoteId string `json:"remoteid"` PhysicalId string `json:"physicalremoteid"` - RemoteAlias string `json:"remotealias"` + RemoteAlias string `json:"remotealias,omitempty"` RemoteCanonicalName string `json:"remotecanonicalname"` RemoteVars map[string]string `json:"remotevars"` Status string `json:"status"` + ErrorStr string `json:"errorstr,omitempty"` DefaultState *sstore.RemoteState `json:"defaultstate"` + AutoConnect bool `json:"autoconnect"` } type MShellProc struct { @@ -62,10 +71,11 @@ type MShellProc struct { Remote *sstore.RemoteType // runtime - Status string - ServerProc *shexec.ClientProc - UName string - Err error + Status string + ServerProc *shexec.ClientProc + UName string + Err error + ControllingPty *os.File RunningCmds []base.CommandKey } @@ -119,6 +129,10 @@ func GetAllRemoteState() []RemoteState { RemoteCanonicalName: proc.Remote.RemoteCanonicalName, PhysicalId: proc.Remote.PhysicalId, Status: proc.Status, + AutoConnect: proc.Remote.AutoConnect, + } + if proc.Err != nil { + state.ErrorStr = proc.Err.Error() } vars := make(map[string]string) vars["user"] = proc.Remote.RemoteUser @@ -179,17 +193,70 @@ func convertSSHOpts(opts *sstore.SSHOpts) shexec.SSHOpts { } } +func (msh *MShellProc) addControllingTty(ecmd *exec.Cmd) error { + cmdPty, cmdTty, err := pty.Open() + if err != nil { + return err + } + msh.ControllingPty = cmdPty + ecmd.ExtraFiles = append(ecmd.ExtraFiles, cmdTty) + if ecmd.SysProcAttr == nil { + ecmd.SysProcAttr = &syscall.SysProcAttr{} + } + ecmd.SysProcAttr.Setsid = true + ecmd.SysProcAttr.Setctty = true + ecmd.SysProcAttr.Ctty = len(ecmd.ExtraFiles) + 3 - 1 + return nil +} + func (msh *MShellProc) Launch() { msh.Lock.Lock() defer msh.Lock.Unlock() ecmd := convertSSHOpts(msh.Remote.SSHOpts).MakeSSHExecCmd(MShellServerCommand) + err := msh.addControllingTty(ecmd) + if err != nil { + msh.Status = StatusError + msh.Err = fmt.Errorf("cannot attach controlling tty to mshell command: %w", err) + return + } + defer func() { + if len(ecmd.ExtraFiles) > 0 { + ecmd.ExtraFiles[len(ecmd.ExtraFiles)-1].Close() + } + }() + remoteName := msh.Remote.GetName() + go func() { + fmt.Printf("[c-pty %s] starting...\n", msh.Remote.GetName()) + buf := make([]byte, 100) + for { + n, readErr := msh.ControllingPty.Read(buf) + if readErr == io.EOF { + break + } + if readErr != nil { + fmt.Printf("[error] read from controlling-pty [%s]: %v\n", remoteName, readErr) + break + } + readStr := string(buf[0:n]) + readStr = strings.ReplaceAll(readStr, "\r", "") + readStr = strings.ReplaceAll(readStr, "\n", "\\n") + fmt.Printf("[c-pty %s] %d '%s'\n", remoteName, n, readStr) + } + }() + if remoteName == "test2" { + go func() { + time.Sleep(2 * time.Second) + msh.ControllingPty.Write([]byte(Test2Pw)) + fmt.Printf("[c-pty %s] wrote password!\n", remoteName) + }() + } cproc, uname, err := shexec.MakeClientProc(ecmd) msh.UName = uname if err != nil { msh.Status = StatusError msh.Err = err - fmt.Printf("[error] connecting remote %s (%s): %w\n", msh.Remote.GetName(), msh.UName, err) + fmt.Printf("[error] connecting remote %s (%s): %v\n", msh.Remote.GetName(), msh.UName, err) return } fmt.Printf("connected remote %s\n", msh.Remote.GetName()) @@ -203,7 +270,6 @@ func (msh *MShellProc) Launch() { msh.Status = StatusDisconnected } }) - fmt.Printf("[error] RUNNER PROC EXITED code[%d]\n", exitCode) }() go msh.ProcessPackets() @@ -270,7 +336,7 @@ func (msh *MShellProc) SendInput(pk *packet.InputPacketType) error { } func makeTermOpts() sstore.TermOpts { - return sstore.TermOpts{Rows: DefaultTermRows, Cols: DefaultTermCols, FlexRows: true} + return sstore.TermOpts{Rows: DefaultTermRows, Cols: DefaultTermCols, FlexRows: true, MaxPtySize: DefaultMaxPtySize} } func RunCommand(ctx context.Context, cmdId string, remoteId string, remoteState *sstore.RemoteState, runPacket *packet.RunPacketType) (*sstore.CmdType, error) { @@ -318,7 +384,7 @@ func RunCommand(ctx context.Context, cmdId string, remoteId string, remoteState DonePk: nil, RunOut: nil, } - err = sstore.AppendToCmdPtyBlob(ctx, cmd.SessionId, cmd.CmdId, nil, 0) + err = sstore.CreateCmdPtyFile(ctx, cmd.SessionId, cmd.CmdId, cmd.TermOpts.MaxPtySize) if err != nil { return nil, err } diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index f30b53092..a64c39bdc 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -90,7 +90,7 @@ func PtyOutFile(sessionId string, cmdId string) (string, error) { if err != nil { return "", err } - return fmt.Sprintf("%s/%s.ptyout", sdir, cmdId), nil + return fmt.Sprintf("%s/%s.ptyout.cf", sdir, cmdId), nil } func RunOutFile(sessionId string, cmdId string) (string, error) { @@ -108,7 +108,7 @@ func RemotePtyOut(remoteId string) (string, error) { if err != nil { return "", err } - return fmt.Sprintf("%s/%s.ptyout", rdir, remoteId), nil + return fmt.Sprintf("%s/%s.ptyout.cf", rdir, remoteId), nil } type ScFileNameGenerator struct { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 14fde7c0e..954d6d08e 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -90,8 +90,8 @@ func InsertRemote(ctx context.Context, remote *RemoteType) error { if err != nil { return err } - query := `INSERT INTO remote ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, autoconnect, initpk, sshopts, lastconnectts) VALUES - (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:autoconnect,:initpk,:sshopts,:lastconnectts)` + query := `INSERT INTO remote ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, autoconnect, initpk, sshopts, remoteopts, lastconnectts) VALUES + (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:autoconnect,:initpk,:sshopts,:remoteopts,:lastconnectts)` _, err = db.NamedExec(query, remote.ToMap()) if err != nil { return err diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index ad89f4dee..e55002600 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -4,11 +4,23 @@ import ( "context" "encoding/base64" "fmt" - "os" + "github.com/scripthaus-dev/mshell/pkg/cirfile" "github.com/scripthaus-dev/sh2-server/pkg/scbase" ) +func CreateCmdPtyFile(ctx context.Context, sessionId string, cmdId string, maxSize int64) error { + ptyOutFileName, err := scbase.PtyOutFile(sessionId, cmdId) + if err != nil { + return err + } + f, err := cirfile.CreateCirFile(ptyOutFileName, maxSize) + if err != nil { + return err + } + return f.Close() +} + func AppendToCmdPtyBlob(ctx context.Context, sessionId string, cmdId string, data []byte, pos int64) error { if pos < 0 { return fmt.Errorf("invalid seek pos '%d' in AppendToCmdPtyBlob", pos) @@ -17,22 +29,12 @@ func AppendToCmdPtyBlob(ctx context.Context, sessionId string, cmdId string, dat if err != nil { return err } - fd, err := os.OpenFile(ptyOutFileName, os.O_WRONLY|os.O_CREATE, 0600) + f, err := cirfile.OpenCirFile(ptyOutFileName) if err != nil { return err } - realPos, err := fd.Seek(pos, 0) - if err != nil { - return err - } - if realPos != pos { - return fmt.Errorf("could not seek to pos:%d (realpos=%d)", pos, realPos) - } - defer fd.Close() - if len(data) == 0 { - return nil - } - _, err = fd.Write(data) + defer f.Close() + err = f.WriteAt(ctx, data, pos) if err != nil { return err } @@ -40,10 +42,23 @@ func AppendToCmdPtyBlob(ctx context.Context, sessionId string, cmdId string, dat update := &PtyDataUpdate{ SessionId: sessionId, CmdId: cmdId, - PtyPos: realPos, + PtyPos: pos, PtyData64: data64, PtyDataLen: int64(len(data)), } MainBus.SendUpdate(sessionId, update) return nil } + +func ReadFullPtyOutFile(ctx context.Context, sessionId string, cmdId string) (int64, []byte, error) { + ptyOutFileName, err := scbase.PtyOutFile(sessionId, cmdId) + if err != nil { + return 0, nil, err + } + f, err := cirfile.OpenCirFile(ptyOutFileName) + if err != nil { + return 0, nil, err + } + defer f.Close() + return f.ReadAll(ctx) +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 046569820..c189f8860 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -231,10 +231,10 @@ func (s RemoteState) Value() (driver.Value, error) { } type TermOpts struct { - Rows int64 `json:"rows"` - Cols int64 `json:"cols"` - FlexRows bool `json:"flexrows,omitempty"` - CmdSize int64 `json:"cmdsize,omitempty"` + Rows int64 `json:"rows"` + Cols int64 `json:"cols"` + FlexRows bool `json:"flexrows,omitempty"` + MaxPtySize int64 `json:"maxptysize,omitempty"` } func (opts *TermOpts) Scan(val interface{}) error { @@ -277,6 +277,18 @@ type SSHOpts struct { SSHUser string `json:"sshuser"` } +type RemoteOptsType struct { + Color string `json:"color"` +} + +func (opts *RemoteOptsType) Scan(val interface{}) error { + return quickScanJson(opts, val) +} + +func (opts RemoteOptsType) Value() (driver.Value, error) { + return quickValueJson(opts) +} + type RemoteType struct { RemoteId string `json:"remoteid"` PhysicalId string `json:"physicalid"` @@ -289,6 +301,7 @@ type RemoteType struct { AutoConnect bool `json:"autoconnect"` InitPk *packet.InitPacketType `json:"inipk"` SSHOpts *SSHOpts `json:"sshopts"` + RemoteOpts *RemoteOptsType `json:"remoteopts"` LastConnectTs int64 `json:"lastconnectts"` } @@ -330,6 +343,7 @@ func (r *RemoteType) ToMap() map[string]interface{} { rtn["autoconnect"] = r.AutoConnect rtn["initpk"] = quickJson(r.InitPk) rtn["sshopts"] = quickJson(r.SSHOpts) + rtn["remoteopts"] = quickJson(r.RemoteOpts) rtn["lastconnectts"] = r.LastConnectTs return rtn } @@ -350,6 +364,7 @@ func RemoteFromMap(m map[string]interface{}) *RemoteType { quickSetBool(&r.AutoConnect, m, "autoconnect") quickSetJson(&r.InitPk, m, "initpk") quickSetJson(&r.SSHOpts, m, "sshopts") + quickSetJson(&r.RemoteOpts, m, "remoteopts") quickSetInt64(&r.LastConnectTs, m, "lastconnectts") return &r } @@ -502,6 +517,36 @@ func AddTest01Remote(ctx context.Context) error { return nil } +func AddTest02Remote(ctx context.Context) error { + remote, err := GetRemoteByAlias(ctx, "test2") + if err != nil { + return fmt.Errorf("getting remote[test01] from db: %w", err) + } + if remote != nil { + return nil + } + testRemote := &RemoteType{ + RemoteId: uuid.New().String(), + RemoteType: "ssh", + RemoteAlias: "test2", + RemoteCanonicalName: "test2@test01.ec2", + RemoteSudo: false, + RemoteUser: "test2", + RemoteHost: "test01.ec2", + SSHOpts: &SSHOpts{ + SSHHost: "test01.ec2", + SSHUser: "test2", + }, + AutoConnect: true, + } + err = InsertRemote(ctx, testRemote) + if err != nil { + return err + } + log.Printf("[db] added remote '%s', id=%s\n", testRemote.GetName(), testRemote.RemoteId) + return nil +} + func EnsureDefaultSession(ctx context.Context) (*SessionType, error) { session, err := GetSessionByName(ctx, DefaultSessionName) if err != nil {