package site import ( "fmt" "net/http" "os" "runtime" "strings" "sync" "github.com/0xJacky/Nginx-UI/internal/helper" "github.com/0xJacky/Nginx-UI/internal/nginx" "github.com/0xJacky/Nginx-UI/internal/notification" "github.com/0xJacky/Nginx-UI/model" "github.com/0xJacky/Nginx-UI/settings" "github.com/go-resty/resty/v2" "github.com/tufanbarisyildirim/gonginx/config" "github.com/tufanbarisyildirim/gonginx/parser" "github.com/uozi-tech/cosy" "github.com/uozi-tech/cosy/logger" cSettings "github.com/uozi-tech/cosy/settings" ) const MaintenanceSuffix = "_nginx_ui_maintenance" // EnableMaintenance enables maintenance mode for a site func EnableMaintenance(name string) (err error) { // Check if the site exists in sites-available configFilePath := nginx.GetConfPath("sites-available", name) _, err = os.Stat(configFilePath) if err != nil { return } // Path for the maintenance configuration file maintenanceConfigPath := nginx.GetConfPath("sites-enabled", name+MaintenanceSuffix) // Path for original configuration in sites-enabled originalEnabledPath := nginx.GetConfPath("sites-enabled", name) // Check if the site is already in maintenance mode if helper.FileExists(maintenanceConfigPath) { return } // Read the original configuration file content, err := os.ReadFile(configFilePath) if err != nil { return } // Parse the nginx configuration p := parser.NewStringParser(string(content), parser.WithSkipValidDirectivesErr()) conf, err := p.Parse() if err != nil { return fmt.Errorf("failed to parse nginx configuration: %s", err) } // Create new maintenance configuration maintenanceConfig := createMaintenanceConfig(conf) // Write maintenance configuration to file err = os.WriteFile(maintenanceConfigPath, []byte(maintenanceConfig), 0644) if err != nil { return } // Remove the original symlink from sites-enabled if it exists if helper.FileExists(originalEnabledPath) { err = os.Remove(originalEnabledPath) if err != nil { // If we couldn't remove the original, remove the maintenance file and return the error _ = os.Remove(maintenanceConfigPath) return } } // Test nginx config, if not pass, then restore original configuration output, err := nginx.TestConfig() if err != nil { return } if nginx.GetLogLevel(output) > nginx.Warn { // Configuration error, cleanup and revert _ = os.Remove(maintenanceConfigPath) if helper.FileExists(originalEnabledPath + "_backup") { _ = os.Rename(originalEnabledPath+"_backup", originalEnabledPath) } return cosy.WrapErrorWithParams(ErrNginxTestFailed, output) } // Reload nginx output, err = nginx.Reload() if err != nil { return } if nginx.GetLogLevel(output) > nginx.Warn { return cosy.WrapErrorWithParams(ErrNginxReloadFailed, output) } // Synchronize with other nodes go syncEnableMaintenance(name) return nil } // DisableMaintenance disables maintenance mode for a site func DisableMaintenance(name string) (err error) { // Check if the site is in maintenance mode maintenanceConfigPath := nginx.GetConfPath("sites-enabled", name+MaintenanceSuffix) _, err = os.Stat(maintenanceConfigPath) if err != nil { return } // Original configuration paths configFilePath := nginx.GetConfPath("sites-available", name) enabledConfigFilePath := nginx.GetConfPath("sites-enabled", name) // Check if the original configuration exists _, err = os.Stat(configFilePath) if err != nil { return } // Create symlink to original configuration err = os.Symlink(configFilePath, enabledConfigFilePath) if err != nil { return } // Remove maintenance configuration err = os.Remove(maintenanceConfigPath) if err != nil { // If we couldn't remove the maintenance file, remove the new symlink and return the error _ = os.Remove(enabledConfigFilePath) return } // Test nginx config, if not pass, then revert output, err := nginx.TestConfig() if err != nil { return } if nginx.GetLogLevel(output) > nginx.Warn { // Configuration error, cleanup and revert _ = os.Remove(enabledConfigFilePath) _ = os.Symlink(configFilePath, maintenanceConfigPath) return fmt.Errorf("%s", output) } // Reload nginx output, err = nginx.Reload() if err != nil { return } if nginx.GetLogLevel(output) > nginx.Warn { return fmt.Errorf("%s", output) } // Synchronize with other nodes go syncDisableMaintenance(name) return nil } // createMaintenanceConfig creates a maintenance configuration based on the original config func createMaintenanceConfig(conf *config.Config) string { nginxUIPort := cSettings.ServerSettings.Port schema := "http" if cSettings.ServerSettings.EnableHTTPS { schema = "https" } // Create new configuration ngxConfig := nginx.NewNgxConfig("") // Find all server blocks in the original configuration serverBlocks := findServerBlocks(conf.Block) // Create maintenance mode configuration for each server block for _, server := range serverBlocks { ngxServer := nginx.NewNgxServer() // Copy listen directives listenDirectives := extractDirectives(server, "listen") for _, directive := range listenDirectives { ngxDirective := &nginx.NgxDirective{ Directive: directive.GetName(), Params: strings.Join(extractParams(directive), " "), } ngxServer.Directives = append(ngxServer.Directives, ngxDirective) } // Copy server_name directives serverNameDirectives := extractDirectives(server, "server_name") for _, directive := range serverNameDirectives { ngxDirective := &nginx.NgxDirective{ Directive: directive.GetName(), Params: strings.Join(extractParams(directive), " "), } ngxServer.Directives = append(ngxServer.Directives, ngxDirective) } // Copy SSL certificate directives sslCertDirectives := extractDirectives(server, "ssl_certificate") for _, directive := range sslCertDirectives { ngxDirective := &nginx.NgxDirective{ Directive: directive.GetName(), Params: strings.Join(extractParams(directive), " "), } ngxServer.Directives = append(ngxServer.Directives, ngxDirective) } // Copy SSL certificate key directives sslKeyDirectives := extractDirectives(server, "ssl_certificate_key") for _, directive := range sslKeyDirectives { ngxDirective := &nginx.NgxDirective{ Directive: directive.GetName(), Params: strings.Join(extractParams(directive), " "), } ngxServer.Directives = append(ngxServer.Directives, ngxDirective) } // Copy http2 directives http2Directives := extractDirectives(server, "http2") for _, directive := range http2Directives { ngxDirective := &nginx.NgxDirective{ Directive: directive.GetName(), Params: strings.Join(extractParams(directive), " "), } ngxServer.Directives = append(ngxServer.Directives, ngxDirective) } // Add acme-challenge location acmeChallengeLocation := &nginx.NgxLocation{ Path: "^~ /.well-known/acme-challenge", } // Build location content using string builder var locationContent strings.Builder locationContent.WriteString("proxy_set_header Host $host;\n") locationContent.WriteString("proxy_set_header X-Real-IP $remote_addr;\n") locationContent.WriteString("proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n") locationContent.WriteString(fmt.Sprintf("proxy_pass http://127.0.0.1:%s;\n", settings.CertSettings.HTTPChallengePort)) acmeChallengeLocation.Content = locationContent.String() ngxServer.Locations = append(ngxServer.Locations, acmeChallengeLocation) // Add maintenance mode location location := &nginx.NgxLocation{ Path: "~ .*", } locationContent.Reset() // Build location content using string builder locationContent.WriteString("proxy_set_header Host $host;\n") locationContent.WriteString("proxy_set_header X-Real-IP $remote_addr;\n") locationContent.WriteString("proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n") locationContent.WriteString("proxy_set_header X-Forwarded-Proto $scheme;\n") locationContent.WriteString(fmt.Sprintf("rewrite ^ /pages/maintenance break;\n")) locationContent.WriteString(fmt.Sprintf("proxy_pass %s://127.0.0.1:%d;\n", schema, nginxUIPort)) location.Content = locationContent.String() ngxServer.Locations = append(ngxServer.Locations, location) // Add to configuration ngxConfig.Servers = append(ngxConfig.Servers, ngxServer) } // Generate configuration file content content, err := ngxConfig.BuildConfig() if err != nil { logger.Error("Failed to build maintenance config", err) return "" } return content } // findServerBlocks finds all server blocks in a configuration func findServerBlocks(block config.IBlock) []config.IDirective { var servers []config.IDirective if block == nil { return servers } for _, directive := range block.GetDirectives() { if directive.GetName() == "server" { servers = append(servers, directive) } } return servers } // extractDirectives extracts all directives with a specific name from a server block func extractDirectives(server config.IDirective, name string) []config.IDirective { var directives []config.IDirective if server.GetBlock() == nil { return directives } for _, directive := range server.GetBlock().GetDirectives() { if directive.GetName() == name { directives = append(directives, directive) } } return directives } // extractParams extracts all parameters from a directive func extractParams(directive config.IDirective) []string { var params []string for _, param := range directive.GetParameters() { params = append(params, param.Value) } return params } // syncEnableMaintenance synchronizes enabling maintenance mode with other nodes func syncEnableMaintenance(name string) { nodes := getSyncNodes(name) wg := &sync.WaitGroup{} wg.Add(len(nodes)) for _, node := range nodes { go func(node *model.Environment) { defer func() { if err := recover(); err != nil { buf := make([]byte, 1024) runtime.Stack(buf, false) logger.Error(err) } }() defer wg.Done() client := resty.New() client.SetBaseURL(node.URL) resp, err := client.R(). SetHeader("X-Node-Secret", node.Token). Post(fmt.Sprintf("/api/sites/%s/maintenance", name)) if err != nil { notification.Error("Enable Remote Site Maintenance Error", err.Error(), nil) return } if resp.StatusCode() != http.StatusOK { notification.Error("Enable Remote Site Maintenance Error", "Enable site %{name} maintenance on %{node} failed", NewSyncResult(node.Name, name, resp)) return } notification.Success("Enable Remote Site Maintenance Success", "Enable site %{name} maintenance on %{node} successfully", NewSyncResult(node.Name, name, resp)) }(node) } wg.Wait() } // syncDisableMaintenance synchronizes disabling maintenance mode with other nodes func syncDisableMaintenance(name string) { nodes := getSyncNodes(name) wg := &sync.WaitGroup{} wg.Add(len(nodes)) for _, node := range nodes { go func(node *model.Environment) { defer func() { if err := recover(); err != nil { buf := make([]byte, 1024) runtime.Stack(buf, false) logger.Error(err) } }() defer wg.Done() client := resty.New() client.SetBaseURL(node.URL) resp, err := client.R(). SetHeader("X-Node-Secret", node.Token). Post(fmt.Sprintf("/api/sites/%s/enable", name)) if err != nil { notification.Error("Disable Remote Site Maintenance Error", err.Error(), nil) return } if resp.StatusCode() != http.StatusOK { notification.Error("Disable Remote Site Maintenance Error", "Disable site %{name} maintenance on %{node} failed", NewSyncResult(node.Name, name, resp)) return } notification.Success("Disable Remote Site Maintenance Success", "Disable site %{name} maintenance on %{node} successfully", NewSyncResult(node.Name, name, resp)) }(node) } wg.Wait() }