c3464d692e
- 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.
166 lines
4.7 KiB
Go
166 lines
4.7 KiB
Go
// 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
|
|
}
|