// 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 unix

package net

import (
	"io/fs"
	"os"
	"testing"
	"time"
)

type nssHostTest struct {
	host      string
	localhost string
	want      hostLookupOrder
}

func nssStr(t *testing.T, s string) *nssConf {
	f, err := os.CreateTemp(t.TempDir(), "nss")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := f.WriteString(s); err != nil {
		t.Fatal(err)
	}
	if err := f.Close(); err != nil {
		t.Fatal(err)
	}
	return parseNSSConfFile(f.Name())
}

// represents a dnsConfig returned by parsing a nonexistent resolv.conf
var defaultResolvConf = &dnsConfig{
	servers:  defaultNS,
	ndots:    1,
	timeout:  5,
	attempts: 2,
	err:      fs.ErrNotExist,
}

func TestConfHostLookupOrder(t *testing.T) {
	// These tests are written for a system with cgo available,
	// without using the netgo tag.
	if netGoBuildTag {
		t.Skip("skipping test because net package built with netgo tag")
	}
	if !cgoAvailable {
		t.Skip("skipping test because cgo resolver not available")
	}

	tests := []struct {
		name      string
		c         *conf
		nss       *nssConf
		resolver  *Resolver
		resolv    *dnsConfig
		hostTests []nssHostTest
	}{
		{
			name: "force",
			c: &conf{
				preferCgo: true,
				netCgo:    true,
			},
			resolv: defaultResolvConf,
			nss:    nssStr(t, "foo: bar"),
			hostTests: []nssHostTest{
				{"foo.local", "myhostname", hostLookupCgo},
				{"google.com", "myhostname", hostLookupCgo},
			},
		},
		{
			name: "netgo_dns_before_files",
			c: &conf{
				netGo: true,
			},
			resolv: defaultResolvConf,
			nss:    nssStr(t, "hosts: dns files"),
			hostTests: []nssHostTest{
				{"x.com", "myhostname", hostLookupDNSFiles},
			},
		},
		{
			name: "netgo_fallback_on_cgo",
			c: &conf{
				netGo: true,
			},
			resolv: defaultResolvConf,
			nss:    nssStr(t, "hosts: dns files something_custom"),
			hostTests: []nssHostTest{
				{"x.com", "myhostname", hostLookupDNSFiles},
			},
		},
		{
			name: "ubuntu_trusty_avahi",
			c: &conf{
				mdnsTest: mdnsAssumeDoesNotExist,
			},
			resolv: defaultResolvConf,
			nss:    nssStr(t, "hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4"),
			hostTests: []nssHostTest{
				{"foo.local", "myhostname", hostLookupCgo},
				{"foo.local.", "myhostname", hostLookupCgo},
				{"foo.LOCAL", "myhostname", hostLookupCgo},
				{"foo.LOCAL.", "myhostname", hostLookupCgo},
				{"google.com", "myhostname", hostLookupFilesDNS},
			},
		},
		{
			name: "freebsdlinux_no_resolv_conf",
			c: &conf{
				goos: "freebsd",
			},
			resolv:    defaultResolvConf,
			nss:       nssStr(t, "foo: bar"),
			hostTests: []nssHostTest{{"google.com", "myhostname", hostLookupFilesDNS}},
		},
		// On OpenBSD, no resolv.conf means no DNS.
		{
			name: "openbsd_no_resolv_conf",
			c: &conf{
				goos: "openbsd",
			},
			resolv:    defaultResolvConf,
			hostTests: []nssHostTest{{"google.com", "myhostname", hostLookupFiles}},
		},
		{
			name: "solaris_no_nsswitch",
			c: &conf{
				goos: "solaris",
			},
			resolv:    defaultResolvConf,
			nss:       &nssConf{err: fs.ErrNotExist},
			hostTests: []nssHostTest{{"google.com", "myhostname", hostLookupCgo}},
		},
		{
			name: "openbsd_lookup_bind_file",
			c: &conf{
				goos: "openbsd",
			},
			resolv: &dnsConfig{lookup: []string{"bind", "file"}},
			hostTests: []nssHostTest{
				{"google.com", "myhostname", hostLookupDNSFiles},
				{"foo.local", "myhostname", hostLookupDNSFiles},
			},
		},
		{
			name: "openbsd_lookup_file_bind",
			c: &conf{
				goos: "openbsd",
			},
			resolv:    &dnsConfig{lookup: []string{"file", "bind"}},
			hostTests: []nssHostTest{{"google.com", "myhostname", hostLookupFilesDNS}},
		},
		{
			name: "openbsd_lookup_bind",
			c: &conf{
				goos: "openbsd",
			},
			resolv:    &dnsConfig{lookup: []string{"bind"}},
			hostTests: []nssHostTest{{"google.com", "myhostname", hostLookupDNS}},
		},
		{
			name: "openbsd_lookup_file",
			c: &conf{
				goos: "openbsd",
			},
			resolv:    &dnsConfig{lookup: []string{"file"}},
			hostTests: []nssHostTest{{"google.com", "myhostname", hostLookupFiles}},
		},
		{
			name: "openbsd_lookup_yp",
			c: &conf{
				goos: "openbsd",
			},
			resolv:    &dnsConfig{lookup: []string{"file", "bind", "yp"}},
			hostTests: []nssHostTest{{"google.com", "myhostname", hostLookupCgo}},
		},
		{
			name: "openbsd_lookup_two",
			c: &conf{
				goos: "openbsd",
			},
			resolv:    &dnsConfig{lookup: []string{"file", "foo"}},
			hostTests: []nssHostTest{{"google.com", "myhostname", hostLookupCgo}},
		},
		{
			name: "openbsd_lookup_empty",
			c: &conf{
				goos: "openbsd",
			},
			resolv:    &dnsConfig{lookup: nil},
			hostTests: []nssHostTest{{"google.com", "myhostname", hostLookupDNSFiles}},
		},
		{
			name: "linux_no_nsswitch.conf",
			c: &conf{
				goos: "linux",
			},
			resolv:    defaultResolvConf,
			nss:       &nssConf{err: fs.ErrNotExist},
			hostTests: []nssHostTest{{"google.com", "myhostname", hostLookupFilesDNS}},
		},
		{
			name: "linux_empty_nsswitch.conf",
			c: &conf{
				goos: "linux",
			},
			resolv:    defaultResolvConf,
			nss:       nssStr(t, ""),
			hostTests: []nssHostTest{{"google.com", "myhostname", hostLookupFilesDNS}},
		},
		{
			name: "files_mdns_dns",
			c: &conf{
				mdnsTest: mdnsAssumeDoesNotExist,
			},
			resolv: defaultResolvConf,
			nss:    nssStr(t, "hosts: files mdns dns"),
			hostTests: []nssHostTest{
				{"x.com", "myhostname", hostLookupFilesDNS},
				{"x.local", "myhostname", hostLookupCgo},
			},
		},
		{
			name:   "dns_special_hostnames",
			c:      &conf{},
			resolv: defaultResolvConf,
			nss:    nssStr(t, "hosts: dns"),
			hostTests: []nssHostTest{
				{"x.com", "myhostname", hostLookupDNS},
				{"x\\.com", "myhostname", hostLookupCgo},     // punt on weird glibc escape
				{"foo.com%en0", "myhostname", hostLookupCgo}, // and IPv6 zones
			},
		},
		{
			name: "mdns_allow",
			c: &conf{
				mdnsTest: mdnsAssumeExists,
			},
			resolv: defaultResolvConf,
			nss:    nssStr(t, "hosts: files mdns dns"),
			hostTests: []nssHostTest{
				{"x.com", "myhostname", hostLookupCgo},
				{"x.local", "myhostname", hostLookupCgo},
			},
		},
		{
			name:   "files_dns",
			c:      &conf{},
			resolv: defaultResolvConf,
			nss:    nssStr(t, "hosts: files dns"),
			hostTests: []nssHostTest{
				{"x.com", "myhostname", hostLookupFilesDNS},
				{"x", "myhostname", hostLookupFilesDNS},
				{"x.local", "myhostname", hostLookupCgo},
			},
		},
		{
			name:   "dns_files",
			c:      &conf{},
			resolv: defaultResolvConf,
			nss:    nssStr(t, "hosts: dns files"),
			hostTests: []nssHostTest{
				{"x.com", "myhostname", hostLookupDNSFiles},
				{"x", "myhostname", hostLookupDNSFiles},
				{"x.local", "myhostname", hostLookupCgo},
			},
		},
		{
			name:   "something_custom",
			c:      &conf{},
			resolv: defaultResolvConf,
			nss:    nssStr(t, "hosts: dns files something_custom"),
			hostTests: []nssHostTest{
				{"x.com", "myhostname", hostLookupCgo},
			},
		},
		{
			name:   "myhostname",
			c:      &conf{},
			resolv: defaultResolvConf,
			nss:    nssStr(t, "hosts: files dns myhostname"),
			hostTests: []nssHostTest{
				{"x.com", "myhostname", hostLookupFilesDNS},
				{"myhostname", "myhostname", hostLookupCgo},
				{"myHostname", "myhostname", hostLookupCgo},
				{"myhostname.dot", "myhostname.dot", hostLookupCgo},
				{"myHostname.dot", "myhostname.dot", hostLookupCgo},
				{"_gateway", "myhostname", hostLookupCgo},
				{"_Gateway", "myhostname", hostLookupCgo},
				{"_outbound", "myhostname", hostLookupCgo},
				{"_Outbound", "myhostname", hostLookupCgo},
				{"localhost", "myhostname", hostLookupCgo},
				{"Localhost", "myhostname", hostLookupCgo},
				{"anything.localhost", "myhostname", hostLookupCgo},
				{"Anything.localhost", "myhostname", hostLookupCgo},
				{"localhost.localdomain", "myhostname", hostLookupCgo},
				{"Localhost.Localdomain", "myhostname", hostLookupCgo},
				{"anything.localhost.localdomain", "myhostname", hostLookupCgo},
				{"Anything.Localhost.Localdomain", "myhostname", hostLookupCgo},
				{"somehostname", "myhostname", hostLookupFilesDNS},
			},
		},
		{
			name: "ubuntu14.04.02",
			c: &conf{
				mdnsTest: mdnsAssumeDoesNotExist,
			},
			resolv: defaultResolvConf,
			nss:    nssStr(t, "hosts: files myhostname mdns4_minimal [NOTFOUND=return] dns mdns4"),
			hostTests: []nssHostTest{
				{"x.com", "myhostname", hostLookupFilesDNS},
				{"somehostname", "myhostname", hostLookupFilesDNS},
				{"myhostname", "myhostname", hostLookupCgo},
			},
		},
		// Debian Squeeze is just "dns,files", but lists all
		// the default criteria for dns, but then has a
		// non-standard but redundant notfound=return for the
		// files.
		{
			name:   "debian_squeeze",
			c:      &conf{},
			resolv: defaultResolvConf,
			nss:    nssStr(t, "hosts: dns [success=return notfound=continue unavail=continue tryagain=continue] files [notfound=return]"),
			hostTests: []nssHostTest{
				{"x.com", "myhostname", hostLookupDNSFiles},
				{"somehostname", "myhostname", hostLookupDNSFiles},
			},
		},
		{
			name:      "resolv.conf-unknown",
			c:         &conf{},
			resolv:    &dnsConfig{servers: defaultNS, ndots: 1, timeout: 5, attempts: 2, unknownOpt: true},
			nss:       nssStr(t, "foo: bar"),
			hostTests: []nssHostTest{{"google.com", "myhostname", hostLookupCgo}},
		},
		// Issue 24393: make sure "Resolver.PreferGo = true" acts like netgo.
		{
			name:     "resolver-prefergo",
			resolver: &Resolver{PreferGo: true},
			c: &conf{
				preferCgo: true,
				netCgo:    true,
			},
			resolv: defaultResolvConf,
			nss:    nssStr(t, ""),
			hostTests: []nssHostTest{
				{"localhost", "myhostname", hostLookupFilesDNS},
			},
		},
		{
			name:     "unknown-source",
			resolver: &Resolver{PreferGo: true},
			c:        &conf{},
			resolv:   defaultResolvConf,
			nss:      nssStr(t, "hosts: resolve files"),
			hostTests: []nssHostTest{
				{"x.com", "myhostname", hostLookupDNSFiles},
			},
		},
		{
			name:     "dns-among-unknown-sources",
			resolver: &Resolver{PreferGo: true},
			c:        &conf{},
			resolv:   defaultResolvConf,
			nss:      nssStr(t, "hosts: mymachines files dns"),
			hostTests: []nssHostTest{
				{"x.com", "myhostname", hostLookupFilesDNS},
			},
		},
		{
			name:     "dns-among-unknown-sources-2",
			resolver: &Resolver{PreferGo: true},
			c:        &conf{},
			resolv:   defaultResolvConf,
			nss:      nssStr(t, "hosts: dns mymachines files"),
			hostTests: []nssHostTest{
				{"x.com", "myhostname", hostLookupDNSFiles},
			},
		},
	}

	origGetHostname := getHostname
	defer func() { getHostname = origGetHostname }()
	defer setSystemNSS(getSystemNSS(), 0)
	conf, err := newResolvConfTest()
	if err != nil {
		t.Fatal(err)
	}
	defer conf.teardown()

	for _, tt := range tests {
		if !conf.forceUpdateConf(tt.resolv, time.Now().Add(time.Hour)) {
			t.Errorf("%s: failed to change resolv config", tt.name)
		}
		for _, ht := range tt.hostTests {
			getHostname = func() (string, error) { return ht.localhost, nil }
			setSystemNSS(tt.nss, time.Hour)

			gotOrder, _ := tt.c.hostLookupOrder(tt.resolver, ht.host)
			if gotOrder != ht.want {
				t.Errorf("%s: hostLookupOrder(%q) = %v; want %v", tt.name, ht.host, gotOrder, ht.want)
			}
		}
	}
}

func TestAddrLookupOrder(t *testing.T) {
	// This test is written for a system with cgo available,
	// without using the netgo tag.
	if netGoBuildTag {
		t.Skip("skipping test because net package built with netgo tag")
	}
	if !cgoAvailable {
		t.Skip("skipping test because cgo resolver not available")
	}

	defer setSystemNSS(getSystemNSS(), 0)
	c, err := newResolvConfTest()
	if err != nil {
		t.Fatal(err)
	}
	defer c.teardown()

	if !c.forceUpdateConf(defaultResolvConf, time.Now().Add(time.Hour)) {
		t.Fatal("failed to change resolv config")
	}

	setSystemNSS(nssStr(t, "hosts: files myhostname dns"), time.Hour)
	cnf := &conf{}
	order, _ := cnf.addrLookupOrder(nil, "192.0.2.1")
	if order != hostLookupCgo {
		t.Errorf("addrLookupOrder returned: %v, want cgo", order)
	}

	setSystemNSS(nssStr(t, "hosts: files mdns4 dns"), time.Hour)
	order, _ = cnf.addrLookupOrder(nil, "192.0.2.1")
	if order != hostLookupCgo {
		t.Errorf("addrLookupOrder returned: %v, want cgo", order)
	}

}

func setSystemNSS(nss *nssConf, addDur time.Duration) {
	nssConfig.mu.Lock()
	nssConfig.nssConf = nss
	nssConfig.mu.Unlock()
	nssConfig.acquireSema()
	nssConfig.lastChecked = time.Now().Add(addDur)
	nssConfig.releaseSema()
}

func TestSystemConf(t *testing.T) {
	systemConf()
}
