<commit_before>// +skip_license_check

/*
This file contains portions of code directly taken from the 'xenolf/lego' project.
A copy of the license for this code can be found in the file named LICENSE in
this directory.
*/

package util

import (
	"fmt"
	"net"
	"strings"
	"sync"
	"time"

	"github.com/miekg/dns"
	"k8s.io/klog"
)

type preCheckDNSFunc func(fqdn, value string, nameservers []string,
	useAuthoritative bool) (bool, error)

var (
	// PreCheckDNS checks DNS propagation before notifying ACME that
	// the DNS challenge is ready.
	PreCheckDNS preCheckDNSFunc = checkDNSPropagation

	fqdnToZoneLock sync.RWMutex
	fqdnToZone     = map[string]string{}
)

const defaultResolvConf = "/etc/resolv.conf"

var defaultNameservers = []string{
	"8.8.8.8:53",
	"8.8.4.4:53",
}

var RecursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers)

// DNSTimeout is used to override the default DNS timeout of 10 seconds.
var DNSTimeout = 10 * time.Second

// getNameservers attempts to get systems nameservers before falling back to the defaults
func getNameservers(path string, defaults []string) []string {
	config, err := dns.ClientConfigFromFile(path)
	if err != nil || len(config.Servers) == 0 {
		return defaults
	}

	systemNameservers := []string{}
	for _, server := range config.Servers {
		// ensure all servers have a port number
		if _, _, err := net.SplitHostPort(server); err != nil {
			systemNameservers = append(systemNameservers, net.JoinHostPort(server, "53"))
		} else {
			systemNameservers = append(systemNameservers, server)
		}
	}
	return systemNameservers
}

// Update FQDN with CNAME if any
func updateDomainWithCName(r *dns.Msg, fqdn string) string {
	for _, rr := range r.Answer {
		if cn, ok := rr.(*dns.CNAME); ok {
			if cn.Hdr.Name == fqdn {
				klog.Infof("Updating FQDN: %s with its CNAME: %s", fqdn, cn.Target)
				fqdn = cn.Target
				break
			}
		}
	}

	return fqdn
}

// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
func checkDNSPropagation(fqdn, value string, nameservers []string,
	useAuthoritative bool) (bool, error) {
	// Initial attempt to resolve at the recursive NS
	r, err := dnsQuery(fqdn, dns.TypeTXT, nameservers, true)
	if err != nil {
		return false, err
	}
	if r.Rcode == dns.RcodeSuccess {
		fqdn = updateDomainWithCName(r, fqdn)
	}

	if !useAuthoritative {
		return checkAuthoritativeNss(fqdn, value, nameservers)
	}

	authoritativeNss, err := lookupNameservers(fqdn, nameservers)
	if err != nil {
		return false, err
	}

	for i, ans := range authoritativeNss {
		authoritativeNss[i] = net.JoinHostPort(ans, "53")
	}
	return checkAuthoritativeNss(fqdn, value, authoritativeNss)
}

// checkAuthoritativeNss queries each of the given nameservers for the expected TXT record.
func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) {
	for _, ns := range nameservers {
		r, err := dnsQuery(fqdn, dns.TypeTXT, []string{ns}, true)
		if err != nil {
			return false, err
		}

		// NXDomain response is not really an error, just waiting for propagation to happen
		if !(r.Rcode == dns.RcodeSuccess || r.Rcode == dns.RcodeNameError) {
			return false, fmt.Errorf("NS %s returned %s for %s", ns, dns.RcodeToString[r.Rcode], fqdn)
		}

		klog.V(6).Infof("Looking up TXT records for %q", fqdn)
		var found bool
		for _, rr := range r.Answer {
			if txt, ok := rr.(*dns.TXT); ok {
				if strings.Join(txt.Txt, "") == value {
					found = true
					break
				}
			}
		}

		if !found {
			return false, nil
		}
	}

	return true, nil
}

// dnsQuery will query a nameserver, iterating through the supplied servers as it retries
// The nameserver should include a port, to facilitate testing where we talk to a mock dns server.
func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (in *dns.Msg, err error) {
	m := new(dns.Msg)
	m.SetQuestion(fqdn, rtype)
	m.SetEdns0(4096, false)

	if !recursive {
		m.RecursionDesired = false
	}

	// Will retry the request based on the number of servers (n+1)
	for i := 1; i <= len(nameservers)+1; i++ {
		ns := nameservers[i%len(nameservers)]
		udp := &dns.Client{Net: "udp", Timeout: DNSTimeout}
		in, _, err = udp.Exchange(m, ns)

		if err == dns.ErrTruncated ||
			(err != nil && strings.HasPrefix(err.Error(), "read udp") && strings.HasSuffix(err.Error(), "i/o timeout")) {
			klog.V(6).Infof("UDP dns lookup failed, retrying with TCP: %v", err)
			tcp := &dns.Client{Net: "tcp", Timeout: DNSTimeout}
			// If the TCP request succeeds, the err will reset to nil
			in, _, err = tcp.Exchange(m, ns)
		}

		if err == nil {
			break
		}
	}
	return
}

func ValidateCAA(domain string, issuerID []string, iswildcard bool, nameservers []string) error {
	// see https://tools.ietf.org/html/rfc6844#section-4
	// for more information about how CAA lookup is performed
	fqdn := ToFqdn(domain)

	issuerSet := make(map[string]bool)
	for _, s := range issuerID {
		issuerSet[s] = true
	}

	var caas []*dns.CAA
	for {
		// follow at most 8 cnames per label
		queryDomain := fqdn
		var msg *dns.Msg
		var err error
		for i := 0; i < 8; i++ {
			//TODO(dmo): figure out if we need these servers to be configurable as well
			msg, err = dnsQuery(queryDomain, dns.TypeCAA, nameservers, true)
			if err != nil {
				return fmt.Errorf("Could not validate CAA record: %s", err)
			}
			// domain may not exist, which is fine. It will fail HTTP01 checks
			// but DNS01 checks will create a proper domain
			if msg.Rcode == dns.RcodeNameError {
				break
			}
			if msg.Rcode != dns.RcodeSuccess {
				return fmt.Errorf("Could not validate CAA: Unexpected response code '%s' for %s",
					dns.RcodeToString[msg.Rcode], domain)
			}
			oldQuery := queryDomain
			queryDomain = updateDomainWithCName(msg, queryDomain)
			if queryDomain == oldQuery {
				break
			}
		}
		// we have a response that's not a CNAME. It might be empty.
		// if it is, go up a label and ask again
		for _, rr := range msg.Answer {
			caa, ok := rr.(*dns.CAA)
			if !ok {
				continue
			}
			caas = append(caas, caa)
		}
		if len(caas) != 0 {<commit_msg>use authoritative nameservers for CAA checks<commit_after>// +skip_license_check

/*
This file contains portions of code directly taken from the 'xenolf/lego' project.
A copy of the license for this code can be found in the file named LICENSE in
this directory.
*/

package util

import (
	"fmt"
	"net"
	"strings"
	"sync"
	"time"

	"github.com/miekg/dns"
	"k8s.io/klog"
)

type preCheckDNSFunc func(fqdn, value string, nameservers []string,
	useAuthoritative bool) (bool, error)

var (
	// PreCheckDNS checks DNS propagation before notifying ACME that
	// the DNS challenge is ready.
	PreCheckDNS preCheckDNSFunc = checkDNSPropagation

	fqdnToZoneLock sync.RWMutex
	fqdnToZone     = map[string]string{}
)

const defaultResolvConf = "/etc/resolv.conf"

var defaultNameservers = []string{
	"8.8.8.8:53",
	"8.8.4.4:53",
}

var RecursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers)

// DNSTimeout is used to override the default DNS timeout of 10 seconds.
var DNSTimeout = 10 * time.Second

// getNameservers attempts to get systems nameservers before falling back to the defaults
func getNameservers(path string, defaults []string) []string {
	config, err := dns.ClientConfigFromFile(path)
	if err != nil || len(config.Servers) == 0 {
		return defaults
	}

	systemNameservers := []string{}
	for _, server := range config.Servers {
		// ensure all servers have a port number
		if _, _, err := net.SplitHostPort(server); err != nil {
			systemNameservers = append(systemNameservers, net.JoinHostPort(server, "53"))
		} else {
			systemNameservers = append(systemNameservers, server)
		}
	}
	return systemNameservers
}

// Update FQDN with CNAME if any
func updateDomainWithCName(r *dns.Msg, fqdn string) string {
	for _, rr := range r.Answer {
		if cn, ok := rr.(*dns.CNAME); ok {
			if cn.Hdr.Name == fqdn {
				klog.Infof("Updating FQDN: %s with its CNAME: %s", fqdn, cn.Target)
				fqdn = cn.Target
				break
			}
		}
	}

	return fqdn
}

// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
func checkDNSPropagation(fqdn, value string, nameservers []string,
	useAuthoritative bool) (bool, error) {
	// Initial attempt to resolve at the recursive NS
	r, err := dnsQuery(fqdn, dns.TypeTXT, nameservers, true)
	if err != nil {
		return false, err
	}
	if r.Rcode == dns.RcodeSuccess {
		fqdn = updateDomainWithCName(r, fqdn)
	}

	if !useAuthoritative {
		return checkAuthoritativeNss(fqdn, value, nameservers)
	}

	authoritativeNss, err := lookupNameservers(fqdn, nameservers)
	if err != nil {
		return false, err
	}

	for i, ans := range authoritativeNss {
		authoritativeNss[i] = net.JoinHostPort(ans, "53")
	}
	return checkAuthoritativeNss(fqdn, value, authoritativeNss)
}

// checkAuthoritativeNss queries each of the given nameservers for the expected TXT record.
func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) {
	for _, ns := range nameservers {
		r, err := dnsQuery(fqdn, dns.TypeTXT, []string{ns}, true)
		if err != nil {
			return false, err
		}

		// NXDomain response is not really an error, just waiting for propagation to happen
		if !(r.Rcode == dns.RcodeSuccess || r.Rcode == dns.RcodeNameError) {
			return false, fmt.Errorf("NS %s returned %s for %s", ns, dns.RcodeToString[r.Rcode], fqdn)
		}

		klog.V(6).Infof("Looking up TXT records for %q", fqdn)
		var found bool
		for _, rr := range r.Answer {
			if txt, ok := rr.(*dns.TXT); ok {
				if strings.Join(txt.Txt, "") == value {
					found = true
					break
				}
			}
		}

		if !found {
			return false, nil
		}
	}

	return true, nil
}

// dnsQuery will query a nameserver, iterating through the supplied servers as it retries
// The nameserver should include a port, to facilitate testing where we talk to a mock dns server.
func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (in *dns.Msg, err error) {
	m := new(dns.Msg)
	m.SetQuestion(fqdn, rtype)
	m.SetEdns0(4096, false)

	if !recursive {
		m.RecursionDesired = false
	}

	// Will retry the request based on the number of servers (n+1)
	for i := 1; i <= len(nameservers)+1; i++ {
		ns := nameservers[i%len(nameservers)]
		udp := &dns.Client{Net: "udp", Timeout: DNSTimeout}
		in, _, err = udp.Exchange(m, ns)

		if err == dns.ErrTruncated ||
			(err != nil && strings.HasPrefix(err.Error(), "read udp") && strings.HasSuffix(err.Error(), "i/o timeout")) {
			klog.V(6).Infof("UDP dns lookup failed, retrying with TCP: %v", err)
			tcp := &dns.Client{Net: "tcp", Timeout: DNSTimeout}
			// If the TCP request succeeds, the err will reset to nil
			in, _, err = tcp.Exchange(m, ns)
		}

		if err == nil {
			break
		}
	}
	return
}

func ValidateCAA(domain string, issuerID []string, iswildcard bool, nameservers []string) error {
	// see https://tools.ietf.org/html/rfc6844#section-4
	// for more information about how CAA lookup is performed
	fqdn := ToFqdn(domain)

	issuerSet := make(map[string]bool)
	for _, s := range issuerID {
		issuerSet[s] = true
	}

	var caas []*dns.CAA
	for {
		// follow at most 8 cnames per label
		queryDomain := fqdn
		var msg *dns.Msg
		var err error
		for i := 0; i < 8; i++ {
			// usually, we should be able to just ask the local recursive
			// nameserver for CAA records, but some setups will return SERVFAIL
			// on unknown types like CAA. Instead, ask the authoritative server
			var authNS []string
			authNS, err = lookupNameservers(queryDomain, nameservers)
			if err != nil {
				return fmt.Errorf("Could not validate CAA record: %s", err)
			}
			for i, ans := range authNS {
				authNS[i] = net.JoinHostPort(ans, "53")
			}
			msg, err = dnsQuery(queryDomain, dns.TypeCAA, authNS, false)
			if err != nil {
				return fmt.Errorf("Could not validate CAA record: %s", err)
			}
			// domain may not exist, which is fine. It will fail HTTP01 checks
			// but DNS01 checks will create a proper domain
			if msg.Rcode == dns.RcodeNameError {
				break
			}
			if msg.Rcode != dns.RcodeSuccess {
				return fmt.Errorf("Could not validate CAA: Unexpected response code '%s' for %s",
					dns.RcodeToString[msg.Rcode], domain)
			}
			oldQuery := queryDomain
			queryDomain = updateDomainWithCName(msg, queryDomain)
			if queryDomain == oldQuery {
				break
			}
		}
		// we have a response that's not a CNAME. It might be empty.
		// if it is, go up a label and ask again
		for _, rr := range msg.Answer {
			caa, ok := rr.(*dns.CAA)
			if !ok {
				continue
			}
			caas = append(caas, caa)
		}
		if len(caas) != 0 {
{'fraction_non_alphanumeric': 0.11702296120348377, 'fraction_numerical': 0.007600950118764846, 'mean_word_length': 3.540258806613947, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 3, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 4, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '342138', 'n_tokens_mistral': 4925, 'n_tokens_neox': 4478, 'n_words': 1393}