#!/command/with-contenv bashio
# shellcheck shell=bash
# ==============================================================================
# Home Assistant Community Add-on: Tor
# Prepares the add-on for startup
# ==============================================================================
declare address
declare clientname
declare host
declare key
declare log_level
declare port
declare private_key
declare public_key
declare target_port
declare virtual_port

readonly torrc='/etc/tor/torrc'
readonly hidden_service_dir='/ssl/tor/hidden_service'
readonly authorized_clients_dir="${hidden_service_dir}/authorized_clients"
readonly clients_dir="${hidden_service_dir}/clients"
readonly hostname_file="${hidden_service_dir}/hostname"

# A hidden service without any ports is kinda useless
if bashio::config.true 'hidden_services' \
    && ! bashio::config.has_value 'ports'; then
    bashio::log.fatal
    bashio::log.fatal 'Add-on configuration is incomplete.'
    bashio::log.fatal
    bashio::log.fatal 'Hidden services where enabled, using the'
    bashio::log.fatal '"hidden_services" add-on configuration option,'
    bashio::log.fatal 'But the "port" option does not contain any values!'
    bashio::log.fatal
    bashio::log.fatal 'Please configure the "ports" option.'
    bashio::exit.nok
fi

# Checks if client names where configured when using stealth mode
if bashio::config.true 'hidden_services' \
    && bashio::config.true 'stealth' \
    && ! bashio::config.has_value 'client_names';
then
    bashio::log.fatal
    bashio::log.fatal 'Add-on configuration is incomplete.'
    bashio::log.fatal
    bashio::log.fatal 'Stealth mode is enabled, using the "stealth" add-on'
    bashio::log.fatal 'configuration option, but there are no client names'
    bashio::log.fatal 'configured in the "client_names" add-on option.'
    bashio::log.fatal
    bashio::log.fatal 'Please configure the "client_names" option.'
    bashio::exit.nok
fi

# Created needed directories
mkdir -p \
    "${authorized_clients_dir}" \
    "${clients_dir}" \
    "${hidden_service_dir}" \
    || bashio::exit.nok 'Could not create tor data directories'
chmod -R 0700 /ssl/tor

# Find the matching Tor log level
if bashio::config.has_value 'log_level'; then
    case "$(bashio::string.lower "$(bashio::config 'log_level')")" in
        all|trace)
            log_level="debug"
            ;;
        debug)
            log_level="info"
            ;;
        info|notice)
            log_level="notice"
            ;;
        warning)
            log_level="warn"
            ;;
        error|fatal|off)
            log_level="err"
            ;;
    esac

    echo "Log ${log_level} stdout" >> "${torrc}"
fi

# Configure Socks proxy
if bashio::config.true 'socks'; then
    echo 'SOCKSPort 0.0.0.0:9050' >> "${torrc}"
else
    echo 'SOCKSPort 127.0.0.1:9050' >> "${torrc}"
fi

# Configure hidden services
if bashio::config.true 'hidden_services'; then
    echo "HiddenServiceDir ${hidden_service_dir}" >> "${torrc}"
    
    for port in $(bashio::config 'ports'); do
        count=$(echo "${port}" | sed 's/[^:]//g'| awk '{ print length }')
        if [[ "${count}" == 0 ]]; then
            host='homeassistant'
            virtual_port="${port}"
            target_port="${port}"
        elif [[ "${count}" == 1 ]]; then
            # Check if format is hostname/ip:port or port:port
            first=$(echo "${port}" | cut -f1 -d:)
            if [[ "${first}" =~ ^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]) ]]; then
                host='homeassistant'
                virtual_port=$(echo "${port}" | cut -f1 -d:)
                target_port=$(echo "${port}" | cut -f2 -d:)
            else
                host=$(echo "${port}" | cut -f1 -d:)
                virtual_port=$(echo "${port}" | cut -f2 -d:)
                target_port=$(echo "${port}" | cut -f2 -d:)
            fi
        elif [[ "${count}" == 2 ]]; then
            host=$(echo "${port}" | cut -f1 -d:)
            virtual_port=$(echo "${port}" | cut -f2 -d:)
            target_port=$(echo "${port}" | cut -f3 -d:)
        else
            bashio::log.warning "$port Are not correct format, skipping..."
        fi
        if [[ "${count}" -le 2 ]]; then
            echo "HiddenServicePort ${target_port} ${host}:${virtual_port}" \
                >> "${torrc}"
        fi
    done
fi

# Configure bridges
if bashio::config.exists 'bridges' \
    && ! bashio::config.is_empty 'bridges';
then
    bashio::log.info 'Use bridges:'
    echo "UseBridges 1" >> "${torrc}"

    # Add client for OBFS transport
    echo "ClientTransportPlugin obfs2,obfs3,obfs4,scramblesuit exec /usr/local/bin/obfs4proxy managed" >> "${torrc}"
    
    # Add client for Snowflake transport
    echo "ClientTransportPlugin snowflake exec /usr/local/bin/snowflake" >> "${torrc}"

    # Add client for WebTunnel transport
    echo "ClientTransportPlugin webtunnel exec /usr/local/bin/webtunnel" >> "${torrc}"
    
    # Add bridges
    while read -r bridge; do
        bashio::log.info "Bridge ${bridge}"
        echo "Bridge ${bridge}" >> "${torrc}"
    done <<< "$(bashio::config 'bridges')"
fi

# Figure out the address
if bashio::config.true 'hidden_services'; then
    bashio::log.info 'Starting Tor temporarly...'

    exec 3< <(tor)

    until bashio::fs.file_exists "${hostname_file}"; do
        bashio::log.info "Waiting for service to start..."
        sleep 1
    done

    address=$(<"${hostname_file}")
    grep -m 1 "Bootstrapped 100% (done): Done" <&3 >/dev/null 2>&1

    kill "$(pgrep tor)" >/dev/null 2>&1

    bashio::log.info '---------------------------------------------------------'
    bashio::log.info 'Your Home Assistant instance is available on Tor!'
    bashio::log.info "Address: ${address}"
    bashio::log.info '---------------------------------------------------------'
fi

# Configure stealth mode
if bashio::config.true 'hidden_services' && bashio::config.true 'stealth';
then
    # Following the documentation at:
    # https://community.torproject.org/onion-services/advanced/client-auth/
    while read -r clientname; do
        # Generate key is they do not exist yet
        if ! bashio::fs.file_exists "${authorized_clients_dir}/${clientname}.auth"
        then 
            key=$(openssl genpkey -algorithm x25519)

            private_key=$(
                sed \
                    -e '/----.*PRIVATE KEY----\|^[[:space:]]*$/d' \
                    <<< "${key}" \
                    | base64 -d \
                    | tail -c 32 \
                    | base32 \
                    | sed 's/=//g'
            )

            public_key=$(
                openssl pkey -pubout \
                <<< "${key}" \
                | sed -e '/----.*PUBLIC KEY----\|^[[:space:]]*$/d' \
                | base64 -d \
                | tail -c 32 \
                | base32 \
                | sed 's/=//g'
            )

            # Create authorized client file
            echo "descriptor:x25519:${public_key}" \
                > "${clients_dir}/${clientname}.auth"
            echo "descriptor:x25519:${public_key}" \
                > "${authorized_clients_dir}/${clientname}.auth"

            # Create private key file
            echo "${private_key}" \
                > "${clients_dir}/${clientname}.key.txt"
            echo "${address%.onion}:descriptor:x25519:${private_key}" \
                > "${clients_dir}/${clientname}.auth_private"

            bashio::log.red
            bashio::log.red
            bashio::log.red "Created keys for ${clientname}!"
            bashio::log.red
            bashio::log.red "Keys are stored in:"
            bashio::log.red "${clients_dir}"
            bashio::log.red
            bashio::log.red "Public key":
            bashio::log.red "${public_key}"
            bashio::log.red
            bashio::log.red "Private key:"
            bashio::log.red "${private_key}"
            bashio::log.red
            bashio::log.red 
        else
            bashio::log.info "Keys for ${clientname} already exists; skipping..."
        fi
    done <<< "$(bashio::config 'client_names')"

    echo 'HiddenServiceAllowUnknownPorts 0' >> "${torrc}"
fi
