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.

This commit is contained in:
sawka 2022-08-19 13:23:00 -07:00
parent b142e350be
commit 03cfabd9b6
7 changed files with 167 additions and 59 deletions

View File

@ -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)

View File

@ -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
);

View File

@ -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
}

View File

@ -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 {

View File

@ -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

View File

@ -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)
}

View File

@ -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 {