gterm/gterm.go

363 lines
8.9 KiB
Go
Raw Normal View History

2021-11-16 10:50:52 +00:00
package gterm
import (
2021-11-16 15:57:32 +00:00
"bytes"
_ "embed"
"encoding/json"
"fmt"
2021-11-16 10:50:52 +00:00
"net/http"
2021-11-16 15:57:32 +00:00
"os"
"os/exec"
"strings"
"time"
2021-11-16 10:50:52 +00:00
2021-11-16 15:57:32 +00:00
"github.com/creack/pty"
"github.com/gorilla/websocket"
2021-11-19 09:19:50 +00:00
"github.com/rs/zerolog/log"
2021-11-16 10:50:52 +00:00
"kumoly.io/lib/ksrv"
2021-11-16 15:57:32 +00:00
"kumoly.io/lib/ksrv/engine"
"kumoly.io/lib/xorencrypt"
2021-11-16 15:57:32 +00:00
"kumoly.io/tools/gterm/public"
2021-11-16 10:50:52 +00:00
)
2021-11-16 18:04:21 +00:00
//go:embed public/index.html
2021-11-16 15:57:32 +00:00
var index string
2021-11-16 10:50:52 +00:00
2021-11-17 16:10:35 +00:00
//go:embed profile
var BashProfile string
2021-11-16 15:57:32 +00:00
var tmpl *engine.Engine
var servePublic = http.FileServer(http.FS(public.FS))
2021-11-16 10:50:52 +00:00
2021-11-16 15:57:32 +00:00
var pool map[string]chan struct{}
2021-11-24 15:02:50 +00:00
var locks map[string]chan struct{}
2021-11-16 10:50:52 +00:00
func init() {
2021-11-16 15:57:32 +00:00
tmpl = engine.Must(engine.New("").Parse(index))
pool = map[string]chan struct{}{}
2021-11-24 15:02:50 +00:00
locks = map[string]chan struct{}{}
2021-11-16 15:57:32 +00:00
}
type GTerm struct {
AppName string
2021-11-16 10:50:52 +00:00
2021-11-16 15:57:32 +00:00
// Arguments is a list of strings to pass as arguments to the specified Command
Args []string
// Command is the path to the binary we should create a TTY for
Cmd string
2021-11-18 02:49:15 +00:00
// Dir working dir
Dir string
2021-11-16 15:57:32 +00:00
// Envs env pairs to pass to the command
Envs []string
// ErrorLimit defines the number of consecutive errors that can happen
// before a connection is considered unusable
ErrorLimit int
// Timeout defines the maximum duration between which a ping and pong
// cycle should be tolerated, beyond this the connection should be deemed dead
Timeout time.Duration
BufferSize int
Salt string
2021-11-16 10:50:52 +00:00
}
func New() *GTerm {
2021-11-16 15:57:32 +00:00
// if g.Cmd == "" {
// g.Cmd = "bash"
// }
// if len(g.Envs) == 0 {
// g.Envs = os.Environ()
// }
// if g.Timeout < time.Second {
// g.Timeout = 20 * time.Second
// }
// if g.ErrorLimit == 0 {
// g.ErrorLimit = 10
// }
// if g.BufferSize == 0 {
// g.BufferSize = 512
// }
2021-11-16 18:04:21 +00:00
go func() {
//print pool
for {
cons := []string{}
for k := range pool {
cons = append(cons, k)
}
2021-11-19 09:19:50 +00:00
log.Info().Interface("connections", cons).Msg("")
2021-11-16 18:04:21 +00:00
time.Sleep(5 * time.Minute)
}
}()
2021-11-16 15:57:32 +00:00
return &GTerm{
AppName: "GTERM",
Cmd: "bash",
2021-11-16 18:04:21 +00:00
Envs: append(os.Environ(), "TERM=xterm-256color"),
2021-11-16 15:57:32 +00:00
Timeout: 20 * time.Second,
ErrorLimit: 10,
BufferSize: 512,
}
2021-11-16 10:50:52 +00:00
}
func (g *GTerm) ServeHTTP(w http.ResponseWriter, r *http.Request) {
2021-11-16 15:57:32 +00:00
if strings.HasPrefix(r.URL.String(), "/ws") {
g.WS(w, r)
} else {
g.App(w, r)
}
}
type App struct {
AppName string
}
2021-11-16 10:50:52 +00:00
2021-11-16 15:57:32 +00:00
func (g *GTerm) App(w http.ResponseWriter, r *http.Request) {
2021-11-17 08:05:20 +00:00
if r.URL.Path == "/" {
2021-11-16 15:57:32 +00:00
tmpl.Execute(w, App{g.AppName})
return
}
file, err := public.FS.Open(strings.TrimPrefix(r.URL.String(), "/"))
if err != nil {
// tmpl.Execute(w, App{g.AppName})
http.NotFound(w, r)
return
}
stat, err := file.Stat()
if err != nil || stat.IsDir() {
// tmpl.Execute(w, App{g.AppName})
http.NotFound(w, r)
return
}
servePublic.ServeHTTP(w, r)
2021-11-16 10:50:52 +00:00
}
type NewCmdRequest struct {
2021-11-24 15:02:50 +00:00
Cmd string `json:"cmd"`
Envs []string `json:"envs"`
Args []string `json:"args"`
Dir string `json:"dir"`
Block bool `json:"block"`
}
func (g *GTerm) defaultCmd() *exec.Cmd {
cmd := exec.Command(g.Cmd, g.Args...)
cmd.Env = g.Envs
if g.Dir != "" && g.Dir != "." {
cmd.Dir = g.Dir
}
return cmd
}
2021-11-24 15:02:50 +00:00
func (g *GTerm) echo(msg string) *exec.Cmd {
cmd := exec.Command("echo", msg)
cmd.Env = g.Envs
return cmd
}
2021-11-16 15:57:32 +00:00
func (g *GTerm) WS(w http.ResponseWriter, r *http.Request) {
2021-11-16 18:04:21 +00:00
id := fmt.Sprintf("[%02d] %s", ctr(), ksrv.GetIP(r))
2021-11-16 15:57:32 +00:00
pool[id] = make(chan struct{}, 1)
defer func() {
close(pool[id])
delete(pool, id)
}()
2021-11-19 09:19:50 +00:00
l := log.With().Str("id", id).Logger()
var cmd *exec.Cmd
2021-11-19 07:47:47 +00:00
if newCmd := r.URL.Query().Get("cmd"); newCmd != "" {
param, _ := xorencrypt.Decrypt(newCmd, g.Salt)
req := &NewCmdRequest{}
err := json.Unmarshal([]byte(param), req)
if err != nil {
2021-11-19 09:19:50 +00:00
l.Error().Err(err).Str("base", newCmd).Str("decrypt", param).Msg("cmd decode failed")
2021-11-24 15:02:50 +00:00
cmd = g.echo("cmd decode failed")
} else {
cmd = exec.Command(req.Cmd, req.Args...)
2021-11-18 07:32:50 +00:00
cmd.Env = append(req.Envs, "TERM=xterm-256color")
cmd.Dir = req.Dir
2021-11-24 15:02:50 +00:00
if req.Block {
lock, ok := locks[newCmd]
if !ok {
locks[newCmd] = make(chan struct{}, 1)
}
if len(lock) == 1 {
cmd = g.echo("cmd already running")
} else {
l.Info().Interface("cmd", req).Msg("starting cmd")
locks[newCmd] <- struct{}{}
defer func() {
<-locks[newCmd]
close(locks[newCmd])
delete(locks, newCmd)
}()
}
}
}
} else {
cmd = g.defaultCmd()
}
2021-11-16 15:57:32 +00:00
upgrader := websocket.Upgrader{
HandshakeTimeout: 0,
ReadBufferSize: g.BufferSize,
WriteBufferSize: g.BufferSize,
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
2021-11-19 09:19:50 +00:00
l.Error().Err(err).Msg("upgrade error")
2021-11-16 15:57:32 +00:00
return
}
2021-11-19 09:19:50 +00:00
l.Info().Msg("connection established.")
2021-11-16 15:57:32 +00:00
tty, err := pty.Start(cmd)
if err != nil {
2021-11-19 09:19:50 +00:00
l.Error().Err(err).Msg("start tty error")
2021-11-16 15:57:32 +00:00
conn.WriteMessage(websocket.TextMessage, []byte(err.Error()))
}
defer func() {
if err := cmd.Process.Kill(); err != nil {
2021-11-19 09:19:50 +00:00
l.Error().Err(err).Msg("proccess kill error")
2021-11-16 15:57:32 +00:00
}
if _, err := cmd.Process.Wait(); err != nil {
2021-11-19 09:19:50 +00:00
l.Error().Err(err).Msg("proccess wait error")
2021-11-16 15:57:32 +00:00
}
if err := tty.Close(); err != nil {
2021-11-19 09:19:50 +00:00
l.Error().Err(err).Msg("tty close error")
2021-11-16 15:57:32 +00:00
}
if err := conn.Close(); err != nil {
2021-11-19 09:19:50 +00:00
l.Error().Err(err).Msg("conn close error")
2021-11-16 15:57:32 +00:00
}
}()
var connectionClosed bool
2021-11-17 04:28:47 +00:00
waiter := make(chan struct{}, 1)
2021-11-16 15:57:32 +00:00
// this is a keep-alive loop that ensures connection does not hang-up itself
lastPongTime := time.Now()
conn.SetPongHandler(func(msg string) error {
lastPongTime = time.Now()
return nil
})
go func() {
for {
if err := conn.WriteMessage(websocket.PingMessage, []byte("keepalive")); err != nil {
2021-11-19 09:19:50 +00:00
l.Warn().Msg("failed to write ping message")
2021-11-16 15:57:32 +00:00
return
}
time.Sleep(g.Timeout / 2)
if time.Since(lastPongTime) > g.Timeout {
2021-11-19 09:19:50 +00:00
l.Warn().Msg("failed to get response from ping, triggering disconnect now...")
2021-11-17 04:28:47 +00:00
waiter <- struct{}{}
2021-11-16 15:57:32 +00:00
return
}
2021-11-19 09:19:50 +00:00
l.Debug().Msg("received response from ping successfully")
2021-11-16 15:57:32 +00:00
}
}()
// tty >> xterm.js
go func() {
errorCounter := 0
for {
// consider the connection closed/errored out so that the socket handler
// can be terminated - this frees up memory so the service doesn't get
// overloaded
if errorCounter > g.ErrorLimit {
2021-11-17 04:28:47 +00:00
waiter <- struct{}{}
2021-11-16 15:57:32 +00:00
break
}
buffer := make([]byte, g.BufferSize)
readLength, err := tty.Read(buffer)
if err != nil {
2021-11-19 09:19:50 +00:00
l.Warn().Err(err).Msg("failed to read from tty")
2021-11-16 15:57:32 +00:00
if err := conn.WriteMessage(websocket.TextMessage, []byte("bye!")); err != nil {
2021-11-19 09:19:50 +00:00
l.Warn().Err(err).Msg("failed to send termination message from tty to xterm.js")
2021-11-16 15:57:32 +00:00
}
2021-11-17 04:28:47 +00:00
waiter <- struct{}{}
2021-11-16 15:57:32 +00:00
return
}
if err := conn.WriteMessage(websocket.BinaryMessage, buffer[:readLength]); err != nil {
2021-11-19 09:19:50 +00:00
l.Warn().Msgf("failed to send %s bytes from tty to xterm.js", readLength)
2021-11-16 15:57:32 +00:00
errorCounter++
continue
}
2021-11-19 09:19:50 +00:00
l.Debug().Msgf("sent message of size %s bytes from tty to xterm.js", readLength)
2021-11-16 15:57:32 +00:00
errorCounter = 0
}
}()
// tty << xterm.js
go func() {
for {
// data processing
messageType, data, err := conn.ReadMessage()
if err != nil {
if !connectionClosed {
2021-11-19 09:19:50 +00:00
l.Warn().Err(err).Msg("failed to get next reader")
2021-11-16 15:57:32 +00:00
}
return
}
dataLength := len(data)
dataBuffer := bytes.Trim(data, "\x00")
dataType, ok := WebsocketMessageType[messageType]
if !ok {
dataType = "uunknown"
}
2021-11-19 09:19:50 +00:00
l.Debug().Msgf("received %s (type: %v) message of size %v byte(s) from xterm.js with key sequence: %v", dataType, messageType, dataLength, dataBuffer)
2021-11-16 15:57:32 +00:00
// process
if dataLength == -1 { // invalid
2021-11-19 09:19:50 +00:00
l.Warn().Msg("failed to get the correct number of bytes read, ignoring message")
2021-11-16 15:57:32 +00:00
continue
}
// handle resizing
if messageType == websocket.BinaryMessage {
if dataBuffer[0] == 1 {
ttySize := &TTYSize{}
resizeMessage := bytes.Trim(dataBuffer[1:], " \n\r\t\x00\x01")
if err := json.Unmarshal(resizeMessage, ttySize); err != nil {
2021-11-19 09:19:50 +00:00
l.Warn().Err(err).Msgf("failed to unmarshal received resize message '%s'", string(resizeMessage))
2021-11-16 15:57:32 +00:00
continue
}
2021-11-19 09:19:50 +00:00
l.Debug().Int("rows", int(ttySize.Rows)).Int("columns", int(ttySize.Cols)).Msg("resizing tty...")
2021-11-16 15:57:32 +00:00
if err := pty.Setsize(tty, &pty.Winsize{
Rows: ttySize.Rows,
Cols: ttySize.Cols,
}); err != nil {
2021-11-19 09:19:50 +00:00
l.Warn().Err(err).Msg("failed to resize tty")
2021-11-16 15:57:32 +00:00
}
continue
}
}
// write to tty
bytesWritten, err := tty.Write(dataBuffer)
if err != nil {
2021-11-19 09:19:50 +00:00
l.Warn().Err(err).Msgf("failed to write %v bytes to tty", len(dataBuffer))
2021-11-16 15:57:32 +00:00
continue
}
2021-11-19 09:19:50 +00:00
l.Debug().Msgf("%d bytes written to tty...", bytesWritten)
2021-11-16 15:57:32 +00:00
}
}()
2021-11-17 04:28:47 +00:00
<-waiter
close(waiter)
2021-11-19 09:19:50 +00:00
l.Info().Msg("closing connection...")
2021-11-16 15:57:32 +00:00
connectionClosed = true
}
var WebsocketMessageType = map[int]string{
websocket.BinaryMessage: "binary",
websocket.TextMessage: "text",
websocket.CloseMessage: "close",
websocket.PingMessage: "ping",
websocket.PongMessage: "pong",
}
// TTYSize represents a JSON structure to be sent by the frontend
// xterm.js implementation to the xterm.js websocket handler
type TTYSize struct {
Cols uint16 `json:"cols"`
Rows uint16 `json:"rows"`
X uint16 `json:"x"`
Y uint16 `json:"y"`
}