mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-31 18:18:02 +01:00
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:
parent
b142e350be
commit
03cfabd9b6
@ -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)
|
||||
|
@ -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
|
||||
);
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user