maintenance.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. package site
  2. import (
  3. "fmt"
  4. "net/http"
  5. "os"
  6. "runtime"
  7. "strings"
  8. "sync"
  9. "github.com/0xJacky/Nginx-UI/internal/helper"
  10. "github.com/0xJacky/Nginx-UI/internal/nginx"
  11. "github.com/0xJacky/Nginx-UI/internal/notification"
  12. "github.com/0xJacky/Nginx-UI/model"
  13. "github.com/0xJacky/Nginx-UI/settings"
  14. "github.com/go-resty/resty/v2"
  15. "github.com/tufanbarisyildirim/gonginx/config"
  16. "github.com/tufanbarisyildirim/gonginx/parser"
  17. "github.com/uozi-tech/cosy"
  18. "github.com/uozi-tech/cosy/logger"
  19. cSettings "github.com/uozi-tech/cosy/settings"
  20. )
  21. const MaintenanceSuffix = "_nginx_ui_maintenance"
  22. // EnableMaintenance enables maintenance mode for a site
  23. func EnableMaintenance(name string) (err error) {
  24. // Check if the site exists in sites-available
  25. configFilePath := nginx.GetConfPath("sites-available", name)
  26. _, err = os.Stat(configFilePath)
  27. if err != nil {
  28. return
  29. }
  30. // Path for the maintenance configuration file
  31. maintenanceConfigPath := nginx.GetConfPath("sites-enabled", name+MaintenanceSuffix)
  32. // Path for original configuration in sites-enabled
  33. originalEnabledPath := nginx.GetConfPath("sites-enabled", name)
  34. // Check if the site is already in maintenance mode
  35. if helper.FileExists(maintenanceConfigPath) {
  36. return
  37. }
  38. // Read the original configuration file
  39. content, err := os.ReadFile(configFilePath)
  40. if err != nil {
  41. return
  42. }
  43. // Parse the nginx configuration
  44. p := parser.NewStringParser(string(content), parser.WithSkipValidDirectivesErr())
  45. conf, err := p.Parse()
  46. if err != nil {
  47. return fmt.Errorf("failed to parse nginx configuration: %s", err)
  48. }
  49. // Create new maintenance configuration
  50. maintenanceConfig := createMaintenanceConfig(conf)
  51. // Write maintenance configuration to file
  52. err = os.WriteFile(maintenanceConfigPath, []byte(maintenanceConfig), 0644)
  53. if err != nil {
  54. return
  55. }
  56. // Remove the original symlink from sites-enabled if it exists
  57. if helper.FileExists(originalEnabledPath) {
  58. err = os.Remove(originalEnabledPath)
  59. if err != nil {
  60. // If we couldn't remove the original, remove the maintenance file and return the error
  61. _ = os.Remove(maintenanceConfigPath)
  62. return
  63. }
  64. }
  65. // Test nginx config, if not pass, then restore original configuration
  66. output := nginx.TestConf()
  67. if nginx.GetLogLevel(output) > nginx.Warn {
  68. // Configuration error, cleanup and revert
  69. _ = os.Remove(maintenanceConfigPath)
  70. if helper.FileExists(originalEnabledPath + "_backup") {
  71. _ = os.Rename(originalEnabledPath+"_backup", originalEnabledPath)
  72. }
  73. return cosy.WrapErrorWithParams(ErrNginxTestFailed, output)
  74. }
  75. // Reload nginx
  76. output = nginx.Reload()
  77. if nginx.GetLogLevel(output) > nginx.Warn {
  78. return cosy.WrapErrorWithParams(ErrNginxReloadFailed, output)
  79. }
  80. // Synchronize with other nodes
  81. go syncEnableMaintenance(name)
  82. return nil
  83. }
  84. // DisableMaintenance disables maintenance mode for a site
  85. func DisableMaintenance(name string) (err error) {
  86. // Check if the site is in maintenance mode
  87. maintenanceConfigPath := nginx.GetConfPath("sites-enabled", name+MaintenanceSuffix)
  88. _, err = os.Stat(maintenanceConfigPath)
  89. if err != nil {
  90. return
  91. }
  92. // Original configuration paths
  93. configFilePath := nginx.GetConfPath("sites-available", name)
  94. enabledConfigFilePath := nginx.GetConfPath("sites-enabled", name)
  95. // Check if the original configuration exists
  96. _, err = os.Stat(configFilePath)
  97. if err != nil {
  98. return
  99. }
  100. // Create symlink to original configuration
  101. err = os.Symlink(configFilePath, enabledConfigFilePath)
  102. if err != nil {
  103. return
  104. }
  105. // Remove maintenance configuration
  106. err = os.Remove(maintenanceConfigPath)
  107. if err != nil {
  108. // If we couldn't remove the maintenance file, remove the new symlink and return the error
  109. _ = os.Remove(enabledConfigFilePath)
  110. return
  111. }
  112. // Test nginx config, if not pass, then revert
  113. output := nginx.TestConf()
  114. if nginx.GetLogLevel(output) > nginx.Warn {
  115. // Configuration error, cleanup and revert
  116. _ = os.Remove(enabledConfigFilePath)
  117. _ = os.Symlink(configFilePath, maintenanceConfigPath)
  118. return fmt.Errorf("%s", output)
  119. }
  120. // Reload nginx
  121. output = nginx.Reload()
  122. if nginx.GetLogLevel(output) > nginx.Warn {
  123. return fmt.Errorf("%s", output)
  124. }
  125. // Synchronize with other nodes
  126. go syncDisableMaintenance(name)
  127. return nil
  128. }
  129. // createMaintenanceConfig creates a maintenance configuration based on the original config
  130. func createMaintenanceConfig(conf *config.Config) string {
  131. nginxUIPort := cSettings.ServerSettings.Port
  132. schema := "http"
  133. if cSettings.ServerSettings.EnableHTTPS {
  134. schema = "https"
  135. }
  136. // Create new configuration
  137. ngxConfig := nginx.NewNgxConfig("")
  138. // Find all server blocks in the original configuration
  139. serverBlocks := findServerBlocks(conf.Block)
  140. // Create maintenance mode configuration for each server block
  141. for _, server := range serverBlocks {
  142. ngxServer := nginx.NewNgxServer()
  143. // Copy listen directives
  144. listenDirectives := extractDirectives(server, "listen")
  145. for _, directive := range listenDirectives {
  146. ngxDirective := &nginx.NgxDirective{
  147. Directive: directive.GetName(),
  148. Params: strings.Join(extractParams(directive), " "),
  149. }
  150. ngxServer.Directives = append(ngxServer.Directives, ngxDirective)
  151. }
  152. // Copy server_name directives
  153. serverNameDirectives := extractDirectives(server, "server_name")
  154. for _, directive := range serverNameDirectives {
  155. ngxDirective := &nginx.NgxDirective{
  156. Directive: directive.GetName(),
  157. Params: strings.Join(extractParams(directive), " "),
  158. }
  159. ngxServer.Directives = append(ngxServer.Directives, ngxDirective)
  160. }
  161. // Copy SSL certificate directives
  162. sslCertDirectives := extractDirectives(server, "ssl_certificate")
  163. for _, directive := range sslCertDirectives {
  164. ngxDirective := &nginx.NgxDirective{
  165. Directive: directive.GetName(),
  166. Params: strings.Join(extractParams(directive), " "),
  167. }
  168. ngxServer.Directives = append(ngxServer.Directives, ngxDirective)
  169. }
  170. // Copy SSL certificate key directives
  171. sslKeyDirectives := extractDirectives(server, "ssl_certificate_key")
  172. for _, directive := range sslKeyDirectives {
  173. ngxDirective := &nginx.NgxDirective{
  174. Directive: directive.GetName(),
  175. Params: strings.Join(extractParams(directive), " "),
  176. }
  177. ngxServer.Directives = append(ngxServer.Directives, ngxDirective)
  178. }
  179. // Copy http2 directives
  180. http2Directives := extractDirectives(server, "http2")
  181. for _, directive := range http2Directives {
  182. ngxDirective := &nginx.NgxDirective{
  183. Directive: directive.GetName(),
  184. Params: strings.Join(extractParams(directive), " "),
  185. }
  186. ngxServer.Directives = append(ngxServer.Directives, ngxDirective)
  187. }
  188. // Add acme-challenge location
  189. acmeChallengeLocation := &nginx.NgxLocation{
  190. Path: "^~ /.well-known/acme-challenge",
  191. }
  192. // Build location content using string builder
  193. var locationContent strings.Builder
  194. locationContent.WriteString("proxy_set_header Host $host;\n")
  195. locationContent.WriteString("proxy_set_header X-Real-IP $remote_addr;\n")
  196. locationContent.WriteString("proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n")
  197. locationContent.WriteString(fmt.Sprintf("proxy_pass http://127.0.0.1:%s;\n", settings.CertSettings.HTTPChallengePort))
  198. acmeChallengeLocation.Content = locationContent.String()
  199. ngxServer.Locations = append(ngxServer.Locations, acmeChallengeLocation)
  200. // Add maintenance mode location
  201. location := &nginx.NgxLocation{
  202. Path: "~ .*",
  203. }
  204. locationContent.Reset()
  205. // Build location content using string builder
  206. locationContent.WriteString("proxy_set_header Host $host;\n")
  207. locationContent.WriteString("proxy_set_header X-Real-IP $remote_addr;\n")
  208. locationContent.WriteString("proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n")
  209. locationContent.WriteString("proxy_set_header X-Forwarded-Proto $scheme;\n")
  210. locationContent.WriteString(fmt.Sprintf("rewrite ^ /pages/maintenance break;\n"))
  211. locationContent.WriteString(fmt.Sprintf("proxy_pass %s://127.0.0.1:%d;\n", schema, nginxUIPort))
  212. location.Content = locationContent.String()
  213. ngxServer.Locations = append(ngxServer.Locations, location)
  214. // Add to configuration
  215. ngxConfig.Servers = append(ngxConfig.Servers, ngxServer)
  216. }
  217. // Generate configuration file content
  218. content, err := ngxConfig.BuildConfig()
  219. if err != nil {
  220. logger.Error("Failed to build maintenance config", err)
  221. return ""
  222. }
  223. return content
  224. }
  225. // findServerBlocks finds all server blocks in a configuration
  226. func findServerBlocks(block config.IBlock) []config.IDirective {
  227. var servers []config.IDirective
  228. if block == nil {
  229. return servers
  230. }
  231. for _, directive := range block.GetDirectives() {
  232. if directive.GetName() == "server" {
  233. servers = append(servers, directive)
  234. }
  235. }
  236. return servers
  237. }
  238. // extractDirectives extracts all directives with a specific name from a server block
  239. func extractDirectives(server config.IDirective, name string) []config.IDirective {
  240. var directives []config.IDirective
  241. if server.GetBlock() == nil {
  242. return directives
  243. }
  244. for _, directive := range server.GetBlock().GetDirectives() {
  245. if directive.GetName() == name {
  246. directives = append(directives, directive)
  247. }
  248. }
  249. return directives
  250. }
  251. // extractParams extracts all parameters from a directive
  252. func extractParams(directive config.IDirective) []string {
  253. var params []string
  254. for _, param := range directive.GetParameters() {
  255. params = append(params, param.Value)
  256. }
  257. return params
  258. }
  259. // syncEnableMaintenance synchronizes enabling maintenance mode with other nodes
  260. func syncEnableMaintenance(name string) {
  261. nodes := getSyncNodes(name)
  262. wg := &sync.WaitGroup{}
  263. wg.Add(len(nodes))
  264. for _, node := range nodes {
  265. go func(node *model.Environment) {
  266. defer func() {
  267. if err := recover(); err != nil {
  268. buf := make([]byte, 1024)
  269. runtime.Stack(buf, false)
  270. logger.Error(err)
  271. }
  272. }()
  273. defer wg.Done()
  274. client := resty.New()
  275. client.SetBaseURL(node.URL)
  276. resp, err := client.R().
  277. SetHeader("X-Node-Secret", node.Token).
  278. Post(fmt.Sprintf("/api/sites/%s/maintenance", name))
  279. if err != nil {
  280. notification.Error("Enable Remote Site Maintenance Error", err.Error(), nil)
  281. return
  282. }
  283. if resp.StatusCode() != http.StatusOK {
  284. notification.Error("Enable Remote Site Maintenance Error", "Enable site %{name} maintenance on %{node} failed", NewSyncResult(node.Name, name, resp))
  285. return
  286. }
  287. notification.Success("Enable Remote Site Maintenance Success", "Enable site %{name} maintenance on %{node} successfully", NewSyncResult(node.Name, name, resp))
  288. }(node)
  289. }
  290. wg.Wait()
  291. }
  292. // syncDisableMaintenance synchronizes disabling maintenance mode with other nodes
  293. func syncDisableMaintenance(name string) {
  294. nodes := getSyncNodes(name)
  295. wg := &sync.WaitGroup{}
  296. wg.Add(len(nodes))
  297. for _, node := range nodes {
  298. go func(node *model.Environment) {
  299. defer func() {
  300. if err := recover(); err != nil {
  301. buf := make([]byte, 1024)
  302. runtime.Stack(buf, false)
  303. logger.Error(err)
  304. }
  305. }()
  306. defer wg.Done()
  307. client := resty.New()
  308. client.SetBaseURL(node.URL)
  309. resp, err := client.R().
  310. SetHeader("X-Node-Secret", node.Token).
  311. Post(fmt.Sprintf("/api/sites/%s/enable", name))
  312. if err != nil {
  313. notification.Error("Disable Remote Site Maintenance Error", err.Error(), nil)
  314. return
  315. }
  316. if resp.StatusCode() != http.StatusOK {
  317. notification.Error("Disable Remote Site Maintenance Error", "Disable site %{name} maintenance on %{node} failed", NewSyncResult(node.Name, name, resp))
  318. return
  319. }
  320. notification.Success("Disable Remote Site Maintenance Success", "Disable site %{name} maintenance on %{node} successfully", NewSyncResult(node.Name, name, resp))
  321. }(node)
  322. }
  323. wg.Wait()
  324. }