mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-01 22:46:38 +08:00
[功能] 接口与校验改造
This commit is contained in:
@@ -2,10 +2,11 @@ package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"openflare/service"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetProxyRoutes godoc
|
||||
@@ -31,6 +32,39 @@ func GetProxyRoutes(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// GetProxyRoute godoc
|
||||
// @Summary Get proxy route detail
|
||||
// @Tags ProxyRoutes
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Route ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/proxy-routes/{id} [get]
|
||||
func GetProxyRoute(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "invalid id",
|
||||
})
|
||||
return
|
||||
}
|
||||
route, err := service.GetProxyRoute(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": route,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateProxyRoute godoc
|
||||
// @Summary Create proxy route
|
||||
// @Tags ProxyRoutes
|
||||
@@ -46,7 +80,7 @@ func CreateProxyRoute(c *gin.Context) {
|
||||
if err := json.NewDecoder(c.Request.Body).Decode(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
"message": "invalid payload",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -81,7 +115,7 @@ func UpdateProxyRoute(c *gin.Context) {
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
"message": "invalid id",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -89,7 +123,7 @@ func UpdateProxyRoute(c *gin.Context) {
|
||||
if err = json.NewDecoder(c.Request.Body).Decode(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
"message": "invalid payload",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -122,7 +156,7 @@ func DeleteProxyRoute(c *gin.Context) {
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
"message": "invalid id",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import "time"
|
||||
|
||||
const (
|
||||
legacyDatabaseSchemaVersion = 1
|
||||
currentDatabaseSchemaVersion = 5
|
||||
currentDatabaseSchemaVersion = 6
|
||||
databaseSchemaVersionRowID = 1
|
||||
)
|
||||
|
||||
|
||||
@@ -34,6 +34,32 @@ func (legacyProxyRouteV4) TableName() string {
|
||||
return "proxy_routes"
|
||||
}
|
||||
|
||||
type legacyProxyRouteV5 struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
SiteName string `gorm:"size:255;not null;default:''"`
|
||||
Domain string `gorm:"uniqueIndex;size:255;not null"`
|
||||
Domains string `gorm:"type:text;not null;default:'[]'"`
|
||||
OriginID *uint `gorm:"index"`
|
||||
OriginURL string `gorm:"size:2048;not null"`
|
||||
OriginHost string `gorm:"size:255"`
|
||||
Upstreams string `gorm:"type:text;not null;default:'[]'"`
|
||||
Enabled bool `gorm:"not null;default:true"`
|
||||
EnableHTTPS bool `gorm:"column:enable_https;not null;default:false"`
|
||||
CertID *uint
|
||||
RedirectHTTP bool `gorm:"not null;default:false"`
|
||||
CacheEnabled bool `gorm:"not null;default:false"`
|
||||
CachePolicy string `gorm:"size:32;not null;default:''"`
|
||||
CacheRules string `gorm:"type:text;not null;default:'[]'"`
|
||||
CustomHeaders string `gorm:"type:text;not null;default:'[]'"`
|
||||
Remark string `gorm:"size:255"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (legacyProxyRouteV5) TableName() string {
|
||||
return "proxy_routes"
|
||||
}
|
||||
|
||||
func openBareTestSQLiteDB(t *testing.T, name string) *gorm.DB {
|
||||
t.Helper()
|
||||
|
||||
@@ -560,6 +586,69 @@ func TestEnsureDatabaseSchemaUpToDateBackfillsProxyRouteSiteFields(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureDatabaseSchemaUpToDateAddsProxyRouteRateLimitFields(t *testing.T) {
|
||||
db := openBareTestSQLiteDB(t, "legacy-proxy-route-rate-limits.db")
|
||||
if err := registerSharding(db, "sqlite"); err != nil {
|
||||
t.Fatalf("register sharding: %v", err)
|
||||
}
|
||||
if err := autoMigrateSchemaMetadata(db); err != nil {
|
||||
t.Fatalf("auto migrate schema metadata: %v", err)
|
||||
}
|
||||
|
||||
for _, item := range registeredModels() {
|
||||
if _, ok := item.(*ProxyRoute); ok {
|
||||
continue
|
||||
}
|
||||
if err := db.AutoMigrate(item); err != nil {
|
||||
t.Fatalf("auto migrate supporting table: %v", err)
|
||||
}
|
||||
}
|
||||
if err := db.AutoMigrate(&legacyProxyRouteV5{}); err != nil {
|
||||
t.Fatalf("auto migrate legacy proxy_routes v5: %v", err)
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
if err := db.Create(&legacyProxyRouteV5{
|
||||
SiteName: "main-site",
|
||||
Domain: "app.example.com",
|
||||
Domains: `["app.example.com","www.example.com"]`,
|
||||
OriginURL: "https://origin-a.internal:8443",
|
||||
Upstreams: `["https://origin-a.internal:8443"]`,
|
||||
Enabled: true,
|
||||
EnableHTTPS: false,
|
||||
RedirectHTTP: false,
|
||||
CacheEnabled: false,
|
||||
CachePolicy: "",
|
||||
CacheRules: `[]`,
|
||||
CustomHeaders: `[]`,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed legacy proxy route v5: %v", err)
|
||||
}
|
||||
if err := saveDatabaseSchemaVersion(db, 5); err != nil {
|
||||
t.Fatalf("save schema version: %v", err)
|
||||
}
|
||||
|
||||
previousDB := DB
|
||||
DB = db
|
||||
t.Cleanup(func() {
|
||||
DB = previousDB
|
||||
})
|
||||
|
||||
if err := ensureDatabaseSchemaUpToDate(db, "sqlite"); err != nil {
|
||||
t.Fatalf("ensureDatabaseSchemaUpToDate: %v", err)
|
||||
}
|
||||
|
||||
var route ProxyRoute
|
||||
if err := db.First(&route).Error; err != nil {
|
||||
t.Fatalf("query migrated proxy route: %v", err)
|
||||
}
|
||||
if route.LimitConnPerServer != 0 || route.LimitConnPerIP != 0 || route.LimitRate != "" {
|
||||
t.Fatalf("expected new rate limit fields to default to disabled values, got %+v", route)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunDatabaseSchemaMigrationDoesNotAdvanceVersionWhenValidationFails(t *testing.T) {
|
||||
db := openBareTestSQLiteDB(t, "failed-validation.db")
|
||||
|
||||
|
||||
@@ -384,6 +384,22 @@ func validateDatabaseSchemaV5(db *gorm.DB, backend string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDatabaseSchemaV6(db *gorm.DB, backend string) error {
|
||||
if err := validateDatabaseSchemaV5(db, backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if !db.Migrator().HasColumn(&ProxyRoute{}, "limit_conn_per_server") {
|
||||
return fmt.Errorf("column proxy_routes.limit_conn_per_server is missing")
|
||||
}
|
||||
if !db.Migrator().HasColumn(&ProxyRoute{}, "limit_conn_per_ip") {
|
||||
return fmt.Errorf("column proxy_routes.limit_conn_per_ip is missing")
|
||||
}
|
||||
if !db.Migrator().HasColumn(&ProxyRoute{}, "limit_rate") {
|
||||
return fmt.Errorf("column proxy_routes.limit_rate is missing")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func renameLegacyObservabilityShardTables(db *gorm.DB) error {
|
||||
for _, baseTable := range shardedObservabilityBaseTables() {
|
||||
for _, table := range observabilityShardTables(baseTable) {
|
||||
@@ -738,12 +754,27 @@ func migrateV5(db *gorm.DB, backend string) error {
|
||||
return ensureProxyRouteSiteNameUniqueIndex(db)
|
||||
}
|
||||
|
||||
// migrateV6 adds structured website-level rate limit fields to proxy_routes.
|
||||
func migrateV6(db *gorm.DB, backend string) error {
|
||||
if err := applyCurrentSchema(db, backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := backfillOriginsFromProxyRoutes(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := backfillProxyRouteSiteFields(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return ensureProxyRouteSiteNameUniqueIndex(db)
|
||||
}
|
||||
|
||||
func databaseSchemaMigrations() []databaseSchemaMigration {
|
||||
return []databaseSchemaMigration{
|
||||
{fromVersion: 1, toVersion: 2, migrate: migrateV2, validate: validateDatabaseSchemaV2},
|
||||
{fromVersion: 2, toVersion: 3, migrate: migrateV3, validate: validateDatabaseSchemaV3},
|
||||
{fromVersion: 3, toVersion: 4, migrate: migrateV4, validate: validateDatabaseSchemaV4},
|
||||
{fromVersion: 4, toVersion: 5, migrate: migrateV5, validate: validateDatabaseSchemaV5},
|
||||
{fromVersion: 5, toVersion: 6, migrate: migrateV6, validate: validateDatabaseSchemaV6},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -820,7 +851,7 @@ func initializeFreshDatabaseSchema(db *gorm.DB, backend string) error {
|
||||
if err := ensureProxyRouteSiteNameUniqueIndex(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateDatabaseSchemaV5(db, backend); err != nil {
|
||||
if err := validateDatabaseSchemaV6(db, backend); err != nil {
|
||||
return err
|
||||
}
|
||||
return saveDatabaseSchemaVersion(db, currentDatabaseSchemaVersion)
|
||||
|
||||
@@ -3,25 +3,28 @@ package model
|
||||
import "time"
|
||||
|
||||
type ProxyRoute struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
SiteName string `json:"site_name" gorm:"size:255;not null;default:''"`
|
||||
Domain string `json:"domain" gorm:"uniqueIndex;size:255;not null"`
|
||||
Domains string `json:"domains" gorm:"type:text;not null;default:'[]'"`
|
||||
OriginID *uint `json:"origin_id" gorm:"index"`
|
||||
OriginURL string `json:"origin_url" gorm:"size:2048;not null"`
|
||||
OriginHost string `json:"origin_host" gorm:"size:255"`
|
||||
Upstreams string `json:"upstreams" gorm:"type:text;not null;default:'[]'"`
|
||||
Enabled bool `json:"enabled" gorm:"not null;default:true"`
|
||||
EnableHTTPS bool `json:"enable_https" gorm:"column:enable_https;not null;default:false"`
|
||||
CertID *uint `json:"cert_id"`
|
||||
RedirectHTTP bool `json:"redirect_http" gorm:"not null;default:false"`
|
||||
CacheEnabled bool `json:"cache_enabled" gorm:"not null;default:false"`
|
||||
CachePolicy string `json:"cache_policy" gorm:"size:32;not null;default:''"`
|
||||
CacheRules string `json:"cache_rules" gorm:"type:text;not null;default:'[]'"`
|
||||
CustomHeaders string `json:"custom_headers" gorm:"type:text;not null;default:'[]'"`
|
||||
Remark string `json:"remark" gorm:"size:255"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
SiteName string `json:"site_name" gorm:"size:255;not null;default:''"`
|
||||
Domain string `json:"domain" gorm:"uniqueIndex;size:255;not null"`
|
||||
Domains string `json:"domains" gorm:"type:text;not null;default:'[]'"`
|
||||
OriginID *uint `json:"origin_id" gorm:"index"`
|
||||
OriginURL string `json:"origin_url" gorm:"size:2048;not null"`
|
||||
OriginHost string `json:"origin_host" gorm:"size:255"`
|
||||
Upstreams string `json:"upstreams" gorm:"type:text;not null;default:'[]'"`
|
||||
Enabled bool `json:"enabled" gorm:"not null;default:true"`
|
||||
EnableHTTPS bool `json:"enable_https" gorm:"column:enable_https;not null;default:false"`
|
||||
CertID *uint `json:"cert_id"`
|
||||
RedirectHTTP bool `json:"redirect_http" gorm:"not null;default:false"`
|
||||
LimitConnPerServer int `json:"limit_conn_per_server" gorm:"not null;default:0"`
|
||||
LimitConnPerIP int `json:"limit_conn_per_ip" gorm:"not null;default:0"`
|
||||
LimitRate string `json:"limit_rate" gorm:"size:32;not null;default:''"`
|
||||
CacheEnabled bool `json:"cache_enabled" gorm:"not null;default:false"`
|
||||
CachePolicy string `json:"cache_policy" gorm:"size:32;not null;default:''"`
|
||||
CacheRules string `json:"cache_rules" gorm:"type:text;not null;default:'[]'"`
|
||||
CustomHeaders string `json:"custom_headers" gorm:"type:text;not null;default:'[]'"`
|
||||
Remark string `json:"remark" gorm:"size:255"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func ListProxyRoutes() (routes []*ProxyRoute, err error) {
|
||||
@@ -51,22 +54,25 @@ func (route *ProxyRoute) Insert() error {
|
||||
|
||||
func (route *ProxyRoute) Update() error {
|
||||
return DB.Model(&ProxyRoute{}).Where("id = ?", route.ID).Updates(map[string]any{
|
||||
"site_name": route.SiteName,
|
||||
"domain": route.Domain,
|
||||
"domains": route.Domains,
|
||||
"origin_id": route.OriginID,
|
||||
"origin_url": route.OriginURL,
|
||||
"origin_host": route.OriginHost,
|
||||
"upstreams": route.Upstreams,
|
||||
"enabled": route.Enabled,
|
||||
"enable_https": route.EnableHTTPS,
|
||||
"cert_id": route.CertID,
|
||||
"redirect_http": route.RedirectHTTP,
|
||||
"cache_enabled": route.CacheEnabled,
|
||||
"cache_policy": route.CachePolicy,
|
||||
"cache_rules": route.CacheRules,
|
||||
"custom_headers": route.CustomHeaders,
|
||||
"remark": route.Remark,
|
||||
"site_name": route.SiteName,
|
||||
"domain": route.Domain,
|
||||
"domains": route.Domains,
|
||||
"origin_id": route.OriginID,
|
||||
"origin_url": route.OriginURL,
|
||||
"origin_host": route.OriginHost,
|
||||
"upstreams": route.Upstreams,
|
||||
"enabled": route.Enabled,
|
||||
"enable_https": route.EnableHTTPS,
|
||||
"cert_id": route.CertID,
|
||||
"redirect_http": route.RedirectHTTP,
|
||||
"limit_conn_per_server": route.LimitConnPerServer,
|
||||
"limit_conn_per_ip": route.LimitConnPerIP,
|
||||
"limit_rate": route.LimitRate,
|
||||
"cache_enabled": route.CacheEnabled,
|
||||
"cache_policy": route.CachePolicy,
|
||||
"cache_rules": route.CacheRules,
|
||||
"custom_headers": route.CustomHeaders,
|
||||
"remark": route.Remark,
|
||||
}).Error
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ func SetApiRouter(router *gin.Engine) {
|
||||
proxyRoute.Use(middleware.AdminAuth())
|
||||
{
|
||||
proxyRoute.GET("/", controller.GetProxyRoutes)
|
||||
proxyRoute.GET("/:id", controller.GetProxyRoute)
|
||||
proxyRoute.POST("/", controller.CreateProxyRoute)
|
||||
proxyRoute.POST("/:id/update", controller.UpdateProxyRoute)
|
||||
proxyRoute.POST("/:id/delete", controller.DeleteProxyRoute)
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestPhase1PublishLifecycle(t *testing.T) {
|
||||
"remark": "primary route",
|
||||
}
|
||||
resp := performJSONRequest(t, engine, token, http.MethodPost, "/api/proxy-routes/", createBody)
|
||||
var createdRoute model.ProxyRoute
|
||||
var createdRoute service.ProxyRouteView
|
||||
decodeResponseData(t, resp, &createdRoute)
|
||||
if createdRoute.Domain != "app.example.com" {
|
||||
t.Fatalf("unexpected created route domain: %s", createdRoute.Domain)
|
||||
@@ -74,7 +74,7 @@ func TestPhase1PublishLifecycle(t *testing.T) {
|
||||
}
|
||||
|
||||
resp = performJSONRequest(t, engine, token, http.MethodGet, "/api/proxy-routes/", nil)
|
||||
var routes []model.ProxyRoute
|
||||
var routes []service.ProxyRouteView
|
||||
decodeResponseData(t, resp, &routes)
|
||||
if len(routes) != 1 {
|
||||
t.Fatalf("expected 1 route, got %d", len(routes))
|
||||
@@ -289,7 +289,7 @@ func TestPhase1HTTPSAndCertificateImportLifecycle(t *testing.T) {
|
||||
"redirect_http": true,
|
||||
"remark": "https route",
|
||||
})
|
||||
var route model.ProxyRoute
|
||||
var route service.ProxyRouteView
|
||||
decodeResponseData(t, resp, &route)
|
||||
if !route.EnableHTTPS || route.CertID == nil || *route.CertID != manualCertificate.ID {
|
||||
t.Fatal("expected route to persist https certificate binding")
|
||||
@@ -324,7 +324,7 @@ func TestPhase1HTTPSAndCertificateImportLifecycle(t *testing.T) {
|
||||
}
|
||||
|
||||
listResp := performJSONRequest(t, engine, token, http.MethodGet, "/api/proxy-routes/", nil)
|
||||
var routes []model.ProxyRoute
|
||||
var routes []service.ProxyRouteView
|
||||
decodeResponseData(t, listResp, &routes)
|
||||
if len(routes) != 1 || !routes[0].EnableHTTPS || routes[0].CertID == nil || *routes[0].CertID != manualCertificate.ID || !routes[0].RedirectHTTP {
|
||||
t.Fatalf("expected route list to reflect https update, got %+v", routes)
|
||||
|
||||
@@ -460,7 +460,7 @@ func TestPhase2CustomHeadersPreviewAndDiffLifecycle(t *testing.T) {
|
||||
{"key": "X-Trace-Id", "value": "$request_id"},
|
||||
},
|
||||
})
|
||||
var createdRoute model.ProxyRoute
|
||||
var createdRoute service.ProxyRouteView
|
||||
decodeResponseData(t, createResp, &createdRoute)
|
||||
if !strings.Contains(createdRoute.CustomHeaders, "X-Trace-Id") {
|
||||
t.Fatalf("expected custom headers to be stored as json, got %s", createdRoute.CustomHeaders)
|
||||
@@ -468,6 +468,9 @@ func TestPhase2CustomHeadersPreviewAndDiffLifecycle(t *testing.T) {
|
||||
if createdRoute.OriginHost != "preview-origin.internal" {
|
||||
t.Fatalf("expected origin_host to be stored, got %s", createdRoute.OriginHost)
|
||||
}
|
||||
if createdRoute.SiteName != "preview.example.com" || createdRoute.PrimaryDomain != "preview.example.com" || createdRoute.DomainCount != 1 {
|
||||
t.Fatalf("expected website identity fields in create response, got %+v", createdRoute)
|
||||
}
|
||||
|
||||
performJSONRequest(t, engine, token, http.MethodPost, "/api/config-versions/publish", nil)
|
||||
|
||||
@@ -491,6 +494,9 @@ func TestPhase2CustomHeadersPreviewAndDiffLifecycle(t *testing.T) {
|
||||
var preview map[string]any
|
||||
decodeResponseData(t, previewResp, &preview)
|
||||
renderedConfig, _ := preview["rendered_config"].(string)
|
||||
if websiteCount, ok := preview["website_count"].(float64); !ok || int(websiteCount) != 2 {
|
||||
t.Fatalf("expected preview website_count=2, got %#v", preview["website_count"])
|
||||
}
|
||||
if !strings.Contains(renderedConfig, `proxy_set_header X-Release "candidate";`) {
|
||||
t.Fatalf("expected preview endpoint to return custom header, got %s", renderedConfig)
|
||||
}
|
||||
@@ -515,6 +521,67 @@ func TestPhase2CustomHeadersPreviewAndDiffLifecycle(t *testing.T) {
|
||||
if !ok || len(addedDomains) != 1 || addedDomains[0].(string) != "new-preview.example.com" {
|
||||
t.Fatalf("unexpected added domains: %#v", diff["added_domains"])
|
||||
}
|
||||
modifiedSites, ok := diff["modified_sites"].([]any)
|
||||
if !ok || len(modifiedSites) != 1 || modifiedSites[0].(string) != "preview.example.com" {
|
||||
t.Fatalf("unexpected modified sites: %#v", diff["modified_sites"])
|
||||
}
|
||||
addedSites, ok := diff["added_sites"].([]any)
|
||||
if !ok || len(addedSites) != 1 || addedSites[0].(string) != "new-preview.example.com" {
|
||||
t.Fatalf("unexpected added sites: %#v", diff["added_sites"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhase2ProxyRouteWebsiteDetailAndLimits(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
common.RedisEnabled = false
|
||||
setupTestDB(t)
|
||||
|
||||
engine := gin.New()
|
||||
engine.Use(sessions.Sessions("session", cookie.NewStore([]byte("test-secret"))))
|
||||
router.SetApiRouter(engine)
|
||||
|
||||
token := prepareRootToken(t)
|
||||
|
||||
createResp := performJSONRequest(t, engine, token, http.MethodPost, "/api/proxy-routes/", map[string]any{
|
||||
"site_name": "marketing-site",
|
||||
"domains": []string{"app.example.com", "www.example.com"},
|
||||
"origin_url": "https://origin.internal",
|
||||
"enabled": true,
|
||||
"limit_conn_per_server": 120,
|
||||
"limit_conn_per_ip": 12,
|
||||
"limit_rate": "512K",
|
||||
})
|
||||
var createdRoute service.ProxyRouteView
|
||||
decodeResponseData(t, createResp, &createdRoute)
|
||||
if createdRoute.SiteName != "marketing-site" || createdRoute.PrimaryDomain != "app.example.com" {
|
||||
t.Fatalf("unexpected create payload: %+v", createdRoute)
|
||||
}
|
||||
if createdRoute.DomainCount != 2 || len(createdRoute.Domains) != 2 || createdRoute.Domains[1] != "www.example.com" {
|
||||
t.Fatalf("expected multi-domain website view, got %+v", createdRoute)
|
||||
}
|
||||
if createdRoute.LimitConnPerServer != 120 || createdRoute.LimitConnPerIP != 12 || createdRoute.LimitRate != "512k" {
|
||||
t.Fatalf("expected normalized rate limit fields, got %+v", createdRoute)
|
||||
}
|
||||
if len(createdRoute.UpstreamList) != 1 || createdRoute.UpstreamList[0] != "https://origin.internal" {
|
||||
t.Fatalf("expected structured upstream list, got %+v", createdRoute.UpstreamList)
|
||||
}
|
||||
|
||||
detailResp := performJSONRequest(t, engine, token, http.MethodGet, "/api/proxy-routes/"+toString(createdRoute.ID), nil)
|
||||
var detail service.ProxyRouteView
|
||||
decodeResponseData(t, detailResp, &detail)
|
||||
if detail.ID != createdRoute.ID || detail.SiteName != "marketing-site" || detail.LimitRate != "512k" {
|
||||
t.Fatalf("unexpected detail response: %+v", detail)
|
||||
}
|
||||
if len(detail.Domains) != 2 || detail.Domains[0] != "app.example.com" || detail.Domains[1] != "www.example.com" {
|
||||
t.Fatalf("expected detail response to expose full domain list, got %+v", detail.Domains)
|
||||
}
|
||||
|
||||
listResp := performJSONRequest(t, engine, token, http.MethodGet, "/api/proxy-routes/", nil)
|
||||
var routes []service.ProxyRouteView
|
||||
decodeResponseData(t, listResp, &routes)
|
||||
if len(routes) != 1 || routes[0].SiteName != "marketing-site" || routes[0].LimitConnPerServer != 120 {
|
||||
t.Fatalf("unexpected proxy route list response: %+v", routes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhase2GlobalDiscoveryRegistration(t *testing.T) {
|
||||
|
||||
@@ -35,6 +35,7 @@ type ConfigPreviewResult struct {
|
||||
SupportFiles []SupportFile `json:"support_files"`
|
||||
Checksum string `json:"checksum"`
|
||||
RouteCount int `json:"route_count"`
|
||||
WebsiteCount int `json:"website_count"`
|
||||
}
|
||||
|
||||
type ConfigVersionSummary = model.ConfigVersionSummary
|
||||
@@ -43,12 +44,17 @@ type ConfigVersionDetail = model.ConfigVersion
|
||||
|
||||
type ConfigDiffResult struct {
|
||||
ActiveVersion string `json:"active_version,omitempty"`
|
||||
AddedSites []string `json:"added_sites"`
|
||||
RemovedSites []string `json:"removed_sites"`
|
||||
ModifiedSites []string `json:"modified_sites"`
|
||||
AddedDomains []string `json:"added_domains"`
|
||||
RemovedDomains []string `json:"removed_domains"`
|
||||
ModifiedDomains []string `json:"modified_domains"`
|
||||
MainConfigChanged bool `json:"main_config_changed"`
|
||||
ChangedOptionKeys []string `json:"changed_option_keys"`
|
||||
ChangedOptionDetails []ConfigOptionDiffItem `json:"changed_option_details"`
|
||||
CurrentWebsiteCount int `json:"current_website_count"`
|
||||
ActiveWebsiteCount int `json:"active_website_count"`
|
||||
}
|
||||
|
||||
type ConfigOptionDiffItem struct {
|
||||
@@ -58,21 +64,24 @@ type ConfigOptionDiffItem struct {
|
||||
}
|
||||
|
||||
type snapshotRoute struct {
|
||||
SiteName string `json:"site_name,omitempty"`
|
||||
Domain string `json:"domain"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
OriginURL string `json:"origin_url"`
|
||||
OriginHost string `json:"origin_host,omitempty"`
|
||||
Upstreams []string `json:"upstreams,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
EnableHTTPS bool `json:"enable_https"`
|
||||
CertID *uint `json:"cert_id,omitempty"`
|
||||
RedirectHTTP bool `json:"redirect_http"`
|
||||
CacheEnabled bool `json:"cache_enabled"`
|
||||
CachePolicy string `json:"cache_policy,omitempty"`
|
||||
CacheRules []string `json:"cache_rules,omitempty"`
|
||||
CustomHeaders []ProxyRouteCustomHeaderInput `json:"custom_headers,omitempty"`
|
||||
Remark string `json:"remark,omitempty"`
|
||||
SiteName string `json:"site_name,omitempty"`
|
||||
Domain string `json:"domain"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
OriginURL string `json:"origin_url"`
|
||||
OriginHost string `json:"origin_host,omitempty"`
|
||||
Upstreams []string `json:"upstreams,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
EnableHTTPS bool `json:"enable_https"`
|
||||
CertID *uint `json:"cert_id,omitempty"`
|
||||
RedirectHTTP bool `json:"redirect_http"`
|
||||
LimitConnPerServer int `json:"limit_conn_per_server,omitempty"`
|
||||
LimitConnPerIP int `json:"limit_conn_per_ip,omitempty"`
|
||||
LimitRate string `json:"limit_rate,omitempty"`
|
||||
CacheEnabled bool `json:"cache_enabled"`
|
||||
CachePolicy string `json:"cache_policy,omitempty"`
|
||||
CacheRules []string `json:"cache_rules,omitempty"`
|
||||
CustomHeaders []ProxyRouteCustomHeaderInput `json:"custom_headers,omitempty"`
|
||||
Remark string `json:"remark,omitempty"`
|
||||
}
|
||||
|
||||
type routeCacheConfig struct {
|
||||
@@ -81,6 +90,12 @@ type routeCacheConfig struct {
|
||||
Rules []string
|
||||
}
|
||||
|
||||
type routeLimitConfig struct {
|
||||
LimitConnPerServer int
|
||||
LimitConnPerIP int
|
||||
LimitRate string
|
||||
}
|
||||
|
||||
type routeUpstreamConfig struct {
|
||||
Name string
|
||||
Scheme string
|
||||
@@ -207,6 +222,7 @@ func PreviewConfigVersion() (*ConfigPreviewResult, error) {
|
||||
SupportFiles: bundle.SupportFiles,
|
||||
Checksum: bundle.Checksum,
|
||||
RouteCount: len(bundle.Routes),
|
||||
WebsiteCount: len(bundle.SnapshotRoutes),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -216,21 +232,29 @@ func DiffConfigVersion() (*ConfigDiffResult, error) {
|
||||
return nil, err
|
||||
}
|
||||
result := &ConfigDiffResult{
|
||||
AddedSites: []string{},
|
||||
RemovedSites: []string{},
|
||||
ModifiedSites: []string{},
|
||||
AddedDomains: []string{},
|
||||
RemovedDomains: []string{},
|
||||
ModifiedDomains: []string{},
|
||||
ChangedOptionKeys: []string{},
|
||||
ChangedOptionDetails: []ConfigOptionDiffItem{},
|
||||
CurrentWebsiteCount: len(bundle.SnapshotRoutes),
|
||||
}
|
||||
activeVersion, err := model.GetActiveConfigVersion()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
for _, route := range bundle.SnapshotRoutes {
|
||||
result.AddedSites = append(result.AddedSites, route.SiteName)
|
||||
result.AddedDomains = append(result.AddedDomains, route.Domains...)
|
||||
}
|
||||
result.MainConfigChanged = true
|
||||
result.ChangedOptionKeys = openRestyOptionKeys()
|
||||
result.ChangedOptionDetails = buildInitialOpenRestyOptionDiffs(bundle.OpenRestyConfig)
|
||||
sort.Strings(result.AddedSites)
|
||||
sort.Strings(result.AddedDomains)
|
||||
sort.Strings(result.ChangedOptionKeys)
|
||||
return result, nil
|
||||
}
|
||||
return nil, err
|
||||
@@ -240,6 +264,24 @@ func DiffConfigVersion() (*ConfigDiffResult, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.ActiveWebsiteCount = len(activeSnapshot.Routes)
|
||||
currentSiteMap := flattenSnapshotRoutesBySite(bundle.SnapshotRoutes)
|
||||
activeSiteMap := flattenSnapshotRoutesBySite(activeSnapshot.Routes)
|
||||
for siteName, currentRoute := range currentSiteMap {
|
||||
activeRoute, ok := activeSiteMap[siteName]
|
||||
if !ok {
|
||||
result.AddedSites = append(result.AddedSites, siteName)
|
||||
continue
|
||||
}
|
||||
if !snapshotRouteConfigEqual(activeRoute, currentRoute) {
|
||||
result.ModifiedSites = append(result.ModifiedSites, siteName)
|
||||
}
|
||||
}
|
||||
for siteName := range activeSiteMap {
|
||||
if _, ok := currentSiteMap[siteName]; !ok {
|
||||
result.RemovedSites = append(result.RemovedSites, siteName)
|
||||
}
|
||||
}
|
||||
currentMap := flattenSnapshotRoutesByDomain(bundle.SnapshotRoutes)
|
||||
activeMap := flattenSnapshotRoutesByDomain(activeSnapshot.Routes)
|
||||
for domain, currentRoute := range currentMap {
|
||||
@@ -260,6 +302,9 @@ func DiffConfigVersion() (*ConfigDiffResult, error) {
|
||||
result.MainConfigChanged = activeVersion.MainConfig != bundle.MainConfig
|
||||
result.ChangedOptionDetails = diffOpenRestyOptionDetails(activeSnapshot.OpenRestyConfig, bundle.OpenRestyConfig)
|
||||
result.ChangedOptionKeys = extractOptionDiffKeys(result.ChangedOptionDetails)
|
||||
sort.Strings(result.AddedSites)
|
||||
sort.Strings(result.RemovedSites)
|
||||
sort.Strings(result.ModifiedSites)
|
||||
sort.Strings(result.AddedDomains)
|
||||
sort.Strings(result.RemovedDomains)
|
||||
sort.Strings(result.ModifiedDomains)
|
||||
@@ -416,21 +461,24 @@ func buildSnapshotRoutes(routes []*model.ProxyRoute) ([]snapshotRoute, error) {
|
||||
return nil, fmt.Errorf("路由 %s 缓存规则无效", route.Domain)
|
||||
}
|
||||
items = append(items, snapshotRoute{
|
||||
SiteName: normalizeProxyRouteSiteNameInput(route, route.SiteName, domains[0]),
|
||||
Domain: domains[0],
|
||||
Domains: domains,
|
||||
OriginURL: route.OriginURL,
|
||||
OriginHost: route.OriginHost,
|
||||
Upstreams: upstreams,
|
||||
Enabled: route.Enabled,
|
||||
EnableHTTPS: route.EnableHTTPS,
|
||||
CertID: route.CertID,
|
||||
RedirectHTTP: route.RedirectHTTP,
|
||||
CacheEnabled: route.CacheEnabled,
|
||||
CachePolicy: route.CachePolicy,
|
||||
CacheRules: cacheRules,
|
||||
CustomHeaders: customHeaders,
|
||||
Remark: route.Remark,
|
||||
SiteName: normalizeProxyRouteSiteNameInput(route, route.SiteName, domains[0]),
|
||||
Domain: domains[0],
|
||||
Domains: domains,
|
||||
OriginURL: route.OriginURL,
|
||||
OriginHost: route.OriginHost,
|
||||
Upstreams: upstreams,
|
||||
Enabled: route.Enabled,
|
||||
EnableHTTPS: route.EnableHTTPS,
|
||||
CertID: route.CertID,
|
||||
RedirectHTTP: route.RedirectHTTP,
|
||||
LimitConnPerServer: route.LimitConnPerServer,
|
||||
LimitConnPerIP: route.LimitConnPerIP,
|
||||
LimitRate: route.LimitRate,
|
||||
CacheEnabled: route.CacheEnabled,
|
||||
CachePolicy: route.CachePolicy,
|
||||
CacheRules: cacheRules,
|
||||
CustomHeaders: customHeaders,
|
||||
Remark: route.Remark,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
@@ -488,10 +536,22 @@ func normalizeSnapshotRoutes(routes []snapshotRoute) []snapshotRoute {
|
||||
routes[index].CachePolicy = normalizeCachePolicy(routes[index].CacheEnabled, routes[index].CachePolicy)
|
||||
routes[index].CacheRules = normalizedCacheRules
|
||||
}
|
||||
normalizedLimitRate, err := normalizeProxyRouteLimitRate(routes[index].LimitRate)
|
||||
if err == nil {
|
||||
routes[index].LimitRate = normalizedLimitRate
|
||||
}
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
func flattenSnapshotRoutesBySite(routes []snapshotRoute) map[string]snapshotRoute {
|
||||
siteMap := make(map[string]snapshotRoute)
|
||||
for _, route := range normalizeSnapshotRoutes(routes) {
|
||||
siteMap[route.SiteName] = route
|
||||
}
|
||||
return siteMap
|
||||
}
|
||||
|
||||
func flattenSnapshotRoutesByDomain(routes []snapshotRoute) map[string]snapshotRoute {
|
||||
domainMap := make(map[string]snapshotRoute)
|
||||
for _, route := range normalizeSnapshotRoutes(routes) {
|
||||
@@ -505,7 +565,7 @@ func flattenSnapshotRoutesByDomain(routes []snapshotRoute) map[string]snapshotRo
|
||||
}
|
||||
|
||||
func snapshotRouteConfigEqual(left snapshotRoute, right snapshotRoute) bool {
|
||||
if left.SiteName != right.SiteName || left.Domain != right.Domain || left.OriginURL != right.OriginURL || left.OriginHost != right.OriginHost || left.EnableHTTPS != right.EnableHTTPS || left.RedirectHTTP != right.RedirectHTTP || left.CacheEnabled != right.CacheEnabled || left.CachePolicy != right.CachePolicy || !uintPointerEqual(left.CertID, right.CertID) {
|
||||
if left.SiteName != right.SiteName || left.Domain != right.Domain || left.OriginURL != right.OriginURL || left.OriginHost != right.OriginHost || left.EnableHTTPS != right.EnableHTTPS || left.RedirectHTTP != right.RedirectHTTP || left.LimitConnPerServer != right.LimitConnPerServer || left.LimitConnPerIP != right.LimitConnPerIP || left.LimitRate != right.LimitRate || left.CacheEnabled != right.CacheEnabled || left.CachePolicy != right.CachePolicy || !uintPointerEqual(left.CertID, right.CertID) {
|
||||
return false
|
||||
}
|
||||
if len(left.Domains) != len(right.Domains) {
|
||||
@@ -719,12 +779,17 @@ func renderRouteConfig(routes []*model.ProxyRoute, cfg openRestyConfigSnapshot)
|
||||
Policy: route.CachePolicy,
|
||||
Rules: cacheRules,
|
||||
}
|
||||
limitConfig := routeLimitConfig{
|
||||
LimitConnPerServer: route.LimitConnPerServer,
|
||||
LimitConnPerIP: route.LimitConnPerIP,
|
||||
LimitRate: route.LimitRate,
|
||||
}
|
||||
upstreamConfig := buildRouteUpstreamConfig(route, upstreams)
|
||||
if upstreamConfig.UsesNamedUpstream {
|
||||
builder.WriteString(renderNamedUpstreamBlock(upstreamConfig))
|
||||
}
|
||||
if !route.EnableHTTPS {
|
||||
builder.WriteString(renderHTTPProxyServer(serverNames, route.OriginURL, route.OriginHost, customHeaders, cacheConfig, upstreamConfig, cfg))
|
||||
builder.WriteString(renderHTTPProxyServer(serverNames, route.OriginURL, route.OriginHost, customHeaders, cacheConfig, limitConfig, upstreamConfig, cfg))
|
||||
continue
|
||||
}
|
||||
if route.CertID == nil || *route.CertID == 0 {
|
||||
@@ -744,9 +809,9 @@ func renderRouteConfig(routes []*model.ProxyRoute, cfg openRestyConfigSnapshot)
|
||||
if route.RedirectHTTP {
|
||||
builder.WriteString(renderHTTPRedirectServer(serverNames))
|
||||
} else {
|
||||
builder.WriteString(renderHTTPProxyServer(serverNames, route.OriginURL, route.OriginHost, customHeaders, cacheConfig, upstreamConfig, cfg))
|
||||
builder.WriteString(renderHTTPProxyServer(serverNames, route.OriginURL, route.OriginHost, customHeaders, cacheConfig, limitConfig, upstreamConfig, cfg))
|
||||
}
|
||||
builder.WriteString(renderHTTPSServer(serverNames, route.OriginURL, route.OriginHost, certificate.ID, customHeaders, cacheConfig, upstreamConfig, cfg))
|
||||
builder.WriteString(renderHTTPSServer(serverNames, route.OriginURL, route.OriginHost, certificate.ID, customHeaders, cacheConfig, limitConfig, upstreamConfig, cfg))
|
||||
}
|
||||
return builder.String(), dedupeSupportFiles(supportFiles), nil
|
||||
}
|
||||
@@ -819,7 +884,8 @@ func renderTemplateDirective(enabled bool, statement string) string {
|
||||
}
|
||||
|
||||
func renderOpenRestyCacheTemplateBlock(cfg openRestyConfigSnapshot) string {
|
||||
lines := make([]string, 0, 8)
|
||||
lines := make([]string, 0, 12)
|
||||
lines = append(lines, renderOpenRestyLimitZoneBlock())
|
||||
if !cfg.CacheEnabled {
|
||||
lines = append(lines, renderOpenRestyObservabilityTemplateBlock())
|
||||
return strings.Join(lines, "")
|
||||
@@ -836,6 +902,14 @@ func renderOpenRestyCacheTemplateBlock(cfg openRestyConfigSnapshot) string {
|
||||
return strings.Join(lines, "")
|
||||
}
|
||||
|
||||
func renderOpenRestyLimitZoneBlock() string {
|
||||
return strings.Join([]string{
|
||||
" limit_conn_zone $server_name zone=openflare_conn_per_server:10m;",
|
||||
" limit_conn_zone $binary_remote_addr zone=openflare_conn_per_ip:10m;",
|
||||
"",
|
||||
}, "\n")
|
||||
}
|
||||
|
||||
func onOff(value bool) string {
|
||||
if value {
|
||||
return "on"
|
||||
@@ -883,18 +957,18 @@ func nextVersionNumber(now time.Time) (string, error) {
|
||||
return fmt.Sprintf("%s-%03d", prefix, count+1), nil
|
||||
}
|
||||
|
||||
func renderHTTPProxyServer(serverNames string, originURL string, originHost string, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, upstreamConfig routeUpstreamConfig, cfg openRestyConfigSnapshot) string {
|
||||
return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n\n location / {\n%s%s%s }\n}\n\n", serverNames, renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig))
|
||||
func renderHTTPProxyServer(serverNames string, originURL string, originHost string, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, upstreamConfig routeUpstreamConfig, cfg openRestyConfigSnapshot) string {
|
||||
return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n\n location / {\n%s%s%s%s }\n}\n\n", serverNames, renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig))
|
||||
}
|
||||
|
||||
func renderHTTPRedirectServer(serverNames string) string {
|
||||
return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n\n return 301 https://$host$request_uri;\n}\n\n", serverNames)
|
||||
}
|
||||
|
||||
func renderHTTPSServer(serverNames string, originURL string, originHost string, certificateID uint, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, upstreamConfig routeUpstreamConfig, cfg openRestyConfigSnapshot) string {
|
||||
func renderHTTPSServer(serverNames string, originURL string, originHost string, certificateID uint, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, upstreamConfig routeUpstreamConfig, cfg openRestyConfigSnapshot) string {
|
||||
certPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateCertFileName(certificateID))
|
||||
keyPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateKeyFileName(certificateID))
|
||||
return fmt.Sprintf("server {\n listen 443 ssl;\n http2 on;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n\n location / {\n%s%s%s }\n}\n\n", serverNames, certPath, keyPath, renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig))
|
||||
return fmt.Sprintf("server {\n listen 443 ssl;\n http2 on;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n\n location / {\n%s%s%s%s }\n}\n\n", serverNames, certPath, keyPath, renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig))
|
||||
}
|
||||
|
||||
func renderServerNames(domains []string) string {
|
||||
@@ -973,6 +1047,23 @@ func renderRouteCacheBlock(cacheConfig routeCacheConfig, cfg openRestyConfigSnap
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func renderRouteLimitBlock(limitConfig routeLimitConfig) string {
|
||||
if limitConfig.LimitConnPerServer <= 0 && limitConfig.LimitConnPerIP <= 0 && strings.TrimSpace(limitConfig.LimitRate) == "" {
|
||||
return ""
|
||||
}
|
||||
var builder strings.Builder
|
||||
if limitConfig.LimitConnPerServer > 0 {
|
||||
builder.WriteString(fmt.Sprintf(" limit_conn openflare_conn_per_server %d;\n", limitConfig.LimitConnPerServer))
|
||||
}
|
||||
if limitConfig.LimitConnPerIP > 0 {
|
||||
builder.WriteString(fmt.Sprintf(" limit_conn openflare_conn_per_ip %d;\n", limitConfig.LimitConnPerIP))
|
||||
}
|
||||
if strings.TrimSpace(limitConfig.LimitRate) != "" {
|
||||
builder.WriteString(fmt.Sprintf(" limit_rate %s;\n", limitConfig.LimitRate))
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func renderRouteCachePolicyCondition(cacheConfig routeCacheConfig) string {
|
||||
switch cacheConfig.Policy {
|
||||
case proxyRouteCachePolicySuffix:
|
||||
|
||||
@@ -110,7 +110,7 @@ func TestCreateProxyRouteRejectsHTTPSWithoutCertificate(t *testing.T) {
|
||||
Enabled: true,
|
||||
EnableHTTPS: true,
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "必须选择证书") {
|
||||
if err == nil || !strings.Contains(err.Error(), "must select a certificate") {
|
||||
t.Fatalf("expected certificate validation error, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -133,8 +133,8 @@ func TestCreateProxyRouteSupportsWebsiteDomains(t *testing.T) {
|
||||
if route.Domain != "app.example.com" {
|
||||
t.Fatalf("expected primary domain mirror, got %s", route.Domain)
|
||||
}
|
||||
if !strings.Contains(route.Domains, "www.example.com") {
|
||||
t.Fatalf("expected domains payload to contain alias, got %s", route.Domains)
|
||||
if len(route.Domains) != 2 || route.Domains[1] != "www.example.com" {
|
||||
t.Fatalf("expected domains payload to contain alias, got %#v", route.Domains)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ func TestCreateProxyRouteRejectsCachePolicyWithoutRules(t *testing.T) {
|
||||
CacheEnabled: true,
|
||||
CachePolicy: proxyRouteCachePolicySuffix,
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "至少填写一个后缀") {
|
||||
if err == nil || !strings.Contains(err.Error(), "at least one suffix") {
|
||||
t.Fatalf("expected cache rule validation error, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -409,6 +409,73 @@ func TestDiffConfigVersionTracksAddedDomainWithinWebsite(t *testing.T) {
|
||||
if len(diff.ModifiedDomains) != 1 || diff.ModifiedDomains[0] != "app.example.com" {
|
||||
t.Fatalf("unexpected modified domains: %#v", diff.ModifiedDomains)
|
||||
}
|
||||
if len(diff.ModifiedSites) != 1 || diff.ModifiedSites[0] != "main-site" {
|
||||
t.Fatalf("unexpected modified sites: %#v", diff.ModifiedSites)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProxyRouteRejectsInvalidRateLimitFields(t *testing.T) {
|
||||
setupServiceTestDB(t)
|
||||
|
||||
_, err := CreateProxyRoute(ProxyRouteInput{
|
||||
Domain: "limit.example.com",
|
||||
OriginURL: "https://origin.internal",
|
||||
Enabled: true,
|
||||
LimitConnPerServer: -1,
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "limit_conn_per_server") {
|
||||
t.Fatalf("expected limit_conn_per_server validation error, got %v", err)
|
||||
}
|
||||
|
||||
_, err = CreateProxyRoute(ProxyRouteInput{
|
||||
Domain: "limit.example.com",
|
||||
OriginURL: "https://origin.internal",
|
||||
Enabled: true,
|
||||
LimitRate: "12x",
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "limit_rate") {
|
||||
t.Fatalf("expected limit_rate validation error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishConfigVersionRendersRouteRateLimits(t *testing.T) {
|
||||
setupServiceTestDB(t)
|
||||
|
||||
_, err := CreateProxyRoute(ProxyRouteInput{
|
||||
SiteName: "limited-site",
|
||||
Domains: []string{"limit.example.com", "www.limit.example.com"},
|
||||
OriginURL: "https://origin.internal",
|
||||
Enabled: true,
|
||||
LimitConnPerServer: 120,
|
||||
LimitConnPerIP: 12,
|
||||
LimitRate: "512K",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProxyRoute failed: %v", err)
|
||||
}
|
||||
|
||||
result, err := PublishConfigVersion("root")
|
||||
if err != nil {
|
||||
t.Fatalf("PublishConfigVersion failed: %v", err)
|
||||
}
|
||||
if !strings.Contains(result.Version.MainConfig, "limit_conn_zone $server_name zone=openflare_conn_per_server:10m;") {
|
||||
t.Fatal("expected main config to include server limit_conn_zone")
|
||||
}
|
||||
if !strings.Contains(result.Version.MainConfig, "limit_conn_zone $binary_remote_addr zone=openflare_conn_per_ip:10m;") {
|
||||
t.Fatal("expected main config to include ip limit_conn_zone")
|
||||
}
|
||||
if !strings.Contains(result.Version.RenderedConfig, "limit_conn openflare_conn_per_server 120;") {
|
||||
t.Fatal("expected rendered config to include per-server limit_conn")
|
||||
}
|
||||
if !strings.Contains(result.Version.RenderedConfig, "limit_conn openflare_conn_per_ip 12;") {
|
||||
t.Fatal("expected rendered config to include per-ip limit_conn")
|
||||
}
|
||||
if !strings.Contains(result.Version.RenderedConfig, "limit_rate 512k;") {
|
||||
t.Fatal("expected rendered config to include normalized limit_rate")
|
||||
}
|
||||
if !strings.Contains(result.Version.SnapshotJSON, `"limit_rate":"512k"`) {
|
||||
t.Fatal("expected snapshot to include normalized limit_rate")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishConfigVersionRendersHostnameLoadBalancingUpstream(t *testing.T) {
|
||||
|
||||
@@ -8,11 +8,13 @@ import (
|
||||
"openflare/model"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var proxyHeaderKeyPattern = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
||||
var proxyRouteLimitRatePattern = regexp.MustCompile(`^\d+(?:[kKmM])?$`)
|
||||
|
||||
const (
|
||||
proxyRouteCachePolicyURL = "url"
|
||||
@@ -27,47 +29,92 @@ type ProxyRouteCustomHeaderInput struct {
|
||||
}
|
||||
|
||||
type ProxyRouteInput struct {
|
||||
SiteName string `json:"site_name"`
|
||||
Domain string `json:"domain"`
|
||||
Domains []string `json:"domains"`
|
||||
OriginID *uint `json:"origin_id"`
|
||||
OriginURL string `json:"origin_url"`
|
||||
OriginScheme string `json:"origin_scheme"`
|
||||
OriginAddress string `json:"origin_address"`
|
||||
OriginPort string `json:"origin_port"`
|
||||
OriginURI string `json:"origin_uri"`
|
||||
OriginHost string `json:"origin_host"`
|
||||
Upstreams []string `json:"upstreams"`
|
||||
Enabled bool `json:"enabled"`
|
||||
EnableHTTPS bool `json:"enable_https"`
|
||||
CertID *uint `json:"cert_id"`
|
||||
RedirectHTTP bool `json:"redirect_http"`
|
||||
CacheEnabled bool `json:"cache_enabled"`
|
||||
CachePolicy string `json:"cache_policy"`
|
||||
CacheRules []string `json:"cache_rules"`
|
||||
CustomHeaders []ProxyRouteCustomHeaderInput `json:"custom_headers"`
|
||||
Remark string `json:"remark"`
|
||||
SiteName string `json:"site_name"`
|
||||
Domain string `json:"domain"`
|
||||
Domains []string `json:"domains"`
|
||||
OriginID *uint `json:"origin_id"`
|
||||
OriginURL string `json:"origin_url"`
|
||||
OriginScheme string `json:"origin_scheme"`
|
||||
OriginAddress string `json:"origin_address"`
|
||||
OriginPort string `json:"origin_port"`
|
||||
OriginURI string `json:"origin_uri"`
|
||||
OriginHost string `json:"origin_host"`
|
||||
Upstreams []string `json:"upstreams"`
|
||||
Enabled bool `json:"enabled"`
|
||||
EnableHTTPS bool `json:"enable_https"`
|
||||
CertID *uint `json:"cert_id"`
|
||||
RedirectHTTP bool `json:"redirect_http"`
|
||||
LimitConnPerServer int `json:"limit_conn_per_server"`
|
||||
LimitConnPerIP int `json:"limit_conn_per_ip"`
|
||||
LimitRate string `json:"limit_rate"`
|
||||
CacheEnabled bool `json:"cache_enabled"`
|
||||
CachePolicy string `json:"cache_policy"`
|
||||
CacheRules []string `json:"cache_rules"`
|
||||
CustomHeaders []ProxyRouteCustomHeaderInput `json:"custom_headers"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
func ListProxyRoutes() ([]*model.ProxyRoute, error) {
|
||||
return model.ListProxyRoutes()
|
||||
type ProxyRouteView struct {
|
||||
ID uint `json:"id"`
|
||||
SiteName string `json:"site_name"`
|
||||
Domain string `json:"domain"`
|
||||
Domains []string `json:"domains"`
|
||||
PrimaryDomain string `json:"primary_domain"`
|
||||
DomainCount int `json:"domain_count"`
|
||||
OriginID *uint `json:"origin_id"`
|
||||
OriginURL string `json:"origin_url"`
|
||||
OriginHost string `json:"origin_host"`
|
||||
Upstreams string `json:"upstreams"`
|
||||
UpstreamList []string `json:"upstream_list"`
|
||||
Enabled bool `json:"enabled"`
|
||||
EnableHTTPS bool `json:"enable_https"`
|
||||
CertID *uint `json:"cert_id"`
|
||||
RedirectHTTP bool `json:"redirect_http"`
|
||||
LimitConnPerServer int `json:"limit_conn_per_server"`
|
||||
LimitConnPerIP int `json:"limit_conn_per_ip"`
|
||||
LimitRate string `json:"limit_rate"`
|
||||
CacheEnabled bool `json:"cache_enabled"`
|
||||
CachePolicy string `json:"cache_policy"`
|
||||
CacheRules string `json:"cache_rules"`
|
||||
CacheRuleList []string `json:"cache_rule_list"`
|
||||
CustomHeaders string `json:"custom_headers"`
|
||||
CustomHeaderList []ProxyRouteCustomHeaderInput `json:"custom_header_list"`
|
||||
Remark string `json:"remark"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func CreateProxyRoute(input ProxyRouteInput) (*model.ProxyRoute, error) {
|
||||
func ListProxyRoutes() ([]*ProxyRouteView, error) {
|
||||
routes, err := model.ListProxyRoutes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buildProxyRouteViews(routes)
|
||||
}
|
||||
|
||||
func GetProxyRoute(id uint) (*ProxyRouteView, error) {
|
||||
route, err := model.GetProxyRouteByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buildProxyRouteView(route)
|
||||
}
|
||||
|
||||
func CreateProxyRoute(input ProxyRouteInput) (*ProxyRouteView, error) {
|
||||
route, err := buildProxyRoute(nil, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = route.Insert(); err != nil {
|
||||
if isUniqueConstraintError(err) {
|
||||
return nil, errors.New("域名已存在")
|
||||
return nil, errors.New("proxy route identity already exists")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return route, nil
|
||||
return buildProxyRouteView(route)
|
||||
}
|
||||
|
||||
func UpdateProxyRoute(id uint, input ProxyRouteInput) (*model.ProxyRoute, error) {
|
||||
func UpdateProxyRoute(id uint, input ProxyRouteInput) (*ProxyRouteView, error) {
|
||||
route, err := model.GetProxyRouteByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -78,11 +125,11 @@ func UpdateProxyRoute(id uint, input ProxyRouteInput) (*model.ProxyRoute, error)
|
||||
}
|
||||
if err = route.Update(); err != nil {
|
||||
if isUniqueConstraintError(err) {
|
||||
return nil, errors.New("域名已存在")
|
||||
return nil, errors.New("proxy route identity already exists")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return route, nil
|
||||
return buildProxyRouteView(route)
|
||||
}
|
||||
|
||||
func DeleteProxyRoute(id uint) error {
|
||||
@@ -120,6 +167,19 @@ func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.Pro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitConnPerServer, err := normalizeProxyRouteLimitConnValue(input.LimitConnPerServer, "limit_conn_per_server")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitConnPerIP, err := normalizeProxyRouteLimitConnValue(input.LimitConnPerIP, "limit_conn_per_ip")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitRate, err := normalizeProxyRouteLimitRate(input.LimitRate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cacheRulesJSON, err := json.Marshal(cacheRules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -152,14 +212,14 @@ func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.Pro
|
||||
}
|
||||
if input.EnableHTTPS {
|
||||
if input.CertID == nil || *input.CertID == 0 {
|
||||
return nil, errors.New("启用 HTTPS 时必须选择证书")
|
||||
return nil, errors.New("must select a certificate when HTTPS is enabled")
|
||||
}
|
||||
if _, err := model.GetTLSCertificateByID(*input.CertID); err != nil {
|
||||
return nil, errors.New("所选证书不存在")
|
||||
return nil, errors.New("selected certificate does not exist")
|
||||
}
|
||||
}
|
||||
if input.RedirectHTTP && !input.EnableHTTPS {
|
||||
return nil, errors.New("仅启用 HTTPS 后才能开启 HTTP 重定向")
|
||||
return nil, errors.New("redirect_http requires enable_https")
|
||||
}
|
||||
|
||||
if route == nil {
|
||||
@@ -176,6 +236,9 @@ func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.Pro
|
||||
route.EnableHTTPS = input.EnableHTTPS
|
||||
route.CertID = input.CertID
|
||||
route.RedirectHTTP = input.RedirectHTTP
|
||||
route.LimitConnPerServer = limitConnPerServer
|
||||
route.LimitConnPerIP = limitConnPerIP
|
||||
route.LimitRate = limitRate
|
||||
route.CacheEnabled = input.CacheEnabled
|
||||
route.CachePolicy = normalizeCachePolicy(input.CacheEnabled, cachePolicy)
|
||||
route.CacheRules = string(cacheRulesJSON)
|
||||
@@ -184,6 +247,70 @@ func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.Pro
|
||||
return route, nil
|
||||
}
|
||||
|
||||
func buildProxyRouteViews(routes []*model.ProxyRoute) ([]*ProxyRouteView, error) {
|
||||
views := make([]*ProxyRouteView, 0, len(routes))
|
||||
for _, route := range routes {
|
||||
view, err := buildProxyRouteView(route)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
views = append(views, view)
|
||||
}
|
||||
return views, nil
|
||||
}
|
||||
|
||||
func buildProxyRouteView(route *model.ProxyRoute) (*ProxyRouteView, error) {
|
||||
if route == nil {
|
||||
return nil, errors.New("proxy route is nil")
|
||||
}
|
||||
domains, err := decodeStoredDomains(route.Domains, route.Domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
upstreams, err := decodeStoredUpstreams(route.Upstreams, route.OriginURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cacheRules, err := decodeStoredCacheRules(route.CacheRules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
customHeaders, err := decodeStoredCustomHeaders(route.CustomHeaders)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
primaryDomain := domains[0]
|
||||
return &ProxyRouteView{
|
||||
ID: route.ID,
|
||||
SiteName: normalizeProxyRouteSiteNameInput(route, route.SiteName, primaryDomain),
|
||||
Domain: primaryDomain,
|
||||
Domains: domains,
|
||||
PrimaryDomain: primaryDomain,
|
||||
DomainCount: len(domains),
|
||||
OriginID: route.OriginID,
|
||||
OriginURL: route.OriginURL,
|
||||
OriginHost: route.OriginHost,
|
||||
Upstreams: route.Upstreams,
|
||||
UpstreamList: upstreams,
|
||||
Enabled: route.Enabled,
|
||||
EnableHTTPS: route.EnableHTTPS,
|
||||
CertID: route.CertID,
|
||||
RedirectHTTP: route.RedirectHTTP,
|
||||
LimitConnPerServer: route.LimitConnPerServer,
|
||||
LimitConnPerIP: route.LimitConnPerIP,
|
||||
LimitRate: route.LimitRate,
|
||||
CacheEnabled: route.CacheEnabled,
|
||||
CachePolicy: route.CachePolicy,
|
||||
CacheRules: route.CacheRules,
|
||||
CacheRuleList: cacheRules,
|
||||
CustomHeaders: route.CustomHeaders,
|
||||
CustomHeaderList: customHeaders,
|
||||
Remark: route.Remark,
|
||||
CreatedAt: route.CreatedAt,
|
||||
UpdatedAt: route.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normalizeProxyRouteSiteNameInput(route *model.ProxyRoute, raw string, primaryDomain string) string {
|
||||
siteName := strings.TrimSpace(raw)
|
||||
if siteName != "" {
|
||||
@@ -234,7 +361,7 @@ func normalizeProxyRouteDomains(rawDomains []string) ([]string, error) {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(domain, "://") || strings.Contains(domain, "/") {
|
||||
return nil, errors.New("域名格式不合法")
|
||||
return nil, errors.New("domain format is invalid")
|
||||
}
|
||||
if _, ok := seen[domain]; ok {
|
||||
continue
|
||||
@@ -243,14 +370,14 @@ func normalizeProxyRouteDomains(rawDomains []string) ([]string, error) {
|
||||
normalized = append(normalized, domain)
|
||||
}
|
||||
if len(normalized) == 0 {
|
||||
return nil, errors.New("至少填写一个域名")
|
||||
return nil, errors.New("at least one domain is required")
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func validateProxyRouteSiteName(siteName string) error {
|
||||
if strings.TrimSpace(siteName) == "" {
|
||||
return errors.New("站点标识不能为空")
|
||||
return errors.New("site_name cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -272,7 +399,7 @@ func validateProxyRouteIdentityUniqueness(route *model.ProxyRoute, siteName stri
|
||||
}
|
||||
existingSiteName := normalizeProxyRouteSiteNameInput(item, item.SiteName, item.Domain)
|
||||
if existingSiteName == siteName {
|
||||
return errors.New("站点标识已存在")
|
||||
return errors.New("site_name already exists")
|
||||
}
|
||||
|
||||
existingDomains, err := decodeStoredDomains(item.Domains, item.Domain)
|
||||
@@ -285,7 +412,7 @@ func validateProxyRouteIdentityUniqueness(route *model.ProxyRoute, siteName stri
|
||||
}
|
||||
for _, domain := range domains {
|
||||
if _, ok := existingSet[domain]; ok {
|
||||
return fmt.Errorf("域名 %s 已存在", domain)
|
||||
return fmt.Errorf("domain %s already exists", domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,6 +420,27 @@ func validateProxyRouteIdentityUniqueness(route *model.ProxyRoute, siteName stri
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeProxyRouteLimitConnValue(value int, field string) (int, error) {
|
||||
if value < 0 {
|
||||
return 0, fmt.Errorf("%s must be greater than or equal to 0", field)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func normalizeProxyRouteLimitRate(raw string) (string, error) {
|
||||
normalized := strings.ToLower(strings.TrimSpace(raw))
|
||||
if normalized == "" || normalized == "0" {
|
||||
return "", nil
|
||||
}
|
||||
if !proxyRouteLimitRatePattern.MatchString(normalized) {
|
||||
return "", errors.New("limit_rate must be a number or use the 512k / 1m format")
|
||||
}
|
||||
if strings.TrimRight(normalized, "km") == "" {
|
||||
return "", nil
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func resolveProxyRoutePrimaryOrigin(input ProxyRouteInput) (string, *uint, error) {
|
||||
if hasStructuredOriginInput(input) {
|
||||
scheme, err := normalizeOriginScheme(input.OriginScheme)
|
||||
@@ -310,7 +458,7 @@ func resolveProxyRoutePrimaryOrigin(input ProxyRouteInput) (string, *uint, error
|
||||
if input.OriginID != nil && *input.OriginID != 0 {
|
||||
origin, err := model.GetOriginByID(*input.OriginID)
|
||||
if err != nil {
|
||||
return "", nil, errors.New("所选源站不存在")
|
||||
return "", nil, errors.New("selected origin does not exist")
|
||||
}
|
||||
originURL, err := buildOriginURLFromParts(
|
||||
scheme,
|
||||
@@ -341,7 +489,7 @@ func resolveProxyRoutePrimaryOrigin(input ProxyRouteInput) (string, *uint, error
|
||||
|
||||
originURL := strings.TrimSpace(input.OriginURL)
|
||||
if originURL == "" {
|
||||
return "", nil, errors.New("源站地址不能为空")
|
||||
return "", nil, errors.New("origin_url cannot be empty")
|
||||
}
|
||||
address, err := extractOriginAddress(originURL)
|
||||
if err != nil {
|
||||
@@ -377,13 +525,13 @@ func normalizeCustomHeaders(headers []ProxyRouteCustomHeaderInput) ([]ProxyRoute
|
||||
continue
|
||||
}
|
||||
if key == "" {
|
||||
return nil, errors.New("自定义请求头名称不能为空")
|
||||
return nil, errors.New("custom header key cannot be empty")
|
||||
}
|
||||
if !proxyHeaderKeyPattern.MatchString(key) {
|
||||
return nil, errors.New("自定义请求头名称格式不合法")
|
||||
return nil, errors.New("custom header key format is invalid")
|
||||
}
|
||||
if strings.ContainsAny(key, "\r\n") || strings.ContainsAny(value, "\r\n") {
|
||||
return nil, errors.New("自定义请求头不能包含换行")
|
||||
return nil, errors.New("custom headers cannot contain newlines")
|
||||
}
|
||||
normalized = append(normalized, ProxyRouteCustomHeaderInput{
|
||||
Key: key,
|
||||
@@ -425,23 +573,23 @@ func normalizeUpstreams(originURL string, upstreams []string) ([]string, error)
|
||||
}
|
||||
parsed, err := url.ParseRequestURI(item)
|
||||
if err != nil {
|
||||
return nil, errors.New("源站地址格式不合法")
|
||||
return nil, errors.New("origin URL format is invalid")
|
||||
}
|
||||
if multiUpstream && parsed.Path != "" && parsed.Path != "/" {
|
||||
return nil, errors.New("多上游模式暂不支持带路径的源站地址")
|
||||
return nil, errors.New("multi-upstream mode does not support origin paths")
|
||||
}
|
||||
if multiUpstream && parsed.RawQuery != "" {
|
||||
return nil, errors.New("多上游模式暂不支持带查询参数的源站地址")
|
||||
return nil, errors.New("multi-upstream mode does not support origin query strings")
|
||||
}
|
||||
if scheme == "" {
|
||||
scheme = parsed.Scheme
|
||||
} else if scheme != parsed.Scheme {
|
||||
return nil, errors.New("同一规则的多个上游必须使用相同协议")
|
||||
return nil, errors.New("all upstreams must use the same scheme")
|
||||
}
|
||||
normalized = append(normalized, item)
|
||||
}
|
||||
if len(normalized) == 0 {
|
||||
return nil, errors.New("至少填写一个上游地址")
|
||||
return nil, errors.New("at least one upstream is required")
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
@@ -453,7 +601,7 @@ func decodeStoredCustomHeaders(raw string) ([]ProxyRouteCustomHeaderInput, error
|
||||
}
|
||||
var headers []ProxyRouteCustomHeaderInput
|
||||
if err := json.Unmarshal([]byte(text), &headers); err != nil {
|
||||
return nil, errors.New("自定义请求头配置格式不合法")
|
||||
return nil, errors.New("custom_headers payload is invalid")
|
||||
}
|
||||
return normalizeCustomHeaders(headers)
|
||||
}
|
||||
@@ -484,7 +632,7 @@ func normalizeCacheRules(enabled bool, rawPolicy string, rules []string) ([]stri
|
||||
case proxyRouteCachePolicyPathExact:
|
||||
return normalizeCachePathRules(rules, false)
|
||||
default:
|
||||
return nil, errors.New("缓存策略不支持")
|
||||
return nil, errors.New("cache policy is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,7 +645,7 @@ func normalizeCacheSuffixRules(rules []string) ([]string, error) {
|
||||
continue
|
||||
}
|
||||
if strings.ContainsAny(item, "/\\ \t\r\n") {
|
||||
return nil, errors.New("缓存后缀格式不合法")
|
||||
return nil, errors.New("cache suffix format is invalid")
|
||||
}
|
||||
if _, ok := seen[item]; ok {
|
||||
continue
|
||||
@@ -506,7 +654,7 @@ func normalizeCacheSuffixRules(rules []string) ([]string, error) {
|
||||
normalized = append(normalized, item)
|
||||
}
|
||||
if len(normalized) == 0 {
|
||||
return nil, errors.New("按后缀缓存时至少填写一个后缀")
|
||||
return nil, errors.New("at least one suffix is required")
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
@@ -520,7 +668,7 @@ func normalizeCachePathRules(rules []string, allowPrefix bool) ([]string, error)
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(item, "/") || strings.Contains(item, "://") || strings.ContainsAny(item, " \t\r\n") {
|
||||
return nil, errors.New("缓存路径规则格式不合法")
|
||||
return nil, errors.New("cache path rule format is invalid")
|
||||
}
|
||||
if !allowPrefix && strings.HasSuffix(item, "/") && len(item) > 1 {
|
||||
item = strings.TrimRight(item, "/")
|
||||
@@ -533,9 +681,9 @@ func normalizeCachePathRules(rules []string, allowPrefix bool) ([]string, error)
|
||||
}
|
||||
if len(normalized) == 0 {
|
||||
if allowPrefix {
|
||||
return nil, errors.New("按路径前缀缓存时至少填写一个路径")
|
||||
return nil, errors.New("at least one path prefix is required")
|
||||
}
|
||||
return nil, errors.New("按精确路径缓存时至少填写一个路径")
|
||||
return nil, errors.New("at least one exact path is required")
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
@@ -547,7 +695,7 @@ func decodeStoredCacheRules(raw string) ([]string, error) {
|
||||
}
|
||||
var rules []string
|
||||
if err := json.Unmarshal([]byte(text), &rules); err != nil {
|
||||
return nil, errors.New("缓存规则格式不合法")
|
||||
return nil, errors.New("cache_rules payload is invalid")
|
||||
}
|
||||
normalized := make([]string, 0, len(rules))
|
||||
for _, rule := range rules {
|
||||
@@ -567,7 +715,7 @@ func decodeStoredUpstreams(raw string, fallbackOriginURL string) ([]string, erro
|
||||
}
|
||||
var upstreams []string
|
||||
if err := json.Unmarshal([]byte(text), &upstreams); err != nil {
|
||||
return nil, errors.New("上游配置格式不合法")
|
||||
return nil, errors.New("upstreams payload is invalid")
|
||||
}
|
||||
return normalizeUpstreams(fallbackOriginURL, upstreams)
|
||||
}
|
||||
@@ -579,24 +727,24 @@ func decodeStoredDomains(raw string, fallbackDomain string) ([]string, error) {
|
||||
}
|
||||
var domains []string
|
||||
if err := json.Unmarshal([]byte(text), &domains); err != nil {
|
||||
return nil, errors.New("域名配置格式不合法")
|
||||
return nil, errors.New("domains payload is invalid")
|
||||
}
|
||||
return normalizeProxyRouteDomains(domains)
|
||||
}
|
||||
|
||||
func validateOriginURL(raw string) error {
|
||||
if raw == "" {
|
||||
return errors.New("源站地址不能为空")
|
||||
return errors.New("origin URL cannot be empty")
|
||||
}
|
||||
parsed, err := url.ParseRequestURI(raw)
|
||||
if err != nil {
|
||||
return errors.New("源站地址格式不合法")
|
||||
return errors.New("origin URL format is invalid")
|
||||
}
|
||||
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||||
return errors.New("源站地址必须以 http:// 或 https:// 开头")
|
||||
return errors.New("origin URL must start with http:// or https://")
|
||||
}
|
||||
if parsed.Host == "" {
|
||||
return errors.New("源站地址格式不合法")
|
||||
return errors.New("origin URL format is invalid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -606,14 +754,14 @@ func validateOriginHost(raw string) error {
|
||||
return nil
|
||||
}
|
||||
if strings.ContainsAny(raw, "/\\ \t\r\n") || strings.Contains(raw, "://") {
|
||||
return errors.New("回源主机名格式不合法")
|
||||
return errors.New("origin_host format is invalid")
|
||||
}
|
||||
parsed, err := url.Parse("//" + raw)
|
||||
if err != nil || parsed.Host == "" || parsed.Host != raw {
|
||||
return errors.New("回源主机名格式不合法")
|
||||
return errors.New("origin_host format is invalid")
|
||||
}
|
||||
if parsed.Hostname() == "" {
|
||||
return errors.New("回源主机名格式不合法")
|
||||
return errors.New("origin_host format is invalid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -27,16 +27,22 @@ export interface ConfigPreviewResult {
|
||||
support_files: SupportFile[];
|
||||
checksum: string;
|
||||
route_count: number;
|
||||
website_count: number;
|
||||
}
|
||||
|
||||
export interface ConfigDiffResult {
|
||||
active_version?: string;
|
||||
added_sites: string[];
|
||||
removed_sites: string[];
|
||||
modified_sites: string[];
|
||||
added_domains: string[];
|
||||
removed_domains: string[];
|
||||
modified_domains: string[];
|
||||
main_config_changed: boolean;
|
||||
changed_option_keys: string[];
|
||||
changed_option_details: ConfigOptionDiffItem[];
|
||||
current_website_count: number;
|
||||
active_website_count: number;
|
||||
}
|
||||
|
||||
export interface ConfigOptionDiffItem {
|
||||
|
||||
@@ -7,12 +7,16 @@ import type {
|
||||
TlsCertificateItem,
|
||||
} from '@/features/proxy-routes/types';
|
||||
|
||||
export function getProxyRoutes() {
|
||||
return apiRequest<ProxyRouteItem[]>('/proxy-routes/');
|
||||
}
|
||||
|
||||
export function createProxyRoute(payload: ProxyRouteMutationPayload) {
|
||||
return apiRequest<ProxyRouteItem>('/proxy-routes/', {
|
||||
export function getProxyRoutes() {
|
||||
return apiRequest<ProxyRouteItem[]>('/proxy-routes/');
|
||||
}
|
||||
|
||||
export function getProxyRoute(id: number) {
|
||||
return apiRequest<ProxyRouteItem>(`/proxy-routes/${id}`);
|
||||
}
|
||||
|
||||
export function createProxyRoute(payload: ProxyRouteMutationPayload) {
|
||||
return apiRequest<ProxyRouteItem>('/proxy-routes/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
@@ -5,26 +5,38 @@ export interface ProxyRouteCustomHeader {
|
||||
|
||||
export interface ProxyRouteItem {
|
||||
id: number;
|
||||
site_name: string;
|
||||
domain: string;
|
||||
domains: string[];
|
||||
primary_domain: string;
|
||||
domain_count: number;
|
||||
origin_id: number | null;
|
||||
origin_url: string;
|
||||
origin_host: string;
|
||||
upstreams: string;
|
||||
upstream_list: string[];
|
||||
enabled: boolean;
|
||||
enable_https: boolean;
|
||||
cert_id: number | null;
|
||||
redirect_http: boolean;
|
||||
limit_conn_per_server: number;
|
||||
limit_conn_per_ip: number;
|
||||
limit_rate: string;
|
||||
cache_enabled: boolean;
|
||||
cache_policy: string;
|
||||
cache_rules: string;
|
||||
cache_rule_list: string[];
|
||||
custom_headers: string;
|
||||
custom_header_list: ProxyRouteCustomHeader[];
|
||||
remark: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ProxyRouteMutationPayload {
|
||||
site_name?: string;
|
||||
domain: string;
|
||||
domains?: string[];
|
||||
origin_id: number | null;
|
||||
origin_url: string;
|
||||
origin_scheme: 'http' | 'https';
|
||||
@@ -37,6 +49,9 @@ export interface ProxyRouteMutationPayload {
|
||||
enable_https: boolean;
|
||||
cert_id: number | null;
|
||||
redirect_http: boolean;
|
||||
limit_conn_per_server?: number;
|
||||
limit_conn_per_ip?: number;
|
||||
limit_rate?: string;
|
||||
cache_enabled: boolean;
|
||||
cache_policy: string;
|
||||
cache_rules: string[];
|
||||
|
||||
Reference in New Issue
Block a user