mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-09 00:56:37 +08:00
refactor(config): render routes from zone domains
This commit is contained in:
@@ -26,6 +26,10 @@ sidebar: false
|
|||||||
- 新增第一阶段 Zone 与正规化 Zone 域名数据库表及路由绑定模型,为后续以稳定 ID 管理网站与域名关联提供基础。
|
- 新增第一阶段 Zone 与正规化 Zone 域名数据库表及路由绑定模型,为后续以稳定 ID 管理网站与域名关联提供基础。
|
||||||
- 新增 Zone 管理 API 与显式历史域名导入命令,使用公共后缀列表验证注册根域和域名归属。
|
- 新增 Zone 管理 API 与显式历史域名导入命令,使用公共后缀列表验证注册根域和域名归属。
|
||||||
|
|
||||||
|
### 修改
|
||||||
|
|
||||||
|
- 配置快照、OpenResty 渲染、Tunnel 与 Uptime Kuma 监控改为从 Zone 域名绑定读取域名和证书,移除对反代路由旧域名/证书字段的运行时回退。
|
||||||
|
|
||||||
### 修复
|
### 修复
|
||||||
|
|
||||||
- Docker ClickHouse 性能配置改为单文件挂载,避免覆盖镜像内置的 Docker 网络监听配置,导致宿主机无法通过 8123/9000 访问服务。
|
- Docker ClickHouse 性能配置改为单文件挂载,避免覆盖镜像内置的 Docker 网络监听配置,导致宿主机无法通过 8123/9000 访问服务。
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func TestBuildCertificateSupportFilesDecryptsSealedPrivateKey(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
files, err := buildCertificateSupportFiles(ctx, []snapshotRoute{
|
files, err := buildCertificateSupportFiles(ctx, []snapshotRoute{
|
||||||
{CertIDs: []uint{certificate.ID}},
|
{DomainCertIDs: []uint{certificate.ID}},
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, files, 2)
|
require.Len(t, files, 2)
|
||||||
@@ -58,13 +58,51 @@ func TestBuildCertificateSupportFilesDecryptsSealedPrivateKey(t *testing.T) {
|
|||||||
assert.Equal(t, normalizePEM(strings.TrimSpace(keyPEM)), keyContent)
|
assert.Equal(t, normalizePEM(strings.TrimSpace(keyPEM)), keyContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildSnapshotReadsZoneDomainCertificates(t *testing.T) {
|
||||||
|
cleanup := setupConfigVersionTestDB(t)
|
||||||
|
defer cleanup()
|
||||||
|
ctx := context.Background()
|
||||||
|
require.NoError(t, db.DB(ctx).AutoMigrate(&model.TLSCertificate{}))
|
||||||
|
|
||||||
|
oldSecret := config.Config.App.SessionSecret
|
||||||
|
config.Config.App.SessionSecret = "test-session-secret-for-zone-domain-snapshots"
|
||||||
|
t.Cleanup(func() { config.Config.App.SessionSecret = oldSecret })
|
||||||
|
|
||||||
|
firstCertPEM, firstKeyPEM := generateTestCertKeyPairForSnapshotForDomain(t, "one.example.com")
|
||||||
|
first, err := oftls.CreateCertificate(ctx, oftls.CertificateInput{Name: "first", CertPEM: firstCertPEM, KeyPEM: firstKeyPEM})
|
||||||
|
require.NoError(t, err)
|
||||||
|
secondCertPEM, secondKeyPEM := generateTestCertKeyPairForSnapshotForDomain(t, "two.example.com")
|
||||||
|
second, err := oftls.CreateCertificate(ctx, oftls.CertificateInput{Name: "second", CertPEM: secondCertPEM, KeyPEM: secondKeyPEM})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
route := &model.ProxyRoute{SiteName: "tls-site", Domain: "legacy.invalid", OriginURL: "http://origin:8080", Upstreams: `["http://origin:8080"]`, Enabled: true, EnableHTTPS: true}
|
||||||
|
require.NoError(t, model.CreateProxyRouteRecord(ctx, route))
|
||||||
|
zone := &model.Zone{Domain: "example.com"}
|
||||||
|
require.NoError(t, db.DB(ctx).Create(zone).Error)
|
||||||
|
require.NoError(t, db.DB(ctx).Create(&model.ZoneDomain{ZoneID: zone.ID, ProxyRouteID: &route.ID, Domain: "one.example.com", CertID: &first.ID}).Error)
|
||||||
|
require.NoError(t, db.DB(ctx).Create(&model.ZoneDomain{ZoneID: zone.ID, ProxyRouteID: &route.ID, Domain: "two.example.com", CertID: &second.ID}).Error)
|
||||||
|
|
||||||
|
bundle, err := buildCurrentConfigBundle(ctx, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, bundle.SnapshotRoutes, 1)
|
||||||
|
assert.Equal(t, []string{"one.example.com", "two.example.com"}, bundle.SnapshotRoutes[0].Domains)
|
||||||
|
assert.Equal(t, []uint{first.ID, second.ID}, bundle.SnapshotRoutes[0].DomainCertIDs)
|
||||||
|
assert.Contains(t, bundle.RouteConfig, "server_name one.example.com;")
|
||||||
|
assert.Contains(t, bundle.RouteConfig, "server_name two.example.com;")
|
||||||
|
}
|
||||||
|
|
||||||
func generateTestCertKeyPairForSnapshot(t *testing.T) (certPEM string, keyPEM string) {
|
func generateTestCertKeyPairForSnapshot(t *testing.T) (certPEM string, keyPEM string) {
|
||||||
|
return generateTestCertKeyPairForSnapshotForDomain(t, "test.example.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTestCertKeyPairForSnapshotForDomain(t *testing.T, domain string) (certPEM string, keyPEM string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
template := x509.Certificate{
|
template := x509.Certificate{
|
||||||
SerialNumber: big.NewInt(1),
|
SerialNumber: big.NewInt(1),
|
||||||
Subject: pkix.Name{CommonName: "test.example.com"},
|
Subject: pkix.Name{CommonName: domain},
|
||||||
|
DNSNames: []string{domain},
|
||||||
NotBefore: time.Now().Add(-time.Hour),
|
NotBefore: time.Now().Add(-time.Hour),
|
||||||
NotAfter: time.Now().Add(24 * time.Hour),
|
NotAfter: time.Now().Add(24 * time.Hour),
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||||
|
|||||||
@@ -19,6 +19,26 @@ type customHeaderInput struct {
|
|||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeSnapshotDomains(domains []string) ([]string, error) {
|
||||||
|
normalized := make([]string, 0, len(domains))
|
||||||
|
seen := make(map[string]struct{}, len(domains))
|
||||||
|
for _, raw := range domains {
|
||||||
|
domain := strings.ToLower(strings.TrimSpace(raw))
|
||||||
|
if domain == "" || strings.Contains(domain, "://") || strings.Contains(domain, "/") {
|
||||||
|
return nil, fmt.Errorf("domains payload is invalid")
|
||||||
|
}
|
||||||
|
if _, ok := seen[domain]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[domain] = struct{}{}
|
||||||
|
normalized = append(normalized, domain)
|
||||||
|
}
|
||||||
|
if len(normalized) == 0 {
|
||||||
|
return nil, fmt.Errorf("domain is required")
|
||||||
|
}
|
||||||
|
return normalized, nil
|
||||||
|
}
|
||||||
|
|
||||||
func isUniqueConstraintError(err error) bool {
|
func isUniqueConstraintError(err error) bool {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return false
|
return false
|
||||||
@@ -94,92 +114,6 @@ func decodeStoredCacheRules(raw string) ([]string, error) {
|
|||||||
return normalized, nil
|
return normalized, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeStoredCertIDs(raw string, fallbackCertID *uint) ([]uint, error) {
|
|
||||||
text := strings.TrimSpace(raw)
|
|
||||||
if text == "" {
|
|
||||||
if fallbackCertID == nil || *fallbackCertID == 0 {
|
|
||||||
return []uint{}, nil
|
|
||||||
}
|
|
||||||
return []uint{*fallbackCertID}, nil
|
|
||||||
}
|
|
||||||
var certIDs []uint
|
|
||||||
if err := json.Unmarshal([]byte(text), &certIDs); err != nil {
|
|
||||||
return nil, fmt.Errorf("cert_ids payload is invalid")
|
|
||||||
}
|
|
||||||
normalized := make([]uint, 0, len(certIDs))
|
|
||||||
seen := make(map[uint]struct{}, len(certIDs))
|
|
||||||
for _, certID := range certIDs {
|
|
||||||
if certID == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := seen[certID]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[certID] = struct{}{}
|
|
||||||
normalized = append(normalized, certID)
|
|
||||||
}
|
|
||||||
if len(normalized) == 0 && fallbackCertID != nil && *fallbackCertID != 0 {
|
|
||||||
return []uint{*fallbackCertID}, nil
|
|
||||||
}
|
|
||||||
return normalized, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveDomainCertIDs(domains []string, certIDs []uint, rawDomainCertIDs string) ([]uint, error) {
|
|
||||||
text := strings.TrimSpace(rawDomainCertIDs)
|
|
||||||
if text != "" {
|
|
||||||
var domainCertIDs []uint
|
|
||||||
if err := json.Unmarshal([]byte(text), &domainCertIDs); err != nil {
|
|
||||||
return nil, fmt.Errorf("domain_cert_ids payload is invalid")
|
|
||||||
}
|
|
||||||
if len(domains) > 0 && len(domainCertIDs) != len(domains) {
|
|
||||||
return nil, fmt.Errorf("domain_cert_ids length is invalid")
|
|
||||||
}
|
|
||||||
return domainCertIDs, nil
|
|
||||||
}
|
|
||||||
if len(certIDs) == 0 {
|
|
||||||
return []uint{}, nil
|
|
||||||
}
|
|
||||||
if len(certIDs) == 1 {
|
|
||||||
result := make([]uint, len(domains))
|
|
||||||
for index := range result {
|
|
||||||
result[index] = certIDs[0]
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
if len(certIDs) == len(domains) {
|
|
||||||
result := make([]uint, len(certIDs))
|
|
||||||
copy(result, certIDs)
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
return []uint{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustDecodeCertIDs(route *model.ProxyRoute) []uint {
|
|
||||||
if route == nil {
|
|
||||||
return []uint{}
|
|
||||||
}
|
|
||||||
certIDs, err := decodeStoredCertIDs(route.CertIDs, route.CertID)
|
|
||||||
if err != nil {
|
|
||||||
return []uint{}
|
|
||||||
}
|
|
||||||
return certIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustDecodeDomainCertIDs(route *model.ProxyRoute, domains []string) []uint {
|
|
||||||
if route == nil {
|
|
||||||
return []uint{}
|
|
||||||
}
|
|
||||||
certIDs, err := decodeStoredCertIDs(route.CertIDs, route.CertID)
|
|
||||||
if err != nil {
|
|
||||||
return []uint{}
|
|
||||||
}
|
|
||||||
domainCertIDs, err := resolveDomainCertIDs(domains, certIDs, route.DomainCertIDs)
|
|
||||||
if err != nil {
|
|
||||||
return []uint{}
|
|
||||||
}
|
|
||||||
return domainCertIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeUpstreamType(raw string) string {
|
func normalizeUpstreamType(raw string) string {
|
||||||
value := strings.ToLower(strings.TrimSpace(raw))
|
value := strings.ToLower(strings.TrimSpace(raw))
|
||||||
switch value {
|
switch value {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/websocket"
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/websocket"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
pkgprotocol "github.com/Rain-kl/Wavelet/pkg/protocol"
|
pkgprotocol "github.com/Rain-kl/Wavelet/pkg/protocol"
|
||||||
@@ -333,23 +332,10 @@ func normalizeSnapshotRoutes(routes []snapshotRoute) []snapshotRoute {
|
|||||||
return []snapshotRoute{}
|
return []snapshotRoute{}
|
||||||
}
|
}
|
||||||
for index := range routes {
|
for index := range routes {
|
||||||
normalizedDomains, err := routeidentity.DecodeDomains("", routes[index].Domain)
|
normalizedDomains, err := normalizeSnapshotDomains(routes[index].Domains)
|
||||||
if len(routes[index].Domains) > 0 {
|
|
||||||
normalizedDomains, err = routeidentity.NormalizeDomains(routes[index].Domains)
|
|
||||||
}
|
|
||||||
if err == nil && len(normalizedDomains) > 0 {
|
if err == nil && len(normalizedDomains) > 0 {
|
||||||
routes[index].Domains = normalizedDomains
|
routes[index].Domains = normalizedDomains
|
||||||
routes[index].Domain = normalizedDomains[0]
|
routes[index].SiteName = strings.TrimSpace(routes[index].SiteName)
|
||||||
routes[index].SiteName = routeidentity.ResolveSiteName(nil, routes[index].SiteName, normalizedDomains[0])
|
|
||||||
}
|
|
||||||
normalizedCertIDs, primaryCertID, certErr := normalizeSnapshotCertificateIDs(routes[index].CertID, routes[index].CertIDs)
|
|
||||||
if certErr == nil {
|
|
||||||
routes[index].CertID = primaryCertID
|
|
||||||
routes[index].CertIDs = normalizedCertIDs
|
|
||||||
}
|
|
||||||
normalizedDomainCertIDs, domainCertErr := resolveDomainCertIDs(routes[index].Domains, routes[index].CertIDs, "")
|
|
||||||
if domainCertErr == nil && len(routes[index].DomainCertIDs) == 0 {
|
|
||||||
routes[index].DomainCertIDs = normalizedDomainCertIDs
|
|
||||||
}
|
}
|
||||||
normalizedUpstreams, upstreamErr := normalizeUpstreams(routes[index].OriginURL, routes[index].Upstreams)
|
normalizedUpstreams, upstreamErr := normalizeUpstreams(routes[index].OriginURL, routes[index].Upstreams)
|
||||||
if upstreamErr == nil {
|
if upstreamErr == nil {
|
||||||
@@ -378,7 +364,6 @@ func flattenSnapshotRoutesByDomain(routes []snapshotRoute) map[string]snapshotRo
|
|||||||
for _, route := range normalizeSnapshotRoutes(routes) {
|
for _, route := range normalizeSnapshotRoutes(routes) {
|
||||||
for _, domain := range route.Domains {
|
for _, domain := range route.Domains {
|
||||||
item := route
|
item := route
|
||||||
item.Domain = domain
|
|
||||||
domainMap[domain] = item
|
domainMap[domain] = item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -398,12 +383,11 @@ func snapshotRouteScalarsEqual(left, right snapshotRoute) bool {
|
|||||||
snapshotRouteOriginEqual(left, right) &&
|
snapshotRouteOriginEqual(left, right) &&
|
||||||
snapshotRoutePolicyEqual(left, right) &&
|
snapshotRoutePolicyEqual(left, right) &&
|
||||||
snapshotRouteTunnelEqual(left, right) &&
|
snapshotRouteTunnelEqual(left, right) &&
|
||||||
uintSliceEqual(left.CertIDs, right.CertIDs) &&
|
|
||||||
uintSliceEqual(left.DomainCertIDs, right.DomainCertIDs)
|
uintSliceEqual(left.DomainCertIDs, right.DomainCertIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func snapshotRouteIdentityEqual(left, right snapshotRoute) bool {
|
func snapshotRouteIdentityEqual(left, right snapshotRoute) bool {
|
||||||
return left.SiteName == right.SiteName && left.Domain == right.Domain
|
return left.SiteName == right.SiteName
|
||||||
}
|
}
|
||||||
|
|
||||||
func snapshotRouteOriginEqual(left, right snapshotRoute) bool {
|
func snapshotRouteOriginEqual(left, right snapshotRoute) bool {
|
||||||
@@ -463,31 +447,6 @@ func snapshotWAFConfigEqual(left snapshotWAFDocument, right snapshotWAFDocument)
|
|||||||
return string(leftJSON) == string(rightJSON)
|
return string(leftJSON) == string(rightJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeSnapshotCertificateIDs(primaryCertID *uint, certIDs []uint) ([]uint, *uint, error) {
|
|
||||||
candidates := make([]uint, 0, len(certIDs)+1)
|
|
||||||
if primaryCertID != nil && *primaryCertID != 0 {
|
|
||||||
candidates = append(candidates, *primaryCertID)
|
|
||||||
}
|
|
||||||
candidates = append(candidates, certIDs...)
|
|
||||||
normalized := make([]uint, 0, len(candidates))
|
|
||||||
seen := make(map[uint]struct{}, len(candidates))
|
|
||||||
for _, certID := range candidates {
|
|
||||||
if certID == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := seen[certID]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[certID] = struct{}{}
|
|
||||||
normalized = append(normalized, certID)
|
|
||||||
}
|
|
||||||
var normalizedPrimary *uint
|
|
||||||
if len(normalized) > 0 {
|
|
||||||
normalizedPrimary = &normalized[0]
|
|
||||||
}
|
|
||||||
return normalized, normalizedPrimary, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildInitialOpenRestyOptionDiffs(current openRestyConfigSnapshot) []ConfigOptionDiffItem {
|
func buildInitialOpenRestyOptionDiffs(current openRestyConfigSnapshot) []ConfigOptionDiffItem {
|
||||||
details := diffOpenRestyOptionDetails(openRestyConfigSnapshot{}, current)
|
details := diffOpenRestyOptionDetails(openRestyConfigSnapshot{}, current)
|
||||||
for index := range details {
|
for index := range details {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package config_version
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -27,6 +28,8 @@ func setupConfigVersionTestDB(t *testing.T) func() {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, sqliteDB.AutoMigrate(
|
require.NoError(t, sqliteDB.AutoMigrate(
|
||||||
&model.ProxyRoute{},
|
&model.ProxyRoute{},
|
||||||
|
&model.Zone{},
|
||||||
|
&model.ZoneDomain{},
|
||||||
&model.ConfigVersion{},
|
&model.ConfigVersion{},
|
||||||
&model.OpenFlareWAFRuleGroup{},
|
&model.OpenFlareWAFRuleGroup{},
|
||||||
&model.OpenFlareWAFRuleGroupBinding{},
|
&model.OpenFlareWAFRuleGroupBinding{},
|
||||||
@@ -40,6 +43,19 @@ func setupConfigVersionTestDB(t *testing.T) func() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createSnapshotZoneDomains(t *testing.T, ctx context.Context, route *model.ProxyRoute, domains ...string) {
|
||||||
|
t.Helper()
|
||||||
|
zone := &model.Zone{Domain: fmt.Sprintf("zone-%d.example", route.ID)}
|
||||||
|
require.NoError(t, db.DB(ctx).Create(zone).Error)
|
||||||
|
for _, domain := range domains {
|
||||||
|
require.NoError(t, db.DB(ctx).Create(&model.ZoneDomain{
|
||||||
|
ZoneID: zone.ID,
|
||||||
|
ProxyRouteID: &route.ID,
|
||||||
|
Domain: domain,
|
||||||
|
}).Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestListConfigVersionsOrdersByCreatedAtDesc(t *testing.T) {
|
func TestListConfigVersionsOrdersByCreatedAtDesc(t *testing.T) {
|
||||||
cleanup := setupConfigVersionTestDB(t)
|
cleanup := setupConfigVersionTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
@@ -87,6 +103,7 @@ func TestPublishConfigVersionCreatesVersion(t *testing.T) {
|
|||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
require.NoError(t, model.CreateProxyRouteRecord(ctx, route))
|
require.NoError(t, model.CreateProxyRouteRecord(ctx, route))
|
||||||
|
createSnapshotZoneDomains(t, ctx, route, "publish.example.com")
|
||||||
|
|
||||||
version, err := PublishConfigVersion(ctx, "tester", false)
|
version, err := PublishConfigVersion(ctx, "tester", false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -103,7 +120,7 @@ func TestPublishConfigVersionCreatesVersion(t *testing.T) {
|
|||||||
require.NoError(t, json.Unmarshal([]byte(version.SnapshotJSON), &snapshot))
|
require.NoError(t, json.Unmarshal([]byte(version.SnapshotJSON), &snapshot))
|
||||||
require.Len(t, snapshot.Routes, 1)
|
require.Len(t, snapshot.Routes, 1)
|
||||||
assert.Equal(t, "publish-site", snapshot.Routes[0].SiteName)
|
assert.Equal(t, "publish-site", snapshot.Routes[0].SiteName)
|
||||||
assert.Equal(t, "publish.example.com", snapshot.Routes[0].Domain)
|
assert.Equal(t, []string{"publish.example.com"}, snapshot.Routes[0].Domains)
|
||||||
|
|
||||||
active, err := GetActiveConfigVersion(ctx)
|
active, err := GetActiveConfigVersion(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -124,6 +141,7 @@ func TestBuildSnapshotWAFDocumentUsesNormalizedSiteNames(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
route := &model.ProxyRoute{
|
route := &model.ProxyRoute{
|
||||||
|
SiteName: "example.com",
|
||||||
Domain: "Example.COM",
|
Domain: "Example.COM",
|
||||||
Domains: `["example.com","www.example.com"]`,
|
Domains: `["example.com","www.example.com"]`,
|
||||||
OriginURL: "http://origin.example.com:8080",
|
OriginURL: "http://origin.example.com:8080",
|
||||||
@@ -131,6 +149,7 @@ func TestBuildSnapshotWAFDocumentUsesNormalizedSiteNames(t *testing.T) {
|
|||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
require.NoError(t, model.CreateProxyRouteRecord(ctx, route))
|
require.NoError(t, model.CreateProxyRouteRecord(ctx, route))
|
||||||
|
createSnapshotZoneDomains(t, ctx, route, "example.com", "www.example.com")
|
||||||
|
|
||||||
require.NoError(t, waf.EnsureDefaultRuleGroup(ctx))
|
require.NoError(t, waf.EnsureDefaultRuleGroup(ctx))
|
||||||
globalGroup, err := model.GetGlobalOpenFlareWAFRuleGroup(ctx)
|
globalGroup, err := model.GetGlobalOpenFlareWAFRuleGroup(ctx)
|
||||||
@@ -184,6 +203,7 @@ func TestBuildCurrentConfigBundleEnablesGlobalPoWWithoutExplicitBinding(t *testi
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
route := &model.ProxyRoute{
|
route := &model.ProxyRoute{
|
||||||
|
SiteName: "pow-global.example.com",
|
||||||
Domain: "pow-global.example.com",
|
Domain: "pow-global.example.com",
|
||||||
Domains: `["pow-global.example.com"]`,
|
Domains: `["pow-global.example.com"]`,
|
||||||
OriginURL: "http://origin.example.com:8080",
|
OriginURL: "http://origin.example.com:8080",
|
||||||
@@ -191,6 +211,7 @@ func TestBuildCurrentConfigBundleEnablesGlobalPoWWithoutExplicitBinding(t *testi
|
|||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
require.NoError(t, model.CreateProxyRouteRecord(ctx, route))
|
require.NoError(t, model.CreateProxyRouteRecord(ctx, route))
|
||||||
|
createSnapshotZoneDomains(t, ctx, route, "pow-global.example.com")
|
||||||
|
|
||||||
require.NoError(t, waf.EnsureDefaultRuleGroup(ctx))
|
require.NoError(t, waf.EnsureDefaultRuleGroup(ctx))
|
||||||
globalGroup, err := model.GetGlobalOpenFlareWAFRuleGroup(ctx)
|
globalGroup, err := model.GetGlobalOpenFlareWAFRuleGroup(ctx)
|
||||||
|
|||||||
@@ -25,36 +25,36 @@ func buildPagesRouteSnapshot(
|
|||||||
return "", nil, nil, nil, errors.New("pages 路由配置无效")
|
return "", nil, nil, nil, errors.New("pages 路由配置无效")
|
||||||
}
|
}
|
||||||
if !model.HasPagesProjectsTable(ctx) {
|
if !model.HasPagesProjectsTable(ctx) {
|
||||||
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 模块不可用", route.Domain)
|
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 模块不可用", route.SiteName)
|
||||||
}
|
}
|
||||||
if route.PagesProjectID == nil || *route.PagesProjectID == 0 {
|
if route.PagesProjectID == nil || *route.PagesProjectID == 0 {
|
||||||
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: 未绑定 Pages 项目", route.Domain)
|
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: 未绑定 Pages 项目", route.SiteName)
|
||||||
}
|
}
|
||||||
project, err := model.GetPagesProjectByID(ctx, *route.PagesProjectID)
|
project, err := model.GetPagesProjectByID(ctx, *route.PagesProjectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 项目不存在", route.Domain)
|
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 项目不存在", route.SiteName)
|
||||||
}
|
}
|
||||||
return "", nil, nil, nil, err
|
return "", nil, nil, nil, err
|
||||||
}
|
}
|
||||||
if !project.Enabled {
|
if !project.Enabled {
|
||||||
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 项目未启用", route.Domain)
|
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 项目未启用", route.SiteName)
|
||||||
}
|
}
|
||||||
if project.ActiveDeploymentID == nil || *project.ActiveDeploymentID == 0 {
|
if project.ActiveDeploymentID == nil || *project.ActiveDeploymentID == 0 {
|
||||||
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 项目没有激活部署", route.Domain)
|
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 项目没有激活部署", route.SiteName)
|
||||||
}
|
}
|
||||||
activeDeployment, err := model.GetPagesDeploymentByID(ctx, *project.ActiveDeploymentID)
|
activeDeployment, err := model.GetPagesDeploymentByID(ctx, *project.ActiveDeploymentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 激活部署不存在", route.Domain)
|
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 激活部署不存在", route.SiteName)
|
||||||
}
|
}
|
||||||
return "", nil, nil, nil, err
|
return "", nil, nil, nil, err
|
||||||
}
|
}
|
||||||
if activeDeployment.ProjectID != project.ID {
|
if activeDeployment.ProjectID != project.ID {
|
||||||
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 激活部署不匹配", route.Domain)
|
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 激活部署不匹配", route.SiteName)
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(activeDeployment.Checksum) == "" {
|
if strings.TrimSpace(activeDeployment.Checksum) == "" {
|
||||||
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 部署校验和缺失", route.Domain)
|
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 部署校验和缺失", route.SiteName)
|
||||||
}
|
}
|
||||||
|
|
||||||
pagesProjectID = route.PagesProjectID
|
pagesProjectID = route.PagesProjectID
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ func TestBuildSnapshotRoutesPages(t *testing.T) {
|
|||||||
PagesProjectID: &project.ID,
|
PagesProjectID: &project.ID,
|
||||||
}
|
}
|
||||||
require.NoError(t, model.CreateProxyRouteRecord(ctx, route))
|
require.NoError(t, model.CreateProxyRouteRecord(ctx, route))
|
||||||
|
createSnapshotZoneDomains(t, ctx, route, "speedtest.arctel.net")
|
||||||
|
|
||||||
bundle, err := buildCurrentConfigBundle(ctx, true)
|
bundle, err := buildCurrentConfigBundle(ctx, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity"
|
|
||||||
oftls "github.com/Rain-kl/Wavelet/internal/apps/openflare/tls"
|
oftls "github.com/Rain-kl/Wavelet/internal/apps/openflare/tls"
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/waf"
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/waf"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
@@ -40,15 +39,12 @@ const (
|
|||||||
type snapshotRoute struct {
|
type snapshotRoute struct {
|
||||||
ID uint `json:"id,omitempty"`
|
ID uint `json:"id,omitempty"`
|
||||||
SiteName string `json:"site_name,omitempty"`
|
SiteName string `json:"site_name,omitempty"`
|
||||||
Domain string `json:"domain"`
|
|
||||||
Domains []string `json:"domains,omitempty"`
|
Domains []string `json:"domains,omitempty"`
|
||||||
OriginURL string `json:"origin_url"`
|
OriginURL string `json:"origin_url"`
|
||||||
OriginHost string `json:"origin_host,omitempty"`
|
OriginHost string `json:"origin_host,omitempty"`
|
||||||
Upstreams []string `json:"upstreams,omitempty"`
|
Upstreams []string `json:"upstreams,omitempty"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
EnableHTTPS bool `json:"enable_https"`
|
EnableHTTPS bool `json:"enable_https"`
|
||||||
CertID *uint `json:"cert_id,omitempty"`
|
|
||||||
CertIDs []uint `json:"cert_ids,omitempty"`
|
|
||||||
DomainCertIDs []uint `json:"domain_cert_ids,omitempty"`
|
DomainCertIDs []uint `json:"domain_cert_ids,omitempty"`
|
||||||
RedirectHTTP bool `json:"redirect_http"`
|
RedirectHTTP bool `json:"redirect_http"`
|
||||||
LimitConnPerServer int `json:"limit_conn_per_server,omitempty"`
|
LimitConnPerServer int `json:"limit_conn_per_server,omitempty"`
|
||||||
@@ -231,19 +227,32 @@ func buildCurrentConfigBundle(ctx context.Context, requireRoutes bool) (*configB
|
|||||||
func buildSnapshotRoutes(ctx context.Context, routes []*model.ProxyRoute) ([]snapshotRoute, error) {
|
func buildSnapshotRoutes(ctx context.Context, routes []*model.ProxyRoute) ([]snapshotRoute, error) {
|
||||||
items := make([]snapshotRoute, 0, len(routes))
|
items := make([]snapshotRoute, 0, len(routes))
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
domains, err := routeidentity.DecodeDomains(route.Domains, route.Domain)
|
zoneDomains, err := model.ListZoneDomainsByRouteID(ctx, route.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("route %s domains are invalid", route.Domain)
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(zoneDomains) == 0 {
|
||||||
|
return nil, fmt.Errorf("route %s has no zone domains", route.SiteName)
|
||||||
|
}
|
||||||
|
domains := make([]string, 0, len(zoneDomains))
|
||||||
|
domainCertIDs := make([]uint, 0, len(zoneDomains))
|
||||||
|
for _, zoneDomain := range zoneDomains {
|
||||||
|
domains = append(domains, zoneDomain.Domain)
|
||||||
|
if zoneDomain.CertID == nil {
|
||||||
|
domainCertIDs = append(domainCertIDs, 0)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
domainCertIDs = append(domainCertIDs, *zoneDomain.CertID)
|
||||||
}
|
}
|
||||||
customHeaders, err := decodeStoredCustomHeaders(route.CustomHeaders)
|
customHeaders, err := decodeStoredCustomHeaders(route.CustomHeaders)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("路由 %s 自定义请求头无效", route.Domain)
|
return nil, fmt.Errorf("路由 %s 自定义请求头无效", route.SiteName)
|
||||||
}
|
}
|
||||||
upstreamType := normalizeUpstreamType(route.UpstreamType)
|
upstreamType := normalizeUpstreamType(route.UpstreamType)
|
||||||
originURL := route.OriginURL
|
originURL := route.OriginURL
|
||||||
upstreams, err := decodeStoredUpstreams(route.Upstreams, route.OriginURL)
|
upstreams, err := decodeStoredUpstreams(route.Upstreams, route.OriginURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("路由 %s 上游配置无效", route.Domain)
|
return nil, fmt.Errorf("路由 %s 上游配置无效", route.SiteName)
|
||||||
}
|
}
|
||||||
var tunnelNodeID *uint
|
var tunnelNodeID *uint
|
||||||
var tunnelTargetAddr string
|
var tunnelTargetAddr string
|
||||||
@@ -265,21 +274,18 @@ func buildSnapshotRoutes(ctx context.Context, routes []*model.ProxyRoute) ([]sna
|
|||||||
}
|
}
|
||||||
cacheRules, err := decodeStoredCacheRules(route.CacheRules)
|
cacheRules, err := decodeStoredCacheRules(route.CacheRules)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("路由 %s 缓存规则无效", route.Domain)
|
return nil, fmt.Errorf("路由 %s 缓存规则无效", route.SiteName)
|
||||||
}
|
}
|
||||||
items = append(items, snapshotRoute{
|
items = append(items, snapshotRoute{
|
||||||
ID: route.ID,
|
ID: route.ID,
|
||||||
SiteName: routeidentity.ResolveSiteName(route, route.SiteName, domains[0]),
|
SiteName: route.SiteName,
|
||||||
Domain: domains[0],
|
|
||||||
Domains: domains,
|
Domains: domains,
|
||||||
OriginURL: originURL,
|
OriginURL: originURL,
|
||||||
OriginHost: route.OriginHost,
|
OriginHost: route.OriginHost,
|
||||||
Upstreams: upstreams,
|
Upstreams: upstreams,
|
||||||
Enabled: route.Enabled,
|
Enabled: route.Enabled,
|
||||||
EnableHTTPS: route.EnableHTTPS,
|
EnableHTTPS: route.EnableHTTPS,
|
||||||
CertID: route.CertID,
|
DomainCertIDs: domainCertIDs,
|
||||||
CertIDs: mustDecodeCertIDs(route),
|
|
||||||
DomainCertIDs: mustDecodeDomainCertIDs(route, domains),
|
|
||||||
RedirectHTTP: route.RedirectHTTP,
|
RedirectHTTP: route.RedirectHTTP,
|
||||||
LimitConnPerServer: route.LimitConnPerServer,
|
LimitConnPerServer: route.LimitConnPerServer,
|
||||||
LimitConnPerIP: route.LimitConnPerIP,
|
LimitConnPerIP: route.LimitConnPerIP,
|
||||||
@@ -344,11 +350,14 @@ func buildSnapshotWAFDocument(ctx context.Context, routes []*model.ProxyRoute) (
|
|||||||
if route == nil {
|
if route == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
domains, domainErr := routeidentity.DecodeDomains(route.Domains, route.Domain)
|
domains, domainErr := model.ListZoneDomainsByRouteID(ctx, route.ID)
|
||||||
if domainErr != nil {
|
if domainErr != nil {
|
||||||
return snapshotWAFDocument{}, fmt.Errorf("route %s domains are invalid", route.Domain)
|
return snapshotWAFDocument{}, domainErr
|
||||||
}
|
}
|
||||||
enabledRouteSiteNames[route.ID] = routeidentity.ResolveSiteName(route, route.SiteName, domains[0])
|
if len(domains) == 0 {
|
||||||
|
return snapshotWAFDocument{}, fmt.Errorf("route %s has no zone domains", route.SiteName)
|
||||||
|
}
|
||||||
|
enabledRouteSiteNames[route.ID] = route.SiteName
|
||||||
}
|
}
|
||||||
rawBindings, err := model.ListOpenFlareWAFRuleGroupBindings(ctx)
|
rawBindings, err := model.ListOpenFlareWAFRuleGroupBindings(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -552,14 +561,6 @@ func normalizeProxyCachePathForSnapshot(cacheEnabled bool, cachePath string) str
|
|||||||
func buildCertificateSupportFiles(ctx context.Context, routes []snapshotRoute) ([]SupportFile, error) {
|
func buildCertificateSupportFiles(ctx context.Context, routes []snapshotRoute) ([]SupportFile, error) {
|
||||||
certIDSet := make(map[uint]struct{})
|
certIDSet := make(map[uint]struct{})
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
if route.CertID != nil && *route.CertID != 0 {
|
|
||||||
certIDSet[*route.CertID] = struct{}{}
|
|
||||||
}
|
|
||||||
for _, certID := range route.CertIDs {
|
|
||||||
if certID != 0 {
|
|
||||||
certIDSet[certID] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, certID := range route.DomainCertIDs {
|
for _, certID := range route.DomainCertIDs {
|
||||||
if certID != 0 {
|
if certID != 0 {
|
||||||
certIDSet[certID] = struct{}{}
|
certIDSet[certID] = struct{}{}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/agent"
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/agent"
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -134,17 +133,17 @@ func GetTunnelConfig(ctx context.Context, node *model.OpenFlareNode) (*TunnelCon
|
|||||||
if !route.Enabled {
|
if !route.Enabled {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
domains, decodeErr := routeidentity.DecodeDomains(route.Domains, route.Domain)
|
zoneDomains, domainErr := model.ListZoneDomainsByRouteID(ctx, route.ID)
|
||||||
if decodeErr != nil {
|
if domainErr != nil || len(zoneDomains) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
localAddr, localPort := parseTunnelTargetAddr(route.TunnelTargetAddr)
|
localAddr, localPort := parseTunnelTargetAddr(route.TunnelTargetAddr)
|
||||||
proxies = append(proxies, ProxyEntry{
|
proxies = append(proxies, ProxyEntry{
|
||||||
Name: fmt.Sprintf("%s-%s", node.NodeID, sanitizeProxyName(domains[0])),
|
Name: fmt.Sprintf("%s-%s", node.NodeID, sanitizeProxyName(zoneDomains[0].Domain)),
|
||||||
Type: "http",
|
Type: "http",
|
||||||
LocalAddr: localAddr,
|
LocalAddr: localAddr,
|
||||||
LocalPort: localPort,
|
LocalPort: localPort,
|
||||||
CustomDomains: domains,
|
CustomDomains: zoneDomainNames(zoneDomains),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +155,14 @@ func GetTunnelConfig(ctx context.Context, node *model.OpenFlareNode) (*TunnelCon
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func zoneDomainNames(domains []model.ZoneDomain) []string {
|
||||||
|
names := make([]string, 0, len(domains))
|
||||||
|
for _, domain := range domains {
|
||||||
|
names = append(names, domain.Domain)
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
// ReportApplyLog records an apply result from OpenFlared.
|
// ReportApplyLog records an apply result from OpenFlared.
|
||||||
func ReportApplyLog(ctx context.Context, payload ApplyLogPayload) (*model.OpenFlareApplyLog, error) {
|
func ReportApplyLog(ctx context.Context, payload ApplyLogPayload) (*model.OpenFlareApplyLog, error) {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NormalizeDomains lowercases, deduplicates, and validates proxy route domains.
|
// NormalizeDomains lowercases, deduplicates, and validates proxy route domains.
|
||||||
@@ -38,7 +36,8 @@ func NormalizeDomains(rawDomains []string) ([]string, error) {
|
|||||||
return normalized, nil
|
return normalized, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeDomains parses stored domains JSON or falls back to a single domain value.
|
// DecodeDomains parses legacy route domain fields for the explicit migration
|
||||||
|
// command. Runtime consumers must read ZoneDomain bindings instead.
|
||||||
func DecodeDomains(raw string, fallbackDomain string) ([]string, error) {
|
func DecodeDomains(raw string, fallbackDomain string) ([]string, error) {
|
||||||
text := strings.TrimSpace(raw)
|
text := strings.TrimSpace(raw)
|
||||||
if text == "" {
|
if text == "" {
|
||||||
@@ -50,27 +49,3 @@ func DecodeDomains(raw string, fallbackDomain string) ([]string, error) {
|
|||||||
}
|
}
|
||||||
return NormalizeDomains(domains)
|
return NormalizeDomains(domains)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveSiteName returns the runtime site identifier for a proxy route.
|
|
||||||
func ResolveSiteName(route *model.ProxyRoute, raw, primaryDomain string) string {
|
|
||||||
siteName := strings.TrimSpace(raw)
|
|
||||||
if siteName != "" {
|
|
||||||
return siteName
|
|
||||||
}
|
|
||||||
if route != nil && strings.TrimSpace(route.SiteName) != "" {
|
|
||||||
return strings.TrimSpace(route.SiteName)
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(primaryDomain)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveFromRoute decodes domains and resolves the site name for a stored route.
|
|
||||||
func ResolveFromRoute(route *model.ProxyRoute) (siteName string, domains []string, err error) {
|
|
||||||
if route == nil {
|
|
||||||
return "", nil, errors.New("proxy route is nil")
|
|
||||||
}
|
|
||||||
domains, err = DecodeDomains(route.Domains, route.Domain)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
return ResolveSiteName(route, route.SiteName, domains[0]), domains, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,11 +4,9 @@
|
|||||||
package routeidentity
|
package routeidentity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDecodeDomainsNormalizesCaseAndOrder(t *testing.T) {
|
func TestDecodeDomainsNormalizesCaseAndOrder(t *testing.T) {
|
||||||
@@ -16,23 +14,3 @@ func TestDecodeDomainsNormalizesCaseAndOrder(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, []string{"www.example.com", "example.com"}, domains)
|
assert.Equal(t, []string{"www.example.com", "example.com"}, domains)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolveSiteNamePrefersExplicitValue(t *testing.T) {
|
|
||||||
route := &model.ProxyRoute{SiteName: "stored-name", Domain: "example.com"}
|
|
||||||
assert.Equal(t, "custom", ResolveSiteName(route, "custom", "example.com"))
|
|
||||||
assert.Equal(t, "stored-name", ResolveSiteName(route, "", "example.com"))
|
|
||||||
|
|
||||||
routeWithoutSiteName := &model.ProxyRoute{Domain: "example.com"}
|
|
||||||
assert.Equal(t, "example.com", ResolveSiteName(routeWithoutSiteName, "", "example.com"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveFromRoute(t *testing.T) {
|
|
||||||
route := &model.ProxyRoute{
|
|
||||||
Domain: "Example.COM",
|
|
||||||
Domains: `["example.com","www.example.com"]`,
|
|
||||||
}
|
|
||||||
siteName, domains, err := ResolveFromRoute(route)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "example.com", siteName)
|
|
||||||
assert.Equal(t, []string{"example.com", "www.example.com"}, domains)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/repository"
|
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||||
)
|
)
|
||||||
@@ -123,7 +122,7 @@ func SyncToUptimeKuma(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
existingOpenFlareMonitors := filterOpenFlareMonitors(client.GetMonitorList(), openFlareTagID)
|
existingOpenFlareMonitors := filterOpenFlareMonitors(client.GetMonitorList(), openFlareTagID)
|
||||||
expectedSitesMap := syncRouteMonitors(client, expectedRoutes, existingOpenFlareMonitors, openFlareTagID, config)
|
expectedSitesMap := syncRouteMonitors(ctx, client, expectedRoutes, existingOpenFlareMonitors, openFlareTagID, config)
|
||||||
removeStaleMonitors(client, existingOpenFlareMonitors, expectedSitesMap)
|
removeStaleMonitors(client, existingOpenFlareMonitors, expectedSitesMap)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -224,12 +223,18 @@ func filterOpenFlareMonitors(monitors map[string]Monitor, openFlareTagID int) ma
|
|||||||
return existingOpenFlareMonitors
|
return existingOpenFlareMonitors
|
||||||
}
|
}
|
||||||
|
|
||||||
func routeMonitorURL(route *model.ProxyRoute) (string, error) {
|
func routeMonitorURL(ctx context.Context, route *model.ProxyRoute) (string, error) {
|
||||||
domains, err := routeidentity.DecodeDomains(route.Domains, route.Domain)
|
if route == nil {
|
||||||
|
return "", fmt.Errorf("proxy route is nil")
|
||||||
|
}
|
||||||
|
domains, err := model.ListZoneDomainsByRouteID(ctx, route.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
domain := domains[0]
|
if len(domains) == 0 {
|
||||||
|
return "", fmt.Errorf("route %s has no zone domains", route.SiteName)
|
||||||
|
}
|
||||||
|
domain := domains[0].Domain
|
||||||
if route.EnableHTTPS {
|
if route.EnableHTTPS {
|
||||||
return "https://" + domain, nil
|
return "https://" + domain, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
package uptimekuma
|
package uptimekuma
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -69,11 +70,11 @@ func connectAndLoginUptimeKuma(kumaURL, kumaUsername, kumaPassword string) (*Soc
|
|||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncRouteMonitors(client *SocketIOClient, expectedRoutes []*model.ProxyRoute, existingMonitors map[string]Monitor, openFlareTagID int, config *kumaConfig) map[string]bool {
|
func syncRouteMonitors(ctx context.Context, client *SocketIOClient, expectedRoutes []*model.ProxyRoute, existingMonitors map[string]Monitor, openFlareTagID int, config *kumaConfig) map[string]bool {
|
||||||
expectedSitesMap := make(map[string]bool, len(expectedRoutes))
|
expectedSitesMap := make(map[string]bool, len(expectedRoutes))
|
||||||
for _, route := range expectedRoutes {
|
for _, route := range expectedRoutes {
|
||||||
expectedSitesMap[route.SiteName] = true
|
expectedSitesMap[route.SiteName] = true
|
||||||
targetURL, urlErr := routeMonitorURL(route)
|
targetURL, urlErr := routeMonitorURL(ctx, route)
|
||||||
if urlErr != nil {
|
if urlErr != nil {
|
||||||
slog.Error("Failed to resolve monitor URL", "name", route.SiteName, "error", urlErr)
|
slog.Error("Failed to resolve monitor URL", "name", route.SiteName, "error", urlErr)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ func setupSyncTestDB(t *testing.T) func() {
|
|||||||
DisableForeignKeyConstraintWhenMigrating: true,
|
DisableForeignKeyConstraintWhenMigrating: true,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, sqliteDB.AutoMigrate(&model.ProxyRoute{}, &model.SystemConfig{}))
|
require.NoError(t, sqliteDB.AutoMigrate(&model.ProxyRoute{}, &model.Zone{}, &model.ZoneDomain{}, &model.SystemConfig{}))
|
||||||
|
|
||||||
db.SetDB(sqliteDB)
|
db.SetDB(sqliteDB)
|
||||||
return func() {
|
return func() {
|
||||||
@@ -130,6 +130,17 @@ func setupSyncTestDB(t *testing.T) func() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createRouteZoneDomain(t *testing.T, ctx context.Context, route *model.ProxyRoute, domain string) {
|
||||||
|
t.Helper()
|
||||||
|
zone := &model.Zone{Domain: fmt.Sprintf("zone-%d.example", route.ID)}
|
||||||
|
require.NoError(t, db.DB(ctx).Create(zone).Error)
|
||||||
|
require.NoError(t, db.DB(ctx).Create(&model.ZoneDomain{
|
||||||
|
ZoneID: zone.ID,
|
||||||
|
ProxyRouteID: &route.ID,
|
||||||
|
Domain: domain,
|
||||||
|
}).Error)
|
||||||
|
}
|
||||||
|
|
||||||
func backupUptimeKumaConfig(ctx context.Context) func() {
|
func backupUptimeKumaConfig(ctx context.Context) func() {
|
||||||
// 备份所有 UptimeKuma 相关配置
|
// 备份所有 UptimeKuma 相关配置
|
||||||
configs := []string{
|
configs := []string{
|
||||||
@@ -215,6 +226,9 @@ func TestSyncToUptimeKumaSuccess(t *testing.T) {
|
|||||||
require.NoError(t, model.CreateProxyRouteRecord(ctx, routeA))
|
require.NoError(t, model.CreateProxyRouteRecord(ctx, routeA))
|
||||||
require.NoError(t, model.CreateProxyRouteRecord(ctx, routeB))
|
require.NoError(t, model.CreateProxyRouteRecord(ctx, routeB))
|
||||||
require.NoError(t, model.CreateProxyRouteRecord(ctx, routeC))
|
require.NoError(t, model.CreateProxyRouteRecord(ctx, routeC))
|
||||||
|
createRouteZoneDomain(t, ctx, routeA, "site-a.com")
|
||||||
|
createRouteZoneDomain(t, ctx, routeB, "site-b.com")
|
||||||
|
createRouteZoneDomain(t, ctx, routeC, "site-c.com")
|
||||||
|
|
||||||
monitorListJSON := `{
|
monitorListJSON := `{
|
||||||
"99": {
|
"99": {
|
||||||
@@ -318,6 +332,8 @@ func TestSyncToUptimeKumaSelectedScope(t *testing.T) {
|
|||||||
|
|
||||||
require.NoError(t, model.CreateProxyRouteRecord(ctx, routeA))
|
require.NoError(t, model.CreateProxyRouteRecord(ctx, routeA))
|
||||||
require.NoError(t, model.CreateProxyRouteRecord(ctx, routeB))
|
require.NoError(t, model.CreateProxyRouteRecord(ctx, routeB))
|
||||||
|
createRouteZoneDomain(t, ctx, routeA, "site-a.com")
|
||||||
|
createRouteZoneDomain(t, ctx, routeB, "site-b.com")
|
||||||
|
|
||||||
mockSrv := newMockKumaServer(`{}`)
|
mockSrv := newMockKumaServer(`{}`)
|
||||||
server := httptest.NewServer(mockSrv)
|
server := httptest.NewServer(mockSrv)
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ func RenderRouteConfig(doc Document, certificateFiles []SupportFile) (string, er
|
|||||||
for _, route := range doc.Routes {
|
for _, route := range doc.Routes {
|
||||||
domains := normalizedRouteDomains(route)
|
domains := normalizedRouteDomains(route)
|
||||||
if len(domains) == 0 {
|
if len(domains) == 0 {
|
||||||
return "", fmt.Errorf("route %s domains are invalid", route.Domain)
|
return "", fmt.Errorf("route %s domains are invalid", route.SiteName)
|
||||||
}
|
}
|
||||||
serverNames := renderServerNames(domains)
|
serverNames := renderServerNames(domains)
|
||||||
displayName := resolveRouteSiteName(route)
|
displayName := resolveRouteSiteName(route)
|
||||||
@@ -642,7 +642,7 @@ func resolveRouteSiteName(route Route) string {
|
|||||||
if domains := normalizedRouteDomains(route); len(domains) > 0 {
|
if domains := normalizedRouteDomains(route); len(domains) > 0 {
|
||||||
return domains[0]
|
return domains[0]
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(route.Domain)
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildRouteUpstreamName(route Route) string {
|
func buildRouteUpstreamName(route Route) string {
|
||||||
@@ -708,24 +708,13 @@ func renderDefaultServerBlock(statusCode int, http3Enabled bool) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func normalizedRouteDomains(route Route) []string {
|
func normalizedRouteDomains(route Route) []string {
|
||||||
if len(route.Domains) > 0 {
|
return route.Domains
|
||||||
return route.Domains
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(route.Domain) == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return []string{strings.TrimSpace(route.Domain)}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeCertIDs(primaryCertID *uint, certIDs []uint) []uint {
|
func certificateIDsFromDomainCertIDs(domainCertIDs []uint) []uint {
|
||||||
candidates := make([]uint, 0, len(certIDs)+1)
|
seen := make(map[uint]struct{}, len(domainCertIDs))
|
||||||
if primaryCertID != nil && *primaryCertID != 0 {
|
normalized := make([]uint, 0, len(domainCertIDs))
|
||||||
candidates = append(candidates, *primaryCertID)
|
for _, id := range domainCertIDs {
|
||||||
}
|
|
||||||
candidates = append(candidates, certIDs...)
|
|
||||||
seen := make(map[uint]struct{}, len(candidates))
|
|
||||||
normalized := make([]uint, 0, len(candidates))
|
|
||||||
for _, id := range candidates {
|
|
||||||
if id == 0 {
|
if id == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -738,27 +727,6 @@ func normalizeCertIDs(primaryCertID *uint, certIDs []uint) []uint {
|
|||||||
return normalized
|
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 {
|
func certificatesByID(files []SupportFile) map[uint]string {
|
||||||
result := make(map[uint]string)
|
result := make(map[uint]string)
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func validateRouteCertificates(route Route, displayName string, certIDs []uint,
|
|||||||
}
|
}
|
||||||
certPEM, ok := certificates[certID]
|
certPEM, ok := certificates[certID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("route %s certificate %d does not exist", route.Domain, certID)
|
return fmt.Errorf("route %s certificate %d does not exist", route.SiteName, certID)
|
||||||
}
|
}
|
||||||
if err := validateCertificateCoverage(certPEM, assignedDomains); err != nil {
|
if err := validateCertificateCoverage(certPEM, assignedDomains); err != nil {
|
||||||
return fmt.Errorf("site %s certificate validation failed: %w", displayName, err)
|
return fmt.Errorf("site %s certificate validation failed: %w", displayName, err)
|
||||||
@@ -105,16 +105,16 @@ func renderProxyRouteHTTPS(
|
|||||||
|
|
||||||
func renderPagesRoute(builder *strings.Builder, route Route, displayName, serverNames string, certificates map[uint]string, limitConfig routeLimitConfig, powEnabled bool, cfg ConfigSnapshot) error {
|
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 {
|
if route.PagesDeployment == nil {
|
||||||
return fmt.Errorf("route %s pages deployment is missing", route.Domain)
|
return fmt.Errorf("route %s pages deployment is missing", route.SiteName)
|
||||||
}
|
}
|
||||||
if !route.EnableHTTPS {
|
if !route.EnableHTTPS {
|
||||||
builder.WriteString(renderHTTPPagesServer(serverNames, displayName, route.PagesDeployment, limitConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword))
|
builder.WriteString(renderHTTPPagesServer(serverNames, displayName, route.PagesDeployment, limitConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
certIDs := normalizeCertIDs(route.CertID, route.CertIDs)
|
certIDs := certificateIDsFromDomainCertIDs(route.DomainCertIDs)
|
||||||
domainCertIDs := normalizeDomainCertIDs(normalizedRouteDomains(route), certIDs, route.DomainCertIDs)
|
domainCertIDs := route.DomainCertIDs
|
||||||
if len(certIDs) == 0 {
|
if len(certIDs) == 0 {
|
||||||
return fmt.Errorf("路由 %s 未配置证书", route.Domain)
|
return fmt.Errorf("路由 %s 未配置证书", route.SiteName)
|
||||||
}
|
}
|
||||||
partition := partitionRouteDomainsByCert(normalizedRouteDomains(route), certIDs, domainCertIDs)
|
partition := partitionRouteDomainsByCert(normalizedRouteDomains(route), certIDs, domainCertIDs)
|
||||||
if err := validateRouteCertificates(route, displayName, certIDs, partition, certificates); err != nil {
|
if err := validateRouteCertificates(route, displayName, certIDs, partition, certificates); err != nil {
|
||||||
@@ -137,10 +137,10 @@ func renderProxyRoute(builder *strings.Builder, route Route, displayName, server
|
|||||||
builder.WriteString(renderHTTPProxyServer(serverNames, displayName, route.OriginURL, route.OriginHost, route.CustomHeaders, cacheConfig, limitConfig, upstreamConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, cfg))
|
builder.WriteString(renderHTTPProxyServer(serverNames, displayName, route.OriginURL, route.OriginHost, route.CustomHeaders, cacheConfig, limitConfig, upstreamConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, cfg))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
certIDs := normalizeCertIDs(route.CertID, route.CertIDs)
|
certIDs := certificateIDsFromDomainCertIDs(route.DomainCertIDs)
|
||||||
domainCertIDs := normalizeDomainCertIDs(normalizedRouteDomains(route), certIDs, route.DomainCertIDs)
|
domainCertIDs := route.DomainCertIDs
|
||||||
if len(certIDs) == 0 {
|
if len(certIDs) == 0 {
|
||||||
return fmt.Errorf("路由 %s 未配置证书", route.Domain)
|
return fmt.Errorf("路由 %s 未配置证书", route.SiteName)
|
||||||
}
|
}
|
||||||
partition := partitionRouteDomainsByCert(normalizedRouteDomains(route), certIDs, domainCertIDs)
|
partition := partitionRouteDomainsByCert(normalizedRouteDomains(route), certIDs, domainCertIDs)
|
||||||
if err := validateRouteCertificates(route, displayName, certIDs, partition, certificates); err != nil {
|
if err := validateRouteCertificates(route, displayName, certIDs, partition, certificates); err != nil {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
func TestRenderWAFConfigIncludesAllRouteSiteNames(t *testing.T) {
|
func TestRenderWAFConfigIncludesAllRouteSiteNames(t *testing.T) {
|
||||||
doc := Document{
|
doc := Document{
|
||||||
Routes: []Route{
|
Routes: []Route{
|
||||||
{ID: 1, SiteName: "", Domain: "Example.COM", Domains: []string{"example.com", "www.example.com"}},
|
{ID: 1, SiteName: "example.com", Domains: []string{"example.com", "www.example.com"}},
|
||||||
{ID: 2, SiteName: "named-site", Domain: "other.example.com"},
|
{ID: 2, SiteName: "named-site", Domains: []string{"other.example.com"}},
|
||||||
},
|
},
|
||||||
WAF: WAFDocument{
|
WAF: WAFDocument{
|
||||||
RuleGroups: []WAFRuleGroup{
|
RuleGroups: []WAFRuleGroup{
|
||||||
@@ -293,7 +293,8 @@ func TestRenderRouteConfigPagesWithoutSPAFallbackServesRoot(t *testing.T) {
|
|||||||
Routes: []Route{
|
Routes: []Route{
|
||||||
{
|
{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Domain: "speedtest.example.com",
|
SiteName: "speedtest.example.com",
|
||||||
|
Domains: []string{"speedtest.example.com"},
|
||||||
UpstreamType: "pages",
|
UpstreamType: "pages",
|
||||||
EnableHTTPS: false,
|
EnableHTTPS: false,
|
||||||
PagesDeployment: &PagesDeployment{
|
PagesDeployment: &PagesDeployment{
|
||||||
@@ -325,7 +326,8 @@ func TestRenderRouteConfigPagesWithSPAFallbackServesRoot(t *testing.T) {
|
|||||||
Routes: []Route{
|
Routes: []Route{
|
||||||
{
|
{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Domain: "speedtest.example.com",
|
SiteName: "speedtest.example.com",
|
||||||
|
Domains: []string{"speedtest.example.com"},
|
||||||
UpstreamType: "pages",
|
UpstreamType: "pages",
|
||||||
EnableHTTPS: false,
|
EnableHTTPS: false,
|
||||||
PagesDeployment: &PagesDeployment{
|
PagesDeployment: &PagesDeployment{
|
||||||
|
|||||||
@@ -135,15 +135,12 @@ func DefaultPoWConfig() PoWConfig {
|
|||||||
type Route struct {
|
type Route struct {
|
||||||
ID uint `json:"id,omitempty"`
|
ID uint `json:"id,omitempty"`
|
||||||
SiteName string `json:"site_name,omitempty"`
|
SiteName string `json:"site_name,omitempty"`
|
||||||
Domain string `json:"domain"`
|
|
||||||
Domains []string `json:"domains,omitempty"`
|
Domains []string `json:"domains,omitempty"`
|
||||||
OriginURL string `json:"origin_url"`
|
OriginURL string `json:"origin_url"`
|
||||||
OriginHost string `json:"origin_host,omitempty"`
|
OriginHost string `json:"origin_host,omitempty"`
|
||||||
Upstreams []string `json:"upstreams,omitempty"`
|
Upstreams []string `json:"upstreams,omitempty"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
EnableHTTPS bool `json:"enable_https"`
|
EnableHTTPS bool `json:"enable_https"`
|
||||||
CertID *uint `json:"cert_id,omitempty"`
|
|
||||||
CertIDs []uint `json:"cert_ids,omitempty"`
|
|
||||||
DomainCertIDs []uint `json:"domain_cert_ids,omitempty"`
|
DomainCertIDs []uint `json:"domain_cert_ids,omitempty"`
|
||||||
RedirectHTTP bool `json:"redirect_http"`
|
RedirectHTTP bool `json:"redirect_http"`
|
||||||
LimitConnPerServer int `json:"limit_conn_per_server,omitempty"`
|
LimitConnPerServer int `json:"limit_conn_per_server,omitempty"`
|
||||||
|
|||||||
Reference in New Issue
Block a user