#!/usr/bin/env bash

set -Eeuo pipefail

readonly TARGET_DIR="/usr/local/share/ca-certificates/SLU"
readonly SOURCE_URL="https://pki.slu.se/"
readonly LOCK_FILE="/run/lock/install-slu-certs.lock"
readonly STATE_DIR="/usr/local/share/.install-slu-certs"
readonly TRANSACTION_DIR="${STATE_DIR}/transaction"

DOWNLOAD_DIR=""
LOCK_ACQUIRED=0
umask 077

log() {
    printf '%s\n' "$*"
}

fail() {
    printf 'ERROR: %s\n' "$*" >&2
    exit 1
}

check_target() {
    if [[ -L "${TARGET_DIR}" || ( -e "${TARGET_DIR}" && ! -d "${TARGET_DIR}" ) ]]; then
        printf 'ERROR: Target must be a directory, not a symlink: %s\n' "${TARGET_DIR}" >&2
        return 1
    fi
}

recover_transaction() {
    [[ -d "${TRANSACTION_DIR}" ]] || return 0

    if [[ -e "${TRANSACTION_DIR}/committed" || ! -e "${TRANSACTION_DIR}/installing" ]]; then
        rm -rf -- "${TRANSACTION_DIR}" || return 1
        return 0
    fi

    log "Recovering the previous SLU certificate state"
    check_target || return 1
    if [[ -e "${TRANSACTION_DIR}/previous-present" ]]; then
        if [[ -d "${TRANSACTION_DIR}/previous" ]]; then
            rm -rf -- "${TARGET_DIR}" || return 1
            mv -T -- "${TRANSACTION_DIR}/previous" "${TARGET_DIR}" || return 1
        elif [[ ! -d "${TARGET_DIR}" ]]; then
            printf 'ERROR: Both the previous and live certificate directories are missing.\n' >&2
            return 1
        fi
    else
        rm -rf -- "${TARGET_DIR}" || return 1
    fi

    sync -f "${STATE_DIR}" || return 1
    timeout --signal=TERM --kill-after=10s 2m update-ca-certificates --fresh || return 1
    sync || return 1
    rm -rf -- "${TRANSACTION_DIR}" || return 1
}

cleanup() {
    local exit_code=$?
    trap - EXIT
    trap '' INT TERM
    set +e

    if (( LOCK_ACQUIRED )) && ! recover_transaction; then
        printf 'ERROR: Recovery failed; preserve %s and rerun with --recover-only.\n' "${TRANSACTION_DIR}" >&2
        if (( exit_code == 0 )); then exit_code=1; fi
    fi

    if [[ -n "${DOWNLOAD_DIR}" ]] && ! rm -rf -- "${DOWNLOAD_DIR}"; then
        printf 'ERROR: Failed to remove download directory: %s\n' "${DOWNLOAD_DIR}" >&2
        if (( exit_code == 0 )); then exit_code=1; fi
    fi

    exit "${exit_code}"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM

if (( EUID != 0 )); then
    fail "This script must be run as root (for example: sudo $0)."
fi

if (( BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4) )); then
    fail "Bash 4.4 or newer is required."
fi

if (( $# > 1 )) || [[ "${1:-}" != "" && "${1:-}" != --recover-only ]]; then
    fail "Usage: $0 [--recover-only]"
fi

for command_name in wget openssl update-ca-certificates find install mktemp flock timeout awk stat mv rm chmod sync cat; do
    command -v "${command_name}" >/dev/null 2>&1 \
        || fail "Required command not found: ${command_name}"
done

# Prevent two instances from modifying the trust store at the same time.
exec 9>"${LOCK_FILE}"
flock -n 9 || fail "Another instance of this script is already running."

if [[ -L "${STATE_DIR}" || ( -e "${STATE_DIR}" && ! -d "${STATE_DIR}" ) ]]; then
    fail "Recovery state must be a directory, not a symlink: ${STATE_DIR}"
fi
if [[ -e "${STATE_DIR}" && "$(stat -c %u -- "${STATE_DIR}")" != 0 ]]; then
    fail "Recovery state must be owned by root: ${STATE_DIR}"
fi
install -d -m 0700 "${STATE_DIR}"
if [[ -L "${TRANSACTION_DIR}" || ( -e "${TRANSACTION_DIR}" && ! -d "${TRANSACTION_DIR}" ) ]]; then
    fail "Invalid recovery transaction directory: ${TRANSACTION_DIR}"
fi
LOCK_ACQUIRED=1
check_target || fail "Unsafe target directory."
recover_transaction || fail "Cannot recover the previous transaction; preserve ${TRANSACTION_DIR}."

if [[ "${1:-}" == --recover-only ]]; then
    log "Recovery completed successfully."
    exit 0
fi

DOWNLOAD_DIR="$(mktemp -d)"

log "Downloading CA certificates from ${SOURCE_URL}"
if ! timeout --signal=TERM --kill-after=10s 5m wget \
    --output-file="${DOWNLOAD_DIR}/wget.log" \
    --recursive \
    --level=1 \
    --no-parent \
    --no-directories \
    --accept='*.crt' \
    --reject='index.html*' \
    --https-only \
    --timeout=15 \
    --tries=3 \
    --directory-prefix="${DOWNLOAD_DIR}" \
    "${SOURCE_URL}"; then
    if [[ -s "${DOWNLOAD_DIR}/wget.log" ]]; then
        printf 'ERROR: Wget failed; details follow:\n' >&2
        cat -- "${DOWNLOAD_DIR}/wget.log" >&2
    fi
    fail "Failed to download certificates from ${SOURCE_URL}"
fi

if ! find "${DOWNLOAD_DIR}" -maxdepth 1 -type f -name '*.crt' -print0 \
    >"${DOWNLOAD_DIR}/certificate-list"; then
    fail "Failed to enumerate downloaded certificates."
fi
mapfile -d '' CERTIFICATES <"${DOWNLOAD_DIR}/certificate-list"

if (( ${#CERTIFICATES[@]} == 0 )); then
    fail "No .crt files were downloaded from ${SOURCE_URL}"
fi

TARGET_PARENT="${TARGET_DIR%/*}"
readonly TARGET_PARENT
install -d -m 0755 "${TARGET_PARENT}"
legacy_directory="$(find "${TARGET_PARENT}" -maxdepth 1 \
    \( -name '.SLU.backup.*' -o -name '.SLU.new.*' \) -print -quit)"
[[ -z "${legacy_directory}" ]] \
    || fail "Legacy SLU staging or backup exists in the trust tree; move it outside the tree before continuing."
[[ "$(stat -c %d -- "${STATE_DIR}")" == "$(stat -c %d -- "${TARGET_PARENT}")" ]] \
    || fail "Recovery state and target must be on the same filesystem."
if [[ -d "${TARGET_DIR}" ]]; then
    [[ "$(stat -c %d -- "${TARGET_DIR}")" == "$(stat -c %d -- "${STATE_DIR}")" ]] \
        || fail "Target must not be a separate filesystem."
fi
install -d -m 0700 "${TRANSACTION_DIR}"
install -d -m 0755 "${TRANSACTION_DIR}/new"

log "Checking PEM format for ${#CERTIFICATES[@]} downloaded certificate(s)"
for certificate in "${CERTIFICATES[@]}"; do
    if ! LC_ALL=C awk '
        { sub(/\r$/, "") }
        $0 == "-----BEGIN CERTIFICATE-----" {
            if (state != 0) exit 1
            state = 1
            next
        }
        $0 == "-----END CERTIFICATE-----" {
            if (state != 1) exit 1
            state = 2
            next
        }
        state == 1 { if ($0 !~ /^[A-Za-z0-9+\/=]+$/) exit 1; next }
        $0 !~ /^[ \t]*$/ { exit 1 }
        END { if (state != 2) exit 1 }
    ' "${certificate}"; then
        fail "Download must contain exactly one PEM certificate and no other data."
    fi

    openssl x509 -inform PEM -in "${certificate}" -outform PEM \
        -out "${TRANSACTION_DIR}/new/${certificate##*/}"
    chmod 0644 "${TRANSACTION_DIR}/new/${certificate##*/}"
done

check_target || fail "Unsafe target directory."
if [[ -d "${TARGET_DIR}" ]]; then
    : >"${TRANSACTION_DIR}/previous-present"
fi
: >"${TRANSACTION_DIR}/installing"
sync -f "${STATE_DIR}"

if [[ -e "${TRANSACTION_DIR}/previous-present" ]]; then
    mv -T -- "${TARGET_DIR}" "${TRANSACTION_DIR}/previous"
fi
mv -T -- "${TRANSACTION_DIR}/new" "${TARGET_DIR}"
sync -f "${STATE_DIR}"

log "Rebuilding system CA links; 'Replacing debian:...' messages are expected and refer to regenerated links."
if ! timeout --signal=TERM --kill-after=10s 2m update-ca-certificates --fresh; then
    fail "Trust-store update failed; attempting recovery."
fi
sync
: >"${TRANSACTION_DIR}/committed"
sync -f "${STATE_DIR}"
rm -rf -- "${TRANSACTION_DIR}"

log "Installed ${#CERTIFICATES[@]} SLU CA certificate(s) successfully."
