#!/bin/sh
set +x
set -eu
PATH=/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/sbin:/usr/local/bin
LC_ALL=C
export PATH LC_ALL
unset ENV BASH_ENV CDPATH PERL5OPT PERL5LIB PYTHONPATH PYTHONHOME PYTHONSTARTUP PYTHONINSPECT
unset http_proxy https_proxy ftp_proxy all_proxy http_cookies HTTP_PROXY HTTPS_PROXY FTP_PROXY ALL_PROXY
unset SSL_CERT_FILE SSL_CERT_DIR CURL_CA_BUNDLE REQUESTS_CA_BUNDLE
umask 077

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

[ "$#" -eq 2 ] && [ "$1" = --server ] || fail 'usage: install.sh --server https://server.example.org'
server=$2
printf '%s\n' "$server" | awk '
    NR != 1 { exit 1 }
    !/^https:\/\/[A-Za-z0-9.-]+(:[0-9]+)?\/?$/ { exit 1 }
    {
        sub(/^https:\/\//, ""); sub(/\/$/, "")
        n = split($0, endpoint, ":")
        if (n == 2 && (length(endpoint[2]) > 5 || endpoint[2] + 0 < 1 || endpoint[2] + 0 > 65535)) exit 1
        if (length(endpoint[1]) > 253 || index(endpoint[1], ".") == 0) exit 1
        n = split(endpoint[1], labels, ".")
        for (i = 1; i <= n; i++) {
            if (length(labels[i]) < 1 || length(labels[i]) > 63 ||
                labels[i] !~ /^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?$/) exit 1
        }
    }
' || fail 'invalid server URL: require HTTPS and a DNS hostname with optional port'
[ "$(id -u)" -eq 0 ] || fail 'run as root'
bootstrap=/etc/patchouli-bootstrap.key
config=/etc/patchouli-reporter.conf
reporter=/usr/local/sbin/patchouli-reporter
assets=https://ftp.cuandu.org/pub/cuandu/patchouli
stage=
created_config=no
created_reporter=no
created_state=no
created_service=no
created_timer=no
registration_attempted=no
cleanup() {
    status=$?
    trap - EXIT HUP INT TERM
    if [ "$registration_attempted" = no ]; then
        [ "$created_config" = no ] || rm -f "$config"
        [ "$created_reporter" = no ] || rm -f "$reporter"
        [ "$created_service" = no ] || rm -f /etc/systemd/system/patchouli-reporter.service
        [ "$created_timer" = no ] || rm -f /etc/systemd/system/patchouli-reporter.timer
        [ "$created_state" = no ] || rm -rf "$state"
    elif [ "$status" -ne 0 ]; then
        printf '%s\n' \
            'patchouli: local registration state is retained; verify the server and repair the config and schedule manually' >&2
    fi
    [ -z "$stage" ] || rm -rf "$stage"
    exit "$status"
}
trap cleanup EXIT
trap 'exit 1' HUP INT TERM

os=$(uname -s)
case "$os" in
OpenBSD)
    command -v perl >/dev/null || fail 'Perl is required'
    perl -MFcntl=:DEFAULT,:mode -MJSON::PP -e '
        my $p = shift;
        my @s = lstat $p;
        die "patchouli: unsafe bootstrap file\n" unless @s && S_ISREG($s[2]) && $s[4] == 0 && ($s[2] & 07777) == 0600;
        sysopen(my $f, $p, O_RDONLY | O_NOFOLLOW | O_NONBLOCK) or die "patchouli: cannot open bootstrap file\n";
        @s = stat $f;
        die "patchouli: unsafe bootstrap file\n" unless S_ISREG($s[2]) && $s[4] == 0 && ($s[2] & 07777) == 0600;
        my $n = read($f, my $key, 258);
        die "patchouli: invalid bootstrap key\n" unless defined($n) && $key =~ /\A[A-Za-z0-9_-]{20,256}\n?\z/;
    ' "$bootstrap" || fail 'bootstrap validation failed'
    state=/var/db/patchouli-reporter
    asset=patchouli-reporter
    tools='ftp perl nc sysctl syspatch pkg_info ntpctl machine what crontab install mktemp hostname'
    host=$(hostname)
    ;;
Linux)
    command -v python3 >/dev/null || fail 'Python 3 is required'
    python3 -I -c '
import os, re, stat, sys
try:
    path = sys.argv[1]
    info = os.lstat(path)
    if not stat.S_ISREG(info.st_mode) or info.st_uid != 0 or stat.S_IMODE(info.st_mode) != 0o600:
        raise ValueError()
    fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
    with os.fdopen(fd, "rb") as handle:
        info = os.fstat(handle.fileno())
        if not stat.S_ISREG(info.st_mode) or info.st_uid != 0 or stat.S_IMODE(info.st_mode) != 0o600:
            raise ValueError()
        if not re.fullmatch(rb"[A-Za-z0-9_-]{20,256}\n?", handle.read(258)):
            raise ValueError()
except (OSError, ValueError):
    sys.exit("patchouli: missing or unsafe bootstrap file or invalid key")
' "$bootstrap" || fail 'bootstrap validation failed'
    distro=$(python3 -I -c '
import re, sys
with open("/etc/os-release", encoding="utf-8") as f:
    data = f.read(8192)
m = re.search(r"^ID=[\"\x27]?([a-z0-9_-]+)[\"\x27]?$", data, re.M)
if not m:
    sys.exit("patchouli: missing distribution ID")
print(m[1])
') || fail 'cannot identify Linux distribution'
    case "$distro" in
        debian|ubuntu) packages=dpkg-query ;;
        rocky|almalinux|cloudlinux) packages=rpm ;;
        *) fail 'unsupported Linux distribution' ;;
    esac
    state=/var/lib/patchouli-reporter
    asset=patchouli-reporter-linux
    tools="curl python3 systemctl install mktemp hostname $packages"
    host=$(hostname -f)
    ;;
*) fail 'unsupported operating system' ;;
esac
for tool in $tools; do
    command -v "$tool" >/dev/null || fail "required command unavailable: $tool"
done
[ -d /usr/local/sbin ] && [ ! -L /usr/local/sbin ] || fail 'missing or symlinked /usr/local/sbin'
if [ "$os" = Linux ]; then
    python3 -I -c 'import ssl, json, fcntl, urllib.request, selectors, subprocess' || fail 'Python runtime modules unavailable'
    [ -d /etc/systemd/system ] && [ ! -L /etc/systemd/system ] || fail 'missing or symlinked systemd unit directory'
    systemctl show --property=Version --value >/dev/null || fail 'systemd manager unavailable'
fi
host=$(printf '%s' "$host" | tr '[:upper:]' '[:lower:]')
host=${host%.}
printf '%s\n' "$host" | awk '
    length($0) > 253 || index($0, ".") == 0 { exit 1 }
    { n = split($0, labels, "."); for (i = 1; i <= n; i++) {
        if (length(labels[i]) < 1 || length(labels[i]) > 63 ||
            labels[i] !~ /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/) exit 1
    } }
' || fail 'set a valid host FQDN before installation'
for path in "$config" "$reporter" \
    /var/db/patchouli-reporter /var/lib/patchouli-reporter \
    /etc/systemd/system/patchouli-reporter.service \
    /etc/systemd/system/patchouli-reporter.timer \
    /usr/lib/systemd/system/patchouli-reporter.service \
    /usr/lib/systemd/system/patchouli-reporter.timer \
    /lib/systemd/system/patchouli-reporter.service \
    /lib/systemd/system/patchouli-reporter.timer \
    /etc/systemd/system/timers.target.wants/patchouli-reporter.timer; do
    [ ! -e "$path" ] && [ ! -L "$path" ] || fail "existing installation path: $path"
done
stage=$(mktemp -d /tmp/patchouli-install.XXXXXXXX)
if command -v crontab >/dev/null; then
    if ! crontab -l > "$stage/crontab" 2> "$stage/crontab.err"; then
        grep -q 'no crontab for root' "$stage/crontab.err" || fail 'cannot read root crontab'
        : > "$stage/crontab"
    fi
    if grep -q 'patchouli-reporter' "$stage/crontab"; then
        fail 'existing reporter cron schedule'
    fi
fi
fetch() {
    if [ "$os" = OpenBSD ]; then
        ftp -v -w 30 -o "$stage/$1" "$assets/$1" > "$stage/fetch.log" 2>&1 || fail "download failed: $1"
        if grep -q 'Redirected to ' "$stage/fetch.log"; then
            fail "download redirect refused: $1"
        fi
    else
        code=$(curl -q --fail --silent --show-error --proto '=https' \
            --max-redirs 0 --connect-timeout 15 --max-time 60 \
            --write-out '%{http_code}' -o "$stage/$1" "$assets/$1") \
            || fail "download failed: $1"
        [ "$code" = 200 ] || fail "unexpected download status: $1"
    fi
    [ -s "$stage/$1" ] || fail "empty download: $1"
}
fetch "$asset"
if [ "$os" = OpenBSD ]; then
    perl -c "$stage/$asset" >/dev/null || fail 'invalid reporter program'
else
    python3 -I -c 'import sys; compile(open(sys.argv[1], "rb").read(), sys.argv[1], "exec")' "$stage/$asset" || fail 'invalid reporter program'
fi
if [ "$os" = Linux ]; then
    fetch patchouli-reporter.service
    fetch patchouli-reporter.timer
fi
mkdir -m 0700 "$state"
created_state=yes
created_reporter=yes
install -m 0755 "$stage/$asset" "$reporter"
created_config=yes
(set -C; printf 'server=%s\nfqdn=%s\nstate_dir=%s\nbootstrap_file=%s\n' \
    "$server" "$host" "$state" "$bootstrap" > "$config")
registration_attempted=yes
if ! "$reporter" --register; then
    fail 'registration failed; the server may have accepted it; verify server registrations before retrying'
fi
rm -f "$bootstrap"
printf 'server=%s\nfqdn=%s\nstate_dir=%s\n' "$server" "$host" "$state" > "$config"
"$reporter" || fail 'first report failed'
if [ "$os" = OpenBSD ]; then
    printf '*/15 * * * * /usr/local/sbin/patchouli-reporter --jitter\n' >> "$stage/crontab"
    crontab "$stage/crontab" || fail 'cannot install root cron schedule'
else
    install -m 0644 "$stage/patchouli-reporter.service" /etc/systemd/system/patchouli-reporter.service
    created_service=yes
    install -m 0644 "$stage/patchouli-reporter.timer" /etc/systemd/system/patchouli-reporter.timer
    created_timer=yes
    systemctl daemon-reload || fail 'cannot reload systemd'
    systemctl enable --now patchouli-reporter.timer || fail 'cannot enable reporter timer'
fi
printf 'patchouli: registered %s and enabled reporting\n' "$host"
