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
445 lines
15 KiB
Go
445 lines
15 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 (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/nabbar/golib/static"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("Router", func() {
|
|
Describe("RegisterRouter", func() {
|
|
Context("when registering without group", func() {
|
|
It("should serve files from root", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static", testMiddleware)
|
|
|
|
w := performRequest(engine, "GET", "/static/test.txt")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
Expect(w.Body.String()).To(ContainSubstring("This is a test file"))
|
|
Expect(w.Header().Get("X-Test-Middleware")).To(Equal("true"))
|
|
})
|
|
|
|
It("should serve JSON files", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/test.json")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
Expect(w.Body.String()).To(ContainSubstring("test json file"))
|
|
Expect(w.Header().Get("Content-Type")).To(ContainSubstring("application/json"))
|
|
})
|
|
|
|
It("should serve nested files", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/subdir/nested.txt")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
Expect(w.Body.String()).To(ContainSubstring("nested test file"))
|
|
})
|
|
|
|
It("should return 404 for non-existent files", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/nonexistent.txt")
|
|
Expect(w.Code).To(Equal(http.StatusNotFound))
|
|
})
|
|
|
|
It("should serve CSS files with correct content type", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/assets/style.css")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
Expect(w.Header().Get("Content-Type")).To(ContainSubstring("text/css"))
|
|
})
|
|
})
|
|
|
|
Context("when using multiple middlewares", func() {
|
|
It("should apply all middlewares", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static", newMiddleware(1), newMiddleware(2))
|
|
|
|
w := performRequest(engine, "GET", "/static/test.txt")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
Expect(w.Header().Get("X-Test-Middleware-1")).To(Equal("true"))
|
|
Expect(w.Header().Get("X-Test-Middleware-2")).To(Equal("true"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("RegisterRouterInGroup", func() {
|
|
Context("when registering with group", func() {
|
|
It("should serve files from group", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouterInGroup(handler, "/static", "/api")
|
|
|
|
w := performRequest(engine, "GET", "/api/static/test.txt")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
Expect(w.Body.String()).To(ContainSubstring("This is a test file"))
|
|
})
|
|
|
|
It("should not serve files without group prefix", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouterInGroup(handler, "/static", "/api")
|
|
|
|
w := performRequest(engine, "GET", "/static/test.txt")
|
|
Expect(w.Code).To(Equal(http.StatusNotFound))
|
|
})
|
|
|
|
It("should apply middlewares in group", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouterInGroup(handler, "/static", "/api", testMiddleware)
|
|
|
|
w := performRequest(engine, "GET", "/api/static/test.txt")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
Expect(w.Header().Get("X-Test-Middleware")).To(Equal("true"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("SendFile", func() {
|
|
Context("when sending regular file", func() {
|
|
It("should send file with correct headers", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/test.txt")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
Expect(w.Body.String()).To(ContainSubstring("This is a test file"))
|
|
Expect(w.Header().Get("Content-Type")).ToNot(BeEmpty())
|
|
})
|
|
|
|
It("should send HTML files", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/index.html")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
Expect(w.Body.String()).To(ContainSubstring("Test Index Page"))
|
|
Expect(w.Header().Get("Content-Type")).To(ContainSubstring("text/html"))
|
|
})
|
|
})
|
|
|
|
Context("when file is marked for download", func() {
|
|
It("should add Content-Disposition header", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
handler.SetDownload("testdata/test.txt", true)
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/test.txt")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
Expect(w.Header().Get("Content-Disposition")).To(ContainSubstring("attachment"))
|
|
Expect(w.Header().Get("Content-Disposition")).To(ContainSubstring("test.txt"))
|
|
})
|
|
|
|
It("should not add Content-Disposition for normal files", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/test.txt")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
Expect(w.Header().Get("Content-Disposition")).To(BeEmpty())
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Index Handling", func() {
|
|
Context("when index is set", func() {
|
|
It("should serve index file for route", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
handler.SetIndex("", "/static", "testdata/index.html")
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
Expect(w.Body.String()).To(ContainSubstring("Test Index Page"))
|
|
})
|
|
|
|
It("should serve index for exact route match", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
handler.SetIndex("", "/static/", "testdata/index.html")
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
Expect(w.Body.String()).To(ContainSubstring("Test Index Page"))
|
|
})
|
|
})
|
|
|
|
Context("when index is not set", func() {
|
|
It("should return 404 for directory pth", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/subdir")
|
|
Expect(w.Code).To(Equal(http.StatusNotFound))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Redirect Handling", func() {
|
|
Context("when redirect is set", func() {
|
|
It("should redirect to destination", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
handler.SetRedirect("", "/static/old", "", "/static/test.txt")
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/old")
|
|
Expect(w.Code).To(Equal(http.StatusPermanentRedirect))
|
|
|
|
location := w.Header().Get("Location")
|
|
Expect(location).To(ContainSubstring("/static/test.txt"))
|
|
})
|
|
|
|
It("should preserve query parameters on redirect", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
handler.SetRedirect("", "/static/old", "", "/static/new")
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/old?param=value")
|
|
Expect(w.Code).To(Equal(http.StatusPermanentRedirect))
|
|
|
|
location := w.Header().Get("Location")
|
|
Expect(location).To(ContainSubstring("param=value"))
|
|
})
|
|
})
|
|
|
|
Context("when redirect is not set", func() {
|
|
It("should serve file normally", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/test.txt")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Specific Handler", func() {
|
|
Context("when specific handler is set", func() {
|
|
It("should use custom handler", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
handler.SetSpecific("", "/static/custom", customMiddlewareOK("Custom Response", nil))
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/custom")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
Expect(w.Body.String()).To(Equal("Custom Response"))
|
|
})
|
|
|
|
It("should override file serving", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
handler.SetSpecific("", "/static/test.txt", customMiddlewareOK("Overridden", nil))
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/test.txt")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
Expect(w.Body.String()).To(Equal("Overridden"))
|
|
})
|
|
|
|
It("should allow custom status codes", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
handler.SetSpecific("", "/static/custom", customMiddlewareCreated("Created"))
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/custom")
|
|
Expect(w.Code).To(Equal(http.StatusCreated))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Logger Integration", func() {
|
|
Context("when logger is registered", func() {
|
|
It("should accept logger without error", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
|
|
// Use nil logger (should create default)
|
|
Expect(func() {
|
|
handler.RegisterLogger(nil)
|
|
}).ToNot(Panic())
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Complex Routing Scenarios", func() {
|
|
Context("when combining features", func() {
|
|
It("should handle redirect before specific handler", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
|
|
handler.SetRedirect("", "/static/path", "", "/static/redirect")
|
|
handler.SetSpecific("", "/static/path", customMiddlewareOK("Custom", nil))
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/path")
|
|
// Redirect should take precedence
|
|
Expect(w.Code).To(Equal(http.StatusPermanentRedirect))
|
|
})
|
|
|
|
It("should handle specific handler before index", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
|
|
handler.SetIndex("", "/static/path", "testdata/index.html")
|
|
handler.SetSpecific("", "/static/path", customMiddlewareOK("Custom", nil))
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/path")
|
|
// Specific handler should take precedence
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
Expect(w.Body.String()).To(Equal("Custom"))
|
|
})
|
|
|
|
It("should serve index when no redirect or specific handler", func() {
|
|
handler := newTestStatic().(static.Static)
|
|
handler.SetIndex("", "/static", "testdata/index.html")
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
Expect(w.Body.String()).To(ContainSubstring("Test Index Page"))
|
|
})
|
|
})
|
|
|
|
Context("when handling different file types", func() {
|
|
It("should serve all file types correctly", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
// Test TXT
|
|
w := performRequest(engine, "GET", "/static/test.txt")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
|
|
// Test JSON
|
|
w = performRequest(engine, "GET", "/static/test.json")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
|
|
// Test HTML
|
|
w = performRequest(engine, "GET", "/static/index.html")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
|
|
// Test CSS
|
|
w = performRequest(engine, "GET", "/static/assets/style.css")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Error Handling", func() {
|
|
Context("when encountering errors", func() {
|
|
It("should handle path traversal attempts", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/../../../etc/passwd")
|
|
// Should not be able to access files outside embed.FS
|
|
Expect(w.Code).To(Equal(http.StatusNotFound))
|
|
})
|
|
|
|
It("should handle double slashes", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static//test.txt")
|
|
// Should still work due to path cleaning
|
|
Expect(w.Code).To(Or(Equal(http.StatusOK), Equal(http.StatusNotFound)))
|
|
})
|
|
|
|
It("should handle trailing slashes", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/test.txt/")
|
|
// Trailing slashes are stripped, so the file should be served normally
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
Expect(w.Body.String()).To(ContainSubstring("This is a test file"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Base Path Handling", func() {
|
|
Context("when using custom base pth", func() {
|
|
It("should serve files from custom base", func() {
|
|
handler := newTestStaticWithRoot("testdata")
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/test.txt")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
})
|
|
|
|
It("should handle multiple base pth", func() {
|
|
handler := newTestStaticWithRoot("testdata", "testdata/subdir")
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/test.txt")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
|
|
w = performRequest(engine, "GET", "/static/nested.txt")
|
|
Expect(w.Code).To(Or(Equal(http.StatusOK), Equal(http.StatusNotFound)))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Case Sensitivity", func() {
|
|
Context("when checking file pth", func() {
|
|
It("should be case sensitive", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/Test.txt")
|
|
Expect(w.Code).To(Equal(http.StatusNotFound))
|
|
|
|
w = performRequest(engine, "GET", "/static/test.txt")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Content Length", func() {
|
|
Context("when serving files", func() {
|
|
It("should include content length header", func() {
|
|
handler := newTestStatic()
|
|
engine := setupTestRouter(handler, "/static")
|
|
|
|
w := performRequest(engine, "GET", "/static/test.txt")
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
|
|
contentLength := w.Header().Get("Content-Length")
|
|
// Should have content length (may vary based on implementation)
|
|
if contentLength != "" {
|
|
Expect(strings.TrimSpace(contentLength)).ToNot(BeEmpty())
|
|
}
|
|
})
|
|
})
|
|
})
|
|
})
|