From 54e0649313f49ca8d7f58c266f85d27d6505ba95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E5=BF=B5?= <1@72wo.cn> Date: Fri, 9 Jan 2026 00:43:06 +0800 Subject: [PATCH] =?UTF-8?q?```=20feat:=20=E6=B7=BB=E5=8A=A0WebSSH=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=B9=B6=E9=87=8D=E6=9E=84=E5=A1=94=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加WebSSH中间件,支持通过 --- common/rpc/client.go | 6 +- logic/controller/login_getserver.go | 12 +- modules/base/middleware/websocket.go | 366 ++++++++++++++++++++------- modules/blazing/service/item.go | 3 +- modules/config/service/server.go | 102 -------- modules/config/service/tower.go | 58 +++-- 6 files changed, 320 insertions(+), 227 deletions(-) diff --git a/common/rpc/client.go b/common/rpc/client.go index b5f0c8e86..e3537280f 100644 --- a/common/rpc/client.go +++ b/common/rpc/client.go @@ -6,7 +6,7 @@ import ( "github.com/butoften/array" ) -func GetServerInfoList(isdebug bool) []ServerInfo { +func GetServerInfoList(isdebug int32) []ServerInfo { ret := service.NewServerService().GetServer() @@ -27,7 +27,9 @@ func GetServerInfoList(isdebug bool) []ServerInfo { tt.UserCnt = 300 } - if v.IsDebug != 0 { + if v.IsDebug != 0 && isdebug == 0 { + //如果是调试服务器,但是不是测试玩家,直接返回下一个 + continue } tt.Name = v.Name diff --git a/logic/controller/login_getserver.go b/logic/controller/login_getserver.go index 7c517c6f3..3dc785d6a 100644 --- a/logic/controller/login_getserver.go +++ b/logic/controller/login_getserver.go @@ -22,13 +22,11 @@ import ( func (h Controller) GetServerOnline(data *user.SidInfo, c gnet.Conn) (result *rpc.CommendSvrInfo, err errorcode.ErrorCode) { //这个时候player应该是空的 result = rpc.NewInInfo() - if service.NewBaseSysUserService().GetPerson(data.Head.UserID).Debug == 1 { - result.IsVip = 1 - } - //v, _, _ := sg.Do("GetServerInfoList", fetchData) - - //result.ServerList = v.([]rpc.ServerInfo) //todo 待修改增加缓存 - result.ServerList = rpc.GetServerInfoList() + // if service.NewBaseSysUserService().GetPerson(data.Head.UserID).Debug == 1 { + // result.IsVip = 1 + // } + result.IsVip = 1 + result.ServerList = rpc.GetServerInfoList(service.NewBaseSysUserService().GetPerson(data.Head.UserID).Debug) return //return //TODO 这里待实现改成接口调用Ret方法 diff --git a/modules/base/middleware/websocket.go b/modules/base/middleware/websocket.go index 2672e6bdb..0ae4aeb41 100644 --- a/modules/base/middleware/websocket.go +++ b/modules/base/middleware/websocket.go @@ -1,66 +1,296 @@ package middleware import ( + "bufio" "context" - "net" + "encoding/json" + "fmt" + "io" + "sync" "time" "github.com/gogf/gf/v2/os/glog" "github.com/lxzan/gws" + "golang.org/x/crypto/ssh" ) const PingInterval = 10 * time.Second -type Handler struct { - gws.BuiltinEventHandler - port int - target net.Conn +// SSHConfig SSH 连接配置 +type SSHConfig struct { + ServerIP string `json:"server_ip"` + Port string `json:"port"` + Username string `json:"username"` + Password string `json:"password"` } +// WebSSHMessage WebSSH 消息结构 +type WebSSHMessage struct { + Type string `json:"type"` // "connect", "command", "resize", "disconnect" + Payload string `json:"payload"` // JSON encoded data +} + +// TerminalSession 终端会话 +type TerminalSession struct { + WebSocket *gws.Conn + SSHClient *ssh.Client + SSHSession *ssh.Session + Stdin io.WriteCloser + Stdout io.Reader + Mutex sync.Mutex +} + +type Handler struct { + gws.BuiltinEventHandler + session *TerminalSession // 保存当前会话 +} + +// OnOpen 连接建立时的处理 func (c *Handler) OnOpen(socket *gws.Conn) { - // target, err := net.Dial("tcp", "127.0.0.1:"+gconv.String(c.port)) + _ = socket.SetReadDeadline(time.Now().Add(60 * time.Second)) + glog.Info(context.Background(), "WebSSH WebSocket 连接建立") +} - // if err != nil { - // glog.Debug(context.Background(), "连接失败") - // } - // c.target = target - // //errChan := make(chan error, 2) - // if c.target == nil { - // return - // } - // go func(conn net.Conn, socket *gws.Conn) { - // reader := bufio.NewReader(conn) - // LOOP: - // for { +// OnMessage 处理收到的消息 +func (c *Handler) OnMessage(socket *gws.Conn, message *gws.Message) { + defer message.Close() - // // select { - // // default: - // packlen, err := reader.Peek(4) + // 解析消息 + var webSSHMsg WebSSHMessage + err := json.Unmarshal(message.Data, &webSSHMsg) + if err != nil { + glog.Error(context.Background(), "解析 WebSSH 消息失败:", err) + c.sendError(socket, "无效的消息格式: "+err.Error()) + return + } - // if err != nil { - // socket.WriteClose(1000, nil) - // break LOOP - // } + switch webSSHMsg.Type { + case "connect": + c.handleConnect(socket, webSSHMsg.Payload) + case "command": + c.handleCommand(socket, webSSHMsg.Payload) + case "resize": + c.handleResize(socket, webSSHMsg.Payload) + case "disconnect": + c.handleDisconnect(socket) + default: + glog.Warning(context.Background(), "未知的 WebSSH 消息类型:", webSSHMsg.Type) + } +} - // length := int32(binary.BigEndian.Uint32(packlen)) +// handleConnect 处理连接请求 +func (c *Handler) handleConnect(socket *gws.Conn, payload string) { + var config SSHConfig + err := json.Unmarshal([]byte(payload), &config) + if err != nil { + glog.Error(context.Background(), "解析 SSH 配置失败:", err) + c.sendError(socket, "无效的 SSH 配置: "+err.Error()) + return + } - // data := make([]byte, length) - // io.ReadFull(reader, data) + // 设置默认端口 + if config.Port == "" { + config.Port = "22" + } - // //pack_Ver := data[4] //因为包体已经解析,所以这里直接取0 - // // var pack = make([]byte, length) + // SSH 客户端配置 + sshConfig := &ssh.ClientConfig{ + User: config.Username, + Auth: []ssh.AuthMethod{ + ssh.Password(config.Password), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 生产环境应使用更安全的验证 + Timeout: 30 * time.Second, + } - // socket.WriteMessage(gws.OpcodeBinary, data) - // //t.event.RecvHandler(pack) - // // client.OnReceiveBase(client, pack, length) + // 连接到 SSH 服务器 + addr := fmt.Sprintf("%s:%s", config.ServerIP, config.Port) + sshClient, err := ssh.Dial("tcp", addr, sshConfig) + if err != nil { + glog.Error(context.Background(), "SSH 连接失败:", err) + c.sendError(socket, "SSH 连接失败: "+err.Error()) + return + } - // } - // }(c.target, socket) - // //err = <-errChan - // if err != io.EOF { - // log.Println("proxy error:", err) + // 创建 SSH 会话 + session, err := sshClient.NewSession() + if err != nil { + glog.Error(context.Background(), "创建 SSH 会话失败:", err) + sshClient.Close() + c.sendError(socket, "创建 SSH 会话失败: "+err.Error()) + return + } - // } + // 设置终端 + modes := ssh.TerminalModes{ + ssh.ECHO: 1, + ssh.TTY_OP_ISPEED: 14400, + ssh.TTY_OP_OSPEED: 14400, + } + + err = session.RequestPty("xterm", 80, 40, modes) + if err != nil { + glog.Error(context.Background(), "请求 PTY 失败:", err) + session.Close() + sshClient.Close() + c.sendError(socket, "请求 PTY 失败: "+err.Error()) + return + } + + // 获取标准输入输出 + stdin, err := session.StdinPipe() + if err != nil { + glog.Error(context.Background(), "获取 SSH 输入管道失败:", err) + session.Close() + sshClient.Close() + c.sendError(socket, "获取 SSH 输入管道失败: "+err.Error()) + return + } + + stdout, err := session.StdoutPipe() + if err != nil { + glog.Error(context.Background(), "获取 SSH 输出管道失败:", err) + session.Close() + sshClient.Close() + c.sendError(socket, "获取 SSH 输出管道失败: "+err.Error()) + return + } + + // 启动会话 + err = session.Shell() + if err != nil { + glog.Error(context.Background(), "启动 SSH Shell 失败:", err) + session.Close() + sshClient.Close() + c.sendError(socket, "启动 SSH Shell 失败: "+err.Error()) + return + } + + // 保存会话信息 + c.session = &TerminalSession{ + WebSocket: socket, + SSHClient: sshClient, + SSHSession: session, + Stdin: stdin, + Stdout: stdout, + } + + // 启动输出转发协程 + c.startOutputForwarding() + + c.sendSuccess(socket, "SSH 连接成功") +} + +// startOutputForwarding 启动输出转发协程 +func (c *Handler) startOutputForwarding() { + if c.session == nil { + return + } + + go func() { + scanner := bufio.NewScanner(c.session.Stdout) + for scanner.Scan() { + line := scanner.Text() + c.sendTerminalOutput(c.session.WebSocket, line+"\r\n") + } + + if err := scanner.Err(); err != nil && c.session != nil { + glog.Error(context.Background(), "读取 SSH 输出失败:", err) + c.sendError(c.session.WebSocket, "读取 SSH 输出失败: "+err.Error()) + } + }() +} + +// handleCommand 处理命令输入 +func (c *Handler) handleCommand(socket *gws.Conn, payload string) { + if c.session == nil || c.session.Stdin == nil { + c.sendError(socket, "SSH 会话未建立") + return + } + + _, err := c.session.Stdin.Write([]byte(payload)) + if err != nil { + glog.Error(context.Background(), "写入 SSH 命令失败:", err) + c.sendError(socket, "写入 SSH 命令失败: "+err.Error()) + } +} + +// handleResize 处理终端大小调整 +func (c *Handler) handleResize(socket *gws.Conn, payload string) { + if c.session == nil || c.session.SSHSession == nil { + c.sendError(socket, "SSH 会话未建立") + return + } + + // 解析终端大小 + var size struct { + Width int `json:"width"` + Height int `json:"height"` + } + err := json.Unmarshal([]byte(payload), &size) + if err != nil { + c.sendError(socket, "无效的终端大小参数") + return + } + + // 调整终端大小 + err = c.session.SSHSession.WindowChange(size.Height, size.Width) + if err != nil { + glog.Error(context.Background(), "调整终端大小失败:", err) + c.sendError(socket, "调整终端大小失败: "+err.Error()) + return + } + + c.sendSuccess(socket, "终端大小已调整") +} + +// handleDisconnect 处理断开连接 +func (c *Handler) handleDisconnect(socket *gws.Conn) { + c.cleanup() + c.sendSuccess(socket, "已断开连接") +} + +// cleanup 清理会话资源 +func (c *Handler) cleanup() { + if c.session != nil { + if c.session.SSHSession != nil { + c.session.SSHSession.Close() + } + if c.session.SSHClient != nil { + c.session.SSHClient.Close() + } + c.session = nil + } +} + +// sendTerminalOutput 发送终端输出到 WebSocket +func (c *Handler) sendTerminalOutput(ws *gws.Conn, output string) { + msg := WebSSHMessage{ + Type: "output", + Payload: output, + } + data, _ := json.Marshal(msg) + ws.WriteMessage(gws.OpcodeText, data) +} + +// sendSuccess 发送成功消息 +func (c *Handler) sendSuccess(ws *gws.Conn, message string) { + msg := WebSSHMessage{ + Type: "success", + Payload: message, + } + data, _ := json.Marshal(msg) + ws.WriteMessage(gws.OpcodeText, data) +} + +// sendError 发送错误消息 +func (c *Handler) sendError(ws *gws.Conn, message string) { + msg := WebSSHMessage{ + Type: "error", + Payload: message, + } + data, _ := json.Marshal(msg) + ws.WriteMessage(gws.OpcodeText, data) } func (c *Handler) OnPing(socket *gws.Conn, payload []byte) { @@ -70,61 +300,7 @@ func (c *Handler) OnPing(socket *gws.Conn, payload []byte) { func (c *Handler) OnPong(socket *gws.Conn, payload []byte) {} -func (c *Handler) OnMessage(socket *gws.Conn, gwsmessage *gws.Message) { - if c.target != nil { - c.target.Write(gwsmessage.Bytes()) - } - //fmt.Println(gwsmessage.Bytes()) - -} - func (c *Handler) OnClose(socket *gws.Conn, err error) { - - glog.Debug(context.Background(), "断开连接") - if c.target != nil { - c.target.Close() - - } - -} - -// RemoveSocket 移除WebSocket连接 - -func messeunpack(id int64, x []byte) { - - // json := gjson.ParseBytes(x) - // json.ForEach(func(key, value gjson.Result) bool { - // fmt.Println(key, value) - // numeric := regexp.MustCompile(`\d`).MatchString(value.String()) - - // var ob int - // if numeric { //如果正确 则使用常量 - - // } else { //如果错误,则使用变量,即从数据库查询常量定义值是多少 - - // } - - // work.User_script(uint(id), strings.Trim(message.Data.String(), " ")) - // // switch s:=value.String() { - // // case regexp.MustCompile(`\d`).MatchString(s): - - // // } - - // return true - // }) - // json.Get("1").String() - // var ob int - // numeric := regexp.MustCompile(`\d`).MatchString(json.Get("1").String()) - // if numeric { //如果正确 则使用常量 - // ob, _ = strconv.Atoi(json.Get("1").String()) - // } else { //如果错误,则使用变量,即从数据库查询常量定义值是多少 - // ob, _ = strconv.Atoi(json.Get("1").String()) - // } - // switch ob { - // case 1: //发包函数 - // work.User_script(uint(id), strings.Trim(message.Data.String(), " ")) - // } - - // Go 1.9.7 onwards only. - + glog.Debug(context.Background(), "WebSSH 连接断开:", err) + c.cleanup() } diff --git a/modules/blazing/service/item.go b/modules/blazing/service/item.go index cf1098ce8..af2ff5b2b 100644 --- a/modules/blazing/service/item.go +++ b/modules/blazing/service/item.go @@ -33,12 +33,13 @@ func (s *ItemService) UPDATE(id uint32, count int) { panic(err) } } else { + m := s.TestModel(s.Model) data := g.Map{ "player_id": s.userid, "item_id": id, "item_cnt": count, } - //data = cool.SetTest(data) + data = cool.SetTest(data) m.Data(data).Insert() } diff --git a/modules/config/service/server.go b/modules/config/service/server.go index 7febf2e94..9810c09dd 100644 --- a/modules/config/service/server.go +++ b/modules/config/service/server.go @@ -3,10 +3,7 @@ package service import ( "blazing/cool" "blazing/modules/config/model" - "bufio" "context" - "fmt" - "os" "sort" "github.com/gogf/gf/v2/database/gdb" @@ -14,7 +11,6 @@ import ( "github.com/gogf/gf/v2/util/gconv" "github.com/qiniu/go-sdk/v7/auth/qbox" "github.com/qiniu/go-sdk/v7/storage" - "golang.org/x/crypto/ssh" ) type ServerService struct { @@ -127,101 +123,3 @@ type File struct { Modified string `json:"modified"` Time int64 `json:"time"` } - -// RemoteExecuteScript 远程执行脚本并实时显示输出 -// ip: 服务器IP -// sshPort: SSH端口 -// user: SSH用户名 -// password: SSH密码 -// scriptPort: 脚本执行的端口参数 -func RemoteExecuteScript(ip, sshPort, user, password, scriptPort string) error { - // 执行的命令:下载脚本并执行 - cmd := fmt.Sprintf( - `wget -qO- http://125.208.20.223:59480/start.sh | bash -s -- -p %s`, - scriptPort, - ) - - // SSH 配置 - config := &ssh.ClientConfig{ - User: user, - Auth: []ssh.AuthMethod{ - ssh.Password(password), - }, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 生产环境建议替换为严格的主机key校验 - } - - // 连接 SSH - client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%s", ip, sshPort), config) - if err != nil { - return fmt.Errorf("SSH连接失败: %v", err) - } - defer client.Close() - - // 创建会话执行命令 - session, err := client.NewSession() - if err != nil { - return fmt.Errorf("创建SSH会话失败: %v", err) - } - defer session.Close() - - // 获取标准输出和标准错误输出的管道 - stdoutPipe, err := session.StdoutPipe() - if err != nil { - return fmt.Errorf("获取标准输出管道失败: %v", err) - } - - stderrPipe, err := session.StderrPipe() - if err != nil { - return fmt.Errorf("获取标准错误输出管道失败: %v", err) - } - - // 启动命令执行 - if err := session.Start(cmd); err != nil { - return fmt.Errorf("启动命令执行失败: %v", err) - } - - // 实时打印标准输出 - go func() { - scanner := bufio.NewScanner(stdoutPipe) - for scanner.Scan() { - fmt.Println(scanner.Text()) - } - if err := scanner.Err(); err != nil { - fmt.Printf("读取标准输出时出错: %v\n", err) - } - }() - - // 实时打印标准错误输出 - go func() { - scanner := bufio.NewScanner(stderrPipe) - for scanner.Scan() { - fmt.Fprintln(os.Stderr, scanner.Text()) - } - if err := scanner.Err(); err != nil { - fmt.Printf("读取标准错误输出时出错: %v\n", err) - } - }() - - // 等待命令执行完成 - if err := session.Wait(); err != nil { - return fmt.Errorf("命令执行失败: %v", err) - } - - // 显示 screen 会话列表 - fmt.Println("\n=== Screen会话列表 ===") - screenSession, err := client.NewSession() - if err != nil { - return fmt.Errorf("创建screen会话失败: %v", err) - } - defer screenSession.Close() - - // 将 screen -ls 的输出直接连接到本地标准输出 - screenSession.Stdout = os.Stdout - screenSession.Stderr = os.Stderr - - if err := screenSession.Run("screen -ls"); err != nil { - return fmt.Errorf("获取screen列表失败: %v", err) - } - - return nil -} diff --git a/modules/config/service/tower.go b/modules/config/service/tower.go index 8ed647311..537153ac9 100644 --- a/modules/config/service/tower.go +++ b/modules/config/service/tower.go @@ -20,27 +20,49 @@ const ( type TowerService struct { *cool.Service towerType TowerType // 标记当前服务对应的塔类型 + tableName string // 存储当前塔类型对应的表名 } // NewTowerService 创建指定类型的塔配置服务 func NewTowerService(towerType TowerType) *TowerService { - // 根据塔类型初始化对应的Model - var modelInstance cool.IModel + var tableName string switch towerType { case TowerType110: - modelInstance = model.New110TowerConfig() + tableName = model.TableNamedARKTowerConfig case TowerType500: - modelInstance = model.New500TowerConfig() + tableName = model.TableNameTrialTowerConfig case TowerType600: - modelInstance = model.New600TowerConfig() + tableName = model.TableNameBraveTowerConfig default: panic("unsupported tower type: " + string(rune(towerType))) // 非支持类型直接panic,也可返回error } - return &TowerService{ - Service: &cool.Service{Model: modelInstance}, - towerType: towerType, + // 创建一个统一的模型,设置对应的表名 + unifiedModel := &UnifiedTowerModel{ + tableName: tableName, } + + return &TowerService{ + Service: &cool.Service{Model: unifiedModel}, + towerType: towerType, + tableName: tableName, + } +} + +// UnifiedTowerModel 统一的塔模型,通过设置不同的表名来区分不同类型的塔 +type UnifiedTowerModel struct { + *cool.Model `json:"-" gorm:"embedded"` // 嵌入通用Model + tableName string +} + +// TableName 返回当前模型对应的表名 +func (m *UnifiedTowerModel) TableName() string { + return m.tableName +} + +// GroupName 返回模型所属组 +func (m *UnifiedTowerModel) GroupName() string { + return "default" } // Boss 根据塔等级获取对应的Boss配置(统一入口) @@ -48,26 +70,22 @@ func (s *TowerService) Boss(towerLevel uint32) *model.BaseTowerConfig { // 构建基础查询条件 query := cool.DBM(s.Model).Where("tower_level = ?", towerLevel) - // 根据塔类型处理不同的配置结构体和缓存逻辑 + // 根据塔类型处理不同的缓存逻辑 switch s.towerType { - case TowerType110: - var config model.Tower110Config + case TowerType110, TowerType500: + // 110塔和500塔使用普通查询 + var config model.BaseTowerConfig query.Scan(&config) - return &config.BaseTowerConfig - - case TowerType500: - var config model.Tower500Config - query.Scan(&config) - return &config.BaseTowerConfig + return &config case TowerType600: - var config model.Tower600Config // 600塔专属的缓存配置 + var config model.BaseTowerConfig query.Cache(gdb.CacheOption{ // Duration: time.Hour, // 可根据需要开启缓存时长 Force: false, }).Scan(&config) - return &config.BaseTowerConfig + return &config default: return nil // 非支持类型返回nil,也可根据业务需求调整 @@ -85,4 +103,4 @@ func NewTower500Service() *TowerService { func NewTower600Service() *TowerService { return NewTowerService(TowerType600) -} +} \ No newline at end of file