#!/bin/sh

# This software originates from Freifunk Berlin and implements a basic autoupdate mechanism
# by using OpenWrts built-in sysupgrade.
# It is licensed under GNU General Public License v3.0 or later
# Copyright (C) 2022   Martin Hübner and Tobias Schwarz

# shellcheck shell=dash

# except than noted, this script is not posix-compliant in one way: we use "local"
# variables definition. As nearly all shells out there implement local, this should
# work anyway. This is a little reminder to you, if you use some rare shell without
# a builtin "local" statement.

# We don't need the return values and check the correct execution in other ways.
# shellcheck disable=SC2155

# we can't check those dependencies at the CI
# shellcheck source=/dev/null
. /lib/functions.sh
. /lib/config/uci.sh
. /etc/freifunk_release

print_help() {
    printf "\

Autoupdate: Tool for updating Freifunk-Berlin-Firmware automatically

Mostly you should call this programm without any options. If you specify
options on the command line, they will superseed the options from the
configuration file.

Optional arguments:
    -h: show this help text
    -i: ignore certs
            Don't prove the images origin by checking the certificates.
    -m INT: minimum certs
            flash image, if it was signed by minimum amount of certs.
    -N: update now
            overrides the uptime check and performs update now.
    -n: new installation
            flash the image and wipe configuration. So you will start
            with a new wizard-run.
    -t: test-run
            this will perform everything like in automatic-mode, except
            that it won't flash the image and won't tidy up afterwards.
    -f: force update
            CAUTION: This will ignore all checks except the certificates
            and checksums!

Example call:
    autoupdate
\n"
}

##########################
#   Load Configuration   #
##########################

FW_URL=$(uci_get autoupdate cfg url)
MIN_CERTS=$(uci_get autoupdate cfg minimum_certs)
DISABLED=$(uci_get autoupdate cfg disabled)

PATH_DIR="/tmp/autoupdate"
PATH_BIN="$PATH_DIR/freifunk_syupgrade.bin"
export KEY_DIR="/etc/autoupdate/keys/"

MIN_RAM_FREE=1536 # amount of kiB that must be free in RAM after firmware-download

# load lib-autoupdate after configuration-load, to substitute global vars...
. /lib/autoupdate/lib_autoupdate.sh

#####################
#   Main Programm   #
#####################

#########################
#  Commandline parsing

while getopts him:Nntf option; do
    case $option in
        h)
            print_help
            exit 0
            ;;
        i) OPT_IGNORE_CERTS=1 ;;
        m) MIN_CERTS=$OPTARG ;;
        N) OPT_NOW=1 ;;
        n) OPT_N=1 ;;
        t) OPT_TESTRUN=1 ;;
        f) OPT_FORCE=1 ;;
        *)
            printf "\nUnknown argument! Please use valid arguments only.\n\n"
            print_help
            exit 2
            ;;
    esac
done

# sanitise min-certs-input
MIN_CERTS=$(echo "$MIN_CERTS" | sed -e 's|[^0-9]||g')
if [ -z "$MIN_CERTS" ]; then
    echo "please give numbers only for -m"
    exit 2
fi

log "starting autoupdate..."

#############################
#  Checks and Checks again

is_stable_release=$(echo "$FREIFUNK_RELEASE" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$')
if [ -z "$OPT_FORCE" ] && [ -z "$is_stable_release" ]; then
    log "automatic updates aren't supported for development-firmwares. Please update manually or use the force update option '-f' or new-install option '-n'."
    exit 2
fi

if [ -z "$OPT_FORCE" ] && { [ "$DISABLED" = "1" ] || [ "$DISABLED" = "yes" ] || [ "$DISABLED" = "true" ]; }; then
    log "autoupdate is disabled. Change the configs at /et/config/autoupdate to enable it."
    exit 2
fi

UPTIME=$(cut -d'.' -f1 </proc/uptime)
# only update, if router runs for at least two hours (so the update probably won't get disrupted)
if [ -z "$OPT_FORCE" ] && [ "$UPTIME" -lt 7200 ] && [ -z "$OPT_NOW" ]; then
    log "Router didn't run for two hours. It might be just plugged in for testing. Aborting..."
    exit 2
fi

# create tmp-dir
rm -rf "$PATH_DIR"
mkdir -p "$PATH_DIR"

log "fetching $FW_URL ..."
load_overview_and_certs "$FW_URL"
if [ $? != 0 ]; then
    log "fetching autoupdate.json failed. Probably no internet connection."
    exit 2
fi
log "done."

# prove to be signed by minimum amount of certs
if [ -z "$OPT_IGNORE_CERTS" ]; then
    log "Verifying image-signatures..."
    min_valid_certificates "$PATH_DIR/autoupdate.json" "$MIN_CERTS"
    ret_code=$?
    if [ $ret_code != 255 ]; then
        log "autoupdate.json was signed by $ret_code certificates only. At least $MIN_CERTS required."
        exit 2
    else
        log "autoupdate.json was signed by at least $MIN_CERTS certificates. Continuing..."
    fi
else
    log "ignoring certificates as requested."
fi

latest_release=$(read_latest_stable "$PATH_DIR/autoupdate.json")
if [ $? != 0 ]; then
    log "wasn't able to read latest stable version from autoupdate.json"
    exit 2
else
    log "latest release is $latest_release"
fi

##################
#  Update-stuff

if semverLT "$FREIFUNK_RELEASE" "$latest_release"; then

    flavour=$(get_firmware_flavour)
    log "router board is: $(board_name). firmware-flavour is: $flavour."
    if [ "$flavour" = "unknown" ]; then
        log "failed to determine the firmware-type of your installation. Please consider a manual update."
        exit 1
    fi

    log "parsing download-link and images hashsum (takes up to 40 seconds)..."
    link_and_hash=$(get_download_link_and_hash "$latest_release" "$flavour")
    log "done."

    link=$(echo "$link_and_hash" | cut -d' ' -f 1)
    hash_sum=$(echo "$link_and_hash" | cut -d' ' -f 2)

    log "download link is: $link."

    # delete json and signatures to save space in RAM
    if [ -z "$OPT_TESTRUN" ]; then
        json_sig_files=$(find "$PATH_DIR" -name "autoupdate.json*")
        for f in $json_sig_files; do
            rm "$f"
        done
    fi

    log "Try loading new firmware..."

    # check if the firmware-bin would fit into tmpfs
    request_file_size "$link"
    size=$?
    freemem=$(free | grep Mem | sed -e 's| \+| |g' | cut -d' ' -f 4)
    # only load the firmware, if there would be 1.5 MiB left in RAM
    if [ $((freemem - MIN_RAM_FREE)) -lt $size ]; then
        log "there is not enough ram on your device to download the image. You might free some memory by stopping some services before the update."
        exit 2
    fi

    # download image to /tmp/autoupdate
    wget -qO "$PATH_BIN" "$link"
    ret_code=$?
    if [ $ret_code != 0 ]; then
        log "failed! wget returned $ret_code."
        exit 2
    else
        log "done."
    fi

    # verify image to be correct
    verify_image_hash "$PATH_BIN" "$hash_sum"
    ret_code=$?
    if [ $ret_code != 0 ]; then
        log "The expected hash of the loaded image didn't match the real one."
        exit 2
    else
        log "Image hash is correct. sha256sum: $hash_sum"
    fi

    # The following emulates sysupgrade's option --ignore-minor-compat-version,
    # which isn't available until OpenWrt 23.05.
    # We need it to allow autoupdates for devices that received DSA support
    # and had their compat_version increased from 1.0 to 1.1.
    # In this case, we manually set 1.1 in the system config, check the image,
    # and then do the upgrade.
    # If anything goes wrong, we try to set compat_version back to 1.0.
    #
    # Once we've moved off OpenWrt 21.02 and 22.03, most of this can be removed
    # and we'll simply use the --ignore-minor-compat-version option.

    sysupargs=""
    [ -z "$OPT_N" ] || sysupargs="$sysupargs -n"

    compver_up=""
    compver_cur=""
    compmsg=""
    fwtool -i "$PATH_DIR/sysupgrade.json" "$PATH_BIN"
    json_init
    json_load_file "$PATH_DIR/sysupgrade.json"
    json_get_var "compver_up" "compat_version"
    json_get_var "compmsg" "compat_message"
    json_cleanup
    compver_cur="$(uci get system.@system[0].compat_version 2>/dev/null || echo "1.0")"
    compmsg2="$(echo "$compmsg" | fgrep DSA)"

    if [ "$compver_cur" == "1.0" ] && [ "$compver_up" == "1.1" ] && [ -n "$compmsg2" ]; then
        log "this is a DSA sysupgrade."
        uci set system.@system[0].compat_version=$compver_up
        sysupgrade -T "$PATH_BIN"
        ret_code=$?
        if [ $ret_code != 0 ]; then
            uci set system.@system[0].compat_version=$compver_cur
            log "sysupgrade -T failed."
            exit 2
        else
            log "sysupgrade -T succeeded."
        fi
        if [ -z "$OPT_TESTRUN" ]; then
            sysupargs="$sysupargs -F"
        else
            uci set system.@system[0].compat_version=$compver_cur
        fi
    fi
    if [ -z "$OPT_TESTRUN" ]; then
        log "start flashing the image..."
        echo "running sysupgrade $sysupargs $PATH_BIN"
        sysupgrade $sysupargs "$PATH_BIN"
        ret_code=$?
        if [ $ret_code != 0 ]; then
            uci set system.@system[0].compat_version=$compver_cur
        fi
    fi
else
    log "$FREIFUNK_RELEASE is the latest version. Nothing to do. I will recheck tomorrow."
fi
