#!/usr/bin/env bash

# Handles indexing and SSH keys of machines I
# have access on

MACHINES_HOME=$HOME
MACHINES_CONFIG=$HOME/.config/machines
MACHINES_API=https://machines.frogeye.fr

mkdir -p "$MACHINES_HOME" &> /dev/null
mkdir -p "$MACHINES_CONFIG" &> /dev/null

# COMMON

function prompt { # text
    while true
    do
        read -r -p "$1 [yn] " yn
        case $yn in
            [Yy]* ) return 1;;
            [Nn]* ) return 0;;
            * ) echo "Please answer y or n.";;
        esac
    done
}

# From https://gist.github.com/cdown/1163649

urlencode() { # string
    old_lc_collate=$LC_COLLATE
    LC_COLLATE=C
    local length="${#1}"
    for (( i = 0; i < length; i++ )); do
        local c="${1:i:1}"
        case $c in
            [a-zA-Z0-9.~_-]) printf "%s" "$c" ;;
            *) printf '%%%02X' "'$c" ;;
        esac
    done
    LC_COLLATE=$old_lc_collate
}

urldecode() { # string
    local url_encoded="${1//+/ }"
    printf '%b' "${url_encoded//%/\\x}"
}

# API ACCESS

function _machines-api {
    route=$1
    shift
    temp=$(mktemp)
    wget "$MACHINES_API/$route" --content-on-error --quiet --output-document=$temp "$@"
    result=$?
    if [ $result != 0 ]; then
        echo "[ERROR] wget returned $result for route $route" 1>&2;
        cat $temp 1>&2;
        rm $temp
        exit 2
    fi
    cat $temp
    rm $temp
}

function _machines-apiToken {
    read -r -p 'TOTP token: ' token
    _machines-api "$@" --header="X-TOTP: $token"
}

function _machines-apiSigned {
    _machines-ensureAdmin
    _machines-api "$@" --certificate="$MACHINES_CONFIG/machines.crt" --private-key="$MACHINES_CONFIG/machines.key"
}


# APPLICATION KEYS & CERTIFICATE

function _machines-pubFromCrt {
    openssl x509 -in "$MACHINES_CONFIG/machines.crt" -pubkey -noout > "$MACHINES_CONFIG/machines.pub"
}

function _machines-verifyCertificate {
    return
    if openssl verify "$MACHINES_CONFIG/machines.crt" | grep -v 'error 18' | grep 'error' --quiet; then
        echo "[ERROR] Invalid certificate" 1>&2;
        exit 1
    fi
}

function _machines-ensurePub {
    if [ ! -f "$MACHINES_CONFIG/machines.crt" ]; then
        CERT_FILE=$(mktemp)
        echo "[INFO] Downloading certificate..."
        _machines-api cert > "$CERT_FILE"
        openssl x509 -fingerprint -in "$CERT_FILE" | grep Fingerprint --color=never
        prompt "Is this correct ?"
        if [ $? == 1 ]; then
            mv "$CERT_FILE" "$MACHINES_CONFIG/machines.crt" &> /dev/null
        else
            echo "[ERROR] Certificate rejected." 1>&2;
            exit 1
        fi
    fi
    _machines-verifyCertificate
    if [ ! -f "$MACHINES_CONFIG/machines.pub" ]; then
        _machines-pubFromCrt
    fi
    return 0
}

function _machines-ensureAdmin {
    if [ ! -f "$MACHINES_CONFIG/machines.key" ]; then
        echo "[ERROR] You need have to have the private key to do that" 1>&2;
        exit 1
    fi
}

# SSH ACCESS KEYS

function _machines-signAkey { # network
    KEY_FILE=$(mktemp)
    SIGN_FILE=$(mktemp)
    _machines-apiSigned "akey/$1?unsigned" > "$KEY_FILE"
    openssl dgst -sha256 -sign "$MACHINES_CONFIG/machines.key" -out "$SIGN_FILE" "$KEY_FILE"
    _machines-apiSigned "akey/$1" --method=PUT --body-file="$SIGN_FILE"
    rm "$KEY_FILE" "$SIGN_FILE" &> /dev/null
}

function _machines-getAkey { # network
    _machines-ensurePub
    KEY_FILE=$(mktemp)
    SIGN_FILE=$(mktemp)
    _machines-api "akey/$1" > "$KEY_FILE"
    _machines-api "akey/$1?signature" > "$SIGN_FILE"

    if openssl dgst -sha256 -verify "$MACHINES_CONFIG/machines.pub" -signature "$SIGN_FILE" "$KEY_FILE" &> /dev/null
    then
        cat "$KEY_FILE"
        \rm "$KEY_FILE" "$SIGN_FILE"
        return 0
    else
        \rm "$KEY_FILE" "$SIGN_FILE"
        exit 1
    fi
}

function _machines-updateAkey {
    MYKEY_FILE=$(mktemp)
    network=$(grep '^network=' "$MACHINES_CONFIG/this" | cut -d '=' -f 2)
    if _machines-getAkey "$network" > "$MYKEY_FILE"
    then
        \mv -f "$MYKEY_FILE" "$MACHINES_HOME/.ssh/authorized_keys"
        if [ -f "$MACHINES_HOME/.ssh/authorized_keys.tail" ]
        then
            cat "$MACHINES_HOME/.ssh/authorized_keys.tail" >> "$MACHINES_HOME/.ssh/authorized_keys"
        fi
        return 0
    else
        cat "$MYKEY_FILE"
        echo "[ERROR] Authorized keys are not properly signed" 1>&2;
        \rm "$MYKEY_FILE"
        exit 1
    fi
}

function _machines-postFile { # filename
    cat $1 | while read -r line; do
        parameter=$(echo "$line" | cut -d '=' -f 1)
        value="$(echo "$line" | sed 's/^[a-zA-Z0-9]\+\(\[\]\)\?=//')"
        echo -n "&$parameter=$(urlencode "$value")"
    done
}


function _machines-addElement { # element elementType default
    FILE=$(mktemp)
    echo -e "$3" > "$FILE"
    $EDITOR "$FILE"
    data=$(_machines-postFile "$FILE")
    \rm "$FILE"
    _machines-apiSigned "$2" --post-data "name=$1$data"
}

function _machines-viewElement { # element elementType
    _machines-apiSigned "$2/$1"
}

function _machines-editElement { # element elementType
    FILE=$(mktemp)
    _machines-apiSigned "$2/$1" > "$FILE"
    $EDITOR "$FILE"
    data=$(_machines-postFile "$FILE")
    rm "$FILE" &> /dev/null
    err=$(_machines-apiSigned "$2/$1" --post-data "$data")
}

function _machines-deleteElement { # element elementType
    err=$(_machines-apiSigned "$2/$1" --method=DELETE)
}


# USER ADMIN FUNCTIONS

function machines_history {
    if [ -f "$MACHINES_CONFIG/lastVerifiedLog" ]; then
        from=$(<"$MACHINES_CONFIG/lastVerifiedLog")
    else
        from=0
    fi
    d=$(date +%s)
    _machines-apiSigned log?from=$from | less
    if prompt "Is this OK?"
    then
        exit 1
    else
        echo "$d" > "$MACHINES_CONFIG/lastVerifiedLog"
        return 0
    fi
}

function machines_sign {
    machines_history
    echo "Signing default network authorized_keys..."
    _machines-signAkey
    _machines-apiSigned network | while read -r network; do
        echo "Signing network $network authorized_keys..."
        _machines-signAkey $network
    done
}

function machines_machine_list {
    _machines-apiSigned machine
}

function machines_network_list {
    _machines-apiSigned network
}

function machines_machine_add_help {
    echo "Usage: $0 machine|mac|m add MACHINE"
    echo
    echo "Arguments:"
    echo "    MACHINE  machine to add"
    return 0
}
function machines_machine_add { # machine
    if [ -z "$1" ]; then
        machines_machine_add_help
        exit 1
    fi
    _machines-addElement "$1" machine "host[]=\nnetwork=\nuserkey=\nhostkey=\nuser="
}

function machines_network_add_help {
    echo "Usage: $0 network|net|n add NETWORK"
    echo
    echo "Arguments:"
    echo "    NETWORK  Network to add"
    return 0
}

function machines_network_add { # network
    if [ -z "$1" ]; then
        machines_network_add_help
        exit 1
    fi
    _machines-addElement "$1" network "allowed[]=\nsecure=false"
}

function machines_machine_view_help {
    echo "Usage: $0 machine|mac|m view MACHINE"
    echo
    echo "Arguments:"
    echo "    MACHINE  machine to view"
    return 0
}

function machines_machine_view { # machine
    if [ -z "$1" ]; then
        machines_machine_view_help
        exit 1
    fi
    _machines-viewElement "$1" machine
}

function machines_network_view_help {
    echo "Usage: $0 network|net|n view NETWORK"
    echo
    echo "Arguments:"
    echo "    NETWORK  Network to view"
    return 0
}
function machines_network_view { # network
    if [ -z "$1" ]; then
        machines_network_view_help
        exit 1
    fi
    _machines-viewElement "$1" network
}

function machines_machine_edit_help {
    echo "Usage: $0 machine|mac|m edit MACHINE"
    echo
    echo "Arguments:"
    echo "    MACHINE  machine to edit"
    return 0
}

function machines_machine_edit { # machine
    if [ -z "$1" ]; then
        machines_machine_edit_help
        exit 1
    fi
    _machines-editElement "$1" machine
}

function machines_network_edit_help {
    echo "Usage: $0 network|net|n edit NETWORK"
    echo
    echo "Arguments:"
    echo "    NETWORK  Network to edit"
    return 0
}
function machines_network_edit { # network
    if [ -z "$1" ]; then
        machines_network_edit_help
        exit 1
    fi
    _machines-editElement "$1" network
}

function machines_machine_delete_help {
    echo "Usage: $0 machine|mac|m delete machine"
    echo
    echo "Arguments:"
    echo "    MACHINE  machine to remove"
    return 0
}
function machines_machine_delete { # machine
    if [ -z "$1" ]; then
        machines_machine_delete_help
        exit 1
    fi
    _machines-deleteElement "$1" machine
}

function machines_network_delete_help {
    echo "Usage: $0 network|net|n delete NETWORK"
    echo
    echo "Arguments:"
    echo "    NETWORK  Network to remove"
    return 0
}
function machines_network_delete { # network
    if [ -z "$1" ]; then
        machines_network_delete_help
        exit 1
    fi
    _machines-deleteElement "$1" network
}

function machines_machine_help {
    echo "Usage: $0 machine|mac|m COMMAND"
    echo
    echo "Commands:"
    echo "    list     List all machines"
    echo "    add      Interactively add a machine"
    echo "    view     Display a machine"
    echo "    edit     Interactively edit a specified machine"
    echo "    delete   Remove a specified machine"
    echo "    help     Get help with commands"
    return 0
}
function machines_machine {
    command="$1"
    shift
    if type "machines_machine_$command" &> /dev/null; then
        "machines_machine_$command" "$@"
    else
        machines_machine_help
    fi
}

function machines_network_help {
    echo "Usage: $0 network|net|n COMMAND"
    echo
    echo "Commands:"
    echo "    list     List all networks"
    echo "    add      Interactively add a network"
    echo "    view     Display a network"
    echo "    edit     Interactively edit a specified network"
    echo "    delete   Remove a specified network"
    echo "    help     Get help with commands"
    return 0
}
function machines_network {
    command="$1"
    shift
    if type "machines_network_$command" &> /dev/null; then
        "machines_network_$command" "$@"
    else
        machines_network_help
    fi
}

machines_mac() { machines_machine "$@"; }
machines_m() { machines_machine "$@"; }
machines_net() { machines_network "$@"; }
machines_n() { machines_network "$@"; }
machines_mac_help() { machines_machine_help "$@"; }
machines_m_help() { machines_machine_help "$@"; }
machines_net_help() { machines_network_help "$@"; }
machines_n_help() { machines_network_help "$@"; }

function machines_update-all {
    machines_machine_list | while read -r machine; do
        echo "Updating $machine..."
        if [ $machine = $(cat "$MACHINES_CONFIG/this.name") ]; then
            machines_update
            continue
        fi
        ssh -n "$machine" 'cd .dotfiles; git pull; ./config/scripts/machines update'
    done
}

function machines_regen-keys {
    if [[ -e $MACHINES_CONFIG/machines.key || -e $MACHINES_CONFIG/machines.pub || -e $MACHINES_CONFIG/machines.crt ]]; then
        echo "[ERROR] Please delete the pem files manually to prove you know what you're doing." 1>&2;
        exit 1
    else
        openssl genrsa -out "$MACHINES_CONFIG/machines.key" 4096
        chmod 600 "$MACHINES_CONFIG/machines.key"
        openssl req -key "$MACHINES_CONFIG/machines.key" -new -out "$MACHINES_CONFIG/machines.csr"
        openssl x509 -req -days 1826 -in "$MACHINES_CONFIG/machines.csr" -signkey "$MACHINES_CONFIG/machines.key" -out "$MACHINES_CONFIG/machines.crt"
        _machines-pubFromCrt
    fi
}


# USER FUNCTIONS

function machines_setup {
    if [ -e "$MACHINES_CONFIG/this.name" ]; then
        echo "[ERROR] This machine is already set up" 1>&2;
        exit 1
    fi

    _machines-ensurePub

    # Variables
    read -r -p 'Machine name? ' name
    read -r -p 'Hosts (separated by spaces)? ' hosts

    # User key
    mkdir -p "$MACHINES_HOME/.ssh" &> /dev/null
    if [[ ! -f $MACHINES_HOME/.ssh/id_rsa || ! -f $MACHINES_HOME/.ssh/id_rsa.pub ]]; then
        ssh-keygen -b 4096 -C "$name@machines.frogeye.fr" -f "$MACHINES_HOME/.ssh/id_rsa" -t rsa
    fi
    userkey=$(<"$MACHINES_HOME/.ssh/id_rsa.pub")

    # Host key
    for type in ecdsa ed25519 rsa dsa; do
        if [ -f "/etc/ssh/ssh_host_${type}_key.pub" ]; then
            hostkey=$(<"/etc/ssh/ssh_host_${type}_key.pub")
            break
        fi
    done

    # Subscription
    data="name=$(urlencode "$name")&userkey=$(urlencode "$userkey")&hostkey=$(urlencode "$hostkey")&user=$(urlencode "$USER")"
    for host in $hosts; do
        data="$data&host[]=$(urlencode "$host")"
    done

    _machines-apiToken machine --post-data "$data"

    echo "$name" > "$MACHINES_CONFIG/this.name"
    machines_update
}

function machines_update {
    _machines-api "machine/$(cat "$MACHINES_CONFIG/this.name")" > "$MACHINES_CONFIG/this"
    _machines-updateAkey
}

function machines_totp {
    url=$(_machines-apiSigned totp)
    echo "URL : $url"
    echo "$url" | qrencode -o - | feh -
}


function machines_help {
    command="$1"
    if [ -n "$command" ]; then
        if type "machines_${command}_help" &> /dev/null; then
            shift
            "machines_${command}_help" "$@"
            return $?
        fi
    fi
    echo "Usage: $0 COMMAND"
    echo
    echo "User commands:"
    echo "    setup            Interactive initial setup for new machine"
    echo "    update           Update this machine"
    echo "    help             Get help with commands"
    echo
    echo "Admin commands:"
    echo "    machine|mac|m    Modify machines"
    echo "    network|net|n    Modify networks"
    echo "    update-all       Update all machines available via SSH"
    echo "    regen-keys       Regenerate system keys"
    echo "    sign             Sign recent transactions for propagation"
    echo "    totp             Get TOTP generating QR code / URL"
    return 0
}

# MAIN
command="$1"
shift
if type "machines_$command" &> /dev/null; then
    "machines_$command" "$@"
else
    machines_help "$@"
fi