mirror of
https://github.com/nabbar/golib.git
synced 2025-12-24 11:51:02 +08:00
- ADD flag to register temp file creation
- ADD function to check flag is temp
[ static ]
- FIX bugs & race detection
- UPDATE code: refactor & optimize code, improve security &
preformances
- ADD Path Security: add options & code to improve security
- ADD Rate Limiting: add option to limit capabilities of burst request
- ADD HTTP Security Headers: add option to customize header, improve
security & allow cache crontol
- ADD Suspicious Access Detection: add option to identify & log
suspicious request
- ADD Security Backend Integration: add option to plug WAF/IDF/EDR
backend (with CEF Format or not)
- ADD documentation: add enhanced README and TESTING guidelines
- ADD tests: complete test suites with benchmarks, concurrency, and edge cases
681 lines
16 KiB
Go
681 lines
16 KiB
Go
/*
|
|
* MIT License
|
|
*
|
|
* Copyright (c) 2022 Nicolas JUHEL
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
* copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*
|
|
*/
|
|
|
|
package static_test
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/nabbar/golib/static"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("Concurrency", func() {
|
|
Describe("Concurrent File Access", func() {
|
|
Context("when accessing files concurrently", func() {
|
|
It("should handle concurrent Has calls", func() {
|
|
handler := newTestStatic()
|
|
h := handler.(interface{ Has(string) bool })
|
|
|
|
var wg sync.WaitGroup
|
|
errCount := atomic.Int32{}
|
|
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
if !h.Has("testdata/test.txt") {
|
|
errCount.Add(1)
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
Expect(errCount.Load()).To(Equal(int32(0)))
|
|
})
|
|
|
|
It("should handle concurrent Find calls", func() {
|
|
handler := newTestStatic()
|
|
h := handler.(staticFind)
|
|
|
|
var wg sync.WaitGroup
|
|
successCount := atomic.Int32{}
|
|
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
r, err := h.Find("testdata/test.txt")
|
|
if err == nil && r != nil {
|
|
_, _ = io.Copy(io.Discard, r)
|
|
_ = r.Close()
|
|
successCount.Add(1)
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
Expect(successCount.Load()).To(Equal(int32(50)))
|
|
})
|
|
|
|
It("should handle concurrent Info calls", func() {
|
|
handler := newTestStatic()
|
|
h := handler.(staticInfo)
|
|
|
|
var wg sync.WaitGroup
|
|
successCount := atomic.Int32{}
|
|
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
info, err := h.Info("testdata/test.txt")
|
|
if err == nil && info != nil {
|
|
successCount.Add(1)
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
Expect(successCount.Load()).To(Equal(int32(50)))
|
|
})
|
|
|
|
It("should handle concurrent List calls", func() {
|
|
handler := newTestStatic()
|
|
h := handler.(staticList)
|
|
|
|
var wg sync.WaitGroup
|
|
successCount := atomic.Int32{}
|
|
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
files, err := h.List("testdata")
|
|
if err == nil && len(files) > 0 {
|
|
successCount.Add(1)
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
Expect(successCount.Load()).To(Equal(int32(50)))
|
|
})
|
|
|
|
It("should handle concurrent Temp calls", func() {
|
|
handler := newTestStatic()
|
|
h := handler.(staticTemp)
|
|
|
|
var wg sync.WaitGroup
|
|
successCount := atomic.Int32{}
|
|
|
|
for i := 0; i < 20; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
tmp, err := h.Temp("testdata/test.txt")
|
|
if err == nil && tmp != nil {
|
|
_, _ = io.Copy(io.Discard, tmp)
|
|
_ = tmp.Close()
|
|
if namer, ok := tmp.(interface{ Name() string }); ok {
|
|
_ = os.Remove(namer.Name())
|
|
}
|
|
successCount.Add(1)
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
Expect(successCount.Load()).To(Equal(int32(20)))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Concurrent Configuration Access", func() {
|
|
Context("when modifying configurations concurrently", func() {
|
|
It("should handle concurrent SetDownload and IsDownload", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
if idx%2 == 0 {
|
|
handler.SetDownload("testdata/test.txt", true)
|
|
} else {
|
|
_ = handler.IsDownload("testdata/test.txt")
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
// Should not panic or race
|
|
})
|
|
|
|
It("should handle concurrent SetIndex and GetIndex", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
if idx%2 == 0 {
|
|
handler.SetIndex("", "/", "testdata/index.html")
|
|
} else {
|
|
_ = handler.GetIndex("", "/")
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
// Should not panic or race
|
|
})
|
|
|
|
It("should handle concurrent SetRedirect and GetRedirect", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
if idx%2 == 0 {
|
|
handler.SetRedirect("", "/old", "", "/new")
|
|
} else {
|
|
_ = handler.GetRedirect("", "/old")
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
// Should not panic or race
|
|
})
|
|
|
|
It("should handle concurrent SetSpecific and GetSpecific", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
if idx%2 == 0 {
|
|
handler.SetSpecific("", "/custom", customMiddlewareOK("custom", nil))
|
|
} else {
|
|
_ = handler.GetSpecific("", "/custom")
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
// Should not panic or race
|
|
})
|
|
})
|
|
|
|
Context("when mixing different configurations", func() {
|
|
It("should handle mixed concurrent operations", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
switch idx % 4 {
|
|
case 0:
|
|
handler.SetDownload("testdata/test.txt", true)
|
|
_ = handler.IsDownload("testdata/test.txt")
|
|
case 1:
|
|
handler.SetIndex("", "/", "testdata/index.html")
|
|
_ = handler.GetIndex("", "/")
|
|
case 2:
|
|
handler.SetRedirect("", "/old", "", "/new")
|
|
_ = handler.GetRedirect("", "/old")
|
|
case 3:
|
|
handler.SetSpecific("", "/custom", customMiddlewareOK("custom", nil))
|
|
_ = handler.GetSpecific("", "/custom")
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
// Should not panic or race
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Concurrent HTTP Requests", func() {
|
|
Context("when serving files concurrently", func() {
|
|
It("should handle concurrent HTTP requests", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
var wg sync.WaitGroup
|
|
successCount := atomic.Int32{}
|
|
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
w := performRequest(engine, "GET", "/static/test.txt")
|
|
if w.Code == http.StatusOK {
|
|
successCount.Add(1)
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
Expect(successCount.Load()).To(Equal(int32(100)))
|
|
})
|
|
|
|
It("should handle concurrent requests for different files", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
var wg sync.WaitGroup
|
|
successCount := atomic.Int32{}
|
|
|
|
files := []string{
|
|
"/static/test.txt",
|
|
"/static/test.json",
|
|
"/static/index.html",
|
|
"/static/subdir/nested.txt",
|
|
"/static/assets/style.css",
|
|
}
|
|
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
file := files[idx%len(files)]
|
|
w := performRequest(engine, "GET", file)
|
|
if w.Code == http.StatusOK {
|
|
successCount.Add(1)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
Expect(successCount.Load()).To(Equal(int32(50)))
|
|
})
|
|
|
|
It("should handle concurrent requests with middleware", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static", testMiddleware)
|
|
|
|
var wg sync.WaitGroup
|
|
successCount := atomic.Int32{}
|
|
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
w := performRequest(engine, "GET", "/static/test.txt")
|
|
if w.Code == http.StatusOK && w.Header().Get("X-Test-Middleware") == "true" {
|
|
successCount.Add(1)
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
Expect(successCount.Load()).To(Equal(int32(50)))
|
|
})
|
|
})
|
|
|
|
Context("when serving with special configurations", func() {
|
|
It("should handle concurrent requests with redirects", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
handler.SetRedirect("", "/static/old", "", "/static/test.txt")
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
var wg sync.WaitGroup
|
|
redirectCount := atomic.Int32{}
|
|
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
w := performRequest(engine, "GET", "/static/old")
|
|
if w.Code == http.StatusPermanentRedirect {
|
|
redirectCount.Add(1)
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
Expect(redirectCount.Load()).To(Equal(int32(50)))
|
|
})
|
|
|
|
It("should handle concurrent requests with specific handlers", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
|
|
callCount := atomic.Int32{}
|
|
handler.SetSpecific("", "/static/custom", customMiddlewareOK("custom", func() {
|
|
callCount.Add(1)
|
|
}))
|
|
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
var wg sync.WaitGroup
|
|
successCount := atomic.Int32{}
|
|
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
w := performRequest(engine, "GET", "/static/custom")
|
|
if w.Code == http.StatusOK {
|
|
successCount.Add(1)
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
Expect(successCount.Load()).To(Equal(int32(50)))
|
|
Expect(callCount.Load()).To(Equal(int32(50)))
|
|
})
|
|
|
|
It("should handle concurrent requests with download flag", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
handler.SetDownload("testdata/test.txt", true)
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
var wg sync.WaitGroup
|
|
downloadCount := atomic.Int32{}
|
|
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
w := performRequest(engine, "GET", "/static/test.txt")
|
|
if w.Code == http.StatusOK && w.Header().Get("Content-Disposition") != "" {
|
|
downloadCount.Add(1)
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
Expect(downloadCount.Load()).To(Equal(int32(50)))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Concurrent Map Operations", func() {
|
|
Context("when mapping over files concurrently", func() {
|
|
It("should handle concurrent Map calls", func() {
|
|
handler := newTestStatic()
|
|
h := handler.(staticMap)
|
|
|
|
var wg sync.WaitGroup
|
|
successCount := atomic.Int32{}
|
|
|
|
for i := 0; i < 20; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
err := h.Map(func(pathFile string, inf os.FileInfo) error {
|
|
return nil
|
|
})
|
|
if err == nil {
|
|
successCount.Add(1)
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
Expect(successCount.Load()).To(Equal(int32(20)))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Stress Testing", func() {
|
|
Context("when under heavy load", func() {
|
|
It("should handle high concurrent file access", func() {
|
|
handler := newTestStatic()
|
|
h := handler.(staticFind)
|
|
|
|
var wg sync.WaitGroup
|
|
successCount := atomic.Int32{}
|
|
iterations := 200
|
|
|
|
for i := 0; i < iterations; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
files := []string{
|
|
"testdata/test.txt",
|
|
"testdata/test.json",
|
|
"testdata/index.html",
|
|
"testdata/large.txt",
|
|
}
|
|
file := files[idx%len(files)]
|
|
|
|
r, err := h.Find(file)
|
|
if err == nil && r != nil {
|
|
_, _ = io.Copy(io.Discard, r)
|
|
_ = r.Close()
|
|
successCount.Add(1)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
Expect(successCount.Load()).To(Equal(int32(iterations)))
|
|
})
|
|
|
|
It("should handle high concurrent HTTP requests", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
var wg sync.WaitGroup
|
|
successCount := atomic.Int32{}
|
|
iterations := 200
|
|
|
|
for i := 0; i < iterations; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
files := []string{
|
|
"/static/test.txt",
|
|
"/static/test.json",
|
|
"/static/index.html",
|
|
"/static/subdir/nested.txt",
|
|
}
|
|
file := files[idx%len(files)]
|
|
|
|
w := performRequest(engine, "GET", file)
|
|
if w.Code == http.StatusOK {
|
|
successCount.Add(1)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
Expect(successCount.Load()).To(Equal(int32(iterations)))
|
|
})
|
|
|
|
It("should handle mixed concurrent operations under stress", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
var wg sync.WaitGroup
|
|
iterations := 200
|
|
|
|
for i := 0; i < iterations; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
switch idx % 5 {
|
|
case 0:
|
|
handler.SetDownload("testdata/test.txt", idx%2 == 0)
|
|
case 1:
|
|
handler.SetIndex("", "/", "testdata/index.html")
|
|
case 2:
|
|
handler.SetRedirect("", "/old", "", "/new")
|
|
case 3:
|
|
_ = handler.Has("testdata/test.txt")
|
|
case 4:
|
|
w := performRequest(engine, "GET", "/static/test.txt")
|
|
_ = w.Code
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
// Should complete without panic or race
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Race Condition Prevention", func() {
|
|
Context("when testing for race conditions", func() {
|
|
It("should not race on configuration updates", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(2)
|
|
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
handler.SetDownload("testdata/test.txt", true)
|
|
}()
|
|
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
_ = handler.IsDownload("testdata/test.txt")
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
// Should complete without race detector warnings
|
|
})
|
|
|
|
It("should not race on file access", func() {
|
|
handler := newTestStatic()
|
|
h := handler.(staticFindHas)
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(2)
|
|
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
_ = h.Has("testdata/test.txt")
|
|
}()
|
|
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
r, _ := h.Find("testdata/test.txt")
|
|
if r != nil {
|
|
_ = r.Close()
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
// Should complete without race detector warnings
|
|
})
|
|
|
|
It("should not race on HTTP serving", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(2)
|
|
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
handler.SetDownload("testdata/test.txt", true)
|
|
}()
|
|
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
_ = performRequest(engine, "GET", "/static/test.txt")
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
// Should complete without race detector warnings
|
|
})
|
|
})
|
|
})
|
|
})
|