API route to retrieve RDAP/WHOIS information as domaininfo

This commit is contained in:
nemunaire 2025-06-08 18:11:08 +02:00
commit 32c09bbb86
13 changed files with 588 additions and 1 deletions

8
go.mod
View file

@ -17,8 +17,11 @@ require (
github.com/gorilla/securecookie v1.1.2
github.com/gorilla/sessions v1.4.0
github.com/lib/pq v1.11.2
github.com/likexian/whois v1.15.7
github.com/likexian/whois-parser v1.24.21
github.com/miekg/dns v1.1.72
github.com/mileusna/useragent v1.3.5
github.com/openrdap/rdap v0.9.1
github.com/oracle/nosql-go-sdk v1.4.7
github.com/ovh/go-ovh v1.9.0
github.com/prometheus-community/pro-bing v0.8.0
@ -54,6 +57,8 @@ require (
github.com/PuerkitoBio/goquery v1.11.0 // indirect
github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang/v12 v12.3.0 // indirect
github.com/alecthomas/kingpin/v2 v2.4.0 // indirect
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
@ -158,6 +163,7 @@ require (
github.com/labstack/echo/v4 v4.15.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/likexian/gokit v0.25.16 // indirect
github.com/luadns/luadns-go v0.3.0 // indirect
github.com/mailgun/raymond/v2 v2.0.48 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
@ -208,6 +214,7 @@ require (
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/vultr/govultr/v2 v2.17.2 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
github.com/yosssi/ace v0.0.5 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.mongodb.org/mongo-driver v1.17.9 // indirect
@ -217,7 +224,6 @@ require (
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.uber.org/ratelimit v0.3.1 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect

15
go.sum
View file

@ -62,6 +62,10 @@ github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/akamai/AkamaiOPEN-edgegrid-golang/v12 v12.3.0 h1:iGVPe/gPqzpXggbbmVLWR0TyJ9UoPoqKL+kspjseZzE=
github.com/akamai/AkamaiOPEN-edgegrid-golang/v12 v12.3.0/go.mod h1:76JtkiCKMwTdTOlKe9goT4Md+oWjfMouGBQgy+u1bgc=
github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0=
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/altcha-org/altcha-lib-go v1.0.0 h1:7oPti0aUS+YCep8nwt5b9g4jYfCU55ZruWESL8G9K5M=
@ -432,6 +436,12 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/likexian/gokit v0.25.16 h1:wwBeUIN/OdoPp6t00xTnZE8Di/+s969Bl5N2Kw6bzP8=
github.com/likexian/gokit v0.25.16/go.mod h1:Wqd4f+iifV0qxA1N3MqePJTUsmRy/lpst9/yXriDx/4=
github.com/likexian/whois v1.15.7 h1:sajjDhi2bVD71AHJhjV7jLYxN92H4AWhTwxM8hmj7c0=
github.com/likexian/whois v1.15.7/go.mod h1:kdPQtYb+7SQVftBEbCblDadUkycN7Mg1k1/Li/rwvmc=
github.com/likexian/whois-parser v1.24.21 h1:MxsrGRxDOiZIVp7q7N/yAIbKuN4QAkGjCpOtTDA5OsM=
github.com/likexian/whois-parser v1.24.21/go.mod h1:o3DUruO65Pb8WXCJCTlSVkTbwuYVrBCeoMTw2q0mxY4=
github.com/luadns/luadns-go v0.3.0 h1:mN2yhFv/LnGvPw/HmvYUhXe+lc95oXUqjlYVeJeOJng=
github.com/luadns/luadns-go v0.3.0/go.mod h1:DmPXbrGMpynq1YNDpvgww3NP5Zf4wXM5raAbGrp5L+8=
github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw=
@ -484,6 +494,8 @@ github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGm
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/openrdap/rdap v0.9.1 h1:Rv6YbanbiVPsKRvOLdUmlU1AL5+2OFuEFLjFN+mQsCM=
github.com/openrdap/rdap v0.9.1/go.mod h1:vKSiotbsENrjM/vaHXLddXbW8iQkBfa+ldEuYEjyLTQ=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
github.com/oracle/nosql-go-sdk v1.4.7 h1:dqVBSMulObDj0JHm1mAncTHrQg8wIiQJNC0JRNKPACg=
@ -573,6 +585,7 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
@ -625,6 +638,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=

View file

@ -0,0 +1,74 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package controller
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"git.happydns.org/happyDomain/model"
)
type DomainInfoController struct {
diuService happydns.DomainInfoUsecase
}
func NewDomainInfoController(diuService happydns.DomainInfoUsecase) *DomainInfoController {
return &DomainInfoController{
diuService: diuService,
}
}
// GetDomainInfo retrieves domain's administrative information.
//
// @Summary Get domain administrative information
// @Schemes
// @Description Retrieve domain's administrative information.
// @Tags domains
// @Accept json
// @Produce json
// @Security securitydefinitions.basic
// @Param domain path string true "Domain name"
// @Success 200 {object} happydns.DomainInfo
// @Failure 400 {object} happydns.ErrorResponse "Invalid input"
// @Failure 500 {object} happydns.ErrorResponse
// @Router /domaininfo/{domain} [post]
func (dc *DomainInfoController) GetDomainInfo(c *gin.Context) {
domain := c.Param("domain")
if dn, ok := c.Get("domain"); ok {
domain = dn.(*happydns.Domain).DomainName
}
info, err := dc.diuService.GetDomainInfo(c.Request.Context(), happydns.Origin(domain))
if err != nil {
if errors.Is(err, happydns.DomainDoesNotExist) {
c.AbortWithStatusJSON(http.StatusNotFound, happydns.ErrorResponse{Message: err.Error()})
} else {
c.AbortWithStatusJSON(http.StatusInternalServerError, happydns.ErrorResponse{Message: err.Error()})
}
return
}
c.JSON(http.StatusOK, info)
}

View file

@ -42,6 +42,7 @@ func DeclareDomainRoutes(
checkerUC happydns.CheckerUsecase,
checkResultUC happydns.CheckResultUsecase,
checkScheduler happydns.SchedulerUsecase,
domainInfoUC happydns.DomainInfoUsecase,
tpc *controller.CheckerController,
) {
dc := controller.NewDomainController(
@ -61,6 +62,7 @@ func DeclareDomainRoutes(
apiDomainsRoutes.PUT("", dc.UpdateDomain)
apiDomainsRoutes.DELETE("", dc.DelDomain)
DeclareDomainInfoRoutes(apiDomainsRoutes.Group("/info"), domainInfoUC)
DeclareDomainLogRoutes(apiDomainsRoutes, domainLogUC)
// Declare test result routes for domain scope

View file

@ -0,0 +1,37 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package route
import (
"github.com/gin-gonic/gin"
"git.happydns.org/happyDomain/internal/api/controller"
"git.happydns.org/happyDomain/model"
)
func DeclareDomainInfoRoutes(router *gin.RouterGroup, domainInfoUC happydns.DomainInfoUsecase) {
dc := controller.NewDomainInfoController(
domainInfoUC,
)
router.POST("", dc.GetDomainInfo)
}

View file

@ -39,6 +39,7 @@ type Dependencies struct {
CheckerSchedule happydns.CheckerScheduleUsecase
CheckScheduler happydns.SchedulerUsecase
Domain happydns.DomainUsecase
DomainInfo happydns.DomainInfoUsecase
DomainLog happydns.DomainLogUsecase
FailureTracker happydns.FailureTracker
Provider happydns.ProviderUsecase
@ -92,6 +93,7 @@ func DeclareRoutes(cfg *happydns.Options, router *gin.RouterGroup, dep Dependenc
dep.FailureTracker,
)
auc := DeclareAuthUserRoutes(apiRoutes, dep.AuthUser, lc)
DeclareDomainInfoRoutes(apiRoutes.Group("/domaininfo/:domain"), dep.DomainInfo)
DeclareProviderSpecsRoutes(apiRoutes, dep.ProviderSpecs)
DeclareRegistrationRoutes(apiRoutes, dep.AuthUser, dep.CaptchaVerifier)
DeclareResolverRoutes(apiRoutes, dep.Resolver)
@ -124,6 +126,7 @@ func DeclareRoutes(cfg *happydns.Options, router *gin.RouterGroup, dep Dependenc
dep.Checker,
dep.CheckResult,
dep.CheckScheduler,
dep.DomainInfo,
tpc,
)
DeclareProviderRoutes(apiAuthRoutes, dep.Provider)

View file

@ -50,6 +50,7 @@ import (
zoneUC "git.happydns.org/happyDomain/internal/usecase/zone"
zoneServiceUC "git.happydns.org/happyDomain/internal/usecase/zone_service"
"git.happydns.org/happyDomain/model"
"git.happydns.org/happyDomain/pkg/domaininfo"
"git.happydns.org/happyDomain/web"
)
@ -60,6 +61,7 @@ type Usecases struct {
checkResult happydns.CheckResultUsecase
checkerSchedule happydns.CheckerScheduleUsecase
domain happydns.DomainUsecase
domainInfo happydns.DomainInfoUsecase
domainLog happydns.DomainLogUsecase
provider happydns.ProviderUsecase
providerAdmin happydns.ProviderUsecase
@ -237,6 +239,10 @@ func (app *App) initUsecases() {
app.usecases.service = serviceService
app.usecases.serviceSpecs = usecase.NewServiceSpecsUsecase()
app.usecases.zone = zoneService
app.usecases.domainInfo = usecase.NewDomainInfoUsecase(
domaininfo.GetDomainRDAPInfo,
domaininfo.GetDomainWhoisInfo,
)
app.usecases.domainLog = domainLogService
domainService := domainUC.NewService(
@ -313,6 +319,7 @@ func (app *App) setupRouter() {
CheckerSchedule: app.usecases.checkerSchedule,
CheckScheduler: app.checkScheduler,
Domain: app.usecases.domain,
DomainInfo: app.usecases.domainInfo,
DomainLog: app.usecases.domainLog,
FailureTracker: app.failureTracker,
Provider: app.usecases.provider,

View file

@ -0,0 +1,62 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package usecase
import (
"context"
"errors"
"fmt"
"strings"
"git.happydns.org/happyDomain/model"
)
type domainInfoUsecase struct {
getters []happydns.DomainInfoGetter
}
func NewDomainInfoUsecase(getters ...happydns.DomainInfoGetter) happydns.DomainInfoUsecase {
return &domainInfoUsecase{getters: getters}
}
func (diu *domainInfoUsecase) GetDomainInfo(ctx context.Context, fqdn happydns.Origin) (*happydns.DomainInfo, error) {
domain := happydns.Origin(strings.TrimSuffix(string(fqdn), "."))
var lastErr error
for _, getter := range diu.getters {
infos, err := getter(ctx, domain)
if err != nil {
if errors.Is(err, happydns.DomainDoesNotExist) {
return nil, err
}
lastErr = err
continue
}
if infos == nil {
lastErr = fmt.Errorf("no information found")
continue
}
return infos, nil
}
return nil, fmt.Errorf("unable to retrieve RDAP/WHOIS info about the domain name: %w", lastErr)
}

48
model/domain_info.go Normal file
View file

@ -0,0 +1,48 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package happydns
import (
"context"
"errors"
"time"
)
var (
DomainDoesNotExist = errors.New("domain name doesn't exist")
)
type DomainInfo struct {
Name string `json:"name"`
Nameservers []string `json:"nameservers"`
CreationDate *time.Time `json:"creation"`
ExpirationDate *time.Time `json:"expiration"`
Registrar string `json:"registrar"`
RegistrarURL *string `json:"registrar_url"`
Status []string `json:"status"`
}
type DomainInfoGetter func(context.Context, Origin) (*DomainInfo, error)
type DomainInfoUsecase interface {
GetDomainInfo(context.Context, Origin) (*DomainInfo, error)
}

111
pkg/domaininfo/rdap.go Normal file
View file

@ -0,0 +1,111 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package domaininfo
import (
"context"
"time"
"git.happydns.org/happyDomain/model"
"github.com/openrdap/rdap"
)
func GetDomainRDAPInfo(ctx context.Context, domain happydns.Origin) (*happydns.DomainInfo, error) {
client := &rdap.Client{}
req := rdap.NewDomainRequest(string(domain)).WithContext(ctx)
resp, err := client.Do(req)
var domainInfo *rdap.Domain
if err == nil {
var ok bool
domainInfo, ok = resp.Object.(*rdap.Domain)
if !ok {
err = context.DeadlineExceeded // shouldn't happen, but guard anyway
}
}
if err != nil {
if ce, ok := err.(*rdap.ClientError); ok && ce.Type == rdap.ObjectDoesNotExist {
return nil, happydns.DomainDoesNotExist
}
return nil, err
}
// Registrar
registrar := "Unknown"
var registrar_url *string
for _, ent := range domainInfo.Entities {
if ent.Roles != nil {
for _, role := range ent.Roles {
if role == "registrar" && ent.VCard != nil && len(ent.VCard.Get("fn")) > 0 {
registrar = ent.VCard.Get("fn")[0].Value.(string)
if len(ent.VCard.Get("url")) > 0 {
url := ent.VCard.Get("url")[0].Value.(string)
registrar_url = &url
}
}
}
}
}
// Dates
var expiration *time.Time
var creation *time.Time
for _, event := range domainInfo.Events {
if (event.Action == "expiration" || event.Action == "registration") && event.Date != "" {
date, err := time.Parse(time.RFC3339, event.Date)
if err != nil {
return nil, err
}
if event.Action == "expiration" {
expiration = &date
} else if event.Action == "registration" {
creation = &date
}
}
}
// Nameservers
var nameservers []string
for _, nameserver := range domainInfo.Nameservers {
if nameserver.UnicodeName != "" {
nameservers = append(nameservers, nameserver.UnicodeName)
} else {
nameservers = append(nameservers, nameserver.LDHName)
}
}
name := domainInfo.UnicodeName
if name == "" {
name = domainInfo.LDHName
}
return &happydns.DomainInfo{
Name: name,
Nameservers: nameservers,
CreationDate: creation,
ExpirationDate: expiration,
Registrar: registrar,
RegistrarURL: registrar_url,
Status: domainInfo.Status,
}, nil
}

View file

@ -0,0 +1,72 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package domaininfo_test
import (
"context"
"errors"
"testing"
happydns "git.happydns.org/happyDomain/model"
"git.happydns.org/happyDomain/pkg/domaininfo"
)
func TestGetDomainRDAPInfo_KnownDomain(t *testing.T) {
if testing.Short() {
t.Skip("skipping live RDAP integration test")
}
info, err := domaininfo.GetDomainRDAPInfo(context.Background(), happydns.Origin("example.com"))
if err != nil {
t.Fatalf("unexpected error for example.com: %v", err)
}
if info == nil {
t.Fatal("expected non-nil DomainInfo")
}
if info.Name == "" {
t.Error("expected Name to be set")
}
if len(info.Nameservers) == 0 {
t.Error("expected at least one nameserver")
}
if info.ExpirationDate == nil {
t.Error("expected ExpirationDate to be set")
}
if info.CreationDate == nil {
t.Error("expected CreationDate to be set")
}
if info.Registrar == "" {
t.Error("expected Registrar to be set")
}
}
func TestGetDomainRDAPInfo_NonExistentDomain(t *testing.T) {
if testing.Short() {
t.Skip("skipping live RDAP integration test")
}
_, err := domaininfo.GetDomainRDAPInfo(context.Background(), happydns.Origin("this-domain-definitely-does-not-exist-xyz987654321.com"))
if !errors.Is(err, happydns.DomainDoesNotExist) {
t.Errorf("expected DomainDoesNotExist error, got: %v", err)
}
}

75
pkg/domaininfo/whois.go Normal file
View file

@ -0,0 +1,75 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package domaininfo
import (
"context"
"errors"
"time"
"git.happydns.org/happyDomain/model"
"github.com/likexian/whois"
"github.com/likexian/whois-parser"
)
func GetDomainWhoisInfo(ctx context.Context, domain happydns.Origin) (*happydns.DomainInfo, error) {
client := whois.NewClient()
// The whois library has no context support; derive a timeout from the
// context deadline so we at least honour it approximately.
if deadline, ok := ctx.Deadline(); ok {
if remaining := time.Until(deadline); remaining > 0 {
client.SetTimeout(remaining)
}
}
raw, err := client.Whois(string(domain))
if err != nil {
return nil, err
}
result, err := whoisparser.Parse(raw)
if err != nil {
if errors.Is(err, whoisparser.ErrNotFoundDomain) {
return nil, happydns.DomainDoesNotExist
}
return nil, err
}
registrar := "Unknown"
var registrar_url *string
if result.Registrar != nil {
registrar = result.Registrar.Name
registrar_url = &result.Registrar.ReferralURL
}
return &happydns.DomainInfo{
Name: result.Domain.Domain,
Nameservers: result.Domain.NameServers,
CreationDate: result.Domain.CreatedDateInTime,
ExpirationDate: result.Domain.ExpirationDateInTime,
Registrar: registrar,
RegistrarURL: registrar_url,
Status: result.Domain.Status,
}, nil
}

View file

@ -0,0 +1,75 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package domaininfo_test
import (
"context"
"testing"
happydns "git.happydns.org/happyDomain/model"
"git.happydns.org/happyDomain/pkg/domaininfo"
)
func TestGetDomainWhoisInfo_KnownDomain(t *testing.T) {
if testing.Short() {
t.Skip("skipping live WHOIS integration test")
}
info, err := domaininfo.GetDomainWhoisInfo(context.Background(), happydns.Origin("example.com"))
if err != nil {
t.Fatalf("unexpected error for example.com: %v", err)
}
if info == nil {
t.Fatal("expected non-nil DomainInfo")
}
if info.Name == "" {
t.Error("expected Name to be set")
}
if len(info.Nameservers) == 0 {
t.Error("expected at least one nameserver")
}
if info.ExpirationDate == nil {
t.Error("expected ExpirationDate to be set")
}
if info.CreationDate == nil {
t.Error("expected CreationDate to be set")
}
if info.Registrar == "" {
t.Error("expected Registrar to be set")
}
}
func TestGetDomainWhoisInfo_NilRegistrarURL(t *testing.T) {
if testing.Short() {
t.Skip("skipping live WHOIS integration test")
}
// example.com's registrar referral URL may be empty; the function should
// not panic and should return a valid DomainInfo regardless.
info, err := domaininfo.GetDomainWhoisInfo(context.Background(), happydns.Origin("example.com"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// RegistrarURL may be nil or non-nil depending on the WHOIS data; just
// confirm the function handled it without panicking.
_ = info.RegistrarURL
}