First commit

This commit is contained in:
2025-12-21 18:21:32 +01:00
commit 2c5d0378b6
7 changed files with 874 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
# ---> Rust
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# These are backup files generated by rustfmt
**/*.rs.bk
# deploy script
deploy.sh

546
Cargo.lock generated Normal file
View File

@@ -0,0 +1,546 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bitflags"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "bumpalo"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "cc"
version = "1.2.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "certcheckntfy"
version = "0.1.0"
dependencies = [
"chrono",
"minreq",
"openssl",
"rustix",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "iana-time-zone"
version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "js-sys"
version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "minreq"
version = "2.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05015102dad0f7d61691ca347e9d9d9006685a64aefb3d79eecf62665de2153d"
dependencies = [
"rustls",
"rustls-webpki",
"webpki-roots",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "openssl"
version = "0.10.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "openssl-sys"
version = "0.9.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom",
"libc",
"untrusted",
"windows-sys",
]
[[package]]
name = "rustix"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "rustls"
version = "0.21.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [
"log",
"ring",
"rustls-webpki",
"sct",
]
[[package]]
name = "rustls-webpki"
version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "sct"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasm-bindgen"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [
"unicode-ident",
]
[[package]]
name = "webpki-roots"
version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

10
Cargo.toml Normal file
View File

@@ -0,0 +1,10 @@
[package]
name = "certcheckntfy"
version = "0.1.0"
edition = "2024"
[dependencies]
chrono = { version = "0.4.42", features = ["clock", "now", "std"] }
openssl = { version = "0.10" }
minreq = { version = "2.13.4", features = ["https"] }
rustix = { version = "1.1.2", features = ["system"] }

63
README.md Normal file
View File

@@ -0,0 +1,63 @@
## About
certcheckntfy is my rudimentary Rust-way of getting informed by a push message when my LetsEncrypt certificates are about to expire. Of course I could have achieved the same thing with any other (scripting) language but I want to improve my Rust knowledge and getting more familiar with it. I like Rust ;) I have already replaced my greylister (written in C++) and also my vmail backend (written in Javascript/NodeJS) with solutions written in Rust and replacing NodeJS freed up around 75MB RAM and on a System with only 2GB that's not nothing.
I could simply run `certbot renew` with additional hooks but I have to update the TLSA record at my DNS provider manually as they are not providing any API. That's why I decided against it and went for the _notification way_ only.
## Prerequisits
- The Rust compiler installed on your system to build the software
- A working NTFY server, see [NTFY Homepage](https://ntfy.sh)
- You will need the ntfy app on your Android based mobile device. I do not know
if something like ntfy is available on iOS.
- Of course you need to subscribe to the topic you choose, see below.
- A Linux based server including systemd but since Rust is available on BSD it should be able get it up and running there too. Windows is not supported, there is Powershell ;)
## Install
- checkout and build with `cargo build --release`
- Copy the executable from `target/release/certcheckntfy` to your server(s)
- Copy the .service und the .timer file to `/etc/systemd/system` (on your server(s) of course)
- Adjust the .service and .timer file to your liking
- Enable the timer: `systemctl enable --now cert_check.timer`
- Check timer: `systemctl --list-timers`
On BSD systems where systemd is not available just skip the .timer and .service bs and use `cron` or whatever is used on your system.
## Configuration
The systemd service file shows the environment variables you have to provide where `NOTIFY_URL` **has** to be set, the other ones should work out of the box but maybe you have to adopt it if your aren't using Certbot or the date format of your certificates has another format (which I doubt).
- CERT_DATE_FORMAT="%%b%%e %%H:%%M:%%S %%Y GMT" (can be checked like so: `openssl x509 -enddate -noout -in <path to cert file>`)
- CERT_BASE_PATH=/etc/letsencrypt/live
- CERT_FILE_NAME=cert.pem
- THRESHOLD_DAYS=x (where x is a number greater then zero)
- NOTIFY_URL=$NTFY_URL/TOPIC
Double `%` is necessary in CERT_DATE_FORMAT because systemd uses `%` by itself so it needs to be escaped.
If you made changes to the .service or .timer file after activating the timer remember to `systemctl daemon-reload`
Once LetsEncrypt switches to 45 days lifetime in February 2028 as described [here](https://letsencrypt.org/2025/12/02/from-90-to-45) THRESHOLD_DAYS should be decreased to around 10.
## Test
To test your settings just execute `systemctl start cert_check.service` Check your syslog with `journalctl -xf` while doing so. If none of your certs are due for renewal the program will log something like
```
Certificate OK for CN=$HOSTNAME$. Expires after $TIMESTAMP$ which is in $x day(s)
```
On the other hand, if one of your certificates expires in less then $THRESHOLD_DAYS it will log a `Certificate Warning` instead and it will also log the push notification like so:
```
Certificate Warning for CN=$HOSTNAME$. Expires after $TIMESTAMP$ which is in $x day(s)
Push notification sent to $NOTIFY_URL$
```
And if all went well you should receive a nice push message on your Android device in almost no time. Done ;)
## Caveat
- I have not tested certcheckntfy on certificates with SubjectAltNames. i.e. certs with multiple CN's. It should work but I do not know...
- The ntfy service should not need a username/password, although this can easily be implemented

11
cert_check.service Normal file
View File

@@ -0,0 +1,11 @@
[Unit]
Description=Check SSL Certificates
[Service]
Type=oneshot
Environment=CERT_DATE_FORMAT="%%b%%e %%H:%%M:%%S %%Y GMT"
Environment=CERT_BASE_PATH=/etc/letsencrypt/live
Environment=CERT_FILE_NAME=cert.pem
Environment=THRESHOLD_DAYS=30
Environment=NOTIFY_URL=$NTFY_URL/TOPIC
ExecStart=/usr/local/bin/certcheckntfy

9
cert_check.timer Normal file
View File

@@ -0,0 +1,9 @@
[Unit]
Description=Run check ssl certificates
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target

224
src/main.rs Normal file
View File

@@ -0,0 +1,224 @@
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, ParseResult, Utc};
use openssl::x509::X509;
use std::{env, error::Error, fs};
#[derive(Clone)]
struct CertParseResult {
pub remaining_days: i64,
pub elapse_date: Option<NaiveDateTime>,
pub common_name: Option<Vec<String>>,
pub is_ok: bool,
pub reason: Option<String>,
}
impl CertParseResult {
pub fn invalid(reason: String) -> CertParseResult {
CertParseResult {
elapse_date: None,
remaining_days: 0,
common_name: None,
is_ok: false,
reason: Some(reason),
}
}
}
fn parse_expiration_date(date_string: String, format_string: &str) -> ParseResult<NaiveDateTime> {
let splitted: Vec<&str> = date_string.split_whitespace().collect();
let joined = splitted.join(" ");
NaiveDateTime::parse_from_str(joined.as_str(), format_string)
}
fn calculate_remaining_days_from_now(param: NaiveDateTime) -> i64 {
let dt = Local::now();
let offset = dt.offset().clone();
let naive_utc_and_offset: DateTime<FixedOffset> =
DateTime::from_naive_utc_and_offset(param, offset);
let delta = Utc::now().signed_duration_since(naive_utc_and_offset);
delta.num_days().abs()
}
fn parse_certificate(cert_file: String, date_format: String) -> CertParseResult {
let file_content = match std::fs::read_to_string(cert_file.clone()) {
Ok(x) => x,
Err(e) => {
return CertParseResult::invalid(format!(
"Unable to read cert file {}. Reason: {}",
cert_file.clone(),
e
));
}
};
let x509 = match X509::from_pem(file_content.as_bytes()) {
Ok(x) => x,
Err(e) => {
return CertParseResult::invalid(format!(
"Unable to extract PEM from file {}. Reason: {}",
cert_file.clone(),
e
));
}
};
let mut cnames = Vec::new();
for tmp in x509.subject_name().entries() {
let cname = match tmp.data().as_utf8() {
Ok(a) => a,
Err(_) => continue,
};
cnames.push(cname.to_string());
}
let dt = match parse_expiration_date(x509.not_after().to_string(), date_format.as_str()) {
Ok(x) => x,
Err(e) => {
return CertParseResult::invalid(format!(
"Unable to parse date from file {}. Reason: {}",
cert_file.clone(),
e
));
}
};
let days_remaining = calculate_remaining_days_from_now(dt);
CertParseResult {
common_name: Some(cnames),
elapse_date: Some(dt),
remaining_days: days_remaining,
is_ok: true,
reason: None,
}
}
fn notify(msg: String, url: String) {
match minreq::post(url.clone())
.with_header("Title", "SSL Certificate Expiration")
.with_header("Priority", "high")
.with_header("Tags", "warning,rotating_light")
.with_body(msg)
.send()
{
Ok(_) => {
println!("Push notification sent to {}", url);
}
Err(e) => {
eprintln!("Error posting notification to {}: {}", url, e);
}
}
}
fn get_hostname() -> String {
if let Ok(s) = rustix::system::uname().nodename().to_str() {
String::from(s)
} else {
String::from("Unknown host")
}
}
fn mkerrorbox<T: std::fmt::Display>(msg: &str, e: T) -> Box<dyn Error> {
Box::<dyn Error>::from(format!("{}: {}", msg, e))
}
fn main() -> Result<(), Box<dyn Error>> {
let url = match env::var("NOTIFY_URL") {
Ok(u) => u,
Err(e) => {
return Err(mkerrorbox("NOTIFY_URL not set", e));
}
};
let base_path = match env::var("CERT_BASE_PATH") {
Ok(bp) => bp,
Err(e) => {
return Err(mkerrorbox("CERT_BASE_PATH not set", e));
}
};
let cert_file_name = match env::var("CERT_FILE_NAME") {
Ok(cfn) => cfn,
Err(e) => {
return Err(mkerrorbox("CERT_FILE_NAME not set", e));
}
};
let cert_date_format = match env::var("CERT_DATE_FORMAT") {
Ok(cdf) => cdf,
Err(e) => {
return Err(mkerrorbox("CERT_DATE_FORMAT not set", e));
}
};
let tmp_threshold = match env::var("THRESHOLD_DAYS") {
Ok(s) => s,
Err(e) => {
return Err(mkerrorbox("THRESHOLD_DAYS not set", e));
}
};
let threshold_days = match tmp_threshold.parse::<i64>() {
Ok(i) => i,
Err(e) => {
return Err(mkerrorbox("Unparseable threshold value", e));
}
};
if threshold_days < 1 {
return Err(mkerrorbox(
"Value of threshold_days less then 1",
threshold_days,
));
}
for entry in fs::read_dir(base_path)? {
let path = entry?.path();
if path.is_dir() {
let cert_file = format!("{}/{}", path.display().to_string(), cert_file_name);
let parse_result = parse_certificate(cert_file, cert_date_format.clone());
if parse_result.is_ok {
if parse_result.remaining_days < threshold_days {
println!(
"Certificate Warning for CN={}. Expires after {} which is in {} day(s)",
parse_result.common_name.clone().unwrap().join(","),
parse_result.elapse_date.unwrap(),
parse_result.remaining_days
);
let msg = format!(
"Host: {}\nCN: {}\nExpires at: {}\nRemaining days: {}",
get_hostname(),
parse_result.common_name.unwrap().join(","),
parse_result.elapse_date.unwrap(),
parse_result.remaining_days
);
notify(msg, url.clone());
} else {
println!(
"Certificate OK for CN={}. Expires after {} which is in {} day(s)",
parse_result.common_name.unwrap().join(","),
parse_result.elapse_date.unwrap(),
parse_result.remaining_days
);
}
} else {
if let Some(s) = parse_result.reason {
let msg = format!(
"Invalid cert parse result on host: {}. Reason: {}",
get_hostname(),
s
);
notify(msg, url.clone());
return Err(mkerrorbox("Invalid cert parse result", s));
} else {
let msg = format!(
"Invalid cert parse result on host: {}. Reason: unknown",
get_hostname()
);
notify(msg, url.clone());
return Err(mkerrorbox("Invalid cert parse result", "Unknown reason"));
}
}
}
}
Ok(())
}