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
224 lines
6.4 KiB
Go
224 lines
6.4 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
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"mime"
|
|
"net/http"
|
|
"path"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
ginsdk "github.com/gin-gonic/gin"
|
|
loglvl "github.com/nabbar/golib/logger/level"
|
|
)
|
|
|
|
// SetHeaders configures HTTP headers behavior.
|
|
// This method is thread-safe and uses atomic operations.
|
|
func (s *staticHandler) SetHeaders(cfg HeadersConfig) {
|
|
s.hdr.Store(&cfg)
|
|
}
|
|
|
|
// GetHeaders returns the current HTTP headers configuration.
|
|
// This method is thread-safe and uses atomic operations.
|
|
func (s *staticHandler) GetHeaders() HeadersConfig {
|
|
if cfg := s.hdr.Load(); cfg != nil {
|
|
return *cfg
|
|
}
|
|
return HeadersConfig{}
|
|
}
|
|
|
|
// getMimeType detects the MIME type of a file based on its extension.
|
|
// Custom MIME types are checked first, then falls back to standard detection.
|
|
func (s *staticHandler) getMimeType(filename string) string {
|
|
cfg := s.GetHeaders()
|
|
|
|
ext := strings.ToLower(path.Ext(filename))
|
|
|
|
// Check custom MIME types first
|
|
if cfg.CustomMimeTypes != nil {
|
|
if customMime, ok := cfg.CustomMimeTypes[ext]; ok {
|
|
return customMime
|
|
}
|
|
}
|
|
|
|
// Use standard detection
|
|
mimeType := mime.TypeByExtension(ext)
|
|
if mimeType == "" {
|
|
mimeType = "application/octet-stream"
|
|
}
|
|
|
|
return mimeType
|
|
}
|
|
|
|
// isMimeTypeAllowed checks if a MIME type is allowed to be served.
|
|
// This method:
|
|
// 1. Checks deny list first
|
|
// 2. If allow list is empty, allows all (except denied)
|
|
// 3. Otherwise, checks allow list
|
|
func (s *staticHandler) isMimeTypeAllowed(mimeType string) bool {
|
|
cfg := s.GetHeaders()
|
|
|
|
if !cfg.EnableContentType {
|
|
return true
|
|
}
|
|
|
|
// Extract base type (without charset, etc.)
|
|
baseType := strings.Split(mimeType, ";")[0]
|
|
baseType = strings.TrimSpace(baseType)
|
|
|
|
// Check if in deny list
|
|
if slices.Contains(cfg.DenyMimeTypes, baseType) {
|
|
return false
|
|
}
|
|
|
|
// If allow list is empty, everything is allowed
|
|
if len(cfg.AllowedMimeTypes) == 0 {
|
|
return true
|
|
}
|
|
|
|
// Check if in allow list
|
|
return slices.Contains(cfg.AllowedMimeTypes, baseType)
|
|
}
|
|
|
|
// generateETag generates an ETag for a file based on:
|
|
// - Filename
|
|
// - File size
|
|
// - Modification time
|
|
//
|
|
// The ETag is a SHA-256 hash truncated to 16 bytes for efficiency.
|
|
func (s *staticHandler) generateETag(filename string, size int64, modTime time.Time) string {
|
|
cfg := s.GetHeaders()
|
|
|
|
if !cfg.EnableETag {
|
|
return ""
|
|
}
|
|
|
|
// ETag based on: filename + size + modification date
|
|
// Format: "hash-hex"
|
|
data := fmt.Sprintf("%s-%d-%d", filename, size, modTime.Unix())
|
|
hash := sha256.Sum256([]byte(data))
|
|
etag := fmt.Sprintf(`"%x"`, hash[:16]) // Take first 16 bytes
|
|
|
|
return etag
|
|
}
|
|
|
|
// checkETag verifies if the client's ETag matches the current file.
|
|
// This enables HTTP 304 Not Modified responses to save bandwidth.
|
|
func (s *staticHandler) checkETag(c *ginsdk.Context, etag string) bool {
|
|
cfg := s.GetHeaders()
|
|
|
|
if !cfg.EnableETag || etag == "" {
|
|
return false
|
|
}
|
|
|
|
// Check If-None-Match header
|
|
ifNoneMatch := c.GetHeader("If-None-Match")
|
|
if ifNoneMatch == "" {
|
|
return false
|
|
}
|
|
|
|
// Compare ETags
|
|
return ifNoneMatch == etag
|
|
}
|
|
|
|
// setCacheHeaders sets HTTP caching headers (Cache-Control, Expires).
|
|
// These headers instruct browsers and CDNs how to cache the file.
|
|
func (s *staticHandler) setCacheHeaders(c *ginsdk.Context) {
|
|
cfg := s.GetHeaders()
|
|
|
|
if !cfg.EnableCacheControl {
|
|
return
|
|
}
|
|
|
|
// Cache-Control
|
|
var cacheControl string
|
|
if cfg.CachePublic {
|
|
cacheControl = fmt.Sprintf("public, max-age=%d", cfg.CacheMaxAge)
|
|
} else {
|
|
cacheControl = fmt.Sprintf("private, max-age=%d", cfg.CacheMaxAge)
|
|
}
|
|
|
|
c.Header("Cache-Control", cacheControl)
|
|
|
|
// Expires (for HTTP/1.0 compatibility)
|
|
expires := time.Now().Add(time.Duration(cfg.CacheMaxAge) * time.Second)
|
|
c.Header("Expires", expires.UTC().Format(http.TimeFormat))
|
|
}
|
|
|
|
// setContentTypeHeader sets the Content-Type header and validates MIME type.
|
|
// Returns an error if the MIME type is not allowed.
|
|
// Notifies security backend if MIME type is denied.
|
|
func (s *staticHandler) setContentTypeHeader(c *ginsdk.Context, filename string) (string, error) {
|
|
cfg := s.GetHeaders()
|
|
mimeType := s.getMimeType(filename)
|
|
|
|
// Validate MIME type only if EnableContentType is enabled
|
|
if cfg.EnableContentType && !s.isMimeTypeAllowed(mimeType) {
|
|
ent := s.getLogger().Entry(loglvl.WarnLevel, "mime type not allowed")
|
|
ent.FieldAdd("filename", filename)
|
|
ent.FieldAdd("mimeType", mimeType)
|
|
ent.Log()
|
|
|
|
// Notify WAF/IDS/EDR
|
|
details := map[string]string{
|
|
"filename": filename,
|
|
"mime_type": mimeType,
|
|
}
|
|
event := s.newSecuEvt(c, EventTypeMimeTypeDenied, "medium", true, details)
|
|
s.notifySecuEvt(event)
|
|
|
|
return "", ErrorMimeTypeDenied.Error(fmt.Errorf("mime type not allowed: %s", mimeType))
|
|
}
|
|
|
|
c.Header("Content-Type", mimeType)
|
|
return mimeType, nil
|
|
}
|
|
|
|
// setETagHeader sets ETag and Last-Modified headers and checks cache validation.
|
|
// Returns true if the client has a valid cached version (cache hit).
|
|
// This allows the handler to return HTTP 304 Not Modified.
|
|
func (s *staticHandler) setETagHeader(c *ginsdk.Context, filename string, size int64, modTime time.Time) bool {
|
|
etag := s.generateETag(filename, size, modTime)
|
|
|
|
if etag == "" {
|
|
return false
|
|
}
|
|
|
|
c.Header("ETag", etag)
|
|
c.Header("Last-Modified", modTime.UTC().Format(http.TimeFormat))
|
|
|
|
// Check if client already has cached version
|
|
if s.checkETag(c, etag) {
|
|
return true // Cache hit
|
|
}
|
|
|
|
return false // Cache miss
|
|
}
|