2022-06-12 22:39:48 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2022-07-01 21:17:19 +02:00
|
|
|
"context"
|
2022-06-12 22:39:48 +02:00
|
|
|
"encoding/json"
|
2022-06-17 07:22:47 +02:00
|
|
|
"errors"
|
2022-06-12 22:39:48 +02:00
|
|
|
"fmt"
|
|
|
|
"io"
|
2022-06-17 07:22:47 +02:00
|
|
|
"io/fs"
|
2022-06-12 22:39:48 +02:00
|
|
|
"net/http"
|
|
|
|
"os"
|
2022-06-13 20:11:56 +02:00
|
|
|
"strings"
|
2022-06-15 07:39:09 +02:00
|
|
|
"sync"
|
2022-06-12 22:39:48 +02:00
|
|
|
"time"
|
|
|
|
|
2022-06-13 20:11:56 +02:00
|
|
|
"github.com/google/uuid"
|
2022-06-12 22:39:48 +02:00
|
|
|
"github.com/gorilla/mux"
|
|
|
|
|
2022-06-23 19:15:56 +02:00
|
|
|
"github.com/scripthaus-dev/mshell/pkg/base"
|
|
|
|
"github.com/scripthaus-dev/mshell/pkg/cmdtail"
|
|
|
|
"github.com/scripthaus-dev/mshell/pkg/packet"
|
2022-07-01 19:48:14 +02:00
|
|
|
"github.com/scripthaus-dev/sh2-server/pkg/remote"
|
2022-06-13 20:11:56 +02:00
|
|
|
"github.com/scripthaus-dev/sh2-server/pkg/sstore"
|
2022-06-12 22:39:48 +02:00
|
|
|
"github.com/scripthaus-dev/sh2-server/pkg/wsshell"
|
|
|
|
)
|
|
|
|
|
|
|
|
const HttpReadTimeout = 5 * time.Second
|
|
|
|
const HttpWriteTimeout = 21 * time.Second
|
|
|
|
const HttpMaxHeaderBytes = 60000
|
|
|
|
const HttpTimeoutDuration = 21 * time.Second
|
|
|
|
|
2022-06-16 03:13:16 +02:00
|
|
|
const WebSocketServerAddr = "localhost:8081"
|
|
|
|
const MainServerAddr = "localhost:8080"
|
2022-06-17 07:22:47 +02:00
|
|
|
const WSStateReconnectTime = 30 * time.Second
|
|
|
|
const WSStatePacketChSize = 20
|
2022-06-16 03:13:16 +02:00
|
|
|
|
2022-06-18 00:30:42 +02:00
|
|
|
const MaxInputDataSize = 1000
|
|
|
|
|
2022-07-01 19:48:14 +02:00
|
|
|
var GlobalMShellProc *remote.MShellProc
|
2022-06-17 07:22:47 +02:00
|
|
|
var GlobalLock = &sync.Mutex{}
|
|
|
|
var WSStateMap = make(map[string]*WSState) // clientid -> WsState
|
2022-06-12 22:39:48 +02:00
|
|
|
|
2022-06-17 07:22:47 +02:00
|
|
|
func setWSState(state *WSState) {
|
|
|
|
GlobalLock.Lock()
|
|
|
|
defer GlobalLock.Unlock()
|
|
|
|
WSStateMap[state.ClientId] = state
|
2022-06-12 22:39:48 +02:00
|
|
|
}
|
|
|
|
|
2022-06-17 07:22:47 +02:00
|
|
|
func getWSState(clientId string) *WSState {
|
|
|
|
GlobalLock.Lock()
|
|
|
|
defer GlobalLock.Unlock()
|
|
|
|
return WSStateMap[clientId]
|
|
|
|
}
|
|
|
|
|
|
|
|
func removeWSStateAfterTimeout(clientId string, connectTime time.Time, waitDuration time.Duration) {
|
|
|
|
go func() {
|
|
|
|
time.Sleep(waitDuration)
|
|
|
|
GlobalLock.Lock()
|
|
|
|
defer GlobalLock.Unlock()
|
|
|
|
state := WSStateMap[clientId]
|
|
|
|
if state == nil || state.ConnectTime != connectTime {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
delete(WSStateMap, clientId)
|
|
|
|
err := state.CloseTailer()
|
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("[error] closing tailer on ws %v\n", err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
type WSState struct {
|
|
|
|
Lock *sync.Mutex
|
|
|
|
ClientId string
|
|
|
|
ConnectTime time.Time
|
|
|
|
Shell *wsshell.WSShell
|
|
|
|
Tailer *cmdtail.Tailer
|
|
|
|
PacketCh chan packet.PacketType
|
|
|
|
}
|
|
|
|
|
|
|
|
func MakeWSState(clientId string) (*WSState, error) {
|
|
|
|
var err error
|
|
|
|
rtn := &WSState{}
|
|
|
|
rtn.Lock = &sync.Mutex{}
|
|
|
|
rtn.ClientId = clientId
|
|
|
|
rtn.ConnectTime = time.Now()
|
|
|
|
rtn.PacketCh = make(chan packet.PacketType, WSStatePacketChSize)
|
2022-07-01 02:02:19 +02:00
|
|
|
chSender := packet.MakeChannelPacketSender(rtn.PacketCh)
|
|
|
|
rtn.Tailer, err = cmdtail.MakeTailer(chSender)
|
2022-06-17 07:22:47 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
go func() {
|
|
|
|
defer close(rtn.PacketCh)
|
|
|
|
rtn.Tailer.Run()
|
|
|
|
}()
|
|
|
|
go rtn.runTailerToWS()
|
|
|
|
return rtn, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ws *WSState) CloseTailer() error {
|
|
|
|
return ws.Tailer.Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ws *WSState) getShell() *wsshell.WSShell {
|
|
|
|
ws.Lock.Lock()
|
|
|
|
defer ws.Lock.Unlock()
|
|
|
|
return ws.Shell
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ws *WSState) runTailerToWS() {
|
|
|
|
for pk := range ws.PacketCh {
|
|
|
|
if pk.GetType() == "cmddata" {
|
|
|
|
dataPacket := pk.(*packet.CmdDataPacketType)
|
|
|
|
err := ws.writePacket(dataPacket)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("[error] writing packet to ws: %v\n", err)
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
fmt.Printf("tailer-to-ws, bad packet %v\n", pk.GetType())
|
|
|
|
}
|
2022-06-12 22:39:48 +02:00
|
|
|
}
|
|
|
|
|
2022-06-17 07:22:47 +02:00
|
|
|
func (ws *WSState) writePacket(pk packet.PacketType) error {
|
|
|
|
shell := ws.getShell()
|
|
|
|
if shell == nil || shell.IsClosed() {
|
|
|
|
return fmt.Errorf("cannot write packet, empty or closed wsshell")
|
|
|
|
}
|
|
|
|
err := shell.WriteJson(pk)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ws *WSState) getConnectTime() time.Time {
|
|
|
|
ws.Lock.Lock()
|
|
|
|
defer ws.Lock.Unlock()
|
|
|
|
return ws.ConnectTime
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ws *WSState) updateConnectTime() {
|
|
|
|
ws.Lock.Lock()
|
|
|
|
defer ws.Lock.Unlock()
|
|
|
|
ws.ConnectTime = time.Now()
|
2022-06-15 07:39:09 +02:00
|
|
|
}
|
2022-06-12 22:39:48 +02:00
|
|
|
|
2022-06-17 07:22:47 +02:00
|
|
|
func (ws *WSState) replaceExistingShell(shell *wsshell.WSShell) {
|
|
|
|
ws.Lock.Lock()
|
|
|
|
defer ws.Lock.Unlock()
|
|
|
|
if ws.Shell == nil {
|
|
|
|
ws.Shell = shell
|
|
|
|
return
|
|
|
|
}
|
|
|
|
ws.Shell.Conn.Close()
|
|
|
|
ws.Shell = shell
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-06-12 22:39:48 +02:00
|
|
|
func HandleWs(w http.ResponseWriter, r *http.Request) {
|
|
|
|
shell, err := wsshell.StartWS(w, r)
|
|
|
|
if err != nil {
|
2022-06-16 03:13:16 +02:00
|
|
|
fmt.Printf("WebSocket Upgrade Failed %T: %v\n", w, err)
|
2022-06-12 22:39:48 +02:00
|
|
|
return
|
|
|
|
}
|
2022-06-17 07:22:47 +02:00
|
|
|
defer shell.Conn.Close()
|
|
|
|
clientId := r.URL.Query().Get("clientid")
|
|
|
|
if clientId == "" {
|
|
|
|
close(shell.WriteChan)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
state := getWSState(clientId)
|
|
|
|
if state == nil {
|
|
|
|
state, err = MakeWSState(clientId)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("cannot make wsstate: %v\n", err)
|
|
|
|
close(shell.WriteChan)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
state.replaceExistingShell(shell)
|
|
|
|
setWSState(state)
|
|
|
|
} else {
|
|
|
|
state.updateConnectTime()
|
|
|
|
state.replaceExistingShell(shell)
|
|
|
|
}
|
|
|
|
stateConnectTime := state.getConnectTime()
|
2022-06-15 07:39:09 +02:00
|
|
|
defer func() {
|
2022-06-17 07:22:47 +02:00
|
|
|
removeWSStateAfterTimeout(clientId, stateConnectTime, WSStateReconnectTime)
|
2022-06-12 22:39:48 +02:00
|
|
|
}()
|
2022-06-17 07:22:47 +02:00
|
|
|
shell.WriteJson(map[string]interface{}{"type": "hello"}) // let client know we accepted this connection, ignore error
|
|
|
|
fmt.Printf("WebSocket opened %s %s\n", shell.RemoteAddr, state.ClientId)
|
2022-06-16 10:09:56 +02:00
|
|
|
for msgBytes := range shell.ReadChan {
|
|
|
|
pk, err := packet.ParseJsonPacket(msgBytes)
|
2022-06-12 22:39:48 +02:00
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("error unmarshalling ws message: %v\n", err)
|
2022-06-16 10:09:56 +02:00
|
|
|
continue
|
2022-06-12 22:39:48 +02:00
|
|
|
}
|
2022-06-17 07:22:47 +02:00
|
|
|
if pk.GetType() == "getcmd" {
|
|
|
|
err = state.Tailer.AddWatch(pk.(*packet.GetCmdPacketType))
|
|
|
|
if err != nil {
|
2022-06-18 00:30:42 +02:00
|
|
|
fmt.Printf("[error] adding watch to tailer: %v\n", err)
|
2022-06-17 07:22:47 +02:00
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
2022-06-18 00:30:42 +02:00
|
|
|
if pk.GetType() == "input" {
|
|
|
|
go func() {
|
|
|
|
err = sendCmdInput(pk.(*packet.InputPacketType))
|
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("[error] sending command input: %v\n", err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
continue
|
|
|
|
}
|
2022-06-17 07:22:47 +02:00
|
|
|
fmt.Printf("got ws bad message: %v\n", pk.GetType())
|
2022-06-12 22:39:48 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-18 00:30:42 +02:00
|
|
|
// todo: sync multiple writes to the same fifoName into a single go-routine and do liveness checking on fifo
|
|
|
|
// if this returns an error, likely the fifo is dead and the cmd should be marked as 'done'
|
|
|
|
func writeToFifo(fifoName string, data []byte) error {
|
|
|
|
rwfd, err := os.OpenFile(fifoName, os.O_RDWR, 0600)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer rwfd.Close()
|
|
|
|
fifoWriter, err := os.OpenFile(fifoName, os.O_WRONLY, 0600) // blocking open (open won't block because of rwfd)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer fifoWriter.Close()
|
|
|
|
// this *could* block if the fifo buffer is full
|
|
|
|
// unlikely because if the reader is dead, and len(data) < pipe size, then the buffer will be empty and will clear after rwfd is closed
|
|
|
|
_, err = fifoWriter.Write(data)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func sendCmdInput(pk *packet.InputPacketType) error {
|
2022-07-01 02:02:19 +02:00
|
|
|
err := pk.CK.Validate("input packet")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2022-06-18 00:30:42 +02:00
|
|
|
}
|
|
|
|
if len(pk.InputData) > MaxInputDataSize {
|
|
|
|
return fmt.Errorf("input data size too large, len=%d (max=%d)", len(pk.InputData), MaxInputDataSize)
|
|
|
|
}
|
2022-07-01 02:02:19 +02:00
|
|
|
fileNames, err := base.GetCommandFileNames(pk.CK)
|
2022-06-18 00:30:42 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
err = writeToFifo(fileNames.StdinFifo, []byte(pk.InputData))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-06-12 22:39:48 +02:00
|
|
|
func GetPtyOutFile(sessionId string, cmdId string) string {
|
|
|
|
pathStr := fmt.Sprintf("/Users/mike/scripthaus/.sessions/%s/%s.ptyout", sessionId, cmdId)
|
|
|
|
return pathStr
|
|
|
|
}
|
|
|
|
|
|
|
|
func GetPtyOut(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")
|
|
|
|
w.Header().Set("Vary", "Origin")
|
|
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
|
|
qvals := r.URL.Query()
|
|
|
|
sessionId := qvals.Get("sessionid")
|
|
|
|
cmdId := qvals.Get("cmdid")
|
|
|
|
if sessionId == "" || cmdId == "" {
|
|
|
|
w.WriteHeader(500)
|
|
|
|
w.Write([]byte(fmt.Sprintf("must specify sessionid and cmdid")))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
pathStr := GetPtyOutFile(sessionId, cmdId)
|
|
|
|
fd, err := os.Open(pathStr)
|
|
|
|
if err != nil {
|
2022-06-17 07:22:47 +02:00
|
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
return
|
|
|
|
}
|
2022-06-12 22:39:48 +02:00
|
|
|
w.WriteHeader(500)
|
|
|
|
w.Write([]byte(fmt.Sprintf("cannot open file '%s': %v", pathStr, err)))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
io.Copy(w, fd)
|
|
|
|
}
|
|
|
|
|
|
|
|
func WriteJsonError(w http.ResponseWriter, errVal error) {
|
2022-06-13 20:11:56 +02:00
|
|
|
w.Header().Set("Content-Type", "application/json")
|
2022-06-12 22:39:48 +02:00
|
|
|
w.WriteHeader(500)
|
|
|
|
errMap := make(map[string]interface{})
|
|
|
|
errMap["error"] = errVal.Error()
|
|
|
|
barr, _ := json.Marshal(errMap)
|
|
|
|
w.Write(barr)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func WriteJsonSuccess(w http.ResponseWriter, data interface{}) {
|
2022-06-13 20:11:56 +02:00
|
|
|
w.Header().Set("Content-Type", "application/json")
|
2022-06-12 22:39:48 +02:00
|
|
|
rtnMap := make(map[string]interface{})
|
|
|
|
rtnMap["success"] = true
|
|
|
|
if data != nil {
|
|
|
|
rtnMap["data"] = data
|
|
|
|
}
|
|
|
|
barr, err := json.Marshal(rtnMap)
|
|
|
|
if err != nil {
|
|
|
|
WriteJsonError(w, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
w.WriteHeader(200)
|
|
|
|
w.Write(barr)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-06-13 20:11:56 +02:00
|
|
|
type runCommandParams struct {
|
|
|
|
SessionId string `json:"sessionid"`
|
2022-06-17 00:51:41 +02:00
|
|
|
WindowId string `json:"windowid"`
|
2022-06-13 20:11:56 +02:00
|
|
|
Command string `json:"command"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type runCommandResponse struct {
|
|
|
|
Line *sstore.LineType `json:"line"`
|
|
|
|
}
|
|
|
|
|
2022-06-12 22:39:48 +02:00
|
|
|
func HandleRunCommand(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")
|
|
|
|
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
|
|
|
w.Header().Set("Vary", "Origin")
|
|
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
|
|
if r.Method == "GET" || r.Method == "OPTIONS" {
|
|
|
|
w.WriteHeader(200)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
decoder := json.NewDecoder(r.Body)
|
|
|
|
var params runCommandParams
|
|
|
|
err := decoder.Decode(¶ms)
|
|
|
|
if err != nil {
|
|
|
|
WriteJsonError(w, fmt.Errorf("error decoding json: %w", err))
|
|
|
|
return
|
|
|
|
}
|
2022-06-13 20:11:56 +02:00
|
|
|
if _, err = uuid.Parse(params.SessionId); err != nil {
|
|
|
|
WriteJsonError(w, fmt.Errorf("invalid sessionid '%s': %w", params.SessionId, err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
commandStr := strings.TrimSpace(params.Command)
|
|
|
|
if commandStr == "" {
|
|
|
|
WriteJsonError(w, fmt.Errorf("invalid emtpty command"))
|
|
|
|
return
|
|
|
|
}
|
2022-06-21 06:57:23 +02:00
|
|
|
if strings.HasPrefix(commandStr, "/comment ") {
|
|
|
|
text := strings.TrimSpace(commandStr[9:])
|
|
|
|
rtnLine := sstore.MakeNewLineText(params.SessionId, params.WindowId, text)
|
|
|
|
WriteJsonSuccess(w, &runCommandResponse{Line: rtnLine})
|
|
|
|
return
|
|
|
|
}
|
2022-06-23 19:15:56 +02:00
|
|
|
if strings.HasPrefix(commandStr, "cd ") {
|
|
|
|
newDir := strings.TrimSpace(commandStr[3:])
|
|
|
|
cdPacket := packet.MakeCdPacket()
|
|
|
|
cdPacket.PacketId = uuid.New().String()
|
|
|
|
cdPacket.Dir = newDir
|
|
|
|
GlobalMShellProc.Input.SendPacket(cdPacket)
|
|
|
|
return
|
|
|
|
}
|
2022-06-21 06:57:23 +02:00
|
|
|
rtnLine := sstore.MakeNewLineCmd(params.SessionId, params.WindowId)
|
2022-07-01 19:48:14 +02:00
|
|
|
// rtnLine.CmdText = commandStr
|
2022-06-13 20:11:56 +02:00
|
|
|
runPacket := packet.MakeRunPacket()
|
2022-07-01 02:02:19 +02:00
|
|
|
runPacket.CK = base.MakeCommandKey(params.SessionId, rtnLine.CmdId)
|
2022-06-23 19:15:56 +02:00
|
|
|
runPacket.Cwd = ""
|
2022-06-13 20:11:56 +02:00
|
|
|
runPacket.Env = nil
|
|
|
|
runPacket.Command = commandStr
|
|
|
|
fmt.Printf("run-packet %v\n", runPacket)
|
|
|
|
WriteJsonSuccess(w, &runCommandResponse{Line: rtnLine})
|
|
|
|
go func() {
|
2022-06-23 19:15:56 +02:00
|
|
|
GlobalMShellProc.Input.SendPacket(runPacket)
|
|
|
|
if !GlobalMShellProc.Local {
|
2022-06-16 03:13:16 +02:00
|
|
|
getPacket := packet.MakeGetCmdPacket()
|
2022-07-01 02:02:19 +02:00
|
|
|
getPacket.CK = runPacket.CK
|
2022-06-16 03:13:16 +02:00
|
|
|
getPacket.Tail = true
|
2022-06-23 19:15:56 +02:00
|
|
|
GlobalMShellProc.Input.SendPacket(getPacket)
|
2022-06-16 03:13:16 +02:00
|
|
|
}
|
2022-06-13 20:11:56 +02:00
|
|
|
}()
|
2022-06-12 22:39:48 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// /api/start-session
|
|
|
|
// returns:
|
|
|
|
// * userid
|
|
|
|
// * sessionid
|
|
|
|
//
|
|
|
|
// /api/ptyout (pos=[position]) - returns contents of ptyout file
|
|
|
|
// params:
|
|
|
|
// * sessionid
|
|
|
|
// * cmdid
|
|
|
|
// * pos
|
|
|
|
// returns:
|
|
|
|
// * stream of ptyout file (text, utf-8)
|
|
|
|
//
|
|
|
|
// POST /api/run-command
|
|
|
|
// params
|
|
|
|
// * userid
|
|
|
|
// * sessionid
|
|
|
|
// returns
|
|
|
|
// * cmdid
|
|
|
|
//
|
|
|
|
// /api/refresh-session
|
|
|
|
// params
|
|
|
|
// * sessionid
|
|
|
|
// * start -- can be negative
|
|
|
|
// * numlines
|
|
|
|
// returns
|
|
|
|
// * permissions (readonly, comment, command)
|
|
|
|
// * lines
|
|
|
|
// * lineid
|
|
|
|
// * ts
|
|
|
|
// * userid
|
|
|
|
// * linetype
|
|
|
|
// * text
|
|
|
|
// * cmdid
|
|
|
|
|
|
|
|
// /ws
|
|
|
|
// ->watch-session:
|
|
|
|
// * sessionid
|
|
|
|
// ->watch:
|
|
|
|
// * sessionid
|
|
|
|
// * cmdid
|
|
|
|
// ->focus:
|
|
|
|
// * sessionid
|
|
|
|
// * cmdid
|
|
|
|
// ->input:
|
|
|
|
// * sessionid
|
|
|
|
// * cmdid
|
|
|
|
// * data
|
|
|
|
// ->signal:
|
|
|
|
// * sessionid
|
|
|
|
// * cmdid
|
|
|
|
// * data
|
|
|
|
// <-data:
|
|
|
|
// * sessionid
|
|
|
|
// * cmdid
|
|
|
|
// * pos
|
|
|
|
// * data
|
|
|
|
// <-session-data:
|
|
|
|
// * sessionid
|
|
|
|
// * line
|
|
|
|
|
|
|
|
// session-doc
|
|
|
|
// timestamp | user | cmd-type | data
|
|
|
|
// 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
|
|
|
|
//
|
|
|
|
|
2022-06-16 03:13:16 +02:00
|
|
|
func runWebSocketServer() {
|
|
|
|
gr := mux.NewRouter()
|
|
|
|
gr.HandleFunc("/ws", HandleWs)
|
|
|
|
server := &http.Server{
|
|
|
|
Addr: WebSocketServerAddr,
|
|
|
|
ReadTimeout: HttpReadTimeout,
|
|
|
|
WriteTimeout: HttpWriteTimeout,
|
|
|
|
MaxHeaderBytes: HttpMaxHeaderBytes,
|
|
|
|
Handler: gr,
|
|
|
|
}
|
|
|
|
server.SetKeepAlivesEnabled(false)
|
|
|
|
fmt.Printf("Running websocket server on %s\n", WebSocketServerAddr)
|
|
|
|
err := server.ListenAndServe()
|
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("[error] trying to run websocket server: %v\n", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-12 22:39:48 +02:00
|
|
|
func main() {
|
2022-07-01 19:48:14 +02:00
|
|
|
if len(os.Args) >= 2 && strings.HasPrefix(os.Args[1], "--migrate") {
|
|
|
|
err := sstore.MigrateCommandOpts(os.Args[1:])
|
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("[error] %v\n", err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
err := sstore.TryMigrateUp()
|
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("[error] %v\n", err)
|
|
|
|
return
|
|
|
|
}
|
2022-07-01 21:17:19 +02:00
|
|
|
numSessions, err := sstore.NumSessions(context.Background())
|
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("[error] getting num sessions: %v\n", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
err = sstore.EnsureLocalRemote(context.Background())
|
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("[error] ensuring local remote: %v\n", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
fmt.Printf("[db] sessions count=%d\n", numSessions)
|
|
|
|
if numSessions == 0 {
|
|
|
|
sstore.CreateInitialSession(context.Background())
|
|
|
|
}
|
|
|
|
return
|
|
|
|
|
2022-07-01 19:48:14 +02:00
|
|
|
runnerProc, err := remote.LaunchMShell()
|
2022-06-12 22:39:48 +02:00
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("error launching runner-proc: %v\n", err)
|
|
|
|
return
|
|
|
|
}
|
2022-06-23 19:15:56 +02:00
|
|
|
GlobalMShellProc = runnerProc
|
2022-06-15 07:39:09 +02:00
|
|
|
go runnerProc.ProcessPackets()
|
2022-06-12 22:39:48 +02:00
|
|
|
fmt.Printf("Started local runner pid[%d]\n", runnerProc.Cmd.Process.Pid)
|
2022-06-16 03:13:16 +02:00
|
|
|
go runWebSocketServer()
|
2022-06-12 22:39:48 +02:00
|
|
|
gr := mux.NewRouter()
|
|
|
|
gr.HandleFunc("/api/ptyout", GetPtyOut)
|
|
|
|
gr.HandleFunc("/api/run-command", HandleRunCommand).Methods("GET", "POST", "OPTIONS")
|
|
|
|
server := &http.Server{
|
2022-06-16 03:13:16 +02:00
|
|
|
Addr: MainServerAddr,
|
2022-06-12 22:39:48 +02:00
|
|
|
ReadTimeout: HttpReadTimeout,
|
|
|
|
WriteTimeout: HttpWriteTimeout,
|
|
|
|
MaxHeaderBytes: HttpMaxHeaderBytes,
|
|
|
|
Handler: http.TimeoutHandler(gr, HttpTimeoutDuration, "Timeout"),
|
|
|
|
}
|
|
|
|
server.SetKeepAlivesEnabled(false)
|
2022-06-16 03:13:16 +02:00
|
|
|
fmt.Printf("Running main server on %s\n", MainServerAddr)
|
2022-06-12 22:39:48 +02:00
|
|
|
err = server.ListenAndServe()
|
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("ERROR: %v\n", err)
|
|
|
|
}
|
|
|
|
}
|