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() } }