mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 15:26:36 +08:00
fix(pages): 收紧部署包与 Agent 同步边界
完成 V2 Phase 0 安全与一致性前置:统一真实归档限额、流式拉取、候选裁剪、保留上传删除语义及 Pages 路由引用锁。
This commit is contained in:
@@ -6,12 +6,14 @@ package proxy_route
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// CustomHeaderInput 自定义响应头。
|
||||
@@ -122,6 +124,9 @@ func CreateProxyRoute(ctx context.Context, input Input) (*View, error) {
|
||||
return nil, err
|
||||
}
|
||||
if err = db.DB(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := lockPagesProjectsForRouteMutation(tx, 0, route); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(route).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -141,11 +146,15 @@ func UpdateProxyRoute(ctx context.Context, id uint, input Input) (*View, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
previousPagesProjectID := pagesProjectIDForRoute(route)
|
||||
route, _, err = buildProxyRoute(ctx, route, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = db.DB(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := lockPagesProjectsForRouteMutation(tx, previousPagesProjectID, route); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := updateProxyRouteRecord(tx, route); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -159,6 +168,58 @@ func UpdateProxyRoute(ctx context.Context, id uint, input Input) (*View, error)
|
||||
return buildProxyRouteView(ctx, route)
|
||||
}
|
||||
|
||||
func pagesProjectIDForRoute(route *model.ProxyRoute) uint {
|
||||
if route == nil || route.UpstreamType != proxyRouteUpstreamTypePages || route.PagesProjectID == nil {
|
||||
return 0
|
||||
}
|
||||
return *route.PagesProjectID
|
||||
}
|
||||
|
||||
func lockPagesProjectsForRouteMutation(tx *gorm.DB, previousProjectID uint, route *model.ProxyRoute) error {
|
||||
nextProjectID := pagesProjectIDForRoute(route)
|
||||
var projectIDs []uint
|
||||
if previousProjectID != 0 {
|
||||
projectIDs = append(projectIDs, previousProjectID)
|
||||
}
|
||||
if nextProjectID != 0 && nextProjectID != previousProjectID {
|
||||
projectIDs = append(projectIDs, nextProjectID)
|
||||
}
|
||||
sort.Slice(projectIDs, func(i int, j int) bool { return projectIDs[i] < projectIDs[j] })
|
||||
|
||||
for _, projectID := range projectIDs {
|
||||
var project model.PagesProject
|
||||
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&project, projectID).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) && projectID != nextProjectID {
|
||||
continue
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New(errProxyRoutePagesNotFound)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if projectID == nextProjectID {
|
||||
if err := validateLockedPagesRouteProject(&project); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateLockedPagesRouteProject(project *model.PagesProject) error {
|
||||
if project == nil {
|
||||
return errors.New(errProxyRoutePagesNotFound)
|
||||
}
|
||||
if !project.Enabled {
|
||||
return errors.New(errProxyRoutePagesDisabled)
|
||||
}
|
||||
if project.ActiveDeploymentID == nil || *project.ActiveDeploymentID == 0 {
|
||||
return errors.New(errProxyRoutePagesNoDeploy)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteProxyRoute 删除代理规则。
|
||||
func DeleteProxyRoute(ctx context.Context, id uint) error {
|
||||
if _, err := model.GetProxyRouteByID(ctx, id); err != nil {
|
||||
|
||||
@@ -19,7 +19,14 @@ func setupProxyRouteTestDB(t *testing.T) func() {
|
||||
t.Helper()
|
||||
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, sqliteDB.AutoMigrate(&model.ProxyRoute{}, &model.Origin{}, &model.Zone{}, &model.ZoneDomain{}, &model.TLSCertificate{}))
|
||||
require.NoError(t, sqliteDB.AutoMigrate(
|
||||
&model.ProxyRoute{},
|
||||
&model.Origin{},
|
||||
&model.Zone{},
|
||||
&model.ZoneDomain{},
|
||||
&model.TLSCertificate{},
|
||||
&model.PagesProject{},
|
||||
))
|
||||
db.SetDB(sqliteDB)
|
||||
return func() { db.SetDB(nil) }
|
||||
}
|
||||
@@ -81,6 +88,52 @@ func TestCreateProxyRouteHTTPSRequiresCoveringCertificate(t *testing.T) {
|
||||
require.EqualError(t, err, errProxyRouteCertRequired)
|
||||
}
|
||||
|
||||
func TestPagesRouteLocksAndRevalidatesTargetProject(t *testing.T) {
|
||||
cleanup := setupProxyRouteTestDB(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
domain := createZoneDomain(t, ctx, "pages.example.com", nil)
|
||||
activeDeploymentID := uint(99)
|
||||
project := &model.PagesProject{
|
||||
Name: "Pages Site",
|
||||
Slug: "pages-site",
|
||||
Enabled: true,
|
||||
ActiveDeploymentID: &activeDeploymentID,
|
||||
}
|
||||
require.NoError(t, db.DB(ctx).Create(project).Error)
|
||||
|
||||
view, err := CreateProxyRoute(ctx, Input{
|
||||
SiteName: "pages",
|
||||
ZoneDomainIDs: []uint{domain.ID},
|
||||
UpstreamType: proxyRouteUpstreamTypePages,
|
||||
PagesProjectID: &project.ID,
|
||||
Enabled: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, view.PagesProjectID)
|
||||
assert.Equal(t, project.ID, *view.PagesProjectID)
|
||||
|
||||
require.NoError(t, db.DB(ctx).Delete(&model.PagesProject{}, project.ID).Error)
|
||||
route := &model.ProxyRoute{UpstreamType: proxyRouteUpstreamTypePages, PagesProjectID: &project.ID}
|
||||
err = db.DB(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return lockPagesProjectsForRouteMutation(tx, 0, route)
|
||||
})
|
||||
require.EqualError(t, err, errProxyRoutePagesNotFound)
|
||||
}
|
||||
|
||||
func TestRouteCanMoveAwayFromAlreadyMissingPagesProject(t *testing.T) {
|
||||
cleanup := setupProxyRouteTestDB(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
missingProjectID := uint(404)
|
||||
route := &model.ProxyRoute{UpstreamType: "direct"}
|
||||
|
||||
err := db.DB(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return lockPagesProjectsForRouteMutation(tx, missingProjectID, route)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestNormalizeCachePolicyDefaultsAndLegacy(t *testing.T) {
|
||||
assert.Equal(t, "", normalizeCachePolicy(false, "static"))
|
||||
// Empty/url on write = legacy all (compat); UI sends static explicitly for new default.
|
||||
|
||||
Reference in New Issue
Block a user