Compare commits

...

4 commits

Author SHA1 Message Date
cdfa3c8e09
update changelog
Some checks are pending
ci/woodpecker/push/build Pipeline is pending approval
ci/woodpecker/push/test Pipeline is pending approval
ci/woodpecker/tag/test Pipeline was successful
ci/woodpecker/tag/build Pipeline was successful
2025-10-03 12:51:21 +02:00
5e37669ba6
build: update makefile rules according to the refactor
Some checks are pending
ci/woodpecker/push/build Pipeline is pending approval
ci/woodpecker/push/test Pipeline is pending approval
2025-10-03 12:50:08 +02:00
f9c209e576
ci: upgrade golang to v1.24
Some checks are pending
ci/woodpecker/push/build Pipeline is pending approval
ci/woodpecker/push/test Pipeline is pending approval
2025-10-03 12:47:35 +02:00
0281eea177
refactor: refactor archirecture 2025-10-03 12:47:15 +02:00
18 changed files with 306 additions and 256 deletions

View file

@ -9,7 +9,7 @@ when:
- renovate/* - renovate/*
variables: variables:
- &golang_image 'golang:1.22' - &golang_image 'golang:1.24'
depends_on: depends_on:
- test - test

View file

@ -1,5 +1,9 @@
[Unreleased] [Unreleased]
## v2.0.0
### Changed
- new project archirecture
## v1.4.0 ## v1.4.0
### Added ### Added
- add logger and option `-v` - add logger and option `-v`

View file

@ -50,7 +50,7 @@ $(BIN_LINUX_AMD64):
GOOS=$(GO_OS_LINUX) \ GOOS=$(GO_OS_LINUX) \
CGO_ENABLED=$(CGO_ENABLED) \ CGO_ENABLED=$(CGO_ENABLED) \
$(CC) $(CFLAGS) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" \ $(CC) $(CFLAGS) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" \
-o $(BIN_LINUX_AMD64) . -o $(BIN_LINUX_AMD64) cmd/main.go
.PHONY: $(BIN_LINUX_ARM64) .PHONY: $(BIN_LINUX_ARM64)
$(BIN_LINUX_ARM64): $(BIN_LINUX_ARM64):
@ -59,7 +59,7 @@ $(BIN_LINUX_ARM64):
GOOS=$(GO_OS_LINUX) \ GOOS=$(GO_OS_LINUX) \
CGO_ENABLED=$(CGO_ENABLED) \ CGO_ENABLED=$(CGO_ENABLED) \
$(CC) $(CFLAGS) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" \ $(CC) $(CFLAGS) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" \
-o $(BIN_LINUX_ARM64) . -o $(BIN_LINUX_ARM64) cmd/main.go
.PHONY: $(BIN_WIN_AMD64) .PHONY: $(BIN_WIN_AMD64)
$(BIN_WIN_AMD64): $(BIN_WIN_AMD64):
@ -68,7 +68,7 @@ $(BIN_WIN_AMD64):
GOOS=$(GO_OS_WIN) \ GOOS=$(GO_OS_WIN) \
CGO_ENABLED=$(CGO_ENABLED) \ CGO_ENABLED=$(CGO_ENABLED) \
$(CC) $(CFLAGS) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" \ $(CC) $(CFLAGS) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" \
-o $(BIN_WIN_AMD64) . -o $(BIN_WIN_AMD64) cmd/main.go
.PHONY: $(BIN_WIN_ARM64) .PHONY: $(BIN_WIN_ARM64)
$(BIN_WIN_ARM64): $(BIN_WIN_ARM64):
@ -77,7 +77,7 @@ $(BIN_WIN_ARM64):
GOOS=$(GO_OS_WIN) \ GOOS=$(GO_OS_WIN) \
CGO_ENABLED=$(CGO_ENABLED) \ CGO_ENABLED=$(CGO_ENABLED) \
$(CC) $(CFLAGS) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" \ $(CC) $(CFLAGS) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" \
-o $(BIN_WIN_ARM64) . -o $(BIN_WIN_ARM64) cmd/main.go
.PHONY: $(BIN_DARWIN_AMD64) .PHONY: $(BIN_DARWIN_AMD64)
$(BIN_DARWIN_AMD64): $(BIN_DARWIN_AMD64):
@ -86,7 +86,7 @@ $(BIN_DARWIN_AMD64):
GOOS=$(GO_OS_DARWIN) \ GOOS=$(GO_OS_DARWIN) \
CGO_ENABLED=$(CGO_ENABLED) \ CGO_ENABLED=$(CGO_ENABLED) \
$(CC) $(CFLAGS) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" \ $(CC) $(CFLAGS) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" \
-o $(BIN_DARWIN_AMD64) . -o $(BIN_DARWIN_AMD64) cmd/main.go
.PHONY: $(BIN_DARWIN_ARM64) .PHONY: $(BIN_DARWIN_ARM64)
$(BIN_DARWIN_ARM64): $(BIN_DARWIN_ARM64):
@ -95,7 +95,7 @@ $(BIN_DARWIN_ARM64):
GOOS=$(GO_OS_DARWIN) \ GOOS=$(GO_OS_DARWIN) \
CGO_ENABLED=$(CGO_ENABLED) \ CGO_ENABLED=$(CGO_ENABLED) \
$(CC) $(CFLAGS) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" \ $(CC) $(CFLAGS) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" \
-o $(BIN_DARWIN_ARM64) . -o $(BIN_DARWIN_ARM64) cmd/main.go
.PHONY: $(MAN_OUTPUT) .PHONY: $(MAN_OUTPUT)
$(MAN_OUTPUT): $(MAN_OUTPUT):

View file

@ -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
}

View file

@ -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
}

View file

@ -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"`
}

14
cmd/main.go Normal file
View file

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

View file

@ -1,10 +1,10 @@
package main package cmd
import ( import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"gitnet.fr/deblan/expiration-check/checker" "gitnet.fr/deblan/expiration-check/pkg/checker"
"gitnet.fr/deblan/expiration-check/logger" "gitnet.fr/deblan/expiration-check/pkg/logger"
"gitnet.fr/deblan/expiration-check/render" "gitnet.fr/deblan/expiration-check/pkg/render"
) )
func NormalizeFormat(format string) string { func NormalizeFormat(format string) string {

12
main.go
View file

@ -1,12 +0,0 @@
package main
import (
"log"
"os"
)
func main() {
if err := App().Run(os.Args); err != nil {
log.Fatal(err)
}
}

View file

@ -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
}

53
pkg/checker/domain.go Normal file
View file

@ -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
}

13
pkg/extractor/tld.go Normal file
View file

@ -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:], ".")
}

View file

@ -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
}

24
pkg/model/result.go Normal file
View file

@ -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,
}
}

75
pkg/rdap/rdap.go Normal file
View file

@ -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")
}

View file

@ -8,7 +8,7 @@ import (
"github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text" "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 { 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) 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 { sort.SliceStable(values, func(i, j int) bool {
if values[i].Failed && values[j].Failed { if values[i].Failed && values[j].Failed {
return values[i].Name < values[j].Name return values[i].Name < values[j].Name
@ -33,7 +33,7 @@ func Render(values []checker.Domain, warning, danger float64, format string) {
return true return true
} }
return values[i].DaysLeft < values[j].DaysLeft return *values[i].DaysLeft < *values[j].DaysLeft
}) })
if format == "json" { if format == "json" {
@ -64,15 +64,15 @@ func Render(values []checker.Domain, warning, danger float64, format string) {
var days string var days string
var date string var date string
if value.DaysLeft <= danger { if *value.DaysLeft <= danger {
days = RenderColor(fmt.Sprintf("%.0f", value.DaysLeft), text.Colors{0, text.FgRed}, format) days = RenderColor(fmt.Sprintf("%.0f", *value.DaysLeft), text.Colors{0, text.FgRed}, format)
date = RenderColor(value.Date, text.Colors{0, text.FgRed}, format) date = RenderColor(*value.Date, text.Colors{0, text.FgRed}, format)
} else if value.DaysLeft <= warning { } else if *value.DaysLeft <= warning {
days = RenderColor(fmt.Sprintf("%.0f", value.DaysLeft), text.Colors{0, text.FgYellow}, format) days = RenderColor(fmt.Sprintf("%.0f", *value.DaysLeft), text.Colors{0, text.FgYellow}, format)
date = RenderColor(value.Date, text.Colors{0, text.FgYellow}, format) date = RenderColor(*value.Date, text.Colors{0, text.FgYellow}, format)
} else { } else {
days = RenderColor(fmt.Sprintf("%.0f", value.DaysLeft), 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) date = RenderColor(*value.Date, text.Colors{0, text.FgGreen}, format)
} }
t.AppendRow(table.Row{ t.AppendRow(table.Row{

47
pkg/whois/whois.go Normal file
View file

@ -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")
}