From f1d2906d4e9f4a0da57a2fc6175e5343893db788 Mon Sep 17 00:00:00 2001 From: Angel Stoyanov Date: Thu, 26 Jun 2025 17:54:43 +0100 Subject: [PATCH] v2.0.0 --- check_domain.sh | 1425 +++++++++++++++++++++++++++------------- zbx_domain_expiry.yaml | 174 +++-- 2 files changed, 1067 insertions(+), 532 deletions(-) diff --git a/check_domain.sh b/check_domain.sh index 8406716..a4e5c50 100755 --- a/check_domain.sh +++ b/check_domain.sh @@ -1,505 +1,1008 @@ +# check_domain - v2.0.0 +# Author: A. Stoyanov +# GitHub: https://github.com/a-stoyanov/zabbix-domain-expiry +# License: Apache License 2.0 +# Original source: https://github.com/glensc/monitoring-plugin-check_domain (E. Ruusamäe) +# +# This script checks the expiration status of a domain using RDAP or WHOIS protocols. +# It outputs a JSON-formatted status including the state (OK, WARNING, CRITICAL, UNKNOWN), +# days left until expiration, expiration date, days since expired (if applicable), and a message. +# +# Usage: check_domain.sh -h | -d [-c ] [-w ] [-P ] [-s ] [-r ] [-z] +# Note: domain (-d) is required +# +# Examples: +# ./check_domain.sh -d example.com +# ./check_domain.sh -d example.com -w 15 -c 5 -r https://rdap.example.com -s whois.example.com -z +# +# Options: +# -h, --help Display detailed help information +# -V, --version Display version information in JSON format +# -d, --domain Specify the domain name to check (required) +# -w, --warning Set warning threshold in days (default: 30) +# -c, --critical Set critical threshold in days (default: 7) +# -P, --path Specify path to whois executable +# -s, --whois-server Specify WHOIS server hostname (use "" for default rfc-3912 lookup) +# -r, --rdap-server Specify RDAP server URL (use "" for IANA lookup) +# -z, --debug Enable debug output to stderr +# +# Exit Codes: +# 0: STATE_OK Domain is valid and not near expiration +# 1: STATE_WARNING Domain is nearing expiration (within warning threshold) +# 2: STATE_CRITICAL Domain is expired or very close to expiration (within critical threshold) +# 3: STATE_UNKNOWN Unable to determine domain status (e.g., invalid domain, no WHOIS server) +# +# Dependencies: +# - curl: For RDAP queries +# - mktemp: For creating temporary files +# - date: For date calculations +# - whois: For WHOIS queries +# - grep: For parsing output +# - awk: For parsing WHOIS and RDAP data +# - jq: For parsing RDAP JSON responses +# +# Notes: +# - The script first attempts RDAP lookup and if that fails, it falls back to WHOIS. +# - Specific switches (-r / -s) for RDAP and WHOIS respectively allow queries against target servers (e.g: -r https://valid.rdap.server -s whois.godaddy.com) +# - Debug output is enabled with -z and sent to stderr. +# - Temporary files are cleaned up on exit. + #!/bin/sh -# Licensed under GPL v2 License -# Original Nagios script: https://github.com/glensc/nagios-plugin-check_domain -# Copyright (c) 2005 Tomàs Núñez Lirola (Original Author) -# Copyright (c) 2009-2016 Elan Ruusamäe (Maintainer) -# Zabbix adapted version: https://github.com/a-stoyanov/zabbix-domain-expiry (A.Stoyanov) -# 20/11/2023 - added '-s|--server' switch logic to accept empty quoted value "" -# 20/11/2023 - changed program exit output to align with zabbix template item pre-processing - -# fail on first error, do not continue set -e +exec 2>&1 -PROGRAM=${0##*/} -VERSION=1.8.0 +# Create temp directory for storing output files +tmpdir=$(mktemp -d -t check_domain.XXXXXX 2>/dev/null || echo "${TMPDIR:-/tmp}/check_domain_$$_$RANDOM") +[ -d "$tmpdir" ] || mkdir -p "$tmpdir" || die "$STATE_UNKNOWN" "State: UNKNOWN ; Cannot create temporary directory" +PROGRAM="${0##*/}" +VERSION=2.0.0 STATE_OK=0 STATE_WARNING=1 STATE_CRITICAL=2 STATE_UNKNOWN=3 +default_warning="30" +default_critical="7" +whois_server_override="0" +rdap_server_override="0" +awk="${AWK:-awk}" +outfile="$tmpdir/whois_output.txt" +error_file="$tmpdir/error.txt" +rdap_bootstrap_file="$tmpdir/rdap_bootstrap.json" +debug="false" + +# Terminates the script with a specified exit code and JSON-formatted message +# Parameters: +# $1: Exit code (STATE_OK, STATE_WARNING, STATE_CRITICAL, or STATE_UNKNOWN) +# $2: Message describing the script's status +# Returns: +# Outputs JSON with state, days left, expire date, days since expired, and message +# Cleans up temporary files and exits with the specified code die() { - # shellcheck disable=SC2039 - local rc="$1" - # shellcheck disable=SC2039 - local msg="$2" - echo "$msg" + local rc="$1" msg="$2" + local state + case "$rc" in + 0) state="OK" ;; + 1) state="WARNING" ;; + 2) state="CRITICAL" ;; + 3) state="UNKNOWN" ;; + *) state="UNKNOWN" ;; + esac - if [ "$rc" = 0 ]; then - # remove outfile if not caching - [ -z "$cache_dir" ] && rm -f "$outfile" - else - # always remove output even if caching to force re-check in case on errors - rm -f "$outfile" - fi + days_left="${expdays:-0}" + expire_date="${expdate:-unknown}" + days_since_expired=$([ "$days_left" -lt 0 ] && echo "${days_left#-}" || echo "0") - exit "$rc" + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Entering die() with rc=$rc, msg='$msg'" >&2 + echo "INFO [$(date +'%H:%M:%S')]: Cleaning up temporary directory: $tmpdir and files: $outfile $rdap_bootstrap_file $error_file" >&2 + fi + + json_output=$(printf '{"state":"%s","days_left":%s,"days_since_expired":%s,"expire_date":"%s","message":"%s"}' \ + "$state" "$days_left" "$days_since_expired" "$expire_date" "$msg") + + # Output JSON to stderr + printf '%s\n' "$json_output" >&2 + + # Clean up temporary files + rm -rf "$tmpdir" 2>/dev/null + + exit "$rc" } -# return true if argument is numeric -# http://stackoverflow.com/a/3951175/2314626 +# Checks if a string is a valid numeric value +# Parameters: +# $1: String to check +# Returns: +# 0 if numeric, 1 otherwise is_numeric() { - case "$1" in - ''|*[!0-9]*) - return 1 - ;; - esac - return 0 + echo "$1" | grep -q -E '^[0-9]+$' } +# Validates if a string is a valid URL or "0" +# Parameters: +# $1: String to validate +# Returns: +# 0 if valid URL or "0", 1 otherwise +is_valid_url() { + echo "$1" | grep -q -E '^https?://[a-zA-Z0-9.-]+(/.*)?$|^0$' +} + +# Validates if a string is a valid WHOIS server hostname or "0" +# Parameters: +# $1: String to validate +# Returns: +# 0 if valid hostname or "0", 1 otherwise +is_valid_server() { + echo "$1" | grep -q -E '^[a-zA-Z0-9.-]+$|^0$' +} + +# Outputs the script's version in JSON format +# Parameters: None +# Returns: +# JSON object with the version number version() { - echo "check_domain - v$VERSION" + echo "{\"version\":\"$VERSION\"}" } +# Displays basic usage information +# Parameters: None +# Returns: +# Outputs usage string to stdout usage() { - echo "Usage: $PROGRAM -h | -d [-c ] [-w ] [-P ] [-s ]" + echo "Usage: $PROGRAM -h | -d [-c ] [-w ] [-P ] [-s ] [-r ] [-z]" } +# Displays detailed help information +# Parameters: None +# Returns: +# Outputs detailed help message to stdout fullusage() { - cat < (Original Author) -Copyright (c) 2009-2016 Elan Ruusamäe (Maintainer) -Zabbix adapted version: https://github.com/a-stoyanov/zabbix-domain-expiry (A.Stoyanov) -This script checks the expiration date of a domain name using the whois utility - -Usage: $PROGRAM -h | -d [-c ] [-w ] [-P ] [-s ] -NOTE: -d must be specified +This script checks the expiration status of a domain using RDAP or WHOIS protocols. +Usage: $PROGRAM -h | -d [-c ] [-w ] [-P ] [-s ] [-r ] [-z] Options: --h, --help - Print detailed help --V, --version - Print version information --d, --domain - Domain name to check --w, --warning - Response time to result in warning status (days) --c, --critical - Response time to result in critical status (days) --P, --path - Path to whois binary --s, --server - Specific Whois server for domain name check --a, --cache-age - How many days should each WHOIS lookup be cached for (default 0). Requires cache dir. --C, --cache-dir - Directory where to cache lookups - -This plugin will use whois service to get the expiration date for the domain name. -Example: - $PROGRAM -d domain.tld -w 30 -c 10 - +-h|--help Print detailed help +-V|--version Print version information +-d|--domain Domain name to check +-w|--warning Warning threshold (days) +-c|--critical Critical threshold (days) +-P|--path Path to whois executable +-s|--whois-server Specific WHOIS server (use "" or for default rfc-3912 lookup) +-r|--rdap-server Specific RDAP server URL (use "" for IANA lookup) +-z|--debug Enable debug output EOF } -set_defaults() { - # Default values (days): - critical=7 - warning=30 +# Adjusts RDAP URL for specific TLDs (e.g., .uk, .br, .jp) +# Parameters: +# $1: TLD (e.g., "uk", "br") +# $2: RDAP server URL +# Returns: +# Adjusted RDAP server URL or original if no adjustment needed +adjust_rdap_url() { + local tld="$1" rdap_server="$2" + case "$tld" in + uk) + if ! echo "$rdap_server" | grep -q -E '/uk/$'; then + rdap_server="${rdap_server%/}/uk/" + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Adjusted RDAP server for .uk TLD to: $rdap_server" >&3 + fi + fi + ;; + br) + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: No RDAP URL adjustment needed for .br TLD (preserves /v1/): $rdap_server" >&3 + fi + ;; + jp) + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: No RDAP URL adjustment needed for .jp TLD (preserves /rdap/): $rdap_server" >&3 + fi + ;; + *) + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: No RDAP URL adjustment needed for TLD .$tld" >&3 + fi + ;; + esac + echo "$rdap_server" +} - cache_age=0 - cache_dir= +# Retrieves RDAP server URL for a given TLD from IANA bootstrap file +# Parameters: +# $1: TLD (e.g., "com", "uk") +# Returns: +# RDAP server URL or empty string if not found +# Exit code 0 on success, 1 on failure +get_rdap_server() { + local tld="$1" + local bootstrap_url="https://data.iana.org/rdap/dns.json" - awk=${AWK:-awk} + rdap_bootstrap_file="$tmpdir/rdap_bootstrap.json" + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Using rdap_bootstrap_file: $rdap_bootstrap_file" >&3 + fi + + if [ ! -s "$rdap_bootstrap_file" ]; then + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Fetching IANA RDAP bootstrap from $bootstrap_url" >&3 + fi + set +e + curl -s -f --retry 3 --connect-timeout 3 --max-time 5 "$bootstrap_url" > "$rdap_bootstrap_file" 2>"$error_file" + curl_rc=$? + set -e + if [ $curl_rc -ne 0 ]; then + if [ "$debug" = "true" ]; then + echo "ERROR [$(date +'%H:%M:%S')]: Failed to fetch $bootstrap_url, curl exit code: $curl_rc, error: $(cat "$error_file")" >&3 + fi + return 1 + fi + if ! grep -q '"services"' "$rdap_bootstrap_file"; then + if [ "$debug" = "true" ]; then + echo "ERROR [$(date +'%H:%M:%S')]: Invalid RDAP bootstrap file, missing 'services' key" >&3 + fi + rm -f "$rdap_bootstrap_file" + return 1 + fi + fi + + # Check if jq is available for parsing JSON + local rdap_server + if command -v jq >/dev/null 2>&1; then + set +e + rdap_server=$(jq -r --arg tld "$tld" '.services[] | select(.[0][] == $tld) | .[1][0]' "$rdap_bootstrap_file" 2>"$error_file") + jq_rc=$? + set -e + if [ $jq_rc -ne 0 ] || [ -z "$rdap_server" ] || ! echo "$rdap_server" | grep -q -E '^https://'; then + if [ "$debug" = "true" ]; then + echo "ERROR [$(date +'%H:%M:%S')]: jq failed or invalid/no RDAP server found for TLD .$tld, jq exit code: $jq_rc, output: '$rdap_server', error: $(cat "$error_file")" >&3 + fi + else + # Ensure we only use the first RDAP server if multiple are returned + url_count=$(echo "$rdap_server" | wc -l) + if [ "$url_count" -gt 1 ]; then + if [ "$debug" = "true" ]; then + echo "ERROR [$(date +'%H:%M:%S')]: Multiple RDAP servers found for TLD .$tld: $rdap_server, using first" >&3 + fi + rdap_server=$(echo "$rdap_server" | head -n 1) + fi + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: RDAP server for TLD .$tld: $rdap_server" >&3 + fi + echo "$rdap_server" + return 0 + fi + fi + + echo "$rdap_server" + return 0 +} + +# Queries RDAP server for domain expiration date +# Parameters: +# $1: Domain name +# Returns: +# Expiration date in YYYY-MM-DD format or empty string if RDAP fails +# Exit code 0 on success or fallback to WHOIS +get_rdap_expiration() { + local domain="$1" + local tld="${domain##*.}" + local rdap_server="$rdap_server_override" + local response curl_rc error_output + + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Starting RDAP lookup for $domain" >&3 + fi + + if [ "$rdap_server" = "0" ]; then + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Attempting IANA RDAP lookup for TLD .$tld" >&3 + fi + rdap_server=$(get_rdap_server "$tld") || { + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: No RDAP server found for TLD .$tld, falling back to WHOIS" >&3 + fi + return 0 + } + if [ -z "$rdap_server" ] || ! echo "$rdap_server" | grep -q -E '^https://'; then + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Invalid RDAP server for TLD .$tld, falling back to WHOIS" >&3 + fi + return 0 + fi + fi + + rdap_server=$(adjust_rdap_url "$tld" "$rdap_server") + + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Using RDAP server: $rdap_server" >&3 + fi + if ! echo "$rdap_server" | grep -q -E '^https?://[a-zA-Z0-9.-]+(/.*)?$|^0$'; then + if [ "$debug" = "true" ]; then + echo "ERROR [$(date +'%H:%M:%S')]: Invalid RDAP server URL format: $rdap_server, falling back to WHOIS" >&3 + fi + return 0 + fi + rdap_url="${rdap_server%/}/domain/$domain" + + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Querying RDAP URL: $rdap_url" >&3 + fi + set +e + error_output=$(curl -s -f --retry 3 --connect-timeout 3 --max-time 5 "$rdap_url" 2>"$error_file") + curl_rc=$? + set -e + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: curl completed with exit code $curl_rc" >&3 + fi + if [ $curl_rc -ne 0 ]; then + if [ "$debug" = "true" ]; then + echo "ERROR [$(date +'%H:%M:%S')]: curl failed for $rdap_url, error: $(cat "$error_file"), code: $curl_rc, falling back to WHOIS" >&3 + fi + return 0 + fi + response="$error_output" + if [ -z "$response" ]; then + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: RDAP query failed for $rdap_url, no response, falling back to WHOIS" >&3 + fi + return 0 + fi + + if command -v jq >/dev/null 2>&1; then + set +e + expiration=$(echo "$response" | jq -r '.events[] | select(.eventAction | test("expiration")) | .eventDate' 2>"$error_file") + jq_rc=$? + set -e + if [ $jq_rc -ne 0 ] || [ -z "$expiration" ]; then + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: jq failed to parse RDAP response for $rdap_url, error: $(cat "$error_file"), falling back to WHOIS" >&3 + fi + return 0 + fi + else + expiration=$(echo "$response" | grep -A 1 '"eventAction":".*expiration"' | grep '"eventDate"' | awk -F'"' '{print $4}') + fi + + if [ -z "$expiration" ]; then + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: No expiration date found in RDAP response for $rdap_url, falling back to WHOIS" >&3 + fi + return 0 + fi + + echo "$expiration" | awk -F'T' '{print $1}' + return 0 +} + +# Preprocesses command-line arguments to handle empty server values +# Parameters: +# $@: All command-line arguments +# Returns: +# Space-separated string of processed arguments +preprocess_args() { + new_args="" + while [ $# -gt 0 ]; do + case "$1" in + -r|--rdap-server) + shift + if [ $# -gt 0 ] && [ -z "$1" ]; then + new_args="$new_args -r 0" + shift + else + new_args="$new_args -r" + [ $# -gt 0 ] && new_args="$new_args $1" + [ $# -gt 0 ] && shift + fi + ;; + -s|--whois-server) + shift + if [ $# -gt 0 ] && [ -z "$1" ]; then + new_args="$new_args -s 0" + shift + else + new_args="$new_args -s" + [ $# -gt 0 ] && new_args="$new_args $1" + [ $# -gt 0 ] && shift + fi + ;; + *) + new_args="$new_args $1" + shift + ;; + esac + done + echo "$new_args" +} + +# Parses command-line arguments and sets global variables +# Parameters: +# $@: Command-line arguments +# Returns: +# Sets global variables (domain, whois_server_override, rdap_server_override, etc.) +# Exits with STATE_UNKNOWN on invalid arguments +parse_arguments() { + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Entering parse_arguments with args: $@" >&2 + fi + local short_opts="hVd:w:c:P:s:r:z" + local long_opts="help,version,domain:,warning:,critical:,path:,whois-server:,rdap-server:,debug" + local default_warning="30" default_critical="7" + local processed_args=$(preprocess_args "$@") + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Processed args: $processed_args" >&2 + fi + + set +e + args=$(getopt -o "$short_opts" --long "$long_opts" -u -n "$PROGRAM" -- $processed_args 2>"$error_file") + getopt_rc=$? + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: getopt returned: $args, rc: $getopt_rc" >&2 + fi + if [ $getopt_rc -ne 0 ]; then + echo "ERROR [$(date +'%H:%M:%S')]: getopt failed to parse arguments: $(cat "$error_file")" >&2 + die "$STATE_UNKNOWN" "State: UNKNOWN ; Invalid arguments" + fi + eval set -- "$args" 2>"$error_file" + eval_rc=$? + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: eval set -- returned rc: $eval_rc" >&2 + fi + if [ $eval_rc -ne 0 ]; then + echo "ERROR [$(date +'%H:%M:%S')]: eval set -- failed: $(cat "$error_file")" >&2 + die "$STATE_UNKNOWN" "State: UNKNOWN ; Argument processing failed" + fi + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: After eval set --: $@" >&2 + fi + + warning="$default_warning" + critical="$default_critical" + domain="" + whois_server_override="0" + rdap_server_override="0" + whoispath="" + + while [ $# -gt 0 ]; do + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Processing argument: $1, remaining args: $@" >&2 + fi + case "$1" in + -c|--critical) + if [ $# -ge 2 ] && is_numeric "$2"; then + critical="$2" + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Set critical=$critical" >&2 + fi + shift 2 + else + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Invalid or missing critical value, using default: $default_critical" >&2 + fi + critical="$default_critical" + shift + [ $# -ge 1 ] && ! echo "$1" | grep -q '^-' && shift + fi + ;; + -w|--warning) + if [ $# -ge 2 ] && is_numeric "$2"; then + warning="$2" + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Set warning=$warning" >&2 + fi + shift 2 + else + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Invalid or missing warning value, using default: $default_warning" >&2 + fi + warning="$default_warning" + shift + [ $# -ge 1 ] && ! echo "$1" | grep -q '^-' && shift + fi + ;; + -d|--domain) + if [ $# -ge 2 ]; then + domain="$2" + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Set domain=$domain" >&2 + fi + shift 2 + else + shift + fi + ;; + -P|--path) + if [ $# -ge 2 ]; then + whoispath="$2" + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Set whoispath=$whoispath" >&2 + fi + shift 2 + else + shift + fi + ;; + -s|--whois-server) + if [ $# -ge 2 ]; then + whois_server_override="$2" + is_valid_server "$whois_server_override" || die "$STATE_UNKNOWN" "State: UNKNOWN ; Invalid WHOIS server: '$whois_server_override'" + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Set whois_server='$whois_server_override'" >&2 + fi + shift 2 + else + shift + fi + ;; + -r|--rdap-server) + if [ $# -ge 2 ]; then + rdap_server_override="$2" + is_valid_url "$rdap_server_override" || die "$STATE_UNKNOWN" "State: UNKNOWN ; Invalid RDAP server: '$rdap_server_override'" + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Set rdap_server='$rdap_server_override'" >&2 + fi + shift 2 + else + shift + fi + ;; + -V|--version) + version + exit 0 + ;; + -z|--debug) + debug="true" + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Set debug=$debug" >&2 + fi + shift + ;; + -h|--help) + fullusage + exit 0 + ;; + --) + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Entering -- case" >&2 + fi + shift + break + ;; + *) + die "$STATE_UNKNOWN" "State: UNKNOWN ; Invalid argument: $1" + ;; + esac + done + + set -e + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Successfully completed parsing. domain=$domain, whois_server=$whois_server_override, rdap_server=$rdap_server_override, warning=$warning, critical=$critical" >&2 + fi + [ -z "$domain" ] && die "$STATE_UNKNOWN" "State: UNKNOWN ; No domain specified" + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Exiting parse_arguments" >&2 + fi +} + +# Configures the whois command path +# Parameters: None +# Returns: +# Sets global whois variable to valid whois executable path +# Exits with STATE_UNKNOWN if whois is not found +setup_whois() { + if [ -n "$whoispath" ]; then + [ -x "$whoispath" ] && whois=$whoispath + [ -x "$whoispath/whois" ] && whois=$whoispath/whois + [ -n "$whois" ] || die "$STATE_UNKNOWN" "State: UNKNOWN ; Invalid whois path" + else + command -v whois >/dev/null || die "$STATE_UNKNOWN" "State: UNKNOWN ; whois not found in PATH" + whois=whois + fi +} + +# Executes WHOIS query for the domain and saves output +# Parameters: None +# Returns: +# Saves WHOIS output to $outfile +# Exits with STATE_UNKNOWN on query failure or invalid domain +run_whois() { + setup_whois + local output error + if [ "$whois_server_override" = "0" ]; then + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Running whois for $domain without specific server" >&2 + fi + set +e + output=$("$whois" "$domain" 2>&1) + error=$? + set -e + else + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Running whois for $domain with server $whois_server_override" >&2 + fi + set +e + output=$("$whois" -h "$whois_server_override" "$domain" 2>&1) + error=$? + set -e + fi + echo "$output" > "$outfile" + cp "$outfile" "$tmpdir/whois_raw.txt" + + [ -s "$outfile" ] || die "$STATE_UNKNOWN" "State: UNKNOWN ; Domain $domain doesn't exist or no WHOIS server available" + + if grep -q -E "No match for|NOT FOUND|NO DOMAIN" "$outfile"; then + die "$STATE_UNKNOWN" "State: UNKNOWN ; Domain $domain doesn't exist" + fi + + if grep -q -E "Query rate limit exceeded|WHOIS_LIMIT_EXCEEDED" "$outfile"; then + die "$STATE_UNKNOWN" "State: UNKNOWN ; Rate limited WHOIS" + fi + + if [ $error -ne 0 ] || grep -q -E "fgets|Connection refused|Timeout|No whois server|socket" "$outfile"; then + die "$STATE_UNKNOWN" "State: UNKNOWN ; WHOIS query failed for $domain with error $error" + fi +} + +# Extracts expiration date from WHOIS output file +# Parameters: +# $1: Path to WHOIS output file +# Returns: +# Expiration date in YYYY-MM-DD format +# Exits with STATE_UNKNOWN if parsing fails +get_expiration() { + local outfile="$1" + local expiration + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Running awk on $outfile" >&2 + fi + set +e + expiration=$($awk ' + BEGIN { + split("january february march april may june july august september october november december", months, " ") + for (i in months) { + Month[tolower(months[i])] = sprintf("%02d", i) + Mon[tolower(substr(months[i],1,3))] = sprintf("%02d", i) + } + HH_MM_DD = "[0-9][0-9]:[0-9][0-9]:[0-9][0-9]" + YYYY = "[0-9][0-9][0-9][0-9]" + DD = "[0-9][0-9]" + MON = "[A-Za-z][a-z][a-z]" + DATE_DD_MM_YYYY_DOT = "[0-9][0-9]\\.[0-9][0-9]\\.[0-9][0-9][0-9][0-9]" + DATE_DD_MON_YYYY = "[0-9][0-9]-[A-Za-z][a-z][a-z]-[0-9][0-9][0-9][0-9]" + DATE_ISO_FULL = "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T" + DATE_YYYY_MM_DD_DASH = "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]" + DATE_YYYY_MM_DD_DOT = "[0-9][0-9][0-9][0-9]\\.[0-9][0-9]\\.[0-9][0-9]" + DATE_YYYY_MM_DD_SLASH = "[0-9][0-9][0-9][0-9]/[0-9][0-9]/[0-9][0-9]" + DATE_DD_MM_YYYY_SLASH = "[0-9][0-9]/[0-9][0-9]/[0-9][0-9][0-9][0-9]" + DATE_YYYY_MM_DD_NUM = "[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]" + DATE_YYYY_MM_DD_DASH_HH_MM_SS = DATE_YYYY_MM_DD_DASH " " HH_MM_DD + DATE_DD_MM_YYYY_DOT_HH_MM_SS = DATE_DD_MM_YYYY_DOT " " HH_MM_DD + DATE_DAY_MON_DD_HHMMSS_TZ_YYYY = "[A-Z][a-z][a-z] [A-Z][a-z][a-z] [0-9][0-9] " HH_MM_DD " GMT " YYYY + DATE_DD_MON_YYYY_HHMMSS = "[0-9][0-9]-" MON "-" YYYY " " HH_MM_DD + DATE_DD_MON_YYYY_HHMMSS_TZ = "[0-9][0-9]-" MON "-" YYYY " " HH_MM_DD " UTC" + DATE_YYYYMMDD_HHMMSS = DATE_YYYY_MM_DD_DOT " " HH_MM_DD + DATE_DD_MM_YYYY_SLASH_HHMMSS_TZ = DATE_DD_MM_YYYY_SLASH " " HH_MM_DD " [A-Z]+" + DATE_DD_MON_YYYY_HHMMSS_TZ_SPACE = "[0-9][0-9] " MON " " YYYY " " HH_MM_DD " UTC" + DATE_YYYY_MM_DD_DASH_HH_MM_SS_TZ_SPACE_OFFSET = DATE_YYYY_MM_DD_DASH " " HH_MM_DD " \\(UTC\\+[0-9]+\\)" + DATE_YYYY_MM_DD_HH_MM_SS = DATE_YYYY_MM_DD_DASH " " HH_MM_DD "\\+[0-9]+" + DATE_ISO_LIKE = "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] " + } + + function mon2moy(month) { month = tolower(month); return Mon[month] ? Mon[month] : 0 } + function month2moy(month) { month = tolower(month); return Month[month] ? Month[month] : 0 } + function get_iso_date(line, fs, field, a, d) { + if (split(line, a, fs)) { if (split(a[field], d, /T/)) { return d[1] } } + } + + # Matches "expires: Month Day Year" (e.g., "expires: December 31 2025") + # Extracts year ($4), month ($2 via month2moy), day ($3) and formats as YYYY-MM-DD + /expires:/ && $0 ~ /[A-Za-z]+[ \t]+[0-9]+[ \t]+[0-9]{4}/ { printf("%s-%02d-%02d\n", $4, month2moy($2), $3); exit } + + # Matches "Expiry Date: DD-MMM-YYYY" or "expiry date: DD-MMM-YYYY" (e.g., "Expiry Date: 31-Dec-2025") + # Extracts date via regex match, splits by "-", converts month (MMM) to number, formats as YYYY-MM-DD + /[Ee][xpiryYy][Dd][Aa][Tt][Ee]:.*[0-9]{2}-[A-Za-z]{3}-[0-9]{4}/ { match($0, /[0-9]{2}-[A-Za-z]{3}-[0-9]{4}/); date = substr($0, RSTART, RLENGTH); split(date, a, "-"); m = mon2moy(a[2]); if (m) { printf("%04d-%02d-%02d", a[3], m, a[1]); exit } } + + # Matches "Expire Date: YYYY-MM-DD" or "expire-date: YYYY-MM-DD" (e.g., "Expire Date: 2025-12-31") + # Removes prefix before colon, extracts YYYY-MM-DD via regex, outputs directly + /[Ee]xpire[- ][Dd]ate:.*[0-9]{4}-[0-9]{2}-[0-9]{2}/ { sub(/^.*: */, "", $0); match($0, /[0-9]{4}-[0-9]{2}-[0-9]{2}/); print substr($0, RSTART, RLENGTH); exit } + + # Matches "Expiry Date: DD/MM/YYYY" (e.g., "Expiry Date: 31/12/2025") + # Splits last field ($NF) by "/", formats as YYYY-MM-DD using fields 3, 2, 1 + /[Ee]xpiry [Dd]ate:.*[0-3][0-9]\/[0-1][0-9]\/[0-9]{4}/ { split($NF, a, "/"); printf("%04d-%02d-%02d", a[3], a[2], a[1]); exit } + + # Matches "Expiry Date: YYYY/MM/DD" (e.g., "Expiry Date: 2025/12/31") + # Splits last field ($NF) by "/", formats as YYYY-MM-DD using fields 1, 2, 3 + /[Ee]xpiry [Dd]ate:.*[0-9]{4}\/[0-9]{2}\/[0-9]{2}/ { split($NF, a, "/"); printf("%04d-%02d-%02d", a[1], a[2], a[3]); exit } + + # Matches "Expiration Date: YYYY-MM-DDThh:mm:ss" (e.g., "Expiration Date: 2025-12-31T23:59:59") + # Uses get_iso_date to split by ":" and extract YYYY-MM-DD before "T" + /Expiration Date:.*[0-9]{4}-[0-9]{2}-[0-9]{2}T/ { print get_iso_date($0, ":", 2); exit } + + # Matches "Expiration Date: DD.MM.YYYY" (e.g., "Expiration Date: 31.12.2025") + # Splits last field ($NF) by ".", formats as YYYY-MM-DD using fields 3, 2, 1 + /Expiration [Dd]ate:.*[0-9]{2}\.[0-9]{2}\.[0-9]{4}/ { split($NF, a, "."); printf("%04d-%02d-%02d", a[3], a[2], a[1]); exit } + + # Matches "expires: YYYYMMDD" (e.g., "expires: 20251231") + # Extracts substrings from second field ($2) for year, month, day, formats as YYYY-MM-DD + /expires:.*[0-9]{8}/ { printf("%s-%s-%s", substr($2,1,4), substr($2,5,2), substr($2,7,2)); exit } + + # Matches "[Expires on] YYYY/MM/DD" (e.g., "[Expires on] 2025/12/31") + # Splits last field ($NF) by "/", formats as YYYY-MM-DD using fields 1, 2, 3 + /\[Expires on\].*[0-9]{4}\/[0-9]{2}\/[0-9]{2}/ { split($NF, a, "/"); printf("%04d-%02d-%02d", a[1], a[2], a[3]); exit } + + # Matches "Record expires on YYYY-MM-DD" (e.g., "Record expires on 2025-12-31") + # Extracts YYYY-MM-DD via regex match, outputs directly + /Record expires on[[:space:]]*[0-9]{4}-[0-9]{2}-[0-9]{2}/ { match($0, /[0-9]{4}-[0-9]{2}-[0-9]{2}/); print substr($0, RSTART, RLENGTH); exit } + + # Matches "Expiry Date: YYYY-MM-DD" (e.g., "Expiry Date: 2025-12-31") + # Extracts YYYY-MM-DD via regex match, outputs directly + /Expiry Date:[[:space:]]*[0-9]{4}-[0-9]{2}-[0-9]{2}/ { match($0, /[0-9]{4}-[0-9]{2}-[0-9]{2}/); print substr($0, RSTART, RLENGTH); exit } + + # Matches "Domain Expiration Date: MMM MMM DD hh:mm:ss YYYY" (e.g., "Domain Expiration Date: Wed Dec 31 23:59:59 2025") + # Extracts DD MMM YYYY via regex, splits by space, converts month (MMM) to number, formats as YYYY-MM-DD + /Domain Expiration Date:[[:space:]]*[A-Za-z]{3} [A-Za-z]{3} [0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} [0-9]{4}/ { match($0, /[0-9]{2} [A-Za-z]{3} [0-9]{4}/); date = substr($0, RSTART, RLENGTH); split(date, a, " "); m = mon2moy(a[2]); if (m) { printf("%04d-%02d-%02d", a[3], m, a[1]); exit } } + + # Matches "expires: DD-MMM-YYYY" (e.g., "expires: 31-Dec-2025") + # Splits third field ($3) by "-", converts month (MMM) to number, formats as YYYY-MM-DD + /expires:.*[0-9]{2}-[A-Za-z]{3}-[0-9]{4}/ { split($3, a, "-"); printf("%s-%s-%s\n", a[3], mon2moy(a[2]), a[1]); exit } + + # Matches "Expiration date: DD.MM.YYYY hh:mm" (e.g., "Expiration date: 31.12.2025 23:59") + # Splits second-to-last field ($(NF-1)) by ".", formats as YYYY-MM-DD using fields 3, 2, 1 + /Expiration date:.*[0-9]{2}\.[0-9]{2}\.[0-9]{4} [0-9]{2}:[0-9]{2}/ { split($(NF-1), a, "."); printf("%s-%s-%s", a[3], a[2], a[1]); exit } + + # Matches "expires: YYYY-MM-DD" (e.g., "expires: 2025-12-31") + # Outputs last field ($NF) directly as YYYY-MM-DD + /expires:.*[0-9]{4}-[0-9]{2}-[0-9]{2}/ { print $NF; exit } + + # Matches "expire: YYYY-MM-DD hh:mm:ss" (e.g., "expire: 2025-12-31 23:59:59") + # Extracts YYYY-MM-DD via regex match, outputs directly + /expire:.*[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/ { match($0, /[0-9]{4}-[0-9]{2}-[0-9]{2}/); print substr($0, RSTART, RLENGTH); exit } + + # Matches "expire: YYYY-MM-DD hh:mm" (e.g., "expire: 2025-12-31 23:59") + # Splits entire line by space, splits first field by "-", formats as YYYY-MM-DD + /expire:.*[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}/ { split($0, a, " "); split(a[1], b, "-"); printf("%s-%s-%s", b[1], b[2], b[3]); exit } + + # Matches "renewal date: YYYY.MM.DD hh" (e.g., "renewal date: 2025.12.31 23") + # Splits second-to-last field ($(NF-1)) by ".", formats as YYYY-MM-DD using fields 1, 2, 3 + /renewal date:.*[0-9]{4}\.[0-9]{2}\.[0-9]{2} [0-9]{2}/ { split($(NF-1), a, "."); printf("%s-%s-%s", a[1], a[2], a[3]); exit } + + # Matches "paid-till: YYYY.MM.DD" (e.g., "paid-till: 2025.12.31") + # Splits second field ($2) by ".", formats as YYYY-MM-DD using fields 1, 2, 3 + /paid-till:.*[0-9]{4}\.[0-9]{2}\.[0-9]{2}/ { split($2, a, "."); printf("%s-%s-%s", a[1], a[2], a[3]); exit } + + # Matches "paid-till: YYYY-MM-DD" or "paid-till: YYYY-MM-DDThh:mm:ss" (e.g., "paid-till: 2025-07-03T04:00:09Z") + # Extracts YYYY-MM-DD via regex match, outputs directly + /paid-till:[[:space:]]*[0-9]{4}-[0-9]{2}-[0-9]{2}(T.*)?$/ { match($0, /[0-9]{4}-[0-9]{2}-[0-9]{2}/); print substr($0, RSTART, RLENGTH); exit } + + # Matches "Valid Until: YYYY-MM-DD" (e.g., "Valid Until: 2025-12-31") + # Outputs last field ($NF) directly as YYYY-MM-DD + /Valid Until:.*[0-9]{4}-[0-9]{2}-[0-9]{2}/ { print $NF; exit } + + # Matches "expire: DD.MM.YYYY" (e.g., "expire: 31.12.2025") + # Splits second field ($2) by ".", formats as YYYY-MM-DD using fields 3, 2, 1 + /expire:.*[0-9]{2}\.[0-9]{2}\.[0-9]{4}/ { split($2, a, "."); printf("%s-%s-%s", a[3], a[2], a[1]); exit } + + # Matches "expires: YYYY-MM-DD" (e.g., "expires: 2025-12-31") + # Outputs last field ($NF) directly as YYYY-MM-DD + /expires:.*[0-9]{4}-[0-9]{2}-[0-9]{2}/ { print $NF; exit } + + # Matches "domain_datebilleduntil: YYYY-MM-DDThh:mm:ss" (e.g., "domain_datebilleduntil: 2025-12-31T23:59:59") + # Uses get_iso_date to split by ":" and extract YYYY-MM-DD before "T" + /domain_datebilleduntil:.*[0-9]{4}-[0-9]{2}-[0-9]{2}T/ { print get_iso_date($0, ":", 2); exit } + + # Matches "Registrar Registration Expiration Date: YYYY-MM-DDThh:mm:ss[optional TZ]" (e.g., "Registrar Registration Expiration Date: 2025-12-31T23:59:59Z") + # Extracts YYYY-MM-DD using regex match, outputs directly + /Registrar Registration Expiration Date:.*[0-9]{4}-[0-9]{2}-[0-9]{2}T/ { match($0, /[0-9]{4}-[0-9]{2}-[0-9]{2}/); printf("%s", substr($0, RSTART, RLENGTH)); exit } + + # Matches "Expiration Date (dd/mm/yyyy): DD/MM/YYYY" (e.g., "Expiration Date (dd/mm/yyyy): 31/12/2025") + # Splits third field ($3) by "/", formats as YYYY-MM-DD using fields 3, 2, 1 + /Expiration Date.*\(dd\/mm\/yyyy)/ { split($3, a, "/"); printf("%s-%02d-%02d", a[3], a[2], a[1]); exit } + + # Matches "Domain Expiration Date: Day MMM DD hh:mm:ss GMT YYYY" (e.g., "Domain Expiration Date: Wed Dec 31 23:59:59 GMT 2025") + # Formats as YYYY-MM-DD using fields 9 (year), 5 (month via mon2moy), 6 (day) + /Domain Expiration Date:.*[A-Z][a-z]{2} [A-Z][a-z]{2} [0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} GMT [0-9]{4}/ { printf("%s-%02d-%02d", $9, mon2moy($5), $6); exit } + + # Matches "Expiration Date: DD-MMM-YYYY hh:mm:ss UTC" (e.g., "Expiration Date: 31-Dec-2025 23:59:59 UTC") + # Removes prefix, splits first field ($1) by "-", converts month (MMM) to number, formats as YYYY-MM-DD + /Expiration Date:.*[0-9]{2}-[A-Za-z]{3}-[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2} UTC/ { sub(/^.*Expiration Date: */, "", $0); split($1, a, "-"); printf("%s-%02d-%02d", a[3], mon2moy(a[2]), a[1]); exit } + + # Matches "Expiration Date: DD-MMM-YYYY hh:mm:ss" (e.g., "Expiration Date: 31-Dec-2025 23:59:59") + # Removes prefix, splits first field ($1) by "-", converts month (MMM) to number, formats as YYYY-MM-DD + /Expiration Date:.*[0-3][0-9]-[A-Za-z]{3}-[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}/ { sub(/^.*Expiration Date: */, "", $0); split($1, a, "-"); printf("%s-%02d-%02d", a[3], mon2moy(a[2]), a[1]); exit } + + # Matches "Expiry Date: DD MMM YYYY hh:mm:ss UTC" (e.g., "Expiry Date: 31 Dec 2025 23:59:59 UTC") + # Formats as YYYY-MM-DD using fields 5 (year), 4 (month via mon2moy), 3 (day) + /Expiry Date:.*[0-9]{2} [A-Za-z]{3} [0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2} UTC/ { printf("%s-%02d-%02d", $5, mon2moy($4), $3); exit } + + # Matches "expires on.*YYYY-MM-DD hh:mm:ss \(UTC\+[0-9]+\)" (e.g., "expires on 2025-12-31 23:59:59 (UTC+1)") + # Splits line by space, outputs fourth field (a[3]) as YYYY-MM-DD + /expires on.*[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} \(UTC\+[0-9]+\)/ { split($0, a, " "); print a[3]; exit } + + # Matches "date: YYYY/MM/DD.*" (e.g., "date: 2025/12/31") + # Splits second field ($2) by "/", formats as YYYY-MM-DD using fields 1, 2, 3 + /date:.*[0-9]{4}\/[0-9]{2}\/[0-9]{2}/ { split($2, a, "/"); printf("%s-%02d-%02d", a[1], a[2], a[3]); exit } + + # Matches "Expiry Date: DD/MM/YYYY" (e.g., "Expiry Date: 31/12/2025") + # Splits last field ($NF) by "/", formats as YYYY-MM-DD using fields 3, 2, 1 + /Expiry Date:.*[0-9]{2}\/[0-9]{2}\/[0-9]{4}/ { split($NF, a, "/"); printf("%s-%02d-%02d", a[3], a[2], a[1]); exit } + + # Matches "Valid-date: YYYY-MM-DD" or "Expires: YYYY-MM-DD" or "Expiry: YYYY-MM-DD" (e.g., "Valid-date: 2025-12-31") + # Outputs last field ($NF) directly as YYYY-MM-DD + /(Valid-date|Expir(es|y)):.*[0-9]{4}-[0-9]{2}-[0-9]{2}/ { print $NF; exit } + + # Matches "[State] YYYY/MM/DD/YYYY" (e.g., "[State] 2025/12/31") + # Removes parentheses from last field ($NF), splits by "/", formats as YYYY-MM-DD using fields 1, 2, 3 + /\[State\].*[0-9]{4}\/[0-9]{2}\/[0-9]{2}/ { gsub(/[()]/, "", $NF); split($NF, a, "/"); printf("%s-%s-%s", a[1], a[2], a[3]); exit } + + # Matches "expires at: DD/MM/YYYY" (e.g., "expires at: 31/12/2025") + # Splits third field ($3) by "/", formats as YYYY-MM-DD using fields 3, 2, 1 + /expires at:.*[0-9]{2}\/[0-9]{2}\/[0-9]{4}/ { split($3, a, "/"); printf("%s-%02d-%02d", a[3], a[2], a[1]); exit } + + # Matches "Renewal Date: YYYY-MM-DD" (e.g., "Renewal Date: 2025-12-31") + # Outputs third field ($3) directly as YYYY-MM-DD + /Renewal Date:.*[0-9]{4}-[0-9]{2}-[0-9]{2}/ { print $3; exit } + + # Matches "Expiry Date: DD-MM-YYYY" (e.g., "Expiry Date: 31-12-2025") + # Splits third field ($3) by "-", formats as YYYY-MM-DD using fields 3, 2, 1 + /Expiry Date:.*[0-9]{2}-[0-9]{2}-[0-9]{4}/ { split($3, a, "-"); printf("%s-%02d-%02d", a[3], a[2], a[1]); exit } + + # Matches "validity: DD-MM-YYYY" (e.g., "validity: 31-12-2025") or "validity: N/A" + # If second field ($2) is "N/A", outputs "2100-01-01"; otherwise, splits $2 by "-", formats as YYYY-MM-DD + /validity:.*[0-9]{2}-[0-9]{2}-[0-9]{4}/ { if ($2 == "N/A") { printf "2100-01-01\n"; exit } else { split($2, a, "-"); printf("%s-%02d-%02d", a[3], a[2], a[1]); exit } } + + # Matches "Expired on: YYYY-MM-DD-DD" (e.g., "Expired on: 2025-12-31") + # Splits third field ($3) by "-", formats as YYYY-MM-DD using fields 1, 2, 3 + /Expired on:.*[0-9]{4}-[0-9]{2}-[0-9]{2}/ { split($3, a, "-"); printf("%s-%02d-%02d", a[1], a[2], a[3]); exit } + + # Matches "Expires.*YYYY-MM-DD" (e.g., "Expires 2025-12-31") + # Splits second field ($2) by "-", formats as YYYY-MM-DD using fields 1, 2, 3 + /Expires.*[0-9]{4}-[0-9]{2}-[0-9]{2}/ { split($2, a, "-"); printf("%s-%02d-%02d", a[1], a[2], a[3]); exit } + + # Matches "expires: DD.MM.YYYY hh:mm:ss" or "expires.: DD.MM.YYYY hh:mm:ss" (e.g., "expires: 31.12.2025 23:59:59") + # Removes prefix, splits first field ($1) by ".", formats as YYYY-MM-DD using fields 3, 2, 1 + /^expires\.*:.*[0-9][0-9]?\.[0-9][0-9]?\.[0-9]{4}\s[0-9]{2}:[0-9]{2}:[0-9]{2}/ { sub(/^expires\.*: */, "", $0); split($1, a, "."); printf("%04d-%02d-%02d", a[3], a[2], a[1]); exit } + + # Matches "expires: YYYY-MM-DD hh:mm:ss" (e.g., "expires: 2025-12-31 23:59:59") + # Extracts YYYY-MM-DD via regex match, outputs directly + /expires:.*[0-9]{4}-[0-9]{2}-[0-9]{2}\s[0-9]{2}:[0-9]{2}:[0-9]{2}/ { match($0, /[0-9]{4}-[0-9]{2}-[0-9]{2}/); print substr($0, RSTART, RLENGTH); exit } + + # Matches "expires: YYYY-MM-DD hh:mm" (e.g., "expires: 2025-12-31 23:59") + # Splits line by space, splits first field (a[1]) by "-", formats as YYYY-MM-DD + /expires:.*[0-9]{4}-[0-9]{2}-[0-9]{2}\s[0-9]{2}:[0-9]{2}/ { split($0, a, " "); split(a[1], b, "-"); printf("%s-%s-%s", b[1], b[2], b[3]); exit } + + # Matches "renewal:" to set flag for next line processing + # Sets renewal variable to 1, skips to next line + /renewal:/ { renewal=1; next } + + # Matches line following "renewal:" (e.g., "DD MMM YYYY" after "renewal:") + # Removes non-digits from second field ($2), converts month ($3) to number, formats as YYYY-MM-DD + /renewal/ { sub(/[^0-9]+/, "", $2); printf("%s-%02d-%02d", $4, mon2moy($3), $2); exit } + + # Matches "Expiry date: DD-MMM-YYYY" (e.g., "Expiry date: 31-Dec-2025") + # Splits last field ($NF) by "-", converts month (MMM) to number, formats as YYYY-MM-DD + /Expiry date:.*[0-9]{2}-[A-Za-z]{3}-[0-9]{4}/ { split($NF, a, "-"); printf("%s-%02d-%02d", a[3], mon2moy(a[2]), a[1]); exit } + + # Matches "Expiration Date: YYYY-MM-DD\s" (e.g., "Expiration Date: 2025-12-31 ") + # Uses get_iso_date to split by ":" and extract YYYY-MM-DD + $0 ~ "Expiration Date:\s" DATE_ISO_LIKE { print get_iso_date($0, ":", 2); exit } + + # Matches "Expiration Time: YYYY-MM-DD hh:mm:ss" (e.g., "Expiration Time: 2025-12-31 23:59:59") + # Splits third field ($3) by "-", formats as YYYY-MM-DD using fields 1, 2, 3 + $0 ~ "Expiration Time:.*" DATE_YYYY_MM_DD_DASH_HH_MM_SS { split($3, a, "-"); printf("%s-%s-%s", a[1], a[2], a[3]); exit } + + # Matches "billed until: YYYY-MM-DDThh:mm:ss" (e.g., "billed until: 2025-12-31T23:59:59") + # Uses get_iso_date to split by ":" and extract YYYY-MM-DD before "T" + $0 ~ "billed[ ]*until:\s" DATE_ISO_FULL { print get_iso_date($0, ":", 2); exit } + ' "$outfile" 2>"$error_file") + awk_rc=$? + set -e + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: awk completed with exit code $awk_rc, expiration='$expiration'" >&2 + fi + if [ $awk_rc -ne 0 ]; then + if [ "$debug" = "true" ]; then + echo "ERROR [$(date +'%H:%M:%S')]: awk failed, error: $(cat "$error_file")" >&2 + fi + die "$STATE_UNKNOWN" "State: UNKNOWN ; Failed to parse WHOIS data for $domain" + fi + if [ -z "$expiration" ]; then + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: No expiration date found in WHOIS data for $domain" >&2 + fi + die "$STATE_UNKNOWN" "State: UNKNOWN ; No expiration date found for $domain" + fi + # Validate expiration date format/trim whitespace + expiration=$(echo "$expiration" | tr -d '[:space:]') + if ! echo "$expiration" | grep -q -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'; then + if [ "$debug" = "true" ]; then + echo "ERROR [$(date +'%H:%M:%S')]: Invalid expiration date format: '$expiration' for $domain" >&2 + fi + die "$STATE_UNKNOWN" "State: UNKNOWN ; Invalid expiration date format '$expiration' for $domain" + fi + echo "$expiration" } # Parse command line arguments -parse_arguments() { - # shellcheck disable=SC2039 - local args - args=$(getopt -o hVd:w:c:P:s:a:C: --long help,version,domain:,warning:,critical:,path:,server:,cache-age:,cache-dir: -u -n "$PROGRAM" -- "$@") - eval set -- "$args" - - while :; do - case "$1" in - -c|--critical) - shift - critical=$1 - ;; - -w|--warning) - shift - warning=$1 - ;; - -d|--domain) - shift - domain=$1 - ;; - -P|--path) - shift - whoispath=$1 - ;; - -s|--server) - if [ "$2" = "--" ]; then - shift - break - else - shift - server=$1 - fi - ;; - -V|--version) - version - exit - ;; - -a|--cache-age) - shift - cache_age=$1 - ;; - -C|--cache-dir) - shift - cache_dir=$1 - ;; - -h|--help) - fullusage - exit - ;; - --) - shift - break - ;; - *) - die "$STATE_UNKNOWN" "State: UNKNOWN ; Internal error!" - ;; - esac - shift - done - - if [ -z "$domain" ]; then - die "$STATE_UNKNOWN" "State: UNKNOWN ; There is no domain name to check" - fi - - # validate cache args - if [ -n "$cache_dir" ] && [ ! -d "$cache_dir" ]; then - die "$STATE_UNKNOWN" "State: UNKNOWN ; Cache dir: '$cache_dir' does not exist" - fi - if [ -n "$cache_age" ] && ! is_numeric "$cache_age"; then - die "$STATE_UNKNOWN" "State: UNKNOWN ; Cache age is not numeric: '$cache_age'" - fi - if [ -n "$cache_dir" ] && [ "$cache_age" -le 0 ]; then - die "$STATE_UNKNOWN" "State: UNKNOWN ; Cache dir set, but age not" - fi -} - -# create temporary file. as secure as possible -# tempfile name is returned to stdout -temporary_file() { - # shellcheck disable=SC2039 - # shellcheck disable=SC2169 - mktemp --tmpdir -t check_domainXXXXXX 2>/dev/null || echo "${TMPDIR:-/tmp}/check_domain.$RANDOM.$$" -} - -# Looking for whois binary -setup_whois() { - if [ -n "$whoispath" ]; then - if [ -x "$whoispath" ]; then - whois=$whoispath - elif [ -x "$whoispath/whois" ]; then - whois=$whoispath/whois - fi - [ -n "$whois" ] || die "$STATE_UNKNOWN" "State: UNKNOWN ; Unable to find whois binary, you specified an incorrect path" - else - # shellcheck disable=SC2039 - type whois > /dev/null 2>&1 || die "$STATE_UNKNOWN" "State: UNKNOWN ; Unable to find whois binary in your path. Is it installed? Please specify path." - whois=whois - fi -} - -# Run whois(1) -run_whois() { - # shellcheck disable=SC2039 - local error - - setup_whois - - $whois ${server:+-h $server} "$domain" > "$outfile" 2>&1 && error=$? || error=$? - [ -s "$outfile" ] || die "$STATE_UNKNOWN" "State: UNKNOWN ; Domain $domain doesn't exist or no WHOIS server available." - - if grep -q -e "No match for" -e "NOT FOUND" -e "NO DOMAIN" "$outfile"; then - die "$STATE_UNKNOWN" "State: UNKNOWN ; Domain $domain doesn't exist." - fi - - # check for common errors - if grep -q -e "Query rate limit exceeded. Reduced information." -e "WHOIS LIMIT EXCEEDED" "$outfile"; then - die "$STATE_UNKNOWN" "State: UNKNOWN ; Rate limited WHOIS response" - fi - if grep -q -e "fgets: Connection reset by peer" "$outfile"; then - error=0 - fi - - [ $error -eq 0 ] || die "$STATE_UNKNOWN" "State: UNKNOWN ; WHOIS exited with error $error." -} - -# Calculate days until expiration from whois output -get_expiration() { - # shellcheck disable=SC2039 - local outfile=$1 - - # shellcheck disable=SC2016 - $awk ' - BEGIN { - HH_MM_DD = "[0-9][0-9]:[0-9][0-9]:[0-9][0-9]" - YYYY = "[0-9][0-9][0-9][0-9]" - DD = "[0-9][0-9]" - MON = "[A-Za-z][a-z][a-z]" - DATE_DD_MM_YYYY_DOT = "[0-9][0-9]\\.[0-9][0-9]\\.[0-9][0-9][0-9][0-9]" - DATE_DD_MON_YYYY = "[0-9][0-9]-[A-Za-z][a-z][a-z]-[0-9][0-9][0-9][0-9]" - DATE_ISO_FULL = "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T" - DATE_ISO_LIKE = "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] " - DATE_YYYY_MM_DD_DASH = "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]" - DATE_YYYY_MM_DD_DOT = "[0-9][0-9][0-9][0-9]\\.[0-9][0-9]\\.[0-9][0-9]" - DATE_YYYY_MM_DD_SLASH = "[0-9][0-9][0-9][0-9]/[0-9][0-9]/[0-9][0-9]" - DATE_DD_MM_YYYY_SLASH = "[0-9][0-9]/[0-9][0-9]/[0-9][0-9][0-9][0-9]" - DATE_YYYY_MM_DD_NIL = "[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]" - # 2015-10-03 13:36:48 - DATE_YYYY_MM_DD_DASH_HH_MM_SS = DATE_YYYY_MM_DD_DASH " " HH_MM_DD - # 15.05.2016 13:36:48 - DATE_DD_MM_YYYY_DOT_HH_MM_SS = DATE_DD_MM_YYYY_DOT " " HH_MM_DD - - # Wed Mar 02 23:59:59 GMT 2016 - DATE_DAY_MON_DD_HHMMSS_TZ_YYYY = "[A-Z][a-z][a-z] [A-Z][a-z][a-z] [0-9][0-9] " HH_MM_DD " GMT " YYYY - # 25-Apr-2018 16:00:50 - DATE_DD_MON_YYYY_HHMMSS = "[0-9][0-9]-" MON "-" YYYY " " HH_MM_DD - # 02-May-2018 16:12:25 UTC - DATE_DD_MON_YYYY_HHMMSS_TZ = "[0-9][0-9]-" MON "-" YYYY " " HH_MM_DD " UTC" - # 2016.01.14 18:47:31 - DATE_YYYYMMDD_HHMMSS = DATE_YYYY_MM_DD_DOT " " HH_MM_DD - # 21/05/2017 00:00:00 EEST - DATE_DD_MM_YYYY_SLASH_HHMMSS_TZ = DATE_DD_MM_YYYY_SLASH " " HH_MM_DD " [A-Z]+" - # 14 Jan 2016 22:40:29 UTC - DATE_DD_MON_YYYY_HHMMSS_TZ_SPACE = "[0-9][0-9] " MON " " YYYY " " HH_MM_DD " UTC" - # myname.pchome.com.tw date format: 2023-06-03 00:00:00 (UTC+8) - DATE_YYYY_MM_DD_DASH_HH_MM_SS_TZ_SPACE_OFFSET = DATE_YYYY_MM_DD_DASH " " HH_MM_DD " ""\\(UTC\\+[0-9]+\\)" - - # 2007-02-28 11:48:53+02 - DATE_YYYY_MM_DD_DASH_HH_MM_SS_TZOFFSET = DATE_YYYY_MM_DD_DASH " " HH_MM_DD "\\+[0-9]+" - split("january february march april may june july august september october november december", months, " "); - for (i in months) { - mon = months[i] - Month[mon] = i; - mon = substr(mon, 1, 3) - Mon[mon] = i; - } - } - - # convert short month name to month number (Month Of Year) - function mon2moy(month) { - return Mon[tolower(month)] - } - - # convert long month name to month number (Month Of Year) - function month2moy(month) { - return Month[tolower(month)] - } - - # get date from DATE_ISO_FULL format from `s` using field separator `fs` from index `i` and exit - function get_iso_date(s, fs, i, a, d) { - if (split(s, a, fs)) { - if (split(a[i], d, /T/)) { - print d[1]; - exit; - } - } - } - - # Expiry date: 05-Dec-2014 - /Expir(y|ation) [Dd]ate:/ && $NF ~ DATE_DD_MON_YYYY {split($3, a, "-"); printf("%s-%s-%s\n", a[3], mon2moy(a[2]), a[1]); exit} - - # expires: 05-Dec-2014 - /expires:/ && $NF ~ DATE_DD_MON_YYYY {split($3, a, "-"); printf("%s-%s-%s\n", a[3], mon2moy(a[2]), a[1]); exit} - - # Expiry Date: 19/11/2015 - /Expiry Date:/ && $NF ~ DATE_DD_MM_YYYY_SLASH {split($3, a, "/"); printf("%s-%s-%s", a[3], a[2], a[1]); exit} - - # Expiration date: 16.11.2013 15:30:13 - /Expiration date:/ && $0 ~ DATE_DD_MM_YYYY_DOT_HH_MM_SS {split($(NF-1), a, "."); printf("%s-%s-%s", a[3], a[2], a[1]); exit} - - # Expire Date: 2015-10-22 - # expire-date: 2016-02-05 - /[Ee]xpire[- ][Dd]ate:/ && $NF ~ DATE_YYYY_MM_DD_DASH {print $NF; exit} - - # expires: 20170716 - /expires:/ && $NF ~ DATE_YYYY_MM_DD_NIL {printf("%s-%s-%s", substr($2,0,4), substr($2,5,2), substr($2,7,2)); exit} - - # expires: 2015-11-18 - /expires:[ ]+/ && $NF ~ DATE_YYYY_MM_DD_DASH {print $NF; exit} - - # renewal date: 2016.01.14 18:47:31 - /renewal date:/ && $0 ~ DATE_YYYYMMDD_HHMMSS {split($(NF-1), a, "."); printf("%s-%s-%s", a[1], a[2], a[3]); exit} - - # paid-till: 2013.11.01 - /paid-till:/ && $NF ~ DATE_YYYY_MM_DD_DOT {split($2, a, "."); printf("%s-%s-%s", a[1], a[2], a[3]); exit} - - # paid-till: 2016-01-19 - /paid-till:/ && $NF ~ DATE_YYYY_MM_DD_DASH {print $NF; exit} - - # Valid Until: 2016-01-19 - /Valid Until:/ && $NF ~ DATE_YYYY_MM_DD_DASH {print $NF; exit} - - # expire: 16.11.2013 - /expire:/ && $NF ~ DATE_DD_MM_YYYY_DOT {split($2, a, "."); printf("%s-%s-%s", a[3], a[2], a[1]); exit} - - # expire: 2016-01-19 - /expire:/ && $NF ~ DATE_YYYY_MM_DD_DASH {print $NF; exit} - - # Expiration Date: 2017-01-26T10:14:11Z - # Registrar Registration Expiration Date: 2015-02-22T00:00:00Z - # Registrar Registration Expiration Date: 2015-01-11T23:00:00-07:00Z - $0 ~ "Expiration Date: " DATE_ISO_FULL { get_iso_date($0, ":", 2) } - - # domain_datebilleduntil: 2015-01-11T23:00:00-07:00Z - $0 ~ "billed[ ]*until: " DATE_ISO_FULL { get_iso_date($0, ":", 2) } - - # Registrar Registration Expiration Date: 2018-09-21 00:00:00 -0400 - $0 ~ "Expiration Date: " DATE_ISO_LIKE { get_iso_date($0, ":", 2) } - - # Data de expiração / Expiration Date (dd/mm/yyyy): 18/01/2016 - $0 ~ "Expiration Date .dd/mm/yyyy" {split($NF, a, "/"); printf("%s-%s-%s", a[3], a[2], a[1]); exit} - - # Domain Expiration Date: Wed Mar 02 23:59:59 GMT 2016 - $0 ~ "Expiration Date: *" DATE_DAY_MON_DD_HHMMSS_TZ_YYYY { - printf("%s-%s-%s", $9, mon2moy($5), $6); - } - - # Expiration Date:02-May-2018 16:12:25 UTC - $0 ~ "Expiration Date: *" DATE_DD_MON_YYYY_HHMMSS_TZ { - sub(/^.*Expiration Date: */, "") - split($1, a, "-"); - printf("%s-%s-%s", a[3], mon2moy(a[2]), a[1]); - } - - # .sg domains - # Expiration Date: 25-Apr-2018 16:00:50 - # (uses tabs between colon and date, we match tabs or spaces regardless) - $0 ~ "Expiration Date:[ \t]*" DATE_DD_MON_YYYY_HHMMSS { - sub(/^.*Expiration Date:[ \t]*/, "") - split($1, a, "-"); - printf("%s-%s-%s", a[3], mon2moy(a[2]), a[1]); - } - - # Expiry Date: 14 Jan 2016 22:40:29 UTC - $0 ~ "Expiry Date: *" DATE_DD_MON_YYYY_HHMMSS_TZ_SPACE { - printf("%s-%s-%s", $5, mon2moy($4), $3); - } - - # myname.pchome.com.tw - # Record expires on 2023-06-03 00:00:00 (UTC+8) - $0 ~ "Record expires on " DATE_YYYY_MM_DD_DASH_HH_MM_SS_TZ_SPACE_OFFSET { - split($0, a, " "); printf a[4]; - exit; - } - - # Registry Expiry Date: 2015-08-03T04:00:00Z - # Registry Expiry Date: 2017-01-26T10:14:11Z - $0 ~ "Expiry Date: " DATE_ISO_FULL {split($0, a, ":"); s = a[2]; if (split(s,d,/T/)) print d[1]; exit} - - # Expiry date: 2017/07/16 - /Expiry date:/ && $NF ~ DATE_YYYY_MM_DD_SLASH {split($3, a, "/"); printf("%s-%s-%s", a[1], a[2], a[3]); exit} - - # Expiry Date: 19/11/2015 00:59:58 - /Expiry Date:/ && $(NF-1) ~ DATE_DD_MM_YYYY_SLASH {split($3, a, "/"); printf("%s-%s-%s", a[3], a[2], a[1]); exit} - - # Expires: 2014-01-31 - # Expiry : 2014-03-08 - # Valid-date 2014-10-21 - /Valid-date|Expir(es|ation|y)/ && $NF ~ DATE_YYYY_MM_DD_DASH {print $NF; exit} - - # [Expires on] 2014/12/01 - /\[Expires on\]/ && $NF ~ DATE_YYYY_MM_DD_SLASH {split($3, a, "/"); printf("%s-%s-%s", a[1], a[2], a[3]); exit} - - # [State] Connected (2014/12/01) - /\[State\]/ && $NF ~ DATE_YYYY_MM_DD_SLASH {gsub("[()]", "", $3); split($3, a, "/"); printf("%s-%s-%s", a[1], a[2], a[3]); exit} - - # expires at: 21/05/2017 00:00:00 EEST - $0 ~ "expires at: *" DATE_DD_MM_YYYY_SLASH_HHMMSS_TZ {split($3, a, "/"); printf("%s-%s-%s", a[3], a[2], a[1]); exit} - - # Renewal Date: 2016-06-25 - $0 ~ "Renewal Date: *" DATE_YYYY_MM_DD { print($3); exit} - - # Expiry Date: 31-03-2016 - $0 ~ "Expiry Date: *" DATE_DD_MM_YYYY {split($3, a, "-"); printf("%s-%s-%s", a[3], a[2], a[1]); exit} - - # .il domains - # validity: 05-11-2022 - # .il domains registered at the dawn of Internet never expire... - # validity: N/A - $0 ~ "validity: *" DATE_DD_MM_YYYY {if ($2 == "N/A") {print "2100-01-01"} else {split($2, a, "-"); printf("%s-%s-%s", a[3], a[2], a[1])}; exit} - - # Expired: 2015-10-03 13:36:48 - $0 ~ "Expired: *" DATE_YYYY_MM_DD_DASH_HH_MM_SS {split($2, a, "-"); printf("%s-%s-%s", a[1], a[2], a[3]); exit} - - # Expiration Time: 2015-10-03 13:36:48 - $0 ~ "Expiration Time: *" DATE_YYYY_MM_DD_DASH_HH_MM_SS {split($3, a, "-"); printf("%s-%s-%s", a[1], a[2], a[3]); exit} - - # .fi domains - # expires............: 4.7.2017 13:36:48 - /^expires\.*: +[0-9][0-9]?\.[0-9][0-9]?\.[0-9][0-9][0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]$/ { - sub(/^expires\.*: +/, "") - split($1, a, "."); - printf("%s-%02d-%02d", a[3], a[2], a[1]); - exit; - } - - # .ua domain - # expires: 2017-09-01 17:09:32+03 - $0 ~ "expires: *" DATE_YYYY_MM_DD_DASH_HH_MM_SS_TZOFFSET {split($2, a, "-"); printf("%s-%s-%s", a[1], a[2], a[3]); exit} - # FIXME: XXX: weak patterns - - # renewal: 31-March-2016 - /renewal:/{split($2, a, "-"); printf("%s-%s-%s\n", a[3], month2moy(a[2]), a[1]); exit} - - # expires: March 5 2014 - /expires:/{printf("%s-%s-%s\n", $4, month2moy($2), $3); exit} - - # Renewal date: - # Monday 21st Sep 2015 - /Renewal date:/{renewal = 1; next} - {if (renewal) { sub(/[^0-9]+/, "", $2); printf("%s-%s-%s", $4, mon2moy($3), $2); exit}} - - # paid-till: 2017-12-10T12:42:36Z - /paid-till:/ && $NF ~ DATE_ISO_FULL {split($0, a, ":"); s = a[2]; if (split(s,d,/T/)) print d[1]; exit} - ' "$outfile" -} - -set_defaults +if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Parsing Arguments" >&2 +fi parse_arguments "$@" - -if [ -n "$cache_dir" ]; then - # we might consider whois server name in cache file - outfile=$cache_dir/$domain - - # clean up cache file if it's outdated - test -f "$outfile" && find "$outfile" -mtime "+$cache_age" -delete - - # run whois if cache is empty or missing - test -s "$outfile" || run_whois -else - outfile=$(temporary_file) - run_whois +if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Parsed arguments" >&2 fi -expiration=$(get_expiration "$outfile") -[ -z "$expiration" ] && die "$STATE_UNKNOWN" "State: UNKNOWN ; Unable to figure out expiration date for $domain Domain." +# Check dependencies +if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Checking dependencies" >&2 +fi +command -v curl >/dev/null || die "$STATE_UNKNOWN" "State: UNKNOWN ; curl dependency not found in PATH" +command -v mktemp >/dev/null || die "$STATE_UNKNOWN" "State: UNKNOWN ; mktemp dependency not found in PATH" +command -v date >/dev/null || die "$STATE_UNKNOWN" "State: UNKNOWN ; date dependency not found in PATH" +command -v whois >/dev/null || die "$STATE_UNKNOWN" "State: UNKNOWN ; whois dependency not found in PATH" +command -v grep >/dev/null || die "$STATE_UNKNOWN" "State: UNKNOWN ; grep dependency not found in PATH" +command -v jq >/dev/null || die "$STATE_UNKNOWN" "State: UNKNOWN ; jq dependency not found in PATH" +if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Dependencies checked" >&2 +fi -expseconds=$(date +%s --date="$expiration") -expdate=$(date +'%Y-%m-%d' --date="$expiration") -nowseconds=$(date +%s) -diffseconds=$((expseconds-nowseconds)) -expdays=$((diffseconds/86400)) +if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Reached main execution block after parse_arguments" >&2 +fi +if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Outfile set to: $outfile" >&2 +fi + +set +e +exec 3>&2 +expiration=$(get_rdap_expiration "$domain" 2>"$error_file") +rdap_rc=$? +exec 3>&- +set -e + +if [ $rdap_rc -ne 0 ]; then + echo "ERROR [$(date +'%H:%M:%S')]: get_rdap_expiration failed for $domain, exit code: $rdap_rc, error: $(cat "$error_file")" >&2 + expiration="" +fi + +if [ -z "$expiration" ]; then + if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Falling back to WHOIS for $domain" >&2 + fi + run_whois + expiration=$(get_expiration "$outfile") +fi + +if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Expiration date: $expiration" >&2 +fi + +# Validate expiration date format +if ! echo "$expiration" | grep -q -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'; then + die "$STATE_UNKNOWN" "State: UNKNOWN ; Invalid expiration date format: $expiration" +fi + +# Calculate seconds since epoch for expiration date at midnight UTC +expseconds=$(date -u +%s --date="$expiration 00:00:00 UTC" 2>/dev/null) || die "$STATE_UNKNOWN" "State: UNKNOWN ; Failed to parse expiration date: $expiration" +nowseconds=$(date -u +%s 2>/dev/null) || die "$STATE_UNKNOWN" "State: UNKNOWN ; Failed to get current time" +expdays=$(( (expseconds - nowseconds) / 86400 )) +expdate="$expiration" + +if [ "$debug" = "true" ]; then + echo "INFO [$(date +'%H:%M:%S')]: Time calculation: expseconds=$expseconds, nowseconds=$nowseconds, expdays=$expdays, expdate=$expdate" >&2 + echo "INFO [$(date +'%H:%M:%S')]: Days left: $expdays ; Exp date: $expdate" >&2 +fi -# Trigger alarms (if applicable) if the domain is not expired. if [ $expdays -ge 0 ]; then - [ $expdays -lt "$critical" ] && die "$STATE_CRITICAL" "State: CRITICAL ; Days left: $expdays ; Expire date: $expdate" - [ $expdays -lt "$warning" ] && die "$STATE_WARNING" "State: WARNING ; Days left: $expdays ; Expire date: $expdate" - - # No alarms? Ok, everything is right. - die "$STATE_OK" "State: OK ; Days left: $expdays ; Expire date: $expdate" + [ $expdays -le "$critical" ] && die "$STATE_CRITICAL" "State: CRITICAL ; Days left: $expdays ; Expire date: $expdate" + [ $expdays -le "$warning" ] && die "$STATE_WARNING" "State: WARNING ; Days left: $expdays ; Expire date: $expdate" + die "$STATE_OK" "State: OK ; Days left: $expdays ; Expire date: $expdate" fi +die "$STATE_CRITICAL" "State: CRITICAL ; Days since expired: ${expdays#-} ; Expire date: $expdate" -# Trigger alarms if applicable in the case that $warning and/or $critical are negative -[ $expdays -lt "$critical" ] && die "$STATE_CRITICAL" "State: EXPIRED ; Days since expired: ${expdays#-} ; Expire date: $expdate" -[ $expdays -lt "$warning" ] && die "$STATE_WARNING" "State: EXPIRED ; Days since expired: ${expdays#-} ; Expire date: $expdate" -# No alarms? Ok, everything is right. -die "$STATE_OK" "State: EXPIRED ; Days since expired: ${expdays#-} ; Expire date: $expdate" +exit 0 \ No newline at end of file diff --git a/zbx_domain_expiry.yaml b/zbx_domain_expiry.yaml index 1a89ce6..d2fc22e 100644 --- a/zbx_domain_expiry.yaml +++ b/zbx_domain_expiry.yaml @@ -1,89 +1,98 @@ zabbix_export: version: '6.4' + template_groups: + - uuid: 7df96b18c230490a9a0a9e2307226338 + name: Templates templates: - - uuid: d2df9ca51f754af8aff5d50f96c656e3 + - uuid: 644e89db964346f3b1ecd249d2d57876 template: 'Domain Expiry' name: 'Domain Expiry' - description: 'Template downloaded from: https://github.com/a-stoyanov/zabbix-domain-expiry' + description: | + Template version: 2.0.0 + Downloaded from: https://github.com/a-stoyanov/zabbix-domain-expiry vendor: name: github.com/a-stoyanov version: 6.4-0 groups: - - name: Custom/Domain + - name: Templates items: - - uuid: 24dcc49a7c384aaeb3dd8846b3401986 - name: 'Domain Check Expiry' - type: EXTERNAL - key: 'check_domain.sh["-d",{HOST.NAME},"-s",{$WHOIS_SERVER},"-w",{$EXP_WARN},"-c",{$EXP_CRIT}]' - delay: 1d - history: '0' - trends: '0' - value_type: TEXT - - uuid: 78a9ed45942a482f88d60acd4a24538d + - uuid: 7446af7e8dce480690f2ebd72a843953 name: 'Days Left' type: DEPENDENT - key: domain_check_expiry.days_left + key: check_domain.days_left delay: '0' + value_type: FLOAT preprocessing: - - type: REGEX + - type: JSONPATH parameters: - - 'Days left: (\d+)' - - \1 + - $.days_left error_handler: CUSTOM_VALUE error_handler_params: '0' master_item: - key: 'check_domain.sh["-d",{HOST.NAME},"-s",{$WHOIS_SERVER},"-w",{$EXP_WARN},"-c",{$EXP_CRIT}]' - - uuid: bccdb32210514ba1801f3c9a50c2b737 + key: 'check_domain.sh["-d",{HOST.HOST},"-r",{$RDAP_SERVER},"-s",{$WHOIS_SERVER},"-w",{$EXP_WARN},"-c",{$EXP_CRIT}]' + - uuid: 5a55bf6bf9b8490bb058982428572485 name: 'Days Since Expired' type: DEPENDENT - key: domain_check_expiry.days_since_expired + key: check_domain.days_since_expired delay: '0' + value_type: FLOAT preprocessing: - - type: REGEX + - type: JSONPATH parameters: - - 'Days since expired: (\d+)' - - \1 + - $.days_since_expired error_handler: CUSTOM_VALUE error_handler_params: '0' master_item: - key: 'check_domain.sh["-d",{HOST.NAME},"-s",{$WHOIS_SERVER},"-w",{$EXP_WARN},"-c",{$EXP_CRIT}]' - - uuid: c6c00c3717d84bf291420b7913b1133d + key: 'check_domain.sh["-d",{HOST.HOST},"-r",{$RDAP_SERVER},"-s",{$WHOIS_SERVER},"-w",{$EXP_WARN},"-c",{$EXP_CRIT}]' + - uuid: 0e104f2540204ac686072bd6cec91a86 name: 'Expire Date' type: DEPENDENT - key: domain_check_expiry.expire_date + key: check_domain.expire_date delay: '0' - trends: '0' value_type: TEXT + trends: '0' preprocessing: - - type: REGEX + - type: JSONPATH parameters: - - 'Expire date: (.+)' - - \1 + - $.expire_date error_handler: CUSTOM_VALUE error_handler_params: '0' master_item: - key: 'check_domain.sh["-d",{HOST.NAME},"-s",{$WHOIS_SERVER},"-w",{$EXP_WARN},"-c",{$EXP_CRIT}]' - - uuid: 67828b062ef74c0f8e866a9fbfb78d27 - name: Status + key: 'check_domain.sh["-d",{HOST.HOST},"-r",{$RDAP_SERVER},"-s",{$WHOIS_SERVER},"-w",{$EXP_WARN},"-c",{$EXP_CRIT}]' + - uuid: 99f25b8648814596a72f66f3e3db01cf + name: Message type: DEPENDENT - key: domain_check_expiry.status + key: check_domain.message delay: '0' - trends: '0' value_type: TEXT + trends: '0' preprocessing: - - type: REGEX + - type: JSONPATH parameters: - - 'State: (.+)' - - \1 + - $.message master_item: - key: 'check_domain.sh["-d",{HOST.NAME},"-s",{$WHOIS_SERVER},"-w",{$EXP_WARN},"-c",{$EXP_CRIT}]' - triggers: - - uuid: 2a7fb4f347ce4c5aa0617f08a0b339f0 - expression: 'find(/Domain Expiry/domain_check_expiry.status,#1,"like","UNKNOWN")=1' - name: 'Domain Expiry: {HOST.NAME} - {ITEM.LASTVALUE1}' - tags: - - tag: domain - value: status + key: 'check_domain.sh["-d",{HOST.HOST},"-r",{$RDAP_SERVER},"-s",{$WHOIS_SERVER},"-w",{$EXP_WARN},"-c",{$EXP_CRIT}]' + - uuid: 48baf9c076d1453d8c7272dffa014fd9 + name: 'Check Domain' + type: EXTERNAL + key: 'check_domain.sh["-d",{HOST.HOST},"-r",{$RDAP_SERVER},"-s",{$WHOIS_SERVER},"-w",{$EXP_WARN},"-c",{$EXP_CRIT}]' + delay: 1d + history: '0' + value_type: TEXT + trends: '0' + - uuid: ceb24ded619c4d34b8394a50d700bf52 + name: State + type: DEPENDENT + key: check_domain.state + delay: '0' + value_type: TEXT + trends: '0' + preprocessing: + - type: JSONPATH + parameters: + - $.state + master_item: + key: 'check_domain.sh["-d",{HOST.HOST},"-r",{$RDAP_SERVER},"-s",{$WHOIS_SERVER},"-w",{$EXP_WARN},"-c",{$EXP_CRIT}]' tags: - tag: domain value: registration @@ -94,59 +103,82 @@ zabbix_export: - macro: '{$EXP_WARN}' value: '30' description: 'Threshold value of days remaining before triggering a WARNING alert' + - macro: '{$RDAP_SERVER}' + description: 'Specify which RDAP server to use. Default empty value will use IANA lookup' - macro: '{$WHOIS_SERVER}' - value: '""' - description: 'Used to specify which whois service to use. Default value "" uses the whois util config default' + description: 'Specify which WHOIS server to use. Default empty value will use the whois utility config (rfc-3912 lookup)' triggers: - - uuid: acf67a4bfd9649c9a96c63cafa812e9b + - uuid: 126653ca5057472c9499f5bcf74b17ba expression: | - find(/Domain Expiry/domain_check_expiry.status,#1,"like","EXPIRED")=1 + last(/Domain Expiry/check_domain.state)="UNKNOWN" and - last(/Domain Expiry/domain_check_expiry.days_since_expired)>=0 + last(/Domain Expiry/check_domain.message)<>0 + name: 'Domain Expiry: {HOST.HOST} - {ITEM.LASTVALUE2}' + description: 'Raise alert in case script is unable to determine the expiration status for the domain (state = unknown)' + tags: + - tag: domain + value: status + - uuid: 71b376de644848379c14ef181e12245a + expression: | + last(/Domain Expiry/check_domain.state)="CRITICAL" and - last(/Domain Expiry/domain_check_expiry.expire_date)<>0 - name: 'Domain Expiry: {HOST.NAME} has expired' + last(/Domain Expiry/check_domain.days_since_expired)>0 + and + last(/Domain Expiry/check_domain.expire_date)<>0 + name: 'Domain Expiry: {HOST.HOST} has expired' opdata: '{ITEM.LASTVALUE2} days ago on {ITEM.LASTVALUE3}' priority: DISASTER + description: 'Raise alert if domain registration has expired.' dependencies: - - name: 'Domain Expiry: {HOST.NAME} - {ITEM.LASTVALUE1}' - expression: 'find(/Domain Expiry/domain_check_expiry.status,#1,"like","UNKNOWN")=1' + - name: 'Domain Expiry: {HOST.HOST} - {ITEM.LASTVALUE2}' + expression: | + last(/Domain Expiry/check_domain.state)="UNKNOWN" + and + last(/Domain Expiry/check_domain.message)<>0 tags: - tag: domain value: expired - - uuid: 7664fe7d988e4bcca9dd1518e42abf79 + - uuid: 5f767265e8714f1db4e969d4f7f95448 expression: | - last(/Domain Expiry/domain_check_expiry.days_left)<={$EXP_CRIT} + last(/Domain Expiry/check_domain.state)="CRITICAL" and - last(/Domain Expiry/domain_check_expiry.expire_date)<>0 - name: 'Domain Expiry: {HOST.NAME} will expire soon' - opdata: 'in {ITEM.LASTVALUE1} days on {ITEM.LASTVALUE2}' + last(/Domain Expiry/check_domain.days_left)<={$EXP_CRIT} + and + last(/Domain Expiry/check_domain.expire_date)<>0 + name: 'Domain Expiry: {HOST.HOST} will expire soon' + opdata: 'in {ITEM.LASTVALUE2} days on {ITEM.LASTVALUE3}' priority: HIGH + description: 'Raise alert if number of days remaining before expiry is below critical threshold ({$EXP_CRIT}).' dependencies: - - name: 'Domain Expiry: {HOST.NAME} has expired' + - name: 'Domain Expiry: {HOST.HOST} has expired' expression: | - find(/Domain Expiry/domain_check_expiry.status,#1,"like","EXPIRED")=1 + last(/Domain Expiry/check_domain.state)="CRITICAL" and - last(/Domain Expiry/domain_check_expiry.days_since_expired)>=0 + last(/Domain Expiry/check_domain.days_since_expired)>0 and - last(/Domain Expiry/domain_check_expiry.expire_date)<>0 + last(/Domain Expiry/check_domain.expire_date)<>0 tags: - tag: domain value: expiry - - uuid: ff152c25e3114727ad0a34ccd2a1640e + - uuid: 68e36957e56b4d95860b6a8c131a9e1b expression: | - last(/Domain Expiry/domain_check_expiry.days_left)<={$EXP_WARN} + last(/Domain Expiry/check_domain.state)="WARNING" and - last(/Domain Expiry/domain_check_expiry.expire_date)<>0 - name: 'Domain Expiry: {HOST.NAME} will expire soon' - opdata: 'in {ITEM.LASTVALUE1} days on {ITEM.LASTVALUE2}' + last(/Domain Expiry/check_domain.days_left)<={$EXP_WARN} + and + last(/Domain Expiry/check_domain.expire_date)<>0 + name: 'Domain Expiry: {HOST.HOST} will expire soon' + opdata: 'in {ITEM.LASTVALUE2} days on {ITEM.LASTVALUE3}' priority: WARNING + description: 'Raise alert if number of days remaining before expiry is below warning threshold ({$EXP_WARN}).' dependencies: - - name: 'Domain Expiry: {HOST.NAME} will expire soon' + - name: 'Domain Expiry: {HOST.HOST} will expire soon' expression: | - last(/Domain Expiry/domain_check_expiry.days_left)<={$EXP_CRIT} + last(/Domain Expiry/check_domain.state)="CRITICAL" and - last(/Domain Expiry/domain_check_expiry.expire_date)<>0 + last(/Domain Expiry/check_domain.days_left)<={$EXP_CRIT} + and + last(/Domain Expiry/check_domain.expire_date)<>0 tags: - tag: domain value: expiry