mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 08:36:37 +08:00
refactor(backend): rename OpenFlare directory to lowercase openflare
This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"Wavelet/openflare/plugins/server/kernel/model"
|
||||
"Wavelet/openflare/plugins/server/kernel/runtimeconfig"
|
||||
"Wavelet/openflare/plugins/server/kernel/testhelper"
|
||||
db "Wavelet/plugins/infra/database"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func setupSecurityTest(t *testing.T) (*gin.Engine, adminSeed, func()) {
|
||||
t.Helper()
|
||||
|
||||
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, sqliteDB.AutoMigrate(
|
||||
&model.User{},
|
||||
&model.AccessToken{},
|
||||
&model.Origin{},
|
||||
&model.ProxyRoute{},
|
||||
&model.OpenFlareWAFRuleGroup{},
|
||||
&model.OpenFlareWAFRuleGroupBinding{},
|
||||
&model.OpenFlareWAFIPGroup{},
|
||||
&model.TLSCertificate{},
|
||||
&model.Zone{},
|
||||
&model.ZoneDomain{},
|
||||
&model.DNSAccount{},
|
||||
&model.AcmeAccount{},
|
||||
&model.SystemConfig{},
|
||||
))
|
||||
|
||||
db.SetDB(sqliteDB)
|
||||
|
||||
seed, err := seedAdminWithAccessToken(sqliteDB)
|
||||
require.NoError(t, err)
|
||||
|
||||
previous := runtimeconfig.Get()
|
||||
runtimeconfig.SetSessionSecret("test_session_secret_for_security_integration")
|
||||
|
||||
engine := testhelper.NewTestGinEngine()
|
||||
mountOpenFlareTestRoutes(engine)
|
||||
|
||||
cleanup := func() {
|
||||
runtimeconfig.Set(previous)
|
||||
db.SetDB(nil)
|
||||
}
|
||||
|
||||
return engine, seed, cleanup
|
||||
}
|
||||
|
||||
func generateSelfSignedCertificatePair(t *testing.T, dnsNames []string) (string, string) {
|
||||
t.Helper()
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
||||
Subject: pkix.Name{
|
||||
CommonName: dnsNames[0],
|
||||
},
|
||||
DNSNames: dnsNames,
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
}
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
|
||||
return string(certPEM), string(keyPEM)
|
||||
}
|
||||
|
||||
func TestSecurityWAFTLSMigrationFlow(t *testing.T) {
|
||||
engine, seed, cleanup := setupSecurityTest(t)
|
||||
defer cleanup()
|
||||
|
||||
var (
|
||||
ruleGroupID uint
|
||||
ipGroupID uint
|
||||
proxyRouteID uint
|
||||
certID uint
|
||||
domainID uint
|
||||
dnsAccountID uint
|
||||
)
|
||||
|
||||
t.Run("WAF rule group create", func(t *testing.T) {
|
||||
rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/waf/rule-groups"), map[string]any{
|
||||
"name": "edge-security",
|
||||
}, adminAuthHeaders(seed.Token))
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
resp := requireAPIOK(t, rec)
|
||||
data := unmarshalAPIMap(t, resp.Data)
|
||||
ruleGroupID = uint(data["id"].(float64))
|
||||
assert.NotZero(t, ruleGroupID)
|
||||
assert.Equal(t, "edge-security", data["name"])
|
||||
assert.Equal(t, false, data["is_global"])
|
||||
assert.InDelta(t, float64(1), data["revision"], 1e-9)
|
||||
assert.NotNil(t, data["graph"])
|
||||
})
|
||||
|
||||
t.Run("WAF rule group list includes global and custom groups", func(t *testing.T) {
|
||||
rec := performJSONRequest(t, engine, http.MethodGet, apiPath("/waf/rule-groups"), nil, adminAuthHeaders(seed.Token))
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
resp := requireAPIOK(t, rec)
|
||||
groups := unmarshalAPISlice(t, resp.Data)
|
||||
require.GreaterOrEqual(t, len(groups), 2)
|
||||
|
||||
foundCustom := false
|
||||
foundGlobal := false
|
||||
for _, item := range groups {
|
||||
group, ok := item.(map[string]any)
|
||||
require.True(t, ok)
|
||||
if group["is_global"] == true {
|
||||
foundGlobal = true
|
||||
}
|
||||
if uint(group["id"].(float64)) == ruleGroupID {
|
||||
foundCustom = true
|
||||
assert.Equal(t, "edge-security", group["name"])
|
||||
}
|
||||
}
|
||||
assert.True(t, foundGlobal)
|
||||
assert.True(t, foundCustom)
|
||||
})
|
||||
|
||||
t.Run("WAF rule group get detail", func(t *testing.T) {
|
||||
rec := performJSONRequest(
|
||||
t,
|
||||
engine,
|
||||
http.MethodGet,
|
||||
fmt.Sprintf("%s/waf/rule-groups/%d", apiPath(""), ruleGroupID),
|
||||
nil,
|
||||
adminAuthHeaders(seed.Token),
|
||||
)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
resp := requireAPIOK(t, rec)
|
||||
data := unmarshalAPIMap(t, resp.Data)
|
||||
assert.InDelta(t, float64(ruleGroupID), data["id"], 1e-9)
|
||||
assert.Equal(t, "edge-security", data["name"])
|
||||
})
|
||||
|
||||
t.Run("WAF rule group update", func(t *testing.T) {
|
||||
rec := performJSONRequest(
|
||||
t,
|
||||
engine,
|
||||
http.MethodPost,
|
||||
fmt.Sprintf("%s/waf/rule-groups/%d/meta", apiPath(""), ruleGroupID),
|
||||
map[string]any{
|
||||
"name": "edge-security-updated", "enabled": true,
|
||||
},
|
||||
adminAuthHeaders(seed.Token),
|
||||
)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
resp := requireAPIOK(t, rec)
|
||||
data := unmarshalAPIMap(t, resp.Data)
|
||||
assert.Equal(t, "edge-security-updated", data["name"])
|
||||
assert.Equal(t, true, data["enabled"])
|
||||
})
|
||||
|
||||
t.Run("WAF IP group create", func(t *testing.T) {
|
||||
rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/waf/ip-groups"), map[string]any{
|
||||
"name": "blocked-ips",
|
||||
"type": "manual",
|
||||
"enabled": true,
|
||||
"ip_list": []string{"203.0.113.0/24", "198.51.100.10"},
|
||||
}, adminAuthHeaders(seed.Token))
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
resp := requireAPIOK(t, rec)
|
||||
data := unmarshalAPIMap(t, resp.Data)
|
||||
ipGroupID = uint(data["id"].(float64))
|
||||
assert.NotZero(t, ipGroupID)
|
||||
assert.Equal(t, "blocked-ips", data["name"])
|
||||
assert.Equal(t, "manual", data["type"])
|
||||
})
|
||||
|
||||
t.Run("create proxy route for WAF binding", func(t *testing.T) {
|
||||
// Create Zone and ZoneDomain directly in the DB
|
||||
routeZone := model.Zone{Domain: "example-route.com"}
|
||||
require.NoError(t, db.DB(context.Background()).Create(&routeZone).Error)
|
||||
routeZoneDomain := model.ZoneDomain{
|
||||
ZoneID: routeZone.ID,
|
||||
Domain: "route.example-route.com",
|
||||
}
|
||||
require.NoError(t, db.DB(context.Background()).Create(&routeZoneDomain).Error)
|
||||
|
||||
rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/proxy-routes/"), map[string]any{
|
||||
"site_name": "security-site",
|
||||
"zone_domain_ids": []uint{routeZoneDomain.ID},
|
||||
"origin_url": "http://origin.security.internal:8080",
|
||||
"enabled": true,
|
||||
}, adminAuthHeaders(seed.Token))
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
resp := requireAPIOK(t, rec)
|
||||
data := unmarshalAPIMap(t, resp.Data)
|
||||
proxyRouteID = uint(data["id"].(float64))
|
||||
assert.NotZero(t, proxyRouteID)
|
||||
assert.NotEmpty(t, data["zone_domains"])
|
||||
zoneDomains := data["zone_domains"].([]any)
|
||||
assert.Len(t, zoneDomains, 1)
|
||||
assert.Equal(t, "route.example-route.com", zoneDomains[0].(map[string]any)["domain"])
|
||||
})
|
||||
|
||||
t.Run("bind WAF rule group to proxy route", func(t *testing.T) {
|
||||
rec := performJSONRequest(
|
||||
t,
|
||||
engine,
|
||||
http.MethodPost,
|
||||
fmt.Sprintf("%s/waf/sites/%d/rule-groups", apiPath(""), proxyRouteID),
|
||||
map[string]any{
|
||||
"ids": []uint{ruleGroupID},
|
||||
},
|
||||
adminAuthHeaders(seed.Token),
|
||||
)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
resp := requireAPIOK(t, rec)
|
||||
data := unmarshalAPIMap(t, resp.Data)
|
||||
assert.InDelta(t, float64(proxyRouteID), data["route_id"], 1e-9)
|
||||
|
||||
appliedIDs, ok := data["applied_ids"].([]any)
|
||||
require.True(t, ok)
|
||||
require.Len(t, appliedIDs, 1)
|
||||
assert.InDelta(t, float64(ruleGroupID), appliedIDs[0], 1e-9)
|
||||
})
|
||||
|
||||
t.Run("verify site rule groups binding", func(t *testing.T) {
|
||||
rec := performJSONRequest(
|
||||
t,
|
||||
engine,
|
||||
http.MethodGet,
|
||||
fmt.Sprintf("%s/waf/sites/%d/rule-groups", apiPath(""), proxyRouteID),
|
||||
nil,
|
||||
adminAuthHeaders(seed.Token),
|
||||
)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
resp := requireAPIOK(t, rec)
|
||||
data := unmarshalAPIMap(t, resp.Data)
|
||||
assert.NotNil(t, data["global_rule_group"])
|
||||
|
||||
appliedGroups, ok := data["applied_rule_groups"].([]any)
|
||||
require.True(t, ok)
|
||||
require.Len(t, appliedGroups, 1)
|
||||
group, ok := appliedGroups[0].(map[string]any)
|
||||
require.True(t, ok)
|
||||
assert.InDelta(t, float64(ruleGroupID), group["id"], 1e-9)
|
||||
})
|
||||
|
||||
t.Run("create TLS certificate with PEM", func(t *testing.T) {
|
||||
certPEM, keyPEM := generateSelfSignedCertificatePair(t, []string{"security.example.com"})
|
||||
|
||||
rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/tls-certificates/"), map[string]any{
|
||||
"name": "security-cert",
|
||||
"cert_pem": certPEM,
|
||||
"key_pem": keyPEM,
|
||||
"remark": "self-signed integration cert",
|
||||
}, adminAuthHeaders(seed.Token))
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
resp := requireAPIOK(t, rec)
|
||||
data := unmarshalAPIMap(t, resp.Data)
|
||||
certID = uint(data["id"].(float64))
|
||||
assert.NotZero(t, certID)
|
||||
assert.Equal(t, "security-cert", data["name"])
|
||||
assert.Equal(t, "upload", data["provider"])
|
||||
})
|
||||
|
||||
t.Run("create Zone domain", func(t *testing.T) {
|
||||
zoneRec := performJSONRequest(t, engine, http.MethodPost, apiPath("/zones/"), map[string]any{
|
||||
"domain": "example.com",
|
||||
}, adminAuthHeaders(seed.Token))
|
||||
require.Equal(t, http.StatusOK, zoneRec.Code)
|
||||
zoneData := unmarshalAPIMap(t, requireAPIOK(t, zoneRec).Data)
|
||||
zoneID := uint(zoneData["id"].(float64))
|
||||
|
||||
rec := performJSONRequest(t, engine, http.MethodPost, fmt.Sprintf("%s/zones/%d/domains", apiPath(""), zoneID), map[string]any{
|
||||
"domain": "*.example.com",
|
||||
}, adminAuthHeaders(seed.Token))
|
||||
require.Equal(t, http.StatusBadRequest, rec.Code)
|
||||
errResp := decodeAPIResponse(t, rec)
|
||||
assert.NotEmpty(t, errResp.ErrorMsg)
|
||||
|
||||
rec = performJSONRequest(t, engine, http.MethodPost, fmt.Sprintf("%s/zones/%d/domains", apiPath(""), zoneID), map[string]any{
|
||||
"domain": "security.example.com", "cert_id": certID,
|
||||
}, adminAuthHeaders(seed.Token))
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
resp := requireAPIOK(t, rec)
|
||||
data := unmarshalAPIMap(t, resp.Data)
|
||||
domainID = uint(data["id"].(float64))
|
||||
assert.NotZero(t, domainID)
|
||||
assert.Equal(t, "security.example.com", data["domain"])
|
||||
assert.InDelta(t, float64(certID), data["cert_id"], 1e-9)
|
||||
})
|
||||
|
||||
t.Run("create DNS account", func(t *testing.T) {
|
||||
rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/dns-accounts/"), map[string]any{
|
||||
"name": "cloudflare-dns",
|
||||
"type": "cloudflare",
|
||||
"authorization": "test-api-token-value",
|
||||
}, adminAuthHeaders(seed.Token))
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
resp := requireAPIOK(t, rec)
|
||||
data := unmarshalAPIMap(t, resp.Data)
|
||||
dnsAccountID = uint(data["id"].(float64))
|
||||
assert.NotZero(t, dnsAccountID)
|
||||
assert.Equal(t, "cloudflare-dns", data["name"])
|
||||
assert.Equal(t, "cloudflare", data["type"])
|
||||
// API 响应会脱敏 authorization,不应回显明文凭证。
|
||||
if auth, ok := data["authorization"]; ok {
|
||||
assert.NotEqual(t, "test-api-token-value", auth)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("WAF rule group delete", func(t *testing.T) {
|
||||
rec := performJSONRequest(
|
||||
t,
|
||||
engine,
|
||||
http.MethodPost,
|
||||
fmt.Sprintf("%s/waf/rule-groups/%d/delete", apiPath(""), ruleGroupID),
|
||||
nil,
|
||||
adminAuthHeaders(seed.Token),
|
||||
)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
requireAPIOK(t, rec)
|
||||
|
||||
detailRec := performJSONRequest(
|
||||
t,
|
||||
engine,
|
||||
http.MethodGet,
|
||||
fmt.Sprintf("%s/waf/rule-groups/%d", apiPath(""), ruleGroupID),
|
||||
nil,
|
||||
adminAuthHeaders(seed.Token),
|
||||
)
|
||||
require.Equal(t, http.StatusNotFound, detailRec.Code)
|
||||
detailResp := decodeAPIResponse(t, detailRec)
|
||||
assert.NotEmpty(t, detailResp.ErrorMsg)
|
||||
})
|
||||
|
||||
_ = ipGroupID
|
||||
_ = domainID
|
||||
_ = dnsAccountID
|
||||
}
|
||||
Reference in New Issue
Block a user