package service import ( "errors" "fmt" "os" "path" "regexp" "sort" "strings" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/constant" ) const ( // nginxHTTPConfDir holds http-context directives generated by 1Panel. // load_module is a main-context directive and therefore lives in // modules-enabled, which cannot host http-context directives such as // "brotli on". The directory is included by nginx.conf before conf.d so // that per-site configuration keeps overriding these defaults. nginxHTTPConfDir = "http.d" nginxHTTPConfigPrefix = "1panel-http-" nginxHTTPConfigHeader = "# Managed by 1Panel. Manual changes will be overwritten.\n" // nginxHTTPIncludeDirective is the include line that loads the managed // directory. Fresh installs carry it in the shipped nginx.conf; existing // ones get it inserted by the panel the first time a module needs // http-context configuration. nginxHTTPIncludeDirective = "include /usr/local/openresty/nginx/conf/http.d/*.conf;" ) var ( // nginxHTTPIncludeRe matches the include line wherever it appears. The // absolute path prefix, quoting and whitespace are all optional in the // match so a variant written by an older installer or by hand still // counts; a commented-out copy does not. nginxHTTPIncludeRe = regexp.MustCompile(`(?m)^[ \t]*include\s+"?(/usr/local/openresty/nginx/conf/)?http\.d/\*\.conf"?\s*;[ \t]*\r?$`) // nginxConfDIncludeRe locates the site-config include, the preferred // insertion point, and captures its indentation. nginxConfDIncludeRe = regexp.MustCompile(`(?m)^([ \t]*)include\s+"?(/usr/local/openresty/nginx/conf/)?conf\.d/\*\.conf"?\s*;[ \t]*\r?$`) // nginxHTTPBlockStartRe locates the http block opening, the fallback // insertion point, and captures its indentation. nginxHTTPBlockStartRe = regexp.MustCompile(`(?m)^([ \t]*)http[ \t]*\{[ \t]*\r?$`) ) // nginxHTTPIncludePresent reports whether nginx.conf already loads http.d. func nginxHTTPIncludePresent(install model.AppInstall) bool { content, err := os.ReadFile(nginxMainConfigPath(install)) if err != nil { return false } return nginxHTTPIncludeRe.MatchString(string(content)) } // writeNginxFileAtomic writes through a temp file plus rename so a crash or a // concurrent reader never observes a half-written config. func writeNginxFileAtomic(filePath string, content []byte) error { tmpPath := filePath + ".tmp" if err := os.WriteFile(tmpPath, content, constant.FilePerm); err != nil { return err } return os.Rename(tmpPath, filePath) } // nginxFileLineEnding picks the file's own style so an inserted or rewritten // line does not mix LF into a CRLF file. func nginxFileLineEnding(content string) string { if strings.Contains(content, "\r\n") { return "\r\n" } return "\n" } // insertNginxHTTPInclude returns the config with the http.d include added. // // The include goes right before the conf.d include so panel-managed defaults // are evaluated before per-site configuration; without one, it goes at the // top of the http block. The inserted line follows the file's own line-ending // style, and everything else stays byte-identical. A config without a // locatable http block is rejected, and callers degrade instead of failing // their operation over it. func insertNginxHTTPInclude(content string) (string, error) { if nginxHTTPIncludeRe.MatchString(content) { return content, nil } eol := nginxFileLineEnding(content) if m := nginxConfDIncludeRe.FindStringSubmatchIndex(content); m != nil { indent := content[m[2]:m[3]] return content[:m[0]] + indent + nginxHTTPIncludeDirective + eol + content[m[0]:], nil } if m := nginxHTTPBlockStartRe.FindStringSubmatchIndex(content); m != nil { indent := content[m[2]:m[3]] + " " return content[:m[1]] + eol + indent + nginxHTTPIncludeDirective + content[m[1]:], nil } return "", errors.New("no insertion point for the http.d include in nginx.conf") } // ensureNginxHTTPIncludeActive makes nginx.conf load http.d, inserting the // include when missing. It returns whether the directory is loaded after the // call, plus the original config content so the caller can roll back the edit // together with the rest of its changes. func ensureNginxHTTPIncludeActive(install model.AppInstall) (active bool, snapshot []byte, err error) { configPath := nginxMainConfigPath(install) content, readErr := os.ReadFile(configPath) if readErr != nil { return false, nil, readErr } if nginxHTTPIncludeRe.MatchString(string(content)) { if err = os.MkdirAll(nginxHTTPConfigDir(install), constant.DirPerm); err != nil { return false, nil, err } return true, nil, nil } updated, insErr := insertNginxHTTPInclude(string(content)) if insErr != nil { return false, nil, insErr } if err = writeNginxFileAtomic(configPath, []byte(updated)); err != nil { return false, nil, err } if err = os.MkdirAll(nginxHTTPConfigDir(install), constant.DirPerm); err != nil { return false, nil, err } return true, content, nil } // nginxHTTPDirective is a single http-context directive rendered into a // managed file. type nginxHTTPDirective struct { Name string Params []string } func (d nginxHTTPDirective) render() string { if len(d.Params) == 0 { return d.Name + ";" } return d.Name + " " + strings.Join(d.Params, " ") + ";" } func nginxHTTPConfigDir(install model.AppInstall) string { return path.Join(install.GetPath(), nginxModuleConfDir, nginxHTTPConfDir) } func nginxHTTPConfigFileName(order int, name string) string { return fmt.Sprintf("%s%04d-%s.conf", nginxHTTPConfigPrefix, order, nginxModulePathName(name)) } // renderNginxHTTPConfig builds the content of a managed http.d file. func renderNginxHTTPConfig(directives []nginxHTTPDirective) []byte { var content strings.Builder content.WriteString(nginxHTTPConfigHeader) for _, directive := range directives { content.WriteString(directive.render()) content.WriteString("\n") } return []byte(content.String()) } var nginxHTTPDirectiveRe = regexp.MustCompile(`^[ \t]*([a-z_][a-z0-9_]*)[ \t]+([^;]*);[ \t]*$`) // readNginxHTTPDirectives parses a managed file back into directive values. // A missing or unreadable file yields no directives, which makes callers fall // back to their defaults. func readNginxHTTPDirectives(filePath string) map[string][]string { content, err := os.ReadFile(filePath) if err != nil { return nil } directives := make(map[string][]string) for _, line := range strings.Split(string(content), "\n") { match := nginxHTTPDirectiveRe.FindStringSubmatch(line) if match == nil { continue } directives[match[1]] = strings.Fields(match[2]) } return directives } // snapshotManagedNginxHTTPConfigs captures every managed file so a failed // nginx -t can be rolled back. func snapshotManagedNginxHTTPConfigs(configDir string) (nginxModuleConfigSnapshot, error) { snapshot := make(nginxModuleConfigSnapshot) entries, err := os.ReadDir(configDir) if err != nil { if os.IsNotExist(err) { return snapshot, nil } return nil, err } for _, entry := range entries { if entry.IsDir() || !strings.HasPrefix(entry.Name(), nginxHTTPConfigPrefix) { continue } content, readErr := os.ReadFile(path.Join(configDir, entry.Name())) if readErr != nil { return nil, readErr } snapshot[entry.Name()] = content } return snapshot, nil } // applyManagedNginxHTTPConfigs writes the desired managed files and removes // managed files that are no longer wanted. Files not carrying the managed // prefix are never touched. func applyManagedNginxHTTPConfigs(configDir string, desired map[string][]byte) error { if err := os.MkdirAll(configDir, constant.DirPerm); err != nil { return err } entries, err := os.ReadDir(configDir) if err != nil { return err } names := make([]string, 0, len(desired)) for fileName := range desired { names = append(names, fileName) } sort.Strings(names) for _, fileName := range names { tmpPath := path.Join(configDir, "."+fileName+".tmp") if err = os.WriteFile(tmpPath, desired[fileName], constant.FilePerm); err != nil { return err } if err = os.Rename(tmpPath, path.Join(configDir, fileName)); err != nil { return err } } for _, entry := range entries { if entry.IsDir() || !strings.HasPrefix(entry.Name(), nginxHTTPConfigPrefix) { continue } if _, ok := desired[entry.Name()]; !ok { if err = os.Remove(path.Join(configDir, entry.Name())); err != nil && !os.IsNotExist(err) { return err } } } return nil }