diff --git a/cmd/main-server.go b/cmd/main-server.go index cfc23754..e723e689 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 4854b7bf..eedc7bf4 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 e993fedc..3bd5ac21 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 f30b5309..a64c39bd 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 14fde7c0..954d6d08 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 ad89f4de..e5500260 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 04656982..c189f886 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 {