package main import ( "context" "crypto/tls" "crypto/x509" "log" "net" "os" "time" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/docs" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/config" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/collector" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/commander" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/handlers" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/service" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto" "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" "golang.org/x/sync/errgroup" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) // @securityDefinitions.apikey Bearer // @in header // @name Authorization // @description Type "Bearer" followed by a space and the JWT token. func main() { cfg_path, ok := os.LookupEnv("CONFIG_FILE") if !ok { cfg_path = "/etc/hellreign/config.yml" } cfg, err := config.ImportSettings(cfg_path) if err != nil { log.Fatalf("Err loading config: %v", err) } db, err := storage.Open(cfg.Database.Token_db) if err != nil { log.Fatalf("Err opening database: %v", err) } defer db.Close() h := handlers.New(db) // Initialize registration tokens table if err := h.Repo.InitRegistrationTokens(); err != nil { log.Printf("Warning: failed to initialize registration tokens table: %v", err) } // Initialize jobs table jobRepo := repository.NewJobRepository(db) if err := jobRepo.Init(context.Background()); err != nil { log.Printf("Warning: failed to initialize jobs table: %v", err) } // Initialize ClickHouse and log repository logRepo := repository.NewLogRepository() if cfg.Database.Clickhouse_host != "" { go func() { db, err := storage.OpenClickHouseWithRetry(storage.ClickHouseConfig{ Host: cfg.Database.Clickhouse_host, User: cfg.Database.Clickhouse_user, Password: cfg.Database.Clickhouse_password, Database: cfg.Database.Clickhouse_database, }, 10, 5*time.Second) if err != nil { log.Printf("Warning: ClickHouse connection failed: %v", err) return } log.Println("ClickHouse connected successfully") logRepo.SetDB(db) if err := logRepo.Init(context.Background()); err != nil { log.Printf("Warning: Failed to initialize logs table: %v", err) } }() } // Initialize Collector (log streaming) with its own ConnTracker collTracker := collector.NewConnTracker() coll := collector.New(logRepo, collTracker) // Initialize ConnTracker for Commander agent lifecycle cmdTracker := commander.NewConnTracker() cmdr := commander.New(jobRepo, cmdTracker) // Initialize script interpreter repository and service scriptRepo := repository.NewScriptInterpreterRepo(db) if err := scriptRepo.Init(context.Background()); err != nil { log.Fatalf("Warning: failed to initialize script interpreters table: %v\n", err) } scriptSvc := service.NewScriptServiceWithInterpreters(h.Repo, scriptRepo) scriptHandlers := handlers.NewScriptHandlers(scriptSvc, cmdTracker, os.Getenv("WHEREAMI")) jobsHandlers := handlers.NewJobsHandlers(cmdTracker, scriptSvc, os.Getenv("WHEREAMI"), /* our address for redirects */ jobRepo, ) scriptManageHandlers := handlers.NewScriptHandlersGroup(scriptSvc, cmdr, os.Getenv("WHEREAMI")) graphPath := os.Getenv("GRAPH_YAML_PATH") if graphPath == "" { graphPath = "/etc/hellreign/services.yaml" } graphHandlers := handlers.NewGraphHandlers(graphPath) agents := handlers.NewAgentsGroup(h, coll) auth := handlers.AuthGroup{Handlers: h} agentReg := handlers.NewAgentRegistrationGroup(h) agentDeploy := handlers.NewAgentDeployGroup(h) // Create admin user from config if not exists if cfg.Admin.Admin_login != "" && cfg.Admin.Admin_password != "" { if !h.Repo.ExistsByLogin(cfg.Admin.Admin_login) { _, err := h.Repo.CreateToken(repository.TokenCreate{ Name: cfg.Admin.Admin_name, LastName: cfg.Admin.Admin_last_name, Login: cfg.Admin.Admin_login, Password: cfg.Admin.Admin_password, PermissionView: true, PermissionManage: true, PermissionAdmin: true, IsActive: true, }) if err != nil { log.Printf("Warning: failed to create admin user: %v", err) } else { log.Println("Admin user created from config") } } else { // Ensure existing admin is activated if err := h.Repo.ActivateUserByLogin(cfg.Admin.Admin_login); err != nil { log.Printf("Warning: failed to activate admin user: %v", err) } else { log.Println("Admin user activated") } } } router := gin.Default() router.Use(handlers.CorsMiddleware("http://127.0.0.1:5173;http://localhost:5173")) docs.SwaggerInfo.BasePath = "/api/v1" docs.SwaggerInfo.Title = "HellreigN" docs.SwaggerInfo.Version = "1.0" docs.SwaggerInfo.Description = "API for HellreigN" docs.SwaggerInfo.Schemes = []string{"http"} router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) v1 := router.Group("/api/v1") { // Auth routes (public) authGroup := v1.Group("/auth") { authGroup.POST("/login", auth.Login) authGroup.POST("/register", auth.RegisterUser) } // Auth token management (requires auth) authTokenGroup := v1.Group("/auth") authTokenGroup.Use(auth.AuthMiddleware()) { authTokenGroup.POST("/token", auth.CreateToken) authTokenGroup.GET("/validate", auth.ValidateToken) authTokenGroup.GET("/tokens", handlers.RequireAdmin(), auth.ListTokens) authTokenGroup.DELETE("/token", auth.DeleteMyToken) authTokenGroup.DELETE("/tokens/:login", handlers.RequireAdmin(), auth.DeleteToken) // User management (admin only) - Full CRUD authTokenGroup.GET("/users/:login", handlers.RequireAdmin(), auth.GetUser) authTokenGroup.PUT("/users/:login", handlers.RequireAdmin(), auth.UpdateUser) authTokenGroup.PUT( "/users/:login/permissions", handlers.RequireAdmin(), auth.UpdateUserPermissions, ) authTokenGroup.PUT( "/users/:login/password", handlers.RequireAdmin(), auth.ResetUserPassword, ) // User activation management (admin only) authTokenGroup.POST( "/users/:login/activate", handlers.RequireAdmin(), auth.ActivateUser, ) authTokenGroup.POST( "/users/:login/deactivate", handlers.RequireAdmin(), auth.DeactivateUser, ) authTokenGroup.GET("/users/inactive", handlers.RequireAdmin(), auth.ListInactiveUsers) } // Agents (requires manage_agent permission) agentsGroup := v1.Group("/agents") agentsGroup.Use(auth.AuthMiddleware(), handlers.RequireManageAgent()) { agentsGroup.GET("", agents.List) agentsGroup.GET("/system-metrics", agents.GetSystemMetrics) } // Jobs (requires admin permission) jobsGroup := v1.Group("/jobs") jobsGroup.Use(auth.AuthMiddleware(), handlers.RequireAdmin()) { jobsGroup.POST("", jobsHandlers.AddJob) jobsGroup.POST("/:id/wait", jobsHandlers.WaitJob) jobsGroup.GET("/metrics", jobsHandlers.GetJobMetrics) } // Service dependency graph (requires admin permission) graphGroup := v1.Group("/graph") graphGroup.Use(auth.AuthMiddleware(), handlers.RequireAdmin()) { graphGroup.GET("", graphHandlers.GetYAML) graphGroup.PUT("", graphHandlers.UpdateYAML) graphGroup.GET("/order", graphHandlers.StartupOrder) graphGroup.GET("/cycle", graphHandlers.CycleCheck) } // Agent registration agentRegGroup := v1.Group("/agents") { agentRegGroup.POST("/register", agentReg.Register) } agentRegTokenGroup := v1.Group("/agents") agentRegTokenGroup.Use(auth.AuthMiddleware(), handlers.RequireManageAgent()) { agentRegTokenGroup.POST("/register-token", agentReg.CreateRegistrationToken) agentRegTokenGroup.POST("/deploy", agentDeploy.DeployAgents) } // Logs (requires view permission) logsGroup := v1.Group("/logs") logsGroup.Use(auth.AuthMiddleware(), handlers.RequireView()) { // Mock logs endpoint (always available, no ClickHouse required) mockLogHandlers := handlers.NewLogHandlers(nil) logsGroup.GET("/mock", mockLogHandlers.GetMockLogs) // ClickHouse log handlers (always registered, work when ClickHouse connects) logHandlers := handlers.NewLogHandlers(logRepo) logsGroup.POST("", logHandlers.Insert) logsGroup.POST("/batch", logHandlers.InsertBatch) logsGroup.GET("", logHandlers.Search) logsGroup.GET("/services", logHandlers.GetServices) logsGroup.GET("/agents", logHandlers.GetAgents) logsGroup.GET("/levels", logHandlers.GetLevels) } // Scripts (requires admin permission) scriptsGroup := v1.Group("/scripts") scriptsGroup.Use(auth.AuthMiddleware(), handlers.RequireAdmin()) { scriptsGroup.POST("/run", scriptHandlers.RunScript) scriptsGroup.GET("/interpreters", scriptHandlers.ListInterpreters) scriptsGroup.POST("/interpreters", scriptHandlers.CreateInterpreter) scriptsGroup.GET("/interpreters/:id", scriptHandlers.GetInterpreter) scriptsGroup.PUT("/interpreters/:id", scriptHandlers.UpdateInterpreter) scriptsGroup.DELETE("/interpreters/:id", scriptHandlers.DeleteInterpreter) // Script management (tree, CRUD) scriptsGroup.GET("/tree", scriptManageHandlers.GetTree) scriptsGroup.POST("", scriptManageHandlers.CreateScript) scriptsGroup.GET("/:id", scriptManageHandlers.GetScript) scriptsGroup.PUT("/:id", scriptManageHandlers.UpdateScript) scriptsGroup.DELETE("/:id", scriptManageHandlers.DeleteScript) scriptsGroup.POST("/:id/run", scriptManageHandlers.RunScriptByID) // Folder management scriptsGroup.POST("/folder", scriptManageHandlers.CreateFolder) scriptsGroup.DELETE("/folder", scriptManageHandlers.DeleteFolder) // Rename script or folder scriptsGroup.POST("/rename", scriptManageHandlers.Rename) // Get script by path scriptsGroup.GET("/by-path", scriptManageHandlers.GetScriptByPath) } } // Start gRPC server with mTLS in background grpcPort := os.Getenv("GRPC_PORT") if grpcPort == "" { grpcPort = "9001" } certDir := os.Getenv("SSL_CERT_DIR") if certDir == "" { certDir = "/var/lib/hellreign/ssl" } certFile := certDir + "/server.crt" keyFile := certDir + "/server.key" caFile := certDir + "/ca.crt" // Load server cert cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { log.Fatalf("Failed to load server cert: %v", err) } // Load CA cert for client verification caCert, err := os.ReadFile(caFile) if err != nil { log.Fatalf("Failed to load CA cert: %v", err) } caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) tlsConfig := &tls.Config{ Certificates: []tls.Certificate{cert}, ClientCAs: caCertPool, ClientAuth: tls.RequireAndVerifyClientCert, MinVersion: tls.VersionTLS12, } grpcServer := grpc.NewServer( grpc.Creds(credentials.NewTLS(tlsConfig)), grpc.StatsHandler(collTracker), grpc.StatsHandler(cmdTracker), ) proto.RegisterCommanderServer(grpcServer, cmdr) proto.RegisterCollectorServer(grpcServer, coll) lis, err := net.Listen("tcp", ":"+grpcPort) if err != nil { log.Fatalf("Failed to listen on gRPC port %s: %v", grpcPort, err) } g, ctx := errgroup.WithContext(context.Background()) g.Go(func() error { log.Printf("gRPC server starting on port %s with mTLS", grpcPort) errCh := make(chan error, 1) go func() { errCh <- grpcServer.Serve(lis) }() select { case err := <-errCh: return err case <-ctx.Done(): grpcServer.GracefulStop() return nil } }) g.Go(func() error { log.Printf("HTTP server starting on :8080") errCh := make(chan error, 1) go func() { errCh <- router.Run(":8080") }() select { case err := <-errCh: return err case <-ctx.Done(): return nil } }) if err := g.Wait(); err != nil { log.Fatalf("Server error: %v", err) } }