#!/bin/bash
# Author: Christophe Casalegno / Brain 0verride
# Contact: brain@christophe-casalegno.com
# SxSerialCheck: Check & compare serial dns with dig
#
# Version 1.2
#
# Copyright (c) 2020 Christophe Casalegno
# 
# This program is free software: you can redistribute it and/or modify
#
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <https://www.gnu.org/licenses/>
#
# The license is available on this server here: 
# https://www.christophe-casalegno.com/licences/gpl-3.0.txt

domain=""
raw=0
ns_list=()
declare -A serials
ref_serial=""
ref_ns=""
all_ok=1

GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'

function parse_args()
{
    for arg in "$@"
    do
        case "$arg" in
            --raw)
                raw=1
                ;;
            *)
                if [ -z "$domain" ]
                then
                    domain="$arg"
                elif [ ${#ns_list[@]} -eq 0 ]
                then
                    IFS=',' read -r -a ns_list <<< "$arg"
                fi
                ;;
        esac
    done

    if [ -z "$domain" ]
    then
        echo "Usage: $0 domain [ns1,ns2,...] [--raw]"
        exit 1
    fi
}

function is_valid_fqdn()
{
    echo "$1" | grep -qE '^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$'
}

function is_valid_ipv4()
{
    echo "$1" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}$'
}

function is_valid_ipv6()
{
    echo "$1" | grep -qE '^[a-fA-F0-9:]+$'
}

function is_valid_ns()
{
    is_valid_fqdn "$1" || is_valid_ipv4 "$1" || is_valid_ipv6 "$1"
}

function get_serial()
{
    result=$(dig @"$2" "$1" SOA +norecurse +time=2 +tries=1 2>/dev/null)

    if [ $? -ne 0 ] || [ -z "$result" ]
    then
        echo "timeout"
        return 1
    fi

    echo "$result" | grep -qi "flags:.* aa[ ;]"
    if [ $? -ne 0 ]
    then
        echo "not-authoritative"
        return 1
    fi

    serial=$(echo "$result" | awk '/SOA/ && $1 ~ /^'"$1"'\.?$/ { print $7 }' | head -n 1)

    if [ -z "$serial" ]
    then
        echo "no-soa"
        return 1
    fi

    echo "$serial"
}

function format_serial()
{
    s="$1"
    echo "$s" | grep -qE '^[0-9]{10}$'
    if [ $? -eq 0 ] && [ "$raw" -eq 0 ]
    then
        printf "%s (%s)" "$s" "$(date -d "${s:0:8}" +"%Y-%m-%d")"
    else
        echo "$s"
    fi
}

function validate_domain()
{
    if ! is_valid_fqdn "$domain"
    then
        echo "Error: '$domain' is not a valid fully qualified domain name"
        exit 3
    fi
}

function retrieve_ns_list()
{
    if [ ${#ns_list[@]} -eq 0 ]
    then
        mapfile -t ns_list < <(dig +short NS "$domain" | sed 's/\.$//')
        if [ "${#ns_list[@]}" -lt 2 ]
        then
            echo "Error: could not retrieve at least two NS records for '$domain'"
            exit 2
        fi

        if [ "$raw" -eq 0 ]
        then
            echo "Autodetected nameservers:"
            for ns in "${ns_list[@]}"
            do
                echo " - $ns"
            done
            echo
        fi
    fi
}

function check_serials()
{
    for ns in "${ns_list[@]}"
    do
        if ! is_valid_ns "$ns"
        then
            serials["$ns"]="invalid"
            all_ok=0
            continue
        fi

        serial=$(get_serial "$domain" "$ns")
        case "$serial" in
            timeout|not-authoritative|no-soa)
                serials["$ns"]="$serial"
                all_ok=0
                continue
                ;;
        esac

        serials["$ns"]="$serial"

        if [ -z "$ref_serial" ]
        then
            ref_serial="$serial"
            ref_ns="$ns"
        elif [ "$serial" != "$ref_serial" ]
        then
            all_ok=0
        fi
    done
}

function print_results()
{
    if [ "$all_ok" -eq 1 ]
    then
        if [ "$raw" -eq 0 ]
        then
            echo -e "${GREEN}$domain: synchro ok${NC}"
        else
            echo "$domain: synchro ok"
        fi
    else
        if [ "$raw" -eq 0 ]
        then
            echo -e "${RED}$domain: Error${NC}"
        else
            echo "$domain: Error"
        fi
    fi

    for ns in "${!serials[@]}"
    do
        serial="${serials[$ns]}"
        case "$serial" in
            timeout)
                echo "$ns serial: timeout"
                ;;
            not-authoritative)
                echo "$ns serial: not authoritative"
                ;;
            no-soa)
                echo "$ns serial: no SOA found"
                ;;
            invalid)
                echo "$ns serial: invalid NS"
                ;;
            *)
                echo "$ns serial: $(format_serial "$serial")"
                ;;
        esac
    done
}

function main()
{
    parse_args "$@"
    validate_domain
    retrieve_ns_list
    check_serials
    print_results
}

main "$@"

