// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build !js

package adns

import (
	"errors"
	"io/fs"
	"os"
	"runtime"
	"sync"
	"syscall"

	"github.com/mjl-/adns/internal/bytealg"
)

// The net package's name resolution is rather complicated.
// There are two main approaches, go and cgo.
// The cgo resolver uses C functions like getaddrinfo.
// The go resolver reads system files directly and
// sends DNS packets directly to servers.
//
// The netgo build tag prefers the go resolver.
// The netcgo build tag prefers the cgo resolver.
//
// The netgo build tag also prohibits the use of the cgo tool.
// However, on Darwin, Plan 9, and Windows the cgo resolver is still available.
// On those systems the cgo resolver does not require the cgo tool.
// (The term "cgo resolver" was locked in by GODEBUG settings
// at a time when the cgo resolver did require the cgo tool.)
//
// Adding netdns=go to GODEBUG will prefer the go resolver.
// Adding netdns=cgo to GODEBUG will prefer the cgo resolver.
//
// The Resolver struct has a PreferGo field that user code
// may set to prefer the go resolver. It is documented as being
// equivalent to adding netdns=go to GODEBUG.
//
// When deciding which resolver to use, we first check the PreferGo field.
// If that is not set, we check the GODEBUG setting.
// If that is not set, we check the netgo or netcgo build tag.
// If none of those are set, we normally prefer the go resolver by default.
// However, if the cgo resolver is available,
// there is a complex set of conditions for which we prefer the cgo resolver.
//
// Other files define the netGoBuildTag, netCgoBuildTag, and cgoAvailable
// constants.

// conf is used to determine name resolution configuration.
type conf struct {
	netGo  bool // prefer go approach, based on build tag and GODEBUG
	netCgo bool // prefer cgo approach, based on build tag and GODEBUG

	dnsDebugLevel int // from GODEBUG

	preferCgo bool // if no explicit preference, use cgo

	goos     string   // copy of runtime.GOOS, used for testing
	mdnsTest mdnsTest // assume /etc/mdns.allow exists, for testing
}

// mdnsTest is for testing only.
type mdnsTest int

const (
	mdnsFromSystem mdnsTest = iota
	mdnsAssumeExists
	mdnsAssumeDoesNotExist
)

var (
	confOnce sync.Once // guards init of confVal via initConfVal
	confVal  = &conf{goos: runtime.GOOS}
)

// systemConf returns the machine's network configuration.
func systemConf() *conf {
	confOnce.Do(initConfVal)
	return confVal
}

var netGoBuildTag = true
var netCgoBuildTag = false
var cgoAvailable = false

// initConfVal initializes confVal based on the environment
// that will not change during program execution.
func initConfVal() {
	dnsMode, debugLevel := goDebugNetDNS()
	confVal.netGo = netGoBuildTag || dnsMode == "go"
	confVal.netCgo = netCgoBuildTag || dnsMode == "cgo"
	confVal.dnsDebugLevel = debugLevel

	if confVal.dnsDebugLevel > 0 {
		defer func() {
			if confVal.dnsDebugLevel > 1 {
				println("go package net: confVal.netCgo =", confVal.netCgo, " netGo =", confVal.netGo)
			}
			switch {
			case confVal.netGo:
				if netGoBuildTag {
					println("go package net: built with netgo build tag; using Go's DNS resolver")
				} else {
					println("go package net: GODEBUG setting forcing use of Go's resolver")
				}
			case !cgoAvailable:
				println("go package net: cgo resolver not supported; using Go's DNS resolver")
			case confVal.netCgo || confVal.preferCgo:
				println("go package net: using cgo DNS resolver")
			default:
				println("go package net: dynamic selection of DNS resolver")
			}
		}()
	}

	// The remainder of this function sets preferCgo based on
	// conditions that will not change during program execution.

	// By default, prefer the go resolver.
	confVal.preferCgo = false

	// If the cgo resolver is not available, we can't prefer it.
	if !cgoAvailable {
		return
	}

	// Some operating systems always prefer the cgo resolver.
	if goosPrefersCgo() {
		confVal.preferCgo = true
		return
	}

	// The remaining checks are specific to Unix systems.
	switch runtime.GOOS {
	case "plan9", "windows", "js", "wasip1":
		return
	}

	// If any environment-specified resolver options are specified,
	// prefer the cgo resolver.
	// Note that LOCALDOMAIN can change behavior merely by being
	// specified with the empty string.
	_, localDomainDefined := syscall.Getenv("LOCALDOMAIN")
	if localDomainDefined || os.Getenv("RES_OPTIONS") != "" || os.Getenv("HOSTALIASES") != "" {
		confVal.preferCgo = true
		return
	}

	// OpenBSD apparently lets you override the location of resolv.conf
	// with ASR_CONFIG. If we notice that, defer to libc.
	if runtime.GOOS == "openbsd" && os.Getenv("ASR_CONFIG") != "" {
		confVal.preferCgo = true
		return
	}
}

// goosPrefersCgo reports whether the GOOS value passed in prefers
// the cgo resolver.
func goosPrefersCgo() bool {
	switch runtime.GOOS {
	// Historically on Windows and Plan 9 we prefer the
	// cgo resolver (which doesn't use the cgo tool) rather than
	// the go resolver. This is because originally these
	// systems did not support the go resolver.
	// Keep it this way for better compatibility.
	// Perhaps we can revisit this some day.
	case "windows", "plan9":
		return true

	// Darwin pops up annoying dialog boxes if programs try to
	// do their own DNS requests, so prefer cgo.
	case "darwin", "ios":
		return true

	// DNS requests don't work on Android, so prefer the cgo resolver.
	// Issue #10714.
	case "android":
		return true

	default:
		return false
	}
}

// mustUseGoResolver reports whether a DNS lookup of any sort is
// required to use the go resolver. The provided Resolver is optional.
// This will report true if the cgo resolver is not available.
func (c *conf) mustUseGoResolver(r *Resolver) bool {
	return c.netGo || r.preferGo() || !cgoAvailable
}

// addrLookupOrder determines which strategy to use to resolve addresses.
// The provided Resolver is optional. nil means to not consider its options.
// It also returns dnsConfig when it was used to determine the lookup order.
func (c *conf) addrLookupOrder(r *Resolver, addr string) (ret hostLookupOrder, dnsConf *dnsConfig) {
	if c.dnsDebugLevel > 1 {
		defer func() {
			print("go package net: addrLookupOrder(", addr, ") = ", ret.String(), "\n")
		}()
	}
	return c.lookupOrder(r, "")
}

// hostLookupOrder determines which strategy to use to resolve hostname.
// The provided Resolver is optional. nil means to not consider its options.
// It also returns dnsConfig when it was used to determine the lookup order.
func (c *conf) hostLookupOrder(r *Resolver, hostname string) (ret hostLookupOrder, dnsConf *dnsConfig) {
	if c.dnsDebugLevel > 1 {
		defer func() {
			print("go package net: hostLookupOrder(", hostname, ") = ", ret.String(), "\n")
		}()
	}
	return c.lookupOrder(r, hostname)
}

func (c *conf) lookupOrder(r *Resolver, hostname string) (ret hostLookupOrder, dnsConf *dnsConfig) {
	// fallbackOrder is the order we return if we can't figure it out.
	var fallbackOrder hostLookupOrder

	var canUseCgo bool
	if c.mustUseGoResolver(r) {
		// Go resolver was explicitly requested
		// or cgo resolver is not available.
		// Figure out the order below.
		fallbackOrder = hostLookupFilesDNS
		canUseCgo = false
	} else if c.netCgo {
		// Cgo resolver was explicitly requested.
		return hostLookupCgo, nil
	} else if c.preferCgo {
		// Given a choice, we prefer the cgo resolver.
		return hostLookupCgo, nil
	} else {
		// Neither resolver was explicitly requested
		// and we have no preference.

		if bytealg.IndexByteString(hostname, '\\') != -1 || bytealg.IndexByteString(hostname, '%') != -1 {
			// Don't deal with special form hostnames
			// with backslashes or '%'.
			return hostLookupCgo, nil
		}

		// If something is unrecognized, use cgo.
		fallbackOrder = hostLookupCgo
		canUseCgo = true
	}

	// On systems that don't use /etc/resolv.conf or /etc/nsswitch.conf, we are done.
	switch c.goos {
	case "windows", "plan9", "android", "ios":
		return fallbackOrder, nil
	}

	// Try to figure out the order to use for searches.
	// If we don't recognize something, use fallbackOrder.
	// That will use cgo unless the Go resolver was explicitly requested.
	// If we do figure out the order, return something other
	// than fallbackOrder to use the Go resolver with that order.

	dnsConf = getSystemDNSConfig()

	if canUseCgo && dnsConf.err != nil && !errors.Is(dnsConf.err, fs.ErrNotExist) && !errors.Is(dnsConf.err, fs.ErrPermission) {
		// We can't read the resolv.conf file, so use cgo if we can.
		return hostLookupCgo, dnsConf
	}

	if canUseCgo && dnsConf.unknownOpt {
		// We didn't recognize something in resolv.conf,
		// so use cgo if we can.
		return hostLookupCgo, dnsConf
	}

	// OpenBSD is unique and doesn't use nsswitch.conf.
	// It also doesn't support mDNS.
	if c.goos == "openbsd" {
		// OpenBSD's resolv.conf manpage says that a
		// non-existent resolv.conf means "lookup" defaults
		// to only "files", without DNS lookups.
		if errors.Is(dnsConf.err, fs.ErrNotExist) {
			return hostLookupFiles, dnsConf
		}

		lookup := dnsConf.lookup
		if len(lookup) == 0 {
			// https://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man5/resolv.conf.5
			// "If the lookup keyword is not used in the
			// system's resolv.conf file then the assumed
			// order is 'bind file'"
			return hostLookupDNSFiles, dnsConf
		}
		if len(lookup) < 1 || len(lookup) > 2 {
			// We don't recognize this format.
			return fallbackOrder, dnsConf
		}
		switch lookup[0] {
		case "bind":
			if len(lookup) == 2 {
				if lookup[1] == "file" {
					return hostLookupDNSFiles, dnsConf
				}
				// Unrecognized.
				return fallbackOrder, dnsConf
			}
			return hostLookupDNS, dnsConf
		case "file":
			if len(lookup) == 2 {
				if lookup[1] == "bind" {
					return hostLookupFilesDNS, dnsConf
				}
				// Unrecognized.
				return fallbackOrder, dnsConf
			}
			return hostLookupFiles, dnsConf
		default:
			// Unrecognized.
			return fallbackOrder, dnsConf
		}

		// We always return before this point.
		// The code below is for non-OpenBSD.
	}

	// Canonicalize the hostname by removing any trailing dot.
	if stringsHasSuffix(hostname, ".") {
		hostname = hostname[:len(hostname)-1]
	}
	if canUseCgo && stringsHasSuffixFold(hostname, ".local") {
		// Per RFC 6762, the ".local" TLD is special. And
		// because Go's native resolver doesn't do mDNS or
		// similar local resolution mechanisms, assume that
		// libc might (via Avahi, etc) and use cgo.
		return hostLookupCgo, dnsConf
	}

	nss := getSystemNSS()
	srcs := nss.sources["hosts"]
	// If /etc/nsswitch.conf doesn't exist or doesn't specify any
	// sources for "hosts", assume Go's DNS will work fine.
	if errors.Is(nss.err, fs.ErrNotExist) || (nss.err == nil && len(srcs) == 0) {
		if canUseCgo && c.goos == "solaris" {
			// illumos defaults to
			// "nis [NOTFOUND=return] files",
			// which the go resolver doesn't support.
			return hostLookupCgo, dnsConf
		}

		return hostLookupFilesDNS, dnsConf
	}
	if nss.err != nil {
		// We failed to parse or open nsswitch.conf, so
		// we have nothing to base an order on.
		return fallbackOrder, dnsConf
	}

	var hasDNSSource bool
	var hasDNSSourceChecked bool

	var filesSource, dnsSource bool
	var first string
	for i, src := range srcs {
		if src.source == "files" || src.source == "dns" {
			if canUseCgo && !src.standardCriteria() {
				// non-standard; let libc deal with it.
				return hostLookupCgo, dnsConf
			}
			if src.source == "files" {
				filesSource = true
			} else {
				hasDNSSource = true
				hasDNSSourceChecked = true
				dnsSource = true
			}
			if first == "" {
				first = src.source
			}
			continue
		}

		if canUseCgo {
			switch {
			case hostname != "" && src.source == "myhostname":
				// Let the cgo resolver handle myhostname
				// if we are looking up the local hostname.
				if isLocalhost(hostname) || isGateway(hostname) || isOutbound(hostname) {
					return hostLookupCgo, dnsConf
				}
				hn, err := getHostname()
				if err != nil || stringsEqualFold(hostname, hn) {
					return hostLookupCgo, dnsConf
				}
				continue
			case hostname != "" && stringsHasPrefix(src.source, "mdns"):
				// e.g. "mdns4", "mdns4_minimal"
				// We already returned true before if it was *.local.
				// libc wouldn't have found a hit on this anyway.

				// We don't parse mdns.allow files. They're rare. If one
				// exists, it might list other TLDs (besides .local) or even
				// '*', so just let libc deal with it.
				var haveMDNSAllow bool
				switch c.mdnsTest {
				case mdnsFromSystem:
					_, err := os.Stat("/etc/mdns.allow")
					if err != nil && !errors.Is(err, fs.ErrNotExist) {
						// Let libc figure out what is going on.
						return hostLookupCgo, dnsConf
					}
					haveMDNSAllow = err == nil
				case mdnsAssumeExists:
					haveMDNSAllow = true
				case mdnsAssumeDoesNotExist:
					haveMDNSAllow = false
				}
				if haveMDNSAllow {
					return hostLookupCgo, dnsConf
				}
				continue
			default:
				// Some source we don't know how to deal with.
				return hostLookupCgo, dnsConf
			}
		}

		if !hasDNSSourceChecked {
			hasDNSSourceChecked = true
			for _, v := range srcs[i+1:] {
				if v.source == "dns" {
					hasDNSSource = true
					break
				}
			}
		}

		// If we saw a source we don't recognize, which can only
		// happen if we can't use the cgo resolver, treat it as DNS,
		// but only when there is no dns in all other sources.
		if !hasDNSSource {
			dnsSource = true
			if first == "" {
				first = "dns"
			}
		}
	}

	// Cases where Go can handle it without cgo and C thread overhead,
	// or where the Go resolver has been forced.
	switch {
	case filesSource && dnsSource:
		if first == "files" {
			return hostLookupFilesDNS, dnsConf
		} else {
			return hostLookupDNSFiles, dnsConf
		}
	case filesSource:
		return hostLookupFiles, dnsConf
	case dnsSource:
		return hostLookupDNS, dnsConf
	}

	// Something weird. Fallback to the default.
	return fallbackOrder, dnsConf
}

// goDebugNetDNS parses the value of the GODEBUG "netdns" value.
// The netdns value can be of the form:
//
//	1       // debug level 1
//	2       // debug level 2
//	cgo     // use cgo for DNS lookups
//	go      // use go for DNS lookups
//	cgo+1   // use cgo for DNS lookups + debug level 1
//	1+cgo   // same
//	cgo+2   // same, but debug level 2
//
// etc.
func goDebugNetDNS() (dnsMode string, debugLevel int) {
	return "go", 0
}

// isLocalhost reports whether h should be considered a "localhost"
// name for the myhostname NSS module.
func isLocalhost(h string) bool {
	return stringsEqualFold(h, "localhost") || stringsEqualFold(h, "localhost.localdomain") || stringsHasSuffixFold(h, ".localhost") || stringsHasSuffixFold(h, ".localhost.localdomain")
}

// isGateway reports whether h should be considered a "gateway"
// name for the myhostname NSS module.
func isGateway(h string) bool {
	return stringsEqualFold(h, "_gateway")
}

// isOutbound reports whether h should be considered a "outbound"
// name for the myhostname NSS module.
func isOutbound(h string) bool {
	return stringsEqualFold(h, "_outbound")
}