From af4fbac84da6c3f69d0a17740a3930a17b311af4 Mon Sep 17 00:00:00 2001 From: ju-ef <147659997+ju-ef@users.noreply.github.com> Date: Thu, 11 Sep 2025 23:32:19 +0300 Subject: [PATCH] feat(client): Add RDAP support for domain expiration (#1181) Fixes #1083 Fixes #1254 Co-authored-by: TwiN --- client/client.go | 30 +++++++++++++++++++++++++++++- client/client_test.go | 14 ++++++++++++++ go.mod | 1 + go.sum | 4 ++++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/client/client.go b/client/client.go index 6aea4718..22c663d6 100644 --- a/client/client.go +++ b/client/client.go @@ -21,6 +21,8 @@ import ( "github.com/ishidawataru/sctp" "github.com/miekg/dns" ping "github.com/prometheus-community/pro-bing" + "github.com/registrobr/rdap" + "github.com/registrobr/rdap/protocol" "golang.org/x/crypto/ssh" "golang.org/x/net/websocket" ) @@ -35,6 +37,7 @@ var ( whoisClient = whois.NewClient().WithReferralCache(true) whoisExpirationDateCache = gocache.NewCache().WithMaxSize(10000).WithDefaultTTL(24 * time.Hour) + rdapClient = rdap.NewClient(nil) ) // GetHTTPClient returns the shared HTTP client, or the client from the configuration passed @@ -62,7 +65,12 @@ func GetDomainExpiration(hostname string) (domainExpiration time.Duration, err e return domainExpiration, nil } } - if whoisResponse, err := whoisClient.QueryAndParse(hostname); err != nil { + whoisResponse, err := rdapQuery(hostname) + if err != nil { + // fallback to WHOIS protocol + whoisResponse, err = whoisClient.QueryAndParse(hostname) + } + if err != nil { if !retrievedCachedValue { // Add an error unless we already retrieved a cached value return 0, fmt.Errorf("error querying and parsing hostname using whois client: %w", err) } @@ -453,3 +461,23 @@ func QueryDNS(queryType, queryName, url string) (connected bool, dnsRcode string func InjectHTTPClient(httpClient *http.Client) { injectedHTTPClient = httpClient } + +// rdapQuery returns domain expiration via RDAP protocol +func rdapQuery(hostname string) (*whois.Response, error) { + data, _, err := rdapClient.Query(hostname, nil, nil) + if err != nil { + return nil, err + } + domain, ok := data.(*protocol.Domain) + if !ok { + return nil, errors.New("invalid domain type") + } + response := whois.Response{} + for _, e := range domain.Events { + if e.Action == "expiration" { + response.ExpirationDate = e.Date.Time + break + } + } + return &response, nil +} diff --git a/client/client_test.go b/client/client_test.go index 7232147a..92513148 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -39,6 +39,20 @@ func TestGetHTTPClient(t *testing.T) { } } +func TestRdapQuery(t *testing.T) { + if _, err := rdapQuery("1.1.1.1"); err == nil { + t.Error("expected an error due to the invalid domain type") + } + if _, err := rdapQuery("eurid.eu"); err == nil { + t.Error("expected an error as there is no RDAP support currently in .eu") + } + if response, err := rdapQuery("example.com"); err != nil { + t.Fatal("expected no error, got", err.Error()) + } else if response.ExpirationDate.Unix() <= 0 { + t.Error("expected to have a valid expiry date, got", response.ExpirationDate.Unix()) + } +} + func TestGetDomainExpiration(t *testing.T) { t.Parallel() if domainExpiration, err := GetDomainExpiration("gatus.io"); err != nil { diff --git a/go.mod b/go.mod index b7df03b0..f14daa4e 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/miekg/dns v1.1.68 github.com/prometheus-community/pro-bing v0.6.1 github.com/prometheus/client_golang v1.23.0 + github.com/registrobr/rdap v1.1.8 github.com/valyala/fasthttp v1.64.0 github.com/wcharczuk/go-chart/v2 v2.1.2 golang.org/x/crypto v0.40.0 diff --git a/go.sum b/go.sum index 199c84f2..80757d7f 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ github.com/TwiN/whois v1.1.11 h1:lYiYgPRSQ3kH8sQfgHcBY/uNSGGvWPRikEjn+LJZ9+Q= github.com/TwiN/whois v1.1.11/go.mod h1:TjipCMpJRAJYKmtz/rXQBU6UGxMh6bk8SHazu7OMnQE= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw= +github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -115,6 +117,8 @@ github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2 github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/registrobr/rdap v1.1.8 h1:7egYAM8MsuencdP9mvF/892f8OjXvUFSyp5cT1Lg45U= +github.com/registrobr/rdap v1.1.8/go.mod h1:VY2DVrpsJpUfy9gj2QvurGymCgZV11/11cxQz5CxO+w= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=