Files
TrustTunnel/scripts/install.sh

632 lines
15 KiB
Bash
Executable File

#!/bin/sh
# TrustTunnel Installation Script
set -e -u
# Function log is an echo wrapper that writes to stderr if the caller
# requested verbosity level greater than 0. Otherwise, it does nothing.
log() {
if [ "$verbose" -gt '0' ]
then
echo "$1" 1>&2
fi
}
# Function error_exit is an echo wrapper that writes to stderr and stops the
# script execution with code 1.
error_exit() {
echo "$1" 1>&2
exit 1
}
# Function usage prints the note about how to use the script.
usage() {
echo 'Usage: install.sh [-o output_dir] [-v] [-h] [-u] [-p archive_path] [-V version] [-a y|n]' 1>&2
}
# Function parse_opts parses the options list and validates it's combinations.
parse_opts() {
while getopts 'vho:uV:p:a:' opt "$@"
do
case "$opt"
in
'v')
verbose='1'
;;
'h')
usage
exit 0
;;
'o')
output_dir="$OPTARG"
;;
'u')
uninstall='1'
;;
'V')
version="$OPTARG"
;;
'p')
archive_path="$OPTARG"
;;
'a')
case "$OPTARG" in
[yY]|[yY][eE][sS])
auto_answer="y"
;;
[nN]|[nN][oO])
auto_answer="n"
;;
*)
echo "Invalid value for -a. Use 'y' or 'n' (or 'yes'/'no')." 1>&2
exit 1
;;
esac
;;
*)
log "bad option $OPTARG"
usage
;;
esac
done
}
# Function is_little_endian checks if the CPU is little-endian.
#
# See https://serverfault.com/a/163493/267530.
is_little_endian() {
# The ASCII character "I" has the octal code of 111. In the two-byte octal
# display mode (-o), hexdump will print it either as "000111" on a little
# endian system or as a "111000" on a big endian one. Return the sixth
# character to compare it against the number '1'.
#
# Do not use echo -n, because its behavior in the presence of the -n flag is
# explicitly implementation-defined in POSIX. Use hexdump instead of od,
# because OpenWrt and its derivatives have the former but not the latter.
is_little_endian_result="$(
printf 'I'\
| hexdump -o\
| awk '{ print substr($2, 6, 1); exit; }'
)"
readonly is_little_endian_result
[ "$is_little_endian_result" -eq '1' ]
}
# Function set_os sets the os if needed and validates the value.
set_os() {
# Set if needed.
if [ "$os" = '' ]
then
os="$( uname -s )"
case "$os"
in
# Darwin packages are currently not available
# ('Darwin')
# os='macos'
# ;;
('Linux')
os='linux'
;;
(*)
error_exit "Unsupported operating system: '$os'"
;;
esac
fi
# Validate.
case "$os"
in
('linux')
# All right, go on.
;;
(*)
error_exit "Unsupported operating system: '$os'"
;;
esac
# Log.
log "Operating system: $os"
}
# Function set_cpu sets the cpu if needed and validates the value.
set_cpu() {
# Set if needed.
if [ "$cpu" = '' ]
then
cpu="$( uname -m )"
case "$cpu"
in
('x86_64'|'x86-64'|'x64'|'amd64')
cpu='x86_64'
;;
('armv7l' | 'armv8l')
cpu='armv7'
;;
('aarch64'|'arm64')
cpu='aarch64'
;;
('mips')
if is_little_endian
then
cpu="mipsel"
fi
;;
(*)
error_exit "unsupported cpu type: $cpu"
;;
esac
fi
# Validate.
case "$cpu"
in
('x86_64'|'aarch64')
# All right, go on.
;;
(*)
error_exit "Unsupported cpu type: $cpu"
;;
esac
# Log.
log "CPU type: $cpu"
}
# Function for queries with support for auto-answer
ask_user() {
local prompt="$1"
printf "%s" "$prompt"
if [ -n "$auto_answer" ]; then
response="$auto_answer"
echo "$response"
else
read -r response < /dev/tty
fi
}
set_is_root() {
if [ $USER = "root" ]; then
is_root='1'
fi
if [ "$(id -u)" = "0" ]; then
is_root='1'
fi
}
# Function is_dir_owned_by_current_user checks if the output directory is owned by the current user
is_dir_owned_by_current_user() {
dir="$1"
if [ "$os" = "linux" ]; then
# Linux
if ! owner_name=$(stat -c '%U' "$dir"); then
echo "Cannot stat '$dir'"
return 0
fi
else
echo "Unsupported OS: $os"
return 1
fi
if [ "$owner_name" = "$USER" ]; then
return 0
else
return 1
fi
}
# Function create_dir creates the output directory if it does not exist.
create_dir() {
mkdir -p "$output_dir" 2>/dev/null
if [ $? -eq 1 ];
then
if [ "$is_root" -eq 0 ]; then
echo "Requesting sudo to create directory '$output_dir'"
if sudo mkdir -p "$output_dir"; then
sudo chown -R "${SUDO_USER:-$USER}" "$output_dir"
log "'$output_dir' has been created and ownership has been set to '${SUDO_USER:-$USER}'"
else
error_exit "Failed to create '$output_dir' with sudo"
fi
else
error_exit "Failed to create '$output_dir'"
fi
else
log "'$output_dir' has been created"
fi
}
# Check if the directory is owned by the current user and change the ownership if needed
check_owner() {
if [ "${is_root}" -eq 1 ]; then
return 0
fi
if is_dir_owned_by_current_user "$output_dir";
then
log "'$output_dir' exists and is owned by '$USER'"
else
log "'$output_dir' exists but is not owned by '$USER'"
ask_user "Would you like to change the ownership of $output_dir to $USER? [y/N] "
case "$response" in
[yY]|[yY][eE][sS])
target_user="${SUDO_USER:-$USER}"
if sudo chown -R "$target_user" "$output_dir"; then
log "Ownership of $output_dir has been changed to '${target_user}'"
else
error_exit "Failed to change ownership of '$output_dir'"
fi
;;
*)
error_exit "Installation cannot proceed without changing ownership of '$output_dir'"
;;
esac
fi
}
# Function check_out_dir requires the output directory to be set and exist.
check_out_dir() {
if [ "$output_dir" = '' ]
then
# If output_dir is not set, we will install to /opt
output_dir='/opt'
fi
# If output_dir is '.'or '/opt', create inside it `trusttunnel` directory
if [ "$output_dir" = '.' ] || [ "$output_dir" = '/opt' ]
then
output_dir="${output_dir}/trusttunnel"
fi
if [ "$uninstall" -eq '1' ]
then
echo "TrustTunnel will be uninstalled from '$output_dir'"
return 0
else
echo "TrustTunnel will be installed to '$output_dir'"
fi
set +e
# Check if directory exists
if [ ! -d "$output_dir" ];
then
log "'$output_dir' directory does not exist, attempting to create it..."
create_dir
else
log "'$output_dir' directory exists"
check_owner
fi
set -e
}
# TODO: enable this once binaries are signed with gpg
# Function verify_hint prints a hint about how to verify the installation.
# verify_hint() {
# # Check if `.sig` file exists
# if [ -f "${output_dir}/${exe_name}.sig" ]
# then
# echo
# echo "To verify the installation, run the following command to import the public key and verify the signature:"
# echo " gpg --keyserver 'keys.openpgp.org' --recv-key '28645AC9776EC4C00BCE2AFC0FE641E7235E2EC6'"
# echo " gpg --verify ${output_dir}/${exe_name}.sig ${output_dir}/${exe_name}"
# fi
# }
# Function remove_downloaded_package deletes the archive only if it was downloaded and not transferred.
remove_downloaded_package() {
if [ -z "$archive_path" ]; then
$remove_command "$pkg_name"
fi
}
# Function unpack unpacks the passed archive depending on it's extension.
unpack() {
log "Unpacking package from '$pkg_name' into '$output_dir'"
if ! mkdir -p "$output_dir"
then
error_exit "Cannot create directory '$output_dir'"
fi
if ! tar -C "$output_dir" -f "$pkg_name" -x -z
then
remove_downloaded_package
error_exit "Cannot unpack '$pkg_name'"
fi
remove_downloaded_package
log "Package has been unpacked successfully"
base_name=$(echo "$pkg_name" | sed -E 's!.*/!!')
dir_name=$(echo "${base_name}" | sed -E -e 's/(.*)(\.tar\.gz)/\1/')
if [ -z "${dir_name}" ]; then
error_exit "Can not determine directory name inside archive"
fi
if type mv > /dev/null 2>&1; then
mv -f "${output_dir}/${dir_name}/"* "${output_dir}"
elif type ln > /dev/null 2>&1; then
ln -f "${output_dir}/${dir_name}/"* "${output_dir}"
rm -f "${output_dir}/${dir_name}/"*
else
error_exit "You need mv or ln for this script to work. Make sure that \"coreutils-mv\" is installed."
fi
if type rmdir > /dev/null 2>&1; then
rmdir "${output_dir}/${dir_name}"
else
if [ "$(echo "${output_dir}/${dir_name}/"*)" != "${output_dir}/${dir_name}/*" ]; then
error_exit "Directory not empty"
fi
rm -rf "${output_dir:?}/${dir_name:?}"
fi
}
# Function unpack unpacks the passed archive depending on it's extension.
check_package() {
if [ "$uninstall" -eq '1' ]; then
return 0
fi
log "Checking downloaded package '$pkg_name'"
case "$pkg_ext"
in
('tar.gz')
if ! tar -f "$pkg_name" -z -t > /dev/null
then
remove_downloaded_package
error_exit "Error checking '$pkg_name'"
fi
;;
(*)
error_exit "Unexpected package extension: '$pkg_ext'"
;;
esac
}
# Function parse_version parses the version from the passed script and it's arguments.
parse_version() {
if [ "$uninstall" -eq '1' ]; then
return 0
fi
if [ -n "$version" ]; then
return 0
fi
# Extract the base name of the script
script_name="${0##*/}"
version=$(echo "${script_name}" | sed -nE 's/.*([0-9]+\.[0-9]+\.[0-9]+).*/\1/p')
if [ -z "$version" ]; then
echo "Version required"
return 1
fi
echo "Version number extracted from script name: $version"
}
# Add version to the package name if it's not empty.
apply_version() {
if [ -z "$version" ]; then
return 0
fi
pkg_name=$(echo "${pkg_name}" | sed -E "s/trusttunnel/trusttunnel-v${version}/")
}
# Main function.
configure() {
if [ "$uninstall" -eq '1' ]
then
echo 'Uninstalling TrustTunnel...'
else
echo 'Installing TrustTunnel...'
fi
set_is_root
set_os
set_cpu
check_out_dir
pkg_ext='tar.gz'
if [ -n "$archive_path" ]; then
pkg_name="$archive_path"
need_download='0'
log "Using user-provided archive: $archive_path"
return
fi
parse_version
pkg_name="trusttunnel-${os}-${cpu}.${pkg_ext}"
apply_version
url="https://github.com/TrustTunnel/TrustTunnel/releases/download/v${version}/${pkg_name}"
readonly output_dir url pkg_name
if [ "$uninstall" -eq '0' ]
then
log "Package name: '$pkg_name'"
log "TrustTunnel will be installed in '$output_dir'"
fi
}
# Function handle_uninstall removes the existing package from the output directory.
handle_uninstall() {
# Check if the package is present in the output directory.
if [ ! -f "${output_dir}/trusttunnel_endpoint" ]
then
error_exit "TrustTunnel is not installed in '${output_dir}'. Please specify the correct installation directory"
fi
# Check if vpn is running
if pgrep -x "trusttunnel_endpoint" > /dev/null
then
error_exit "TrustTunnel is currently running, please stop it before uninstalling"
fi
remove_existing
# Check if the directory is empty
if [ -z "$(ls -A "${output_dir}")" ]
then
set +e
log "Remove empty directory: '${output_dir}'"
rmdir "${output_dir}" 2>/dev/null
if [ $? -eq 1 ];
then
if [ "$is_root" -eq 0 ]; then
echo "Requesting sudo to remove '${output_dir}'"
if sudo rmdir "${output_dir}"; then
log "Empty directory '${output_dir}' has been removed"
else
error_exit "Failed to remove empty directory '${output_dir}' with sudo"
fi
else
error_exit "Failed to remove empty directory '${output_dir}'"
fi
else
log "Empty directory '${output_dir}' has been removed"
fi
set -e
fi
# Check systemd service
if [ -L "${systemd_service_path}" ]
then
log "The systemd service for TrustTunnel is found: ${systemd_service_path}. If you want to remove it, do it manually"
fi
}
# Function remove_existing removes the existing package from the output directory before installing a new one.
remove_existing() {
log 'Removing existing package...'
# Remove executable file
rm -f "${output_dir}/trusttunnel_endpoint"
log "'trusttunnel_endpoint' has been removed from '${output_dir}'"
rm -f "${output_dir}/setup_wizard"
log "'setup_wizard' has been removed from '${output_dir}'"
rm -f "${output_dir}/trusttunnel.service.template"
log "'trusttunnel.service.template' has been removed from '${output_dir}'"
rm -f "${output_dir}/LICENSE"
log "'LICENSE' has been removed from '${output_dir}'"
}
# Function checks if the package is already present in the output directory.
handle_existing() {
if [ "$uninstall" -eq '1' ]
then
handle_uninstall
exit 0
fi
if [ ! -f "${output_dir}/trusttunnel_endpoint" ]
then
return
fi
log "Package 'trusttunnel' is already present in '${output_dir}'"
ask_user "Ensure 'TrustTunnel' is stopped before proceeding, continue? [y,N]"
remove_existing
}
# Function download downloads the package from the url.
download() {
if [ "$uninstall" -eq '1' ]; then
return 0
fi
if [ "$need_download" -eq '0' ]; then
return
fi
log "Downloading TrustTunnel package: $url"
# Temporary file name for testing file creation
tmp_file="tmp_file_check_$$"
# Check if we can write to the current directory by creating a temporary file
if ! touch "$tmp_file" > /dev/null 2>&1; then
if [ "$is_root" -eq 0 ]; then
ask_user "Cannot create file in the current directory. Try downloading as root? [y/N] "
case "$response" in
[yY]|[yY][eE][sS])
remove_command="sudo rm -f"
if ! sudo curl -fsSL "$url" -o "$pkg_name"; then
error_exit "Failed to download $pkg_name: $?"
fi
;;
*)
error_exit "Cannot proceed without file creation rights."
;;
esac
else
log "Cannot create file in the current directory."
error_exit "Cannot proceed without file creation rights."
fi
else
# Cleanup the temporary file
rm "$tmp_file"
if ! curl -fsSL "$url" -o "$pkg_name"; then
error_exit "Failed to download $pkg_name: $?"
fi
fi
log "TrustTunnel package has been downloaded successfully"
}
report_success() {
echo
echo "============================================================"
echo " TrustTunnel Endpoint was installed to '${output_dir}'"
echo "============================================================"
echo
echo "--- Continue setup ---"
echo
echo "Run the Setup Wizard to continue the endpoint setup:"
echo
echo " cd ${output_dir}"
echo " sudo ./setup_wizard"
echo
echo "--- Configure systemd service ---"
echo
echo "The '${output_dir}/trusttunnel.service.template' template"
echo "could be used to run a configured endpoint as a systemd service."
echo
echo "Once the endpoint setup is finished, run the following commands"
echo "to configure a systemd service:"
echo
echo " cd ${output_dir}/"
echo " sudo cp trusttunnel.service.template /etc/systemd/system/trusttunnel.service"
echo " sudo systemctl daemon-reload"
echo " sudo systemctl enable --now trusttunnel"
echo
}
# Entrypoint
systemd_service_path='/etc/systemd/system/trusttunnel.service'
output_dir=''
verbose='1'
cpu=''
os=''
version='1.0.29'
uninstall='0'
remove_command="rm -f"
is_root='0'
archive_path=''
auto_answer=''
need_download='1'
parse_opts "$@"
configure
download
check_package
handle_existing
unpack
report_success