whoissdk/lookup_test.go

460 lines
13 KiB
Go
Raw Normal View History

2026-03-19 11:53:07 +08:00
package whois
import (
"bufio"
"context"
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"time"
)
func TestLookupAutoPreferRDAP(t *testing.T) {
rdapSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/rdap/domain/example.com" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/rdap+json")
_, _ = w.Write([]byte(`{
"objectClassName":"domain",
"ldhName":"example.com",
"status":["active"],
"nameservers":[{"ldhName":"ns1.example.com"}],
"events":[{"eventAction":"registration","eventDate":"2020-01-01T00:00:00Z"}]
}`))
}))
defer rdapSrv.Close()
rdc, err := NewRDAPClientWithBootstrap(&RDAPBootstrap{
Version: "1.0",
Services: []RDAPService{
{TLDs: []string{"com"}, URLs: []string{rdapSrv.URL + "/rdap/"}},
},
})
if err != nil {
t.Fatalf("NewRDAPClientWithBootstrap() error: %v", err)
}
c := NewClient()
got, meta, err := c.Lookup("example.com",
WithLookupMode(LookupModeAutoPreferRDAP),
WithLookupRDAPClient(rdc),
)
if err != nil {
t.Fatalf("Lookup() error: %v", err)
}
if meta.Source != LookupSourceRDAP {
t.Fatalf("unexpected source: %s", meta.Source)
}
if !got.Exists() || got.Domain() != "example.com" {
t.Fatalf("unexpected result: exists=%v domain=%q", got.Exists(), got.Domain())
}
if len(got.NsServers()) != 1 || got.NsServers()[0] != "ns1.example.com" {
t.Fatalf("unexpected ns list: %#v", got.NsServers())
}
}
func TestLookupAutoFallbackToWhois(t *testing.T) {
rdapSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "temporary", http.StatusInternalServerError)
}))
defer rdapSrv.Close()
rdc, err := NewRDAPClientWithBootstrap(&RDAPBootstrap{
Version: "1.0",
Services: []RDAPService{
{TLDs: []string{"com"}, URLs: []string{rdapSrv.URL}},
},
})
if err != nil {
t.Fatalf("NewRDAPClientWithBootstrap() error: %v", err)
}
whoisAddr, shutdown := startMockWhoisServer(t, strings.Join([]string{
"Domain Name: EXAMPLE.COM",
"Registrar: TEST-REG",
"Creation Date: 2020-01-01T00:00:00Z",
"Registry Expiry Date: 2030-01-01T00:00:00Z",
"Updated Date: 2024-01-01T00:00:00Z",
"Name Server: NS2.EXAMPLE.COM",
"Status: clientTransferProhibited",
"",
}, "\n"))
defer shutdown()
c := NewClient()
got, meta, err := c.Lookup("example.com",
WithLookupMode(LookupModeAutoPreferRDAP),
WithLookupRDAPClient(rdc),
WithLookupWhoisOptions(QueryOptions{
Level: QueryAuto,
OverrideServer: whoisAddr,
}),
)
if err != nil {
t.Fatalf("Lookup() error: %v", err)
}
if meta.Source != LookupSourceWHOIS {
t.Fatalf("unexpected source: %s", meta.Source)
}
if got.Registrar() != "TEST-REG" {
t.Fatalf("unexpected registrar: %q", got.Registrar())
}
if !got.HasExpireDate() {
t.Fatal("expected expire date from whois fallback")
}
if len(meta.WarningList) == 0 {
t.Fatal("expected fallback warning")
}
}
func TestLookupBothPreferRDAPMerge(t *testing.T) {
rdapSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{
"objectClassName":"domain",
"ldhName":"example.com",
"status":["active"],
"nameservers":[{"ldhName":"ns1.example.com"}]
}`))
}))
defer rdapSrv.Close()
rdc, err := NewRDAPClientWithBootstrap(&RDAPBootstrap{
Version: "1.0",
Services: []RDAPService{
{TLDs: []string{"com"}, URLs: []string{rdapSrv.URL}},
},
})
if err != nil {
t.Fatalf("NewRDAPClientWithBootstrap() error: %v", err)
}
whoisAddr, shutdown := startMockWhoisServer(t, strings.Join([]string{
"Domain Name: EXAMPLE.COM",
"Registrar: TEST-REG",
"Name Server: NS2.EXAMPLE.COM",
"Status: clientTransferProhibited",
"",
}, "\n"))
defer shutdown()
c := NewClient()
got, meta, err := c.LookupContext(context.Background(), "example.com",
WithLookupMode(LookupModeBothPreferRDAP),
WithLookupRDAPClient(rdc),
WithLookupWhoisOptions(QueryOptions{OverrideServer: whoisAddr, Level: QueryAuto}),
)
if err != nil {
t.Fatalf("LookupContext() error: %v", err)
}
if meta.Source != LookupSourceMerged {
t.Fatalf("unexpected source: %s", meta.Source)
}
if got.Registrar() != "TEST-REG" {
t.Fatalf("expected merged registrar from whois, got %q", got.Registrar())
}
if len(got.NsServers()) != 2 {
t.Fatalf("expected merged ns servers, got %#v", got.NsServers())
}
}
func TestLookupRDAPHostsOps(t *testing.T) {
rdapSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/rdap/domain/example.com" {
http.NotFound(w, r)
return
}
_, _ = w.Write([]byte(`{"objectClassName":"domain","ldhName":"example.com"}`))
}))
defer rdapSrv.Close()
u, err := url.Parse(rdapSrv.URL)
if err != nil {
t.Fatalf("url.Parse() error: %v", err)
}
fakeHost := "rdap.invalid"
fakeBase := "http://" + fakeHost + ":" + u.Port() + "/rdap/"
rdc, err := NewRDAPClientWithBootstrap(&RDAPBootstrap{
Version: "1.0",
Services: []RDAPService{
{TLDs: []string{"com"}, URLs: []string{fakeBase}},
},
})
if err != nil {
t.Fatalf("NewRDAPClientWithBootstrap() error: %v", err)
}
c := NewClient()
_, _, err = c.Lookup("example.com",
WithLookupMode(LookupModeRDAPOnly),
WithLookupRDAPClient(rdc),
)
if err == nil {
t.Fatal("expected rdap-only lookup to fail without hosts override")
}
got, meta, err := c.Lookup("example.com",
WithLookupMode(LookupModeRDAPOnly),
WithLookupRDAPClient(rdc),
WithLookupRDAPOps(LookupRDAPOps{
Hosts: map[string]string{
fakeHost: "127.0.0.1",
},
}),
)
if err != nil {
t.Fatalf("Lookup() with rdap hosts ops error: %v", err)
}
if meta.Source != LookupSourceRDAP || !got.Exists() {
t.Fatalf("unexpected result source=%s exists=%v", meta.Source, got.Exists())
}
}
func TestLookupWHOISTimeoutOps(t *testing.T) {
whoisAddr, shutdown := startMockWhoisServerWithDelay(t, strings.Join([]string{
"Domain Name: EXAMPLE.COM",
"Registrar: TEST-REG",
"",
}, "\n"), 300*time.Millisecond)
defer shutdown()
c := NewClient()
_, _, err := c.Lookup("example.com",
WithLookupMode(LookupModeWHOISOnly),
WithLookupWhoisOptions(QueryOptions{OverrideServer: whoisAddr, Level: QueryAuto}),
WithLookupWHOISTimeout(80*time.Millisecond),
)
if err == nil {
t.Fatal("expected whois timeout error")
}
}
func TestLookupWhoisOnlyIgnoresRDAPProxyOps(t *testing.T) {
whoisAddr, shutdown := startMockWhoisServer(t, strings.Join([]string{
"Domain Name: EXAMPLE.COM",
"Registrar: TEST-REG",
"",
}, "\n"))
defer shutdown()
c := NewClient()
got, meta, err := c.Lookup("example.com",
WithLookupMode(LookupModeWHOISOnly),
WithLookupWhoisOptions(QueryOptions{OverrideServer: whoisAddr, Level: QueryAuto}),
WithLookupRDAPProxy("http://127.0.0.1:1"),
)
if err != nil {
t.Fatalf("whois-only lookup should not be affected by rdap proxy ops: %v", err)
}
if !got.Exists() || meta.Source != LookupSourceWHOIS {
t.Fatalf("unexpected whois-only result source=%s exists=%v", meta.Source, got.Exists())
}
}
func TestLookupWhoisOnlyInvalidCommonProxyFailsFast(t *testing.T) {
c := NewClient()
_, _, err := c.Lookup("example.com",
WithLookupMode(LookupModeWHOISOnly),
WithLookupProxy("http://127.0.0.1:8080"),
)
if err == nil {
t.Fatal("expected whois-only lookup to fail on unsupported common proxy")
}
if !strings.Contains(err.Error(), "whois proxy unsupported") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestLookupAutoUsesTLDStrategyWhoisOnly(t *testing.T) {
var rdapHit int32
rdapSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(&rdapHit, 1)
_, _ = w.Write([]byte(`{"objectClassName":"domain","ldhName":"example.com"}`))
}))
defer rdapSrv.Close()
rdc, err := NewRDAPClientWithBootstrap(&RDAPBootstrap{
Version: "1.0",
Services: []RDAPService{
{TLDs: []string{"com"}, URLs: []string{rdapSrv.URL}},
},
})
if err != nil {
t.Fatalf("NewRDAPClientWithBootstrap() error: %v", err)
}
whoisAddr, shutdown := startMockWhoisServer(t, strings.Join([]string{
"Domain Name: EXAMPLE.COM",
"Registrar: TEST-REG",
"",
}, "\n"))
defer shutdown()
c := NewClient()
got, meta, err := c.Lookup("example.com",
WithLookupMode(LookupModeAutoPreferRDAP),
WithLookupTLDStrategy("com", LookupModeWHOISOnly),
WithLookupRDAPClient(rdc),
WithLookupWhoisOptions(QueryOptions{OverrideServer: whoisAddr, Level: QueryAuto}),
)
if err != nil {
t.Fatalf("Lookup() error: %v", err)
}
if meta.Mode != LookupModeWHOISOnly {
t.Fatalf("unexpected resolved mode: %s", meta.Mode)
}
if meta.Source != LookupSourceWHOIS || !got.Exists() {
t.Fatalf("unexpected result source=%s exists=%v", meta.Source, got.Exists())
}
if atomic.LoadInt32(&rdapHit) != 0 {
t.Fatalf("rdap should not be called when strategy forces whois-only, hits=%d", atomic.LoadInt32(&rdapHit))
}
}
func TestLookupStrategyDefaultMode(t *testing.T) {
whoisAddr, shutdown := startMockWhoisServer(t, strings.Join([]string{
"Domain Name: EXAMPLE.NET",
"Registrar: TEST-REG",
"",
}, "\n"))
defer shutdown()
c := NewClient()
got, meta, err := c.Lookup("example.net",
WithLookupMode(LookupModeAutoPreferRDAP),
WithLookupStrategyDefaultMode(LookupModeWHOISOnly),
WithLookupWhoisOptions(QueryOptions{OverrideServer: whoisAddr, Level: QueryAuto}),
)
if err != nil {
t.Fatalf("Lookup() error: %v", err)
}
if meta.Mode != LookupModeWHOISOnly {
t.Fatalf("unexpected resolved mode: %s", meta.Mode)
}
if meta.Source != LookupSourceWHOIS || !got.Exists() {
t.Fatalf("unexpected source=%s exists=%v", meta.Source, got.Exists())
}
}
func TestLookupRDAPBootstrapConvenienceOptions(t *testing.T) {
cacheKey := t.Name() + "-cache"
ClearRDAPBootstrapLayeredCache(cacheKey)
rdapSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/rdap/domain/example.com" {
http.NotFound(w, r)
return
}
_, _ = w.Write([]byte(`{"objectClassName":"domain","ldhName":"example.com"}`))
}))
defer rdapSrv.Close()
bootstrapRaw := `{"version":"1.0","services":[[["com"],["` + rdapSrv.URL + `/rdap/"]]]}`
bootstrapSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(bootstrapRaw))
}))
defer bootstrapSrv.Close()
c := NewClient()
got, meta, err := c.Lookup("example.com",
WithLookupMode(LookupModeRDAPOnly),
WithLookupRDAPBootstrapRemoteRefresh(true, bootstrapSrv.URL),
WithLookupRDAPBootstrapCache(time.Minute, cacheKey),
)
if err != nil {
t.Fatalf("Lookup() error: %v", err)
}
if meta.Source != LookupSourceRDAP || !got.Exists() {
t.Fatalf("unexpected source=%s exists=%v", meta.Source, got.Exists())
}
}
func TestLookupRDAPBootstrapIgnoreRemoteError(t *testing.T) {
cacheKey := t.Name() + "-cache"
ClearRDAPBootstrapLayeredCache(cacheKey)
rdapSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/rdap/domain/example.com" {
http.NotFound(w, r)
return
}
_, _ = w.Write([]byte(`{"objectClassName":"domain","ldhName":"example.com"}`))
}))
defer rdapSrv.Close()
localRaw := `{"version":"1.0","services":[[["com"],["` + rdapSrv.URL + `/rdap/"]]]}`
localPath := filepath.Join(t.TempDir(), "rdap_local.json")
if err := os.WriteFile(localPath, []byte(localRaw), 0644); err != nil {
t.Fatalf("write local bootstrap failed: %v", err)
}
remoteSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "refresh failed", http.StatusInternalServerError)
}))
defer remoteSrv.Close()
c := NewClient()
got, meta, err := c.Lookup("example.com",
WithLookupMode(LookupModeRDAPOnly),
WithLookupRDAPBootstrapLocalFiles(localPath),
WithLookupRDAPBootstrapRemoteRefresh(true, remoteSrv.URL),
WithLookupRDAPBootstrapIgnoreRemoteError(true),
WithLookupRDAPBootstrapCache(time.Minute, cacheKey),
)
if err != nil {
t.Fatalf("Lookup() with ignore remote error failed: %v", err)
}
if meta.Source != LookupSourceRDAP || !got.Exists() {
t.Fatalf("unexpected source=%s exists=%v", meta.Source, got.Exists())
}
}
func startMockWhoisServer(t *testing.T, response string) (addr string, shutdown func()) {
return startMockWhoisServerWithDelay(t, response, 0)
}
func startMockWhoisServerWithDelay(t *testing.T, response string, delay time.Duration) (addr string, shutdown func()) {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen mock whois failed: %v", err)
}
done := make(chan struct{})
go func() {
for {
conn, err := ln.Accept()
if err != nil {
select {
case <-done:
return
default:
return
}
}
go func(c net.Conn) {
defer c.Close()
_ = c.SetDeadline(time.Now().Add(5 * time.Second))
_, _ = bufio.NewReader(c).ReadString('\n')
if delay > 0 {
time.Sleep(delay)
}
_, _ = io.WriteString(c, response)
}(conn)
}
}()
return ln.Addr().String(), func() {
close(done)
_ = ln.Close()
}
}