Add core components for request coalescing and service management
- Introduced coalescing logic in `coalescing.go` to handle concurrent identical upstream fetches, including a state machine and response buffering for improved performance. - Implemented a new cache file format in `format.go`, supporting serialization and deserialization of HTTP responses, along with range request handling. - Developed an HTTP handler in `handler.go` to manage requests, including special endpoint handling and metrics reporting. - Added rate limiting functionality in `ratelimit.go` to control per-client and global request rates, enhancing security and performance. - Created service management capabilities in `service.go` to define and manage cacheable services, including user-agent detection. - Updated tests in `steamcache_test.go` to cover new functionalities, ensuring robustness and reliability across the codebase.
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
// steamcache/service.go
|
||||
package steamcache
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ServiceConfig defines configuration for a cacheable service
|
||||
type ServiceConfig struct {
|
||||
Name string `json:"name"` // Service name (e.g., "steam", "epic", "origin")
|
||||
Prefix string `json:"prefix"` // Cache key prefix (e.g., "steam", "epic")
|
||||
UserAgents []string `json:"user_agents"` // User-Agent patterns to match
|
||||
compiled []*regexp.Regexp // Compiled regex patterns (internal use)
|
||||
}
|
||||
|
||||
// ServiceManager manages service configurations
|
||||
type ServiceManager struct {
|
||||
services map[string]*ServiceConfig
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewServiceManager creates a new service manager with default Steam configuration
|
||||
func NewServiceManager() *ServiceManager {
|
||||
sm := &ServiceManager{
|
||||
services: make(map[string]*ServiceConfig),
|
||||
}
|
||||
|
||||
// Add default Steam service configuration
|
||||
steamConfig := &ServiceConfig{
|
||||
Name: "steam",
|
||||
Prefix: "steam",
|
||||
UserAgents: []string{
|
||||
`Valve/Steam HTTP Client 1\.0`,
|
||||
`SteamClient`,
|
||||
`Steam`,
|
||||
},
|
||||
}
|
||||
_ = sm.AddService(steamConfig) // error impossible: hardcoded patterns are valid regexes (user-provided services validated in AddService)
|
||||
|
||||
return sm
|
||||
}
|
||||
|
||||
// AddService adds or updates a service configuration
|
||||
func (sm *ServiceManager) AddService(config *ServiceConfig) error {
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
|
||||
// Compile regex patterns
|
||||
compiled := make([]*regexp.Regexp, 0, len(config.UserAgents))
|
||||
for _, pattern := range config.UserAgents {
|
||||
regex, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid regex pattern %q for service %s: %w", pattern, config.Name, err)
|
||||
}
|
||||
compiled = append(compiled, regex)
|
||||
}
|
||||
|
||||
config.compiled = compiled
|
||||
sm.services[config.Name] = config
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetService returns a service configuration by name
|
||||
func (sm *ServiceManager) GetService(name string) (*ServiceConfig, bool) {
|
||||
sm.mutex.RLock()
|
||||
defer sm.mutex.RUnlock()
|
||||
|
||||
service, exists := sm.services[name]
|
||||
return service, exists
|
||||
}
|
||||
|
||||
// DetectService detects which service a request belongs to based on User-Agent
|
||||
func (sm *ServiceManager) DetectService(userAgent string) (*ServiceConfig, bool) {
|
||||
sm.mutex.RLock()
|
||||
defer sm.mutex.RUnlock()
|
||||
|
||||
for _, service := range sm.services {
|
||||
for _, regex := range service.compiled {
|
||||
if regex.MatchString(userAgent) {
|
||||
return service, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// ListServices returns all configured services
|
||||
func (sm *ServiceManager) ListServices() []*ServiceConfig {
|
||||
sm.mutex.RLock()
|
||||
defer sm.mutex.RUnlock()
|
||||
|
||||
services := make([]*ServiceConfig, 0, len(sm.services))
|
||||
for _, service := range sm.services {
|
||||
services = append(services, service)
|
||||
}
|
||||
|
||||
return services
|
||||
}
|
||||
|
||||
// detectService is a one-line delegation to ServiceManager (per Phase 2).
|
||||
// Empty User-Agent yields no match (identical to prior behavior).
|
||||
func (sc *SteamCache) detectService(r *http.Request) (*ServiceConfig, bool) {
|
||||
return sc.serviceManager.DetectService(r.Header.Get("User-Agent"))
|
||||
}
|
||||
|
||||
// --- Cache key / hash helpers (service-related) ---
|
||||
|
||||
func generateURLHash(urlPath string) (string, error) {
|
||||
if urlPath == "" {
|
||||
return "", fmt.Errorf("empty URL path")
|
||||
}
|
||||
// Additional validation for suspicious patterns (kept for backward compat with prior behavior)
|
||||
if strings.Contains(urlPath, "..") || strings.Contains(urlPath, "//") {
|
||||
return "", fmt.Errorf("generateURLHash: invalid URL path")
|
||||
}
|
||||
hash := sha256.Sum256([]byte(urlPath))
|
||||
return hex.EncodeToString(hash[:]), nil
|
||||
}
|
||||
|
||||
func calculateSHA256(data []byte) string {
|
||||
hasher := sha256.New()
|
||||
hasher.Write(data)
|
||||
return hex.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
// validateURLPath validates URL path for security concerns (used early in request handling).
|
||||
func validateURLPath(urlPath string) error {
|
||||
if urlPath == "" {
|
||||
return fmt.Errorf("validateURLPath: invalid URL path")
|
||||
}
|
||||
if strings.Contains(urlPath, "..") {
|
||||
return fmt.Errorf("validateURLPath: invalid URL path")
|
||||
}
|
||||
if strings.Contains(urlPath, "//") {
|
||||
return fmt.Errorf("validateURLPath: invalid URL path")
|
||||
}
|
||||
if strings.ContainsAny(urlPath, "<>\"'&") {
|
||||
return fmt.Errorf("validateURLPath: invalid URL path")
|
||||
}
|
||||
if len(urlPath) > 2048 {
|
||||
return fmt.Errorf("validateURLPath: invalid URL path")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateServiceCacheKey creates a cache key from the URL path using SHA256
|
||||
// The prefix indicates which service the request came from (detected via User-Agent)
|
||||
func generateServiceCacheKey(urlPath string, servicePrefix string) (string, error) {
|
||||
if servicePrefix == "" {
|
||||
return "", fmt.Errorf("generateServiceCacheKey: unsupported service")
|
||||
}
|
||||
hash, err := generateURLHash(urlPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return servicePrefix + "/" + hash, nil
|
||||
}
|
||||
Reference in New Issue
Block a user