mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-11-08 13:05:14 +00:00
feat: Remove ETag generation and validation middleware, simplify CacheMiddleware to basic Cache-Control header
- Remove ResponseWriterWrapper struct with Write/Body methods for response body capture - Remove ETagData/ETagGenerator structs and all ETag generation/validation/caching logic - Remove generateETag/generateContentETag/generateWeakETag/etagMatches helper functions - Remove ETagValidationMiddleware/ContentBasedETagMiddleware/CacheInvalidationMiddleware/AdvancedCacheMiddleware handlers
This commit is contained in:
@@ -1,35 +1,15 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResponseWriterWrapper wraps gin.ResponseWriter to capture response body
|
|
||||||
type ResponseWriterWrapper struct {
|
|
||||||
gin.ResponseWriter
|
|
||||||
body *bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write captures the response body while writing to the original writer
|
|
||||||
func (w *ResponseWriterWrapper) Write(data []byte) (int, error) {
|
|
||||||
w.body.Write(data)
|
|
||||||
return w.ResponseWriter.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Body returns the captured response body
|
|
||||||
func (w *ResponseWriterWrapper) Body() []byte {
|
|
||||||
return w.body.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// PerformanceMetrics tracks performance statistics
|
// PerformanceMetrics tracks performance statistics
|
||||||
type PerformanceMetrics struct {
|
type PerformanceMetrics struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
@@ -250,328 +230,11 @@ func CacheMiddleware(duration time.Duration) gin.HandlerFunc {
|
|||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
if c.Request.Method == "GET" {
|
if c.Request.Method == "GET" {
|
||||||
c.Header("Cache-Control", "public, max-age="+duration.String())
|
c.Header("Cache-Control", "public, max-age="+duration.String())
|
||||||
c.Header("ETag", generateETag(c))
|
|
||||||
}
|
}
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
|
||||||
// Wrap the response writer to capture the body
|
|
||||||
wrapper := &ResponseWriterWrapper{
|
|
||||||
ResponseWriter: c.Writer,
|
|
||||||
body: bytes.NewBuffer(nil),
|
|
||||||
}
|
|
||||||
c.Writer = wrapper
|
|
||||||
|
|
||||||
c.Next()
|
|
||||||
|
|
||||||
// Only generate ETags for successful GET requests
|
|
||||||
if c.Request.Method != "GET" || c.Writer.Status() != http.StatusOK {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate ETag based on captured response content
|
|
||||||
responseBody := wrapper.Body()
|
|
||||||
if len(responseBody) > 0 {
|
|
||||||
etag := generateContentETag(c, responseBody)
|
|
||||||
c.Header("ETag", etag)
|
|
||||||
} else {
|
|
||||||
// Fallback to weak ETag for empty responses
|
|
||||||
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
|
// TimeoutMiddleware adds timeout to requests
|
||||||
func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
|
func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
|||||||
Reference in New Issue
Block a user