Compare commits
19 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
cdfa3c8e09 |
|||
|
5e37669ba6 |
|||
|
f9c209e576 |
|||
|
0281eea177 |
|||
| b27f7773ca | |||
| 56a09ca38f | |||
| e1f6b29a03 | |||
| 892cd62f3e | |||
| 827162dde9 | |||
|
6fdf756d33 |
|||
|
e1305e18dd |
|||
|
bef89d5475 |
|||
|
4ca1cd44c7 |
|||
| cdb98adcd3 | |||
|
cd74520662 |
|||
| 2ea71f7a86 | |||
|
867e8952f0 |
|||
|
1eabdf5aa1 |
|||
|
a021d3e8ff |
23 changed files with 475 additions and 271 deletions
|
|
@ -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
15
.woodpecker/test.yml
Normal 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
|
||||||
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -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
|
||||||
|
|
|
||||||
22
Makefile
22
Makefile
|
|
@ -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:
|
||||||
|
|
|
||||||
82
README.md
82
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
|
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
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -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
16
bin/create-manpage.sh
Executable 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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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
14
cmd/main.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
12
main.go
|
|
@ -1,12 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if err := App().Run(os.Args); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
38
pkg/checker/certificate.go
Normal file
38
pkg/checker/certificate.go
Normal 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
53
pkg/checker/domain.go
Normal 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
13
pkg/extractor/tld.go
Normal 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:], ".")
|
||||||
|
}
|
||||||
16
pkg/formatter/http_domain.go
Normal file
16
pkg/formatter/http_domain.go
Normal 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
27
pkg/logger/logger.go
Normal 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
24
pkg/model/result.go
Normal 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
75
pkg/rdap/rdap.go
Normal 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")
|
||||||
|
}
|
||||||
|
|
@ -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
47
pkg/whois/whois.go
Normal 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")
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue