// Package allowlist provides host-based URL allow-listing for the image proxy. package allowlist import ( "net/url" "strings" ) // HostAllowList checks whether source hosts are permitted. type HostAllowList struct { // exactHosts contains hosts that must match exactly (e.g., "cdn.example.com") exactHosts map[string]struct{} // suffixHosts contains domain suffixes to match (e.g., ".example.com" matches "cdn.example.com") suffixHosts []string } // New creates a HostAllowList from a list of host patterns. // Patterns starting with "." are treated as suffix matches. // Examples: // - "cdn.example.com" - exact match only // - ".example.com" - matches cdn.example.com, images.example.com, etc. func New(patterns []string) *HostAllowList { w := &HostAllowList{ exactHosts: make(map[string]struct{}), suffixHosts: make([]string, 0), } for _, pattern := range patterns { pattern = strings.ToLower(strings.TrimSpace(pattern)) if pattern == "" { continue } if strings.HasPrefix(pattern, ".") { w.suffixHosts = append(w.suffixHosts, pattern) } else { w.exactHosts[pattern] = struct{}{} } } return w } // IsAllowed checks if a URL's host is in the allow list. func (w *HostAllowList) IsAllowed(u *url.URL) bool { if u == nil { return false } host := strings.ToLower(u.Hostname()) if host == "" { return false } // Check exact match if _, ok := w.exactHosts[host]; ok { return true } // Check suffix match for _, suffix := range w.suffixHosts { if strings.HasSuffix(host, suffix) { return true } // Also match if host equals the suffix without the leading dot // e.g., pattern ".example.com" should match "example.com" if host == strings.TrimPrefix(suffix, ".") { return true } } return false } // IsEmpty returns true if the allow list has no entries. func (w *HostAllowList) IsEmpty() bool { return len(w.exactHosts) == 0 && len(w.suffixHosts) == 0 } // Count returns the total number of allow list entries. func (w *HostAllowList) Count() int { return len(w.exactHosts) + len(w.suffixHosts) }