From a021d3e8ff545e1bc77e5486dbdb18ae1806f8df Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 24 Jul 2024 16:18:05 +0200 Subject: [PATCH 01/18] update readme --- README.md | 82 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index b84f40f..17001c0 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,62 @@ -Expiration check -================ +# ⏰ Expiration check -Checks the expiration dates of domains and certificates. +**Expiration-check** is a command-line tool designed to verify the expiration dates of domains and TLS certificates. -Domain expiration check uses [`RDAP`](https://about.rdap.org/) and fallback with a `whois` request. +For domain verification, it implements the [RDAP protocol](https://about.rdap.org/) whenever possible. If RDAP is unavailable, it falls back on the WHOIS protocol. -## Usage +## How to install the project -Go to [releases](https://gitnet.fr/deblan/expiration-check/releases) and download the latest version. +Pre-compiled versions are available in the [Releases](https://gitnet.fr/deblan/expiration-check/releases). Multiple operating systems are supported, including Linux, Windows, and macOS. For Debian users, a package is also provided. -```text -$ expiration-check domains -d example.com -d other-example.com -+-------------------+------+---------------------+ -| DOMAIN | DAYS | DATE | -+-------------------+------+---------------------+ -| example.com | XX | YYYY-MM-DD HH:MM:SS | -| other-example.com | XXX | YYYY-MM-DD HH:MM:SS | -+-------------------+------+---------------------+ +If you want to compile the project from source, you will need at least the GO compiler version 1.22. Clone the project and run the make command. The compiled output will be located in the build directory. -$ expiration-check certificates -d example.com -d other-example.com -d mail.example.com:993 -+-------------------+------+---------------------+ -| DOMAIN | DAYS | DATE | -+-------------------+------+---------------------+ -| example.com | XX | YYYY-MM-DD HH:MM:SS | -| other-example.com | XXX | YYYY-MM-DD HH:MM:SS | -| mail.example.com | XXX | YYYY-MM-DD HH:MM:SS | -+-------------------+------+---------------------+ +``` +$ git clone https://gitnet.fr/deblan/expiration-check +$ make ``` -You can specify an ouput format using `--format` or `-f`: +## How to use the Project -- `table` (default) -- `json` -- `csv` -- `tsv` -- `html` -- `markdown` +### Commands + +- `certificates`, `certificate`, `cert`, `certs`, `c`: Checks the expiration dates of TLS certificates. +- `domains`, `domain`, `d`: Checks the expiration dates of domain names. +- `help`, `h`: Displays a list of all commands or detailed help for a specific command. + +### Global Options + +- `--help`, `-h`: Shows the help message, providing information about the usage and available commands. + +Use the `--format` or `-f` option to specify the output format. Available formats are table, csv, tsv, html, json, and markdown. The default format is table. + +### Examples + +Check certificate expirations: + +``` +expiration-check certificate --domain example.com +``` + +Check domain expirations: + +``` +expiration-check domain --domain example.com +``` + +Check certificates for multiple domains with default table format: + +``` +expiration-check certificate --domain example.com --domain example.org +``` + +Check certificates and output results in JSON format: + +``` +expiration-check certificate --domain example.com --format json +``` + +Get help for a specific command: + +``` +expiration-check help certificate +``` From 1eabdf5aa1d9ae84bd2546553883a90b323166a8 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 24 Jul 2024 16:21:44 +0200 Subject: [PATCH 02/18] update readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 17001c0..a6888e9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# ⏰ Expiration check +# 📅 Expiration check **Expiration-check** is a command-line tool designed to verify the expiration dates of domains and TLS certificates. For domain verification, it implements the [RDAP protocol](https://about.rdap.org/) whenever possible. If RDAP is unavailable, it falls back on the WHOIS protocol. -## How to install the project +## 📗 How to install the project Pre-compiled versions are available in the [Releases](https://gitnet.fr/deblan/expiration-check/releases). Multiple operating systems are supported, including Linux, Windows, and macOS. For Debian users, a package is also provided. @@ -15,7 +15,7 @@ $ git clone https://gitnet.fr/deblan/expiration-check $ make ``` -## How to use the Project +## 🧪 How to use the Project ### Commands From 867e8952f016ecd6ddbaf58e38e4550dc2d09512 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 24 Jul 2024 16:23:14 +0200 Subject: [PATCH 03/18] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a6888e9..a94e3aa 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 📅 Expiration check +# 🗓️ Expiration check **Expiration-check** is a command-line tool designed to verify the expiration dates of domains and TLS certificates. @@ -15,7 +15,7 @@ $ git clone https://gitnet.fr/deblan/expiration-check $ make ``` -## 🧪 How to use the Project +## 🧪 How to use the Project ### Commands From cd74520662de7aed336878667680fab6a68a1004 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 24 Jul 2024 17:21:29 +0200 Subject: [PATCH 04/18] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a94e3aa..691d2d9 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ For domain verification, it implements the [RDAP protocol](https://about.rdap.or Pre-compiled versions are available in the [Releases](https://gitnet.fr/deblan/expiration-check/releases). Multiple operating systems are supported, including Linux, Windows, and macOS. For Debian users, a package is also provided. -If you want to compile the project from source, you will need at least the GO compiler version 1.22. Clone the project and run the make command. The compiled output will be located in the build directory. +If you want to compile the project from source, you will need at least the GO compiler version 1.22. Clone the project and run the make command. The compiled output will be located in the `build` directory. ``` $ git clone https://gitnet.fr/deblan/expiration-check $ make ``` -## 🧪 How to use the Project +## 🧪 How to use the project ### Commands From cdb98adcd379a20a30b78df3d6181fde6c9c02a5 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 29 Jul 2024 12:11:02 +0200 Subject: [PATCH 05/18] add manpage builder --- .woodpecker/build.yml | 9 +++++++++ Makefile | 10 +++++++++- bin/create-manpage.sh | 14 ++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100755 bin/create-manpage.sh diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml index 324bcc5..ea1fe02 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/build.yml @@ -22,6 +22,15 @@ steps: commands: - make + "Test packaging": + image: deblan/fpm-packager + commands: + - VERSION=test + - ./bin/build-debs.sh "$VERSION" + - ./bin/rename-builds.sh "$VERSION" + when: + event: [push, pull_request] + "Create packages": image: deblan/fpm-packager commands: diff --git a/Makefile b/Makefile index 9d7e66d..b895abb 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ BIN_DARWIN_AMD64 = $(DIR)/$(EXECUTABLE)-darwin-$(GO_ARCH_AMD) BIN_DARWIN_ARM64 = $(DIR)/$(EXECUTABLE)-darwin-$(GO_ARCH_ARM) BIN_LINUX_AMD64 = $(DIR)/$(EXECUTABLE)-linux-$(GO_ARCH_AMD) BIN_LINUX_ARM64 = $(DIR)/$(EXECUTABLE)-linux-$(GO_ARCH_ARM) +MAN_OUTPUT = $(DIR)/expiration-check.1 CC = go build CFLAGS = -trimpath @@ -24,7 +25,7 @@ GCFLAGS = all= ASMFLAGS = all= .PHONY: all -all: linux windows darwin +all: linux windows darwin man .PHONY: linux linux: $(BIN_LINUX_AMD64) $(BIN_LINUX_ARM64) @@ -39,6 +40,9 @@ darwin: $(BIN_DARWIN_AMD64) $(BIN_DARWIN_ARM64) .PHONY: windows windows: $(BIN_WIN_AMD64) $(BIN_WIN_ARM64) +.PHONY: man +man: $(BIN_LINUX_AMD64) $(MAN_OUTPUT) + .PHONY: $(BIN_LINUX_AMD64) $(BIN_LINUX_AMD64): GO111MODULE=$(GOMOD) \ @@ -93,6 +97,10 @@ $(BIN_DARWIN_ARM64): $(CC) $(CFLAGS) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" \ -o $(BIN_DARWIN_ARM64) . +.PHONY: $(MAN_OUTPUT) +$(MAN_OUTPUT): + ./bin/create-manpage.sh + .PHONY: clean clean: rm -rf $(DIR)/* diff --git a/bin/create-manpage.sh b/bin/create-manpage.sh new file mode 100755 index 0000000..5ed1756 --- /dev/null +++ b/bin/create-manpage.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +( + printf ".TH expiration-check 1 \"%s\" \"Manual of expiration-check\"\n" "$(date +'%Y-%m-%d')" + printf ".LO 1\n" + + ./build/expiration-check-linux-amd64 -h \ + | sed -E 's/^([A-Z ]+):$/.SH \1/g' \ + | sed -E 's/^ (.+) (.+)/.TP\n.B \1\n\2\n\n/g' \ + | sed -E 's/^ (\w+)/\1/g' \ + | sed -E 's/\\{0}-([a-z]+)/\\-\1/g' \ + | sed -E 's/-\\-/\\-\\-/g' \ + | sed -E 's/\s+$//g' +) | gzip -9 > ./build/expiration-check.1.gz From 4ca1cd44c7e5d3f299311284015205bd7d704ca5 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 29 Jul 2024 12:15:55 +0200 Subject: [PATCH 06/18] replace indentation --- bin/build-debs.sh | 7 ++++--- bin/rename-builds.sh | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/bin/build-debs.sh b/bin/build-debs.sh index ed69cbb..4452dad 100755 --- a/bin/build-debs.sh +++ b/bin/build-debs.sh @@ -3,7 +3,8 @@ VERSION="$1" for ARCH in amd64 arm64; do - fpm -t deb -p "build/expiration-check-$VERSION-$ARCH.deb" \ - --architecture $ARCH --version "$VERSION" \ - "build/expiration-check-linux-$ARCH"=/usr/bin/expiration-check + fpm -t deb -p "build/expiration-check-$VERSION-$ARCH.deb" \ + --architecture $ARCH --version "$VERSION" \ + "build/expiration-check-linux-$ARCH=/usr/bin/expiration-check" \ + "build/expiration-check.1.gz=/usr/share/man/man1/expiration-check.1.gz" done diff --git a/bin/rename-builds.sh b/bin/rename-builds.sh index ef1276c..9153853 100755 --- a/bin/rename-builds.sh +++ b/bin/rename-builds.sh @@ -3,15 +3,15 @@ VERSION="$1" for OS in linux windows darwin; do - if [ "$OS" = "windows" ]; then - EXTENSION=".exe" - else - EXTENSION= - fi + if [ "$OS" = "windows" ]; then + EXTENSION=".exe" + else + EXTENSION= + fi - for ARCH in amd64 arm64; do - mv -v \ - "build/expiration-check-${OS}-${ARCH}${EXTENSION}" \ - "build/expiration-check-${VERSION}-${OS}-${ARCH}${EXTENSION}" - done + for ARCH in amd64 arm64; do + mv -v \ + "build/expiration-check-${OS}-${ARCH}${EXTENSION}" \ + "build/expiration-check-${VERSION}-${OS}-${ARCH}${EXTENSION}" + done done From bef89d5475ae14b07f6f2a349bf86ecc91a565f6 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 29 Jul 2024 12:16:03 +0200 Subject: [PATCH 07/18] add shell check --- .woodpecker/build.yml | 3 +++ .woodpecker/test.yml | 15 +++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 .woodpecker/test.yml diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml index ea1fe02..2a09ff7 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/build.yml @@ -11,6 +11,9 @@ when: variables: - &golang_image 'golang:1.22' +depends_on: + - test + steps: "Add vendor": image: *golang_image diff --git a/.woodpecker/test.yml b/.woodpecker/test.yml new file mode 100644 index 0000000..09fb0ed --- /dev/null +++ b/.woodpecker/test.yml @@ -0,0 +1,15 @@ +when: + - event: [pull_request, tag] + - event: push + branch: + - ${CI_REPO_DEFAULT_BRANCH} + - develop + - feature/* + - release/* + - renovate/* + +steps: + "Check sehll scripts": + image: pipelinecomponents/shellcheck + commands: + - shellcheck ./bin/*.sh From e1305e18ddcd691e3675e4be14ce38efe21b607b Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 29 Jul 2024 12:19:44 +0200 Subject: [PATCH 08/18] add set -e option in shell scripts --- bin/build-debs.sh | 2 ++ bin/create-manpage.sh | 2 ++ bin/rename-builds.sh | 2 ++ 3 files changed, 6 insertions(+) diff --git a/bin/build-debs.sh b/bin/build-debs.sh index 4452dad..2e4fb3f 100755 --- a/bin/build-debs.sh +++ b/bin/build-debs.sh @@ -1,5 +1,7 @@ #!/bin/sh +set -e + VERSION="$1" for ARCH in amd64 arm64; do diff --git a/bin/create-manpage.sh b/bin/create-manpage.sh index 5ed1756..78608de 100755 --- a/bin/create-manpage.sh +++ b/bin/create-manpage.sh @@ -1,5 +1,7 @@ #!/bin/sh +set -e + ( printf ".TH expiration-check 1 \"%s\" \"Manual of expiration-check\"\n" "$(date +'%Y-%m-%d')" printf ".LO 1\n" diff --git a/bin/rename-builds.sh b/bin/rename-builds.sh index 9153853..75fbada 100755 --- a/bin/rename-builds.sh +++ b/bin/rename-builds.sh @@ -1,5 +1,7 @@ #!/bin/sh +set -e + VERSION="$1" for OS in linux windows darwin; do From 6fdf756d337dedfbbe66d2d5b2667d564edebb71 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 29 Jul 2024 12:22:40 +0200 Subject: [PATCH 09/18] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc71a94..0df0213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ [Unreleased] +## v1.3.0 +### Added +- add manpage for Debian + ## v1.2.1 ### Fixed - fix helper descriptions From 827162dde9c553f9690005d86b64d212af345c36 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 24 Feb 2025 15:07:28 +0100 Subject: [PATCH 10/18] add logger --- logger/logger.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 logger/logger.go diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..1461150 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,27 @@ +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...) + } +} From 892cd62f3e597b73b2ea2bf2db0efca81fb9264a Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 24 Feb 2025 15:07:37 +0100 Subject: [PATCH 11/18] add logger --- app.go | 10 ++++++++++ checker/certificates.go | 10 +++++++++- checker/domains.go | 25 ++++++++++++++++++++++--- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/app.go b/app.go index a39982f..a45ac26 100644 --- a/app.go +++ b/app.go @@ -3,6 +3,7 @@ package main import ( "github.com/urfave/cli/v2" "gitnet.fr/deblan/expiration-check/checker" + "gitnet.fr/deblan/expiration-check/logger" "gitnet.fr/deblan/expiration-check/render" ) @@ -33,6 +34,11 @@ 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{ @@ -45,6 +51,8 @@ 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, @@ -60,6 +68,8 @@ 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 index d0721c6..1ee3caf 100644 --- a/checker/certificates.go +++ b/checker/certificates.go @@ -6,6 +6,8 @@ import ( "math" "strings" "time" + + "gitnet.fr/deblan/expiration-check/logger" ) func FormatDomain(domain string) string { @@ -26,13 +28,19 @@ func CheckCertificate(domain string) Domain { date := conn.ConnectionState().PeerCertificates[0].NotAfter daysLeft := date.Sub(now).Hours() / 24 - return Domain{ + 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} } diff --git a/checker/domains.go b/checker/domains.go index 119d229..1d975e2 100644 --- a/checker/domains.go +++ b/checker/domains.go @@ -10,6 +10,7 @@ import ( "time" "github.com/likexian/whois" + "gitnet.fr/deblan/expiration-check/logger" ) type RdapResponseData struct { @@ -64,15 +65,23 @@ func RdapCheck(domain, service string) Domain { date, _ := time.Parse(time.RFC3339, event.EventDate) daysLeft := date.Sub(now).Hours() / 24 - return Domain{ + 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 } } - return Domain{Name: domain, Failed: true} + d := Domain{Name: domain, Failed: true} + + logger.Get().Logf(`RdapCheck: domain="%s" value="%+v"`, domain, d) + + return d } func WhoisCheck(domain string) Domain { @@ -108,16 +117,22 @@ func WhoisCheck(domain string) Domain { if err == nil { daysLeft := date.Sub(now).Hours() / 24 - return Domain{ + 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 } @@ -130,8 +145,12 @@ func CheckDomains(domains []string) []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)) } } From e1f6b29a033594e20df490e63ed81f9137eda0bd Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 24 Feb 2025 15:08:33 +0100 Subject: [PATCH 12/18] fix: render days and date when the value is later than danger value --- render/render.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/render/render.go b/render/render.go index d3cd037..eed71eb 100644 --- a/render/render.go +++ b/render/render.go @@ -65,8 +65,8 @@ func Render(values []checker.Domain, warning, danger float64, format string) { var date string if value.DaysLeft <= danger { - days = failed - date = failed + 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) From 56a09ca38f969f64e88559a680fc824c972e1159 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 24 Feb 2025 15:09:22 +0100 Subject: [PATCH 13/18] update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0df0213..3211966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ [Unreleased] +## 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 From b27f7773ca0b2d9b80d796059ecabb8607d402ab Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 24 Feb 2025 15:11:04 +0100 Subject: [PATCH 14/18] fix typo in CI --- .woodpecker/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker/test.yml b/.woodpecker/test.yml index 09fb0ed..81b482f 100644 --- a/.woodpecker/test.yml +++ b/.woodpecker/test.yml @@ -9,7 +9,7 @@ when: - renovate/* steps: - "Check sehll scripts": + "Check shell scripts": image: pipelinecomponents/shellcheck commands: - shellcheck ./bin/*.sh From 0281eea177a8bf11607d2077f0741df7c0d7d249 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 3 Oct 2025 12:47:15 +0200 Subject: [PATCH 15/18] 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 16/18] 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 17/18] 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 18/18] 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`