// 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 }