diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml index 44b684f..2a09ff7 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/build.yml @@ -9,7 +9,7 @@ when: - renovate/* variables: - - &golang_image 'golang:1.24' + - &golang_image 'golang:1.22' depends_on: - test diff --git a/.woodpecker/test.yml b/.woodpecker/test.yml index 81b482f..09fb0ed 100644 --- a/.woodpecker/test.yml +++ b/.woodpecker/test.yml @@ -9,7 +9,7 @@ when: - renovate/* steps: - "Check shell scripts": + "Check sehll scripts": image: pipelinecomponents/shellcheck commands: - shellcheck ./bin/*.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index e3fb5b1..0df0213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,5 @@ [Unreleased] -## v2.0.0 -### Changed -- new project archirecture - -## v1.4.0 -### Added -- add logger and option `-v` -### Fixed -- fix: render days and date when the value is later than danger value - ## v1.3.0 ### Added - add manpage for Debian diff --git a/Makefile b/Makefile index 576acde..b895abb 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) cmd/main.go + -o $(BIN_LINUX_AMD64) . .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) cmd/main.go + -o $(BIN_LINUX_ARM64) . .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) cmd/main.go + -o $(BIN_WIN_AMD64) . .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) cmd/main.go + -o $(BIN_WIN_ARM64) . .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) cmd/main.go + -o $(BIN_DARWIN_AMD64) . .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) cmd/main.go + -o $(BIN_DARWIN_ARM64) . .PHONY: $(MAN_OUTPUT) $(MAN_OUTPUT): diff --git a/internal/cmd/app.go b/app.go similarity index 80% rename from internal/cmd/app.go rename to app.go index f0704f7..a39982f 100644 --- a/internal/cmd/app.go +++ b/app.go @@ -1,10 +1,9 @@ -package cmd +package main import ( "github.com/urfave/cli/v2" - "gitnet.fr/deblan/expiration-check/pkg/checker" - "gitnet.fr/deblan/expiration-check/pkg/logger" - "gitnet.fr/deblan/expiration-check/pkg/render" + "gitnet.fr/deblan/expiration-check/checker" + "gitnet.fr/deblan/expiration-check/render" ) func NormalizeFormat(format string) string { @@ -34,11 +33,6 @@ func App() *cli.App { Value: "table", Usage: "output format: table, csv, tsv, html, json, markdown", }, - &cli.BoolFlag{ - Name: "verbose", - Aliases: []string{"v"}, - Required: false, - }, } return &cli.App{ @@ -51,8 +45,6 @@ func App() *cli.App { Usage: "Checks certificate", Flags: flags, Action: func(c *cli.Context) error { - logger.Get().SetVerbose(c.Bool("verbose")) - render.Render( checker.CheckCertificates(c.StringSlice("domain")), 30, 14, @@ -68,8 +60,6 @@ func App() *cli.App { Aliases: []string{"d", "domains"}, Flags: flags, Action: func(c *cli.Context) error { - logger.Get().SetVerbose(c.Bool("verbose")) - render.Render( checker.CheckDomains(c.StringSlice("domain")), 30, 14, diff --git a/checker/certificates.go b/checker/certificates.go new file mode 100644 index 0000000..d0721c6 --- /dev/null +++ b/checker/certificates.go @@ -0,0 +1,47 @@ +package checker + +import ( + "crypto/tls" + "fmt" + "math" + "strings" + "time" +) + +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 + + return Domain{ + Name: domain, + DaysLeft: math.Floor(daysLeft), + Date: date.Format(time.DateTime), + } + } + + 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 new file mode 100644 index 0000000..119d229 --- /dev/null +++ b/checker/domains.go @@ -0,0 +1,140 @@ +package checker + +import ( + "encoding/json" + "fmt" + "math" + "net/http" + "regexp" + "strings" + "time" + + "github.com/likexian/whois" +) + +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 + + return Domain{ + Name: domain, + DaysLeft: math.Floor(daysLeft), + Date: date.Format(time.DateTime), + } + } + } + + return Domain{Name: domain, Failed: true} +} + +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 + + return Domain{ + Name: domain, + DaysLeft: math.Floor(daysLeft), + Date: date.Format(time.DateTime), + } + } + } + } + } + + return domainFailed +} + +func CheckDomains(domains []string) []Domain { + values := []Domain{} + services := GetRdapServices() + + for _, domain := range domains { + tld := ExtractTld(domain) + service := services[tld] + + if service != "" { + values = append(values, RdapCheck(domain, service)) + } else { + values = append(values, WhoisCheck(domain)) + } + } + + return values +} diff --git a/checker/struct.go b/checker/struct.go new file mode 100644 index 0000000..ffdcfde --- /dev/null +++ b/checker/struct.go @@ -0,0 +1,8 @@ +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 deleted file mode 100644 index f436970..0000000 --- a/cmd/main.go +++ /dev/null @@ -1,14 +0,0 @@ -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/main.go b/main.go new file mode 100644 index 0000000..b2bb647 --- /dev/null +++ b/main.go @@ -0,0 +1,12 @@ +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 deleted file mode 100644 index ccc1f67..0000000 --- a/pkg/checker/certificate.go +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 3b078bf..0000000 --- a/pkg/checker/domain.go +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index 4031eeb..0000000 --- a/pkg/extractor/tld.go +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 42dd1e3..0000000 --- a/pkg/formatter/http_domain.go +++ /dev/null @@ -1,16 +0,0 @@ -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/pkg/logger/logger.go b/pkg/logger/logger.go deleted file mode 100644 index 1461150..0000000 --- a/pkg/logger/logger.go +++ /dev/null @@ -1,27 +0,0 @@ -package logger - -import "log" - -type Logger struct { - Verbose bool -} - -var logger *Logger - -func Get() *Logger { - if logger == nil { - logger = new(Logger) - } - - return logger -} - -func (l *Logger) SetVerbose(value bool) { - l.Verbose = value -} - -func (l *Logger) Logf(format string, v ...any) { - if l.Verbose { - log.Printf(format, v...) - } -} diff --git a/pkg/model/result.go b/pkg/model/result.go deleted file mode 100644 index 4384f08..0000000 --- a/pkg/model/result.go +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 1335d61..0000000 --- a/pkg/rdap/rdap.go +++ /dev/null @@ -1,75 +0,0 @@ -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/pkg/whois/whois.go b/pkg/whois/whois.go deleted file mode 100644 index afc0617..0000000 --- a/pkg/whois/whois.go +++ /dev/null @@ -1,47 +0,0 @@ -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") -} diff --git a/pkg/render/render.go b/render/render.go similarity index 67% rename from pkg/render/render.go rename to render/render.go index fded2b0..d3cd037 100644 --- a/pkg/render/render.go +++ b/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/pkg/model" + "gitnet.fr/deblan/expiration-check/checker" ) 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 []*model.Result, warning, danger float64, format string) { +func Render(values []checker.Domain, 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 []*model.Result, 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 []*model.Result, 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 = failed + date = failed + } 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{