diff --git a/cmd/dashboard/controller/common_page.go b/cmd/dashboard/controller/common_page.go index ad43d7e..5d0b309 100644 --- a/cmd/dashboard/controller/common_page.go +++ b/cmd/dashboard/controller/common_page.go @@ -46,6 +46,8 @@ func (cp *commonPage) serve() { cr.GET("/network", cp.network) cr.GET("/ws", cp.ws) cr.POST("/terminal", cp.createTerminal) + cr.GET("/file", cp.createFM) + cr.GET("/file/:id", cp.fm) } type viewPasswordForm struct { @@ -257,8 +259,8 @@ func (cp *commonPage) home(c *gin.Context) { } var upgrader = websocket.Upgrader{ - ReadBufferSize: 10240, - WriteBufferSize: 10240, + ReadBufferSize: 32768, + WriteBufferSize: 32768, } type Data struct { @@ -427,5 +429,130 @@ func (cp *commonPage) createTerminal(c *gin.Context) { c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/terminal", mygin.CommonEnvironment(c, gin.H{ "SessionID": streamId, "ServerName": server.Name, + "ServerID": server.ID, + })) +} + +func (cp *commonPage) fm(c *gin.Context) { + streamId := c.Param("id") + if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil { + mygin.ShowErrorPage(c, mygin.ErrInfo{ + Code: http.StatusForbidden, + Title: "无权访问", + Msg: "FM会话不存在", + Link: "/", + Btn: "返回首页", + }, true) + return + } + defer rpc.NezhaHandlerSingleton.CloseStream(streamId) + + wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + mygin.ShowErrorPage(c, mygin.ErrInfo{ + Code: http.StatusInternalServerError, + Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{ + MessageID: "NetworkError", + }), + Msg: "Websocket协议切换失败", + Link: "/", + Btn: "返回首页", + }, true) + return + } + defer wsConn.Close() + conn := websocketx.NewConn(wsConn) + + go func() { + // PING 保活 + for { + if err = conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil { + return + } + time.Sleep(time.Second * 10) + } + }() + + if err = rpc.NezhaHandlerSingleton.UserConnected(streamId, conn); err != nil { + return + } + + rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10) +} + +func (cp *commonPage) createFM(c *gin.Context) { + IdString := c.Query("id") + if _, authorized := c.Get(model.CtxKeyAuthorizedUser); !authorized { + mygin.ShowErrorPage(c, mygin.ErrInfo{ + Code: http.StatusForbidden, + Title: "无权访问", + Msg: "用户未登录", + Link: "/login", + Btn: "去登录", + }, true) + return + } + + streamId, err := uuid.GenerateUUID() + if err != nil { + mygin.ShowErrorPage(c, mygin.ErrInfo{ + Code: http.StatusInternalServerError, + Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{ + MessageID: "SystemError", + }), + Msg: "生成会话ID失败", + Link: "/server", + Btn: "返回重试", + }, true) + return + } + + rpc.NezhaHandlerSingleton.CreateStream(streamId) + + serverId, err := strconv.Atoi(IdString) + if err != nil { + mygin.ShowErrorPage(c, mygin.ErrInfo{ + Code: http.StatusForbidden, + Title: "请求失败", + Msg: "请求参数有误:" + err.Error(), + Link: "/server", + Btn: "返回重试", + }, true) + return + } + + singleton.ServerLock.RLock() + server := singleton.ServerList[uint64(serverId)] + singleton.ServerLock.RUnlock() + if server == nil { + mygin.ShowErrorPage(c, mygin.ErrInfo{ + Code: http.StatusForbidden, + Title: "请求失败", + Msg: "服务器不存在或处于离线状态", + Link: "/server", + Btn: "返回重试", + }, true) + return + } + + fmData, _ := utils.Json.Marshal(&model.TaskFM{ + StreamID: streamId, + }) + if err := server.TaskStream.Send(&proto.Task{ + Type: model.TaskTypeFM, + Data: string(fmData), + }); err != nil { + mygin.ShowErrorPage(c, mygin.ErrInfo{ + Code: http.StatusForbidden, + Title: "请求失败", + Msg: "Agent信令下发失败", + Link: "/server", + Btn: "返回重试", + }, true) + return + } + + c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/file", mygin.CommonEnvironment(c, gin.H{ + "SessionID": streamId, })) } diff --git a/model/monitor.go b/model/monitor.go index ec1cd42..9bfc57e 100644 --- a/model/monitor.go +++ b/model/monitor.go @@ -23,6 +23,7 @@ const ( TaskTypeTerminalGRPC TaskTypeNAT TaskTypeReportHostInfo + TaskTypeFM ) type TerminalTask struct { @@ -34,6 +35,10 @@ type TaskNAT struct { Host string } +type TaskFM struct { + StreamID string +} + const ( MonitorCoverAll = iota MonitorCoverIgnoreAll diff --git a/resource/template/dashboard-default/file.html b/resource/template/dashboard-default/file.html new file mode 100644 index 0000000..f62d3c8 --- /dev/null +++ b/resource/template/dashboard-default/file.html @@ -0,0 +1,508 @@ +{{define "dashboard-default/file"}} + + + + + + + + File List + + + + + + + + + +
+ + + + Refresh + Copy path + Go to + + + + +
+ + + + + + + + + + + + Go + Close + + + + + + +{{end}} \ No newline at end of file diff --git a/resource/template/dashboard-default/terminal.html b/resource/template/dashboard-default/terminal.html index b9b57ae..2ad03b4 100644 --- a/resource/template/dashboard-default/terminal.html +++ b/resource/template/dashboard-default/terminal.html @@ -23,10 +23,43 @@ body { background-color: black; } + + #file-list-iframe { + position: absolute; + top: 0; + right: 0; + width: 30%; + height: 100%; + border: none; + background-color: white; + display: none; + z-index: 10; + } + + #folder-button { + position: absolute; + bottom: 20px; + right: 20px; + width: 50px; + height: 50px; + background-color: #007bff; + color: white; + border: none; + border-radius: 25px; + font-size: 24px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + z-index: 20; + }
+ + + @@ -95,6 +128,27 @@ socket.onerror = () => { alert('{{tr "TerminalConnectionFailed"}}') } + + // 处理文件夹按钮点击事件 + const folderButton = document.getElementById('folder-button'); + const fileListIframe = document.getElementById('file-list-iframe'); + let fileListVisible = false; + + folderButton.addEventListener('click', () => { + if (!fileListVisible) { + // 显示文件列表 + const params = new URLSearchParams({ + id: "{{.ServerID}}" + }).toString(); + fileListIframe.src = `/file?${params}`; + fileListIframe.style.display = 'block'; + fileListVisible = true; + } else { + // 隐藏文件列表 + fileListIframe.style.display = 'none'; + fileListVisible = false; + } + }); diff --git a/service/rpc/io_stream.go b/service/rpc/io_stream.go index 0a56d8b..82a63e7 100644 --- a/service/rpc/io_stream.go +++ b/service/rpc/io_stream.go @@ -117,7 +117,7 @@ LOOP: endCh := make(chan struct{}) go func() { - _, innerErr := io.Copy(stream.userIo, stream.agentIo) + _, innerErr := io.CopyBuffer(stream.userIo, stream.agentIo, make([]byte, 1048576)) if innerErr != nil { err = innerErr } @@ -126,7 +126,7 @@ LOOP: } }() go func() { - _, innerErr := io.Copy(stream.agentIo, stream.userIo) + _, innerErr := io.CopyBuffer(stream.agentIo, stream.userIo, make([]byte, 1048576)) if innerErr != nil { err = innerErr }