Compare commits

...

19 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
b27f7773ca fix typo in CI
All checks were successful
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/tag/test Pipeline was successful
ci/woodpecker/tag/build Pipeline was successful
2025-02-24 15:11:04 +01:00
56a09ca38f update changelog
Some checks are pending
ci/woodpecker/push/build Pipeline is pending approval
ci/woodpecker/push/test Pipeline is pending approval
2025-02-24 15:09:22 +01:00
e1f6b29a03 fix: render days and date when the value is later than danger value 2025-02-24 15:08:33 +01:00
892cd62f3e add logger 2025-02-24 15:07:37 +01:00
827162dde9 add logger 2025-02-24 15:07:28 +01:00
6fdf756d33
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
2024-07-29 12:22:40 +02:00
e1305e18dd
add set -e option in shell scripts
All checks were successful
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2024-07-29 12:19:44 +02:00
bef89d5475
add shell check
All checks were successful
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2024-07-29 12:16:03 +02:00
4ca1cd44c7
replace indentation 2024-07-29 12:15:55 +02:00
cdb98adcd3 add manpage builder 2024-07-29 12:11:06 +02:00
cd74520662
update readme
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2024-07-24 17:21:29 +02:00
2ea71f7a86 Merge branch 'feature/changelog' into develop
Some checks are pending
ci/woodpecker/push/build Pipeline is pending approval
2024-07-24 16:23:44 +02:00
867e8952f0
update readme
Some checks are pending
ci/woodpecker/push/build Pipeline is pending approval
2024-07-24 16:23:14 +02:00
1eabdf5aa1
update readme
Some checks are pending
ci/woodpecker/push/build Pipeline is pending approval
2024-07-24 16:21:44 +02:00
a021d3e8ff
update readme
Some checks are pending
ci/woodpecker/push/build Pipeline is pending approval
2024-07-24 16:18:05 +02:00
23 changed files with 475 additions and 271 deletions

View file

@ -9,7 +9,10 @@ when:
- renovate/* - renovate/*
variables: variables:
- &golang_image 'golang:1.22' - &golang_image 'golang:1.24'
depends_on:
- test
steps: steps:
"Add vendor": "Add vendor":
@ -22,6 +25,15 @@ steps:
commands: commands:
- make - 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": "Create packages":
image: deblan/fpm-packager image: deblan/fpm-packager
commands: commands:

15
.woodpecker/test.yml Normal file
View file

@ -0,0 +1,15 @@
when:
- event: [pull_request, tag]
- event: push
branch:
- ${CI_REPO_DEFAULT_BRANCH}
- develop
- feature/*
- release/*
- renovate/*
steps:
"Check shell scripts":
image: pipelinecomponents/shellcheck
commands:
- shellcheck ./bin/*.sh

View file

@ -1,5 +1,19 @@
[Unreleased] [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
## v1.2.1 ## v1.2.1
### Fixed ### Fixed
- fix helper descriptions - fix helper descriptions

View file

@ -16,6 +16,7 @@ BIN_DARWIN_AMD64 = $(DIR)/$(EXECUTABLE)-darwin-$(GO_ARCH_AMD)
BIN_DARWIN_ARM64 = $(DIR)/$(EXECUTABLE)-darwin-$(GO_ARCH_ARM) BIN_DARWIN_ARM64 = $(DIR)/$(EXECUTABLE)-darwin-$(GO_ARCH_ARM)
BIN_LINUX_AMD64 = $(DIR)/$(EXECUTABLE)-linux-$(GO_ARCH_AMD) BIN_LINUX_AMD64 = $(DIR)/$(EXECUTABLE)-linux-$(GO_ARCH_AMD)
BIN_LINUX_ARM64 = $(DIR)/$(EXECUTABLE)-linux-$(GO_ARCH_ARM) BIN_LINUX_ARM64 = $(DIR)/$(EXECUTABLE)-linux-$(GO_ARCH_ARM)
MAN_OUTPUT = $(DIR)/expiration-check.1
CC = go build CC = go build
CFLAGS = -trimpath CFLAGS = -trimpath
@ -24,7 +25,7 @@ GCFLAGS = all=
ASMFLAGS = all= ASMFLAGS = all=
.PHONY: all .PHONY: all
all: linux windows darwin all: linux windows darwin man
.PHONY: linux .PHONY: linux
linux: $(BIN_LINUX_AMD64) $(BIN_LINUX_ARM64) linux: $(BIN_LINUX_AMD64) $(BIN_LINUX_ARM64)
@ -39,6 +40,9 @@ darwin: $(BIN_DARWIN_AMD64) $(BIN_DARWIN_ARM64)
.PHONY: windows .PHONY: windows
windows: $(BIN_WIN_AMD64) $(BIN_WIN_ARM64) windows: $(BIN_WIN_AMD64) $(BIN_WIN_ARM64)
.PHONY: man
man: $(BIN_LINUX_AMD64) $(MAN_OUTPUT)
.PHONY: $(BIN_LINUX_AMD64) .PHONY: $(BIN_LINUX_AMD64)
$(BIN_LINUX_AMD64): $(BIN_LINUX_AMD64):
GO111MODULE=$(GOMOD) \ GO111MODULE=$(GOMOD) \
@ -46,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):
@ -55,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):
@ -64,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):
@ -73,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):
@ -82,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):
@ -91,7 +95,11 @@ $(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)
$(MAN_OUTPUT):
./bin/create-manpage.sh
.PHONY: clean .PHONY: clean
clean: clean:

View file

@ -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 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 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 |
+-------------------+------+---------------------+
$ expiration-check certificates -d example.com -d other-example.com -d mail.example.com:993 ```
+-------------------+------+---------------------+ $ git clone https://gitnet.fr/deblan/expiration-check
| DOMAIN | DAYS | DATE | $ make
+-------------------+------+---------------------+
| 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 |
+-------------------+------+---------------------+
``` ```
You can specify an ouput format using `--format` or `-f`: ## 🧪 How to use the project
- `table` (default) ### Commands
- `json`
- `csv` - `certificates`, `certificate`, `cert`, `certs`, `c`: Checks the expiration dates of TLS certificates.
- `tsv` - `domains`, `domain`, `d`: Checks the expiration dates of domain names.
- `html` - `help`, `h`: Displays a list of all commands or detailed help for a specific command.
- `markdown`
### 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
```

View file

@ -1,9 +1,12 @@
#!/bin/sh #!/bin/sh
set -e
VERSION="$1" VERSION="$1"
for ARCH in amd64 arm64; do for ARCH in amd64 arm64; do
fpm -t deb -p "build/expiration-check-$VERSION-$ARCH.deb" \ fpm -t deb -p "build/expiration-check-$VERSION-$ARCH.deb" \
--architecture $ARCH --version "$VERSION" \ --architecture $ARCH --version "$VERSION" \
"build/expiration-check-linux-$ARCH"=/usr/bin/expiration-check "build/expiration-check-linux-$ARCH=/usr/bin/expiration-check" \
"build/expiration-check.1.gz=/usr/share/man/man1/expiration-check.1.gz"
done done

16
bin/create-manpage.sh Executable file
View file

@ -0,0 +1,16 @@
#!/bin/sh
set -e
(
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

View file

@ -1,17 +1,19 @@
#!/bin/sh #!/bin/sh
set -e
VERSION="$1" VERSION="$1"
for OS in linux windows darwin; do for OS in linux windows darwin; do
if [ "$OS" = "windows" ]; then if [ "$OS" = "windows" ]; then
EXTENSION=".exe" EXTENSION=".exe"
else else
EXTENSION= EXTENSION=
fi fi
for ARCH in amd64 arm64; do for ARCH in amd64 arm64; do
mv -v \ mv -v \
"build/expiration-check-${OS}-${ARCH}${EXTENSION}" \ "build/expiration-check-${OS}-${ARCH}${EXTENSION}" \
"build/expiration-check-${VERSION}-${OS}-${ARCH}${EXTENSION}" "build/expiration-check-${VERSION}-${OS}-${ARCH}${EXTENSION}"
done done
done done

View file

@ -1,47 +0,0 @@
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
}

View file

@ -1,140 +0,0 @@
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
}

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,9 +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/render" "gitnet.fr/deblan/expiration-check/pkg/logger"
"gitnet.fr/deblan/expiration-check/pkg/render"
) )
func NormalizeFormat(format string) string { func NormalizeFormat(format string) string {
@ -33,6 +34,11 @@ func App() *cli.App {
Value: "table", Value: "table",
Usage: "output format: table, csv, tsv, html, json, markdown", Usage: "output format: table, csv, tsv, html, json, markdown",
}, },
&cli.BoolFlag{
Name: "verbose",
Aliases: []string{"v"},
Required: false,
},
} }
return &cli.App{ return &cli.App{
@ -45,6 +51,8 @@ func App() *cli.App {
Usage: "Checks certificate", Usage: "Checks certificate",
Flags: flags, Flags: flags,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
logger.Get().SetVerbose(c.Bool("verbose"))
render.Render( render.Render(
checker.CheckCertificates(c.StringSlice("domain")), checker.CheckCertificates(c.StringSlice("domain")),
30, 14, 30, 14,
@ -60,6 +68,8 @@ func App() *cli.App {
Aliases: []string{"d", "domains"}, Aliases: []string{"d", "domains"},
Flags: flags, Flags: flags,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
logger.Get().SetVerbose(c.Bool("verbose"))
render.Render( render.Render(
checker.CheckDomains(c.StringSlice("domain")), checker.CheckDomains(c.StringSlice("domain")),
30, 14, 30, 14,

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
}

27
pkg/logger/logger.go Normal file
View file

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

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 = failed days = RenderColor(fmt.Sprintf("%.0f", *value.DaysLeft), text.Colors{0, text.FgRed}, format)
date = failed 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")
}