Files
fasthttp/fs_fs_test.go
T

1235 lines
32 KiB
Go

package fasthttp
import (
"bufio"
"bytes"
"embed"
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"testing/fstest"
"time"
)
//go:embed fasthttputil fs.go README.md testdata examples
var fsTestFilesystem embed.FS
func TestFSServeFileHead(t *testing.T) {
t.Parallel()
var ctx RequestCtx
var req Request
req.Header.SetMethod(MethodHead)
req.SetRequestURI("http://foobar.com/baz")
ctx.Init(&req, nil, nil)
ServeFS(&ctx, fsTestFilesystem, "fs.go")
var resp Response
resp.SkipBody = true
s := ctx.Response.String()
br := bufio.NewReader(bytes.NewBufferString(s))
if err := resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
ce := resp.Header.ContentEncoding()
if len(ce) > 0 {
t.Fatalf("Unexpected 'Content-Encoding' %q", ce)
}
body := resp.Body()
if len(body) > 0 {
t.Fatalf("unexpected response body %q. Expecting empty body", body)
}
expectedBody, err := getFileContents("/fs.go")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
contentLength := resp.Header.ContentLength()
if contentLength != len(expectedBody) {
t.Fatalf("unexpected Content-Length: %d. expecting %d", contentLength, len(expectedBody))
}
}
func TestServeFSDoesNotLeakCacheCleaner(t *testing.T) {
testFS := fstest.MapFS{
"index.txt": {Data: []byte("body")},
}
runtime.GC()
before := runtime.NumGoroutine()
const calls = 25
for range calls {
var ctx RequestCtx
var req Request
req.SetRequestURI("http://foobar.com/original")
ctx.Init(&req, nil, TestLogger{t})
ServeFS(&ctx, testFS, "index.txt")
if ctx.Response.StatusCode() != StatusOK {
t.Fatalf("unexpected status code %d. expecting %d", ctx.Response.StatusCode(), StatusOK)
}
}
deadline := time.Now().Add(3 * time.Second)
for time.Now().Before(deadline) {
runtime.GC()
runtime.Gosched()
after := runtime.NumGoroutine()
if leaked := after - before; leaked <= 3 {
return
}
time.Sleep(10 * time.Millisecond)
}
after := runtime.NumGoroutine()
if leaked := after - before; leaked > 3 {
t.Fatalf("ServeFS left %d persistent goroutines; expected no cache cleaner goroutine leak", leaked)
}
}
func TestServeFSLiteral(t *testing.T) {
t.Parallel()
testFS := fstest.MapFS{
"space name.txt": {Data: []byte("space")},
"hash#name.txt": {Data: []byte("hash")},
"percent%61name.txt": {Data: []byte("percent")},
"query?name.txt": {Data: []byte("query")},
}
for name, file := range testFS {
t.Run(name, func(t *testing.T) {
var ctx RequestCtx
var req Request
req.SetRequestURI("http://foobar.com/original")
ctx.Init(&req, nil, nil)
ServeFSLiteral(&ctx, testFS, name)
resp := readResponseFromCtx(t, &ctx, false)
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code %d. expecting %d", resp.StatusCode(), StatusOK)
}
if !bytes.Equal(resp.Body(), file.Data) {
t.Fatalf("unexpected body %q. expecting %q", resp.Body(), file.Data)
}
})
}
}
func TestServeFSSpecialCharsNotLiteral(t *testing.T) {
t.Parallel()
testFS := fstest.MapFS{
"hash#name.txt": {Data: []byte("hash")},
"percent%61name.txt": {Data: []byte("percent")},
"query?name.txt": {Data: []byte("query")},
}
for name := range testFS {
t.Run(name, func(t *testing.T) {
var ctx RequestCtx
var req Request
req.SetRequestURI("http://foobar.com/original")
ctx.Init(&req, nil, TestLogger{t})
ServeFS(&ctx, testFS, name)
resp := readResponseFromCtx(t, &ctx, false)
if resp.StatusCode() == StatusOK {
t.Fatalf("ServeFS should fail for special-character path %q without literal mode, but got 200", name)
}
})
}
}
func TestFSServeFileCompressed(t *testing.T) {
t.Parallel()
var ctx RequestCtx
ctx.Init(&Request{}, nil, nil)
var resp Response
expectedBody, err := getFileContents("/fs.go")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// should prefer brotli over zstd, gzip and ignore unknown encoding
ctx.Request.SetRequestURI("http://foobar.com/baz")
ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, br, wompwomp")
ServeFS(&ctx, fsTestFilesystem, "fs.go")
s := ctx.Response.String()
br := bufio.NewReader(bytes.NewBufferString(s))
if err = resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
}
ce := resp.Header.ContentEncoding()
if string(ce) != "br" {
t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "br")
}
vary := resp.Header.PeekBytes(strVary)
if !bytes.Equal(vary, strAcceptEncoding) {
t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
}
body, err := resp.BodyUnbrotli()
if err != nil {
t.Fatalf("unexpected error on unbrotli response body: %v", err)
}
if !bytes.Equal(body, expectedBody) {
t.Fatalf("unexpected body: len=%d. Expected len=%d", len(body), len(expectedBody))
}
// should prefer zstd over gzip and ignore unknown encoding
ctx.Request.Reset()
ctx.Request.SetRequestURI("http://foobar.com/baz")
ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, wompwomp")
ServeFS(&ctx, fsTestFilesystem, "fs.go")
s = ctx.Response.String()
br = bufio.NewReader(bytes.NewBufferString(s))
if err = resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
}
ce = resp.Header.ContentEncoding()
if string(ce) != "zstd" {
t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "zstd")
}
vary = resp.Header.PeekBytes(strVary)
if !bytes.Equal(vary, strAcceptEncoding) {
t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
}
body, err = resp.BodyUnzstd()
if err != nil {
t.Fatalf("unexpected error on unzstd response body: %v", err)
}
if !bytes.Equal(body, expectedBody) {
t.Fatalf("unexpected body: len=%d. Expected len=%d", len(body), len(expectedBody))
}
// should prefer gzip and ignore unknown encoding
ctx.Request.Reset()
ctx.Request.SetRequestURI("http://foobar.com/baz")
ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, wompwomp")
ServeFS(&ctx, fsTestFilesystem, "fs.go")
s = ctx.Response.String()
br = bufio.NewReader(bytes.NewBufferString(s))
if err = resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
}
ce = resp.Header.ContentEncoding()
if string(ce) != "gzip" {
t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "gzip")
}
vary = resp.Header.PeekBytes(strVary)
if !bytes.Equal(vary, strAcceptEncoding) {
t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
}
body, err = resp.BodyGunzip()
if err != nil {
t.Fatalf("unexpected error on gunzip response body: %v", err)
}
if !bytes.Equal(body, expectedBody) {
t.Fatalf("unexpected body: len=%d. Expected len=%d", len(body), len(expectedBody))
}
}
func TestFSServeFileUncompressed(t *testing.T) {
t.Parallel()
var ctx RequestCtx
ctx.Init(&Request{}, nil, nil)
var resp Response
expectedBody, err := getFileContents("/fs.go")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ctx.Request.SetRequestURI("http://foobar.com/baz")
ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, br, wompwomp")
ServeFileUncompressed(&ctx, "fs.go")
s := ctx.Response.String()
br := bufio.NewReader(bytes.NewBufferString(s))
if err = resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
}
ce := resp.Header.ContentEncoding()
if len(ce) > 0 {
t.Fatalf("unexpected 'Content-Encoding': %q. Expecting \"\"", string(ce))
}
vary := resp.Header.PeekBytes(strVary)
if len(vary) > 0 {
t.Fatalf("unexpected 'Vary': %q. Expecting \"\"", string(vary))
}
body := resp.Body()
if !bytes.Equal(body, expectedBody) {
t.Fatalf("unexpected body: len=%d. Expecting len=%d", len(body), len(expectedBody))
}
}
func TestFSFSByteRangeConcurrent(t *testing.T) {
t.Parallel()
stop := make(chan struct{})
defer close(stop)
fs := &FS{
FS: fsTestFilesystem,
Root: "",
AcceptByteRange: true,
CleanStop: stop,
}
h := fs.NewRequestHandler()
concurrency := 10
ch := make(chan struct{}, concurrency)
for range concurrency {
go func() {
for range 5 {
testFSByteRange(t, h, "/fs.go")
testFSByteRange(t, h, "/README.md")
}
ch <- struct{}{}
}()
}
for range concurrency {
select {
case <-time.After(time.Second):
t.Fatalf("timeout")
case <-ch:
}
}
}
func TestFSFSByteRangeSingleThread(t *testing.T) {
t.Parallel()
stop := make(chan struct{})
defer close(stop)
fs := &FS{
FS: fsTestFilesystem,
Root: ".",
AcceptByteRange: true,
CleanStop: stop,
}
h := fs.NewRequestHandler()
testFSByteRange(t, h, "/fs.go")
testFSByteRange(t, h, "/README.md")
}
func TestFSFSCompressConcurrent(t *testing.T) {
t.Parallel()
// go 1.16 timeout may occur
if strings.HasPrefix(runtime.Version(), "go1.16") {
t.SkipNow()
}
stop := make(chan struct{})
defer close(stop)
fs := &FS{
FS: fsTestFilesystem,
Root: ".",
GenerateIndexPages: true,
Compress: true,
CompressBrotli: true,
CompressZstd: true,
CleanStop: stop,
}
h := fs.NewRequestHandler()
concurrency := 4
ch := make(chan struct{}, concurrency)
for range concurrency {
go func() {
for range 5 {
testFSFSCompress(t, h, "/fs.go")
testFSFSCompress(t, h, "/examples/")
testFSFSCompress(t, h, "/README.md")
}
ch <- struct{}{}
}()
}
for range concurrency {
select {
case <-ch:
case <-time.After(time.Second * 4):
t.Fatalf("timeout")
}
}
}
func TestFSFSCompressSingleThread(t *testing.T) {
t.Parallel()
stop := make(chan struct{})
defer close(stop)
fs := &FS{
FS: fsTestFilesystem,
Root: ".",
GenerateIndexPages: true,
Compress: true,
CompressBrotli: true,
CompressZstd: true,
CleanStop: stop,
}
h := fs.NewRequestHandler()
testFSFSCompress(t, h, "/fs.go")
testFSFSCompress(t, h, "/examples/")
testFSFSCompress(t, h, "/README.md")
}
func testFSFSCompress(t *testing.T, h RequestHandler, filePath string) {
var ctx RequestCtx
ctx.Init(&Request{}, nil, nil)
var resp Response
// get uncompressed
ctx.Request.SetRequestURI(filePath)
h(&ctx)
s := ctx.Response.String()
br := bufio.NewReader(bytes.NewBufferString(s))
if err := resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
}
ce := resp.Header.ContentEncoding()
if len(ce) > 0 {
t.Fatalf("unexpected 'Content-Encoding': %q. Expecting \"\"", string(ce))
}
vary := resp.Header.PeekBytes(strVary)
if len(vary) > 0 {
t.Fatalf("unexpected 'Vary': %q. Expecting \"\"", string(vary))
}
expectedBody := bytes.Clone(resp.Body())
// should prefer brotli over zstd, gzip and ignore unknown encoding
ctx.Request.Reset()
ctx.Request.SetRequestURI(filePath)
ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, br, wompwomp")
h(&ctx)
s = ctx.Response.String()
br = bufio.NewReader(bytes.NewBufferString(s))
if err := resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v. filePath=%q", err, filePath)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
}
ce = resp.Header.ContentEncoding()
if string(ce) != "br" {
t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q. filePath=%q", string(ce), "br", filePath)
}
vary = resp.Header.PeekBytes(strVary)
if !bytes.Equal(vary, strAcceptEncoding) {
t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
}
body, err := resp.BodyUnbrotli()
if err != nil {
t.Fatalf("unexpected error on unbrotli response body: %v. filePath=%q", err, filePath)
}
if !bytes.Equal(body, expectedBody) {
t.Fatalf("unexpected body: len=%d. Expecting len=%d. filePath=%q", len(body), len(expectedBody), filePath)
}
// should prefer zstd over gzip and ignore unknown encoding
ctx.Request.Reset()
ctx.Request.SetRequestURI(filePath)
ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, wompwomp")
h(&ctx)
s = ctx.Response.String()
br = bufio.NewReader(bytes.NewBufferString(s))
if err = resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v. filePath=%q", err, filePath)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
}
ce = resp.Header.ContentEncoding()
if string(ce) != "zstd" {
t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q. filePath=%q", string(ce), "zstd", filePath)
}
vary = resp.Header.PeekBytes(strVary)
if !bytes.Equal(vary, strAcceptEncoding) {
t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
}
body, err = resp.BodyUnzstd()
if err != nil {
t.Fatalf("unexpected error on unzstd response body: %v. filePath=%q", err, filePath)
}
if !bytes.Equal(body, expectedBody) {
t.Fatalf("unexpected body: len=%d. Expecting len=%d. filePath=%q", len(body), len(expectedBody), filePath)
}
// should prefer gzip and ignore unknown encoding
ctx.Request.Reset()
ctx.Request.SetRequestURI(filePath)
ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, wompwomp")
h(&ctx)
s = ctx.Response.String()
br = bufio.NewReader(bytes.NewBufferString(s))
if err = resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v. filePath=%q", err, filePath)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
}
ce = resp.Header.ContentEncoding()
if string(ce) != "gzip" {
t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q. filePath=%q", string(ce), "gzip", filePath)
}
vary = resp.Header.PeekBytes(strVary)
if !bytes.Equal(vary, strAcceptEncoding) {
t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
}
body, err = resp.BodyGunzip()
if err != nil {
t.Fatalf("unexpected error on gunzip response body: %v. filePath=%q", err, filePath)
}
if !bytes.Equal(body, expectedBody) {
t.Fatalf("unexpected body: len=%d. Expecting len=%d. filePath=%q", len(body), len(expectedBody), filePath)
}
}
func TestFSServeFileContentType(t *testing.T) {
t.Parallel()
var ctx RequestCtx
var req Request
req.Header.SetMethod(MethodGet)
req.SetRequestURI("http://foobar.com/baz")
ctx.Init(&req, nil, nil)
ServeFS(&ctx, fsTestFilesystem, "testdata/test.png")
var resp Response
s := ctx.Response.String()
br := bufio.NewReader(bytes.NewBufferString(s))
if err := resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := []byte("image/png")
if !bytes.Equal(resp.Header.ContentType(), expected) {
t.Fatalf("Unexpected Content-Type, expected: %q got %q", expected, resp.Header.ContentType())
}
}
func TestFSServeFileDirectoryRedirect(t *testing.T) {
t.Parallel()
var ctx RequestCtx
var req Request
req.SetRequestURI("http://foobar.com")
ctx.Init(&req, nil, nil)
ctx.Request.Reset()
ctx.Response.Reset()
ServeFS(&ctx, fsTestFilesystem, "fasthttputil")
if ctx.Response.StatusCode() != StatusFound {
t.Fatalf("Unexpected status code %d for directory '/fasthttputil' without trailing slash. Expecting %d.", ctx.Response.StatusCode(), StatusFound)
}
ctx.Request.Reset()
ctx.Response.Reset()
ServeFS(&ctx, fsTestFilesystem, "fasthttputil/")
if ctx.Response.StatusCode() != StatusOK {
t.Fatalf("Unexpected status code %d for directory '/fasthttputil/' with trailing slash. Expecting %d.", ctx.Response.StatusCode(), StatusOK)
}
ctx.Request.Reset()
ctx.Response.Reset()
ServeFS(&ctx, fsTestFilesystem, "fs.go")
if ctx.Response.StatusCode() != StatusOK {
t.Fatalf("Unexpected status code %d for file '/fs.go'. Expecting %d.", ctx.Response.StatusCode(), StatusOK)
}
}
var dirTestFilesystem = os.DirFS(".")
func TestDirFSServeFileHead(t *testing.T) {
t.Parallel()
var ctx RequestCtx
var req Request
req.Header.SetMethod(MethodHead)
req.SetRequestURI("http://foobar.com/baz")
ctx.Init(&req, nil, nil)
ServeFS(&ctx, dirTestFilesystem, "fs.go")
var resp Response
resp.SkipBody = true
s := ctx.Response.String()
br := bufio.NewReader(bytes.NewBufferString(s))
if err := resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
ce := resp.Header.ContentEncoding()
if len(ce) > 0 {
t.Fatalf("Unexpected 'Content-Encoding' %q", ce)
}
body := resp.Body()
if len(body) > 0 {
t.Fatalf("unexpected response body %q. Expecting empty body", body)
}
expectedBody, err := getFileContents("/fs.go")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
contentLength := resp.Header.ContentLength()
if contentLength != len(expectedBody) {
t.Fatalf("unexpected Content-Length: %d. expecting %d", contentLength, len(expectedBody))
}
}
func TestDirFSServeFileCompressed(t *testing.T) {
t.Parallel()
var ctx RequestCtx
ctx.Init(&Request{}, nil, nil)
var resp Response
expectedBody, err := getFileContents("/fs.go")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// should prefer brotli over zstd, gzip and ignore unknown encoding
ctx.Request.SetRequestURI("http://foobar.com/baz")
ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, br, wompwomp")
ServeFS(&ctx, dirTestFilesystem, "fs.go")
s := ctx.Response.String()
br := bufio.NewReader(bytes.NewBufferString(s))
if err = resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
}
ce := resp.Header.ContentEncoding()
if string(ce) != "br" {
t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "br")
}
vary := resp.Header.PeekBytes(strVary)
if !bytes.Equal(vary, strAcceptEncoding) {
t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
}
body, err := resp.BodyUnbrotli()
if err != nil {
t.Fatalf("unexpected error on unbrotli response body: %v", err)
}
if !bytes.Equal(body, expectedBody) {
t.Fatalf("unexpected body: len=%d. Expecting len=%d", len(body), len(expectedBody))
}
// should prefer zstd over gzip and ignore unknown encoding
ctx.Request.Reset()
ctx.Request.SetRequestURI("http://foobar.com/baz")
ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, wompwomp")
ServeFS(&ctx, dirTestFilesystem, "fs.go")
s = ctx.Response.String()
br = bufio.NewReader(bytes.NewBufferString(s))
if err = resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
}
ce = resp.Header.ContentEncoding()
if string(ce) != "zstd" {
t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "zstd")
}
vary = resp.Header.PeekBytes(strVary)
if !bytes.Equal(vary, strAcceptEncoding) {
t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
}
body, err = resp.BodyUnzstd()
if err != nil {
t.Fatalf("unexpected error on unzstd response body: %v", err)
}
if !bytes.Equal(body, expectedBody) {
t.Fatalf("unexpected body: len=%d. Expecting len=%d", len(body), len(expectedBody))
}
// should prefer gzip and ignore unknown encoding
ctx.Request.Reset()
ctx.Request.SetRequestURI("http://foobar.com/baz")
ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, wompwomp")
ServeFS(&ctx, dirTestFilesystem, "fs.go")
s = ctx.Response.String()
br = bufio.NewReader(bytes.NewBufferString(s))
if err = resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
}
ce = resp.Header.ContentEncoding()
if string(ce) != "gzip" {
t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "gzip")
}
vary = resp.Header.PeekBytes(strVary)
if !bytes.Equal(vary, strAcceptEncoding) {
t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
}
body, err = resp.BodyGunzip()
if err != nil {
t.Fatalf("unexpected error on gunzip response body: %v", err)
}
if !bytes.Equal(body, expectedBody) {
t.Fatalf("unexpected body: len=%d. Expecting len=%d", len(body), len(expectedBody))
}
}
func TestDirFSFSByteRangeConcurrent(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip()
}
t.Parallel()
stop := make(chan struct{})
defer close(stop)
fs := &FS{
FS: dirTestFilesystem,
Root: "",
AcceptByteRange: true,
CleanStop: stop,
}
h := fs.NewRequestHandler()
concurrency := 10
ch := make(chan struct{}, concurrency)
for range concurrency {
go func() {
for range 5 {
testFSByteRange(t, h, "/fs.go")
testFSByteRange(t, h, "/README.md")
}
ch <- struct{}{}
}()
}
for range concurrency {
select {
case <-time.After(time.Second):
t.Fatalf("timeout")
case <-ch:
}
}
}
func TestDirFSFSByteRangeSingleThread(t *testing.T) {
t.Parallel()
stop := make(chan struct{})
defer close(stop)
fs := &FS{
FS: dirTestFilesystem,
Root: ".",
AcceptByteRange: true,
CleanStop: stop,
}
h := fs.NewRequestHandler()
testFSByteRange(t, h, "/fs.go")
testFSByteRange(t, h, "/README.md")
}
func TestDirFSFSCompressConcurrent(t *testing.T) {
t.Parallel()
stop := make(chan struct{})
defer close(stop)
fs := &FS{
FS: dirTestFilesystem,
Root: ".",
GenerateIndexPages: true,
Compress: true,
CompressBrotli: true,
CompressZstd: true,
CleanStop: stop,
}
h := fs.NewRequestHandler()
concurrency := 4
ch := make(chan struct{}, concurrency)
for range concurrency {
go func() {
for range 5 {
testFSFSCompress(t, h, "/fs.go")
testFSFSCompress(t, h, "/examples/")
testFSFSCompress(t, h, "/README.md")
}
ch <- struct{}{}
}()
}
for range concurrency {
select {
case <-ch:
case <-time.After(time.Second * 2):
t.Fatalf("timeout")
}
}
}
func TestDirFSFSCompressSingleThread(t *testing.T) {
t.Parallel()
stop := make(chan struct{})
defer close(stop)
fs := &FS{
FS: dirTestFilesystem,
Root: ".",
GenerateIndexPages: true,
Compress: true,
CompressBrotli: true,
CompressZstd: true,
CleanStop: stop,
}
h := fs.NewRequestHandler()
testFSFSCompress(t, h, "/fs.go")
testFSFSCompress(t, h, "/examples/")
testFSFSCompress(t, h, "/README.md")
}
func TestDirFSServeFileContentType(t *testing.T) {
t.Parallel()
var ctx RequestCtx
var req Request
req.Header.SetMethod(MethodGet)
req.SetRequestURI("http://foobar.com/baz")
ctx.Init(&req, nil, nil)
ServeFS(&ctx, dirTestFilesystem, "testdata/test.png")
var resp Response
s := ctx.Response.String()
br := bufio.NewReader(bytes.NewBufferString(s))
if err := resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := []byte("image/png")
if !bytes.Equal(resp.Header.ContentType(), expected) {
t.Fatalf("Unexpected Content-Type, expected: %q got %q", expected, resp.Header.ContentType())
}
}
func TestDirFSServeFileDirectoryRedirect(t *testing.T) {
t.Parallel()
var ctx RequestCtx
var req Request
req.SetRequestURI("http://foobar.com")
ctx.Init(&req, nil, nil)
ctx.Request.Reset()
ctx.Response.Reset()
ServeFS(&ctx, dirTestFilesystem, "fasthttputil")
if ctx.Response.StatusCode() != StatusFound {
t.Fatalf("Unexpected status code %d for directory '/fasthttputil' without trailing slash. Expecting %d.", ctx.Response.StatusCode(), StatusFound)
}
ctx.Request.Reset()
ctx.Response.Reset()
ServeFS(&ctx, dirTestFilesystem, "fasthttputil/")
if ctx.Response.StatusCode() != StatusOK {
t.Fatalf("Unexpected status code %d for directory '/fasthttputil/' with trailing slash. Expecting %d.", ctx.Response.StatusCode(), StatusOK)
}
ctx.Request.Reset()
ctx.Response.Reset()
ServeFS(&ctx, dirTestFilesystem, "fs.go")
if ctx.Response.StatusCode() != StatusOK {
t.Fatalf("Unexpected status code %d for file '/fs.go'. Expecting %d.", ctx.Response.StatusCode(), StatusOK)
}
}
func TestFSFSGenerateIndexOsDirFS(t *testing.T) {
t.Parallel()
t.Run("dirFS", func(t *testing.T) {
t.Parallel()
fs := &FS{
FS: dirTestFilesystem,
Root: ".",
GenerateIndexPages: true,
}
h := fs.NewRequestHandler()
var ctx RequestCtx
var req Request
ctx.Init(&req, nil, nil)
h(&ctx)
cases := []string{"/", "//", ""}
for _, c := range cases {
ctx.Request.Reset()
ctx.Response.Reset()
req.Header.SetMethod(MethodGet)
req.SetRequestURI("http://foobar.com" + c)
h(&ctx)
if ctx.Response.StatusCode() != StatusOK {
t.Fatalf("unexpected status code %d for path %q. Expecting %d", ctx.Response.StatusCode(), ctx.Response.StatusCode(), StatusOK)
}
if !bytes.Contains(ctx.Response.Body(), []byte("fasthttputil")) {
t.Fatalf("unexpected body %q. Expecting to contain %q", ctx.Response.Body(), "fasthttputil")
}
if !bytes.Contains(ctx.Response.Body(), []byte("fs.go")) {
t.Fatalf("unexpected body %q. Expecting to contain %q", ctx.Response.Body(), "fs.go")
}
}
})
t.Run("embedFS", func(t *testing.T) {
t.Parallel()
fs := &FS{
FS: fsTestFilesystem,
Root: ".",
GenerateIndexPages: true,
}
h := fs.NewRequestHandler()
var ctx RequestCtx
var req Request
ctx.Init(&req, nil, nil)
h(&ctx)
cases := []string{"/", "//", ""}
for _, c := range cases {
ctx.Request.Reset()
ctx.Response.Reset()
req.Header.SetMethod(MethodGet)
req.SetRequestURI("http://foobar.com" + c)
h(&ctx)
if ctx.Response.StatusCode() != StatusOK {
t.Fatalf("unexpected status code %d for path %q. Expecting %d", ctx.Response.StatusCode(), ctx.Response.StatusCode(), StatusOK)
}
if !bytes.Contains(ctx.Response.Body(), []byte("fasthttputil")) {
t.Fatalf("unexpected body %q. Expecting to contain %q", ctx.Response.Body(), "fasthttputil")
}
if !bytes.Contains(ctx.Response.Body(), []byte("fs.go")) {
t.Fatalf("unexpected body %q. Expecting to contain %q", ctx.Response.Body(), "fs.go")
}
}
})
}
func TestFSRootEnforcement(t *testing.T) {
t.Parallel()
memFS := fstest.MapFS{
"public/index.html": {Data: []byte("<h1>Public</h1>")},
"secret/admin.json": {Data: []byte(`{"admin": true, "key": "s3cret"}`)},
"public/nested/info": {Data: []byte("nested")},
}
// Skip dirfs subtests on Windows: the FS handler pools open file handles
// (via bigFileReader) which prevents t.TempDir cleanup.
// The mapfs subtests exercise the same root enforcement logic.
var tmpDir string
if runtime.GOOS != "windows" {
tmpDir = t.TempDir()
if err := os.MkdirAll(filepath.Join(tmpDir, "public"), 0o755); err != nil {
t.Fatalf("cannot create public dir: %v", err)
}
if err := os.MkdirAll(filepath.Join(tmpDir, "secret"), 0o755); err != nil {
t.Fatalf("cannot create secret dir: %v", err)
}
if err := os.WriteFile(filepath.Join(tmpDir, "public", "index.html"), []byte("<h1>Public</h1>"), 0o644); err != nil {
t.Fatalf("cannot create public index: %v", err)
}
if err := os.WriteFile(filepath.Join(tmpDir, "secret", "admin.json"), []byte(`{"admin": true, "key": "s3cret"}`), 0o644); err != nil {
t.Fatalf("cannot create secret admin file: %v", err)
}
}
type testCase struct {
name string
root string
filesystem fs.FS
pathRewrite PathRewriteFunc
}
cases := make([]testCase, 0, 9)
for _, root := range []string{"public", "public/", "./public", "/public"} {
cases = append(cases, testCase{
name: "mapfs/" + root,
root: root,
filesystem: memFS,
})
if tmpDir != "" {
cases = append(cases, testCase{
name: "dirfs/" + root,
root: root,
filesystem: os.DirFS(tmpDir),
})
}
}
cases = append(cases, testCase{
name: "mapfs/pathrewrite-no-leading-slash",
root: "./public/",
filesystem: memFS,
pathRewrite: func(ctx *RequestCtx) []byte {
return bytes.TrimPrefix(ctx.Path(), []byte("/"))
},
})
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
stop := make(chan struct{})
defer close(stop)
fs := &FS{
Root: tc.root,
FS: tc.filesystem,
AllowEmptyRoot: true,
CleanStop: stop,
PathRewrite: tc.pathRewrite,
}
h := fs.NewRequestHandler()
var ctx RequestCtx
ctx.Init(&Request{}, nil, TestLogger{t: t})
checkStatus := func(uri string, expected int) {
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.SetRequestURI(uri)
h(&ctx)
if ctx.Response.StatusCode() != expected {
t.Fatalf("unexpected status code for %s: %d. Expecting %d", uri, ctx.Response.StatusCode(), expected)
}
}
checkStatus("http://localhost/index.html", StatusOK)
checkStatus("http://localhost/secret/admin.json", StatusNotFound)
})
}
}
func TestHasDotDotPathSegment(t *testing.T) {
t.Parallel()
testCases := []struct {
path string
want bool
}{
{path: "", want: false},
{path: ".", want: false},
{path: "..", want: true},
{path: "../secret.txt", want: true},
{path: "/../secret.txt", want: true},
{path: "nested/../info", want: true},
{path: "nested/..", want: true},
{path: "nested/..hidden/info", want: false},
{path: "nested..", want: false},
{path: "/index.html", want: false},
}
if filepath.Separator == '\\' {
testCases = append(testCases,
struct {
path string
want bool
}{path: `..\secret.txt`, want: true},
struct {
path string
want bool
}{path: `nested\..\info`, want: true},
)
}
for _, tc := range testCases {
t.Run(tc.path, func(t *testing.T) {
t.Parallel()
if got := hasDotDotPathSegment([]byte(tc.path)); got != tc.want {
t.Fatalf("unexpected result for %q: got %v want %v", tc.path, got, tc.want)
}
})
}
}
func TestFSPathRewriteRejectsDotDotSegments(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
publicDir := filepath.Join(tmpDir, "public")
if err := os.MkdirAll(publicDir, 0o755); err != nil {
t.Fatalf("cannot create public dir: %v", err)
}
if err := os.WriteFile(filepath.Join(publicDir, "index.html"), []byte("<h1>Public</h1>"), 0o644); err != nil {
t.Fatalf("cannot create public index: %v", err)
}
secretPath := filepath.Join(tmpDir, "secret.txt")
if err := os.WriteFile(secretPath, []byte("TOP_SECRET"), 0o644); err != nil {
t.Fatalf("cannot create secret file: %v", err)
}
type testCase struct {
name string
pathRewrite PathRewriteFunc
requestURI string
}
testCases := []testCase{
{
name: "prefix-stripper-leading-dotdot",
pathRewrite: NewPathPrefixStripper(len("/static/")),
requestURI: "http://localhost/aaaaaaa../secret.txt",
},
{
name: "custom-leading-dotdot",
pathRewrite: func(ctx *RequestCtx) []byte {
return []byte("../secret.txt")
},
requestURI: "http://localhost/ignored",
},
{
name: "custom-trailing-dotdot",
pathRewrite: func(ctx *RequestCtx) []byte {
return []byte("nested/..")
},
requestURI: "http://localhost/ignored",
},
{
name: "custom-exact-dotdot",
pathRewrite: func(ctx *RequestCtx) []byte {
return []byte("..")
},
requestURI: "http://localhost/ignored",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
stop := make(chan struct{})
defer close(stop)
fs := &FS{
Root: publicDir,
AllowEmptyRoot: true,
CleanStop: stop,
PathRewrite: tc.pathRewrite,
}
h := fs.NewRequestHandler()
var ctx RequestCtx
ctx.Init(&Request{}, nil, TestLogger{t: t})
ctx.Request.SetRequestURI(tc.requestURI)
h(&ctx)
if ctx.Response.StatusCode() != StatusInternalServerError {
t.Fatalf("unexpected status code for %s: %d. Expecting %d", tc.name, ctx.Response.StatusCode(), StatusInternalServerError)
}
if bytes.Contains(ctx.Response.Body(), []byte("TOP_SECRET")) {
t.Fatalf("unexpected secret disclosure for %s", tc.name)
}
})
}
}