| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387 | package siteimport (	"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/logger"	cSettings "github.com/uozi-tech/cosy/settings")const MaintenanceSuffix = "_nginx_ui_maintenance"// EnableMaintenance enables maintenance mode for a sitefunc 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	res := nginx.Control(nginx.TestConfig)	if res.IsError() {		// Configuration error, cleanup and revert		_ = os.Remove(maintenanceConfigPath)		if helper.FileExists(originalEnabledPath + "_backup") {			_ = os.Rename(originalEnabledPath+"_backup", originalEnabledPath)		}		return res.GetError()	}	// Reload nginx	res = nginx.Control(nginx.Reload)	if res.IsError() {		return res.GetError()	}	// Synchronize with other nodes	go syncEnableMaintenance(name)	return nil}// DisableMaintenance disables maintenance mode for a sitefunc 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	res := nginx.Control(nginx.TestConfig)	if res.IsError() {		// Configuration error, cleanup and revert		_ = os.Remove(enabledConfigFilePath)		_ = os.Symlink(configFilePath, maintenanceConfigPath)		return res.GetError()	}	// Reload nginx	res = nginx.Control(nginx.Reload)	if res.IsError() {		return res.GetError()	}	// Synchronize with other nodes	go syncDisableMaintenance(name)	return nil}// createMaintenanceConfig creates a maintenance configuration based on the original configfunc 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 configurationfunc 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 blockfunc 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 directivefunc 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 nodesfunc syncEnableMaintenance(name string) {	nodes := getSyncNodes(name)	wg := &sync.WaitGroup{}	wg.Add(len(nodes))	for _, node := range nodes {		go func(node *model.Node) {			defer func() {				if err := recover(); err != nil {					buf := make([]byte, 1024)					runtime.Stack(buf, false)					logger.Errorf("%s\n%s", err, buf)				}			}()			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 nodesfunc syncDisableMaintenance(name string) {	nodes := getSyncNodes(name)	wg := &sync.WaitGroup{}	wg.Add(len(nodes))	for _, node := range nodes {		go func(node *model.Node) {			defer func() {				if err := recover(); err != nil {					buf := make([]byte, 1024)					runtime.Stack(buf, false)					logger.Errorf("%s\n%s", err, buf)				}			}()			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()}
 |