package server import ( "log/slog" "mime" "net/http" "os" "path" "sort" "strings" "time" "kra/app/system/internal/conf" websocket "kra/app/system/internal/integration/websocket" "kra/app/system/internal/server/handler" "kra/app/system/internal/server/httpx" "kra/app/system/internal/server/middleware" "kra/app/system/internal/server/router" "kra/app/system/internal/service" platformmodule "kra/pkg/module" "github.com/gin-gonic/gin" kratoshttp "github.com/go-kratos/kratos/v3/transport/http" ) func NewGinEngine(runtime *conf.Runtime, access *service.AccessControlService, handlers *handler.Set, auth *service.AuthService, security *service.SecurityService, audit *service.AuditRecorder, logger *slog.Logger, version string) *gin.Engine { return NewGinEngineWithRuntime(runtime, access, auth, security, audit, logger, version, platformmodule.NewRuntime(router.NewRoutes(handlers)), nil) } func NewGinEngineWithRuntime(runtime *conf.Runtime, access *service.AccessControlService, auth *service.AuthService, security *service.SecurityService, audit *service.AuditRecorder, logger *slog.Logger, version string, routes *platformmodule.Runtime, ws *websocket.Server) *gin.Engine { gin.SetMode(gin.ReleaseMode) engine := gin.New() if err := engine.SetTrustedProxies(nil); err != nil && logger != nil { logger.Error("disable trusted proxies failed", "error", err) } engine.Use(middleware.RequestMeta(), middleware.Recovery(logger), middleware.AccessLog(runtime, logger, version), middleware.CORS(runtime), middleware.ErrorAudit(logger), middleware.SecurityRateLimit(security)) prefix := "" config := runtime.Admin() if config != nil { prefix = strings.TrimSuffix(config.RouterPrefix, "/") } public := engine.Group(prefix) public.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, "ok") }) private := engine.Group(prefix) // The reference administration behavior installs operation recording after JWT, // password and Casbin/data-scope middleware. Keeping the recorder after the // equivalent Kra guards avoids persisting rejected unauthenticated or // unauthorized requests as successful business operations. private.Use(middleware.Auth(auth), middleware.MustChangePassword(), middleware.AccessControl(runtime, access), middleware.OperationAudit(runtime, audit)) if routes != nil { routes.RegisterRoutes(public, private, engine) } if ws != nil && ws.Enabled() { path := ws.Path() engine.GET(path, func(c *gin.Context) { if !ws.Enabled() || c.Request.URL.Path != ws.Path() { c.Status(http.StatusNotFound) return } if err := ws.HandleRequest(c.Writer, c.Request); err != nil && logger != nil { logger.Warn("websocket request failed", "mod", "websocket", "error", err) } }) } registerSwagger(engine, prefix, version, logger) registerLocalStorage(engine, runtime) engine.NoRoute(func(c *gin.Context) { if ws != nil && ws.Enabled() && c.Request.Method == http.MethodGet && c.Request.URL.Path == ws.Path() { if err := ws.HandleRequest(c.Writer, c.Request); err != nil && logger != nil { logger.Warn("websocket request failed", "mod", "websocket", "error", err) } return } if serveLocalStorage(c, runtime) { return } httpx.Fail(c, "请求的接口不存在") }) logRegisteredRoutes(engine, logger) return engine } func logRegisteredRoutes(engine *gin.Engine, logger *slog.Logger) { if logger == nil { return } routes := append([]gin.RouteInfo(nil), engine.Routes()...) sort.Slice(routes, func(i, j int) bool { if routes[i].Path == routes[j].Path { return routes[i].Method < routes[j].Method } return routes[i].Path < routes[j].Path }) systemLogger := logger.With("mod", "system") for _, route := range routes { systemLogger.Info("router registered", "method", route.Method, "path", route.Path) } systemLogger.Info("router register success", "route_count", len(routes)) } func NewGinServer(c *conf.Server, engine *gin.Engine) *kratoshttp.Server { network, address := "tcp", ":8000" if c != nil && c.Http != nil { if c.Http.Network != "" { network = c.Http.Network } if c.Http.Addr != "" { address = c.Http.Addr } } timeout := 10 * time.Minute if c != nil && c.Http != nil && c.Http.Timeout != nil && c.Http.Timeout.AsDuration() > 0 { timeout = c.Http.Timeout.AsDuration() } server := kratoshttp.NewServer(kratoshttp.Network(network), kratoshttp.Address(address), kratoshttp.Timeout(timeout)) server.HandlePrefix("/", engine) server.Server.ReadHeaderTimeout = 10 * time.Second server.Server.ReadTimeout = timeout server.Server.WriteTimeout = timeout server.Server.IdleTimeout = 2 * time.Minute server.Server.MaxHeaderBytes = 1 << 20 return server } // serveLocalStorage resolves the local path for every request so a config // reload takes effect without rebuilding the Gin engine. func serveLocalStorage(c *gin.Context, runtime *conf.Runtime) bool { config := runtime.Admin() if config == nil || config.Local == nil || config.Local.StorePath == "" { return false } prefix := "/" + strings.Trim(config.Local.PathPrefix, "/") return serveLocalStorageAt(c, runtime, prefix) } func registerLocalStorage(engine *gin.Engine, runtime *conf.Runtime) { config := runtime.Admin() if config == nil || config.Local == nil || config.Local.StorePath == "" || strings.Trim(config.Local.PathPrefix, "/") == "" { return } if config.Storage != nil && config.Storage.Type != "" && config.Storage.Type != "local" { return } prefix := "/" + strings.Trim(config.Local.PathPrefix, "/") if localStorageRouteConflicts(engine.Routes(), prefix) { return } handler := func(c *gin.Context) { if !serveLocalStorageAt(c, runtime, prefix) { c.Status(http.StatusNotFound) } } engine.GET(prefix+"/*filepath", handler) engine.HEAD(prefix+"/*filepath", handler) } func localStorageRouteConflicts(routes []gin.RouteInfo, prefix string) bool { staticRoot := strings.Split(strings.TrimPrefix(prefix, "/"), "/")[0] for _, route := range routes { if route.Method != http.MethodGet && route.Method != http.MethodHead { continue } routeRoot := strings.Split(strings.TrimPrefix(route.Path, "/"), "/")[0] if routeRoot == staticRoot { return true } } return false } func serveLocalStorageAt(c *gin.Context, runtime *conf.Runtime, prefix string) bool { config := runtime.Admin() if config == nil || config.Local == nil || config.Local.StorePath == "" { return false } if config.Storage != nil && config.Storage.Type != "" && config.Storage.Type != "local" { return false } if currentPrefix := "/" + strings.Trim(config.Local.PathPrefix, "/"); currentPrefix != prefix { return false } if prefix == "/" || (c.Request.URL.Path != prefix && !strings.HasPrefix(c.Request.URL.Path, prefix+"/")) { return false } c.Header("X-Content-Type-Options", "nosniff") filename := path.Base(c.Request.URL.Path) if !canServeUploadInline(filename) { c.Header("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename})) } http.StripPrefix(prefix, http.FileServer(filesOnly{FileSystem: http.Dir(config.Local.StorePath)})).ServeHTTP(c.Writer, c.Request) return true } func canServeUploadInline(filename string) bool { switch strings.ToLower(path.Ext(filename)) { case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".ico", ".avif", ".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac", ".mp4", ".webm", ".mov", ".avi", ".mkv": return true default: return false } } type filesOnly struct{ http.FileSystem } func (f filesOnly) Open(name string) (http.File, error) { file, err := f.FileSystem.Open(name) if err != nil { return nil, err } info, statErr := file.Stat() if statErr != nil { _ = file.Close() return nil, statErr } if info.IsDir() { _ = file.Close() return nil, os.ErrPermission } return file, nil }