mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-11-07 00:05:14 +00:00
Installs that enabled ngx_brotli before v2.3.0 have no http.d include in nginx.conf, because the include is only inserted when a module is toggled or built. The settings page reported this as "cannot write the managed configuration" and skipped the brotli save, so the save path that inserts the include was never reached, even on a stock nginx.conf. The settings response now tells the two cases apart with an in-memory insertion check that writes nothing: - include missing but insertable, or managed file missing: managedPending, the page explains the values take effect after saving and saves normally - no safe insertion point: managedUnavailable, unchanged The save path now inserts the include through ensureNginxHTTPIncludeActive, the same function module activation uses, so both produce identical results. A sentinel error keeps "no insertion point" mapped to ErrBrotliUnsupported while I/O failures are still returned as-is.
262 lines
8.9 KiB
Go
262 lines
8.9 KiB
Go
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?$`)
|
|
|
|
// errNginxHTTPIncludeNoInsertionPoint tells a config the panel cannot edit
|
|
// safely apart from an I/O failure while editing it.
|
|
errNginxHTTPIncludeNoInsertionPoint = errors.New("no insertion point for the http.d include in nginx.conf")
|
|
)
|
|
|
|
// 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 "", errNginxHTTPIncludeNoInsertionPoint
|
|
}
|
|
|
|
// nginxHTTPIncludeInsertable reports whether nginx.conf already loads http.d
|
|
// or the include can be added automatically. Nothing is written: the
|
|
// insertion is computed in memory and discarded.
|
|
func nginxHTTPIncludeInsertable(install model.AppInstall) bool {
|
|
content, err := os.ReadFile(nginxMainConfigPath(install))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
_, err = insertNginxHTTPInclude(string(content))
|
|
return err == nil
|
|
}
|
|
|
|
// 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
|
|
}
|