nezha/cmd/dashboard/controller/common_page.go

570 lines
15 KiB
Go
Raw Normal View History

2019-12-08 03:59:58 -05:00
package controller
import (
"errors"
2024-06-23 04:15:08 -04:00
"fmt"
2019-12-08 03:59:58 -05:00
"net/http"
"strconv"
2019-12-10 04:57:57 -05:00
"time"
2019-12-08 03:59:58 -05:00
"github.com/gin-gonic/gin"
2019-12-10 04:57:57 -05:00
"github.com/gorilla/websocket"
2021-08-17 23:56:54 -04:00
"github.com/hashicorp/go-uuid"
"github.com/jinzhu/copier"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/crypto/bcrypt"
2022-03-23 09:07:38 -04:00
"golang.org/x/sync/singleflight"
2019-12-08 03:59:58 -05:00
2020-11-10 21:07:45 -05:00
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/pkg/utils"
"github.com/naiba/nezha/pkg/websocketx"
2021-08-17 23:56:54 -04:00
"github.com/naiba/nezha/proto"
"github.com/naiba/nezha/service/rpc"
2022-01-08 22:54:14 -05:00
"github.com/naiba/nezha/service/singleton"
2019-12-08 03:59:58 -05:00
)
type commonPage struct {
r *gin.Engine
requestGroup singleflight.Group
2019-12-08 03:59:58 -05:00
}
func (cp *commonPage) serve() {
cr := cp.r.Group("")
2024-02-25 21:11:02 -05:00
cr.Use(mygin.Authorize(mygin.AuthorizeOption{}))
2024-02-24 09:28:07 -05:00
cr.Use(mygin.PreferredTheme)
cr.POST("/view-password", cp.issueViewPassword)
2024-02-25 21:11:02 -05:00
cr.GET("/terminal/:id", cp.terminal)
cr.Use(mygin.ValidateViewPassword(mygin.ValidateViewPasswordOption{
IsPage: true,
AbortWhenFail: true,
}))
2019-12-08 03:59:58 -05:00
cr.GET("/", cp.home)
cr.GET("/service", cp.service)
// TODO: 界面直接跳转使用该接口
cr.GET("/network/:id", cp.network)
cr.GET("/network", cp.network)
2019-12-10 04:57:57 -05:00
cr.GET("/ws", cp.ws)
2021-08-17 23:56:54 -04:00
cr.POST("/terminal", cp.createTerminal)
cr.GET("/file", cp.createFM)
cr.GET("/file/:id", cp.fm)
2019-12-08 03:59:58 -05:00
}
type viewPasswordForm struct {
Password string
}
2024-10-19 11:14:53 -04:00
// PingExample godoc
// @Summary ping example
// @Schemes
// @Description do ping
// @Tags example
// @Accept json
// @Produce json
// @Success 200 {string} Helloworld
// @Router /example/helloworld [get]
func (p *commonPage) issueViewPassword(c *gin.Context) {
var vpf viewPasswordForm
err := c.ShouldBind(&vpf)
var hash []byte
2022-01-08 22:54:14 -05:00
if err == nil && vpf.Password != singleton.Conf.Site.ViewPassword {
err = errors.New(singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "WrongAccessPassword"}))
}
if err == nil {
hash, err = bcrypt.GenerateFromPassword([]byte(vpf.Password), bcrypt.DefaultCost)
}
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusOK,
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "AnErrorEccurred",
}),
Msg: err.Error(),
}, true)
c.Abort()
return
}
2022-01-08 22:54:14 -05:00
c.SetCookie(singleton.Conf.Site.CookieName+"-vp", string(hash), 60*60*24, "", "", false, false)
c.Redirect(http.StatusFound, c.Request.Referer())
}
func (p *commonPage) service(c *gin.Context) {
res, _, _ := p.requestGroup.Do("servicePage", func() (interface{}, error) {
singleton.AlertsLock.RLock()
defer singleton.AlertsLock.RUnlock()
var stats map[uint64]model.ServiceItemResponse
var statsStore map[uint64]model.CycleTransferStats
copier.Copy(&stats, singleton.ServiceSentinelShared.LoadStats())
copier.Copy(&statsStore, singleton.AlertsCycleTransferStatsStore)
for k, service := range stats {
if !service.Monitor.EnableShowInService {
delete(stats, k)
}
}
return []interface {
}{
stats, statsStore,
}, nil
})
2024-02-24 09:28:07 -05:00
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/service"), mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServicesStatus"}),
"Services": res.([]interface{})[0],
"CycleTransferStats": res.([]interface{})[1],
}))
}
func (cp *commonPage) network(c *gin.Context) {
var (
monitorHistory *model.MonitorHistory
servers []*model.Server
serverIdsWithMonitor []uint64
monitorInfos = []byte("{}")
id uint64
)
if len(singleton.SortedServerList) > 0 {
id = singleton.SortedServerList[0].ID
}
if err := singleton.DB.Model(&model.MonitorHistory{}).Select("monitor_id, server_id").
Where("monitor_id != 0 and server_id != 0").Limit(1).First(&monitorHistory).Error; err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "请求参数有误:" + "server monitor history not found",
Link: "/",
Btn: "返回重试",
}, true)
return
} else {
if monitorHistory == nil || monitorHistory.ServerID == 0 {
if len(singleton.SortedServerList) > 0 {
id = singleton.SortedServerList[0].ID
}
} else {
id = monitorHistory.ServerID
}
}
idStr := c.Param("id")
if idStr != "" {
var err error
id, err = strconv.ParseUint(idStr, 10, 64)
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "请求参数有误:" + err.Error(),
Link: "/",
Btn: "返回重试",
}, true)
return
}
_, ok := singleton.ServerList[id]
if !ok {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "请求参数有误:" + "server id not found",
Link: "/",
Btn: "返回重试",
}, true)
return
}
}
monitorHistories := singleton.MonitorAPI.GetMonitorHistories(map[string]any{"server_id": id})
monitorInfos, _ = utils.Json.Marshal(monitorHistories)
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
_, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified)
if err := singleton.DB.Model(&model.MonitorHistory{}).
Select("distinct(server_id)").
Where("server_id != 0").
Find(&serverIdsWithMonitor).
Error; err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "请求参数有误:" + "no server with monitor histories",
Link: "/",
Btn: "返回重试",
}, true)
return
}
if isMember || isViewPasswordVerfied {
for _, server := range singleton.SortedServerList {
for _, id := range serverIdsWithMonitor {
if server.ID == id {
servers = append(servers, server)
}
}
}
} else {
for _, server := range singleton.SortedServerListForGuest {
for _, id := range serverIdsWithMonitor {
if server.ID == id {
servers = append(servers, server)
}
}
}
}
serversBytes, _ := utils.Json.Marshal(Data{
Now: time.Now().Unix() * 1000,
Servers: servers,
})
2024-02-24 09:28:07 -05:00
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/network"), mygin.CommonEnvironment(c, gin.H{
"Servers": string(serversBytes),
"MonitorInfos": string(monitorInfos),
"MaxTCPPingValue": singleton.Conf.MaxTCPPingValue,
}))
}
func (cp *commonPage) getServerStat(c *gin.Context, withPublicNote bool) ([]byte, error) {
2024-06-23 04:15:08 -04:00
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
_, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified)
authorized := isMember || isViewPasswordVerfied
v, err, _ := cp.requestGroup.Do(fmt.Sprintf("serverStats::%t", authorized), func() (interface{}, error) {
2022-03-23 09:07:38 -04:00
singleton.SortedServerLock.RLock()
defer singleton.SortedServerLock.RUnlock()
var serverList []*model.Server
if authorized {
serverList = singleton.SortedServerList
} else {
serverList = singleton.SortedServerListForGuest
}
var servers []*model.Server
for _, server := range serverList {
item := *server
if !withPublicNote {
item.PublicNote = ""
2024-06-23 04:15:08 -04:00
}
servers = append(servers, &item)
}
2022-03-23 09:07:38 -04:00
return utils.Json.Marshal(Data{
Now: time.Now().Unix() * 1000,
2024-06-23 04:15:08 -04:00
Servers: servers,
2022-03-23 09:07:38 -04:00
})
})
return v.([]byte), err
}
2019-12-08 03:59:58 -05:00
func (cp *commonPage) home(c *gin.Context) {
stat, err := cp.getServerStat(c, true)
2022-03-23 09:07:38 -04:00
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusInternalServerError,
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "SystemError",
}),
Msg: "服务器状态获取失败",
Link: "/",
Btn: "返回首页",
2022-03-23 09:07:38 -04:00
}, true)
return
}
2024-02-24 09:28:07 -05:00
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/home"), mygin.CommonEnvironment(c, gin.H{
2024-08-13 12:24:17 -04:00
"Servers": string(stat),
}))
2019-12-08 03:59:58 -05:00
}
2019-12-10 04:57:57 -05:00
2021-08-17 23:56:54 -04:00
var upgrader = websocket.Upgrader{
ReadBufferSize: 32768,
WriteBufferSize: 32768,
2021-08-17 23:56:54 -04:00
}
2019-12-10 04:57:57 -05:00
type Data struct {
Now int64 `json:"now,omitempty"`
Servers []*model.Server `json:"servers,omitempty"`
}
2019-12-10 04:57:57 -05:00
func (cp *commonPage) ws(c *gin.Context) {
conn, 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{
2022-04-30 03:53:21 -04:00
MessageID: "NetworkError",
}),
Msg: "Websocket协议切换失败",
Link: "/",
Btn: "返回首页",
2019-12-10 04:57:57 -05:00
}, true)
return
}
defer conn.Close()
2021-04-03 12:22:50 -04:00
count := 0
for {
stat, err := cp.getServerStat(c, false)
if err != nil {
2022-03-23 09:07:38 -04:00
continue
}
if err := conn.WriteMessage(websocket.TextMessage, stat); err != nil {
break
2019-12-10 04:57:57 -05:00
}
2021-04-03 12:22:50 -04:00
count += 1
if count%4 == 0 {
2021-04-03 13:01:04 -04:00
err = conn.WriteMessage(websocket.PingMessage, []byte{})
if err != nil {
break
}
2021-04-03 12:22:50 -04:00
}
time.Sleep(time.Second * 2)
}
2019-12-10 04:57:57 -05:00
}
2021-08-17 23:56:54 -04:00
func (cp *commonPage) terminal(c *gin.Context) {
2024-07-14 07:41:50 -04:00
streamId := c.Param("id")
if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil {
2021-08-17 23:56:54 -04:00
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "无权访问",
Msg: "终端会话不存在",
Link: "/",
Btn: "返回首页",
}, true)
return
}
2024-07-14 07:41:50 -04:00
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
2021-08-17 23:56:54 -04:00
wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
2021-08-17 23:56:54 -04:00
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusInternalServerError,
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
2022-04-30 03:53:21 -04:00
MessageID: "NetworkError",
}),
Msg: "Websocket协议切换失败",
Link: "/",
Btn: "返回首页",
2021-08-17 23:56:54 -04:00
}, true)
return
}
defer wsConn.Close()
conn := websocketx.NewConn(wsConn)
2021-08-19 22:45:10 -04:00
go func() {
// PING 保活
for {
if err = conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
return
}
time.Sleep(time.Second * 10)
}
}()
2024-07-14 07:41:50 -04:00
if err = rpc.NezhaHandlerSingleton.UserConnected(streamId, conn); err != nil {
return
2021-08-19 22:45:10 -04:00
}
2024-07-14 07:41:50 -04:00
rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10)
2021-08-17 23:56:54 -04:00
}
type createTerminalRequest struct {
Host string
Protocol string
ID uint64
}
func (cp *commonPage) createTerminal(c *gin.Context) {
if _, authorized := c.Get(model.CtxKeyAuthorizedUser); !authorized {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "无权访问",
Msg: "用户未登录",
Link: "/login",
Btn: "去登录",
}, true)
return
}
var createTerminalReq createTerminalRequest
if err := c.ShouldBind(&createTerminalReq); err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "请求参数有误:" + err.Error(),
Link: "/server",
Btn: "返回重试",
}, true)
return
}
2024-07-14 07:41:50 -04:00
streamId, err := uuid.GenerateUUID()
2021-08-17 23:56:54 -04:00
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusInternalServerError,
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "SystemError",
}),
Msg: "生成会话ID失败",
Link: "/server",
Btn: "返回重试",
2021-08-17 23:56:54 -04:00
}, true)
return
}
2024-07-14 07:41:50 -04:00
rpc.NezhaHandlerSingleton.CreateStream(streamId)
2022-01-08 22:54:14 -05:00
singleton.ServerLock.RLock()
server := singleton.ServerList[createTerminalReq.ID]
singleton.ServerLock.RUnlock()
2021-08-17 23:56:54 -04:00
if server == nil || server.TaskStream == nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "服务器不存在或处于离线状态",
Link: "/server",
Btn: "返回重试",
}, true)
return
}
terminalData, _ := utils.Json.Marshal(&model.TerminalTask{
2024-07-14 07:41:50 -04:00
StreamID: streamId,
})
if err := server.TaskStream.Send(&proto.Task{
Type: model.TaskTypeTerminalGRPC,
Data: string(terminalData),
}); err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "Agent信令下发失败",
Link: "/server",
Btn: "返回重试",
}, true)
return
2021-08-17 23:56:54 -04:00
}
2022-06-02 21:45:11 -04:00
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/terminal", mygin.CommonEnvironment(c, gin.H{
2024-07-14 07:41:50 -04:00
"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,
2021-08-17 23:56:54 -04:00
}))
}