diff --git a/cmd/dashboard/controller/controller.go b/cmd/dashboard/controller/controller.go index 3949991..87eddfe 100644 --- a/cmd/dashboard/controller/controller.go +++ b/cmd/dashboard/controller/controller.go @@ -62,6 +62,9 @@ func routers(r *gin.Engine) { auth.POST("/terminal", commonHandler(createTerminal)) auth.GET("/ws/terminal/:id", commonHandler(terminalStream)) + auth.GET("/file", commonHandler(createFM)) + auth.GET("/ws/file/:id", commonHandler(fmStream)) + auth.GET("/user", commonHandler(listUser)) auth.POST("/user", commonHandler(createUser)) auth.POST("/batch-delete/user", commonHandler(batchDeleteUser)) diff --git a/cmd/dashboard/controller/ddns.go b/cmd/dashboard/controller/ddns.go index 47c0345..5384508 100644 --- a/cmd/dashboard/controller/ddns.go +++ b/cmd/dashboard/controller/ddns.go @@ -102,7 +102,7 @@ func createDDNS(c *gin.Context) (uint64, error) { // @Description Edit DDNS profile // @Tags auth required // @Accept json -// @param id path string true "Profile ID" +// @param id path uint true "Profile ID" // @param request body model.DDNSForm true "DDNS Request" // @Produce json // @Success 200 {object} model.CommonResponse[any] diff --git a/cmd/dashboard/controller/fm.go b/cmd/dashboard/controller/fm.go new file mode 100644 index 0000000..6541ffe --- /dev/null +++ b/cmd/dashboard/controller/fm.go @@ -0,0 +1,99 @@ +package controller + +import ( + "errors" + "strconv" + "time" + + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" + "github.com/hashicorp/go-uuid" + "github.com/naiba/nezha/model" + "github.com/naiba/nezha/pkg/utils" + "github.com/naiba/nezha/pkg/websocketx" + "github.com/naiba/nezha/proto" + "github.com/naiba/nezha/service/rpc" + "github.com/naiba/nezha/service/singleton" +) + +// Create FM session +// @Summary Create FM session +// @Description Create an "attached" FM. It is advised to only call this within a terminal session. +// @Tags auth required +// @Accept json +// @Param id path uint true "Server ID" +// @Produce json +// @Success 200 {object} model.CreateFMResponse +// @Router /file [get] +func createFM(c *gin.Context) (*model.CreateFMResponse, error) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + return nil, err + } + + streamId, err := uuid.GenerateUUID() + if err != nil { + return nil, err + } + + rpc.NezhaHandlerSingleton.CreateStream(streamId) + + singleton.ServerLock.RLock() + server := singleton.ServerList[id] + singleton.ServerLock.RUnlock() + if server == nil || server.TaskStream == nil { + return nil, errors.New("server not found or not connected") + } + + fmData, _ := utils.Json.Marshal(&model.TaskFM{ + StreamID: streamId, + }) + if err := server.TaskStream.Send(&proto.Task{ + Type: model.TaskTypeFM, + Data: string(fmData), + }); err != nil { + return nil, err + } + + return &model.CreateFMResponse{ + SessionID: streamId, + }, nil +} + +// Start FM stream +// @Summary Start FM stream +// @Description Start FM stream +// @Tags auth required +// @Param id path string true "Stream UUID" +// @Router /ws/file/{id} [get] +func fmStream(c *gin.Context) (any, error) { + streamId := c.Param("id") + if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil { + return nil, err + } + defer rpc.NezhaHandlerSingleton.CloseStream(streamId) + + wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + return nil, err + } + 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 nil, err + } + + return nil, rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10) +} diff --git a/cmd/dashboard/controller/notification_group.go b/cmd/dashboard/controller/notification_group.go index f750869..b1ce076 100644 --- a/cmd/dashboard/controller/notification_group.go +++ b/cmd/dashboard/controller/notification_group.go @@ -110,7 +110,7 @@ func createNotificationGroup(c *gin.Context) (uint64, error) { // @Security BearerAuth // @Tags auth required // @Accept json -// @Param id path string true "ID" +// @Param id path uint true "ID" // @Param body body model.NotificationGroupForm true "NotificationGroupForm" // @Produce json // @Success 200 {object} model.CommonResponse[any] diff --git a/cmd/dashboard/controller/server_group.go b/cmd/dashboard/controller/server_group.go index 9a50ba7..301c0b7 100644 --- a/cmd/dashboard/controller/server_group.go +++ b/cmd/dashboard/controller/server_group.go @@ -107,7 +107,7 @@ func createServerGroup(c *gin.Context) (uint64, error) { // @Security BearerAuth // @Tags auth required // @Accept json -// @Param id path string true "ID" +// @Param id path uint true "ID" // @Param body body model.ServerGroupForm true "ServerGroupForm" // @Produce json // @Success 200 {object} model.CommonResponse[any] diff --git a/cmd/dashboard/controller/terminal.go b/cmd/dashboard/controller/terminal.go index acc37a3..7ffbd04 100644 --- a/cmd/dashboard/controller/terminal.go +++ b/cmd/dashboard/controller/terminal.go @@ -65,8 +65,8 @@ func createTerminal(c *gin.Context) (*model.CreateTerminalResponse, error) { // @Summary Terminal stream // @Description Terminal stream // @Tags auth required -// @Param id path string true "Stream ID" -// @Router /terminal/{id} [get] +// @Param id path string true "Stream UUID" +// @Router /ws/terminal/{id} [get] func terminalStream(c *gin.Context) (any, error) { streamId := c.Param("id") if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil { diff --git a/model/fm_api.go b/model/fm_api.go new file mode 100644 index 0000000..bdf92bb --- /dev/null +++ b/model/fm_api.go @@ -0,0 +1,5 @@ +package model + +type CreateFMResponse struct { + SessionID string `json:"session_id,omitempty"` +}