diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index 3ca7452e1..bd9df8223 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -768,6 +768,7 @@ func calculateEtag(d os.FileInfo) string { var sb strings.Builder sb.WriteRune('"') sb.WriteString(strconv.FormatInt(mtime.UnixNano(), 36)) + sb.WriteRune('-') sb.WriteString(strconv.FormatInt(d.Size(), 36)) sb.WriteRune('"') return sb.String() diff --git a/modules/caddyhttp/fileserver/staticfiles_test.go b/modules/caddyhttp/fileserver/staticfiles_test.go index a5f314ffc..a077a2011 100644 --- a/modules/caddyhttp/fileserver/staticfiles_test.go +++ b/modules/caddyhttp/fileserver/staticfiles_test.go @@ -189,6 +189,34 @@ func check_validator_headers(modTime time.Time, expect_headers bool, t *testing. } } +// calculateEtag concatenates the base-36 mtime and size with no separator, +// so distinct (mtime, size) pairs can produce the same digit string and +// therefore the same ETag. +func TestCalculateEtagCollision(t *testing.T) { + fileA := fakeFileInfo{size: 75, modTime: time.Unix(2, 0)} + fileB := fakeFileInfo{size: 3, modTime: time.Unix(72, 2)} + + etagA := calculateEtag(fileA) + etagB := calculateEtag(fileB) + + if etagA == etagB { + t.Fatalf("etag collision: distinct files (size=%d mtime=%s) and (size=%d mtime=%s) both produced ETag %s", + fileA.size, fileA.modTime, fileB.size, fileB.modTime, etagA) + } +} + +type fakeFileInfo struct { + size int64 + modTime time.Time +} + +func (f fakeFileInfo) Name() string { return "fake" } +func (f fakeFileInfo) Size() int64 { return f.size } +func (f fakeFileInfo) Mode() os.FileMode { return 0 } +func (f fakeFileInfo) ModTime() time.Time { return f.modTime } +func (f fakeFileInfo) IsDir() bool { return false } +func (f fakeFileInfo) Sys() any { return nil } + func TestPrecompressedRangeResponse(t *testing.T) { root := t.TempDir() if err := os.WriteFile(filepath.Join(root, "range.txt"), []byte("original response body"), 0o600); err != nil {