sandbox.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. package nginx
  2. import (
  3. "fmt"
  4. "os"
  5. "path/filepath"
  6. "regexp"
  7. "strings"
  8. "github.com/0xJacky/Nginx-UI/internal/helper"
  9. "github.com/0xJacky/Nginx-UI/settings"
  10. "github.com/uozi-tech/cosy/logger"
  11. )
  12. // Site represents minimal site info needed for sandbox testing
  13. type SandboxSite struct {
  14. Path string
  15. }
  16. // Stream represents minimal stream info needed for sandbox testing
  17. type SandboxStream struct {
  18. Path string
  19. }
  20. // NamespaceInfo represents minimal namespace info for sandbox
  21. type NamespaceInfo struct {
  22. ID uint64
  23. Name string
  24. DeployMode string
  25. }
  26. // SandboxTestConfigWithPaths tests nginx config in an isolated sandbox with provided paths
  27. func SandboxTestConfigWithPaths(namespace *NamespaceInfo, sitePaths, streamPaths []string) (stdOut string, stdErr error) {
  28. mutex.Lock()
  29. defer mutex.Unlock()
  30. // If custom test command is set, use it (no sandbox support)
  31. if settings.NginxSettings.TestConfigCmd != "" {
  32. return execShell(settings.NginxSettings.TestConfigCmd)
  33. }
  34. // Skip local test for remote-only namespaces
  35. if namespace != nil && namespace.DeployMode == "remote" {
  36. return "Config validation skipped for remote-only namespace", nil
  37. }
  38. // Create sandbox and test
  39. sandbox, err := createSandbox(namespace, sitePaths, streamPaths)
  40. if err != nil {
  41. logger.Errorf("Failed to create sandbox: %v", err)
  42. return TestConfig() // Fallback to normal test
  43. }
  44. defer sandbox.Cleanup()
  45. // Test the sandbox config
  46. sbin := GetSbinPath()
  47. if sbin == "" {
  48. sbin = "nginx"
  49. }
  50. return execCommand(sbin, "-t", "-c", sandbox.ConfigPath)
  51. }
  52. // Sandbox represents an isolated nginx test environment
  53. type Sandbox struct {
  54. Dir string
  55. ConfigPath string
  56. Namespace *NamespaceInfo
  57. }
  58. // createSandbox creates an isolated nginx configuration environment for testing
  59. func createSandbox(namespace *NamespaceInfo, sitePaths, streamPaths []string) (*Sandbox, error) {
  60. // Create temp directory for sandbox
  61. tempDir, err := os.MkdirTemp("", "nginx-ui-sandbox-*")
  62. if err != nil {
  63. return nil, fmt.Errorf("failed to create sandbox temp dir: %w", err)
  64. }
  65. sandbox := &Sandbox{
  66. Dir: tempDir,
  67. Namespace: namespace,
  68. }
  69. // Copy necessary directories to sandbox for complete isolation
  70. if err := copySandboxDependencies(tempDir); err != nil {
  71. os.RemoveAll(tempDir)
  72. return nil, fmt.Errorf("failed to copy sandbox dependencies: %w", err)
  73. }
  74. // Generate sandbox nginx.conf
  75. configContent, err := generateSandboxConfig(namespace, sitePaths, streamPaths, tempDir)
  76. if err != nil {
  77. os.RemoveAll(tempDir)
  78. return nil, fmt.Errorf("failed to generate sandbox config: %w", err)
  79. }
  80. // Write sandbox nginx.conf
  81. sandbox.ConfigPath = filepath.Join(tempDir, "nginx.conf")
  82. if err := os.WriteFile(sandbox.ConfigPath, []byte(configContent), 0644); err != nil {
  83. os.RemoveAll(tempDir)
  84. return nil, fmt.Errorf("failed to write sandbox config: %w", err)
  85. }
  86. logger.Debugf("Created sandbox at %s for namespace: %v", tempDir, namespace)
  87. return sandbox, nil
  88. }
  89. // copySandboxDependencies copies necessary config directories to sandbox
  90. func copySandboxDependencies(sandboxDir string) error {
  91. confBase := GetConfPath()
  92. // Directories to copy for complete isolation
  93. dirsToCopy := []string{
  94. "conf.d",
  95. "modules-enabled",
  96. "snippets", // Common nginx snippets directory
  97. }
  98. for _, dir := range dirsToCopy {
  99. srcDir := filepath.Join(confBase, dir)
  100. dstDir := filepath.Join(sandboxDir, dir)
  101. // Check if source directory exists
  102. if !helper.FileExists(srcDir) {
  103. continue // Skip non-existent directories
  104. }
  105. // Create destination directory
  106. if err := os.MkdirAll(dstDir, 0755); err != nil {
  107. return fmt.Errorf("failed to create %s: %w", dir, err)
  108. }
  109. // Copy all files from source to destination
  110. entries, err := os.ReadDir(srcDir)
  111. if err != nil {
  112. logger.Warnf("Failed to read %s: %v, skipping", srcDir, err)
  113. continue
  114. }
  115. for _, entry := range entries {
  116. if entry.IsDir() {
  117. continue // Skip subdirectories for now
  118. }
  119. srcFile := filepath.Join(srcDir, entry.Name())
  120. dstFile := filepath.Join(dstDir, entry.Name())
  121. content, err := os.ReadFile(srcFile)
  122. if err != nil {
  123. logger.Warnf("Failed to read %s: %v, skipping", srcFile, err)
  124. continue
  125. }
  126. if err := os.WriteFile(dstFile, content, 0644); err != nil {
  127. logger.Warnf("Failed to write %s: %v, skipping", dstFile, err)
  128. continue
  129. }
  130. }
  131. logger.Debugf("Copied %s to sandbox", dir)
  132. }
  133. // Also copy mime.types if exists
  134. mimeTypes := filepath.Join(confBase, "mime.types")
  135. if helper.FileExists(mimeTypes) {
  136. content, err := os.ReadFile(mimeTypes)
  137. if err == nil {
  138. os.WriteFile(filepath.Join(sandboxDir, "mime.types"), content, 0644)
  139. }
  140. }
  141. return nil
  142. }
  143. // Cleanup removes the sandbox directory
  144. func (s *Sandbox) Cleanup() {
  145. if s.Dir != "" {
  146. if err := os.RemoveAll(s.Dir); err != nil {
  147. logger.Warnf("Failed to cleanup sandbox %s: %v", s.Dir, err)
  148. } else {
  149. logger.Debugf("Cleaned up sandbox: %s", s.Dir)
  150. }
  151. }
  152. }
  153. // generateSandboxConfig generates a minimal nginx.conf that only includes configs from specified paths
  154. func generateSandboxConfig(namespace *NamespaceInfo, sitePaths, streamPaths []string, sandboxDir string) (string, error) {
  155. // Read the main nginx.conf to get basic structure
  156. mainConfPath := GetConfEntryPath()
  157. mainConf, err := os.ReadFile(mainConfPath)
  158. if err != nil {
  159. return "", fmt.Errorf("failed to read main nginx.conf: %w", err)
  160. }
  161. mainConfStr := string(mainConf)
  162. // Generate include patterns based on provided paths
  163. var includePatterns []string
  164. // Add site includes
  165. for _, sitePath := range sitePaths {
  166. siteEnabledPath := GetConfPath("sites-enabled", filepath.Base(sitePath))
  167. if helper.FileExists(siteEnabledPath) {
  168. includePatterns = append(includePatterns, fmt.Sprintf(" include %s;", siteEnabledPath))
  169. }
  170. }
  171. // Add stream includes
  172. for _, streamPath := range streamPaths {
  173. streamEnabledPath := GetConfPath("streams-enabled", filepath.Base(streamPath))
  174. if helper.FileExists(streamEnabledPath) {
  175. includePatterns = append(includePatterns, fmt.Sprintf(" include %s;", streamEnabledPath))
  176. }
  177. }
  178. // If no paths provided, test all enabled configs (original behavior)
  179. if len(includePatterns) == 0 {
  180. sitesEnabledDir := GetConfPath("sites-enabled")
  181. streamsEnabledDir := GetConfPath("streams-enabled")
  182. includePatterns = append(includePatterns, fmt.Sprintf(" include %s/*;", sitesEnabledDir))
  183. includePatterns = append(includePatterns, fmt.Sprintf(" include %s/*;", streamsEnabledDir))
  184. }
  185. // Replace include directives with sandbox-specific ones
  186. sandboxConf := replaceIncludeDirectives(mainConfStr, includePatterns, sandboxDir)
  187. return sandboxConf, nil
  188. }
  189. // replaceIncludeDirectives replaces only sites-enabled and streams-enabled includes
  190. // Rewrites other includes (conf.d, mime.types, etc.) to use sandbox paths
  191. func replaceIncludeDirectives(mainConf string, includePatterns []string, sandboxDir string) string {
  192. lines := strings.Split(mainConf, "\n")
  193. var result []string
  194. insideHTTP := false
  195. insideStream := false
  196. httpIncludesAdded := false
  197. streamIncludesAdded := false
  198. for _, line := range lines {
  199. trimmed := strings.TrimSpace(line)
  200. // Track http and stream blocks
  201. if strings.HasPrefix(trimmed, "http") && strings.Contains(trimmed, "{") {
  202. insideHTTP = true
  203. result = append(result, line)
  204. continue
  205. }
  206. if strings.HasPrefix(trimmed, "stream") && strings.Contains(trimmed, "{") {
  207. insideStream = true
  208. result = append(result, line)
  209. continue
  210. }
  211. // Handle include directives
  212. if strings.Contains(trimmed, "include") {
  213. isSitesEnabled := strings.Contains(trimmed, "sites-enabled")
  214. isStreamsEnabled := strings.Contains(trimmed, "streams-enabled")
  215. // If it's sites-enabled or streams-enabled, replace it
  216. if isSitesEnabled || isStreamsEnabled {
  217. // Add our sandbox-specific includes at the first occurrence
  218. if insideHTTP && isSitesEnabled && !httpIncludesAdded {
  219. result = append(result, " # Sandbox-specific includes (generated for isolated testing)")
  220. for _, pattern := range includePatterns {
  221. if strings.Contains(pattern, "sites-enabled") {
  222. result = append(result, pattern)
  223. }
  224. }
  225. httpIncludesAdded = true
  226. }
  227. if insideStream && isStreamsEnabled && !streamIncludesAdded {
  228. result = append(result, " # Sandbox-specific includes (generated for isolated testing)")
  229. for _, pattern := range includePatterns {
  230. if strings.Contains(pattern, "streams-enabled") {
  231. result = append(result, pattern)
  232. }
  233. }
  234. streamIncludesAdded = true
  235. }
  236. continue // Skip the original include line
  237. }
  238. // Rewrite other includes to use sandbox paths
  239. rewrittenLine := rewriteIncludePath(line, sandboxDir)
  240. result = append(result, rewrittenLine)
  241. continue
  242. }
  243. // Detect end of http/stream block
  244. if strings.Contains(line, "}") {
  245. if insideHTTP {
  246. // Add includes before closing http block if not added yet
  247. if !httpIncludesAdded {
  248. result = append(result, " # Sandbox-specific includes (generated for isolated testing)")
  249. for _, pattern := range includePatterns {
  250. if strings.Contains(pattern, "sites-enabled") {
  251. result = append(result, pattern)
  252. }
  253. }
  254. httpIncludesAdded = true
  255. }
  256. insideHTTP = false
  257. }
  258. if insideStream {
  259. // Add includes before closing stream block if not added yet
  260. if !streamIncludesAdded {
  261. result = append(result, " # Sandbox-specific includes (generated for isolated testing)")
  262. for _, pattern := range includePatterns {
  263. if strings.Contains(pattern, "streams-enabled") {
  264. result = append(result, pattern)
  265. }
  266. }
  267. streamIncludesAdded = true
  268. }
  269. insideStream = false
  270. }
  271. }
  272. result = append(result, line)
  273. }
  274. return strings.Join(result, "\n")
  275. }
  276. // rewriteIncludePath rewrites include paths to use sandbox directory
  277. func rewriteIncludePath(line, sandboxDir string) string {
  278. // Extract the include path using regex
  279. // Match: include /path/to/file; or include /path/*.conf;
  280. includeRegex := regexp.MustCompile(`include\s+([^;]+);`)
  281. matches := includeRegex.FindStringSubmatch(line)
  282. if len(matches) < 2 {
  283. return line // No match, return original
  284. }
  285. origPath := strings.TrimSpace(matches[1])
  286. confBase := GetConfPath()
  287. // Paths to rewrite to sandbox
  288. rewritePaths := map[string]string{
  289. filepath.Join(confBase, "conf.d"): filepath.Join(sandboxDir, "conf.d"),
  290. filepath.Join(confBase, "modules-enabled"): filepath.Join(sandboxDir, "modules-enabled"),
  291. filepath.Join(confBase, "snippets"): filepath.Join(sandboxDir, "snippets"),
  292. filepath.Join(confBase, "mime.types"): filepath.Join(sandboxDir, "mime.types"),
  293. }
  294. // Check if path starts with any of the rewrite paths
  295. newPath := origPath
  296. for oldPrefix, newPrefix := range rewritePaths {
  297. if strings.HasPrefix(origPath, oldPrefix) {
  298. newPath = strings.Replace(origPath, oldPrefix, newPrefix, 1)
  299. break
  300. }
  301. }
  302. // Replace in the original line
  303. return strings.Replace(line, origPath, newPath, 1)
  304. }