123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388 |
- 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 := nginx.TestConf()
- 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 = nginx.Reload()
- 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 := nginx.TestConf()
- 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 = nginx.Reload()
- 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()
- }
|