From 0281eea177a8bf11607d2077f0741df7c0d7d249 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 3 Oct 2025 12:47:15 +0200 Subject: [PATCH 1/4] refactor: refactor archirecture --- checker/certificates.go | 55 ----------- checker/domains.go | 159 ------------------------------- checker/struct.go | 8 -- cmd/main.go | 14 +++ app.go => internal/cmd/app.go | 8 +- main.go | 12 --- pkg/checker/certificate.go | 38 ++++++++ pkg/checker/domain.go | 53 +++++++++++ pkg/extractor/tld.go | 13 +++ pkg/formatter/http_domain.go | 16 ++++ {logger => pkg/logger}/logger.go | 0 pkg/model/result.go | 24 +++++ pkg/rdap/rdap.go | 75 +++++++++++++++ {render => pkg/render}/render.go | 22 ++--- pkg/whois/whois.go | 47 +++++++++ 15 files changed, 295 insertions(+), 249 deletions(-) delete mode 100644 checker/certificates.go delete mode 100644 checker/domains.go delete mode 100644 checker/struct.go create mode 100644 cmd/main.go rename app.go => internal/cmd/app.go (91%) delete mode 100644 main.go create mode 100644 pkg/checker/certificate.go create mode 100644 pkg/checker/domain.go create mode 100644 pkg/extractor/tld.go create mode 100644 pkg/formatter/http_domain.go rename {logger => pkg/logger}/logger.go (100%) create mode 100644 pkg/model/result.go create mode 100644 pkg/rdap/rdap.go rename {render => pkg/render}/render.go (67%) create mode 100644 pkg/whois/whois.go diff --git a/checker/certificates.go b/checker/certificates.go deleted file mode 100644 index 1ee3caf..0000000 --- a/checker/certificates.go +++ /dev/null @@ -1,55 +0,0 @@ -package checker - -import ( - "crypto/tls" - "fmt" - "math" - "strings" - "time" - - "gitnet.fr/deblan/expiration-check/logger" -) - -func FormatDomain(domain string) string { - elements := strings.Split(domain, ":") - - if len(elements) == 1 { - return fmt.Sprintf("%s:443", elements[0]) - } - - return domain -} - -func CheckCertificate(domain string) Domain { - now := time.Now() - conn, err := tls.Dial("tcp", FormatDomain(domain), nil) - - if err == nil { - date := conn.ConnectionState().PeerCertificates[0].NotAfter - daysLeft := date.Sub(now).Hours() / 24 - - d := Domain{ - Name: domain, - DaysLeft: math.Floor(daysLeft), - Date: date.Format(time.DateTime), - } - - logger.Get().Logf(`CheckCertificate: domain="%s" value="%+v"`, domain, d) - - return d - } - - logger.Get().Logf("CheckCertificate: domain=%s err=%s", domain, err) - - return Domain{Name: domain, Failed: true} -} - -func CheckCertificates(domains []string) []Domain { - values := []Domain{} - - for _, domain := range domains { - values = append(values, CheckCertificate(domain)) - } - - return values -} diff --git a/checker/domains.go b/checker/domains.go deleted file mode 100644 index 1d975e2..0000000 --- a/checker/domains.go +++ /dev/null @@ -1,159 +0,0 @@ -package checker - -import ( - "encoding/json" - "fmt" - "math" - "net/http" - "regexp" - "strings" - "time" - - "github.com/likexian/whois" - "gitnet.fr/deblan/expiration-check/logger" -) - -type RdapResponseData struct { - Events []struct { - EventAction string `json:"eventAction"` - EventDate string `json:"eventDate"` - } `json:"events"` -} - -type RdapServices struct { - Services [][][]string `json:"services"` -} - -func GetRdapServices() map[string]string { - response, _ := http.Get("https://data.iana.org/rdap/dns.json") - values := make(map[string]string) - - defer response.Body.Close() - var data RdapServices - json.NewDecoder(response.Body).Decode(&data) - - for _, value := range data.Services { - for _, tld := range value[0] { - values[tld] = value[1][0] - } - } - - return values -} - -func ExtractTld(domain string) string { - elements := strings.Split(domain, ".") - - if len(elements) == 1 { - return elements[0] - } - - return strings.Join(elements[1:], ".") -} - -func RdapCheck(domain, service string) Domain { - url := fmt.Sprintf("%sdomain/%s?jscard=1", service, domain) - response, _ := http.Get(url) - now := time.Now() - - defer response.Body.Close() - var data RdapResponseData - json.NewDecoder(response.Body).Decode(&data) - - for _, event := range data.Events { - if event.EventAction == "expiration" { - date, _ := time.Parse(time.RFC3339, event.EventDate) - daysLeft := date.Sub(now).Hours() / 24 - - d := Domain{ - Name: domain, - DaysLeft: math.Floor(daysLeft), - Date: date.Format(time.DateTime), - } - - logger.Get().Logf(`RdapCheck: domain="%s" value="%+v"`, domain, d) - - return d - } - } - - d := Domain{Name: domain, Failed: true} - - logger.Get().Logf(`RdapCheck: domain="%s" value="%+v"`, domain, d) - - return d -} - -func WhoisCheck(domain string) Domain { - domainFailed := Domain{Name: domain, Failed: true} - result, err := whois.Whois(domain) - if err != nil { - return domainFailed - } - - now := time.Now() - formats := []string{ - "expiration date", - "expiry date", - "expires on", - "paid-till", - "renewal", - "expires", - "domain_datebilleduntil", - "expiration", - "registry expiry", - "registrar registration expiration", - } - - result = strings.ToLower(result) - - for _, format := range formats { - r, _ := regexp.Compile(fmt.Sprintf(`%s\s*:?\s*([^\s]+)`, format)) - - for i, match := range r.FindStringSubmatch(result) { - if i%2 == 1 { - date, err := time.Parse(time.RFC3339, strings.ToUpper(match)) - - if err == nil { - daysLeft := date.Sub(now).Hours() / 24 - - d := Domain{ - Name: domain, - DaysLeft: math.Floor(daysLeft), - Date: date.Format(time.DateTime), - } - - logger.Get().Logf(`WhoisCheck: domain="%s" value="%+v"`, domain, d) - - return d - } - } - } - } - - logger.Get().Logf(`WhoisCheck: domain="%s" value="%+v"`, domain, domainFailed) - - return domainFailed -} - -func CheckDomains(domains []string) []Domain { - values := []Domain{} - services := GetRdapServices() - - for _, domain := range domains { - tld := ExtractTld(domain) - service := services[tld] - - if service != "" { - logger.Get().Logf(`CheckDomains: domain="%s" rdap=true whois=false`, domain) - - values = append(values, RdapCheck(domain, service)) - } else { - logger.Get().Logf(`CheckDomains: domain="%s" rdap=false whois=true`, domain) - - values = append(values, WhoisCheck(domain)) - } - } - - return values -} diff --git a/checker/struct.go b/checker/struct.go deleted file mode 100644 index ffdcfde..0000000 --- a/checker/struct.go +++ /dev/null @@ -1,8 +0,0 @@ -package checker - -type Domain struct { - Name string `json:"name"` - DaysLeft float64 `json:"days"` - Date string `json:"date"` - Failed bool `json:"failed"` -} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..f436970 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "log" + "os" + + "gitnet.fr/deblan/expiration-check/internal/cmd" +) + +func main() { + if err := cmd.App().Run(os.Args); err != nil { + log.Fatal(err) + } +} diff --git a/app.go b/internal/cmd/app.go similarity index 91% rename from app.go rename to internal/cmd/app.go index a45ac26..f0704f7 100644 --- a/app.go +++ b/internal/cmd/app.go @@ -1,10 +1,10 @@ -package main +package cmd import ( "github.com/urfave/cli/v2" - "gitnet.fr/deblan/expiration-check/checker" - "gitnet.fr/deblan/expiration-check/logger" - "gitnet.fr/deblan/expiration-check/render" + "gitnet.fr/deblan/expiration-check/pkg/checker" + "gitnet.fr/deblan/expiration-check/pkg/logger" + "gitnet.fr/deblan/expiration-check/pkg/render" ) func NormalizeFormat(format string) string { diff --git a/main.go b/main.go deleted file mode 100644 index b2bb647..0000000 --- a/main.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -import ( - "log" - "os" -) - -func main() { - if err := App().Run(os.Args); err != nil { - log.Fatal(err) - } -} diff --git a/pkg/checker/certificate.go b/pkg/checker/certificate.go new file mode 100644 index 0000000..ccc1f67 --- /dev/null +++ b/pkg/checker/certificate.go @@ -0,0 +1,38 @@ +package checker + +import ( + "crypto/tls" + "math" + "time" + + "gitnet.fr/deblan/expiration-check/pkg/formatter" + "gitnet.fr/deblan/expiration-check/pkg/model" +) + +func CheckCertificate(domain string) *model.Result { + conn, err := tls.Dial("tcp", formatter.ToDomainAndHttpPort(domain), nil) + + if err == nil { + date := conn.ConnectionState().PeerCertificates[0].NotAfter + + result := model.NewResult( + domain, + math.Floor(date.Sub(time.Now()).Hours()/24), + date.Format(time.DateTime), + ) + + return result + } + + return model.NewResultFailed(domain) +} + +func CheckCertificates(domains []string) []*model.Result { + var values []*model.Result + + for _, domain := range domains { + values = append(values, CheckCertificate(domain)) + } + + return values +} diff --git a/pkg/checker/domain.go b/pkg/checker/domain.go new file mode 100644 index 0000000..3b078bf --- /dev/null +++ b/pkg/checker/domain.go @@ -0,0 +1,53 @@ +package checker + +import ( + "math" + "time" + + "gitnet.fr/deblan/expiration-check/pkg/extractor" + "gitnet.fr/deblan/expiration-check/pkg/logger" + "gitnet.fr/deblan/expiration-check/pkg/model" + "gitnet.fr/deblan/expiration-check/pkg/rdap" + "gitnet.fr/deblan/expiration-check/pkg/whois" +) + +func CheckDomains(domains []string) []*model.Result { + var err error + var date *time.Time + var values []*model.Result + + services, err := rdap.GetRdapServices() + + if err != nil { + logger.Get().Logf("GetRdapServices failed: error=%v", err) + + return values + } + + for _, domain := range domains { + tld := extractor.ExtractTld(domain) + service, ok := services[tld] + + if ok { + date, err = rdap.GetExpiration(domain, service) + } else { + date, err = whois.GetExpiration(domain) + } + + if err != nil { + logger.Get().Logf("CheckDomain: domain=%s error=%s", domain, err) + + values = append(values, model.NewResultFailed(domain)) + } else { + daysLeft := math.Floor(date.Sub(time.Now()).Hours() / 24) + + values = append(values, model.NewResult( + domain, + daysLeft, + date.Format(time.DateTime), + )) + } + } + + return values +} diff --git a/pkg/extractor/tld.go b/pkg/extractor/tld.go new file mode 100644 index 0000000..4031eeb --- /dev/null +++ b/pkg/extractor/tld.go @@ -0,0 +1,13 @@ +package extractor + +import "strings" + +func ExtractTld(domain string) string { + elements := strings.Split(domain, ".") + + if len(elements) == 1 { + return elements[0] + } + + return strings.Join(elements[1:], ".") +} diff --git a/pkg/formatter/http_domain.go b/pkg/formatter/http_domain.go new file mode 100644 index 0000000..42dd1e3 --- /dev/null +++ b/pkg/formatter/http_domain.go @@ -0,0 +1,16 @@ +package formatter + +import ( + "fmt" + "strings" +) + +func ToDomainAndHttpPort(domain string) string { + elements := strings.Split(domain, ":") + + if len(elements) == 1 { + return fmt.Sprintf("%s:443", elements[0]) + } + + return domain +} diff --git a/logger/logger.go b/pkg/logger/logger.go similarity index 100% rename from logger/logger.go rename to pkg/logger/logger.go diff --git a/pkg/model/result.go b/pkg/model/result.go new file mode 100644 index 0000000..4384f08 --- /dev/null +++ b/pkg/model/result.go @@ -0,0 +1,24 @@ +package model + +type Result struct { + Name string `json:"name"` + DaysLeft *float64 `json:"days"` + Date *string `json:"date"` + Failed bool `json:"failed"` +} + +func NewResult(name string, daysLeft float64, date string) *Result { + return &Result{ + Name: name, + DaysLeft: &daysLeft, + Date: &date, + Failed: false, + } +} + +func NewResultFailed(name string) *Result { + return &Result{ + Name: name, + Failed: true, + } +} diff --git a/pkg/rdap/rdap.go b/pkg/rdap/rdap.go new file mode 100644 index 0000000..1335d61 --- /dev/null +++ b/pkg/rdap/rdap.go @@ -0,0 +1,75 @@ +package rdap + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "time" +) + +type RdapResponseData struct { + Events []struct { + EventAction string `json:"eventAction"` + EventDate string `json:"eventDate"` + } `json:"events"` +} + +type RdapServices struct { + Services [][][]string `json:"services"` +} + +func GetRdapServices() (map[string]string, error) { + response, err := http.Get("https://data.iana.org/rdap/dns.json") + + if err != nil { + return nil, err + } + + values := make(map[string]string) + + defer response.Body.Close() + var data RdapServices + + json.NewDecoder(response.Body).Decode(&data) + + for _, value := range data.Services { + for _, tld := range value[0] { + values[tld] = value[1][0] + } + } + + return values, nil +} + +func GetExpiration(domain, service string) (*time.Time, error) { + url := fmt.Sprintf("%sdomain/%s?jscard=1", service, domain) + response, err := http.Get(url) + + if err != nil { + return nil, err + } + + defer response.Body.Close() + var data RdapResponseData + + json.NewDecoder(response.Body).Decode(&data) + + actions := []string{"expiration", "Record expires"} + + for _, event := range data.Events { + for _, action := range actions { + if action == event.EventAction { + date, err := time.Parse(time.RFC3339, event.EventDate) + + if err != nil { + return nil, err + } + + return &date, nil + } + } + } + + return nil, errors.New("Expiration date not found") +} diff --git a/render/render.go b/pkg/render/render.go similarity index 67% rename from render/render.go rename to pkg/render/render.go index eed71eb..fded2b0 100644 --- a/render/render.go +++ b/pkg/render/render.go @@ -8,7 +8,7 @@ import ( "github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/text" - "gitnet.fr/deblan/expiration-check/checker" + "gitnet.fr/deblan/expiration-check/pkg/model" ) func RenderColor(value string, c text.Colors, format string) string { @@ -19,7 +19,7 @@ func RenderColor(value string, c text.Colors, format string) string { return c.Sprint(value) } -func Render(values []checker.Domain, warning, danger float64, format string) { +func Render(values []*model.Result, warning, danger float64, format string) { sort.SliceStable(values, func(i, j int) bool { if values[i].Failed && values[j].Failed { return values[i].Name < values[j].Name @@ -33,7 +33,7 @@ func Render(values []checker.Domain, warning, danger float64, format string) { return true } - return values[i].DaysLeft < values[j].DaysLeft + return *values[i].DaysLeft < *values[j].DaysLeft }) if format == "json" { @@ -64,15 +64,15 @@ func Render(values []checker.Domain, warning, danger float64, format string) { var days string var date string - if value.DaysLeft <= danger { - days = RenderColor(fmt.Sprintf("%.0f", value.DaysLeft), text.Colors{0, text.FgRed}, format) - date = RenderColor(value.Date, text.Colors{0, text.FgRed}, format) - } else if value.DaysLeft <= warning { - days = RenderColor(fmt.Sprintf("%.0f", value.DaysLeft), text.Colors{0, text.FgYellow}, format) - date = RenderColor(value.Date, text.Colors{0, text.FgYellow}, format) + if *value.DaysLeft <= danger { + days = RenderColor(fmt.Sprintf("%.0f", *value.DaysLeft), text.Colors{0, text.FgRed}, format) + date = RenderColor(*value.Date, text.Colors{0, text.FgRed}, format) + } else if *value.DaysLeft <= warning { + days = RenderColor(fmt.Sprintf("%.0f", *value.DaysLeft), text.Colors{0, text.FgYellow}, format) + date = RenderColor(*value.Date, text.Colors{0, text.FgYellow}, format) } else { - days = RenderColor(fmt.Sprintf("%.0f", value.DaysLeft), text.Colors{0, text.FgGreen}, format) - date = RenderColor(value.Date, text.Colors{0, text.FgGreen}, format) + days = RenderColor(fmt.Sprintf("%.0f", *value.DaysLeft), text.Colors{0, text.FgGreen}, format) + date = RenderColor(*value.Date, text.Colors{0, text.FgGreen}, format) } t.AppendRow(table.Row{ diff --git a/pkg/whois/whois.go b/pkg/whois/whois.go new file mode 100644 index 0000000..afc0617 --- /dev/null +++ b/pkg/whois/whois.go @@ -0,0 +1,47 @@ +package whois + +import ( + "errors" + "fmt" + "regexp" + "strings" + "time" + + w "github.com/likexian/whois" +) + +func GetExpiration(domain string) (*time.Time, error) { + result, err := w.Whois(domain) + + if err != nil { + return nil, err + } + + result = strings.ToLower(result) + formats := []string{ + "expiration date", + "expiry date", + "expires on", + "paid-till", + "renewal", + "expires", + "domain_datebilleduntil", + "expiration", + "registry expiry", + "registrar registration expiration", + } + + for _, format := range formats { + r, _ := regexp.Compile(fmt.Sprintf(`%s\s*:?\s*([^\s]+)`, format)) + + for i, match := range r.FindStringSubmatch(result) { + if i%2 == 1 { + if date, err := time.Parse(time.RFC3339, strings.ToUpper(match)); err == nil { + return &date, nil + } + } + } + } + + return nil, errors.New("Expiration date not found") +} From f9c209e57688df6ddb5ed4ed387de67f2ca369e1 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 3 Oct 2025 12:47:33 +0200 Subject: [PATCH 2/4] ci: upgrade golang to v1.24 --- .woodpecker/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml index 2a09ff7..44b684f 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/build.yml @@ -9,7 +9,7 @@ when: - renovate/* variables: - - &golang_image 'golang:1.22' + - &golang_image 'golang:1.24' depends_on: - test From 5e37669ba6fb9058523049df14574ef7f26547a2 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 3 Oct 2025 12:50:08 +0200 Subject: [PATCH 3/4] build: update makefile rules according to the refactor --- Makefile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index b895abb..576acde 100644 --- a/Makefile +++ b/Makefile @@ -50,7 +50,7 @@ $(BIN_LINUX_AMD64): GOOS=$(GO_OS_LINUX) \ CGO_ENABLED=$(CGO_ENABLED) \ $(CC) $(CFLAGS) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" \ - -o $(BIN_LINUX_AMD64) . + -o $(BIN_LINUX_AMD64) cmd/main.go .PHONY: $(BIN_LINUX_ARM64) $(BIN_LINUX_ARM64): @@ -59,7 +59,7 @@ $(BIN_LINUX_ARM64): GOOS=$(GO_OS_LINUX) \ CGO_ENABLED=$(CGO_ENABLED) \ $(CC) $(CFLAGS) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" \ - -o $(BIN_LINUX_ARM64) . + -o $(BIN_LINUX_ARM64) cmd/main.go .PHONY: $(BIN_WIN_AMD64) $(BIN_WIN_AMD64): @@ -68,7 +68,7 @@ $(BIN_WIN_AMD64): GOOS=$(GO_OS_WIN) \ CGO_ENABLED=$(CGO_ENABLED) \ $(CC) $(CFLAGS) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" \ - -o $(BIN_WIN_AMD64) . + -o $(BIN_WIN_AMD64) cmd/main.go .PHONY: $(BIN_WIN_ARM64) $(BIN_WIN_ARM64): @@ -77,7 +77,7 @@ $(BIN_WIN_ARM64): GOOS=$(GO_OS_WIN) \ CGO_ENABLED=$(CGO_ENABLED) \ $(CC) $(CFLAGS) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" \ - -o $(BIN_WIN_ARM64) . + -o $(BIN_WIN_ARM64) cmd/main.go .PHONY: $(BIN_DARWIN_AMD64) $(BIN_DARWIN_AMD64): @@ -86,7 +86,7 @@ $(BIN_DARWIN_AMD64): GOOS=$(GO_OS_DARWIN) \ CGO_ENABLED=$(CGO_ENABLED) \ $(CC) $(CFLAGS) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" \ - -o $(BIN_DARWIN_AMD64) . + -o $(BIN_DARWIN_AMD64) cmd/main.go .PHONY: $(BIN_DARWIN_ARM64) $(BIN_DARWIN_ARM64): @@ -95,7 +95,7 @@ $(BIN_DARWIN_ARM64): GOOS=$(GO_OS_DARWIN) \ CGO_ENABLED=$(CGO_ENABLED) \ $(CC) $(CFLAGS) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" \ - -o $(BIN_DARWIN_ARM64) . + -o $(BIN_DARWIN_ARM64) cmd/main.go .PHONY: $(MAN_OUTPUT) $(MAN_OUTPUT): From cdfa3c8e093ad569ac0580acf84843a28a8bb41f Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 3 Oct 2025 12:51:21 +0200 Subject: [PATCH 4/4] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3211966..e3fb5b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ [Unreleased] +## v2.0.0 +### Changed +- new project archirecture + ## v1.4.0 ### Added - add logger and option `-v`