mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
aa4faddade
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"vitest_failed":0,"vitest_total":116,"measure_s":81}
1662 lines
55 KiB
Go
1662 lines
55 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Package nginx manages OpenResty configuration, runtime, and supporting assets.
|
|
package nginx
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
sharedprotocol "github.com/Rain-kl/Wavelet/pkg/protocol"
|
|
openrestyrender "github.com/Rain-kl/Wavelet/pkg/render/openresty"
|
|
"github.com/Rain-kl/Wavelet/pkg/util"
|
|
|
|
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
|
|
"github.com/Rain-kl/Wavelet/internal/apps/agent/runtimeuser"
|
|
)
|
|
|
|
// RuntimeConfigDirPlaceholder is substituted into generated configs at apply time.
|
|
const RuntimeConfigDirPlaceholder = "__OPENFLARE_RUNTIME_CONFIG_DIR__"
|
|
|
|
// CountryMMDBPathPlaceholder is substituted with the configured Country database path.
|
|
const CountryMMDBPathPlaceholder = "__OPENFLARE_COUNTRY_MMDB_PATH__"
|
|
|
|
// CityMMDBPathPlaceholder is substituted with the configured City database path.
|
|
const CityMMDBPathPlaceholder = "__OPENFLARE_CITY_MMDB_PATH__"
|
|
|
|
// WAFIPGroupsMaxSnapshotBytesPlaceholder is replaced with the shared protocol aggregate snapshot limit.
|
|
const WAFIPGroupsMaxSnapshotBytesPlaceholder = "__OPENFLARE_WAF_IP_GROUPS_MAX_SNAPSHOT_BYTES__"
|
|
|
|
// ResolverDirectivePlaceholder is substituted into generated configs at apply time.
|
|
const ResolverDirectivePlaceholder = "__OPENFLARE_RESOLVER_DIRECTIVE__"
|
|
|
|
// WAFIPGroupsConfigFileName is the runtime filename for synced WAF IP group data.
|
|
const WAFIPGroupsConfigFileName = "waf_ip_groups.json"
|
|
|
|
// WAFIPGroupsChecksumFileName is atomically published after the IP group JSON snapshot.
|
|
const WAFIPGroupsChecksumFileName = WAFIPGroupsConfigFileName + ".checksum"
|
|
const powConfigFileName = "pow_config.json"
|
|
|
|
const (
|
|
nginxConfigFilePerm = 0o644
|
|
nginxPrivateKeyFilePerm = 0o600
|
|
nginxDirPerm = 0o755
|
|
stubStatusCheckTimeout = 1500 * time.Millisecond
|
|
nginxVersionSubmatchCount = 2
|
|
resolverAddressCapacity = 2
|
|
workerInitSubmatchCount = 2
|
|
)
|
|
|
|
// Executor controls OpenResty validation, reload, health, and lifecycle operations.
|
|
type Executor interface {
|
|
Test(ctx context.Context) error
|
|
Reload(ctx context.Context) error
|
|
EnsureRuntime(ctx context.Context, recreate bool) error
|
|
CheckHealth(ctx context.Context) error
|
|
Restart(ctx context.Context) error
|
|
}
|
|
|
|
// CommandRunner executes external commands on behalf of an Executor.
|
|
type CommandRunner interface {
|
|
Run(ctx context.Context, name string, args ...string) ([]byte, error)
|
|
}
|
|
|
|
// OSCommandRunner runs commands using the host operating system.
|
|
type OSCommandRunner struct{}
|
|
|
|
// Run executes the named command and returns its combined output.
|
|
func (r *OSCommandRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
|
|
slog.Debug("OSCommandRunner starting command", "name", name, "args", args)
|
|
tmpFile, err := os.CreateTemp("", "openflare-cmd-*")
|
|
if err != nil {
|
|
slog.Error("OSCommandRunner failed to create temp file, falling back to CombinedOutput", "error", err)
|
|
cmd := exec.CommandContext(ctx, name, args...) //nolint:gosec // command name and args come from trusted OpenResty management paths
|
|
output, outErr := cmd.CombinedOutput()
|
|
slog.Debug("OSCommandRunner finished CombinedOutput", "name", name, "error", outErr)
|
|
return output, outErr
|
|
}
|
|
defer func() { _ = os.Remove(tmpFile.Name()) }()
|
|
|
|
cmd := exec.CommandContext(ctx, name, args...) //nolint:gosec // command name and args come from trusted OpenResty management paths
|
|
cmd.Stdout = tmpFile
|
|
cmd.Stderr = tmpFile
|
|
|
|
slog.Debug("OSCommandRunner executing cmd.Run()", "name", name)
|
|
runErr := cmd.Run()
|
|
slog.Debug("OSCommandRunner cmd.Run() returned", "name", name, "error", runErr)
|
|
_ = tmpFile.Close()
|
|
|
|
output, _ := os.ReadFile(tmpFile.Name())
|
|
slog.Debug("OSCommandRunner command complete", "name", name, "output_len", len(output))
|
|
return output, runErr
|
|
}
|
|
|
|
// PathExecutor runs OpenResty using a configured binary and config path.
|
|
type PathExecutor struct {
|
|
Path string
|
|
ConfigPath string
|
|
Runner CommandRunner
|
|
}
|
|
|
|
// Test validates the current OpenResty configuration.
|
|
func (e *PathExecutor) Test(ctx context.Context) error {
|
|
slog.Debug("running openresty test with binary", "path", e.Path, "config", e.ConfigPath)
|
|
output, err := e.Runner.Run(ctx, e.Path, "-t", "-c", e.ConfigPath)
|
|
if err != nil {
|
|
return fmt.Errorf("openresty -t failed: %w: %s", err, string(output))
|
|
}
|
|
slog.Debug("openresty test succeeded with binary", "path", e.Path)
|
|
return nil
|
|
}
|
|
|
|
// Reload reloads OpenResty or starts it when no runtime process is running.
|
|
func (e *PathExecutor) Reload(ctx context.Context) error {
|
|
slog.Debug("running openresty reload with binary", "path", e.Path, "config", e.ConfigPath)
|
|
output, err := e.Runner.Run(ctx, e.Path, "-s", "reload", "-c", e.ConfigPath)
|
|
if err != nil {
|
|
if isOpenrestyNotRunningError(string(output)) {
|
|
slog.Warn("openresty reload reported runtime is not running, starting binary", "path", e.Path)
|
|
startOutput, startErr := e.Runner.Run(ctx, e.Path, "-c", e.ConfigPath)
|
|
if startErr != nil {
|
|
return fmt.Errorf("openresty reload failed: %w: %s; start failed: %w: %s", err, string(output), startErr, string(startOutput))
|
|
}
|
|
return nil
|
|
}
|
|
return fmt.Errorf("openresty reload failed: %w: %s", err, string(output))
|
|
}
|
|
slog.Debug("openresty reload succeeded with binary", "path", e.Path)
|
|
return nil
|
|
}
|
|
|
|
// EnsureRuntime validates configuration and reloads the OpenResty runtime.
|
|
func (e *PathExecutor) EnsureRuntime(ctx context.Context, _ bool) error {
|
|
if err := e.Test(ctx); err != nil {
|
|
return err
|
|
}
|
|
return e.Reload(ctx)
|
|
}
|
|
|
|
// CheckHealth reports whether the OpenResty configuration is valid.
|
|
func (e *PathExecutor) CheckHealth(ctx context.Context) error {
|
|
return e.Test(ctx)
|
|
}
|
|
|
|
// Restart stops and starts the OpenResty runtime process.
|
|
func (e *PathExecutor) Restart(ctx context.Context) error {
|
|
slog.Info("restarting openresty with binary", "path", e.Path, "config", e.ConfigPath)
|
|
output, err := e.Runner.Run(ctx, e.Path, "-s", "quit", "-c", e.ConfigPath)
|
|
if err != nil {
|
|
text := string(output)
|
|
if !isIgnorableOpenrestyStopError(text) {
|
|
return fmt.Errorf("openresty stop failed: %w: %s", err, text)
|
|
}
|
|
}
|
|
output, err = e.Runner.Run(ctx, e.Path, "-c", e.ConfigPath)
|
|
if err != nil {
|
|
return fmt.Errorf("openresty start failed: %w: %s", err, string(output))
|
|
}
|
|
slog.Info("openresty restart succeeded with binary", "path", e.Path)
|
|
return nil
|
|
}
|
|
|
|
// Manager applies OpenResty configuration and manages runtime assets.
|
|
type Manager struct {
|
|
MainConfigPath string
|
|
RouteConfigPath string
|
|
AccessLogPath string
|
|
CertDir string
|
|
NginxCertDir string
|
|
LuaDir string
|
|
NginxLuaDir string
|
|
RuntimeConfigDir string
|
|
MMDBPath string
|
|
CityMMDBPath string
|
|
PagesDir string
|
|
OpenrestyObservabilityListen string
|
|
OpenrestyObservabilityPort int
|
|
OpenrestyResolverDirective string
|
|
Executor Executor
|
|
atomicFileWriter func(path string, data []byte, perm os.FileMode) error
|
|
wafIPGroupsMu sync.Mutex
|
|
}
|
|
|
|
// ApplyStatus reports the outcome of an OpenResty configuration apply.
|
|
type ApplyStatus string
|
|
|
|
// Apply outcome status values.
|
|
const (
|
|
// ApplyStatusSuccess indicates the configuration was applied successfully.
|
|
ApplyStatusSuccess ApplyStatus = "success"
|
|
ApplyStatusWarning ApplyStatus = "warning"
|
|
ApplyStatusFatal ApplyStatus = "fatal"
|
|
)
|
|
|
|
const safeDefaultFallbackMainConfig = `# This file is generated by OpenFlare safe default fallback.
|
|
user ` + runtimeuser.Name + `;
|
|
worker_processes auto;
|
|
pid __OPENFLARE_PID_PATH__;
|
|
|
|
events {
|
|
worker_connections 1024;
|
|
}
|
|
|
|
http {
|
|
default_type text/plain;
|
|
server_tokens off;
|
|
client_body_temp_path __OPENFLARE_NGINX_CACHE_DIR__/client_temp;
|
|
proxy_temp_path __OPENFLARE_NGINX_CACHE_DIR__/proxy_temp;
|
|
fastcgi_temp_path __OPENFLARE_NGINX_CACHE_DIR__/fastcgi_temp;
|
|
uwsgi_temp_path __OPENFLARE_NGINX_CACHE_DIR__/uwsgi_temp;
|
|
scgi_temp_path __OPENFLARE_NGINX_CACHE_DIR__/scgi_temp;
|
|
|
|
server {
|
|
listen 80 default_server;
|
|
server_name _;
|
|
return 503 "OpenFlare: No Valid Configuration\n";
|
|
}
|
|
%s
|
|
}
|
|
`
|
|
|
|
const safeDefaultFallbackObservabilityServerBlock = `
|
|
server {
|
|
listen %s;
|
|
server_name openflare-observability;
|
|
access_log off;
|
|
|
|
location = /openflare/stub_status {
|
|
stub_status;
|
|
}
|
|
}
|
|
`
|
|
|
|
// ApplyOutcome contains the status and message from a configuration apply.
|
|
type ApplyOutcome struct {
|
|
Status ApplyStatus
|
|
Message string
|
|
}
|
|
|
|
type wafIPGroupsRuntimeConfig struct {
|
|
Groups map[string]protocol.WAFIPGroup `json:"groups"`
|
|
}
|
|
|
|
// Apply writes, validates, and activates new OpenResty configuration files.
|
|
func (m *Manager) Apply(ctx context.Context, mainConfig string, routeConfig string, supportFiles []protocol.SupportFile) ApplyOutcome {
|
|
slog.Info("openresty apply started", "main_config", m.MainConfigPath, "route_config", m.RouteConfigPath, "cert_files", len(supportFiles))
|
|
backup, err := m.backup()
|
|
if err != nil {
|
|
return fatalApplyOutcome(fmt.Errorf("backup openresty config failed: %w", err))
|
|
}
|
|
if err = m.writeTargetFiles(mainConfig, routeConfig, supportFiles); err != nil {
|
|
return m.rollbackAfterFailedApply(ctx, backup, fmt.Errorf("write openresty config failed: %w", err))
|
|
}
|
|
if err = m.activateConfig(ctx); err != nil {
|
|
return m.rollbackAfterFailedApply(ctx, backup, fmt.Errorf("activate openresty runtime failed: %w", err))
|
|
}
|
|
slog.Info("openresty apply completed successfully", "main_config", m.MainConfigPath, "route_config", m.RouteConfigPath)
|
|
return ApplyOutcome{Status: ApplyStatusSuccess}
|
|
}
|
|
|
|
func (m *Manager) writeTargetFiles(mainConfig string, routeConfig string, supportFiles []protocol.SupportFile) error {
|
|
if err := m.EnsureLuaAssets(); err != nil {
|
|
return err
|
|
}
|
|
if err := m.writeCertFiles(supportFiles); err != nil {
|
|
return err
|
|
}
|
|
if err := m.writePowConfig(supportFiles); err != nil {
|
|
return err
|
|
}
|
|
if err := m.writeWAFConfig(supportFiles); err != nil {
|
|
return err
|
|
}
|
|
if err := m.writeSourceConfig(supportFiles); err != nil {
|
|
return err
|
|
}
|
|
if err := m.ensureMimeTypes(); err != nil {
|
|
return err
|
|
}
|
|
if strings.TrimSpace(m.OpenrestyResolverDirective) == "" && strings.Contains(routeConfig, "set $openflare_upstream ") {
|
|
slog.Warn("runtime-resolved hostname upstreams detected without available resolvers; hostname origin requests may fail until resolvers are configured")
|
|
}
|
|
renderedMainConfig := m.renderMainConfig(mainConfig)
|
|
if err := os.WriteFile(m.MainConfigPath, []byte(renderedMainConfig), nginxConfigFilePerm); err != nil {
|
|
return err
|
|
}
|
|
renderedRouteConfig := m.renderRouteConfig(routeConfig)
|
|
if err := os.WriteFile(m.RouteConfigPath, []byte(renderedRouteConfig), nginxConfigFilePerm); err != nil {
|
|
return err
|
|
}
|
|
return m.ensureOpenRestyWorkerReadAccess()
|
|
}
|
|
|
|
// ensureOpenRestyWorkerReadAccess assigns runtime ownership and normalized modes
|
|
// on agent-managed paths so the agent and OpenResty workers share access.
|
|
func (m *Manager) ensureOpenRestyWorkerReadAccess() error {
|
|
targets := []string{
|
|
m.RuntimeConfigDir,
|
|
m.LuaDir,
|
|
m.PagesDir,
|
|
filepath.Dir(m.MainConfigPath),
|
|
filepath.Dir(m.RouteConfigPath),
|
|
}
|
|
if m.AccessLogPath != "" {
|
|
targets = append(targets, filepath.Dir(m.AccessLogPath))
|
|
}
|
|
if pidPath := m.pidRuntimePath(); pidPath != "" {
|
|
targets = append(targets, filepath.Dir(pidPath))
|
|
}
|
|
if proxyCacheDir := m.proxyCacheRuntimeDir(); proxyCacheDir != "" {
|
|
targets = append(targets, proxyCacheDir)
|
|
}
|
|
seen := make(map[string]struct{}, len(targets))
|
|
for _, target := range targets {
|
|
cleaned := filepath.Clean(strings.TrimSpace(target))
|
|
if cleaned == "" || cleaned == "." {
|
|
continue
|
|
}
|
|
if _, ok := seen[cleaned]; ok {
|
|
continue
|
|
}
|
|
seen[cleaned] = struct{}{}
|
|
if err := runtimeuser.EnsurePathOwnership(cleaned, nginxDirPerm, nginxConfigFilePerm); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) activateConfig(ctx context.Context) error {
|
|
if m.Executor == nil {
|
|
return errors.New("executor 未配置")
|
|
}
|
|
if err := m.Executor.Test(ctx); err != nil {
|
|
return err
|
|
}
|
|
return m.Executor.Reload(ctx)
|
|
}
|
|
|
|
func (m *Manager) rollbackAfterFailedApply(ctx context.Context, backup *backupState, applyErr error) ApplyOutcome {
|
|
slog.Warn("openresty apply failed, restoring previous config", "error", applyErr)
|
|
if err := m.restore(backup); err != nil {
|
|
return fatalApplyOutcome(fmt.Errorf("restore openresty backup failed after apply error %w: %w", applyErr, err))
|
|
}
|
|
if err := m.activateConfig(ctx); err != nil {
|
|
if backup != nil && backup.MainExisted {
|
|
return fatalApplyOutcome(fmt.Errorf("apply failed: %w; rollback recovery failed: %w", applyErr, err))
|
|
}
|
|
if fallbackErr := m.EnsureSafeFallbackRuntime(ctx, fmt.Sprintf("apply failed: %v; rollback recovery failed: %v", applyErr, err)); fallbackErr != nil {
|
|
return fatalApplyOutcome(fmt.Errorf("apply failed: %w; rollback recovery failed: %w; fallback recovery failed: %w", applyErr, err, fallbackErr))
|
|
}
|
|
message := fmt.Sprintf("apply failed, but fallback runtime started: %v; rollback recovery failed: %v", applyErr, err)
|
|
slog.Warn("openresty apply recovered with safe default fallback", "message", message)
|
|
return ApplyOutcome{
|
|
Status: ApplyStatusWarning,
|
|
Message: message,
|
|
}
|
|
}
|
|
message := fmt.Sprintf("apply failed, rolled back to previous config: %v", applyErr)
|
|
slog.Warn("openresty apply rolled back successfully", "message", message)
|
|
return ApplyOutcome{
|
|
Status: ApplyStatusWarning,
|
|
Message: message,
|
|
}
|
|
}
|
|
|
|
func fatalApplyOutcome(err error) ApplyOutcome {
|
|
if err == nil {
|
|
return ApplyOutcome{Status: ApplyStatusFatal}
|
|
}
|
|
return ApplyOutcome{
|
|
Status: ApplyStatusFatal,
|
|
Message: strings.TrimSpace(err.Error()),
|
|
}
|
|
}
|
|
|
|
// EnsureLuaAssets synchronizes managed Lua and static assets to the runtime directory.
|
|
func (m *Manager) EnsureLuaAssets() error {
|
|
if strings.TrimSpace(m.LuaDir) == "" {
|
|
return nil
|
|
}
|
|
allSupportFiles := append(ManagedObservabilityLuaFiles(), m.managedPowLuaFiles()...)
|
|
allSupportFiles = append(allSupportFiles, m.managedWAFLuaFiles()...)
|
|
powStaticFiles, err := ManagedPowStaticFiles()
|
|
if err != nil {
|
|
return fmt.Errorf("load pow static files: %w", err)
|
|
}
|
|
allSupportFiles = append(allSupportFiles, powStaticFiles...)
|
|
allSupportFiles = append(allSupportFiles, ManagedSWLuaFiles()...)
|
|
files := make([]managedFile, 0, len(allSupportFiles))
|
|
for _, file := range allSupportFiles {
|
|
targetPath, err := luaFileTargetPath(m.LuaDir, file.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
relativePath, err := filepath.Rel(m.LuaDir, targetPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
files = append(files, managedFile{
|
|
Path: filepath.ToSlash(relativePath),
|
|
Content: []byte(file.Content),
|
|
Mode: nginxConfigFilePerm,
|
|
})
|
|
}
|
|
return syncManagedFiles(m.LuaDir, files)
|
|
}
|
|
|
|
// EnsureRuntime validates and reloads the current OpenResty runtime configuration.
|
|
func (m *Manager) EnsureRuntime(ctx context.Context, recreate bool) error {
|
|
if m.Executor == nil {
|
|
return errors.New("executor 未配置")
|
|
}
|
|
slog.Info("openresty ensure runtime requested", "recreate", recreate)
|
|
return m.Executor.EnsureRuntime(ctx, recreate)
|
|
}
|
|
|
|
// EnsureSafeFallbackRuntime starts a minimal safe default OpenResty runtime.
|
|
func (m *Manager) EnsureSafeFallbackRuntime(ctx context.Context, reason string) error {
|
|
if m.Executor == nil {
|
|
return errors.New("executor 未配置")
|
|
}
|
|
trimmedReason := strings.TrimSpace(reason)
|
|
if trimmedReason == "" {
|
|
trimmedReason = "no valid local openresty config is available"
|
|
}
|
|
slog.Warn("starting openresty safe default fallback runtime", "reason", trimmedReason)
|
|
if err := m.writeSafeDefaultFallbackFiles(); err != nil {
|
|
return fmt.Errorf("write safe default fallback config failed: %w", err)
|
|
}
|
|
if err := m.activateConfig(ctx); err != nil {
|
|
return fmt.Errorf("activate safe default fallback runtime failed: %w", err)
|
|
}
|
|
slog.Warn("openresty safe default fallback runtime started", "main_config", m.MainConfigPath, "route_config", m.RouteConfigPath)
|
|
return nil
|
|
}
|
|
|
|
// CheckHealth verifies that OpenResty configuration and health endpoints are available.
|
|
func (m *Manager) CheckHealth(ctx context.Context) error {
|
|
if m.Executor == nil {
|
|
return errors.New("executor 未配置")
|
|
}
|
|
if m.MainConfigPath != "" {
|
|
if _, err := os.Stat(m.MainConfigPath); os.IsNotExist(err) {
|
|
return errors.New("openresty config not exists: waiting for initial sync")
|
|
}
|
|
}
|
|
if m.OpenrestyObservabilityPort <= 0 {
|
|
return m.Executor.CheckHealth(ctx)
|
|
}
|
|
return m.checkStubStatus(ctx)
|
|
}
|
|
|
|
// Restart restarts the OpenResty runtime process.
|
|
func (m *Manager) Restart(ctx context.Context) error {
|
|
if m.Executor == nil {
|
|
return errors.New("executor 未配置")
|
|
}
|
|
slog.Info("openresty restart requested")
|
|
return m.Executor.Restart(ctx)
|
|
}
|
|
|
|
// CurrentChecksum returns a stable checksum for the active OpenResty configuration bundle.
|
|
func (m *Manager) CurrentChecksum() (string, error) {
|
|
if m.RouteConfigPath == "" {
|
|
return "", errors.New("route config path 不能为空")
|
|
}
|
|
if m.MainConfigPath == "" {
|
|
return "", errors.New("main config path 不能为空")
|
|
}
|
|
mainData, err := os.ReadFile(m.MainConfigPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return "", nil
|
|
}
|
|
return "", err
|
|
}
|
|
data, err := os.ReadFile(m.RouteConfigPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return "", nil
|
|
}
|
|
return "", err
|
|
}
|
|
normalizedMain := string(mainData)
|
|
if includePath := m.routeConfigIncludePath(); includePath != "" {
|
|
normalizedMain = strings.ReplaceAll(normalizedMain, includePath, openrestyrender.RouteConfigPlaceholder)
|
|
}
|
|
if accessLogPath := m.accessLogRuntimePath(); accessLogPath != "" {
|
|
normalizedMain = strings.ReplaceAll(normalizedMain, accessLogPath, openrestyrender.AccessLogPlaceholder)
|
|
errorLogPath := filepath.Join(filepath.Dir(accessLogPath), "error.log")
|
|
normalizedMain = strings.ReplaceAll(normalizedMain, filepath.ToSlash(errorLogPath), openrestyrender.ErrorLogPlaceholder)
|
|
}
|
|
if pidPath := m.pidRuntimePath(); pidPath != "" {
|
|
normalizedMain = strings.ReplaceAll(normalizedMain, filepath.ToSlash(pidPath), openrestyrender.PIDPathPlaceholder)
|
|
}
|
|
if luaDir := m.luaRuntimePath(); luaDir != "" {
|
|
normalizedMain = strings.ReplaceAll(normalizedMain, luaDir, openrestyrender.LuaDirPlaceholder)
|
|
}
|
|
if listen := strings.TrimSpace(m.OpenrestyObservabilityListen); listen != "" {
|
|
normalizedMain = strings.ReplaceAll(normalizedMain, listen, openrestyrender.ObservabilityListenPlaceholder)
|
|
}
|
|
if m.OpenrestyObservabilityPort > 0 {
|
|
normalizedMain = strings.ReplaceAll(normalizedMain, strconv.Itoa(m.OpenrestyObservabilityPort), openrestyrender.ObservabilityPortPlaceholder)
|
|
}
|
|
if resolverDirective := strings.TrimSpace(m.OpenrestyResolverDirective); resolverDirective != "" {
|
|
normalizedMain = strings.ReplaceAll(normalizedMain, resolverDirective, ResolverDirectivePlaceholder)
|
|
}
|
|
normalizedRoute := string(data)
|
|
if m.NginxCertDir != "" {
|
|
// Longer error-page path must be restored before the cert-dir prefix rewrite.
|
|
errorPagePath := filepath.ToSlash(filepath.Join(m.NginxCertDir, openrestyrender.OriginErrorPageSupportPath))
|
|
normalizedRoute = strings.ReplaceAll(normalizedRoute, errorPagePath, openrestyrender.ErrorPageTmplPlaceholder)
|
|
swDir := filepath.ToSlash(filepath.Join(m.NginxCertDir, "sw"))
|
|
normalizedRoute = strings.ReplaceAll(normalizedRoute, swDir, openrestyrender.SWDirPlaceholder)
|
|
normalizedRoute = strings.ReplaceAll(normalizedRoute, m.NginxCertDir, openrestyrender.CertDirPlaceholder)
|
|
}
|
|
if luaDir := m.luaRuntimePath(); luaDir != "" {
|
|
normalizedRoute = strings.ReplaceAll(normalizedRoute, luaDir+"/pow/static", openrestyrender.PowStaticDirPlaceholder)
|
|
normalizedRoute = strings.ReplaceAll(normalizedRoute, luaDir, openrestyrender.LuaDirPlaceholder)
|
|
}
|
|
if pagesDir := m.pagesRuntimePath(); pagesDir != "" {
|
|
normalizedRoute = strings.ReplaceAll(normalizedRoute, pagesDir, openrestyrender.PagesDirPlaceholder)
|
|
}
|
|
files, err := m.readManagedSupportFiles()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
result := bundleChecksum(normalizedMain, normalizedRoute, files)
|
|
slog.Debug("openresty current checksum calculated", "main_config", m.MainConfigPath, "route_config", m.RouteConfigPath, "checksum", result, "cert_files", len(files))
|
|
return result, nil
|
|
}
|
|
|
|
// WAFIPGroupChecksums returns checksums for locally synced WAF IP groups.
|
|
func (m *Manager) WAFIPGroupChecksums() (map[string]string, error) {
|
|
m.wafIPGroupsMu.Lock()
|
|
defer m.wafIPGroupsMu.Unlock()
|
|
|
|
config, err := m.readWAFIPGroupsRuntimeConfig()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err = m.ensureWAFIPGroupsChecksum(); err != nil {
|
|
return nil, err
|
|
}
|
|
result := make(map[string]string, len(config.Groups))
|
|
for id, group := range config.Groups {
|
|
if id != strconv.FormatUint(uint64(group.ID), 10) {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(group.Checksum) != "" {
|
|
result[id] = strings.TrimSpace(group.Checksum)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// ReconcileWAFIPGroups atomically replaces the runtime snapshot with exactly the
|
|
// authoritative target IDs, retaining local definitions that did not change.
|
|
func (m *Manager) ReconcileWAFIPGroups(targetIDs []uint, changed []protocol.WAFIPGroup) error {
|
|
if m.RuntimeConfigDir == "" {
|
|
return nil
|
|
}
|
|
m.wafIPGroupsMu.Lock()
|
|
defer m.wafIPGroupsMu.Unlock()
|
|
|
|
config, err := m.readWAFIPGroupsRuntimeConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
target := make(map[string]protocol.WAFIPGroup, len(targetIDs))
|
|
targetSet := make(map[uint]struct{}, len(targetIDs))
|
|
for _, id := range targetIDs {
|
|
if id == 0 {
|
|
continue
|
|
}
|
|
targetSet[id] = struct{}{}
|
|
key := strconv.FormatUint(uint64(id), 10)
|
|
if group, ok := config.Groups[key]; ok && group.ID == id {
|
|
target[key] = group
|
|
}
|
|
}
|
|
for _, group := range changed {
|
|
if _, ok := targetSet[group.ID]; !ok {
|
|
continue
|
|
}
|
|
target[strconv.FormatUint(uint64(group.ID), 10)] = group
|
|
}
|
|
for _, id := range targetIDs {
|
|
if id == 0 {
|
|
continue
|
|
}
|
|
if _, ok := target[strconv.FormatUint(uint64(id), 10)]; !ok {
|
|
return fmt.Errorf("missing referenced WAF IP group %d after synchronization", id)
|
|
}
|
|
}
|
|
return m.publishWAFIPGroups(target)
|
|
}
|
|
|
|
// UpdateExistingWAFIPGroups applies real-time changes only to groups already
|
|
// present in the authoritative local snapshot.
|
|
func (m *Manager) UpdateExistingWAFIPGroups(changed []protocol.WAFIPGroup) error {
|
|
if m.RuntimeConfigDir == "" || len(changed) == 0 {
|
|
return nil
|
|
}
|
|
m.wafIPGroupsMu.Lock()
|
|
defer m.wafIPGroupsMu.Unlock()
|
|
|
|
config, err := m.readWAFIPGroupsRuntimeConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
updated := false
|
|
for _, group := range changed {
|
|
key := strconv.FormatUint(uint64(group.ID), 10)
|
|
if _, ok := config.Groups[key]; !ok || group.ID == 0 {
|
|
continue
|
|
}
|
|
config.Groups[key] = group
|
|
updated = true
|
|
}
|
|
if !updated {
|
|
return nil
|
|
}
|
|
return m.publishWAFIPGroups(config.Groups)
|
|
}
|
|
|
|
func (m *Manager) publishWAFIPGroups(groups map[string]protocol.WAFIPGroup) error {
|
|
data, err := sharedprotocol.MarshalWAFIPGroupSnapshot(groups)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(data) > sharedprotocol.MaxWAFIPGroupSnapshotBytes {
|
|
return fmt.Errorf("WAF IP group snapshot size %d exceeds maximum %d bytes", len(data), sharedprotocol.MaxWAFIPGroupSnapshotBytes)
|
|
}
|
|
if err := os.MkdirAll(m.RuntimeConfigDir, nginxDirPerm); err != nil {
|
|
return err
|
|
}
|
|
path := filepath.Join(m.RuntimeConfigDir, WAFIPGroupsConfigFileName)
|
|
if err := m.writeAtomicFile(path, data, nginxConfigFilePerm); err != nil {
|
|
return fmt.Errorf("write %s: %w", WAFIPGroupsConfigFileName, err)
|
|
}
|
|
checksumPath := filepath.Join(m.RuntimeConfigDir, WAFIPGroupsChecksumFileName)
|
|
if err := m.writeAtomicFile(checksumPath, []byte(checksum(string(data))+"\n"), nginxConfigFilePerm); err != nil {
|
|
return fmt.Errorf("write %s: %w", WAFIPGroupsChecksumFileName, err)
|
|
}
|
|
slog.Info("synced waf ip groups", "path", path, "group_count", len(groups))
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) ensureWAFIPGroupsChecksum() error {
|
|
if m.RuntimeConfigDir == "" {
|
|
return nil
|
|
}
|
|
jsonPath := filepath.Join(m.RuntimeConfigDir, WAFIPGroupsConfigFileName)
|
|
data, err := os.ReadFile(jsonPath) //nolint:gosec // path is under managed RuntimeConfigDir
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
expected := checksum(string(data))
|
|
checksumPath := filepath.Join(m.RuntimeConfigDir, WAFIPGroupsChecksumFileName)
|
|
current, err := os.ReadFile(checksumPath) //nolint:gosec // path is under managed RuntimeConfigDir
|
|
if err == nil && strings.TrimSpace(string(current)) == expected {
|
|
return nil
|
|
}
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
return m.writeAtomicFile(checksumPath, []byte(expected+"\n"), nginxConfigFilePerm)
|
|
}
|
|
|
|
func (m *Manager) writeAtomicFile(path string, data []byte, perm os.FileMode) error {
|
|
if m.atomicFileWriter != nil {
|
|
return m.atomicFileWriter(path, data, perm)
|
|
}
|
|
return writeAtomicFile(path, data, perm)
|
|
}
|
|
|
|
func writeAtomicFile(path string, data []byte, perm os.FileMode) (resultErr error) {
|
|
tempFile, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".tmp-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tempPath := tempFile.Name()
|
|
closed := false
|
|
defer func() {
|
|
if !closed {
|
|
if closeErr := tempFile.Close(); resultErr == nil && closeErr != nil {
|
|
resultErr = closeErr
|
|
}
|
|
}
|
|
_ = os.Remove(tempPath)
|
|
}()
|
|
if err = tempFile.Chmod(perm); err != nil {
|
|
return err
|
|
}
|
|
if _, err = tempFile.Write(data); err != nil {
|
|
return err
|
|
}
|
|
if err = tempFile.Sync(); err != nil {
|
|
return err
|
|
}
|
|
if err = tempFile.Close(); err != nil {
|
|
return err
|
|
}
|
|
closed = true
|
|
if err = os.Rename(tempPath, path); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) readWAFIPGroupsRuntimeConfig() (*wafIPGroupsRuntimeConfig, error) {
|
|
config := &wafIPGroupsRuntimeConfig{Groups: map[string]protocol.WAFIPGroup{}}
|
|
if m.RuntimeConfigDir == "" {
|
|
return config, nil
|
|
}
|
|
path := filepath.Join(m.RuntimeConfigDir, WAFIPGroupsConfigFileName)
|
|
data, err := os.ReadFile(path) //nolint:gosec // path is under managed RuntimeConfigDir
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return config, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
if len(data) == 0 {
|
|
return config, nil
|
|
}
|
|
if err := json.Unmarshal(data, config); err != nil {
|
|
return nil, err
|
|
}
|
|
if config.Groups == nil {
|
|
config.Groups = map[string]protocol.WAFIPGroup{}
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
// ExecutorOptions configures construction of an OpenResty Executor.
|
|
type ExecutorOptions struct {
|
|
NginxPath string
|
|
MainConfigPath string
|
|
RouteConfigPath string
|
|
CertDir string
|
|
NginxCertDir string
|
|
LuaDir string
|
|
NginxLuaDir string
|
|
OpenrestyObservabilityPort int
|
|
}
|
|
|
|
// NewExecutor creates an Executor backed by a configured OpenResty binary.
|
|
func NewExecutor(options ExecutorOptions) Executor {
|
|
runner := &OSCommandRunner{}
|
|
return &PathExecutor{
|
|
Path: strings.TrimSpace(options.NginxPath),
|
|
ConfigPath: strings.TrimSpace(options.MainConfigPath),
|
|
Runner: runner,
|
|
}
|
|
}
|
|
|
|
// DetectVersion returns the OpenResty version reported by the configured binary.
|
|
func DetectVersion(ctx context.Context, options ExecutorOptions) string {
|
|
version, err := detectVersion(ctx, options, &OSCommandRunner{})
|
|
if err != nil {
|
|
slog.Error("detect openresty version failed", "error", err)
|
|
return ""
|
|
}
|
|
slog.Info("detected openresty version", "version", version)
|
|
return version
|
|
}
|
|
|
|
func detectVersion(ctx context.Context, options ExecutorOptions, runner CommandRunner) (string, error) {
|
|
if runner == nil {
|
|
runner = &OSCommandRunner{}
|
|
}
|
|
if options.NginxPath != "" {
|
|
output, err := runner.Run(ctx, options.NginxPath, "-v")
|
|
if err != nil {
|
|
return "", fmt.Errorf("run runtime -v failed: %w: %s", err, string(output))
|
|
}
|
|
version := parseExtVersion(string(output))
|
|
if version == "" {
|
|
return "", errors.New("cannot parse runtime version from binary output")
|
|
}
|
|
return version, nil
|
|
}
|
|
return "", errors.New("openresty path is empty")
|
|
}
|
|
|
|
func parseExtVersion(output string) string {
|
|
matches := nginxVersionPattern.FindStringSubmatch(output)
|
|
if len(matches) != nginxVersionSubmatchCount {
|
|
return ""
|
|
}
|
|
return matches[1]
|
|
}
|
|
|
|
var nginxVersionPattern = regexp.MustCompile(`(?im)(?:nginx|openresty) version:\s*(?:nginx|openresty)/(\S+)`)
|
|
|
|
func isIgnorableOpenrestyStopError(output string) bool {
|
|
text := strings.ToLower(strings.TrimSpace(output))
|
|
if text == "" {
|
|
return false
|
|
}
|
|
return strings.Contains(text, "invalid pid") || strings.Contains(text, "no such process")
|
|
}
|
|
|
|
func isOpenrestyNotRunningError(output string) bool {
|
|
text := strings.ToLower(strings.TrimSpace(output))
|
|
if text == "" {
|
|
return false
|
|
}
|
|
return strings.Contains(text, "invalid pid") ||
|
|
strings.Contains(text, "no such process") ||
|
|
strings.Contains(text, "open()") && strings.Contains(text, "nginx.pid") && strings.Contains(text, "failed")
|
|
}
|
|
|
|
type backupState struct {
|
|
MainExisted bool
|
|
MainData []byte
|
|
RouteExisted bool
|
|
RouteData []byte
|
|
Files []protocol.SupportFile
|
|
PowConfig *protocol.SupportFile
|
|
WAFConfig *protocol.SupportFile
|
|
SourceConfig *protocol.SupportFile
|
|
}
|
|
|
|
type managedFile struct {
|
|
Path string
|
|
Content []byte
|
|
Mode fs.FileMode
|
|
}
|
|
|
|
func (m *Manager) backup() (*backupState, error) {
|
|
if m.MainConfigPath == "" {
|
|
return nil, errors.New("main config path 不能为空")
|
|
}
|
|
if m.RouteConfigPath == "" {
|
|
return nil, errors.New("route config path 不能为空")
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(m.MainConfigPath), nginxDirPerm); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(m.RouteConfigPath), nginxDirPerm); err != nil {
|
|
return nil, err
|
|
}
|
|
if m.AccessLogPath != "" {
|
|
if err := os.MkdirAll(filepath.Dir(m.AccessLogPath), nginxDirPerm); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if m.CertDir != "" {
|
|
if err := os.MkdirAll(m.CertDir, nginxDirPerm); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if m.RuntimeConfigDir != "" {
|
|
if err := os.MkdirAll(m.RuntimeConfigDir, nginxDirPerm); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
state := &backupState{}
|
|
mainData, err := os.ReadFile(m.MainConfigPath)
|
|
if err == nil {
|
|
state.MainExisted = true
|
|
state.MainData = mainData
|
|
} else if !os.IsNotExist(err) {
|
|
return nil, err
|
|
}
|
|
data, err := os.ReadFile(m.RouteConfigPath)
|
|
if err == nil {
|
|
state.RouteExisted = true
|
|
state.RouteData = data
|
|
} else if !os.IsNotExist(err) {
|
|
return nil, err
|
|
}
|
|
files, err := m.readCertFiles()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
state.Files = files
|
|
powConfig, err := m.readPowConfigFile()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
state.PowConfig = powConfig
|
|
wafConfig, err := m.readRuntimeConfigFile("waf_config.json")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
state.WAFConfig = wafConfig
|
|
sourceConfig, err := m.readRuntimeConfigFile(openrestyrender.SourceConfigFileName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
state.SourceConfig = sourceConfig
|
|
slog.Debug("backup captured", "main_exists", state.MainExisted, "route_exists", state.RouteExisted, "cert_files", len(state.Files))
|
|
return state, nil
|
|
}
|
|
|
|
func (m *Manager) restore(state *backupState) error {
|
|
if state == nil {
|
|
return nil
|
|
}
|
|
slog.Warn("restoring nginx backup", "main_existed", state.MainExisted, "route_existed", state.RouteExisted, "cert_files", len(state.Files))
|
|
if state.MainExisted {
|
|
if err := os.WriteFile(m.MainConfigPath, state.MainData, nginxConfigFilePerm); err != nil {
|
|
return err
|
|
}
|
|
} else if err := os.Remove(m.MainConfigPath); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
if state.RouteExisted {
|
|
if err := os.WriteFile(m.RouteConfigPath, state.RouteData, nginxConfigFilePerm); err != nil {
|
|
return err
|
|
}
|
|
} else if err := os.Remove(m.RouteConfigPath); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
if m.CertDir != "" {
|
|
if err := m.writeManagedCertFiles(state.Files); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := m.restoreRuntimeConfig(state.PowConfig, powConfigFileName); err != nil {
|
|
return err
|
|
}
|
|
if err := m.restoreRuntimeConfig(state.WAFConfig, "waf_config.json"); err != nil {
|
|
return err
|
|
}
|
|
return m.restoreRuntimeConfig(state.SourceConfig, openrestyrender.SourceConfigFileName)
|
|
}
|
|
|
|
func (m *Manager) writeCertFiles(certFiles []protocol.SupportFile) error {
|
|
if m.CertDir == "" {
|
|
return nil
|
|
}
|
|
return m.writeManagedCertFiles(certFiles)
|
|
}
|
|
|
|
// writePowConfig persists legacy pow_config.json for backward compatibility.
|
|
// PoW runtime loads site config from waf_config.json; pow_config.json is deprecated.
|
|
func (m *Manager) writePowConfig(supportFiles []protocol.SupportFile) error {
|
|
if m.RuntimeConfigDir == "" {
|
|
return nil
|
|
}
|
|
configPath := filepath.Join(m.RuntimeConfigDir, powConfigFileName)
|
|
for _, file := range supportFiles {
|
|
if file.Path == powConfigFileName {
|
|
if err := os.WriteFile(configPath, []byte(file.Content), nginxConfigFilePerm); err != nil {
|
|
return fmt.Errorf("write pow_config.json: %w", err)
|
|
}
|
|
slog.Info("wrote pow config", "path", configPath, "size", len(file.Content))
|
|
return nil
|
|
}
|
|
}
|
|
if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("remove pow_config.json: %w", err)
|
|
}
|
|
if err := removeLegacyPowConfig(filepath.Join(m.LuaDir, powConfigFileName)); err != nil {
|
|
return err
|
|
}
|
|
if err := removeLegacyPowConfig(filepath.Join(m.CertDir, powConfigFileName)); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) writeWAFConfig(supportFiles []protocol.SupportFile) error {
|
|
if m.RuntimeConfigDir == "" {
|
|
return nil
|
|
}
|
|
configPath := filepath.Join(m.RuntimeConfigDir, "waf_config.json")
|
|
for _, file := range supportFiles {
|
|
if file.Path == "waf_config.json" {
|
|
if err := os.WriteFile(configPath, []byte(file.Content), nginxConfigFilePerm); err != nil {
|
|
return fmt.Errorf("write waf_config.json: %w", err)
|
|
}
|
|
slog.Info("wrote waf config", "path", configPath, "size", len(file.Content))
|
|
return nil
|
|
}
|
|
}
|
|
if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("remove waf_config.json: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) writeSourceConfig(supportFiles []protocol.SupportFile) error {
|
|
if m.RuntimeConfigDir == "" {
|
|
return nil
|
|
}
|
|
configPath := filepath.Join(m.RuntimeConfigDir, openrestyrender.SourceConfigFileName)
|
|
for _, file := range supportFiles {
|
|
if file.Path == openrestyrender.SourceConfigFileName {
|
|
if err := os.WriteFile(configPath, []byte(file.Content), nginxConfigFilePerm); err != nil {
|
|
return fmt.Errorf("write %s: %w", openrestyrender.SourceConfigFileName, err)
|
|
}
|
|
slog.Info("wrote openresty source config", "path", configPath, "size", len(file.Content))
|
|
return nil
|
|
}
|
|
}
|
|
if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("remove %s: %w", openrestyrender.SourceConfigFileName, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) writeManagedCertFiles(certFiles []protocol.SupportFile) error {
|
|
certFiles = append(certFiles, ManagedSWLuaFiles()...)
|
|
files := make([]managedFile, 0, len(certFiles))
|
|
for _, file := range certFiles {
|
|
if file.Path == powConfigFileName || file.Path == "waf_config.json" || file.Path == openrestyrender.SourceConfigFileName {
|
|
continue
|
|
}
|
|
targetPath, err := m.certFileTargetPath(file.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
relativePath, err := filepath.Rel(m.CertDir, targetPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
files = append(files, managedFile{
|
|
Path: filepath.ToSlash(relativePath),
|
|
Content: []byte(file.Content),
|
|
Mode: certFileMode(file.Path),
|
|
})
|
|
}
|
|
return syncManagedFiles(m.CertDir, files)
|
|
}
|
|
|
|
func (m *Manager) readCertFiles() ([]protocol.SupportFile, error) {
|
|
if m.CertDir == "" {
|
|
return nil, nil
|
|
}
|
|
if _, err := os.Stat(m.CertDir); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
files := make([]protocol.SupportFile, 0)
|
|
err := filepath.Walk(m.CertDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
relativePath, err := filepath.Rel(m.CertDir, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if filepath.ToSlash(relativePath) == powConfigFileName {
|
|
return nil
|
|
}
|
|
data, err := os.ReadFile(path) //nolint:gosec // path is under managed baseDir walk root
|
|
if err != nil {
|
|
return err
|
|
}
|
|
files = append(files, protocol.SupportFile{
|
|
Path: filepath.ToSlash(relativePath),
|
|
Content: string(data),
|
|
})
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sort.Slice(files, func(i int, j int) bool {
|
|
return files[i].Path < files[j].Path
|
|
})
|
|
return files, nil
|
|
}
|
|
|
|
func (m *Manager) readPowConfigFile() (*protocol.SupportFile, error) {
|
|
return m.readRuntimeConfigFile(powConfigFileName)
|
|
}
|
|
|
|
func (m *Manager) readRuntimeConfigFile(name string) (*protocol.SupportFile, error) {
|
|
if m.RuntimeConfigDir == "" {
|
|
return nil, nil
|
|
}
|
|
configPath := filepath.Join(m.RuntimeConfigDir, name)
|
|
data, err := os.ReadFile(configPath) //nolint:gosec // configPath is under managed RuntimeConfigDir
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return &protocol.SupportFile{
|
|
Path: name,
|
|
Content: string(data),
|
|
}, nil
|
|
}
|
|
|
|
func (m *Manager) readManagedSupportFiles() ([]protocol.SupportFile, error) {
|
|
files, err := m.readCertFiles()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Agent-shipped SW Lua assets are not part of the rendered bundle, so they
|
|
// must stay out of the bundle checksum to keep it aligned with the server.
|
|
swManaged := make(map[string]struct{}, len(ManagedSWLuaFiles()))
|
|
for _, file := range ManagedSWLuaFiles() {
|
|
swManaged[file.Path] = struct{}{}
|
|
}
|
|
kept := files[:0]
|
|
for _, file := range files {
|
|
if _, skip := swManaged[file.Path]; skip {
|
|
continue
|
|
}
|
|
kept = append(kept, file)
|
|
}
|
|
files = kept
|
|
powConfig, err := m.readPowConfigFile()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if powConfig != nil {
|
|
files = append(files, *powConfig)
|
|
}
|
|
wafConfig, err := m.readRuntimeConfigFile("waf_config.json")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if wafConfig != nil {
|
|
files = append(files, *wafConfig)
|
|
}
|
|
return files, nil
|
|
}
|
|
|
|
func (m *Manager) restoreRuntimeConfig(file *protocol.SupportFile, name string) error {
|
|
if m.RuntimeConfigDir == "" {
|
|
return nil
|
|
}
|
|
configPath := filepath.Join(m.RuntimeConfigDir, name)
|
|
if file == nil {
|
|
if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
return os.WriteFile(configPath, []byte(file.Content), nginxConfigFilePerm)
|
|
}
|
|
|
|
func (m *Manager) writeSafeDefaultFallbackFiles() error {
|
|
if strings.TrimSpace(m.MainConfigPath) == "" {
|
|
return errors.New("main config path 不能为空")
|
|
}
|
|
if strings.TrimSpace(m.RouteConfigPath) == "" {
|
|
return errors.New("route config path 不能为空")
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(m.MainConfigPath), nginxDirPerm); err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(m.RouteConfigPath), nginxDirPerm); err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(m.RouteConfigPath, nil, nginxConfigFilePerm); err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(m.MainConfigPath, []byte(m.renderMainConfig(m.safeDefaultFallbackMainConfig())), nginxConfigFilePerm); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) safeDefaultFallbackMainConfig() string {
|
|
observabilityBlock := ""
|
|
if listen := strings.TrimSpace(m.OpenrestyObservabilityListen); listen != "" {
|
|
observabilityBlock = fmt.Sprintf(safeDefaultFallbackObservabilityServerBlock, listen)
|
|
}
|
|
return fmt.Sprintf(safeDefaultFallbackMainConfig, observabilityBlock)
|
|
}
|
|
|
|
func (m *Manager) checkStubStatus(ctx context.Context) error {
|
|
ctx, cancel := context.WithTimeout(ctx, stubStatusCheckTimeout)
|
|
defer cancel()
|
|
openrestyStubURL := fmt.Sprintf("http://127.0.0.1:%d/openflare/stub_status", m.OpenrestyObservabilityPort)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, openrestyStubURL, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := (&http.Client{}).Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("openresty health endpoint unreachable: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("openresty health endpoint returned %s", resp.Status)
|
|
}
|
|
slog.Debug("openresty health endpoint is healthy", "url", openrestyStubURL)
|
|
return nil
|
|
}
|
|
|
|
func removeLegacyPowConfig(path string) error {
|
|
if strings.TrimSpace(path) == "" {
|
|
return nil
|
|
}
|
|
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("remove legacy pow_config.json %q: %w", path, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) certFileTargetPath(relativePath string) (string, error) {
|
|
if strings.TrimSpace(m.CertDir) == "" {
|
|
return "", errors.New("cert dir 不能为空")
|
|
}
|
|
candidate := strings.TrimSpace(relativePath)
|
|
if strings.Contains(candidate, `\`) {
|
|
candidate = strings.ReplaceAll(candidate, `\`, "/")
|
|
}
|
|
normalizedPath := filepath.Clean(filepath.FromSlash(candidate))
|
|
if normalizedPath == "." || normalizedPath == "" {
|
|
return "", errors.New("cert file path 不能为空")
|
|
}
|
|
if filepath.IsAbs(normalizedPath) || filepath.VolumeName(normalizedPath) != "" {
|
|
return "", fmt.Errorf("cert file path %q must be relative", relativePath)
|
|
}
|
|
targetPath := filepath.Join(m.CertDir, normalizedPath)
|
|
relativeToBase, err := filepath.Rel(m.CertDir, targetPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if relativeToBase == ".." || strings.HasPrefix(relativeToBase, ".."+string(os.PathSeparator)) {
|
|
return "", fmt.Errorf("cert file path %q escapes cert dir", relativePath)
|
|
}
|
|
return targetPath, nil
|
|
}
|
|
|
|
func certFileMode(relativePath string) fs.FileMode {
|
|
switch strings.ToLower(filepath.Ext(strings.TrimSpace(relativePath))) {
|
|
case ".crt", ".pem":
|
|
return nginxConfigFilePerm
|
|
case ".key":
|
|
return nginxPrivateKeyFilePerm
|
|
default:
|
|
return nginxConfigFilePerm
|
|
}
|
|
}
|
|
|
|
func luaFileTargetPath(baseDir string, relativePath string) (string, error) {
|
|
if strings.TrimSpace(baseDir) == "" {
|
|
return "", errors.New("lua dir 不能为空")
|
|
}
|
|
candidate := strings.TrimSpace(relativePath)
|
|
if strings.Contains(candidate, `\`) {
|
|
candidate = strings.ReplaceAll(candidate, `\`, "/")
|
|
}
|
|
normalizedPath := filepath.Clean(filepath.FromSlash(candidate))
|
|
if normalizedPath == "." || normalizedPath == "" {
|
|
return "", errors.New("lua file path 不能为空")
|
|
}
|
|
if filepath.IsAbs(normalizedPath) || filepath.VolumeName(normalizedPath) != "" {
|
|
return "", fmt.Errorf("lua file path %q must be relative", relativePath)
|
|
}
|
|
targetPath := filepath.Join(baseDir, normalizedPath)
|
|
relativeToBase, err := filepath.Rel(baseDir, targetPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if relativeToBase == ".." || strings.HasPrefix(relativeToBase, ".."+string(os.PathSeparator)) {
|
|
return "", fmt.Errorf("lua file path %q escapes lua dir", relativePath)
|
|
}
|
|
return targetPath, nil
|
|
}
|
|
|
|
func syncManagedFiles(baseDir string, files []managedFile) error {
|
|
if strings.TrimSpace(baseDir) == "" {
|
|
return errors.New("managed dir cannot be empty")
|
|
}
|
|
if info, err := os.Stat(baseDir); err == nil && !info.IsDir() {
|
|
return fmt.Errorf("managed dir %q is not a directory", baseDir)
|
|
} else if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(baseDir, nginxDirPerm); err != nil {
|
|
return err
|
|
}
|
|
|
|
desired := make(map[string]managedFile, len(files))
|
|
for _, file := range files {
|
|
cleanPath := filepath.Clean(filepath.FromSlash(strings.TrimSpace(file.Path)))
|
|
if cleanPath == "." || cleanPath == "" {
|
|
return errors.New("managed file path cannot be empty")
|
|
}
|
|
desired[cleanPath] = managedFile{
|
|
Path: cleanPath,
|
|
Content: file.Content,
|
|
Mode: file.Mode,
|
|
}
|
|
}
|
|
|
|
if err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
relativePath, err := filepath.Rel(baseDir, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, ok := desired[filepath.Clean(relativePath)]; ok {
|
|
return nil
|
|
}
|
|
return os.Remove(path) //nolint:gosec // path is resolved under the managed baseDir walk root
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, file := range desired {
|
|
targetPath := filepath.Join(baseDir, file.Path)
|
|
if err := os.MkdirAll(filepath.Dir(targetPath), nginxDirPerm); err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(targetPath, file.Content, file.Mode); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return removeEmptyManagedDirs(baseDir)
|
|
}
|
|
|
|
func removeEmptyManagedDirs(baseDir string) error {
|
|
dirs := make([]string, 0)
|
|
if err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.IsDir() && path != baseDir {
|
|
dirs = append(dirs, path)
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
sort.Slice(dirs, func(i int, j int) bool {
|
|
return len(dirs[i]) > len(dirs[j])
|
|
})
|
|
for _, dir := range dirs {
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(entries) == 0 {
|
|
if err := os.Remove(dir); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) ensureMimeTypes() error {
|
|
if m.MainConfigPath == "" {
|
|
return nil
|
|
}
|
|
configDir := filepath.Dir(m.MainConfigPath)
|
|
mimeTypesPath := filepath.Join(configDir, "mime.types")
|
|
if _, err := os.Stat(mimeTypesPath); err == nil {
|
|
return nil
|
|
} else if !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(configDir, nginxDirPerm); err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(mimeTypesPath, []byte(DefaultMimeTypes), nginxConfigFilePerm)
|
|
}
|
|
|
|
func (m *Manager) renderRouteConfig(content string) string {
|
|
rendered := content
|
|
if m.NginxCertDir != "" {
|
|
rendered = strings.ReplaceAll(rendered, openrestyrender.CertDirPlaceholder, m.NginxCertDir)
|
|
errorPagePath := filepath.ToSlash(filepath.Join(m.NginxCertDir, openrestyrender.OriginErrorPageSupportPath))
|
|
rendered = strings.ReplaceAll(rendered, openrestyrender.ErrorPageTmplPlaceholder, errorPagePath)
|
|
swDir := filepath.ToSlash(filepath.Join(m.NginxCertDir, "sw"))
|
|
rendered = strings.ReplaceAll(rendered, openrestyrender.SWDirPlaceholder, swDir)
|
|
}
|
|
if luaDir := m.luaRuntimePath(); luaDir != "" {
|
|
rendered = strings.ReplaceAll(rendered, openrestyrender.LuaDirPlaceholder, luaDir)
|
|
rendered = strings.ReplaceAll(rendered, openrestyrender.PowStaticDirPlaceholder, luaDir+"/pow/static")
|
|
}
|
|
if pagesDir := m.pagesRuntimePath(); pagesDir != "" {
|
|
rendered = strings.ReplaceAll(rendered, openrestyrender.PagesDirPlaceholder, pagesDir)
|
|
}
|
|
return rendered
|
|
}
|
|
|
|
func (m *Manager) renderMainConfig(content string) string {
|
|
rendered := content
|
|
if includePath := m.routeConfigIncludePath(); includePath != "" {
|
|
rendered = strings.ReplaceAll(rendered, openrestyrender.RouteConfigPlaceholder, includePath)
|
|
}
|
|
if accessLogPath := m.accessLogRuntimePath(); accessLogPath != "" {
|
|
rendered = strings.ReplaceAll(rendered, openrestyrender.AccessLogPlaceholder, accessLogPath)
|
|
errorLogPath := filepath.Join(filepath.Dir(accessLogPath), "error.log")
|
|
rendered = strings.ReplaceAll(rendered, openrestyrender.ErrorLogPlaceholder, filepath.ToSlash(errorLogPath))
|
|
}
|
|
if pidPath := m.pidRuntimePath(); pidPath != "" {
|
|
slashPIDPath := filepath.ToSlash(pidPath)
|
|
rendered = strings.ReplaceAll(rendered, openrestyrender.PIDPathPlaceholder, slashPIDPath)
|
|
rendered = strings.ReplaceAll(rendered, "pid logs/nginx.pid;", "pid "+slashPIDPath+";")
|
|
if err := os.MkdirAll(filepath.Dir(pidPath), nginxDirPerm); err != nil {
|
|
slog.Warn("ensure nginx pid directory failed", "path", filepath.Dir(pidPath), "error", err)
|
|
}
|
|
}
|
|
if cacheDir := m.nginxCacheRuntimeDir(); cacheDir != "" {
|
|
slashCacheDir := filepath.ToSlash(cacheDir)
|
|
rendered = strings.ReplaceAll(rendered, openrestyrender.NginxCacheDirPlaceholder, slashCacheDir)
|
|
if !strings.Contains(rendered, "client_body_temp_path") {
|
|
writablePaths := fmt.Sprintf(
|
|
" client_body_temp_path %s/client_temp;\n proxy_temp_path %s/proxy_temp;\n fastcgi_temp_path %s/fastcgi_temp;\n uwsgi_temp_path %s/uwsgi_temp;\n scgi_temp_path %s/scgi_temp;\n",
|
|
slashCacheDir, slashCacheDir, slashCacheDir, slashCacheDir, slashCacheDir,
|
|
)
|
|
rendered = strings.Replace(rendered, "http {", "http {\n"+writablePaths, 1)
|
|
}
|
|
for _, subDir := range []string{"client_temp", "proxy_temp", "fastcgi_temp", "uwsgi_temp", "scgi_temp"} {
|
|
if err := os.MkdirAll(filepath.Join(cacheDir, subDir), nginxDirPerm); err != nil {
|
|
slog.Warn("ensure nginx cache directory failed", "path", filepath.Join(cacheDir, subDir), "error", err)
|
|
}
|
|
}
|
|
}
|
|
if proxyCacheDir := m.proxyCacheRuntimeDir(); proxyCacheDir != "" {
|
|
slashProxyCache := filepath.ToSlash(proxyCacheDir)
|
|
rendered = strings.ReplaceAll(rendered, openrestyrender.ProxyCachePathPlaceholder, slashProxyCache)
|
|
rendered = strings.ReplaceAll(rendered, "/var/cache/openresty", slashProxyCache)
|
|
if err := os.MkdirAll(proxyCacheDir, nginxDirPerm); err != nil {
|
|
slog.Warn("ensure proxy cache directory failed", "path", proxyCacheDir, "error", err)
|
|
}
|
|
}
|
|
if luaDir := m.luaRuntimePath(); luaDir != "" {
|
|
rendered = strings.ReplaceAll(rendered, openrestyrender.LuaDirPlaceholder, luaDir)
|
|
if strings.Contains(rendered, "lua_shared_dict openflare_waf_config") {
|
|
rendered = injectWAFWorkerInit(rendered, luaDir)
|
|
}
|
|
}
|
|
if listen := strings.TrimSpace(m.OpenrestyObservabilityListen); listen != "" {
|
|
rendered = strings.ReplaceAll(rendered, openrestyrender.ObservabilityListenPlaceholder, listen)
|
|
}
|
|
if m.OpenrestyObservabilityPort > 0 {
|
|
rendered = strings.ReplaceAll(rendered, openrestyrender.ObservabilityPortPlaceholder, strconv.Itoa(m.OpenrestyObservabilityPort))
|
|
}
|
|
if resolverDirective := strings.TrimSpace(m.OpenrestyResolverDirective); resolverDirective != "" {
|
|
rendered = strings.ReplaceAll(rendered, ResolverDirectivePlaceholder, resolverDirective)
|
|
}
|
|
return rendered
|
|
}
|
|
|
|
func injectWAFWorkerInit(content string, luaDir string) string {
|
|
packagePath := fmt.Sprintf(" lua_package_path \"%s/?.lua;%s/?/init.lua;;\";\n", luaDir, luaDir)
|
|
if !strings.Contains(content, "lua_package_path ") {
|
|
content = strings.Replace(content, "http {", "http {\n"+packagePath, 1)
|
|
}
|
|
initPattern := regexp.MustCompile(`(?m)^[ \t]*init_worker_by_lua_file[ \t]+([^;]+);[ \t]*$`)
|
|
if match := initPattern.FindStringSubmatch(content); len(match) == workerInitSubmatchCount {
|
|
block := fmt.Sprintf(" init_worker_by_lua_block {\n require(\"waf.runtime\").init()\n dofile(%q)\n }", strings.TrimSpace(match[1]))
|
|
return initPattern.ReplaceAllString(content, block)
|
|
}
|
|
return strings.Replace(content, "http {", "http {\n init_worker_by_lua_block { require(\"waf.runtime\").init() }", 1)
|
|
}
|
|
|
|
func (m *Manager) managedPowLuaFiles() []protocol.SupportFile {
|
|
files := ManagedPowLuaFiles()
|
|
runtimeConfigDir := filepath.ToSlash(strings.TrimSpace(m.RuntimeConfigDir))
|
|
for index := range files {
|
|
files[index].Content = strings.ReplaceAll(files[index].Content, RuntimeConfigDirPlaceholder, runtimeConfigDir)
|
|
}
|
|
return files
|
|
}
|
|
|
|
func (m *Manager) managedWAFLuaFiles() []protocol.SupportFile {
|
|
files := ManagedWAFLuaFiles()
|
|
runtimeConfigDir := filepath.ToSlash(strings.TrimSpace(m.RuntimeConfigDir))
|
|
countryMMDBPath := filepath.ToSlash(strings.TrimSpace(m.MMDBPath))
|
|
if countryMMDBPath == "" {
|
|
countryMMDBPath = filepath.ToSlash(filepath.Join(runtimeConfigDir, "GeoLite2-Country.mmdb"))
|
|
}
|
|
cityMMDBPath := filepath.ToSlash(strings.TrimSpace(m.CityMMDBPath))
|
|
if cityMMDBPath == "" {
|
|
cityMMDBPath = filepath.ToSlash(filepath.Join(runtimeConfigDir, "GeoLite2-City.mmdb"))
|
|
}
|
|
for index := range files {
|
|
files[index].Content = strings.ReplaceAll(files[index].Content, RuntimeConfigDirPlaceholder, runtimeConfigDir)
|
|
files[index].Content = strings.ReplaceAll(files[index].Content, CountryMMDBPathPlaceholder, countryMMDBPath)
|
|
files[index].Content = strings.ReplaceAll(files[index].Content, CityMMDBPathPlaceholder, cityMMDBPath)
|
|
files[index].Content = strings.ReplaceAll(files[index].Content, WAFIPGroupsMaxSnapshotBytesPlaceholder, strconv.Itoa(sharedprotocol.MaxWAFIPGroupSnapshotBytes))
|
|
}
|
|
return files
|
|
}
|
|
|
|
// ObservabilityListenAddress returns the localhost listen address for stub_status.
|
|
func ObservabilityListenAddress(port int) string {
|
|
if port <= 0 {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("127.0.0.1:%d", port)
|
|
}
|
|
|
|
// ResolverDirective renders the nginx resolver block for runtime upstream lookups.
|
|
func ResolverDirective(explicitResolvers []string) string {
|
|
resolvers := resolverAddresses(explicitResolvers)
|
|
if len(resolvers) == 0 {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf(" resolver %s valid=30s ipv6=off;\n resolver_timeout 5s;\n", strings.Join(resolvers, " "))
|
|
}
|
|
|
|
func resolverAddresses(explicitResolvers []string) []string {
|
|
if resolvers := util.UniqueAndCleanStringSlice(explicitResolvers); len(resolvers) > 0 {
|
|
return resolvers
|
|
}
|
|
data, err := os.ReadFile("/etc/resolv.conf")
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return parseResolverAddresses(string(data), false)
|
|
}
|
|
|
|
func parseResolverAddresses(content string, dockerMode bool) []string {
|
|
lines := strings.Split(content, "\n")
|
|
resolvers := make([]string, 0, resolverAddressCapacity)
|
|
seen := make(map[string]struct{})
|
|
for _, line := range lines {
|
|
fields := strings.Fields(strings.TrimSpace(line))
|
|
if len(fields) < 2 || fields[0] != "nameserver" {
|
|
continue
|
|
}
|
|
addr := strings.TrimSpace(fields[1])
|
|
if addr == "" {
|
|
continue
|
|
}
|
|
if dockerMode && !isUsableDockerResolver(addr) {
|
|
continue
|
|
}
|
|
if _, ok := seen[addr]; ok {
|
|
continue
|
|
}
|
|
seen[addr] = struct{}{}
|
|
resolvers = append(resolvers, addr)
|
|
}
|
|
return resolvers
|
|
}
|
|
|
|
func isUsableDockerResolver(addr string) bool {
|
|
ip := net.ParseIP(addr)
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
return !ip.IsLoopback() && !ip.IsUnspecified()
|
|
}
|
|
|
|
// RequiresRuntimeResolver reports whether originURL needs a runtime DNS resolver.
|
|
func RequiresRuntimeResolver(originURL string) bool {
|
|
parsed, err := url.Parse(strings.TrimSpace(originURL))
|
|
if err != nil || parsed.Hostname() == "" {
|
|
return false
|
|
}
|
|
return net.ParseIP(parsed.Hostname()) == nil
|
|
}
|
|
|
|
func (m *Manager) routeConfigIncludePath() string {
|
|
return strings.TrimSpace(m.RouteConfigPath)
|
|
}
|
|
|
|
func (m *Manager) accessLogRuntimePath() string {
|
|
return filepath.ToSlash(strings.TrimSpace(m.AccessLogPath))
|
|
}
|
|
|
|
func (m *Manager) varRuntimeDir() string {
|
|
if accessLogPath := strings.TrimSpace(m.AccessLogPath); accessLogPath != "" {
|
|
return filepath.Dir(filepath.Dir(filepath.Dir(accessLogPath)))
|
|
}
|
|
if mainConfigPath := strings.TrimSpace(m.MainConfigPath); mainConfigPath != "" {
|
|
return filepath.Clean(filepath.Join(filepath.Dir(mainConfigPath), "..", ".."))
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (m *Manager) pidRuntimePath() string {
|
|
if varRoot := m.varRuntimeDir(); varRoot != "" {
|
|
return filepath.ToSlash(filepath.Join(varRoot, "run", "nginx.pid"))
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (m *Manager) nginxCacheRuntimeDir() string {
|
|
if varRoot := m.varRuntimeDir(); varRoot != "" {
|
|
return filepath.ToSlash(filepath.Join(varRoot, "cache", "nginx"))
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (m *Manager) proxyCacheRuntimeDir() string {
|
|
if varRoot := m.varRuntimeDir(); varRoot != "" {
|
|
return filepath.Join(varRoot, "cache", "openflare_proxy")
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (m *Manager) luaRuntimePath() string {
|
|
if strings.TrimSpace(m.NginxLuaDir) == "" {
|
|
return ""
|
|
}
|
|
return filepath.ToSlash(m.NginxLuaDir)
|
|
}
|
|
|
|
func (m *Manager) pagesRuntimePath() string {
|
|
return filepath.ToSlash(strings.TrimSpace(m.PagesDir))
|
|
}
|
|
|
|
func checksum(content string) string {
|
|
sum := sha256.Sum256([]byte(content))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func bundleChecksum(mainConfig string, routeConfig string, supportFiles []protocol.SupportFile) string {
|
|
files := append([]protocol.SupportFile(nil), supportFiles...)
|
|
sort.Slice(files, func(i int, j int) bool {
|
|
return files[i].Path < files[j].Path
|
|
})
|
|
var builder strings.Builder
|
|
builder.WriteString(mainConfig)
|
|
builder.WriteString("\n--route-config--\n")
|
|
builder.WriteString(routeConfig)
|
|
builder.WriteString("\n--support-files--\n")
|
|
for _, file := range files {
|
|
builder.WriteString(file.Path)
|
|
builder.WriteString("\n")
|
|
builder.WriteString(file.Content)
|
|
builder.WriteString("\n")
|
|
}
|
|
return checksum(builder.String())
|
|
}
|