package reality import ( "bufio" "fmt" "io" "net" "net/http" "net/netip" "regexp" "strings" "time" ) var domainRe = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9\-.]+\.[A-Za-z]{2,}$`) // urlDomainRe extracts domains from HTML/text content. var urlDomainRe = regexp.MustCompile(`https?://([A-Za-z0-9][A-Za-z0-9\-.]+\.[A-Za-z]{2,})`) // ParseTarget parses a single string as IP, CIDR, or domain and returns hosts. func ParseTarget(s string, ipv6 bool) ([]Host, error) { s = strings.TrimSpace(s) if s == "" { return nil, nil } // Try as CIDR. if strings.Contains(s, "/") { prefix, err := netip.ParsePrefix(s) if err != nil { return nil, fmt.Errorf("invalid CIDR %q: %w", s, err) } return expandCIDR(prefix, ipv6), nil } // Try as IP. if ip := net.ParseIP(s); ip != nil { if !ipv6 && ip.To4() == nil { return nil, nil } return []Host{{IP: ip, Origin: s}}, nil } // Try as domain. if domainRe.MatchString(s) { return resolveDomain(s, ipv6) } return nil, fmt.Errorf("unrecognized target %q", s) } // ParseTargetsFromReader reads line-separated targets and sends hosts to the returned channel. func ParseTargetsFromReader(r io.Reader, ipv6 bool) <-chan Host { ch := make(chan Host, 64) go func() { defer close(ch) scanner := bufio.NewScanner(r) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } hosts, err := ParseTarget(line, ipv6) if err != nil { continue // skip invalid lines } for _, h := range hosts { ch <- h } } }() return ch } // ParseTargetsFromURL fetches a URL, extracts domains from the response body, and returns hosts. func ParseTargetsFromURL(url string, ipv6 bool) ([]Host, error) { client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Get(url) if err != nil { return nil, fmt.Errorf("fetching %s: %w", url, err) } defer resp.Body.Close() body, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20)) // 10 MB limit if err != nil { return nil, fmt.Errorf("reading response: %w", err) } matches := urlDomainRe.FindAllSubmatch(body, -1) seen := make(map[string]bool) var hosts []Host for _, m := range matches { domain := string(m[1]) if seen[domain] { continue } seen[domain] = true resolved, err := resolveDomain(domain, ipv6) if err != nil { continue } hosts = append(hosts, resolved...) } return hosts, nil } func expandCIDR(prefix netip.Prefix, ipv6 bool) []Host { var hosts []Host origin := prefix.String() for addr := prefix.Addr(); prefix.Contains(addr); addr = addr.Next() { if !ipv6 && !addr.Is4() { continue } hosts = append(hosts, Host{ IP: addr.AsSlice(), Origin: origin, }) } return hosts } func resolveDomain(domain string, ipv6 bool) ([]Host, error) { ips, err := net.LookupIP(domain) if err != nil { return nil, fmt.Errorf("resolving %s: %w", domain, err) } var hosts []Host for _, ip := range ips { if !ipv6 && ip.To4() == nil { continue } hosts = append(hosts, Host{ IP: ip, Origin: domain, }) } if len(hosts) == 0 { return nil, fmt.Errorf("no suitable addresses for %s", domain) } return hosts, nil }