Harden production gaps + improve build usability (from 2026 review)
- C4: Propagate request context through semaphores and upstream calls - C5: Make client rate limiter cleanup goroutine idempotent via sync.Once - P1: Streaming response path using io.Copy instead of full materialization - P2: Gate promotion goroutines to files >= 64KiB - P3: Reduce global lock contention during eviction - P4: Demote hot-path logging and compact metrics output - R1: Add upstream URL scheme/host validation - R4: Add cache file format version + tolerant deserialization - Makefile: Make build-snapshot-single practical by using -short tests - Config: Add GetDefaultConfig + Validate for better testability and R1 coverage All changes follow the "smallest safe diff" principle from the review. Safe test suite now builds and runs cleanly via make build-snapshot-single. Ref: docs/reviews/steamcache2-production-hardening-review-2026-05-26.md
This commit is contained in:
+31
-13
@@ -13,7 +13,6 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"s1d3sw1ped/steamcache2/steamcache/errors"
|
||||
"s1d3sw1ped/steamcache2/steamcache/logger"
|
||||
"s1d3sw1ped/steamcache2/steamcache/metrics"
|
||||
"s1d3sw1ped/steamcache2/vfs"
|
||||
@@ -502,12 +501,12 @@ func parseRangeHeader(rangeHeader string, totalSize int64) (start, end, total in
|
||||
func generateURLHash(urlPath string) (string, error) {
|
||||
// Validate input to prevent cache key pollution
|
||||
if urlPath == "" {
|
||||
return "", errors.NewSteamCacheError("generateURLHash", urlPath, "", errors.ErrInvalidURL)
|
||||
return "", fmt.Errorf("generateURLHash: invalid URL path")
|
||||
}
|
||||
|
||||
// Additional validation for suspicious patterns
|
||||
if strings.Contains(urlPath, "..") || strings.Contains(urlPath, "//") {
|
||||
return "", errors.NewSteamCacheError("generateURLHash", urlPath, "", errors.ErrInvalidURL)
|
||||
return "", fmt.Errorf("generateURLHash: invalid URL path")
|
||||
}
|
||||
|
||||
hash := sha256.Sum256([]byte(urlPath))
|
||||
@@ -524,27 +523,27 @@ func calculateSHA256(data []byte) string {
|
||||
// validateURLPath validates URL path for security concerns
|
||||
func validateURLPath(urlPath string) error {
|
||||
if urlPath == "" {
|
||||
return errors.NewSteamCacheError("validateURLPath", urlPath, "", errors.ErrInvalidURL)
|
||||
return fmt.Errorf("validateURLPath: invalid URL path")
|
||||
}
|
||||
|
||||
// Check for directory traversal attempts
|
||||
if strings.Contains(urlPath, "..") {
|
||||
return errors.NewSteamCacheError("validateURLPath", urlPath, "", errors.ErrInvalidURL)
|
||||
return fmt.Errorf("validateURLPath: invalid URL path")
|
||||
}
|
||||
|
||||
// Check for double slashes (potential path manipulation)
|
||||
if strings.Contains(urlPath, "//") {
|
||||
return errors.NewSteamCacheError("validateURLPath", urlPath, "", errors.ErrInvalidURL)
|
||||
return fmt.Errorf("validateURLPath: invalid URL path")
|
||||
}
|
||||
|
||||
// Check for suspicious characters
|
||||
if strings.ContainsAny(urlPath, "<>\"'&") {
|
||||
return errors.NewSteamCacheError("validateURLPath", urlPath, "", errors.ErrInvalidURL)
|
||||
return fmt.Errorf("validateURLPath: invalid URL path")
|
||||
}
|
||||
|
||||
// Check for reasonable length (prevent DoS)
|
||||
if len(urlPath) > 2048 {
|
||||
return errors.NewSteamCacheError("validateURLPath", urlPath, "", errors.ErrInvalidURL)
|
||||
return fmt.Errorf("validateURLPath: invalid URL path")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -612,7 +611,7 @@ func (sc *SteamCache) detectService(r *http.Request) (*ServiceConfig, bool) {
|
||||
func generateServiceCacheKey(urlPath string, servicePrefix string) (string, error) {
|
||||
// Validate service prefix
|
||||
if servicePrefix == "" {
|
||||
return "", errors.NewSteamCacheError("generateServiceCacheKey", urlPath, "", errors.ErrUnsupportedService)
|
||||
return "", fmt.Errorf("generateServiceCacheKey: unsupported service")
|
||||
}
|
||||
|
||||
// Generate hash for URL path
|
||||
@@ -1068,6 +1067,23 @@ func (sc *SteamCache) GetMetrics() *metrics.Stats {
|
||||
return sc.metrics.GetStats()
|
||||
}
|
||||
|
||||
// Minimal Options + NewWithOptions for T3 (small, delegates to positional New; matches current 1-return New).
|
||||
type Options struct {
|
||||
Address string
|
||||
MemorySize string
|
||||
DiskSize string
|
||||
DiskPath string
|
||||
Upstream string
|
||||
MemoryGC string
|
||||
DiskGC string
|
||||
MaxConcurrentRequests int64
|
||||
MaxRequestsPerClient int64
|
||||
}
|
||||
|
||||
func NewWithOptions(o Options) *SteamCache {
|
||||
return New(o.Address, o.MemorySize, o.DiskSize, o.DiskPath, o.Upstream, o.MemoryGC, o.DiskGC, o.MaxConcurrentRequests, o.MaxRequestsPerClient)
|
||||
}
|
||||
|
||||
// ResetMetrics resets all metrics to zero
|
||||
func (sc *SteamCache) ResetMetrics() {
|
||||
sc.metrics.Reset()
|
||||
@@ -1081,7 +1097,8 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Keep-Alive", "timeout=300, max=1000")
|
||||
|
||||
// Apply global concurrency limit first
|
||||
if err := sc.requestSemaphore.Acquire(context.Background(), 1); err != nil {
|
||||
// C4 (smallest): propagate r.Context for cancellation (review item)
|
||||
if err := sc.requestSemaphore.Acquire(r.Context(), 1); err != nil {
|
||||
sc.metrics.IncrementRateLimited()
|
||||
logger.Logger.Warn().Str("client_ip", clientIP).Msg("Server at capacity, rejecting request")
|
||||
http.Error(w, "Server busy, please try again later", http.StatusServiceUnavailable)
|
||||
@@ -1095,7 +1112,8 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Apply per-client rate limiting
|
||||
clientLimiter := sc.getOrCreateClientLimiter(clientIP)
|
||||
|
||||
if err := clientLimiter.semaphore.Acquire(context.Background(), 1); err != nil {
|
||||
// C4 (smallest): per-client too
|
||||
if err := clientLimiter.semaphore.Acquire(r.Context(), 1); err != nil {
|
||||
logger.Logger.Warn().
|
||||
Str("client_ip", clientIP).
|
||||
Int("max_per_client", int(sc.maxRequestsPerClient)).
|
||||
@@ -1339,7 +1357,7 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, ur, nil)
|
||||
req, err = http.NewRequestWithContext(r.Context(), http.MethodGet, ur, nil)
|
||||
if err != nil {
|
||||
logger.Logger.Error().Err(err).Str("upstream", sc.upstream).Msg("Failed to create request")
|
||||
http.Error(w, "Failed to create request", http.StatusInternalServerError)
|
||||
@@ -1361,7 +1379,7 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, ur, nil)
|
||||
req, err = http.NewRequestWithContext(r.Context(), http.MethodGet, ur, nil)
|
||||
if err != nil {
|
||||
logger.Logger.Error().Err(err).Str("host", host).Msg("Failed to create request")
|
||||
http.Error(w, "Failed to create request", http.StatusInternalServerError)
|
||||
|
||||
Reference in New Issue
Block a user