mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 01:36:37 +08:00
[优化] 配置渲染从 Server 转移到 Agent
This commit is contained in:
@@ -19,6 +19,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
openrestyrender "openflare/utils/render/openresty"
|
||||||
|
|
||||||
"openflare-agent/internal/protocol"
|
"openflare-agent/internal/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -223,6 +225,9 @@ func (m *Manager) writeTargetFiles(mainConfig string, routeConfig string, suppor
|
|||||||
if err := m.writeWAFConfig(supportFiles); err != nil {
|
if err := m.writeWAFConfig(supportFiles); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := m.writeSourceConfig(supportFiles); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := m.ensureMimeTypes(); err != nil {
|
if err := m.ensureMimeTypes(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -507,6 +512,8 @@ type backupState struct {
|
|||||||
RouteData []byte
|
RouteData []byte
|
||||||
Files []protocol.SupportFile
|
Files []protocol.SupportFile
|
||||||
PowConfig *protocol.SupportFile
|
PowConfig *protocol.SupportFile
|
||||||
|
WAFConfig *protocol.SupportFile
|
||||||
|
SourceConfig *protocol.SupportFile
|
||||||
}
|
}
|
||||||
|
|
||||||
type managedFile struct {
|
type managedFile struct {
|
||||||
@@ -568,6 +575,16 @@ func (m *Manager) backup() (*backupState, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
state.PowConfig = powConfig
|
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))
|
slog.Debug("backup captured", "main_exists", state.MainExisted, "route_exists", state.RouteExisted, "cert_files", len(state.Files))
|
||||||
return state, nil
|
return state, nil
|
||||||
}
|
}
|
||||||
@@ -596,7 +613,13 @@ func (m *Manager) restore(state *backupState) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m.restorePowConfig(state)
|
if err := m.restoreRuntimeConfig(state.PowConfig, "pow_config.json"); 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 {
|
func (m *Manager) writeCertFiles(certFiles []protocol.SupportFile) error {
|
||||||
@@ -652,10 +675,30 @@ func (m *Manager) writeWAFConfig(supportFiles []protocol.SupportFile) error {
|
|||||||
return nil
|
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), 0o644); 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 {
|
func (m *Manager) writeManagedCertFiles(certFiles []protocol.SupportFile) error {
|
||||||
files := make([]managedFile, 0, len(certFiles))
|
files := make([]managedFile, 0, len(certFiles))
|
||||||
for _, file := range certFiles {
|
for _, file := range certFiles {
|
||||||
if file.Path == "pow_config.json" || file.Path == "waf_config.json" {
|
if file.Path == "pow_config.json" || file.Path == "waf_config.json" || file.Path == openrestyrender.SourceConfigFileName {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
targetPath, err := m.certFileTargetPath(file.Path)
|
targetPath, err := m.certFileTargetPath(file.Path)
|
||||||
@@ -720,10 +763,14 @@ func (m *Manager) readCertFiles() ([]protocol.SupportFile, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) readPowConfigFile() (*protocol.SupportFile, error) {
|
func (m *Manager) readPowConfigFile() (*protocol.SupportFile, error) {
|
||||||
|
return m.readRuntimeConfigFile("pow_config.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) readRuntimeConfigFile(name string) (*protocol.SupportFile, error) {
|
||||||
if m.RuntimeConfigDir == "" {
|
if m.RuntimeConfigDir == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
configPath := filepath.Join(m.RuntimeConfigDir, "pow_config.json")
|
configPath := filepath.Join(m.RuntimeConfigDir, name)
|
||||||
data, err := os.ReadFile(configPath)
|
data, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -732,7 +779,7 @@ func (m *Manager) readPowConfigFile() (*protocol.SupportFile, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &protocol.SupportFile{
|
return &protocol.SupportFile{
|
||||||
Path: "pow_config.json",
|
Path: name,
|
||||||
Content: string(data),
|
Content: string(data),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -749,21 +796,28 @@ func (m *Manager) readManagedSupportFiles() ([]protocol.SupportFile, error) {
|
|||||||
if powConfig != nil {
|
if powConfig != nil {
|
||||||
files = append(files, *powConfig)
|
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
|
return files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) restorePowConfig(state *backupState) error {
|
func (m *Manager) restoreRuntimeConfig(file *protocol.SupportFile, name string) error {
|
||||||
if state == nil || m.RuntimeConfigDir == "" {
|
if m.RuntimeConfigDir == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
configPath := filepath.Join(m.RuntimeConfigDir, "pow_config.json")
|
configPath := filepath.Join(m.RuntimeConfigDir, name)
|
||||||
if state.PowConfig == nil {
|
if file == nil {
|
||||||
if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) {
|
if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return os.WriteFile(configPath, []byte(state.PowConfig.Content), 0o644)
|
return os.WriteFile(configPath, []byte(file.Content), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) writeSafeDefaultFallbackFiles() error {
|
func (m *Manager) writeSafeDefaultFallbackFiles() error {
|
||||||
|
|||||||
@@ -165,13 +165,11 @@ type ApplyLogPayload struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ActiveConfigResponse struct {
|
type ActiveConfigResponse struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Checksum string `json:"checksum"`
|
Checksum string `json:"checksum"`
|
||||||
MainConfig string `json:"main_config"`
|
SourceConfigJSON string `json:"source_config_json"`
|
||||||
RouteConfig string `json:"route_config"`
|
SupportFiles []SupportFile `json:"support_files"`
|
||||||
RenderedConfig string `json:"rendered_config"`
|
CreatedAt string `json:"created_at"`
|
||||||
SupportFiles []SupportFile `json:"support_files"`
|
|
||||||
CreatedAt string `json:"created_at"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActiveConfigMeta struct {
|
type ActiveConfigMeta struct {
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
openrestyrender "openflare/utils/render/openresty"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"openflare-agent/internal/nginx"
|
"openflare-agent/internal/nginx"
|
||||||
@@ -91,6 +93,14 @@ func (s *Service) sync(ctx context.Context, startup bool, target *protocol.Activ
|
|||||||
}
|
}
|
||||||
|
|
||||||
if currentChecksum == target.Checksum {
|
if currentChecksum == target.Checksum {
|
||||||
|
if startup {
|
||||||
|
config, fetchErr := s.client.GetActiveConfig(ctx)
|
||||||
|
if fetchErr != nil {
|
||||||
|
slog.Error("fetch active config failed", "mode", mode, "error", fetchErr)
|
||||||
|
return fetchErr
|
||||||
|
}
|
||||||
|
return s.applyIfNeeded(ctx, mode, startup, snapshot, currentChecksum, target, config)
|
||||||
|
}
|
||||||
slog.Debug("local openresty config already up to date", "mode", mode, "version", target.Version)
|
slog.Debug("local openresty config already up to date", "mode", mode, "version", target.Version)
|
||||||
shouldReport := shouldReportNoopApply(snapshot, target.Version, target.Checksum)
|
shouldReport := shouldReportNoopApply(snapshot, target.Version, target.Checksum)
|
||||||
if startup {
|
if startup {
|
||||||
@@ -152,11 +162,20 @@ func (s *Service) ForceSyncOnce(ctx context.Context, target *protocol.ActiveConf
|
|||||||
clearBlockedTarget(snapshot)
|
clearBlockedTarget(snapshot)
|
||||||
_ = s.stateStore.Save(snapshot)
|
_ = s.stateStore.Save(snapshot)
|
||||||
}
|
}
|
||||||
return s.SyncOnce(ctx, target)
|
currentChecksum, err := s.nginxManager.CurrentChecksum()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
config, err := s.client.GetActiveConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("fetch active config failed", "mode", "force", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.applyIfNeeded(ctx, "force", true, snapshot, currentChecksum, target, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) applyIfNeeded(ctx context.Context, mode string, startup bool, snapshot *state.Snapshot, currentChecksum string, target *protocol.ActiveConfigMeta, config *protocol.ActiveConfigResponse) error {
|
func (s *Service) applyIfNeeded(ctx context.Context, mode string, startup bool, snapshot *state.Snapshot, currentChecksum string, target *protocol.ActiveConfigMeta, config *protocol.ActiveConfigResponse) error {
|
||||||
if currentChecksum == config.Checksum {
|
if currentChecksum == config.Checksum && !startup {
|
||||||
slog.Debug("local openresty config already up to date", "mode", mode, "version", config.Version)
|
slog.Debug("local openresty config already up to date", "mode", mode, "version", config.Version)
|
||||||
shouldReport := shouldReportNoopApply(snapshot, config.Version, config.Checksum)
|
shouldReport := shouldReportNoopApply(snapshot, config.Version, config.Checksum)
|
||||||
if startup {
|
if startup {
|
||||||
@@ -172,11 +191,11 @@ func (s *Service) applyIfNeeded(ctx context.Context, mode string, startup bool,
|
|||||||
snapshot.OpenrestyMessage = ""
|
snapshot.OpenrestyMessage = ""
|
||||||
}
|
}
|
||||||
if shouldReport {
|
if shouldReport {
|
||||||
routeConfig := config.RouteConfig
|
rendered, renderErr := renderActiveConfig(config)
|
||||||
if routeConfig == "" {
|
if renderErr != nil {
|
||||||
routeConfig = config.RenderedConfig
|
return renderErr
|
||||||
}
|
}
|
||||||
if err := s.reportNoopApply(ctx, snapshot.NodeID, config.Version, config.Checksum, checksumString(config.MainConfig), checksumString(routeConfig), len(config.SupportFiles)); err != nil {
|
if err := s.reportNoopApply(ctx, snapshot.NodeID, config.Version, config.Checksum, checksumString(rendered.mainConfig), checksumString(rendered.routeConfig), len(rendered.supportFiles)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,14 +226,14 @@ func (s *Service) applyIfNeeded(ctx context.Context, mode string, startup bool,
|
|||||||
slog.Debug("skipping apply because state already records target version/checksum", "version", config.Version, "checksum", config.Checksum)
|
slog.Debug("skipping apply because state already records target version/checksum", "version", config.Version, "checksum", config.Checksum)
|
||||||
return s.stateStore.Save(snapshot)
|
return s.stateStore.Save(snapshot)
|
||||||
}
|
}
|
||||||
routeConfig := config.RouteConfig
|
rendered, err := renderActiveConfig(config)
|
||||||
if routeConfig == "" {
|
if err != nil {
|
||||||
routeConfig = config.RenderedConfig
|
return err
|
||||||
}
|
}
|
||||||
mainConfigChecksum := checksumString(config.MainConfig)
|
mainConfigChecksum := checksumString(rendered.mainConfig)
|
||||||
routeConfigChecksum := checksumString(routeConfig)
|
routeConfigChecksum := checksumString(rendered.routeConfig)
|
||||||
slog.Info("applying new openresty config", "mode", mode, "from_version", snapshot.CurrentVersion, "to_version", config.Version, "old_checksum", currentChecksum, "new_checksum", config.Checksum)
|
slog.Info("applying new openresty config", "mode", mode, "from_version", snapshot.CurrentVersion, "to_version", config.Version, "old_checksum", currentChecksum, "new_checksum", config.Checksum)
|
||||||
outcome := s.nginxManager.Apply(ctx, config.MainConfig, routeConfig, config.SupportFiles)
|
outcome := s.nginxManager.Apply(ctx, rendered.mainConfig, rendered.routeConfig, rendered.supportFiles)
|
||||||
message := strings.TrimSpace(outcome.Message)
|
message := strings.TrimSpace(outcome.Message)
|
||||||
if outcome.Status == "" {
|
if outcome.Status == "" {
|
||||||
outcome.Status = nginx.ApplyStatusFatal
|
outcome.Status = nginx.ApplyStatusFatal
|
||||||
@@ -269,7 +288,7 @@ func (s *Service) applyIfNeeded(ctx context.Context, mode string, startup bool,
|
|||||||
Checksum: config.Checksum,
|
Checksum: config.Checksum,
|
||||||
MainConfigChecksum: mainConfigChecksum,
|
MainConfigChecksum: mainConfigChecksum,
|
||||||
RouteConfigChecksum: routeConfigChecksum,
|
RouteConfigChecksum: routeConfigChecksum,
|
||||||
SupportFileCount: len(config.SupportFiles),
|
SupportFileCount: len(rendered.supportFiles),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
slog.Error("report apply log failed", "version", config.Version, "result", reportResult, "error", err)
|
slog.Error("report apply log failed", "version", config.Version, "result", reportResult, "error", err)
|
||||||
return err
|
return err
|
||||||
@@ -282,6 +301,55 @@ func (s *Service) applyIfNeeded(ctx context.Context, mode string, startup bool,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type renderedActiveConfig struct {
|
||||||
|
mainConfig string
|
||||||
|
routeConfig string
|
||||||
|
supportFiles []protocol.SupportFile
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderActiveConfig(config *protocol.ActiveConfigResponse) (*renderedActiveConfig, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("active config is nil")
|
||||||
|
}
|
||||||
|
sourceJSON := strings.TrimSpace(config.SourceConfigJSON)
|
||||||
|
if sourceJSON == "" {
|
||||||
|
return nil, errors.New("active config source_config_json is empty")
|
||||||
|
}
|
||||||
|
rendered, err := openrestyrender.RenderJSON(sourceJSON, toOpenRestySupportFiles(config.SupportFiles))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
files := fromOpenRestySupportFiles(rendered.SupportFiles)
|
||||||
|
files = append(files, protocol.SupportFile{Path: openrestyrender.SourceConfigFileName, Content: sourceJSON})
|
||||||
|
return &renderedActiveConfig{
|
||||||
|
mainConfig: rendered.MainConfig,
|
||||||
|
routeConfig: rendered.RouteConfig,
|
||||||
|
supportFiles: files,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toOpenRestySupportFiles(files []protocol.SupportFile) []openrestyrender.SupportFile {
|
||||||
|
if len(files) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]openrestyrender.SupportFile, 0, len(files))
|
||||||
|
for _, file := range files {
|
||||||
|
result = append(result, openrestyrender.SupportFile{Path: file.Path, Content: file.Content})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromOpenRestySupportFiles(files []openrestyrender.SupportFile) []protocol.SupportFile {
|
||||||
|
if len(files) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]protocol.SupportFile, 0, len(files))
|
||||||
|
for _, file := range files {
|
||||||
|
result = append(result, protocol.SupportFile{Path: file.Path, Content: file.Content})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func shouldReportNoopApply(snapshot *state.Snapshot, version string, checksum string) bool {
|
func shouldReportNoopApply(snapshot *state.Snapshot, version string, checksum string) bool {
|
||||||
if snapshot == nil {
|
if snapshot == nil {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package sync
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -36,6 +38,10 @@ type fakeManager struct {
|
|||||||
applyFiles [][]protocol.SupportFile
|
applyFiles [][]protocol.SupportFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testSourceConfigJSON(workerProcesses string, listen int) string {
|
||||||
|
return fmt.Sprintf(`{"routes":[{"id":1,"site_name":"example","domain":"example.com","domains":["example.com"],"origin_url":"http://127.0.0.1:%d","upstreams":["http://127.0.0.1:%d"],"enabled":true}],"openresty_config":{"worker_processes":"%s","worker_connections":1024,"worker_rlimit_nofile":65535,"events_multi_accept_enabled":true,"keepalive_timeout":20,"keepalive_requests":1000,"client_header_timeout":15,"client_body_timeout":15,"client_max_body_size":"64m","large_client_header_buffers":"4 16k","send_timeout":30,"proxy_connect_timeout":3,"proxy_send_timeout":60,"proxy_read_timeout":60,"websocket_enabled":true,"proxy_request_buffering":false,"proxy_buffering_enabled":true,"proxy_buffers":"16 16k","proxy_buffer_size":"8k","proxy_busy_buffers_size":"64k","gzip_enabled":true,"gzip_min_length":1024,"gzip_comp_level":5,"cache_enabled":false,"cache_levels":"1:2","cache_inactive":"30m","cache_max_size":"1g","cache_key_template":"$scheme$host$request_uri","cache_lock_enabled":true,"cache_lock_timeout":"5s","cache_use_stale":"error timeout updating http_500 http_502 http_503 http_504","main_config_template":"worker_processes {{OpenRestyWorkerProcesses}};"},"waf":{"rule_groups":[],"bindings":[]}}`, listen, listen, workerProcesses)
|
||||||
|
}
|
||||||
|
|
||||||
func (f *fakeExecutor) Test(ctx context.Context) error {
|
func (f *fakeExecutor) Test(ctx context.Context) error {
|
||||||
return f.testErr
|
return f.testErr
|
||||||
}
|
}
|
||||||
@@ -93,13 +99,11 @@ func (m *fakeManager) CurrentChecksum() (string, error) {
|
|||||||
func TestSyncOnceSuccess(t *testing.T) {
|
func TestSyncOnceSuccess(t *testing.T) {
|
||||||
client := &fakeClient{
|
client := &fakeClient{
|
||||||
config: protocol.ActiveConfigResponse{
|
config: protocol.ActiveConfigResponse{
|
||||||
Version: "20260309-001",
|
Version: "20260309-001",
|
||||||
Checksum: "checksum-1",
|
Checksum: "checksum-1",
|
||||||
MainConfig: "worker_processes auto;",
|
SourceConfigJSON: testSourceConfigJSON("auto", 80),
|
||||||
RouteConfig: "server { listen 80; }",
|
SupportFiles: []protocol.SupportFile{{Path: "1.crt", Content: "cert"}},
|
||||||
RenderedConfig: "server { listen 80; }",
|
CreatedAt: time.Now().Format(time.RFC3339),
|
||||||
SupportFiles: []protocol.SupportFile{{Path: "1.crt", Content: "cert"}},
|
|
||||||
CreatedAt: time.Now().Format(time.RFC3339),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +136,7 @@ func TestSyncOnceSuccess(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to read route config: %v", err)
|
t.Fatalf("failed to read route config: %v", err)
|
||||||
}
|
}
|
||||||
if string(data) != "server { listen 80; }" {
|
if !strings.Contains(string(data), "listen 80;") || !strings.Contains(string(data), "server_name example.com;") {
|
||||||
t.Fatal("expected rendered config to be written to route file")
|
t.Fatal("expected rendered config to be written to route file")
|
||||||
}
|
}
|
||||||
mainData, err := os.ReadFile(filepath.Join(filepath.Dir(routePath), "nginx.conf"))
|
mainData, err := os.ReadFile(filepath.Join(filepath.Dir(routePath), "nginx.conf"))
|
||||||
@@ -158,7 +162,7 @@ func TestSyncOnceSuccess(t *testing.T) {
|
|||||||
if client.reports[0].MainConfigChecksum == "" || client.reports[0].RouteConfigChecksum == "" {
|
if client.reports[0].MainConfigChecksum == "" || client.reports[0].RouteConfigChecksum == "" {
|
||||||
t.Fatal("expected main and route config checksums to be reported")
|
t.Fatal("expected main and route config checksums to be reported")
|
||||||
}
|
}
|
||||||
if client.reports[0].SupportFileCount != 1 {
|
if client.reports[0].SupportFileCount != 4 {
|
||||||
t.Fatalf("expected support file count to be reported, got %d", client.reports[0].SupportFileCount)
|
t.Fatalf("expected support file count to be reported, got %d", client.reports[0].SupportFileCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,13 +170,11 @@ func TestSyncOnceSuccess(t *testing.T) {
|
|||||||
func TestSyncOnceRollbackOnNginxFailure(t *testing.T) {
|
func TestSyncOnceRollbackOnNginxFailure(t *testing.T) {
|
||||||
client := &fakeClient{
|
client := &fakeClient{
|
||||||
config: protocol.ActiveConfigResponse{
|
config: protocol.ActiveConfigResponse{
|
||||||
Version: "20260309-002",
|
Version: "20260309-002",
|
||||||
Checksum: "checksum-2",
|
Checksum: "checksum-2",
|
||||||
MainConfig: "worker_processes 2;",
|
SourceConfigJSON: testSourceConfigJSON("2", 81),
|
||||||
RouteConfig: "server { listen 81; }",
|
SupportFiles: []protocol.SupportFile{{Path: "1.crt", Content: "cert"}},
|
||||||
RenderedConfig: "server { listen 81; }",
|
CreatedAt: time.Now().Format(time.RFC3339),
|
||||||
SupportFiles: []protocol.SupportFile{{Path: "1.crt", Content: "cert"}},
|
|
||||||
CreatedAt: time.Now().Format(time.RFC3339),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +227,7 @@ func TestSyncOnceRollbackOnNginxFailure(t *testing.T) {
|
|||||||
if client.reports[0].MainConfigChecksum == "" || client.reports[0].RouteConfigChecksum == "" {
|
if client.reports[0].MainConfigChecksum == "" || client.reports[0].RouteConfigChecksum == "" {
|
||||||
t.Fatal("expected failed report to include main and route config checksums")
|
t.Fatal("expected failed report to include main and route config checksums")
|
||||||
}
|
}
|
||||||
if client.reports[0].SupportFileCount != 1 {
|
if client.reports[0].SupportFileCount != 4 {
|
||||||
t.Fatalf("expected failed report to include support file count, got %d", client.reports[0].SupportFileCount)
|
t.Fatalf("expected failed report to include support file count, got %d", client.reports[0].SupportFileCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,13 +235,11 @@ func TestSyncOnceRollbackOnNginxFailure(t *testing.T) {
|
|||||||
func TestSyncOnceReportsWarningWhenRollbackKeepsOpenrestyHealthy(t *testing.T) {
|
func TestSyncOnceReportsWarningWhenRollbackKeepsOpenrestyHealthy(t *testing.T) {
|
||||||
client := &fakeClient{
|
client := &fakeClient{
|
||||||
config: protocol.ActiveConfigResponse{
|
config: protocol.ActiveConfigResponse{
|
||||||
Version: "20260309-002",
|
Version: "20260309-002",
|
||||||
Checksum: "checksum-2",
|
Checksum: "checksum-2",
|
||||||
MainConfig: "worker_processes 2;",
|
SourceConfigJSON: testSourceConfigJSON("2", 81),
|
||||||
RouteConfig: "server { listen 81; }",
|
SupportFiles: []protocol.SupportFile{{Path: "1.crt", Content: "cert"}},
|
||||||
RenderedConfig: "server { listen 81; }",
|
CreatedAt: time.Now().Format(time.RFC3339),
|
||||||
SupportFiles: []protocol.SupportFile{{Path: "1.crt", Content: "cert"}},
|
|
||||||
CreatedAt: time.Now().Format(time.RFC3339),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,13 +294,11 @@ func TestSyncOnceReportsWarningWhenRollbackKeepsOpenrestyHealthy(t *testing.T) {
|
|||||||
func TestSyncOnStartupRecreatesRuntimeWhenChecksumMatches(t *testing.T) {
|
func TestSyncOnStartupRecreatesRuntimeWhenChecksumMatches(t *testing.T) {
|
||||||
client := &fakeClient{
|
client := &fakeClient{
|
||||||
config: protocol.ActiveConfigResponse{
|
config: protocol.ActiveConfigResponse{
|
||||||
Version: "20260309-003",
|
Version: "20260309-003",
|
||||||
Checksum: "checksum-3",
|
Checksum: "checksum-3",
|
||||||
MainConfig: "worker_processes auto;",
|
SourceConfigJSON: testSourceConfigJSON("auto", 82),
|
||||||
RouteConfig: "server { listen 82; }",
|
SupportFiles: []protocol.SupportFile{{Path: "1.crt", Content: "cert"}},
|
||||||
RenderedConfig: "server { listen 82; }",
|
CreatedAt: time.Now().Format(time.RFC3339),
|
||||||
SupportFiles: []protocol.SupportFile{{Path: "1.crt", Content: "cert"}},
|
|
||||||
CreatedAt: time.Now().Format(time.RFC3339),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
|
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
|
||||||
@@ -320,14 +318,11 @@ func TestSyncOnStartupRecreatesRuntimeWhenChecksumMatches(t *testing.T) {
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
t.Fatalf("SyncOnStartup failed: %v", err)
|
t.Fatalf("SyncOnStartup failed: %v", err)
|
||||||
}
|
}
|
||||||
if len(manager.ensureCalls) != 1 || !manager.ensureCalls[0] {
|
if len(manager.applyMainContents) != 1 {
|
||||||
t.Fatal("expected startup sync to recreate runtime")
|
t.Fatal("expected startup sync to re-render and apply local config")
|
||||||
}
|
}
|
||||||
if len(client.reports) != 1 || client.reports[0].Result != ApplyResultSuccess {
|
if len(client.reports) != 1 || client.reports[0].Result != ApplyResultSuccess {
|
||||||
t.Fatal("expected startup sync to report noop success when state is refreshed")
|
t.Fatal("expected startup sync to report apply success when state is refreshed")
|
||||||
}
|
|
||||||
if client.reports[0].Message != "local config already matches active version; apply skipped" {
|
|
||||||
t.Fatalf("unexpected noop apply message: %q", client.reports[0].Message)
|
|
||||||
}
|
}
|
||||||
snapshot, err := stateStore.Load()
|
snapshot, err := stateStore.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -416,12 +411,10 @@ func TestSyncOnceDoesNotRepeatNoopReportWhenStateAlreadyMatches(t *testing.T) {
|
|||||||
func TestSyncOnStartupRecordsRuntimeFailure(t *testing.T) {
|
func TestSyncOnStartupRecordsRuntimeFailure(t *testing.T) {
|
||||||
client := &fakeClient{
|
client := &fakeClient{
|
||||||
config: protocol.ActiveConfigResponse{
|
config: protocol.ActiveConfigResponse{
|
||||||
Version: "20260309-004",
|
Version: "20260309-004",
|
||||||
Checksum: "checksum-4",
|
Checksum: "checksum-4",
|
||||||
MainConfig: "worker_processes 4;",
|
SourceConfigJSON: testSourceConfigJSON("4", 83),
|
||||||
RouteConfig: "server { listen 83; }",
|
CreatedAt: time.Now().Format(time.RFC3339),
|
||||||
RenderedConfig: "server { listen 83; }",
|
|
||||||
CreatedAt: time.Now().Format(time.RFC3339),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
|
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
|
||||||
@@ -435,7 +428,7 @@ func TestSyncOnStartupRecordsRuntimeFailure(t *testing.T) {
|
|||||||
|
|
||||||
manager := &fakeManager{
|
manager := &fakeManager{
|
||||||
currentChecksum: "checksum-4",
|
currentChecksum: "checksum-4",
|
||||||
ensureErr: context.DeadlineExceeded,
|
applyOutcome: nginx.ApplyOutcome{Status: nginx.ApplyStatusFatal, Message: context.DeadlineExceeded.Error()},
|
||||||
}
|
}
|
||||||
service := New(client, manager, stateStore)
|
service := New(client, manager, stateStore)
|
||||||
if err = service.SyncOnStartup(context.Background(), &protocol.ActiveConfigMeta{
|
if err = service.SyncOnStartup(context.Background(), &protocol.ActiveConfigMeta{
|
||||||
@@ -459,12 +452,10 @@ func TestSyncOnStartupRecordsRuntimeFailure(t *testing.T) {
|
|||||||
func TestSyncOnceSkipsPreviouslyBlockedVersion(t *testing.T) {
|
func TestSyncOnceSkipsPreviouslyBlockedVersion(t *testing.T) {
|
||||||
client := &fakeClient{
|
client := &fakeClient{
|
||||||
config: protocol.ActiveConfigResponse{
|
config: protocol.ActiveConfigResponse{
|
||||||
Version: "20260309-006",
|
Version: "20260309-006",
|
||||||
Checksum: "checksum-6",
|
Checksum: "checksum-6",
|
||||||
MainConfig: "worker_processes 6;",
|
SourceConfigJSON: testSourceConfigJSON("6", 86),
|
||||||
RouteConfig: "server { listen 86; }",
|
CreatedAt: time.Now().Format(time.RFC3339),
|
||||||
RenderedConfig: "server { listen 86; }",
|
|
||||||
CreatedAt: time.Now().Format(time.RFC3339),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
|
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
|
||||||
@@ -506,12 +497,10 @@ func TestSyncOnceSkipsPreviouslyBlockedVersion(t *testing.T) {
|
|||||||
func TestSyncOnStartupKeepsBlockedVersionSuppressedUntilNewTargetArrives(t *testing.T) {
|
func TestSyncOnStartupKeepsBlockedVersionSuppressedUntilNewTargetArrives(t *testing.T) {
|
||||||
client := &fakeClient{
|
client := &fakeClient{
|
||||||
config: protocol.ActiveConfigResponse{
|
config: protocol.ActiveConfigResponse{
|
||||||
Version: "20260309-007",
|
Version: "20260309-007",
|
||||||
Checksum: "checksum-7",
|
Checksum: "checksum-7",
|
||||||
MainConfig: "worker_processes 7;",
|
SourceConfigJSON: testSourceConfigJSON("7", 87),
|
||||||
RouteConfig: "server { listen 87; }",
|
CreatedAt: time.Now().Format(time.RFC3339),
|
||||||
RenderedConfig: "server { listen 87; }",
|
|
||||||
CreatedAt: time.Now().Format(time.RFC3339),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
|
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
|
||||||
@@ -565,12 +554,10 @@ func TestSyncOnStartupKeepsBlockedVersionSuppressedUntilNewTargetArrives(t *test
|
|||||||
func TestSyncOnStartupStartsFallbackWhenBlockedVersionHasNoLocalConfig(t *testing.T) {
|
func TestSyncOnStartupStartsFallbackWhenBlockedVersionHasNoLocalConfig(t *testing.T) {
|
||||||
client := &fakeClient{
|
client := &fakeClient{
|
||||||
config: protocol.ActiveConfigResponse{
|
config: protocol.ActiveConfigResponse{
|
||||||
Version: "20260309-007",
|
Version: "20260309-007",
|
||||||
Checksum: "checksum-7",
|
Checksum: "checksum-7",
|
||||||
MainConfig: "worker_processes 7;",
|
SourceConfigJSON: testSourceConfigJSON("7", 87),
|
||||||
RouteConfig: "server { listen 87; }",
|
CreatedAt: time.Now().Format(time.RFC3339),
|
||||||
RenderedConfig: "server { listen 87; }",
|
|
||||||
CreatedAt: time.Now().Format(time.RFC3339),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
|
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
|
||||||
@@ -625,12 +612,10 @@ func TestSyncOnStartupStartsFallbackWhenBlockedVersionHasNoLocalConfig(t *testin
|
|||||||
func TestSyncOnStartupStartsFallbackWhenResidualConfigCannotRecover(t *testing.T) {
|
func TestSyncOnStartupStartsFallbackWhenResidualConfigCannotRecover(t *testing.T) {
|
||||||
client := &fakeClient{
|
client := &fakeClient{
|
||||||
config: protocol.ActiveConfigResponse{
|
config: protocol.ActiveConfigResponse{
|
||||||
Version: "20260309-007",
|
Version: "20260309-007",
|
||||||
Checksum: "checksum-7",
|
Checksum: "checksum-7",
|
||||||
MainConfig: "worker_processes 7;",
|
SourceConfigJSON: testSourceConfigJSON("7", 87),
|
||||||
RouteConfig: "server { listen 87; }",
|
CreatedAt: time.Now().Format(time.RFC3339),
|
||||||
RenderedConfig: "server { listen 87; }",
|
|
||||||
CreatedAt: time.Now().Format(time.RFC3339),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
|
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
|
||||||
@@ -679,12 +664,10 @@ func TestSyncOnStartupStartsFallbackWhenResidualConfigCannotRecover(t *testing.T
|
|||||||
func TestSyncOnceClearsBlockedTargetWhenNewVersionArrives(t *testing.T) {
|
func TestSyncOnceClearsBlockedTargetWhenNewVersionArrives(t *testing.T) {
|
||||||
client := &fakeClient{
|
client := &fakeClient{
|
||||||
config: protocol.ActiveConfigResponse{
|
config: protocol.ActiveConfigResponse{
|
||||||
Version: "20260309-008",
|
Version: "20260309-008",
|
||||||
Checksum: "checksum-8",
|
Checksum: "checksum-8",
|
||||||
MainConfig: "worker_processes 8;",
|
SourceConfigJSON: testSourceConfigJSON("8", 88),
|
||||||
RouteConfig: "server { listen 88; }",
|
CreatedAt: time.Now().Format(time.RFC3339),
|
||||||
RenderedConfig: "server { listen 88; }",
|
|
||||||
CreatedAt: time.Now().Format(time.RFC3339),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
|
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
|
||||||
@@ -732,12 +715,10 @@ func TestSyncOnceClearsBlockedTargetWhenNewVersionArrives(t *testing.T) {
|
|||||||
func TestSyncOnceSkipsFetchWhenHeartbeatChecksumMatches(t *testing.T) {
|
func TestSyncOnceSkipsFetchWhenHeartbeatChecksumMatches(t *testing.T) {
|
||||||
client := &fakeClient{
|
client := &fakeClient{
|
||||||
config: protocol.ActiveConfigResponse{
|
config: protocol.ActiveConfigResponse{
|
||||||
Version: "20260309-005",
|
Version: "20260309-005",
|
||||||
Checksum: "checksum-5",
|
Checksum: "checksum-5",
|
||||||
MainConfig: "worker_processes auto;",
|
SourceConfigJSON: testSourceConfigJSON("auto", 84),
|
||||||
RouteConfig: "server { listen 84; }",
|
CreatedAt: time.Now().Format(time.RFC3339),
|
||||||
RenderedConfig: "server { listen 84; }",
|
|
||||||
CreatedAt: time.Now().Format(time.RFC3339),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
|
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
|
||||||
|
|||||||
@@ -378,13 +378,13 @@ func TestPhase1HTTPSAndCertificateImportLifecycle(t *testing.T) {
|
|||||||
agentResp := performAgentJSONRequestWithToken(t, engine, common.AgentToken, http.MethodGet, "/api/agent/config-versions/active", nil)
|
agentResp := performAgentJSONRequestWithToken(t, engine, common.AgentToken, http.MethodGet, "/api/agent/config-versions/active", nil)
|
||||||
var activeConfig map[string]any
|
var activeConfig map[string]any
|
||||||
decodeResponseData(t, agentResp, &activeConfig)
|
decodeResponseData(t, agentResp, &activeConfig)
|
||||||
mainConfig, ok := activeConfig["main_config"].(string)
|
sourceConfigJSON, ok := activeConfig["source_config_json"].(string)
|
||||||
if !ok || !strings.Contains(mainConfig, "include __OPENFLARE_ROUTE_CONFIG__;") {
|
if !ok || !strings.Contains(sourceConfigJSON, "secure.example.com") {
|
||||||
t.Fatalf("expected active config to expose main_config, got %#v", activeConfig["main_config"])
|
t.Fatalf("expected active config to expose source_config_json, got %#v", activeConfig["source_config_json"])
|
||||||
}
|
}
|
||||||
supportFiles, ok := activeConfig["support_files"].([]any)
|
supportFiles, ok := activeConfig["support_files"].([]any)
|
||||||
if !ok || len(supportFiles) != 4 {
|
if !ok || len(supportFiles) != 2 {
|
||||||
t.Fatalf("expected active config to expose 4 support files, got %#v", activeConfig["support_files"])
|
t.Fatalf("expected active config to expose 2 certificate support files, got %#v", activeConfig["support_files"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -457,7 +457,7 @@ func TestPhase2AgentLifecycle(t *testing.T) {
|
|||||||
activeConfigResp := performAgentJSONRequestWithToken(t, engine, createdNode.AgentToken, http.MethodGet, "/api/agent/config-versions/active", nil)
|
activeConfigResp := performAgentJSONRequestWithToken(t, engine, createdNode.AgentToken, http.MethodGet, "/api/agent/config-versions/active", nil)
|
||||||
var activeConfig service.AgentConfigResponse
|
var activeConfig service.AgentConfigResponse
|
||||||
decodeResponseData(t, activeConfigResp, &activeConfig)
|
decodeResponseData(t, activeConfigResp, &activeConfig)
|
||||||
if activeConfig.Version == "" || activeConfig.RenderedConfig == "" || activeConfig.Checksum == "" {
|
if activeConfig.Version == "" || activeConfig.SourceConfigJSON == "" || activeConfig.Checksum == "" {
|
||||||
t.Fatal("expected active config response to contain version payload")
|
t.Fatal("expected active config response to contain version payload")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,13 +79,11 @@ type ApplyLogCleanupResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentConfigResponse struct {
|
type AgentConfigResponse struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Checksum string `json:"checksum"`
|
Checksum string `json:"checksum"`
|
||||||
MainConfig string `json:"main_config"`
|
SourceConfigJSON string `json:"source_config_json"`
|
||||||
RouteConfig string `json:"route_config"`
|
SupportFiles []SupportFile `json:"support_files"`
|
||||||
RenderedConfig string `json:"rendered_config"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
SupportFiles []SupportFile `json:"support_files"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentSettings struct {
|
type AgentSettings struct {
|
||||||
@@ -233,13 +231,11 @@ func GetActiveConfigForAgent() (*AgentConfigResponse, error) {
|
|||||||
}
|
}
|
||||||
slog.Debug("agent fetched active config", "version", version.Version, "checksum", version.Checksum)
|
slog.Debug("agent fetched active config", "version", version.Version, "checksum", version.Checksum)
|
||||||
return &AgentConfigResponse{
|
return &AgentConfigResponse{
|
||||||
Version: version.Version,
|
Version: version.Version,
|
||||||
Checksum: version.Checksum,
|
Checksum: version.Checksum,
|
||||||
MainConfig: version.MainConfig,
|
SourceConfigJSON: version.SnapshotJSON,
|
||||||
RouteConfig: version.RenderedConfig,
|
SupportFiles: sourceSupportFiles(supportFiles),
|
||||||
RenderedConfig: version.RenderedConfig,
|
CreatedAt: version.CreatedAt,
|
||||||
SupportFiles: supportFiles,
|
|
||||||
CreatedAt: version.CreatedAt,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,10 +422,6 @@ func normalizeApplyLogPageSize(pageSize int) int {
|
|||||||
return pageSize
|
return pageSize
|
||||||
}
|
}
|
||||||
|
|
||||||
func upsertNode(payload AgentNodePayload) (*model.Node, error) {
|
|
||||||
return nil, errors.New("不再支持匿名自动注册")
|
|
||||||
}
|
|
||||||
|
|
||||||
func computeNodeStatus(node *model.Node) string {
|
func computeNodeStatus(node *model.Node) string {
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return NodeStatusOffline
|
return NodeStatusOffline
|
||||||
|
|||||||
@@ -33,18 +33,13 @@ func TestGetActiveConfigForAgentIncludesPoWConfig(t *testing.T) {
|
|||||||
t.Fatalf("GetActiveConfigForAgent failed: %v", err)
|
t.Fatalf("GetActiveConfigForAgent failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
foundPowConfig := false
|
|
||||||
for _, file := range activeConfig.SupportFiles {
|
for _, file := range activeConfig.SupportFiles {
|
||||||
if file.Path != "pow_config.json" {
|
if file.Path == "pow_config.json" || file.Path == "waf_config.json" {
|
||||||
continue
|
t.Fatalf("agent config should not receive rendered runtime config file %s", file.Path)
|
||||||
}
|
|
||||||
foundPowConfig = true
|
|
||||||
if file.Content == "" {
|
|
||||||
t.Fatal("expected pow_config.json content to be populated")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !foundPowConfig {
|
if !strings.Contains(activeConfig.SourceConfigJSON, `"pow_enabled":true`) {
|
||||||
t.Fatal("expected agent config to include pow_config.json support file")
|
t.Fatal("expected agent config source json to include PoW source configuration")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,13 +66,12 @@ func TestGetActiveConfigForAgentIncludesWAFConfig(t *testing.T) {
|
|||||||
|
|
||||||
for _, file := range activeConfig.SupportFiles {
|
for _, file := range activeConfig.SupportFiles {
|
||||||
if file.Path == "waf_config.json" {
|
if file.Path == "waf_config.json" {
|
||||||
if !strings.Contains(file.Content, `"rule_groups"`) {
|
t.Fatal("agent config should not receive rendered waf_config.json")
|
||||||
t.Fatalf("expected waf_config.json content to include rule groups, got %s", file.Content)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.Fatal("expected agent config to include waf_config.json support file")
|
if !strings.Contains(activeConfig.SourceConfigJSON, `"waf"`) {
|
||||||
|
t.Fatal("expected agent config source json to include WAF source configuration")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetActiveConfigForAgentUsesTenMinutePoWSessionDefault(t *testing.T) {
|
func TestGetActiveConfigForAgentUsesTenMinutePoWSessionDefault(t *testing.T) {
|
||||||
@@ -105,13 +99,12 @@ func TestGetActiveConfigForAgentUsesTenMinutePoWSessionDefault(t *testing.T) {
|
|||||||
|
|
||||||
for _, file := range activeConfig.SupportFiles {
|
for _, file := range activeConfig.SupportFiles {
|
||||||
if file.Path == "pow_config.json" {
|
if file.Path == "pow_config.json" {
|
||||||
if !strings.Contains(file.Content, `"session_ttl":600`) {
|
t.Fatal("agent config should not receive rendered pow_config.json")
|
||||||
t.Fatalf("expected default PoW session TTL to be 600 seconds, got %s", file.Content)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.Fatal("expected agent config to include pow_config.json support file")
|
if !strings.Contains(activeConfig.SourceConfigJSON, `"session_ttl":600`) {
|
||||||
|
t.Fatalf("expected default PoW session TTL to be in source json, got %s", activeConfig.SourceConfigJSON)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegisterNodeWithAgentToken(t *testing.T) {
|
func TestRegisterNodeWithAgentToken(t *testing.T) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,51 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
const (
|
|
||||||
openRestyObservabilityInitLuaPath = "init.lua"
|
|
||||||
openRestyObservabilityLogLuaPath = "log.lua"
|
|
||||||
openRestyObservabilityReadLuaPath = "read.lua"
|
|
||||||
)
|
|
||||||
|
|
||||||
func renderOpenRestyObservabilityTemplateBlock() string {
|
|
||||||
return stringsJoinLines(
|
|
||||||
" lua_shared_dict openflare_observability 10m;",
|
|
||||||
" lua_shared_dict openflare_pow_config 1m;",
|
|
||||||
" lua_shared_dict openflare_pow_challenges 10m;",
|
|
||||||
" lua_shared_dict openflare_pow_sessions 20m;",
|
|
||||||
" lua_shared_dict openflare_waf_config 2m;",
|
|
||||||
fmt.Sprintf(" init_worker_by_lua_file %s/%s;", nginxLuaDirPlaceholder, openRestyObservabilityInitLuaPath),
|
|
||||||
fmt.Sprintf(" log_by_lua_file %s/%s;", nginxLuaDirPlaceholder, openRestyObservabilityLogLuaPath),
|
|
||||||
"",
|
|
||||||
fmt.Sprintf(" server {"),
|
|
||||||
fmt.Sprintf(" listen %s;", nginxObservabilityListenPlaceholder),
|
|
||||||
" server_name openflare-observability;",
|
|
||||||
" access_log off;",
|
|
||||||
"",
|
|
||||||
" location = /openflare/observability {",
|
|
||||||
" default_type application/json;",
|
|
||||||
fmt.Sprintf(" content_by_lua_file %s/%s;", nginxLuaDirPlaceholder, openRestyObservabilityReadLuaPath),
|
|
||||||
" }",
|
|
||||||
"",
|
|
||||||
" location = /openflare/stub_status {",
|
|
||||||
" stub_status;",
|
|
||||||
" }",
|
|
||||||
" }",
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringsJoinLines(lines ...string) string {
|
|
||||||
if len(lines) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
result := ""
|
|
||||||
for index, line := range lines {
|
|
||||||
if index > 0 {
|
|
||||||
result += "\n"
|
|
||||||
}
|
|
||||||
result += line
|
|
||||||
}
|
|
||||||
return result + "\n"
|
|
||||||
}
|
|
||||||
@@ -168,25 +168,3 @@ func rewriteOriginURLAddress(rawURL string, newAddress string) (string, error) {
|
|||||||
parsed.Host = formatOriginHost(address, port)
|
parsed.Host = formatOriginHost(address, port)
|
||||||
return parsed.String(), nil
|
return parsed.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitOriginURL(rawURL string) (scheme string, address string, port string, uri string, err error) {
|
|
||||||
parsed, err := url.ParseRequestURI(strings.TrimSpace(rawURL))
|
|
||||||
if err != nil {
|
|
||||||
return "", "", "", "", err
|
|
||||||
}
|
|
||||||
scheme = parsed.Scheme
|
|
||||||
address = normalizeOriginAddress(parsed.Hostname())
|
|
||||||
port = parsed.Port()
|
|
||||||
uri = parsed.EscapedPath()
|
|
||||||
if uri == "" {
|
|
||||||
uri = parsed.Path
|
|
||||||
}
|
|
||||||
if parsed.RawQuery != "" {
|
|
||||||
if uri == "" {
|
|
||||||
uri = "?" + parsed.RawQuery
|
|
||||||
} else {
|
|
||||||
uri = uri + "?" + parsed.RawQuery
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return scheme, address, port, uri, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,749 @@
|
|||||||
|
package openresty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RenderJSON(sourceJSON string, certificateFiles []SupportFile) (*Result, error) {
|
||||||
|
var doc Document
|
||||||
|
if err := json.Unmarshal([]byte(strings.TrimSpace(sourceJSON)), &doc); err != nil {
|
||||||
|
return nil, fmt.Errorf("openresty source config json is invalid: %w", err)
|
||||||
|
}
|
||||||
|
return Render(doc, certificateFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Render(doc Document, certificateFiles []SupportFile) (*Result, error) {
|
||||||
|
mainConfig := RenderMainConfig(doc.OpenRestyConfig)
|
||||||
|
routeConfig, err := RenderRouteConfig(doc, certificateFiles)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
wafConfig, err := RenderWAFConfig(doc.WAF)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
powConfig, err := RenderPoWConfig(doc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
files := append([]SupportFile(nil), certificateFiles...)
|
||||||
|
files = append(files, SupportFile{Path: "pow_config.json", Content: powConfig})
|
||||||
|
files = append(files, SupportFile{Path: "waf_config.json", Content: wafConfig})
|
||||||
|
files = DedupeSupportFiles(files)
|
||||||
|
return &Result{
|
||||||
|
MainConfig: mainConfig,
|
||||||
|
RouteConfig: routeConfig,
|
||||||
|
SupportFiles: files,
|
||||||
|
Checksum: ChecksumBundle(mainConfig, routeConfig, files),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderMainConfig(cfg ConfigSnapshot) string {
|
||||||
|
templateText := cfg.MainConfigTemplate
|
||||||
|
if strings.TrimSpace(templateText) == "" {
|
||||||
|
templateText = defaultMainConfigTemplate
|
||||||
|
}
|
||||||
|
return renderMainConfigTemplate(templateText, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateMainConfigTemplate(templateText string) error {
|
||||||
|
trimmed := strings.TrimSpace(templateText)
|
||||||
|
if trimmed == "" {
|
||||||
|
return errors.New("OpenRestyMainConfigTemplate 不能为空")
|
||||||
|
}
|
||||||
|
for _, placeholder := range requiredMainConfigTemplatePlaceholders {
|
||||||
|
if !strings.Contains(trimmed, placeholder) {
|
||||||
|
return fmt.Errorf("OpenRestyMainConfigTemplate 必须保留占位符 %s", placeholder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderRouteConfig(doc Document, certificateFiles []SupportFile) (string, error) {
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString("# This file is generated by OpenFlare. Do not edit manually.\n")
|
||||||
|
certificates := certificatesByID(certificateFiles)
|
||||||
|
for _, route := range doc.Routes {
|
||||||
|
domains := normalizedRouteDomains(route)
|
||||||
|
if len(domains) == 0 {
|
||||||
|
return "", fmt.Errorf("route %s domains are invalid", route.Domain)
|
||||||
|
}
|
||||||
|
serverNames := renderServerNames(domains)
|
||||||
|
displayName := strings.TrimSpace(route.SiteName)
|
||||||
|
if displayName == "" {
|
||||||
|
displayName = domains[0]
|
||||||
|
}
|
||||||
|
upstreams := route.Upstreams
|
||||||
|
if len(upstreams) == 0 && strings.TrimSpace(route.OriginURL) != "" {
|
||||||
|
upstreams = []string{route.OriginURL}
|
||||||
|
}
|
||||||
|
upstreamConfig := buildRouteUpstreamConfig(route, upstreams)
|
||||||
|
if upstreamConfig.UsesNamedUpstream {
|
||||||
|
builder.WriteString(renderNamedUpstreamBlock(upstreamConfig))
|
||||||
|
}
|
||||||
|
cacheConfig := routeCacheConfig{Enabled: route.CacheEnabled, Policy: route.CachePolicy, Rules: route.CacheRules}
|
||||||
|
limitConfig := routeLimitConfig{LimitConnPerServer: route.LimitConnPerServer, LimitConnPerIP: route.LimitConnPerIP, LimitRate: route.LimitRate}
|
||||||
|
powEnabled, _ := getPoWConfigForRoute(route.ID, doc.WAF)
|
||||||
|
if route.PoWEnabled {
|
||||||
|
powEnabled = true
|
||||||
|
}
|
||||||
|
if !route.EnableHTTPS {
|
||||||
|
builder.WriteString(renderHTTPProxyServer(serverNames, displayName, route.OriginURL, route.OriginHost, route.CustomHeaders, cacheConfig, limitConfig, upstreamConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, doc.OpenRestyConfig))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
certIDs := normalizeCertIDs(route.CertID, route.CertIDs)
|
||||||
|
domainCertIDs := normalizeDomainCertIDs(domains, certIDs, route.DomainCertIDs)
|
||||||
|
if len(certIDs) == 0 {
|
||||||
|
return "", fmt.Errorf("路由 %s 未配置证书", route.Domain)
|
||||||
|
}
|
||||||
|
httpOnlyDomains := make([]string, 0, len(domains))
|
||||||
|
domainsByCertID := make(map[uint][]string, len(certIDs))
|
||||||
|
for index, domain := range domains {
|
||||||
|
if index >= len(domainCertIDs) || domainCertIDs[index] == 0 {
|
||||||
|
httpOnlyDomains = append(httpOnlyDomains, domain)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
domainsByCertID[domainCertIDs[index]] = append(domainsByCertID[domainCertIDs[index]], domain)
|
||||||
|
}
|
||||||
|
for _, certID := range certIDs {
|
||||||
|
assignedDomains := domainsByCertID[certID]
|
||||||
|
if len(assignedDomains) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
certPEM, ok := certificates[certID]
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("route %s certificate %d does not exist", route.Domain, certID)
|
||||||
|
}
|
||||||
|
if err := validateCertificateCoverage(certPEM, assignedDomains); err != nil {
|
||||||
|
return "", fmt.Errorf("site %s certificate validation failed: %w", displayName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if route.RedirectHTTP {
|
||||||
|
if len(httpOnlyDomains) > 0 {
|
||||||
|
builder.WriteString(renderHTTPProxyServer(renderServerNames(httpOnlyDomains), displayName, route.OriginURL, route.OriginHost, route.CustomHeaders, cacheConfig, limitConfig, upstreamConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, doc.OpenRestyConfig))
|
||||||
|
}
|
||||||
|
for _, certID := range certIDs {
|
||||||
|
if assignedDomains := domainsByCertID[certID]; len(assignedDomains) > 0 {
|
||||||
|
builder.WriteString(renderHTTPRedirectServer(renderServerNames(assignedDomains)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder.WriteString(renderHTTPProxyServer(serverNames, displayName, route.OriginURL, route.OriginHost, route.CustomHeaders, cacheConfig, limitConfig, upstreamConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, doc.OpenRestyConfig))
|
||||||
|
}
|
||||||
|
for _, certID := range certIDs {
|
||||||
|
if assignedDomains := domainsByCertID[certID]; len(assignedDomains) > 0 {
|
||||||
|
builder.WriteString(renderHTTPSServer(renderServerNames(assignedDomains), displayName, route.OriginURL, route.OriginHost, certID, route.CustomHeaders, cacheConfig, limitConfig, upstreamConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, doc.OpenRestyConfig))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderPoWConfig(doc Document) (string, error) {
|
||||||
|
type domainEntry struct {
|
||||||
|
Domains []string `json:"domains"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Config *PoWConfig `json:"config"`
|
||||||
|
}
|
||||||
|
entries := make([]domainEntry, 0)
|
||||||
|
for _, route := range doc.Routes {
|
||||||
|
powEnabled, powConfig := getPoWConfigForRoute(route.ID, doc.WAF)
|
||||||
|
if route.PoWEnabled {
|
||||||
|
powEnabled = true
|
||||||
|
if route.PoWConfig != nil {
|
||||||
|
powConfig = route.PoWConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !powEnabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entries = append(entries, domainEntry{Domains: normalizedRouteDomains(route), Enabled: true, Config: powConfig})
|
||||||
|
}
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return "{}", nil
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(entries)
|
||||||
|
return string(data), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderWAFConfig(snapshot WAFDocument) (string, error) {
|
||||||
|
type wafRuntimeRuleGroup struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
IsGlobal bool `json:"is_global"`
|
||||||
|
BlockStatusCode int `json:"block_status_code"`
|
||||||
|
BlockResponseBody string `json:"block_response_body"`
|
||||||
|
IPWhitelist []string `json:"ip_whitelist"`
|
||||||
|
IPBlacklist []string `json:"ip_blacklist"`
|
||||||
|
CountryWhitelist []string `json:"country_whitelist"`
|
||||||
|
CountryBlacklist []string `json:"country_blacklist"`
|
||||||
|
RegionWhitelist []string `json:"region_whitelist"`
|
||||||
|
RegionBlacklist []string `json:"region_blacklist"`
|
||||||
|
}
|
||||||
|
type wafRuntimeConfig struct {
|
||||||
|
DefaultBlockStatusCode int `json:"default_block_status_code"`
|
||||||
|
RuleGroups []wafRuntimeRuleGroup `json:"rule_groups"`
|
||||||
|
SiteRuleGroups map[string][]uint `json:"site_rule_groups"`
|
||||||
|
}
|
||||||
|
groups := make([]wafRuntimeRuleGroup, 0, len(snapshot.RuleGroups))
|
||||||
|
globalGroupIDs := make([]uint, 0)
|
||||||
|
enabledGroupIDs := make(map[uint]struct{}, len(snapshot.RuleGroups))
|
||||||
|
for _, group := range snapshot.RuleGroups {
|
||||||
|
if !group.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
statusCode := group.BlockStatusCode
|
||||||
|
if statusCode == 0 {
|
||||||
|
statusCode = defaultWAFBlockStatus
|
||||||
|
}
|
||||||
|
if group.IsGlobal {
|
||||||
|
globalGroupIDs = append(globalGroupIDs, group.ID)
|
||||||
|
}
|
||||||
|
enabledGroupIDs[group.ID] = struct{}{}
|
||||||
|
groups = append(groups, wafRuntimeRuleGroup{ID: group.ID, Name: group.Name, IsGlobal: group.IsGlobal, BlockStatusCode: statusCode, BlockResponseBody: group.BlockResponseBody, IPWhitelist: group.IPWhitelist, IPBlacklist: group.IPBlacklist, CountryWhitelist: group.CountryWhitelist, CountryBlacklist: group.CountryBlacklist, RegionWhitelist: group.RegionWhitelist, RegionBlacklist: group.RegionBlacklist})
|
||||||
|
}
|
||||||
|
sort.Slice(groups, func(i, j int) bool {
|
||||||
|
if groups[i].IsGlobal != groups[j].IsGlobal {
|
||||||
|
return groups[i].IsGlobal
|
||||||
|
}
|
||||||
|
return groups[i].ID < groups[j].ID
|
||||||
|
})
|
||||||
|
sort.Slice(globalGroupIDs, func(i, j int) bool { return globalGroupIDs[i] < globalGroupIDs[j] })
|
||||||
|
siteRuleGroups := make(map[string][]uint, len(snapshot.Bindings))
|
||||||
|
for _, binding := range snapshot.Bindings {
|
||||||
|
ids := append([]uint{}, globalGroupIDs...)
|
||||||
|
for _, id := range binding.RuleGroupIDs {
|
||||||
|
if _, ok := enabledGroupIDs[id]; ok {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
siteRuleGroups[binding.SiteName] = uniqueUintIDs(ids)
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(wafRuntimeConfig{DefaultBlockStatusCode: defaultWAFBlockStatus, RuleGroups: groups, SiteRuleGroups: siteRuleGroups})
|
||||||
|
return string(data), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChecksumBundle(mainConfig string, routeConfig string, supportFiles []SupportFile) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString(mainConfig)
|
||||||
|
builder.WriteString("\n--route-config--\n")
|
||||||
|
builder.WriteString(routeConfig)
|
||||||
|
builder.WriteString("\n--support-files--\n")
|
||||||
|
files := DedupeSupportFiles(supportFiles)
|
||||||
|
sort.Slice(files, func(i int, j int) bool { return files[i].Path < files[j].Path })
|
||||||
|
for _, file := range files {
|
||||||
|
if file.Path == SourceConfigFileName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
builder.WriteString(file.Path)
|
||||||
|
builder.WriteString("\n")
|
||||||
|
builder.WriteString(file.Content)
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
|
sum := sha256.Sum256([]byte(builder.String()))
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func DedupeSupportFiles(files []SupportFile) []SupportFile {
|
||||||
|
if len(files) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
unique := make(map[string]SupportFile, len(files))
|
||||||
|
for _, file := range files {
|
||||||
|
unique[file.Path] = file
|
||||||
|
}
|
||||||
|
result := make([]SupportFile, 0, len(unique))
|
||||||
|
for _, file := range unique {
|
||||||
|
result = append(result, file)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderMainConfigTemplate(templateText string, cfg ConfigSnapshot) string {
|
||||||
|
replacer := strings.NewReplacer(
|
||||||
|
"{{OpenRestyWorkerProcesses}}", cfg.WorkerProcesses,
|
||||||
|
"{{OpenRestyWorkerConnections}}", fmt.Sprintf("%d", cfg.WorkerConnections),
|
||||||
|
"{{OpenRestyWorkerRlimitNofile}}", fmt.Sprintf("%d", cfg.WorkerRlimitNofile),
|
||||||
|
"{{OpenRestyConnectionUpgradeMap}}", renderConnectionUpgradeMap(),
|
||||||
|
"{{OpenRestyDefaultServerBlock}}", renderDefaultServerBlock(),
|
||||||
|
"{{OpenRestyAccessLogPath}}", AccessLogPlaceholder,
|
||||||
|
"{{OpenRestyEventsUseDirective}}", renderTemplateDirective(cfg.EventsUse != "", fmt.Sprintf("use %s;", cfg.EventsUse)),
|
||||||
|
"{{OpenRestyEventsMultiAcceptDirective}}", renderTemplateDirective(cfg.EventsMultiAcceptEnabled, "multi_accept on;"),
|
||||||
|
"{{OpenRestyKeepaliveTimeout}}", fmt.Sprintf("%d", cfg.KeepaliveTimeout),
|
||||||
|
"{{OpenRestyKeepaliveRequests}}", fmt.Sprintf("%d", cfg.KeepaliveRequests),
|
||||||
|
"{{OpenRestyClientHeaderTimeout}}", fmt.Sprintf("%d", cfg.ClientHeaderTimeout),
|
||||||
|
"{{OpenRestyClientBodyTimeout}}", fmt.Sprintf("%d", cfg.ClientBodyTimeout),
|
||||||
|
"{{OpenRestyClientMaxBodySize}}", cfg.ClientMaxBodySize,
|
||||||
|
"{{OpenRestyLargeClientHeaderBuffers}}", cfg.LargeClientHeaderBuffers,
|
||||||
|
"{{OpenRestySendTimeout}}", fmt.Sprintf("%d", cfg.SendTimeout),
|
||||||
|
"{{OpenRestyProxyConnectTimeout}}", fmt.Sprintf("%d", cfg.ProxyConnectTimeout),
|
||||||
|
"{{OpenRestyProxySendTimeout}}", fmt.Sprintf("%d", cfg.ProxySendTimeout),
|
||||||
|
"{{OpenRestyProxyReadTimeout}}", fmt.Sprintf("%d", cfg.ProxyReadTimeout),
|
||||||
|
"{{OpenRestyProxyRequestBuffering}}", onOff(cfg.ProxyRequestBuffering),
|
||||||
|
"{{OpenRestyProxyBuffering}}", onOff(cfg.ProxyBufferingEnabled),
|
||||||
|
"{{OpenRestyProxyBuffers}}", cfg.ProxyBuffers,
|
||||||
|
"{{OpenRestyProxyBufferSize}}", cfg.ProxyBufferSize,
|
||||||
|
"{{OpenRestyProxyBusyBuffersSize}}", cfg.ProxyBusyBuffersSize,
|
||||||
|
"{{OpenRestyGzip}}", onOff(cfg.GzipEnabled),
|
||||||
|
"{{OpenRestyGzipMinLength}}", fmt.Sprintf("%d", cfg.GzipMinLength),
|
||||||
|
"{{OpenRestyGzipCompLevel}}", fmt.Sprintf("%d", cfg.GzipCompLevel),
|
||||||
|
"{{OpenRestyResolverDirective}}", renderTemplateDirective(cfg.Resolvers != "", fmt.Sprintf("resolver %s;", cfg.Resolvers)),
|
||||||
|
"{{OpenRestyCacheBlock}}", renderOpenRestyCacheTemplateBlock(cfg),
|
||||||
|
"{{OpenRestyRouteConfigInclude}}", RouteConfigPlaceholder,
|
||||||
|
)
|
||||||
|
return replacer.Replace(templateText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTemplateDirective(enabled bool, statement string) string {
|
||||||
|
if !enabled {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(" %s\n", statement)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderOpenRestyCacheTemplateBlock(cfg ConfigSnapshot) string {
|
||||||
|
lines := []string{renderOpenRestyLimitZoneBlock()}
|
||||||
|
if !cfg.CacheEnabled {
|
||||||
|
lines = append(lines, renderOpenRestyObservabilityTemplateBlock())
|
||||||
|
return strings.Join(lines, "")
|
||||||
|
}
|
||||||
|
lines = append(lines, strings.Join([]string{
|
||||||
|
fmt.Sprintf(" proxy_cache_path %s levels=%s keys_zone=openflare_cache:10m inactive=%s max_size=%s;", cfg.CachePath, cfg.CacheLevels, cfg.CacheInactive, cfg.CacheMaxSize),
|
||||||
|
fmt.Sprintf(" proxy_cache_key \"%s\";", cfg.CacheKeyTemplate),
|
||||||
|
fmt.Sprintf(" proxy_cache_lock %s;", onOff(cfg.CacheLockEnabled)),
|
||||||
|
fmt.Sprintf(" proxy_cache_lock_timeout %s;", cfg.CacheLockTimeout),
|
||||||
|
fmt.Sprintf(" proxy_cache_use_stale %s;", cfg.CacheUseStale),
|
||||||
|
"",
|
||||||
|
}, "\n"))
|
||||||
|
lines = append(lines, renderOpenRestyObservabilityTemplateBlock())
|
||||||
|
return strings.Join(lines, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderOpenRestyLimitZoneBlock() string {
|
||||||
|
return " limit_conn_zone $server_name zone=openflare_conn_per_server:10m;\n limit_conn_zone $binary_remote_addr zone=openflare_conn_per_ip:10m;\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderOpenRestyObservabilityTemplateBlock() string {
|
||||||
|
return fmt.Sprintf(" log_by_lua_file %s/log.lua;\n lua_shared_dict openflare_pow_challenges 10m;\n lua_shared_dict openflare_pow_sessions 10m;\n lua_shared_dict openflare_pow_config 1m;\n lua_shared_dict openflare_waf_config 1m;\n\n server {\n listen %s;\n server_name openflare-observability;\n access_log off;\n\n location = /openflare/stub_status {\n stub_status;\n }\n\n location = /openflare/observability {\n content_by_lua_file %s/observability.lua;\n }\n }\n\n", LuaDirPlaceholder, ObservabilityListenPlaceholder, LuaDirPlaceholder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderHTTPProxyServer(serverNames string, siteName string, originURL string, originHost string, customHeaders []CustomHeader, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, upstreamConfig routeUpstreamConfig, powEnabled bool, basicAuthEnabled bool, basicAuthUsername string, basicAuthPassword string, cfg ConfigSnapshot) string {
|
||||||
|
return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n%s%s location / {\n%s%s%s%s%s }\n%s}\n\n", serverNames, renderAccessBlock(siteName, powEnabled), renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig, cfg), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig), renderPowStaticLocationBlock(powEnabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderHTTPRedirectServer(serverNames string) string {
|
||||||
|
return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n\n return 301 https://$host$request_uri;\n}\n\n", serverNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderHTTPSServer(serverNames string, siteName string, originURL string, originHost string, certificateID uint, customHeaders []CustomHeader, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, upstreamConfig routeUpstreamConfig, powEnabled bool, basicAuthEnabled bool, basicAuthUsername string, basicAuthPassword string, cfg ConfigSnapshot) string {
|
||||||
|
certPath := fmt.Sprintf("%s/%d.crt", CertDirPlaceholder, certificateID)
|
||||||
|
keyPath := fmt.Sprintf("%s/%d.key", CertDirPlaceholder, certificateID)
|
||||||
|
return fmt.Sprintf("server {\n listen 443 ssl;\n http2 on;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s%s location / {\n%s%s%s%s%s }\n%s}\n\n", serverNames, certPath, keyPath, renderAccessBlock(siteName, powEnabled), renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig, cfg), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig), renderPowStaticLocationBlock(powEnabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderProxyHeaderBlock(originURL string, originHost string, customHeaders []CustomHeader, upstreamConfig routeUpstreamConfig, cfg ConfigSnapshot) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
if strings.TrimSpace(originHost) != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf(" proxy_set_header Host %s;\n", quoteNginxStringLiteral(originHost)))
|
||||||
|
} else {
|
||||||
|
builder.WriteString(" proxy_set_header Host $host;\n")
|
||||||
|
}
|
||||||
|
if upstreamServerName := resolveUpstreamServerName(originURL, originHost); upstreamServerName != "" {
|
||||||
|
builder.WriteString(" proxy_ssl_server_name on;\n")
|
||||||
|
builder.WriteString(fmt.Sprintf(" proxy_ssl_name %s;\n", quoteNginxStringLiteral(upstreamServerName)))
|
||||||
|
}
|
||||||
|
builder.WriteString(" proxy_set_header X-Real-IP $remote_addr;\n")
|
||||||
|
builder.WriteString(" proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n")
|
||||||
|
builder.WriteString(" proxy_set_header X-Forwarded-Proto $scheme;\n")
|
||||||
|
if cfg.WebsocketEnabled {
|
||||||
|
builder.WriteString(" proxy_http_version 1.1;\n")
|
||||||
|
builder.WriteString(" proxy_set_header Connection $connection_upgrade;\n")
|
||||||
|
builder.WriteString(" proxy_set_header Upgrade $http_upgrade;\n")
|
||||||
|
} else if upstreamConfig.UsesNamedUpstream {
|
||||||
|
builder.WriteString(" proxy_http_version 1.1;\n")
|
||||||
|
builder.WriteString(" proxy_set_header Connection \"\";\n")
|
||||||
|
}
|
||||||
|
for _, header := range customHeaders {
|
||||||
|
builder.WriteString(fmt.Sprintf(" proxy_set_header %s %s;\n", header.Key, quoteNginxStringLiteral(header.Value)))
|
||||||
|
}
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderAccessBlock(siteName string, powEnabled bool) string {
|
||||||
|
escapedSiteName := escapeNginxString(siteName)
|
||||||
|
if !powEnabled {
|
||||||
|
return fmt.Sprintf(" set $openflare_waf_site \"%s\";\n access_by_lua_file %s/waf/check.lua;\n", escapedSiteName, LuaDirPlaceholder)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(` set $openflare_waf_site "%s";
|
||||||
|
access_by_lua_block {
|
||||||
|
package.path = "%s/?.lua;%s/?/init.lua;" .. package.path
|
||||||
|
require("waf.runtime").check()
|
||||||
|
if ngx.ctx.openflare_waf_blocked then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
require("pow.runtime").check()
|
||||||
|
}
|
||||||
|
`, escapedSiteName, LuaDirPlaceholder, LuaDirPlaceholder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderBasicAuthBlock(enabled bool, username, password string) string {
|
||||||
|
if !enabled || username == "" || password == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
encoded := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
|
||||||
|
return fmt.Sprintf(" rewrite_by_lua_block {\n local auth = ngx.var.http_authorization\n if auth ~= \"Basic %s\" then\n ngx.header[\"WWW-Authenticate\"] = 'Basic realm=\"Restricted\"'\n return ngx.exit(401)\n end\n }\n", encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderPowLocationBlocks(powEnabled bool) string {
|
||||||
|
if !powEnabled {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("\n location = %spass-challenge {\n content_by_lua_file %s/pow/verify.lua;\n }\n\n location = %smake-challenge {\n content_by_lua_file %s/pow/challenge.lua;\n }\n\n", anubisAPIPrefix, LuaDirPlaceholder, anubisAPIPrefix, LuaDirPlaceholder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderPowStaticLocationBlock(powEnabled bool) string {
|
||||||
|
if !powEnabled {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(" location %s {\n alias %s/;\n types {\n text/css css;\n application/javascript js mjs;\n application/json json;\n image/webp webp;\n font/woff2 woff2;\n }\n }\n\n", anubisStaticPrefix, PowStaticDirPlaceholder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderRouteCacheBlock(cacheConfig routeCacheConfig, cfg ConfigSnapshot) string {
|
||||||
|
if !cfg.CacheEnabled || !cacheConfig.Enabled {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString(" set $openflare_skip_cache 0;\n")
|
||||||
|
builder.WriteString(" if ($request_method != GET) {\n set $openflare_skip_cache 1;\n }\n")
|
||||||
|
builder.WriteString(" if ($http_authorization != \"\") {\n set $openflare_skip_cache 1;\n }\n")
|
||||||
|
builder.WriteString(" if ($http_cookie ~* \"(session|sess|token|auth|jwt|logged_in|remember|laravel_session|connect\\\\.sid|_session)\") {\n set $openflare_skip_cache 1;\n }\n")
|
||||||
|
builder.WriteString(" if ($http_cache_control ~* \"(no-cache|no-store|private)\") {\n set $openflare_skip_cache 1;\n }\n")
|
||||||
|
if condition := renderRouteCachePolicyCondition(cacheConfig); condition != "" {
|
||||||
|
builder.WriteString(condition)
|
||||||
|
}
|
||||||
|
builder.WriteString(" proxy_cache openflare_cache;\n")
|
||||||
|
builder.WriteString(" proxy_cache_methods GET;\n")
|
||||||
|
builder.WriteString(" proxy_cache_bypass $openflare_skip_cache;\n")
|
||||||
|
builder.WriteString(" proxy_no_cache $openflare_skip_cache;\n")
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderRouteLimitBlock(limitConfig routeLimitConfig) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
if limitConfig.LimitConnPerServer > 0 {
|
||||||
|
builder.WriteString(fmt.Sprintf(" limit_conn openflare_conn_per_server %d;\n", limitConfig.LimitConnPerServer))
|
||||||
|
}
|
||||||
|
if limitConfig.LimitConnPerIP > 0 {
|
||||||
|
builder.WriteString(fmt.Sprintf(" limit_conn openflare_conn_per_ip %d;\n", limitConfig.LimitConnPerIP))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(limitConfig.LimitRate) != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf(" limit_rate %s;\n", limitConfig.LimitRate))
|
||||||
|
}
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderRouteCachePolicyCondition(cacheConfig routeCacheConfig) string {
|
||||||
|
switch cacheConfig.Policy {
|
||||||
|
case cachePolicySuffix:
|
||||||
|
return fmt.Sprintf(" if ($uri !~* %s) {\n set $openflare_skip_cache 1;\n }\n", quoteNginxStringLiteral(buildSuffixMatchPattern(cacheConfig.Rules)))
|
||||||
|
case cachePolicyPathPrefix:
|
||||||
|
return fmt.Sprintf(" if ($uri !~ %s) {\n set $openflare_skip_cache 1;\n }\n", quoteNginxStringLiteral(buildPathPrefixMatchPattern(cacheConfig.Rules)))
|
||||||
|
case cachePolicyPathExact:
|
||||||
|
return fmt.Sprintf(" if ($uri !~ %s) {\n set $openflare_skip_cache 1;\n }\n", quoteNginxStringLiteral(buildPathExactMatchPattern(cacheConfig.Rules)))
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderProxyPassBlock(originURL string, upstreamConfig routeUpstreamConfig) string {
|
||||||
|
parsed, err := url.Parse(originURL)
|
||||||
|
if err != nil || parsed.Host == "" || parsed.Scheme == "" {
|
||||||
|
return fmt.Sprintf(" proxy_pass %s;\n", originURL)
|
||||||
|
}
|
||||||
|
if upstreamConfig.UsesNamedUpstream {
|
||||||
|
return fmt.Sprintf(" proxy_pass %s://%s%s;\n", upstreamConfig.Scheme, upstreamConfig.Name, upstreamConfig.ProxyPassURI)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(" proxy_pass %s;\n", originURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildRouteUpstreamConfig(route Route, upstreams []string) routeUpstreamConfig {
|
||||||
|
if len(upstreams) == 0 {
|
||||||
|
return routeUpstreamConfig{}
|
||||||
|
}
|
||||||
|
if len(upstreams) == 1 {
|
||||||
|
parsed, err := url.Parse(strings.TrimSpace(upstreams[0]))
|
||||||
|
if err != nil || parsed.Host == "" || parsed.Scheme == "" {
|
||||||
|
return routeUpstreamConfig{}
|
||||||
|
}
|
||||||
|
return routeUpstreamConfig{Name: buildRouteUpstreamName(route), Scheme: parsed.Scheme, ProxyPassURI: buildUpstreamProxyPassURI(parsed), Servers: []string{parsed.Host}, UsesNamedUpstream: true}
|
||||||
|
}
|
||||||
|
servers := make([]string, 0, len(upstreams))
|
||||||
|
var scheme string
|
||||||
|
for _, upstream := range upstreams {
|
||||||
|
parsed, err := url.Parse(strings.TrimSpace(upstream))
|
||||||
|
if err != nil || parsed.Host == "" || parsed.Scheme == "" || (strings.TrimSpace(parsed.EscapedPath()) != "" && strings.TrimSpace(parsed.EscapedPath()) != "/") || parsed.RawQuery != "" {
|
||||||
|
return routeUpstreamConfig{}
|
||||||
|
}
|
||||||
|
if scheme == "" {
|
||||||
|
scheme = parsed.Scheme
|
||||||
|
} else if scheme != parsed.Scheme {
|
||||||
|
return routeUpstreamConfig{}
|
||||||
|
}
|
||||||
|
servers = append(servers, parsed.Host)
|
||||||
|
}
|
||||||
|
return routeUpstreamConfig{Name: buildRouteUpstreamName(route), Scheme: scheme, Servers: servers, UsesNamedUpstream: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderNamedUpstreamBlock(upstreamConfig routeUpstreamConfig) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString(fmt.Sprintf("upstream %s {\n", upstreamConfig.Name))
|
||||||
|
for _, server := range upstreamConfig.Servers {
|
||||||
|
builder.WriteString(fmt.Sprintf(" server %s max_fails=3 fail_timeout=10s;\n", server))
|
||||||
|
}
|
||||||
|
builder.WriteString(" keepalive 128;\n}\n\n")
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildRouteUpstreamName(route Route) string {
|
||||||
|
identity := strings.TrimSpace(route.SiteName)
|
||||||
|
if identity == "" {
|
||||||
|
identity = route.Domain
|
||||||
|
}
|
||||||
|
sanitized := strings.Map(func(r rune) rune {
|
||||||
|
switch {
|
||||||
|
case r >= 'a' && r <= 'z':
|
||||||
|
return r
|
||||||
|
case r >= 'A' && r <= 'Z':
|
||||||
|
return r + ('a' - 'A')
|
||||||
|
case r >= '0' && r <= '9':
|
||||||
|
return r
|
||||||
|
default:
|
||||||
|
return '_'
|
||||||
|
}
|
||||||
|
}, identity)
|
||||||
|
sanitized = strings.Trim(sanitized, "_")
|
||||||
|
if sanitized == "" {
|
||||||
|
sanitized = "backend"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("backend_%s_%d", sanitized, route.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildUpstreamProxyPassURI(parsed *url.URL) string {
|
||||||
|
path := parsed.EscapedPath()
|
||||||
|
if path == "/" {
|
||||||
|
path = ""
|
||||||
|
}
|
||||||
|
if parsed.RawQuery == "" {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s?%s", path, parsed.RawQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderConnectionUpgradeMap() string {
|
||||||
|
return " map $http_upgrade $connection_upgrade {\n default upgrade;\n '' \"\";\n }\n\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderDefaultServerBlock() string {
|
||||||
|
return strings.Join([]string{" server {", " listen 80 default_server;", " server_name _;", "", " return 404;", " }", "", " server {", " listen 443 ssl default_server;", " server_name _;", "", " ssl_reject_handshake on;", " }", ""}, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizedRouteDomains(route Route) []string {
|
||||||
|
if len(route.Domains) > 0 {
|
||||||
|
return route.Domains
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(route.Domain) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []string{strings.TrimSpace(route.Domain)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeCertIDs(primaryCertID *uint, certIDs []uint) []uint {
|
||||||
|
candidates := make([]uint, 0, len(certIDs)+1)
|
||||||
|
if primaryCertID != nil && *primaryCertID != 0 {
|
||||||
|
candidates = append(candidates, *primaryCertID)
|
||||||
|
}
|
||||||
|
candidates = append(candidates, certIDs...)
|
||||||
|
seen := make(map[uint]struct{}, len(candidates))
|
||||||
|
normalized := make([]uint, 0, len(candidates))
|
||||||
|
for _, id := range candidates {
|
||||||
|
if id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[id]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
normalized = append(normalized, id)
|
||||||
|
}
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeDomainCertIDs(domains []string, certIDs []uint, domainCertIDs []uint) []uint {
|
||||||
|
if len(domainCertIDs) > 0 {
|
||||||
|
normalized := make([]uint, len(domainCertIDs))
|
||||||
|
copy(normalized, domainCertIDs)
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
if len(certIDs) == 1 {
|
||||||
|
normalized := make([]uint, len(domains))
|
||||||
|
for index := range normalized {
|
||||||
|
normalized[index] = certIDs[0]
|
||||||
|
}
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
if len(certIDs) == len(domains) {
|
||||||
|
normalized := make([]uint, len(certIDs))
|
||||||
|
copy(normalized, certIDs)
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
return []uint{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func certificatesByID(files []SupportFile) map[uint]string {
|
||||||
|
result := make(map[uint]string)
|
||||||
|
for _, file := range files {
|
||||||
|
if !strings.HasSuffix(file.Path, ".crt") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
idText := strings.TrimSuffix(file.Path, ".crt")
|
||||||
|
var id uint
|
||||||
|
if _, err := fmt.Sscanf(idText, "%d", &id); err == nil && id != 0 {
|
||||||
|
result[id] = file.Content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateCertificateCoverage(certPEM string, domains []string) error {
|
||||||
|
block, _ := pem.Decode([]byte(certPEM))
|
||||||
|
if block == nil {
|
||||||
|
return errors.New("certificate PEM is invalid")
|
||||||
|
}
|
||||||
|
leaf, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, domain := range domains {
|
||||||
|
if err := leaf.VerifyHostname(domain); err != nil {
|
||||||
|
return fmt.Errorf("certificate does not cover domain %s", domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPoWConfigForRoute(routeID uint, snapshot WAFDocument) (bool, *PoWConfig) {
|
||||||
|
for _, binding := range snapshot.Bindings {
|
||||||
|
if binding.RouteID != routeID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, groupID := range binding.RuleGroupIDs {
|
||||||
|
for _, group := range snapshot.RuleGroups {
|
||||||
|
if group.ID == groupID && group.PoWEnabled {
|
||||||
|
return true, group.PoWConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for _, group := range snapshot.RuleGroups {
|
||||||
|
if group.IsGlobal && group.PoWEnabled {
|
||||||
|
return true, group.PoWConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func uniqueUintIDs(values []uint) []uint {
|
||||||
|
seen := make(map[uint]struct{}, len(values))
|
||||||
|
result := make([]uint, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
if value == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[value]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[value] = struct{}{}
|
||||||
|
result = append(result, value)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveUpstreamServerName(originURL string, originHost string) string {
|
||||||
|
parsed, err := url.Parse(originURL)
|
||||||
|
if err != nil || !strings.EqualFold(parsed.Scheme, "https") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(originHost) != "" {
|
||||||
|
parsedHost, err := url.Parse("//" + originHost)
|
||||||
|
if err == nil && parsedHost.Hostname() != "" {
|
||||||
|
return parsedHost.Hostname()
|
||||||
|
}
|
||||||
|
return originHost
|
||||||
|
}
|
||||||
|
return parsed.Hostname()
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderServerNames(domains []string) string { return strings.Join(domains, " ") }
|
||||||
|
|
||||||
|
func onOff(value bool) string {
|
||||||
|
if value {
|
||||||
|
return "on"
|
||||||
|
}
|
||||||
|
return "off"
|
||||||
|
}
|
||||||
|
|
||||||
|
func quoteNginxStringLiteral(value string) string {
|
||||||
|
escaped := strings.ReplaceAll(value, `\`, `\\`)
|
||||||
|
escaped = strings.ReplaceAll(escaped, `"`, `\"`)
|
||||||
|
return fmt.Sprintf(`"%s"`, escaped)
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeNginxString(value string) string {
|
||||||
|
escaped := strings.ReplaceAll(value, `\`, `\\`)
|
||||||
|
escaped = strings.ReplaceAll(escaped, `"`, `\"`)
|
||||||
|
return escaped
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSuffixMatchPattern(rules []string) string {
|
||||||
|
parts := make([]string, 0, len(rules))
|
||||||
|
for _, rule := range rules {
|
||||||
|
parts = append(parts, regexp.QuoteMeta(rule))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("\\.(?:%s)$", strings.Join(parts, "|"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPathPrefixMatchPattern(rules []string) string {
|
||||||
|
parts := make([]string, 0, len(rules))
|
||||||
|
for _, rule := range rules {
|
||||||
|
trimmed := strings.TrimRight(rule, "/")
|
||||||
|
if trimmed == "" {
|
||||||
|
trimmed = "/"
|
||||||
|
}
|
||||||
|
if trimmed == "/" {
|
||||||
|
parts = append(parts, "/")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts = append(parts, fmt.Sprintf("%s(?:/|$)", regexp.QuoteMeta(trimmed)))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("^(?:%s)", strings.Join(parts, "|"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPathExactMatchPattern(rules []string) string {
|
||||||
|
parts := make([]string, 0, len(rules))
|
||||||
|
for _, rule := range rules {
|
||||||
|
parts = append(parts, regexp.QuoteMeta(rule))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("^(?:%s)$", strings.Join(parts, "|"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
package openresty
|
||||||
|
|
||||||
|
const (
|
||||||
|
CertDirPlaceholder = "__OPENFLARE_CERT_DIR__"
|
||||||
|
RouteConfigPlaceholder = "__OPENFLARE_ROUTE_CONFIG__"
|
||||||
|
AccessLogPlaceholder = "__OPENFLARE_ACCESS_LOG__"
|
||||||
|
LuaDirPlaceholder = "__OPENFLARE_LUA_DIR__"
|
||||||
|
ObservabilityListenPlaceholder = "__OPENFLARE_OBSERVABILITY_LISTEN__"
|
||||||
|
ObservabilityPortPlaceholder = "__OPENFLARE_OBSERVABILITY_PORT__"
|
||||||
|
PowStaticDirPlaceholder = "__OPENFLARE_POW_STATIC_DIR__"
|
||||||
|
|
||||||
|
SourceConfigFileName = "openresty_source_config.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cachePolicySuffix = "suffix"
|
||||||
|
cachePolicyPathPrefix = "path_prefix"
|
||||||
|
cachePolicyPathExact = "path_exact"
|
||||||
|
defaultWAFBlockStatus = 418
|
||||||
|
anubisStaticPrefix = "/.within.website/x/cmd/anubis/static/"
|
||||||
|
anubisAPIPrefix = "/.within.website/x/cmd/anubis/api/"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultMainConfigTemplate = `# This file is generated by OpenFlare. Do not edit manually.
|
||||||
|
worker_processes {{OpenRestyWorkerProcesses}};
|
||||||
|
worker_rlimit_nofile {{OpenRestyWorkerRlimitNofile}};
|
||||||
|
pid logs/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections {{OpenRestyWorkerConnections}};
|
||||||
|
{{OpenRestyEventsUseDirective}}{{OpenRestyEventsMultiAcceptDirective}}}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
{{OpenRestyConnectionUpgradeMap}}{{OpenRestyDefaultServerBlock}} log_format openflare_json escape=json '{"ts":"$time_iso8601","host":"$host","path":"$request_uri","remote_addr":"$remote_addr","status":$status,"request_time":$request_time,"bytes_sent":$body_bytes_sent,"request_length":$request_length}';
|
||||||
|
access_log {{OpenRestyAccessLogPath}} openflare_json;
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout {{OpenRestyKeepaliveTimeout}};
|
||||||
|
keepalive_requests {{OpenRestyKeepaliveRequests}};
|
||||||
|
client_header_timeout {{OpenRestyClientHeaderTimeout}};
|
||||||
|
client_body_timeout {{OpenRestyClientBodyTimeout}};
|
||||||
|
client_max_body_size {{OpenRestyClientMaxBodySize}};
|
||||||
|
large_client_header_buffers {{OpenRestyLargeClientHeaderBuffers}};
|
||||||
|
send_timeout {{OpenRestySendTimeout}};
|
||||||
|
proxy_connect_timeout {{OpenRestyProxyConnectTimeout}};
|
||||||
|
proxy_send_timeout {{OpenRestyProxySendTimeout}};
|
||||||
|
proxy_read_timeout {{OpenRestyProxyReadTimeout}};
|
||||||
|
proxy_request_buffering {{OpenRestyProxyRequestBuffering}};
|
||||||
|
proxy_buffering {{OpenRestyProxyBuffering}};
|
||||||
|
proxy_buffers {{OpenRestyProxyBuffers}};
|
||||||
|
proxy_buffer_size {{OpenRestyProxyBufferSize}};
|
||||||
|
proxy_busy_buffers_size {{OpenRestyProxyBusyBuffersSize}};
|
||||||
|
gzip {{OpenRestyGzip}};
|
||||||
|
gzip_min_length {{OpenRestyGzipMinLength}};
|
||||||
|
gzip_comp_level {{OpenRestyGzipCompLevel}};
|
||||||
|
{{OpenRestyResolverDirective}}{{OpenRestyCacheBlock}} include {{OpenRestyRouteConfigInclude}};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
type SupportFile struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomHeader struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PoWListConfig struct {
|
||||||
|
IPs []string `json:"ips"`
|
||||||
|
IPCidrs []string `json:"ip_cidrs"`
|
||||||
|
Paths []string `json:"paths"`
|
||||||
|
PathRegexes []string `json:"path_regexes"`
|
||||||
|
UserAgents []string `json:"user_agents"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PoWConfig struct {
|
||||||
|
Difficulty int `json:"difficulty"`
|
||||||
|
Algorithm string `json:"algorithm"`
|
||||||
|
SessionTTL int `json:"session_ttl"`
|
||||||
|
ChallengeTTL int `json:"challenge_ttl"`
|
||||||
|
Whitelist PoWListConfig `json:"whitelist"`
|
||||||
|
Blacklist PoWListConfig `json:"blacklist"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Route struct {
|
||||||
|
ID uint `json:"id,omitempty"`
|
||||||
|
SiteName string `json:"site_name,omitempty"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Domains []string `json:"domains,omitempty"`
|
||||||
|
OriginURL string `json:"origin_url"`
|
||||||
|
OriginHost string `json:"origin_host,omitempty"`
|
||||||
|
Upstreams []string `json:"upstreams,omitempty"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
EnableHTTPS bool `json:"enable_https"`
|
||||||
|
CertID *uint `json:"cert_id,omitempty"`
|
||||||
|
CertIDs []uint `json:"cert_ids,omitempty"`
|
||||||
|
DomainCertIDs []uint `json:"domain_cert_ids,omitempty"`
|
||||||
|
RedirectHTTP bool `json:"redirect_http"`
|
||||||
|
LimitConnPerServer int `json:"limit_conn_per_server,omitempty"`
|
||||||
|
LimitConnPerIP int `json:"limit_conn_per_ip,omitempty"`
|
||||||
|
LimitRate string `json:"limit_rate,omitempty"`
|
||||||
|
CacheEnabled bool `json:"cache_enabled"`
|
||||||
|
CachePolicy string `json:"cache_policy,omitempty"`
|
||||||
|
CacheRules []string `json:"cache_rules,omitempty"`
|
||||||
|
CustomHeaders []CustomHeader `json:"custom_headers,omitempty"`
|
||||||
|
PoWEnabled bool `json:"pow_enabled,omitempty"`
|
||||||
|
PoWConfig *PoWConfig `json:"pow_config,omitempty"`
|
||||||
|
BasicAuthEnabled bool `json:"basic_auth_enabled,omitempty"`
|
||||||
|
BasicAuthUsername string `json:"basic_auth_username,omitempty"`
|
||||||
|
BasicAuthPassword string `json:"basic_auth_password,omitempty"`
|
||||||
|
Remark string `json:"remark,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WAFRuleGroup struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
IsGlobal bool `json:"is_global"`
|
||||||
|
BlockStatusCode int `json:"block_status_code"`
|
||||||
|
BlockResponseBody string `json:"block_response_body,omitempty"`
|
||||||
|
IPWhitelist []string `json:"ip_whitelist,omitempty"`
|
||||||
|
IPBlacklist []string `json:"ip_blacklist,omitempty"`
|
||||||
|
CountryWhitelist []string `json:"country_whitelist,omitempty"`
|
||||||
|
CountryBlacklist []string `json:"country_blacklist,omitempty"`
|
||||||
|
RegionWhitelist []string `json:"region_whitelist,omitempty"`
|
||||||
|
RegionBlacklist []string `json:"region_blacklist,omitempty"`
|
||||||
|
PoWEnabled bool `json:"pow_enabled,omitempty"`
|
||||||
|
PoWConfig *PoWConfig `json:"pow_config,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WAFBinding struct {
|
||||||
|
RouteID uint `json:"route_id"`
|
||||||
|
SiteName string `json:"site_name"`
|
||||||
|
RuleGroupIDs []uint `json:"rule_group_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WAFDocument struct {
|
||||||
|
RuleGroups []WAFRuleGroup `json:"rule_groups"`
|
||||||
|
Bindings []WAFBinding `json:"bindings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigSnapshot struct {
|
||||||
|
WorkerProcesses string `json:"worker_processes"`
|
||||||
|
WorkerConnections int `json:"worker_connections"`
|
||||||
|
WorkerRlimitNofile int `json:"worker_rlimit_nofile"`
|
||||||
|
EventsUse string `json:"events_use,omitempty"`
|
||||||
|
EventsMultiAcceptEnabled bool `json:"events_multi_accept_enabled"`
|
||||||
|
KeepaliveTimeout int `json:"keepalive_timeout"`
|
||||||
|
KeepaliveRequests int `json:"keepalive_requests"`
|
||||||
|
ClientHeaderTimeout int `json:"client_header_timeout"`
|
||||||
|
ClientBodyTimeout int `json:"client_body_timeout"`
|
||||||
|
ClientMaxBodySize string `json:"client_max_body_size"`
|
||||||
|
LargeClientHeaderBuffers string `json:"large_client_header_buffers"`
|
||||||
|
SendTimeout int `json:"send_timeout"`
|
||||||
|
ProxyConnectTimeout int `json:"proxy_connect_timeout"`
|
||||||
|
ProxySendTimeout int `json:"proxy_send_timeout"`
|
||||||
|
ProxyReadTimeout int `json:"proxy_read_timeout"`
|
||||||
|
WebsocketEnabled bool `json:"websocket_enabled"`
|
||||||
|
ProxyRequestBuffering bool `json:"proxy_request_buffering"`
|
||||||
|
ProxyBufferingEnabled bool `json:"proxy_buffering_enabled"`
|
||||||
|
ProxyBuffers string `json:"proxy_buffers"`
|
||||||
|
ProxyBufferSize string `json:"proxy_buffer_size"`
|
||||||
|
ProxyBusyBuffersSize string `json:"proxy_busy_buffers_size"`
|
||||||
|
GzipEnabled bool `json:"gzip_enabled"`
|
||||||
|
GzipMinLength int `json:"gzip_min_length"`
|
||||||
|
GzipCompLevel int `json:"gzip_comp_level"`
|
||||||
|
Resolvers string `json:"resolvers,omitempty"`
|
||||||
|
CacheEnabled bool `json:"cache_enabled"`
|
||||||
|
CachePath string `json:"cache_path,omitempty"`
|
||||||
|
CacheLevels string `json:"cache_levels"`
|
||||||
|
CacheInactive string `json:"cache_inactive"`
|
||||||
|
CacheMaxSize string `json:"cache_max_size"`
|
||||||
|
CacheKeyTemplate string `json:"cache_key_template"`
|
||||||
|
CacheLockEnabled bool `json:"cache_lock_enabled"`
|
||||||
|
CacheLockTimeout string `json:"cache_lock_timeout"`
|
||||||
|
CacheUseStale string `json:"cache_use_stale"`
|
||||||
|
MainConfigTemplate string `json:"main_config_template,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Document struct {
|
||||||
|
Routes []Route `json:"routes"`
|
||||||
|
OpenRestyConfig ConfigSnapshot `json:"openresty_config"`
|
||||||
|
WAF WAFDocument `json:"waf"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
MainConfig string
|
||||||
|
RouteConfig string
|
||||||
|
SupportFiles []SupportFile
|
||||||
|
Checksum string
|
||||||
|
}
|
||||||
|
|
||||||
|
type routeCacheConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
Policy string
|
||||||
|
Rules []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type routeLimitConfig struct {
|
||||||
|
LimitConnPerServer int
|
||||||
|
LimitConnPerIP int
|
||||||
|
LimitRate string
|
||||||
|
}
|
||||||
|
|
||||||
|
type routeUpstreamConfig struct {
|
||||||
|
Name string
|
||||||
|
Scheme string
|
||||||
|
ProxyPassURI string
|
||||||
|
Servers []string
|
||||||
|
UsesNamedUpstream bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var requiredMainConfigTemplatePlaceholders = []string{
|
||||||
|
"{{OpenRestyWorkerProcesses}}",
|
||||||
|
"{{OpenRestyWorkerConnections}}",
|
||||||
|
"{{OpenRestyWorkerRlimitNofile}}",
|
||||||
|
"{{OpenRestyConnectionUpgradeMap}}",
|
||||||
|
"{{OpenRestyDefaultServerBlock}}",
|
||||||
|
"{{OpenRestyAccessLogPath}}",
|
||||||
|
"{{OpenRestyEventsUseDirective}}",
|
||||||
|
"{{OpenRestyEventsMultiAcceptDirective}}",
|
||||||
|
"{{OpenRestyKeepaliveTimeout}}",
|
||||||
|
"{{OpenRestyKeepaliveRequests}}",
|
||||||
|
"{{OpenRestyClientHeaderTimeout}}",
|
||||||
|
"{{OpenRestyClientBodyTimeout}}",
|
||||||
|
"{{OpenRestyClientMaxBodySize}}",
|
||||||
|
"{{OpenRestyLargeClientHeaderBuffers}}",
|
||||||
|
"{{OpenRestySendTimeout}}",
|
||||||
|
"{{OpenRestyProxyConnectTimeout}}",
|
||||||
|
"{{OpenRestyProxySendTimeout}}",
|
||||||
|
"{{OpenRestyProxyReadTimeout}}",
|
||||||
|
"{{OpenRestyProxyRequestBuffering}}",
|
||||||
|
"{{OpenRestyProxyBuffering}}",
|
||||||
|
"{{OpenRestyProxyBuffers}}",
|
||||||
|
"{{OpenRestyProxyBufferSize}}",
|
||||||
|
"{{OpenRestyProxyBusyBuffersSize}}",
|
||||||
|
"{{OpenRestyGzip}}",
|
||||||
|
"{{OpenRestyGzipMinLength}}",
|
||||||
|
"{{OpenRestyGzipCompLevel}}",
|
||||||
|
"{{OpenRestyCacheBlock}}",
|
||||||
|
"{{OpenRestyRouteConfigInclude}}",
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user