package main import ( "bufio" "os" "strings" "sync" ) // GatedHosts is the set of hostnames that require FIDO2 authentication. // Format matches k_phone's gated_hosts.txt: one "host" or "host:port" per line, // lines starting with "#" and blank lines are ignored. type GatedHosts struct { mu sync.RWMutex entries map[string]bool } // Load reads the gated hosts file. Missing file is not an error (empty list). func (g *GatedHosts) Load(path string) error { f, err := os.Open(path) if err != nil { if os.IsNotExist(err) { return nil } return err } defer f.Close() entries := make(map[string]bool) sc := bufio.NewScanner(f) for sc.Scan() { line := strings.TrimSpace(sc.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } // Normalise: lowercase, strip any trailing port-free colon. entries[strings.ToLower(line)] = true } g.mu.Lock() g.entries = entries g.mu.Unlock() return sc.Err() } // Len returns the number of entries in the gated list. func (g *GatedHosts) Len() int { g.mu.RLock() defer g.mu.RUnlock() return len(g.entries) } // IsGated returns true if host:port matches a gated entry. // An entry "example.com" matches any port; "example.com:8080" matches only port 8080. func (g *GatedHosts) IsGated(host, port string) bool { g.mu.RLock() defer g.mu.RUnlock() if len(g.entries) == 0 { return false } h := strings.ToLower(host) return g.entries[h] || (port != "" && g.entries[h+":"+port]) }