[新增] OpenFlare Pages

This commit is contained in:
ryan
2026-06-03 17:31:14 +08:00
parent 2c74d042ed
commit 1088086399
18 changed files with 624 additions and 157 deletions
@@ -105,6 +105,7 @@ type snapshotPagesDeployment struct {
Checksum string `json:"checksum"`
EntryFile string `json:"entry_file"`
SPAFallbackEnabled bool `json:"spa_fallback_enabled"`
SPAFallbackPath string `json:"spa_fallback_path"`
LocalRoot string `json:"local_root"`
}
@@ -624,6 +625,7 @@ func buildSnapshotPagesDeployment(projectID *uint) (*snapshotPagesDeployment, er
Checksum: deployment.Checksum,
EntryFile: deployment.EntryFile,
SPAFallbackEnabled: project.SPAFallbackEnabled,
SPAFallbackPath: normalizeStoredPagesFallbackPath(project.SPAFallbackPath),
LocalRoot: fmt.Sprintf("%s/deployments/%d/current", openrestyrender.PagesDirPlaceholder, deployment.ID),
}, nil
}
+58 -3
View File
@@ -21,9 +21,10 @@ import (
)
const (
pagesMaxDeploymentFiles = 1000
pagesMaxDeploymentBytes = 25 * 1024 * 1024
defaultPagesEntryFile = "index.html"
pagesMaxDeploymentFiles = 1000
pagesMaxDeploymentBytes = 25 * 1024 * 1024
defaultPagesEntryFile = "index.html"
defaultPagesFallbackPath = "/index.html"
)
var pagesSlugPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,126}[a-z0-9]$|^[a-z0-9]$`)
@@ -34,6 +35,7 @@ type PagesProjectInput struct {
Description string `json:"description"`
Enabled bool `json:"enabled"`
SPAFallbackEnabled bool `json:"spa_fallback_enabled"`
SPAFallbackPath string `json:"spa_fallback_path"`
}
type PagesProjectView struct {
@@ -43,6 +45,7 @@ type PagesProjectView struct {
Description string `json:"description"`
Enabled bool `json:"enabled"`
SPAFallbackEnabled bool `json:"spa_fallback_enabled"`
SPAFallbackPath string `json:"spa_fallback_path"`
ActiveDeploymentID *uint `json:"active_deployment_id"`
ActiveDeployment *PagesDeploymentView `json:"active_deployment,omitempty"`
DeploymentCount int64 `json:"deployment_count"`
@@ -133,6 +136,7 @@ func UpdatePagesProject(id uint, input PagesProjectInput) (*PagesProjectView, er
"description": project.Description,
"enabled": project.Enabled,
"spa_fallback_enabled": project.SPAFallbackEnabled,
"spa_fallback_path": project.SPAFallbackPath,
}).Error; err != nil {
if model.IsUniqueConstraintError(err) {
return nil, errors.New("Pages 项目标识已存在")
@@ -404,6 +408,11 @@ func buildPagesProject(project *model.PagesProject, input PagesProjectInput) (*m
project.Description = strings.TrimSpace(input.Description)
project.Enabled = input.Enabled
project.SPAFallbackEnabled = input.SPAFallbackEnabled
fallbackPath, err := normalizePagesFallbackPath(input.SPAFallbackPath)
if err != nil {
return nil, err
}
project.SPAFallbackPath = fallbackPath
return project, nil
}
@@ -418,6 +427,7 @@ func buildPagesProjectView(project *model.PagesProject) (*PagesProjectView, erro
Description: project.Description,
Enabled: project.Enabled,
SPAFallbackEnabled: project.SPAFallbackEnabled,
SPAFallbackPath: normalizeStoredPagesFallbackPath(project.SPAFallbackPath),
ActiveDeploymentID: project.ActiveDeploymentID,
CreatedAt: project.CreatedAt,
UpdatedAt: project.UpdatedAt,
@@ -472,6 +482,51 @@ func normalizePagesSlug(raw string) string {
return strings.Trim(builder.String(), "-")
}
func normalizePagesFallbackPath(raw string) (string, error) {
value := strings.TrimSpace(raw)
if value == "" {
value = defaultPagesFallbackPath
}
if len(value) > 512 {
return "", errors.New("SPA fallback 回退路径长度不能超过 512")
}
if !strings.HasPrefix(value, "/") {
return "", errors.New("SPA fallback 回退路径必须以 / 开头")
}
if value == "/" || strings.HasSuffix(value, "/") {
return "", errors.New("SPA fallback 回退路径必须指向具体文件")
}
if strings.Contains(value, "\\") || strings.ContainsAny(value, "\"';") {
return "", errors.New("SPA fallback 回退路径包含不支持的字符")
}
for _, r := range value {
if r <= 0x20 || r == 0x7f {
return "", errors.New("SPA fallback 回退路径不能包含空白或控制字符")
}
}
for _, segment := range strings.Split(value, "/") {
if segment == "." || segment == ".." {
return "", errors.New("SPA fallback 回退路径不能包含 . 或 .. 路径段")
}
}
cleaned := path.Clean(value)
if cleaned == "." || !strings.HasPrefix(cleaned, "/") {
return "", errors.New("SPA fallback 回退路径不合法")
}
if cleaned == "/" || strings.HasSuffix(cleaned, "/") {
return "", errors.New("SPA fallback 回退路径必须指向具体文件")
}
return cleaned, nil
}
func normalizeStoredPagesFallbackPath(value string) string {
normalized, err := normalizePagesFallbackPath(value)
if err != nil {
return defaultPagesFallbackPath
}
return normalized
}
func normalizePagesEntryFile(raw string) string {
value := path.Clean(strings.TrimSpace(filepath.ToSlash(raw)))
if value == "." || value == "/" {
+20 -1
View File
@@ -18,6 +18,7 @@ func TestPagesUploadActivateAndPublishStaticRoute(t *testing.T) {
Slug: "marketing-site",
Enabled: true,
SPAFallbackEnabled: true,
SPAFallbackPath: "/app.html",
})
if err != nil {
t.Fatalf("CreatePagesProject failed: %v", err)
@@ -68,7 +69,10 @@ func TestPagesUploadActivateAndPublishStaticRoute(t *testing.T) {
if !strings.Contains(result.Version.RenderedConfig, "root \"__OPENFLARE_PAGES_DIR__/deployments/") {
t.Fatalf("expected rendered config to use pages dir placeholder, got:\n%s", result.Version.RenderedConfig)
}
if !strings.Contains(result.Version.RenderedConfig, "try_files $uri $uri/ /index.html;") {
if !strings.Contains(result.Version.SnapshotJSON, `"spa_fallback_path":"/app.html"`) {
t.Fatalf("expected snapshot to include custom SPA fallback path, got %s", result.Version.SnapshotJSON)
}
if !strings.Contains(result.Version.RenderedConfig, "try_files $uri $uri/ /app.html;") {
t.Fatalf("expected SPA fallback try_files, got:\n%s", result.Version.RenderedConfig)
}
if strings.Contains(result.Version.RenderedConfig, "proxy_pass") {
@@ -76,6 +80,21 @@ func TestPagesUploadActivateAndPublishStaticRoute(t *testing.T) {
}
}
func TestPagesProjectRejectsUnsafeFallbackPath(t *testing.T) {
setupServiceTestDB(t)
_, err := CreatePagesProject(PagesProjectInput{
Name: "Unsafe Fallback",
Slug: "unsafe-fallback",
Enabled: true,
SPAFallbackEnabled: true,
SPAFallbackPath: "/index.html; proxy_pass http://evil",
})
if err == nil || !strings.Contains(err.Error(), "回退路径") {
t.Fatalf("expected unsafe SPA fallback path rejection, got %v", err)
}
}
func TestUploadPagesDeploymentRejectsZipSlip(t *testing.T) {
setupServiceTestDB(t)