#!/bin/bash
#
# Copyright (c) 2021 ExpressVPN. All rights reserved.
#

set -eu

version="3.49.0beta"
build="0"
expect_engine_diagnostics="OPTIN"
command_timeout="3s"

timestamp="$(date "+%Y%m%d%H%M%S")"

output_name="ExpressVPN-Linux-Diagnostics-${version}-${timestamp}"
output_zip="${output_name}.zip"

bold='\e[1m'
reg='\e[0m'

echo "ExpressVPN diagnostics collector ${version} (${build})"

if [ $EUID -ne 0 ] ; then
    echo -e >&2 "${bold}ERROR${reg}: This script must be run as root."
    echo -e >&2 ""
    echo -e >&2 "Try:"
    echo -e >&2 ""
    echo -e >&2 "    sudo /usr/lib/expressvpn/expressvpn-diagnostics-collector"
    exit 1
fi

if ! command -v zip >/dev/null ; then
    echo -e >&2 "${bold}ERROR${reg}: zip utility not found."
    echo -e >&2 ""
    echo -e >&2 "Please install your distribution ${bold}zip${reg} package before"
    echo -e >&2 "rerunning this tool."
    exit 1
fi

if [ -e "${output_zip}" ] ; then
    echo -e >&2 "${bold}ERROR${reg}: ${output_zip} already exists, not overwriting."
    exit 1
fi

if [ "x$expect_engine_diagnostics" = "xOPTIN" ] && [ ! -e /var/log/expressvpn/expressvpnd.log ] ; then
    echo -e "${bold}WARNING${reg}: /var/log/expressvpn/expressvpnd.log not found."
    echo -e ""
    echo -e "Enable the collection of diagnostic information to report bugs and give"
    echo -e "feedback on this beta version of ExpressVPN. To enable run:"
    echo -e ""
    echo -e "  ${bold}sudo /usr/lib/expressvpn/expressvpn-enable-beta-diagnostics${reg}"
    echo -e ""
    echo -e "Once this is done you may wish to reproduce your issue and collect a"
    echo -e "new set of diagnostics."
fi

verbose() {
    if [ "${VERBOSE:-0}" -eq 0 ]; then return; fi
    echo "$@"
}

# copy_file copies the provided file to the diagnostics, substituting /
# into _. On failure leaves a stamp file to indicate why the file was
# not provided (failed, notfile, notfound, etc), often with `stat`
# output in it.
copy_file() {
    local f="$1"
    local t="${o}/${f//\//_}"
    if [ -f "${f}" ] ; then
        verbose "copy_file: $f -> ${t#$tdir/}"
        if ! cp -- "${f}" "${t}" ; then
            stat "${f}" > "${t}.failed" 2>&1 || true
        fi
    elif [ -L "${f}" ] ; then
        # "test -f $f" dereferences symlinks, so we would
        # only get here if the final element of $f was a bad
        # symlink.
        verbose "copy_file: $f bad sym-link"
        stat "${f}" > "${t}.badsymlink" 2>&1 || true
    elif [ -e "${f}" ] ; then
        verbose "copy_file: $f not a file"
        stat "${f}" > "${t}.notfile" 2>&1 || true
    else
        verbose "copy_file: $f not found"
        touch "${t}.notfound" || true
    fi
}

# maybe_copy_file copies the provided file only if it exists,
# otherwise identical to copy_file.
maybe_copy_file() {
    local f="$1"
    if [ ! -f "${f}" ] ; then return ; fi
    copy_file "$f"
}

# copy_directory copies the provided directory to the diagnostics,
# substituting / into _. On failure leaves a stamp file to indicate
# why the file was not provided (failed, notdir, notfound, etc), often
# with `stat` output in it
copy_directory() {
    local d="$1"
    local t="${o}/${d//\//_}"
    if [ -d "${d}" ] ; then
        verbose "copy_directory: ${d} -> ${t#$tdir}"
        if ! cp -r -- "${d}" "${t}" ; then
            stat "${d}" > "${t}.failed" 2>&1 || true
        fi
    elif [ -L "${d}" ] ; then
        # "test -d $d" dereferences symlinks, so we would
        # only get here if the final element of $d was a bad
        # symlink.
        verbose "copy_directory: $d bad sym-link"
        stat "${d}" > "${t}.badsymlink" 2>&1 || true
    elif [ -e "${d}" ]  ; then
        verbose "copy_directory: $d not a directory"
        stat "${d}" > "${t}.notdir" 2>&1 || true
    else
        verbose "copy_directory: $d not found"
        touch "${t}.notfound" || true
    fi
}

# capture_command_output captures the combined output (stdout &
# stderr) of the provided command into a file. If the binary doesn't
# exist or running it fails (returns non-zero) then leaves a stamp
# file (notfound, failed, etc) indicating why.
capture_command_output() {
    local b="$1"
    local oldIFS="$IFS"
    IFS="_" # join $* with _
    local t="${o}/${*//\//_}"
    IFS="$oldIFS"

    if [ -x "$b" ]; then
        verbose "capture_command_output: Capture \`$*\` to ${t#$tdir}.output"
        if timeout "${command_timeout}" "$@" >"${t}.output" 2>&1 ; then
	    # We don't want to invert the if, so that $? is correct
	    # below, but there is nothing to do here.
	    :
	else
            echo "$?" > "${t}.status" || true
            stat "$b" > "${t}.failed" 2>&1 || true
        fi
    elif [ -e "$b" ] ; then
        verbose "capture_command_output: $b is not executable, cannot capture \`$*\`"
        stat "$b" > "${t}.notexec" 2>&1 || true
    elif [ -L "$b" ] ; then
        # "test -x $b" and "test -e $b" dereference symlinks, so we would
        # only get here if the final element of $b was a bad
        # symlink.
        verbose "capture_command_output: $b is a bad symlink, cannot capture \`$*\`"
        stat "$b" > "${t}.badsymlink" 2>&1 || true
    else
        verbose "capture_command_output: $b does not exist, cannot capture \`$*\`"
        touch "$b" > "${t}.notfound" || true
    fi
}


# maybe_capture_command_output captures the combined output (stdout &
# stderr) of the provided command into a file only if the binary
# exists. otherwise identical to capture_command_output.
maybe_capture_command_output() {
    local b="$1"
    if [ ! -e "${b}" ] ; then return ; fi
    capture_command_output "$@"
}

# capture_diagnostic_message appends the provided error to a log which
# is part of the final zip file
capture_diagnostic_message() {
    echo "$*" >> "${o}/expressvpn-diagnostics-collector-errors.log"
}

# Setup a temporary staging directory
tdir=$(mktemp -d)
if [ ! -d "${tdir}" ]; then
    echo -w >&2 "${bold}ERROR${reg}: Failed to create temporary directory."
    exit 1
fi
exit_handler() {
    rm -rf -- "${tdir}"
    if [ ! -s "${output_zip}" ] ; then
       rm -f "${output_zip}"
    fi
}
trap exit_handler EXIT
o="${tdir}/${output_name}"
verbose "Staging diagnostics in ${o}"
mkdir -p "${o}"

echo "${version} (build: ${build})" > "${o}/VERSION" || true

# Gather everything we want
copy_file /etc/resolv.conf
maybe_copy_file /etc/resolv.conf.expressvpn-new
maybe_copy_file /etc/resolv.conf.expressvpn-orig
maybe_copy_file /var/lib/expressvpn/resolv.conf
maybe_copy_file /var/lib/expressvpn/resolv.conf.orig

maybe_copy_file /etc/os-release
maybe_copy_file /etc/lsb-release

maybe_copy_file /etc/debian_version
maybe_copy_file /etc/arch-release
maybe_copy_file /etc/fedora-release
maybe_copy_file /etc/default/expressvpn

maybe_copy_file /etc/systemd/system/expressvpn.service
maybe_copy_file /etc/systemd/system/expressvpn.service.bak
maybe_copy_file /etc/systemd/system/expressvpn.service.d/override.conf

copy_directory /var/log/expressvpn
copy_directory /var/lib/expressvpn/errors
maybe_copy_file /var/lib/expressvpn/dns_config_method

if [ -f /usr/bin/expressvpnd ] ; then
    capture_command_output /usr/bin/expressvpnd version
    capture_command_output /usr/bin/ldd /usr/bin/expressvpnd
elif [ -f /usr/sbin/expressvpnd ] ; then
    capture_command_output /usr/sbin/expressvpnd version
    capture_command_output /usr/bin/ldd /usr/sbin/expressvpnd
else
    capture_diagnostic_message "No expressvpnd binary found in /usr/bin or /usr/sbin"
fi

capture_command_output /usr/bin/expressvpn diagnostics
capture_command_output /usr/bin/expressvpn status
capture_command_output /usr/bin/expressvpn preferences
capture_command_output /usr/bin/expressvpn preferences get dns_config_method
capture_command_output /usr/bin/expressvpn list
capture_command_output /usr/bin/expressvpn list all

capture_command_output /usr/bin/ldd /usr/bin/expressvpn
capture_command_output /usr/bin/ldd /usr/bin/expressvpn-browser-helper
capture_command_output /usr/bin/ldd /usr/lib/expressvpn/lightway
capture_command_output /usr/bin/ldd /usr/lib/expressvpn/openvpn
capture_command_output /usr/bin/ldd /usr/lib/expressvpn/libxvclient.so

capture_command_output /bin/ip link show
capture_command_output /bin/ip addr show
capture_command_output /bin/ip route show
capture_command_output /bin/ping -c 2 -W 2 8.8.8.8
if [ -f /usr/sbin/iptables ] ; then
    capture_command_output /usr/sbin/iptables -L -n
elif [ -f /sbin/iptables ] ; then
    capture_command_output /sbin/iptables -L -n
else
    capture_diagnostic_message "No iptables binary found in /usr/sbin or /sbin"
fi
if [ -f /usr/sbin/ip6tables ] ; then
    capture_command_output /usr/sbin/ip6tables -L -n
elif [ -f /sbin/ip6tables ] ; then
    capture_command_output /sbin/ip6tables -L -n
else
    capture_diagnostic_message "No ip6tables binary found in /usr/sbin or /sbin"
fi
if [ -f /usr/bin/nmcli ] ; then
    capture_command_output /usr/bin/nmcli general
    capture_command_output /usr/bin/nmcli device
else
    capture_diagnostic_message "/usr/bin/nmcli not present"
fi

capture_command_output /bin/uname -a
copy_file /proc/cmdline

maybe_capture_command_output /bin/systemctl
maybe_capture_command_output /bin/systemctl status
command_timeout=5s maybe_capture_command_output /bin/journalctl --since '-3d'
maybe_capture_command_output /bin/journalctl --since '-1h'

# We only need one of these
speedtest_domain="speedtest.expressvpn.com"
if [ -x /usr/bin/dig ] ; then
    capture_command_output /usr/bin/dig "$speedtest_domain"
elif [ -x /usr/bin/nslookup ] ; then
    capture_command_output /usr/bin/nslookup "$speedtest_domain"
else
    touch "${o}/speedtest.dns-tool-nonfound"
fi
speedtest_url="https://speedtest.expressvpn.com/sample128k.bin"
if [ -x /usr/bin/curl ] ; then
    capture_command_output /usr/bin/curl -v -o /dev/null "${speedtest_url}"
elif [ -x /usr/bin/wget ] ; then
    capture_command_output /usr/bin/wget --progress=dot -v -d -O /dev/null -o "${o}/wget.speedtest.log" "${speedtest_url}"
else
    touch "${o}/speedtest.fetch-tool-nonfound"
fi
captive_url="http://captive.apple.com"
if [ -x /usr/bin/curl ] ; then
    capture_command_output /usr/bin/curl -v -o /dev/null "${captive_url}"
elif [ -x /usr/bin/wget ] ; then
    capture_command_output /usr/bin/wget --progress=dot -v -d -O /dev/null -o "${o}/wget.captive.log" "${captive_url}"
else
    touch "${o}/captive.fetch-tool-nonfound"
fi

# Sanity check, if there are no diagnostics and
# expect_engine_diagnostics is OPTIN we already gave instructions on
# enabling diagnostics above, but maybe we failed to collect the
# diagnostics which exist for some reason.
if [ -e /var/log/expressvpn/expressvpnd.log ] && [ ! -e "${o}/_var_log_expressvpn" ] ; then
    echo -e "${bold}WARNING${reg}: No engine diagnostics were collected."
fi

# Create the final archive

# zip apparently lacks an equivalent of `tar -C`
if ! ( cd "${tdir}" && zip --quiet --recurse-paths t.zip "${output_name}" ) ; then
    echo >&2 "Failed to create zip"
    exit 1
fi

mv "${tdir}/t.zip" "${output_zip}"

if [ -s "${output_zip}" ] ; then
    echo "Diagnostics written to: ${output_zip}"
else
    echo "Failed to create ${output_zip}"
fi
