maintenance.go 11 KB

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