Files
GoTunnel/internal/server/router/router.go
Flik 02f8c521c2
Some checks failed
Build Multi-Platform Binaries / build-frontend (push) Successful in 56s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 7m28s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 2m44s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 7m27s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 1m18s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 5m49s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 2m17s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 5m4s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 5m57s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 4m36s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Failing after 13m47s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Failing after 26m24s
feat: update plugin handling to use unique PluginID across client and server interactions
2026-01-04 21:31:36 +08:00

202 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package router
import (
"io"
"io/fs"
"net/http"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/gotunnel/internal/server/router/handler"
"github.com/gotunnel/internal/server/router/middleware"
"github.com/gotunnel/pkg/auth"
)
// GinRouter Gin 路由管理器
type GinRouter struct {
Engine *gin.Engine
}
// New 创建 Gin 路由管理器
func New() *GinRouter {
gin.SetMode(gin.ReleaseMode)
engine := gin.New()
return &GinRouter{Engine: engine}
}
// Handler 返回 http.Handler
func (r *GinRouter) Handler() http.Handler {
return r.Engine
}
// SetupRoutes 配置所有路由
func (r *GinRouter) SetupRoutes(app handler.AppInterface, jwtAuth *auth.JWTAuth, username, password string) {
engine := r.Engine
// 全局中间件
engine.Use(middleware.Recovery())
engine.Use(middleware.Logger())
engine.Use(middleware.CORS())
// Swagger 文档
engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// 认证路由 (无需 JWT)
authHandler := handler.NewAuthHandler(username, password, jwtAuth)
engine.POST("/api/auth/login", authHandler.Login)
engine.GET("/api/auth/check", authHandler.Check)
// API 路由 (需要 JWT)
api := engine.Group("/api")
api.Use(middleware.JWTAuth(jwtAuth))
{
// 状态
statusHandler := handler.NewStatusHandler(app)
api.GET("/status", statusHandler.GetStatus)
api.GET("/update/version", statusHandler.GetVersion)
// 客户端管理
clientHandler := handler.NewClientHandler(app)
api.GET("/clients", clientHandler.List)
api.POST("/clients", clientHandler.Create)
api.GET("/client/:id", clientHandler.Get)
api.PUT("/client/:id", clientHandler.Update)
api.DELETE("/client/:id", clientHandler.Delete)
api.POST("/client/:id/push", clientHandler.PushConfig)
api.POST("/client/:id/disconnect", clientHandler.Disconnect)
api.POST("/client/:id/restart", clientHandler.Restart)
api.POST("/client/:id/install-plugins", clientHandler.InstallPlugins)
api.POST("/client/:id/plugin/:pluginID/:action", clientHandler.PluginAction)
// 配置管理
configHandler := handler.NewConfigHandler(app)
api.GET("/config", configHandler.Get)
api.PUT("/config", configHandler.Update)
api.POST("/config/reload", configHandler.Reload)
// 插件管理
pluginHandler := handler.NewPluginHandler(app)
api.GET("/plugins", pluginHandler.List)
api.POST("/plugin/:name/enable", pluginHandler.Enable)
api.POST("/plugin/:name/disable", pluginHandler.Disable)
api.GET("/rule-schemas", pluginHandler.GetRuleSchemas)
api.GET("/client-plugin/:clientID/:pluginName/config", pluginHandler.GetClientConfig)
api.PUT("/client-plugin/:clientID/:pluginName/config", pluginHandler.UpdateClientConfig)
// JS 插件管理
jsPluginHandler := handler.NewJSPluginHandler(app)
api.GET("/js-plugins", jsPluginHandler.List)
api.POST("/js-plugins", jsPluginHandler.Create)
api.GET("/js-plugin/:name", jsPluginHandler.Get)
api.PUT("/js-plugin/:name", jsPluginHandler.Update)
api.DELETE("/js-plugin/:name", jsPluginHandler.Delete)
api.POST("/js-plugin/:name/push/:clientID", jsPluginHandler.PushToClient)
// 插件商店
storeHandler := handler.NewStoreHandler(app)
api.GET("/store/plugins", storeHandler.ListPlugins)
api.POST("/store/install", storeHandler.Install)
// 更新管理
updateHandler := handler.NewUpdateHandler(app)
api.GET("/update/check/server", updateHandler.CheckServer)
api.GET("/update/check/client", updateHandler.CheckClient)
api.POST("/update/apply/server", updateHandler.ApplyServer)
api.POST("/update/apply/client", updateHandler.ApplyClient)
// 日志管理
logHandler := handler.NewLogHandler(app)
api.GET("/client/:id/logs", logHandler.StreamLogs)
// 插件 API 代理 (通过 Web API 访问客户端插件)
pluginAPIHandler := handler.NewPluginAPIHandler(app)
api.Any("/client/:id/plugin-api/:pluginID/*route", pluginAPIHandler.ProxyRequest)
}
}
// SetupStaticFiles 配置静态文件处理
func (r *GinRouter) SetupStaticFiles(staticFS fs.FS) {
// 使用 NoRoute 处理 SPA 路由
r.Engine.NoRoute(gin.WrapH(&spaHandler{fs: http.FS(staticFS)}))
}
// spaHandler SPA 路由处理器
type spaHandler struct {
fs http.FileSystem
}
func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
// API 请求不应该返回 SPA 页面
if len(path) >= 4 && path[:4] == "/api" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"code":404,"message":"Not Found"}`))
return
}
// 尝试打开请求的文件
f, err := h.fs.Open(path)
if err != nil {
// 文件不存在时,检查是否是静态资源请求
// 静态资源js, css, 图片等)应该返回 404而不是 index.html
if isStaticAsset(path) {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
// 其他路径返回 index.htmlSPA 路由)
f, err = h.fs.Open("index.html")
if err != nil {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
}
defer f.Close()
stat, err := f.Stat()
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if stat.IsDir() {
f.Close()
f, err = h.fs.Open(path + "/index.html")
if err != nil {
f, _ = h.fs.Open("index.html")
}
stat, _ = f.Stat()
}
if seeker, ok := f.(io.ReadSeeker); ok {
http.ServeContent(w, r, path, stat.ModTime(), seeker)
}
}
// isStaticAsset 检查路径是否是静态资源
func isStaticAsset(path string) bool {
staticExtensions := []string{
".js", ".css", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico",
".woff", ".woff2", ".ttf", ".eot", ".map", ".json",
}
for _, ext := range staticExtensions {
if len(path) > len(ext) && path[len(path)-len(ext):] == ext {
return true
}
}
return false
}
// Re-export types from handler package for backward compatibility
type (
ServerInterface = handler.ServerInterface
AppInterface = handler.AppInterface
ConfigField = handler.ConfigField
RuleSchema = handler.RuleSchema
PluginInfo = handler.PluginInfo
JSPluginInstallRequest = handler.JSPluginInstallRequest
)