This commit is contained in:
ryan
2026-06-19 15:13:24 +08:00
parent 0b34792709
commit 32861c5db9
376 changed files with 3648 additions and 19957 deletions
+51 -126
View File
@@ -1,3 +1,4 @@
// Package openresty renders OpenResty configuration from proxy route definitions.
package openresty
import (
@@ -16,6 +17,13 @@ import (
"strings"
)
const (
routeUpstreamTypePages = "pages"
indexHTML = "/index.html"
)
// RenderJSON parses the given JSON string as a Document and renders the full
// OpenResty configuration bundle, injecting the provided certificate support files.
func RenderJSON(sourceJSON string, certificateFiles []SupportFile) (*Result, error) {
var doc Document
if err := json.Unmarshal([]byte(strings.TrimSpace(sourceJSON)), &doc); err != nil {
@@ -24,6 +32,8 @@ func RenderJSON(sourceJSON string, certificateFiles []SupportFile) (*Result, err
return Render(doc, certificateFiles)
}
// Render produces a complete OpenResty configuration Result from a Document and
// a set of certificate support files.
func Render(doc Document, certificateFiles []SupportFile) (*Result, error) {
mainConfig := RenderMainConfig(doc.OpenRestyConfig)
routeConfig, err := RenderRouteConfig(doc, certificateFiles)
@@ -45,6 +55,8 @@ func Render(doc Document, certificateFiles []SupportFile) (*Result, error) {
}, nil
}
// RenderMainConfig renders the nginx main configuration string from the given
// ConfigSnapshot, falling back to the built-in default template when none is set.
func RenderMainConfig(cfg ConfigSnapshot) string {
templateText := cfg.MainConfigTemplate
if strings.TrimSpace(templateText) == "" {
@@ -53,6 +65,8 @@ func RenderMainConfig(cfg ConfigSnapshot) string {
return renderMainConfigTemplate(templateText, cfg)
}
// ValidateMainConfigTemplate checks that the provided template text is non-empty
// and contains all required OpenResty placeholder tokens.
func ValidateMainConfigTemplate(templateText string) error {
trimmed := strings.TrimSpace(templateText)
if trimmed == "" {
@@ -66,6 +80,8 @@ func ValidateMainConfigTemplate(templateText string) error {
return nil
}
// RenderRouteConfig generates the nginx server-block configuration for all
// routes in the Document, resolving certificate files as needed.
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")
@@ -83,120 +99,21 @@ func RenderRouteConfig(doc Document, certificateFiles []SupportFile) (string, er
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 normalizeRouteUpstreamType(route.UpstreamType) == "pages" {
if route.PagesDeployment == nil {
return "", fmt.Errorf("route %s pages deployment is missing", route.Domain)
}
if !route.EnableHTTPS {
builder.WriteString(renderHTTPPagesServer(serverNames, displayName, route.PagesDeployment, limitConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword))
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(renderHTTPPagesServer(renderServerNames(httpOnlyDomains), displayName, route.PagesDeployment, limitConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword))
}
for _, certID := range certIDs {
if assignedDomains := domainsByCertID[certID]; len(assignedDomains) > 0 {
builder.WriteString(renderHTTPRedirectServer(renderServerNames(assignedDomains)))
}
}
} else {
builder.WriteString(renderHTTPPagesServer(serverNames, displayName, route.PagesDeployment, limitConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword))
}
for _, certID := range certIDs {
if assignedDomains := domainsByCertID[certID]; len(assignedDomains) > 0 {
builder.WriteString(renderHTTPSPagesServer(renderServerNames(assignedDomains), displayName, certID, route.PagesDeployment, limitConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, doc.OpenRestyConfig))
}
if normalizeRouteUpstreamType(route.UpstreamType) == routeUpstreamTypePages {
if err := renderPagesRoute(&builder, route, displayName, serverNames, certificates, limitConfig, powEnabled, doc.OpenRestyConfig); err != nil {
return "", err
}
continue
}
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))
}
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))
}
if err := renderProxyRoute(&builder, route, displayName, serverNames, certificates, cacheConfig, limitConfig, powEnabled, doc.OpenRestyConfig); err != nil {
return "", err
}
}
return builder.String(), nil
}
// RenderPoWConfig serialises the Proof-of-Work configuration for all enabled
// routes as a JSON string consumed by the OpenResty Lua runtime.
func RenderPoWConfig(doc Document) (string, error) {
type domainEntry struct {
Domains []string `json:"domains"`
@@ -218,6 +135,8 @@ func RenderPoWConfig(doc Document) (string, error) {
return string(data), err
}
// RenderWAFConfig serialises the WAF runtime configuration (rule groups and
// per-site bindings) as a JSON string consumed by the OpenResty Lua runtime.
func RenderWAFConfig(snapshot WAFDocument) (string, error) {
type wafRuntimeRuleGroup struct {
ID uint `json:"id"`
@@ -312,6 +231,9 @@ func sortedUniqueUintIDs(values []uint) []uint {
return items
}
// ChecksumBundle returns a stable SHA-256 hex digest over the combined content
// of the main config, route config, and deduplicated support files, excluding
// the source config JSON file itself.
func ChecksumBundle(mainConfig string, routeConfig string, supportFiles []SupportFile) string {
var builder strings.Builder
builder.WriteString(mainConfig)
@@ -333,6 +255,8 @@ func ChecksumBundle(mainConfig string, routeConfig string, supportFiles []Suppor
return hex.EncodeToString(sum[:])
}
// DedupeSupportFiles returns a new slice with duplicate paths removed, keeping
// the last occurrence of each path.
func DedupeSupportFiles(files []SupportFile) []SupportFile {
if len(files) == 0 {
return nil
@@ -437,21 +361,22 @@ func renderPagesAPIProxyLocationBlock(deployment *PagesDeployment) string {
cleanPath := strings.TrimSuffix(path, "/")
var builder strings.Builder
builder.WriteString(fmt.Sprintf("\n location %s {\n", cleanPath))
// 使用 fmt.Fprintf 替代 WriteString(fmt.Sprintf(...))(QF1012)
fmt.Fprintf(&builder, "\n location %s {\n", cleanPath)
if rewrite != "" {
if !strings.HasPrefix(rewrite, "/") {
rewrite = "/" + rewrite
}
cleanRewrite := strings.TrimSuffix(rewrite, "/")
if cleanRewrite == "" {
builder.WriteString(fmt.Sprintf(" rewrite ^%s/(.*)$ /$1 break;\n", regexp.QuoteMeta(cleanPath)))
builder.WriteString(fmt.Sprintf(" rewrite ^%s$ / break;\n", regexp.QuoteMeta(cleanPath)))
fmt.Fprintf(&builder, " rewrite ^%s/(.*)$ /$1 break;\n", regexp.QuoteMeta(cleanPath))
fmt.Fprintf(&builder, " rewrite ^%s$ / break;\n", regexp.QuoteMeta(cleanPath))
} else {
builder.WriteString(fmt.Sprintf(" rewrite ^%s/(.*)$ %s/$1 break;\n", regexp.QuoteMeta(cleanPath), cleanRewrite))
builder.WriteString(fmt.Sprintf(" rewrite ^%s$ %s break;\n", regexp.QuoteMeta(cleanPath), cleanRewrite))
fmt.Fprintf(&builder, " rewrite ^%s/(.*)$ %s/$1 break;\n", regexp.QuoteMeta(cleanPath), cleanRewrite)
fmt.Fprintf(&builder, " rewrite ^%s$ %s break;\n", regexp.QuoteMeta(cleanPath), cleanRewrite)
}
}
builder.WriteString(fmt.Sprintf(" proxy_pass %s;\n", pass))
fmt.Fprintf(&builder, " proxy_pass %s;\n", pass)
builder.WriteString(" proxy_http_version 1.1;\n")
builder.WriteString(" proxy_set_header Host $http_host;\n")
builder.WriteString(" proxy_set_header X-Real-IP $remote_addr;\n")
@@ -499,7 +424,7 @@ func renderPagesLocationBlock(deployment *PagesDeployment, limitConfig routeLimi
var builder strings.Builder
builder.WriteString(renderRouteLimitBlock(limitConfig))
if deployment != nil && deployment.SPAFallbackEnabled {
builder.WriteString(fmt.Sprintf(" try_files $uri $uri/ %s;\n", pagesFallbackPath(deployment)))
fmt.Fprintf(&builder, " try_files $uri $uri/ %s;\n", pagesFallbackPath(deployment))
} else {
builder.WriteString(" try_files $uri $uri/ =404;\n")
}
@@ -522,18 +447,18 @@ func pagesEntryFile(deployment *PagesDeployment) string {
func pagesFallbackPath(deployment *PagesDeployment) string {
if deployment == nil || strings.TrimSpace(deployment.SPAFallbackPath) == "" {
return "/index.html"
return indexHTML
}
value := filepathToNginxPath(strings.TrimSpace(deployment.SPAFallbackPath))
if !strings.HasPrefix(value, "/") {
value = "/" + value
}
if value == "/" || strings.HasSuffix(value, "/") || strings.Contains(value, "\\") || strings.ContainsAny(value, "\"';") || strings.ContainsAny(value, " \t\r\n") {
return "/index.html"
return indexHTML
}
for _, segment := range strings.Split(value, "/") {
if segment == "." || segment == ".." {
return "/index.html"
return indexHTML
}
}
cleaned := path.Clean(value)
@@ -546,13 +471,13 @@ func pagesFallbackPath(deployment *PagesDeployment) string {
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)))
fmt.Fprintf(&builder, " 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)))
fmt.Fprintf(&builder, " 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")
@@ -566,7 +491,7 @@ func renderProxyHeaderBlock(originURL string, originHost string, customHeaders [
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)))
fmt.Fprintf(&builder, " proxy_set_header %s %s;\n", header.Key, quoteNginxStringLiteral(header.Value))
}
return builder.String()
}
@@ -635,13 +560,13 @@ func renderRouteCacheBlock(cacheConfig routeCacheConfig, cfg ConfigSnapshot) str
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))
fmt.Fprintf(&builder, " 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))
fmt.Fprintf(&builder, " 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))
fmt.Fprintf(&builder, " limit_rate %s;\n", limitConfig.LimitRate)
}
return builder.String()
}
@@ -700,8 +625,8 @@ func buildRouteUpstreamConfig(route Route, upstreams []string) routeUpstreamConf
func normalizeRouteUpstreamType(raw string) string {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "pages":
return "pages"
case routeUpstreamTypePages:
return routeUpstreamTypePages
default:
return "direct"
}
@@ -709,9 +634,9 @@ func normalizeRouteUpstreamType(raw string) string {
func renderNamedUpstreamBlock(upstreamConfig routeUpstreamConfig) string {
var builder strings.Builder
builder.WriteString(fmt.Sprintf("upstream %s {\n", upstreamConfig.Name))
fmt.Fprintf(&builder, "upstream %s {\n", upstreamConfig.Name)
for _, server := range upstreamConfig.Servers {
builder.WriteString(fmt.Sprintf(" server %s max_fails=3 fail_timeout=10s;\n", server))
fmt.Fprintf(&builder, " server %s max_fails=3 fail_timeout=10s;\n", server)
}
builder.WriteString(" keepalive 128;\n}\n\n")
return builder.String()
+151
View File
@@ -0,0 +1,151 @@
package openresty
import (
"fmt"
"strings"
)
type routeCertPartition struct {
httpOnlyDomains []string
domainsByCertID map[uint][]string
}
func partitionRouteDomainsByCert(domains []string, certIDs, domainCertIDs []uint) routeCertPartition {
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)
}
return routeCertPartition{
httpOnlyDomains: httpOnlyDomains,
domainsByCertID: domainsByCertID,
}
}
func validateRouteCertificates(route Route, displayName string, certIDs []uint, partition routeCertPartition, certificates map[uint]string) error {
for _, certID := range certIDs {
assignedDomains := partition.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)
}
}
return nil
}
func renderPagesRouteHTTPS(
builder *strings.Builder,
serverNames, displayName string,
route Route,
partition routeCertPartition,
certIDs []uint,
limitConfig routeLimitConfig,
powEnabled bool,
cfg ConfigSnapshot,
) {
if route.RedirectHTTP {
if len(partition.httpOnlyDomains) > 0 {
builder.WriteString(renderHTTPPagesServer(renderServerNames(partition.httpOnlyDomains), displayName, route.PagesDeployment, limitConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword))
}
for _, certID := range certIDs {
if assignedDomains := partition.domainsByCertID[certID]; len(assignedDomains) > 0 {
builder.WriteString(renderHTTPRedirectServer(renderServerNames(assignedDomains)))
}
}
} else {
builder.WriteString(renderHTTPPagesServer(serverNames, displayName, route.PagesDeployment, limitConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword))
}
for _, certID := range certIDs {
if assignedDomains := partition.domainsByCertID[certID]; len(assignedDomains) > 0 {
builder.WriteString(renderHTTPSPagesServer(renderServerNames(assignedDomains), displayName, certID, route.PagesDeployment, limitConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, cfg))
}
}
}
func renderProxyRouteHTTPS(
builder *strings.Builder,
serverNames, displayName string,
route Route,
partition routeCertPartition,
certIDs []uint,
cacheConfig routeCacheConfig,
limitConfig routeLimitConfig,
upstreamConfig routeUpstreamConfig,
powEnabled bool,
cfg ConfigSnapshot,
) {
if route.RedirectHTTP {
if len(partition.httpOnlyDomains) > 0 {
builder.WriteString(renderHTTPProxyServer(renderServerNames(partition.httpOnlyDomains), displayName, route.OriginURL, route.OriginHost, route.CustomHeaders, cacheConfig, limitConfig, upstreamConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, cfg))
}
for _, certID := range certIDs {
if assignedDomains := partition.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, cfg))
}
for _, certID := range certIDs {
if assignedDomains := partition.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, cfg))
}
}
}
func renderPagesRoute(builder *strings.Builder, route Route, displayName, serverNames string, certificates map[uint]string, limitConfig routeLimitConfig, powEnabled bool, cfg ConfigSnapshot) error {
if route.PagesDeployment == nil {
return fmt.Errorf("route %s pages deployment is missing", route.Domain)
}
if !route.EnableHTTPS {
builder.WriteString(renderHTTPPagesServer(serverNames, displayName, route.PagesDeployment, limitConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword))
return nil
}
certIDs := normalizeCertIDs(route.CertID, route.CertIDs)
domainCertIDs := normalizeDomainCertIDs(normalizedRouteDomains(route), certIDs, route.DomainCertIDs)
if len(certIDs) == 0 {
return fmt.Errorf("路由 %s 未配置证书", route.Domain)
}
partition := partitionRouteDomainsByCert(normalizedRouteDomains(route), certIDs, domainCertIDs)
if err := validateRouteCertificates(route, displayName, certIDs, partition, certificates); err != nil {
return err
}
renderPagesRouteHTTPS(builder, serverNames, displayName, route, partition, certIDs, limitConfig, powEnabled, cfg)
return nil
}
func renderProxyRoute(builder *strings.Builder, route Route, displayName, serverNames string, certificates map[uint]string, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, powEnabled bool, cfg ConfigSnapshot) error {
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))
}
if !route.EnableHTTPS {
builder.WriteString(renderHTTPProxyServer(serverNames, displayName, route.OriginURL, route.OriginHost, route.CustomHeaders, cacheConfig, limitConfig, upstreamConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, cfg))
return nil
}
certIDs := normalizeCertIDs(route.CertID, route.CertIDs)
domainCertIDs := normalizeDomainCertIDs(normalizedRouteDomains(route), certIDs, route.DomainCertIDs)
if len(certIDs) == 0 {
return fmt.Errorf("路由 %s 未配置证书", route.Domain)
}
partition := partitionRouteDomainsByCert(normalizedRouteDomains(route), certIDs, domainCertIDs)
if err := validateRouteCertificates(route, displayName, certIDs, partition, certificates); err != nil {
return err
}
renderProxyRouteHTTPS(builder, serverNames, displayName, route, partition, certIDs, cacheConfig, limitConfig, upstreamConfig, powEnabled, cfg)
return nil
}
+28
View File
@@ -1,5 +1,7 @@
package openresty
// Placeholder constants used as sentinel values in rendered OpenResty config
// files; the deploy process replaces them with real paths before reload.
const (
CertDirPlaceholder = "__OPENFLARE_CERT_DIR__"
RouteConfigPlaceholder = "__OPENFLARE_ROUTE_CONFIG__"
@@ -63,16 +65,22 @@ http {
}
`
// SupportFile represents an auxiliary file (certificate, WAF config, etc.)
// that is written alongside the main OpenResty configuration.
type SupportFile struct {
Path string `json:"path"`
Content string `json:"content"`
}
// CustomHeader is a key/value pair injected as an additional proxy_set_header
// directive for a specific route.
type CustomHeader struct {
Key string `json:"key"`
Value string `json:"value"`
}
// PoWListConfig holds the IP, CIDR, path, and user-agent lists used by the
// Proof-of-Work whitelist or blacklist filter.
type PoWListConfig struct {
IPs []string `json:"ips"`
IPCidrs []string `json:"ip_cidrs"`
@@ -81,6 +89,8 @@ type PoWListConfig struct {
UserAgents []string `json:"user_agents"`
}
// PoWConfig holds the full Proof-of-Work challenge parameters for a route,
// including difficulty, algorithm, TTLs, and allow/block lists.
type PoWConfig struct {
Difficulty int `json:"difficulty"`
Algorithm string `json:"algorithm"`
@@ -90,6 +100,8 @@ type PoWConfig struct {
Blacklist PoWListConfig `json:"blacklist"`
}
// Route describes a single proxy or pages site entry in the OpenFlare config
// document, including upstream, TLS, caching, rate-limiting and WAF settings.
type Route struct {
ID uint `json:"id,omitempty"`
SiteName string `json:"site_name,omitempty"`
@@ -121,6 +133,8 @@ type Route struct {
PagesDeployment *PagesDeployment `json:"pages_deployment,omitempty"`
}
// PagesDeployment holds the static-site deployment parameters for a Pages-type
// route, including local root, entry file, SPA fallback, and API proxy options.
type PagesDeployment struct {
ProjectID uint `json:"project_id"`
ProjectSlug string `json:"project_slug"`
@@ -137,6 +151,8 @@ type PagesDeployment struct {
LocalRoot string `json:"local_root"`
}
// WAFRuleGroup defines a WAF rule group with IP/country/region lists, PoW
// integration, and per-group block status configuration.
type WAFRuleGroup struct {
ID uint `json:"id"`
Name string `json:"name"`
@@ -156,6 +172,8 @@ type WAFRuleGroup struct {
PoWConfig *PoWConfig `json:"pow_config,omitempty"`
}
// WAFIPGroup is a named, reusable list of IP addresses or CIDRs that can be
// referenced by multiple WAF rule groups as a whitelist or blacklist.
type WAFIPGroup struct {
ID uint `json:"id"`
Name string `json:"name"`
@@ -164,18 +182,24 @@ type WAFIPGroup struct {
IPList []string `json:"ip_list,omitempty"`
}
// WAFBinding associates a route (by site name) with the WAF rule groups that
// should be enforced for that site.
type WAFBinding struct {
RouteID uint `json:"route_id"`
SiteName string `json:"site_name"`
RuleGroupIDs []uint `json:"rule_group_ids"`
}
// WAFDocument is the top-level WAF configuration snapshot containing rule
// groups, IP groups, and per-site bindings.
type WAFDocument struct {
RuleGroups []WAFRuleGroup `json:"rule_groups"`
IPGroups []WAFIPGroup `json:"ip_groups,omitempty"`
Bindings []WAFBinding `json:"bindings"`
}
// ConfigSnapshot holds the full set of OpenResty tuning parameters that are
// rendered into the nginx main configuration template.
type ConfigSnapshot struct {
DefaultServerReturnStatus int `json:"default_server_return_status"`
WorkerProcesses string `json:"worker_processes"`
@@ -216,12 +240,16 @@ type ConfigSnapshot struct {
MainConfigTemplate string `json:"main_config_template,omitempty"`
}
// Document is the top-level input structure for the OpenResty renderer,
// combining routes, OpenResty tuning, and WAF configuration.
type Document struct {
Routes []Route `json:"routes"`
OpenRestyConfig ConfigSnapshot `json:"openresty_config"`
WAF WAFDocument `json:"waf"`
}
// Result is the output produced by Render, containing the rendered main
// config, route config, support files, and a content checksum.
type Result struct {
MainConfig string
RouteConfig string