2023-10-30 17:34:56 +08:00
|
|
|
|
package mcm
|
2022-08-13 19:31:16 +08:00
|
|
|
|
|
|
|
|
|
|
import (
|
2025-06-27 12:17:45 +08:00
|
|
|
|
"cmp"
|
2022-08-13 19:31:16 +08:00
|
|
|
|
"context"
|
2024-04-21 19:35:58 +08:00
|
|
|
|
"fmt"
|
2022-08-13 19:31:16 +08:00
|
|
|
|
"io"
|
2024-02-23 22:53:17 +08:00
|
|
|
|
"mayfly-go/pkg/errorx"
|
2023-09-02 17:24:18 +08:00
|
|
|
|
"mayfly-go/pkg/logx"
|
2024-03-21 17:15:52 +08:00
|
|
|
|
|
2025-06-27 12:17:45 +08:00
|
|
|
|
"github.com/spf13/cast"
|
2024-03-21 17:15:52 +08:00
|
|
|
|
|
2024-02-23 22:53:17 +08:00
|
|
|
|
"strings"
|
2022-08-13 19:31:16 +08:00
|
|
|
|
"time"
|
|
|
|
|
|
"unicode/utf8"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gorilla/websocket"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
Resize = 1
|
|
|
|
|
|
Data = 2
|
2022-08-19 21:42:26 +08:00
|
|
|
|
Ping = 3
|
2024-02-23 22:53:17 +08:00
|
|
|
|
|
|
|
|
|
|
MsgSplit = "|"
|
2022-08-13 19:31:16 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type TerminalSession struct {
|
|
|
|
|
|
ID string
|
|
|
|
|
|
wsConn *websocket.Conn
|
|
|
|
|
|
terminal *Terminal
|
2024-04-21 19:35:58 +08:00
|
|
|
|
handler *TerminalHandler
|
2022-08-29 21:43:24 +08:00
|
|
|
|
recorder *Recorder
|
2022-08-13 19:31:16 +08:00
|
|
|
|
ctx context.Context
|
|
|
|
|
|
cancel context.CancelFunc
|
|
|
|
|
|
dataChan chan rune
|
|
|
|
|
|
tick *time.Ticker
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-04-21 19:35:58 +08:00
|
|
|
|
type CreateTerminalSessionParam struct {
|
|
|
|
|
|
SessionId string
|
|
|
|
|
|
Cli *Cli
|
|
|
|
|
|
WsConn *websocket.Conn
|
|
|
|
|
|
Rows int
|
|
|
|
|
|
Cols int
|
|
|
|
|
|
Recorder *Recorder
|
|
|
|
|
|
LogCmd bool // 是否记录命令
|
|
|
|
|
|
CmdFilterFuncs []CmdFilterFunc // 命令过滤器
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func NewTerminalSession(param *CreateTerminalSessionParam) (*TerminalSession, error) {
|
|
|
|
|
|
sessionId, rows, cols := param.SessionId, param.Rows, param.Cols
|
|
|
|
|
|
cli, ws, recorder := param.Cli, param.WsConn, param.Recorder
|
|
|
|
|
|
|
2022-08-13 19:31:16 +08:00
|
|
|
|
terminal, err := NewTerminal(cli)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
err = terminal.RequestPty("xterm-256color", rows, cols)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
err = terminal.Shell()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-08-29 21:43:24 +08:00
|
|
|
|
if recorder != nil {
|
2023-12-06 13:17:50 +08:00
|
|
|
|
recorder.WriteHeader(rows-3, cols)
|
2022-08-29 21:43:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-04-21 19:35:58 +08:00
|
|
|
|
var handler *TerminalHandler
|
|
|
|
|
|
// 记录命令或者存在命令过滤器时,则创建对应的终端处理器
|
|
|
|
|
|
if param.LogCmd || param.CmdFilterFuncs != nil {
|
|
|
|
|
|
handler = &TerminalHandler{Parser: NewParser(120, 40), Filters: param.CmdFilterFuncs}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-08-13 19:31:16 +08:00
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
|
tick := time.NewTicker(time.Millisecond * time.Duration(60))
|
|
|
|
|
|
ts := &TerminalSession{
|
|
|
|
|
|
ID: sessionId,
|
|
|
|
|
|
wsConn: ws,
|
|
|
|
|
|
terminal: terminal,
|
2024-04-21 19:35:58 +08:00
|
|
|
|
handler: handler,
|
2022-08-29 21:43:24 +08:00
|
|
|
|
recorder: recorder,
|
2022-08-13 19:31:16 +08:00
|
|
|
|
ctx: ctx,
|
|
|
|
|
|
cancel: cancel,
|
|
|
|
|
|
dataChan: make(chan rune),
|
|
|
|
|
|
tick: tick,
|
|
|
|
|
|
}
|
2024-02-23 22:53:17 +08:00
|
|
|
|
|
|
|
|
|
|
// 清除终端内容
|
2024-04-21 19:35:58 +08:00
|
|
|
|
ts.WriteToWs("\033[2J\033[3J\033[1;1H")
|
2022-08-13 19:31:16 +08:00
|
|
|
|
return ts, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (r TerminalSession) Start() {
|
2024-03-21 17:15:52 +08:00
|
|
|
|
go r.readFromTerminal()
|
2022-08-13 19:31:16 +08:00
|
|
|
|
go r.writeToWebsocket()
|
|
|
|
|
|
r.receiveWsMsg()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (r TerminalSession) Stop() {
|
2023-09-02 17:24:18 +08:00
|
|
|
|
logx.Debug("close machine ssh terminal session")
|
2022-08-13 19:31:16 +08:00
|
|
|
|
r.tick.Stop()
|
|
|
|
|
|
r.cancel()
|
|
|
|
|
|
if r.terminal != nil {
|
|
|
|
|
|
if err := r.terminal.Close(); err != nil {
|
2023-09-06 18:06:52 +08:00
|
|
|
|
if err != io.EOF {
|
2024-11-20 22:43:53 +08:00
|
|
|
|
logx.Errorf("failed to close the machine ssh terminal: %s", err.Error())
|
2023-09-06 18:06:52 +08:00
|
|
|
|
}
|
2022-08-13 19:31:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-04-21 19:35:58 +08:00
|
|
|
|
// 获取终端会话执行的所有命令
|
|
|
|
|
|
func (r TerminalSession) GetExecCmds() []*ExecutedCmd {
|
|
|
|
|
|
if r.handler != nil {
|
|
|
|
|
|
return r.handler.ExecutedCmds
|
|
|
|
|
|
}
|
|
|
|
|
|
return []*ExecutedCmd{}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-03-21 17:15:52 +08:00
|
|
|
|
func (ts TerminalSession) readFromTerminal() {
|
2022-08-13 19:31:16 +08:00
|
|
|
|
for {
|
|
|
|
|
|
select {
|
|
|
|
|
|
case <-ts.ctx.Done():
|
|
|
|
|
|
return
|
|
|
|
|
|
default:
|
|
|
|
|
|
rn, size, err := ts.terminal.ReadRune()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
if err != io.EOF {
|
2024-11-20 22:43:53 +08:00
|
|
|
|
logx.Error("the machine ssh terminal failed to read the message: ", err)
|
2022-08-13 19:31:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if size > 0 {
|
|
|
|
|
|
ts.dataChan <- rn
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (ts TerminalSession) writeToWebsocket() {
|
|
|
|
|
|
var buf []byte
|
|
|
|
|
|
for {
|
|
|
|
|
|
select {
|
|
|
|
|
|
case <-ts.ctx.Done():
|
|
|
|
|
|
return
|
|
|
|
|
|
case <-ts.tick.C:
|
2024-04-21 19:35:58 +08:00
|
|
|
|
if len(buf) == 0 {
|
|
|
|
|
|
continue
|
2022-08-13 19:31:16 +08:00
|
|
|
|
}
|
2024-04-21 19:35:58 +08:00
|
|
|
|
if ts.handler != nil {
|
|
|
|
|
|
ts.handler.HandleRead(buf)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
s := string(buf)
|
|
|
|
|
|
if err := ts.WriteToWs(s); err != nil {
|
2024-11-20 22:43:53 +08:00
|
|
|
|
logx.Error("the machine ssh endpoint failed to send a message to the websocket: ", err)
|
2024-04-21 19:35:58 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果记录器存在,则记录操作回放信息
|
|
|
|
|
|
if ts.recorder != nil {
|
|
|
|
|
|
ts.recorder.Lock()
|
|
|
|
|
|
ts.recorder.WriteData(OutPutType, s)
|
|
|
|
|
|
ts.recorder.Unlock()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
buf = []byte{}
|
2022-08-13 19:31:16 +08:00
|
|
|
|
case data := <-ts.dataChan:
|
|
|
|
|
|
if data != utf8.RuneError {
|
|
|
|
|
|
p := make([]byte, utf8.RuneLen(data))
|
|
|
|
|
|
utf8.EncodeRune(p, data)
|
|
|
|
|
|
buf = append(buf, p...)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
buf = append(buf, []byte("@")...)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type WsMsg struct {
|
|
|
|
|
|
Type int `json:"type"`
|
|
|
|
|
|
Msg string `json:"msg"`
|
|
|
|
|
|
Cols int `json:"cols"`
|
|
|
|
|
|
Rows int `json:"rows"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-04-21 19:35:58 +08:00
|
|
|
|
// receiveWsMsg 接收客户端ws发送过来的消息,并写入终端会话中。
|
2022-08-13 19:31:16 +08:00
|
|
|
|
func (ts *TerminalSession) receiveWsMsg() {
|
|
|
|
|
|
for {
|
|
|
|
|
|
select {
|
|
|
|
|
|
case <-ts.ctx.Done():
|
|
|
|
|
|
return
|
|
|
|
|
|
default:
|
|
|
|
|
|
// read websocket msg
|
2024-04-21 19:35:58 +08:00
|
|
|
|
_, wsData, err := ts.wsConn.ReadMessage()
|
2022-08-13 19:31:16 +08:00
|
|
|
|
if err != nil {
|
2024-11-20 22:43:53 +08:00
|
|
|
|
logx.Debugf("the machine ssh terminal failed to read the websocket message: %s", err.Error())
|
2022-08-13 19:31:16 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// 解析消息
|
2024-02-23 22:53:17 +08:00
|
|
|
|
msgObj, err := parseMsg(wsData)
|
|
|
|
|
|
if err != nil {
|
2024-11-20 22:43:53 +08:00
|
|
|
|
ts.WriteToWs(GetErrorContentRn("failed to parse the message content..."))
|
|
|
|
|
|
logx.Error("machine ssh terminal message parsing failed: ", err)
|
2024-02-23 22:53:17 +08:00
|
|
|
|
return
|
2022-08-13 19:31:16 +08:00
|
|
|
|
}
|
2024-02-23 22:53:17 +08:00
|
|
|
|
|
2022-08-13 19:31:16 +08:00
|
|
|
|
switch msgObj.Type {
|
|
|
|
|
|
case Resize:
|
|
|
|
|
|
if msgObj.Cols > 0 && msgObj.Rows > 0 {
|
|
|
|
|
|
if err := ts.terminal.WindowChange(msgObj.Rows, msgObj.Cols); err != nil {
|
2023-09-02 17:24:18 +08:00
|
|
|
|
logx.Error("ssh pty change windows size failed")
|
2022-08-13 19:31:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
case Data:
|
2024-04-21 19:35:58 +08:00
|
|
|
|
data := []byte(msgObj.Msg)
|
|
|
|
|
|
if ts.handler != nil {
|
|
|
|
|
|
if err := ts.handler.PreWriteHandle(data); err != nil {
|
|
|
|
|
|
ts.WriteToWs(err.Error())
|
|
|
|
|
|
// 发送命令终止指令
|
|
|
|
|
|
ts.terminal.Write([]byte{EOT})
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-08-13 19:31:16 +08:00
|
|
|
|
_, err := ts.terminal.Write([]byte(msgObj.Msg))
|
|
|
|
|
|
if err != nil {
|
2024-11-20 22:43:53 +08:00
|
|
|
|
logx.Errorf("failed to write data to the ssh terminal: %s", err)
|
|
|
|
|
|
ts.WriteToWs(GetErrorContentRn(fmt.Sprintf("failed to write data to the ssh terminal: %s", err.Error())))
|
2022-08-13 19:31:16 +08:00
|
|
|
|
}
|
2022-08-19 21:42:26 +08:00
|
|
|
|
case Ping:
|
|
|
|
|
|
_, err := ts.terminal.SshSession.SendRequest("ping", true, nil)
|
|
|
|
|
|
if err != nil {
|
2024-11-20 22:43:53 +08:00
|
|
|
|
ts.WriteToWs(GetErrorContentRn("the terminal connection has been disconnected..."))
|
2022-08-19 21:42:26 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2022-08-13 19:31:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-04-21 19:35:58 +08:00
|
|
|
|
// WriteToWs 将消息写入websocket连接
|
|
|
|
|
|
func (ts *TerminalSession) WriteToWs(msg string) error {
|
|
|
|
|
|
return ts.wsConn.WriteMessage(websocket.TextMessage, []byte(msg))
|
2022-08-13 19:31:16 +08:00
|
|
|
|
}
|
2024-02-23 22:53:17 +08:00
|
|
|
|
|
|
|
|
|
|
// 解析消息
|
|
|
|
|
|
func parseMsg(msg []byte) (*WsMsg, error) {
|
|
|
|
|
|
// 消息格式为 msgType|msgContent, 如果msgType为resize则为msgType|rows|cols
|
|
|
|
|
|
msgStr := string(msg)
|
|
|
|
|
|
// 查找第一个 "|" 的位置
|
|
|
|
|
|
index := strings.Index(msgStr, MsgSplit)
|
|
|
|
|
|
if index == -1 {
|
2024-11-20 22:43:53 +08:00
|
|
|
|
return nil, errorx.NewBiz("the message content does not conform to the specified rules")
|
2024-02-23 22:53:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取消息类型, 提取第一个 "|" 之前的内容
|
2025-06-27 12:17:45 +08:00
|
|
|
|
msgType := cmp.Or(cast.ToInt(msgStr[:index]), Ping)
|
2024-02-23 22:53:17 +08:00
|
|
|
|
// 其余内容则为消息内容
|
|
|
|
|
|
msgContent := msgStr[index+1:]
|
|
|
|
|
|
|
|
|
|
|
|
wsMsg := &WsMsg{Type: msgType, Msg: msgContent}
|
|
|
|
|
|
if msgType == Resize {
|
|
|
|
|
|
rowsAndCols := strings.Split(msgContent, MsgSplit)
|
2025-06-27 12:17:45 +08:00
|
|
|
|
wsMsg.Rows = cmp.Or(cast.ToInt(rowsAndCols[0]), 80)
|
|
|
|
|
|
wsMsg.Cols = cmp.Or(cast.ToInt(rowsAndCols[1]), 80)
|
2024-02-23 22:53:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
return wsMsg, nil
|
|
|
|
|
|
}
|