172 lines
4.2 KiB
Go
172 lines
4.2 KiB
Go
package http
|
||
|
||
import (
|
||
"context"
|
||
_ "embed"
|
||
"log"
|
||
"meshgo/config"
|
||
"meshgo/stats"
|
||
"net/http"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// Server HTTP 管理界面服务
|
||
type Server struct {
|
||
srv *http.Server
|
||
enabled bool
|
||
}
|
||
|
||
// New 创建 HTTP 服务(不启动)
|
||
func New(cfg *config.AdminConfig) *Server {
|
||
if !cfg.Enabled {
|
||
return &Server{enabled: false}
|
||
}
|
||
|
||
gin.SetMode(gin.ReleaseMode)
|
||
r := gin.New()
|
||
r.Use(gin.Recovery())
|
||
|
||
// 预留认证中间件扩展点
|
||
r.Use(AuthMiddleware(cfg))
|
||
|
||
// 路由
|
||
admin := r.Group("/admin/mqtt")
|
||
{
|
||
admin.GET("/api/stats", handleStats)
|
||
admin.GET("/api/clients", handleClients)
|
||
}
|
||
r.GET("/admin/mqtt", serveIndex)
|
||
r.GET("/", serveHome)
|
||
|
||
addr := cfg.Port
|
||
if addr == "" {
|
||
addr = ":8080"
|
||
}
|
||
|
||
return &Server{
|
||
enabled: true,
|
||
srv: &http.Server{
|
||
Addr: addr,
|
||
Handler: r,
|
||
ReadTimeout: 10 * time.Second,
|
||
WriteTimeout: 10 * time.Second,
|
||
},
|
||
}
|
||
}
|
||
|
||
// Start 启动 HTTP 服务(goroutine)
|
||
func (s *Server) Start() {
|
||
if !s.enabled {
|
||
return
|
||
}
|
||
go func() {
|
||
addr := s.srv.Addr
|
||
log.Printf("[http] 管理界面已启动: http://%s/", addr)
|
||
if err := s.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||
log.Printf("[http] 管理界面启动失败: %v", err)
|
||
}
|
||
}()
|
||
}
|
||
|
||
// Close 优雅关闭
|
||
func (s *Server) Close() error {
|
||
if !s.enabled {
|
||
return nil
|
||
}
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
return s.srv.Shutdown(ctx)
|
||
}
|
||
|
||
// Enabled 返回是否启用
|
||
func (s *Server) Enabled() bool { return s.enabled }
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 路由处理
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// serveIndex 返回管理页面
|
||
func serveIndex(c *gin.Context) {
|
||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||
c.String(http.StatusOK, indexHTML)
|
||
}
|
||
|
||
// serveHome 主页(meshmap 占位)
|
||
func serveHome(c *gin.Context) {
|
||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||
c.String(http.StatusOK, `<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head><meta charset="UTF-8"><title>meshgo</title></head>
|
||
<body style="font-family:sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#f0f2f5">
|
||
<div style="text-align:center">
|
||
<h1 style="font-size:48px;color:#1677ff;margin-bottom:8px">meshmap</h1>
|
||
<p style="color:#888;margin-bottom:24px">Mesh Network Map · 网格拓扑可视化</p>
|
||
<a href="/admin/mqtt" style="display:inline-block;padding:10px 24px;background:#1677ff;color:#fff;text-decoration:none;border-radius:6px;font-size:14px">MQTT 管理面板</a>
|
||
</div>
|
||
</body>
|
||
</html>`)
|
||
}
|
||
|
||
// handleStats 返回实时统计(JSON)
|
||
func handleStats(c *gin.Context) {
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"code": 0,
|
||
"data": stats.GetStats(),
|
||
})
|
||
}
|
||
|
||
// handleClients 返回在线客户端列表(JSON)
|
||
func handleClients(c *gin.Context) {
|
||
st := stats.GetStats()
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"code": 0,
|
||
"data": st.Clients,
|
||
})
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 认证中间件(预留扩展点)
|
||
// AuthMiddleware 根据 cfg.AuthType 选择对应认证策略
|
||
// 当前支持:none
|
||
// 预留支持:basic、token
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// AuthMiddleware 认证中间件工厂
|
||
func AuthMiddleware(cfg *config.AdminConfig) gin.HandlerFunc {
|
||
switch cfg.AuthType {
|
||
case "basic":
|
||
return basicAuth(cfg.Username, cfg.Password)
|
||
case "token":
|
||
// TODO: token 认证
|
||
return func(c *gin.Context) { c.Next() }
|
||
default:
|
||
// none:不做认证
|
||
return func(c *gin.Context) { c.Next() }
|
||
}
|
||
}
|
||
|
||
// basicAuth Basic 认证
|
||
func basicAuth(user, pass string) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
u, p, ok := c.Request.BasicAuth()
|
||
if !ok || u != user || p != pass {
|
||
c.Header("WWW-Authenticate", `Basic realm="meshgo"`)
|
||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||
"code": 401,
|
||
"msg": "未授权",
|
||
})
|
||
return
|
||
}
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 模板(go:embed 内嵌)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
//go:embed index.html
|
||
var indexHTML string
|