Files
CHFS-NG/internal/api/review_b_audit_test.go
T

182 lines
5.5 KiB
Go

package api
import (
"bytes"
"encoding/binary"
"hash/crc32"
"image"
"image/color"
"image/png"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"chfs-ng/internal/auth"
"chfs-ng/internal/storage"
)
func TestAuditExistEnforcesReadPermission(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, "secret.txt"), []byte("x"), 0600); err != nil {
t.Fatal(err)
}
s := &Server{Store: storage.Local{Root: root}, Auth: &auth.Manager{Guest: &auth.Account{Mask: auth.None}}}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/chfs/exist?file=secret.txt", nil)
s.exist(rr, req)
if rr.Code != 403 {
t.Fatalf("exist status=%d want 403", rr.Code)
}
}
func TestAuditThumbnailRejectsOversizeBeforeDecode(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, "big.bin"), make([]byte, 64), 0600); err != nil {
t.Fatal(err)
}
s := &Server{Store: storage.Local{Root: root}, Auth: &auth.Manager{Guest: &auth.Account{Mask: auth.Read}}, ImagePreview: true, ThumbnailMaxSize: 32}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/chfs/thumbnail?file=big.bin", nil)
s.thumbnail(rr, req)
if rr.Code != 413 {
t.Fatalf("thumbnail status=%d want 413", rr.Code)
}
}
func TestAuditZipLimitsUseOriginalFileSizes(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, "a.txt"), make([]byte, 100), 0600); err != nil {
t.Fatal(err)
}
s := &Server{Store: storage.Local{Root: root}, ZipMaxSize: 99, ZipMaxFiles: 10}
a := &auth.Account{Mask: auth.Read}
if err := s.checkZipLimits(a, []string{""}); err == nil {
t.Fatal("expected zip size limit")
}
}
func TestAuditSearchLimit(t *testing.T) {
root := t.TempDir()
for _, name := range []string{"a.txt", "b.txt", "c.txt"} {
if err := os.WriteFile(filepath.Join(root, name), []byte(name), 0600); err != nil {
t.Fatal(err)
}
}
s := &Server{Store: storage.Local{Root: root}, Auth: &auth.Manager{Guest: &auth.Account{Mask: auth.Read}}, SearchMaxResults: 2}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/chfs/search?str=.txt", nil)
s.search(rr, req)
if rr.Code != 200 {
t.Fatalf("search status=%d", rr.Code)
}
if len(rr.Body.Bytes()) == 0 || !contains(rr.Body.String(), `"truncated":true`) {
t.Fatalf("search did not report truncation: %s", rr.Body.String())
}
}
func contains(s, sub string) bool {
return len(s) >= len(sub) && (s == sub || func() bool {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
}())
}
func TestAuditThumbnailPixelLimit(t *testing.T) {
root := t.TempDir()
img := image.NewRGBA(image.Rect(0, 0, 20, 20))
img.Set(0, 0, color.White)
f, err := os.Create(filepath.Join(root, "large.png"))
if err != nil {
t.Fatal(err)
}
if err := png.Encode(f, img); err != nil {
f.Close()
t.Fatal(err)
}
f.Close()
s := &Server{Store: storage.Local{Root: root}, Auth: &auth.Manager{Guest: &auth.Account{Mask: auth.Read}}, ImagePreview: true, ThumbnailMaxPixels: 100}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/chfs/thumbnail?file=large.png", nil)
s.thumbnail(rr, req)
if rr.Code != 415 {
t.Fatalf("thumbnail pixel-limit status=%d want 415", rr.Code)
}
}
func TestAuditThumbnailRejectsDeclaredHugeDimensionsBeforeDecode(t *testing.T) {
root := t.TempDir()
f, err := os.Create(filepath.Join(root, "bomb.png"))
if err != nil {
t.Fatal(err)
}
img := image.NewRGBA(image.Rect(0, 0, 1, 1))
if err := png.Encode(f, img); err != nil {
f.Close()
t.Fatal(err)
}
if err := f.Close(); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(filepath.Join(root, "bomb.png"))
if err != nil {
t.Fatal(err)
}
// PNG IHDR starts at byte 16. Replace the 1x1 declaration with a
// deliberately huge image while keeping the file otherwise tiny/valid.
binary.BigEndian.PutUint32(data[16:20], 100000)
binary.BigEndian.PutUint32(data[20:24], 100000)
crc := crc32.ChecksumIEEE(data[12:29])
binary.BigEndian.PutUint32(data[29:33], crc)
if err := os.WriteFile(filepath.Join(root, "bomb.png"), data, 0600); err != nil {
t.Fatal(err)
}
s := &Server{Store: storage.Local{Root: root}, Auth: &auth.Manager{Guest: &auth.Account{Mask: auth.Read}}, ImagePreview: true, ThumbnailMaxPixels: 40_000_000}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/chfs/thumbnail?file=bomb.png", nil)
s.thumbnail(rr, req)
if rr.Code != 415 {
t.Fatalf("thumbnail bomb status=%d want 415", rr.Code)
}
}
func TestAuditUploadMaxBytesReaderBeforeMultipartSpool(t *testing.T) {
for _, tc := range []struct {
name string
handler func(*Server, http.ResponseWriter, *http.Request)
}{
{"upload", (*Server).upload},
{"upload2", (*Server).upload2},
} {
t.Run(tc.name, func(t *testing.T) {
root := t.TempDir()
var body bytes.Buffer
mw := multipart.NewWriter(&body)
part, err := mw.CreateFormFile("file", "oversize.bin")
if err != nil {
t.Fatal(err)
}
if _, err := part.Write(make([]byte, 1024)); err != nil {
t.Fatal(err)
}
if err := mw.Close(); err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodPost, "/chfs/"+tc.name+"?folder=", &body)
req.Header.Set("Content-Type", mw.FormDataContentType())
s := &Server{Store: storage.Local{Root: root}, Auth: &auth.Manager{Guest: &auth.Account{Mask: auth.Write}}, UploadMaxTotalSize: 128}
rr := httptest.NewRecorder()
tc.handler(s, rr, req)
if rr.Code != http.StatusRequestEntityTooLarge {
t.Fatalf("status=%d want 413", rr.Code)
}
})
}
}