mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 09:46:37 +08:00
修复 Pages 上传或节点同步时报 pages file size out of bounds:允许 ZIP 包内的 0 字节文件,并兼容未声明解压大小的 ZIP 条目。
This commit is contained in:
@@ -22,6 +22,8 @@ sidebar: false
|
|||||||
|
|
||||||
### 修复
|
### 修复
|
||||||
|
|
||||||
|
- 修复 Pages 上传或节点同步时报 `pages file size out of bounds`:允许 ZIP 包内的 0 字节文件,并兼容未声明解压大小的 ZIP 条目。
|
||||||
|
|
||||||
- 修复节点详情 OpenResty 连接数与吞吐显示为「—」:节点可观测 API 将 OpenResty 观测数据合并进 `metric_snapshots`;指标文案改为「请求/分钟」(近 60 秒窗口),连接数为 0 时正常显示 0。
|
- 修复节点详情 OpenResty 连接数与吞吐显示为「—」:节点可观测 API 将 OpenResty 观测数据合并进 `metric_snapshots`;指标文案改为「请求/分钟」(近 60 秒窗口),连接数为 0 时正常显示 0。
|
||||||
|
|
||||||
- 修复仪表盘「24 小时请求趋势」摘要误显示当前小时请求量/错误量:改为汇总近 24 小时总量。
|
- 修复仪表盘「24 小时请求趋势」摘要误显示当前小时请求量/错误量:改为汇总近 24 小时总量。
|
||||||
|
|||||||
@@ -205,11 +205,19 @@ func extractPagesPackage(packageBytes []byte, releaseDir string, deployment page
|
|||||||
return os.Rename(tmpDir, releaseDir)
|
return os.Rename(tmpDir, releaseDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
func pagesZipEntryCopyLimit(size uint64) (int64, error) {
|
func copyPagesZipEntryContent(dst io.Writer, src io.Reader, declaredSize uint64) (int64, error) {
|
||||||
if size == 0 || size > pagesMaxExtractedFileBytes || size > uint64(math.MaxInt64) {
|
if declaredSize > pagesMaxExtractedFileBytes || declaredSize > uint64(math.MaxInt64) {
|
||||||
return 0, errors.New("pages file size out of bounds")
|
return 0, errors.New("pages file size out of bounds")
|
||||||
}
|
}
|
||||||
return int64(size), nil //nolint:gosec // size is bounded to math.MaxInt64 above
|
if declaredSize > 0 {
|
||||||
|
return io.CopyN(dst, src, int64(declaredSize)) //nolint:gosec // declaredSize is bounded to math.MaxInt64 above
|
||||||
|
}
|
||||||
|
limited := io.LimitReader(src, pagesMaxExtractedFileBytes+1)
|
||||||
|
written, err := io.Copy(dst, limited)
|
||||||
|
if written > pagesMaxExtractedFileBytes {
|
||||||
|
return written, errors.New("pages file size out of bounds")
|
||||||
|
}
|
||||||
|
return written, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractPagesFile(item *zip.File, targetPath string) error {
|
func extractPagesFile(item *zip.File, targetPath string) error {
|
||||||
@@ -226,12 +234,11 @@ func extractPagesFile(item *zip.File, targetPath string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer func() { _ = target.Close() }()
|
defer func() { _ = target.Close() }()
|
||||||
limit, err := pagesZipEntryCopyLimit(item.UncompressedSize64)
|
_, err = copyPagesZipEntryContent(target, source, item.UncompressedSize64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %w", item.Name, err)
|
return fmt.Errorf("%s: %w", item.Name, err)
|
||||||
}
|
}
|
||||||
_, err = io.CopyN(target, source, limit)
|
return nil
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func switchPagesCurrentDir(baseDir string, deploymentID uint, releaseDir string) error {
|
func switchPagesCurrentDir(baseDir string, deploymentID uint, releaseDir string) error {
|
||||||
|
|||||||
@@ -237,6 +237,49 @@ func TestSyncOnceDownloadsPagesDeploymentBeforeApply(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSyncOnceExtractsPagesPackageWithZeroByteFiles(t *testing.T) {
|
||||||
|
packageBytes := testPagesPackage(t, map[string]string{
|
||||||
|
"index.html": "hello",
|
||||||
|
".gitkeep": "",
|
||||||
|
})
|
||||||
|
checksum := testBytesChecksum(packageBytes)
|
||||||
|
client := &fakeClient{
|
||||||
|
config: protocol.ActiveConfigResponse{
|
||||||
|
Version: "20260309-103",
|
||||||
|
Checksum: "pages-config-checksum",
|
||||||
|
SourceConfigJSON: testPagesSourceConfigJSON(9, checksum),
|
||||||
|
CreatedAt: time.Now().Format(time.RFC3339),
|
||||||
|
},
|
||||||
|
pagesPackages: map[uint][]byte{9: packageBytes},
|
||||||
|
}
|
||||||
|
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
|
||||||
|
nodeID, err := stateStore.EnsureNodeID()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EnsureNodeID failed: %v", err)
|
||||||
|
}
|
||||||
|
snapshot, _ := stateStore.Load()
|
||||||
|
snapshot.NodeID = nodeID
|
||||||
|
if err = stateStore.Save(snapshot); err != nil {
|
||||||
|
t.Fatalf("save state failed: %v", err)
|
||||||
|
}
|
||||||
|
manager := &fakeManager{currentChecksum: "old-checksum"}
|
||||||
|
service := New(client, manager, stateStore)
|
||||||
|
pagesDir := t.TempDir()
|
||||||
|
service.SetPagesDir(pagesDir)
|
||||||
|
|
||||||
|
if err = service.SyncOnce(context.Background(), &protocol.ActiveConfigMeta{Version: "20260309-103", Checksum: "pages-config-checksum"}); err != nil {
|
||||||
|
t.Fatalf("SyncOnce failed: %v", err)
|
||||||
|
}
|
||||||
|
gitkeepPath := filepath.Join(pagesDir, "deployments", "9", "current", ".gitkeep")
|
||||||
|
info, err := os.Stat(gitkeepPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected zero-byte Pages file to be extracted: %v", err)
|
||||||
|
}
|
||||||
|
if info.Size() != 0 {
|
||||||
|
t.Fatalf("expected zero-byte Pages file, got %d bytes", info.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSyncOnceRejectsPagesZipSlipBeforeApply(t *testing.T) {
|
func TestSyncOnceRejectsPagesZipSlipBeforeApply(t *testing.T) {
|
||||||
packageBytes := testPagesPackage(t, map[string]string{"../escape.html": "bad", "index.html": "ok"})
|
packageBytes := testPagesPackage(t, map[string]string{"../escape.html": "bad", "index.html": "ok"})
|
||||||
checksum := testBytesChecksum(packageBytes)
|
checksum := testBytesChecksum(packageBytes)
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ const (
|
|||||||
pagesLegacyArtifactCandidateCapacity = 8
|
pagesLegacyArtifactCandidateCapacity = 8
|
||||||
pagesLegacyArtifactRootCapacity = 4
|
pagesLegacyArtifactRootCapacity = 4
|
||||||
pagesMaxDeploymentFiles = 1000
|
pagesMaxDeploymentFiles = 1000
|
||||||
pagesMaxDeploymentBytes = 100 * 1024 * 1024
|
pagesMaxDeploymentBytes = 100 * 1024 * 1024
|
||||||
defaultPagesEntryFile = "index.html"
|
defaultPagesEntryFile = "index.html"
|
||||||
defaultPagesFallbackPath = "/index.html"
|
defaultPagesFallbackPath = "/index.html"
|
||||||
pagesDeploymentUploadType = "openflare_pages_deployment"
|
pagesDeploymentUploadType = "openflare_pages_deployment"
|
||||||
mimeTypeApplicationZip = "application/zip"
|
mimeTypeApplicationZip = "application/zip"
|
||||||
pagesMaxPathLength = 512
|
pagesMaxPathLength = 512
|
||||||
bytesPerKiB = 1024
|
bytesPerKiB = 1024
|
||||||
)
|
)
|
||||||
|
|
||||||
var pagesSlugPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,126}[a-z0-9]$|^[a-z0-9]$`)
|
var pagesSlugPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,126}[a-z0-9]$|^[a-z0-9]$`)
|
||||||
@@ -397,20 +397,20 @@ func inspectPagesZip(zipPath string, rootDir string, entryFile string) (*deploym
|
|||||||
if manifest.FileCount > pagesMaxDeploymentFiles {
|
if manifest.FileCount > pagesMaxDeploymentFiles {
|
||||||
return nil, fmt.Errorf("pages 部署文件数不能超过 %d", pagesMaxDeploymentFiles)
|
return nil, fmt.Errorf("pages 部署文件数不能超过 %d", pagesMaxDeploymentFiles)
|
||||||
}
|
}
|
||||||
manifest.TotalSize += int64(item.UncompressedSize64)
|
checksum, fileSize, err := checksumZipFile(item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %w", normalizedPath, err)
|
||||||
|
}
|
||||||
|
manifest.TotalSize += fileSize
|
||||||
if manifest.TotalSize > pagesMaxDeploymentBytes {
|
if manifest.TotalSize > pagesMaxDeploymentBytes {
|
||||||
return nil, fmt.Errorf("pages 部署展开后不能超过 %d MiB", pagesMaxDeploymentBytes/bytesPerKiB/bytesPerKiB)
|
return nil, fmt.Errorf("pages 部署展开后不能超过 %d MiB", pagesMaxDeploymentBytes/bytesPerKiB/bytesPerKiB)
|
||||||
}
|
}
|
||||||
checksum, err := checksumZipFile(item)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if normalizedPath == targetEntryPath {
|
if normalizedPath == targetEntryPath {
|
||||||
entrySeen = true
|
entrySeen = true
|
||||||
}
|
}
|
||||||
manifest.Files = append(manifest.Files, model.PagesDeploymentFile{
|
manifest.Files = append(manifest.Files, model.PagesDeploymentFile{
|
||||||
Path: normalizedPath,
|
Path: normalizedPath,
|
||||||
Size: int64(item.UncompressedSize64),
|
Size: fileSize,
|
||||||
Checksum: checksum,
|
Checksum: checksum,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -444,26 +444,31 @@ func normalizePagesZipPath(raw string) (string, bool, error) {
|
|||||||
return cleaned, false, nil
|
return cleaned, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func pagesZipEntryCopyLimit(size uint64) (int64, error) {
|
func copyPagesZipEntryContent(dst io.Writer, src io.Reader, declaredSize uint64) (int64, error) {
|
||||||
if size == 0 || size > pagesMaxDeploymentBytes || size > uint64(math.MaxInt64) {
|
if declaredSize > pagesMaxDeploymentBytes || declaredSize > uint64(math.MaxInt64) {
|
||||||
return 0, errors.New("pages file size out of bounds")
|
return 0, errors.New("pages file size out of bounds")
|
||||||
}
|
}
|
||||||
return int64(size), nil //nolint:gosec // size is bounded to math.MaxInt64 above
|
if declaredSize > 0 {
|
||||||
|
return io.CopyN(dst, src, int64(declaredSize)) //nolint:gosec // declaredSize is bounded to math.MaxInt64 above
|
||||||
|
}
|
||||||
|
limited := io.LimitReader(src, pagesMaxDeploymentBytes+1)
|
||||||
|
written, err := io.Copy(dst, limited)
|
||||||
|
if written > pagesMaxDeploymentBytes {
|
||||||
|
return written, errors.New("pages file size out of bounds")
|
||||||
|
}
|
||||||
|
return written, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func checksumZipFile(item *zip.File) (string, error) {
|
func checksumZipFile(item *zip.File) (string, int64, error) {
|
||||||
file, err := item.Open()
|
file, err := item.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", 0, err
|
||||||
}
|
}
|
||||||
defer func() { _ = file.Close() }()
|
defer func() { _ = file.Close() }()
|
||||||
hash := sha256.New()
|
hash := sha256.New()
|
||||||
limit, err := pagesZipEntryCopyLimit(item.UncompressedSize64)
|
written, err := copyPagesZipEntryContent(hash, file, item.UncompressedSize64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", written, err
|
||||||
}
|
}
|
||||||
if _, err = io.CopyN(hash, file, limit); err != nil {
|
return hex.EncodeToString(hash.Sum(nil)), written, nil
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return hex.EncodeToString(hash.Sum(nil)), nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,6 +142,28 @@ func TestCreateProjectRejectsUnsafeFallbackPath(t *testing.T) {
|
|||||||
assert.Contains(t, err.Error(), "回退路径")
|
assert.Contains(t, err.Error(), "回退路径")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUploadDeploymentAcceptsZeroByteFiles(t *testing.T) {
|
||||||
|
cleanup := setupPagesTestDB(t)
|
||||||
|
defer cleanup()
|
||||||
|
_, disableStorage := setupPagesStorageMock(t)
|
||||||
|
defer disableStorage()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
project, err := CreateProject(ctx, Input{
|
||||||
|
Name: "Zero Byte Site",
|
||||||
|
Slug: "zero-byte-site",
|
||||||
|
Enabled: true,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
deployment, err := UploadDeployment(ctx, project.ID, testPagesMultipartFile(t, "site.zip", testPagesZip(t, map[string]string{
|
||||||
|
"index.html": "ok",
|
||||||
|
".gitkeep": "",
|
||||||
|
})), "root")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 2, deployment.FileCount)
|
||||||
|
}
|
||||||
|
|
||||||
func TestUploadDeploymentStoresPackageInUploadFramework(t *testing.T) {
|
func TestUploadDeploymentStoresPackageInUploadFramework(t *testing.T) {
|
||||||
cleanup := setupPagesTestDB(t)
|
cleanup := setupPagesTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|||||||
Reference in New Issue
Block a user