maintenance.go 12 KB

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