feat: Add sophisticated ETag generation with SHA-256 hashing, cache storage, validation middleware, and content-based caching

- Add ETagData struct with Path/Method/Content/Timestamp/UserID/Params fields for ETag generation
- Add ETagGenerator struct with secretKey/cacheStorage fields and NewETagGenerator constructor
- Add GenerateETag method with SHA-256 hashing, path/method/content/timestamp/userID/params/secretKey composition
- Add GenerateWeakETag for dynamic content, GetCachedETag/CacheETag for cache operations,
This commit is contained in:
nutcas3
2026-04-23 02:53:01 +03:00
parent 6f3a4e2690
commit 5773cf00bb
@@ -2,8 +2,10 @@ package middleware
import (
"context"
"crypto/sha256"
"fmt"
"net/http"
"strings"
"sync"
"time"
@@ -230,23 +232,315 @@ func CacheMiddleware(duration time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.Method == "GET" {
c.Header("Cache-Control", "public, max-age="+duration.String())
c.Header("ETag", generateETag(c.Request.URL.Path))
c.Header("ETag", generateETag(c))
}
c.Next()
}
}
// generateETag generates a simple ETag for caching
func generateETag(path string) string {
// Simple hash-based ETag generation
// In production, this would be more sophisticated
hash := 0
for _, char := range path {
hash += int(char)
// ETagData represents the components used for ETag generation
type ETagData struct {
Path string
Method string
Content []byte
Timestamp time.Time
UserID string
Params string
}
// ETagGenerator handles sophisticated ETag generation
type ETagGenerator struct {
secretKey []byte
cacheStorage sync.Map // For storing computed ETags
}
// NewETagGenerator creates a new ETag generator instance
func NewETagGenerator(secretKey string) *ETagGenerator {
return &ETagGenerator{
secretKey: []byte(secretKey),
}
}
// GenerateETag creates a sophisticated ETag based on multiple factors
func (g *ETagGenerator) GenerateETag(data ETagData) string {
// Create a composite hash from multiple factors
h := sha256.New()
// Include path and method
h.Write([]byte(data.Path))
h.Write([]byte(data.Method))
// Include content if available
if len(data.Content) > 0 {
h.Write(data.Content)
}
// Include timestamp (rounded to second for consistency)
timestampBytes := []byte(fmt.Sprintf("%d", data.Timestamp.Unix()))
h.Write(timestampBytes)
// Include user context for personalized responses
if data.UserID != "" {
h.Write([]byte(data.UserID))
}
// Include query parameters for cache differentiation
if data.Params != "" {
h.Write([]byte(data.Params))
}
// Add secret key for security
h.Write(g.secretKey)
// Generate final ETag
hash := h.Sum(nil)
return fmt.Sprintf("\"%x\"", hash)
}
// GenerateWeakETag creates a weak ETag for dynamic content
func (g *ETagGenerator) GenerateWeakETag(data ETagData) string {
etag := g.GenerateETag(data)
return "W/" + etag
}
// GetCachedETag retrieves a cached ETag if available
func (g *ETagGenerator) GetCachedETag(key string) (string, bool) {
if cached, ok := g.cacheStorage.Load(key); ok {
if etag, ok := cached.(string); ok {
return etag, true
}
}
return "", false
}
// CacheETag stores an ETag in the cache
func (g *ETagGenerator) CacheETag(key, etag string) {
g.cacheStorage.Store(key, etag)
}
// InvalidateCache removes ETag entries matching a pattern
func (g *ETagGenerator) InvalidateCache(pattern string) {
g.cacheStorage.Range(func(key, value interface{}) bool {
if keyStr, ok := key.(string); ok {
if strings.Contains(keyStr, pattern) {
g.cacheStorage.Delete(key)
}
}
return true
})
}
// Global ETag generator instance
var etagGenerator = NewETagGenerator("telecom-platform-etag-secret")
// generateETag generates a sophisticated ETag for caching
func generateETag(c *gin.Context) string {
// Extract relevant data for ETag generation
data := ETagData{
Path: c.Request.URL.Path,
Method: c.Request.Method,
Timestamp: time.Now(),
Params: c.Request.URL.RawQuery,
}
// Add user context if available
if userID, exists := c.Get("user_id"); exists {
if idStr, ok := userID.(string); ok {
data.UserID = idStr
}
}
// Try to get cached ETag first
cacheKey := fmt.Sprintf("%s:%s:%s", data.Method, data.Path, data.Params)
if cached, exists := etagGenerator.GetCachedETag(cacheKey); exists {
return cached
}
// Generate new ETag
etag := etagGenerator.GenerateETag(data)
// Cache the ETag for future use
etagGenerator.CacheETag(cacheKey, etag)
return etag
}
// generateContentETag creates an ETag based on response content
func generateContentETag(c *gin.Context, content []byte) string {
data := ETagData{
Path: c.Request.URL.Path,
Method: c.Request.Method,
Content: content,
Timestamp: time.Now(),
Params: c.Request.URL.RawQuery,
}
// Add user context if available
if userID, exists := c.Get("user_id"); exists {
if idStr, ok := userID.(string); ok {
data.UserID = idStr
}
}
return etagGenerator.GenerateETag(data)
}
// generateWeakETag creates a weak ETag for dynamic content
func generateWeakETag(c *gin.Context) string {
data := ETagData{
Path: c.Request.URL.Path,
Method: c.Request.Method,
Timestamp: time.Now(),
Params: c.Request.URL.RawQuery,
}
// Add user context if available
if userID, exists := c.Get("user_id"); exists {
if idStr, ok := userID.(string); ok {
data.UserID = idStr
}
}
return etagGenerator.GenerateWeakETag(data)
}
// ETagValidationMiddleware handles ETag validation for conditional requests
func ETagValidationMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Only handle GET and HEAD requests for ETag validation
if c.Request.Method != "GET" && c.Request.Method != "HEAD" {
c.Next()
return
}
// Check If-None-Match header (for GET requests)
if noneMatch := c.GetHeader("If-None-Match"); noneMatch != "" {
// Generate current ETag
currentETag := generateETag(c)
// Check if any of the ETags match
if etagMatches(noneMatch, currentETag) {
c.Header("ETag", currentETag)
c.Status(http.StatusNotModified)
c.Abort()
return
}
}
// Check If-Match header (for PUT/PATCH/DELETE requests)
if match := c.GetHeader("If-Match"); match != "" {
currentETag := generateETag(c)
if !etagMatches(match, currentETag) {
c.Status(http.StatusPreconditionFailed)
c.Abort()
return
}
}
c.Next()
}
}
// ContentBasedETagMiddleware generates ETags based on response content
func ContentBasedETagMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
// Only generate ETags for successful GET requests
if c.Request.Method != "GET" || c.Writer.Status() != http.StatusOK {
return
}
// For content-based ETags, we would need to capture the response body
// This is a simplified version that uses weak ETags for dynamic content
etag := generateWeakETag(c)
c.Header("ETag", etag)
}
}
// CacheInvalidationMiddleware handles cache invalidation for modified resources
func CacheInvalidationMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Store original request for post-processing
c.Next()
// Invalidate cache for POST, PUT, PATCH, DELETE requests
if c.Request.Method == "POST" || c.Request.Method == "PUT" ||
c.Request.Method == "PATCH" || c.Request.Method == "DELETE" {
// Invalidate ETags for this resource pattern
pattern := c.Request.URL.Path
etagGenerator.InvalidateCache(pattern)
// Also invalidate related collection resources
if strings.Contains(pattern, "/") {
parts := strings.Split(strings.Trim(pattern, "/"), "/")
if len(parts) > 0 {
collectionPattern := "/" + parts[0]
etagGenerator.InvalidateCache(collectionPattern)
}
}
}
}
}
// etagMatches checks if any of the provided ETags match the current ETag
func etagMatches(headerValue, currentETag string) bool {
// Handle wildcard
if headerValue == "*" {
return true
}
// Parse the header value (can contain multiple ETags)
etags := strings.Split(headerValue, ",")
for _, etag := range etags {
etag = strings.TrimSpace(etag)
// Remove quotes for comparison
etag = strings.Trim(etag, "\"")
current := strings.Trim(currentETag, "\"W/")
if etag == current || etag == "W/"+current {
return true
}
}
return false
}
// AdvancedCacheMiddleware provides sophisticated caching with ETags
func AdvancedCacheMiddleware(maxAge time.Duration, useWeakETag bool) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.Method == "GET" || c.Request.Method == "HEAD" {
// Set cache control headers
cacheControl := fmt.Sprintf("public, max-age=%d", int(maxAge.Seconds()))
if useWeakETag {
cacheControl += ", must-revalidate"
}
c.Header("Cache-Control", cacheControl)
// Generate appropriate ETag
var etag string
if useWeakETag {
etag = generateWeakETag(c)
} else {
etag = generateETag(c)
}
c.Header("ETag", etag)
// Add Vary header for proper caching with different user contexts
if userID, exists := c.Get("user_id"); exists {
c.Header("Vary", "Accept, Authorization")
if userIDStr, ok := userID.(string); ok && userIDStr != "" {
c.Header("X-User-Context", userIDStr)
}
}
}
c.Next()
}
}
// TimeoutMiddleware adds timeout to requests
func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {