refactor: refactor archirecture

This commit is contained in:
Simon Vieille 2025-10-03 12:47:15 +02:00
commit 0281eea177
Signed by: deblan
GPG key ID: 579388D585F70417
15 changed files with 295 additions and 249 deletions

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 (
"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 {

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/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{

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