#!/usr/bin/env bash set -o nounset set -o errexit DEFAULT_SERVER_PORT="80" SERVER_PORT="" DEFAULT_SERVER_IP="${HOSTNAME}" SERVER_IP="" SERVER_ID="" INSTALLATION_TOKEN="183bb99c-17ae-4a26-b784-7e60f1a60d47" API_SERVER_URL="https://sp-dir.uwn.com" DIRECTORY_SERVER_URL="wss://sp-dir.uwn.com/connect" SPEEDTEST_IMAGE=${SPEEDTEST_IMAGE:-"ubnt/speedtest"} SPEEDTEST_CONTAINER="ubnt-speedtest" WATCHTOWER_CONTAINER="ubnt-speedtest-watchtower" AUTH_HEADER="x-auth-token: ${INSTALLATION_TOKEN}" ENDPOINT_IP="${API_SERVER_URL}/api/v1/ip" ENDPOINT_INSTALL="${API_SERVER_URL}/api/v1/install" BANNED_PORTS=(69 137 161 554 1719 1720 1723 5060 5061 6566 10080) IS_LINUX=false if uname | grep -q Linux; then IS_LINUX=true fi if [ "x$*" != "x" ]; then API_KEY="$*" fi fail() { echo -e "ERROR: $1" >&2 exit 1 } # usage: confirm # Prints given question and asks user to type Y or N. # Returns 0 if user typed Y, 1 if user typed N. # Exits if user failed to type Y or N too many times. # examples: # confirm "Do you want to continue?" || exit 1 confirm() { local question="$1" for i in 0 1 2 3 4 5 6 7 8 9 10; do read -p "${question} [Y/N]" -n 1 -r echo if [[ ${REPLY} =~ ^[Yy]$ ]]; then echo "Yes" return 0 fi if [[ ${REPLY} =~ ^[Nn]$ ]]; then echo "No" return 1 fi echo "Please type Y or N." done fail "Too many failed attempts." } accept_terms() { if ! confirm "I accept the Terms of Service (https://www.ui.com/legal/termsofservice/)"; then fail "Cannot continue installation without accepting Terms of Service" fi } fetch_api_key() { # maybe no need to fetch api key if [ -n "${API_KEY:-}" ]; then echo "Using provided API Key: ${API_KEY}" return fi echo "Registering server..." RESULT=$( curl -s -X POST \ -H "${AUTH_HEADER}" \ -H "Accept: text/plain" \ -F ip="${SERVER_IP}" \ -F port="${SERVER_PORT}" \ "${ENDPOINT_INSTALL}" ) API_KEY=$(echo $RESULT | jq -r '.apiKey') SERVER_ID=$(echo $RESULT | jq -r '.serverId') if [ -z "${API_KEY:-}" ]; then fail "Cannot authorize new server (API_KEY invalid)" fi if ! echo "${API_KEY}" | grep -E -q '^[0123456789ABCDFHKLMNPQRSTUVWXYZ]+$'; then fail "Server generated invalid authentication key (API_KEY ${API_KEY})" >&2 fi } pull_docker_images() { echo "Pulling docker images." docker pull "$SPEEDTEST_IMAGE" docker pull v2tec/watchtower } install_jq() { if which jq > /dev/null 2>&1; then return fi if [[ "$OSTYPE" == "linux-gnu"* ]]; then echo "Installing jq." apt update if ! (apt install jq -y > /dev/null); then fail "Unable to install jq using apt. To continue, please install jq manually." fi fi if [[ "$OSTYPE" == "darwin"* ]]; then echo "Installing jq." if ! (HOMEBREW_NO_AUTO_UPDATE=true brew install jq > /dev/null); then fail "Unable to install jq using Homebrew. To continue, please install jq manually." fi fi } install_docker() { if ! which docker > /dev/null 2>&1; then echo "Download and install Docker" export CHANNEL="stable" curl -fsSL https://get.docker.com/ | sh fi DOCKER_VERSION=$(docker -v | sed 's/.*version \([0-9.]*\).*/\1/'); echo "Docker version: ${DOCKER_VERSION}" if ! which docker > /dev/null 2>&1; then fail "Docker not installed. Please check previous logs. Aborting." fi } stop_docker_containers() { echo "Stopping docker containers." docker stop "${WATCHTOWER_CONTAINER}" 2>/dev/null || true docker stop "${SPEEDTEST_CONTAINER}" 2>/dev/null || true } remove_old_docker_containers() { echo "Removing docker containers." docker rm "${WATCHTOWER_CONTAINER}" 2>/dev/null || true docker rm "${SPEEDTEST_CONTAINER}" 2>/dev/null || true } ask_server_ip() { PUBLIC_IP=$(curl -fs -H "Accept: text/plain" "${ENDPOINT_IP}" | cut -f1 || echo -n "") if [ -n "${PUBLIC_IP:-}" ]; then DEFAULT_SERVER_IP=${PUBLIC_IP} fi read -r -p "Please enter a public ipv4. [${DEFAULT_SERVER_IP}]: " SERVER_IP SERVER_IP=${SERVER_IP:-${DEFAULT_SERVER_IP}} if ! echo "${SERVER_IP}" | grep -E -q '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$'; then echo -e "${SERVER_IP} is not a valid ipv4." >&2 ask_server_ip return fi echo "Using ip ${SERVER_IP}" } ask_server_port() { read -r -p "Please enter HTTP port. [${DEFAULT_SERVER_PORT}]: " SERVER_PORT SERVER_PORT=${SERVER_PORT:-${DEFAULT_SERVER_PORT}} if [[ ! $SERVER_PORT =~ ^[0-9]+$ ]] || [ $SERVER_PORT -lt 10 -o $SERVER_PORT -gt 65355 ] || echo " ${BANNED_PORTS[@]} " | grep -q " $SERVER_PORT "; then local banned_ports_join=`printf "%s, " ${BANNED_PORTS[@]}` echo -e "${SERVER_PORT} is not a valid port number. Ports less than 10, equal to ${banned_ports_join%??} or greater than 65355 are prohibited." >&2 ask_server_port return fi echo "Checking available HTTP port" if nc -z 127.0.0.1 "${SERVER_PORT}" >/dev/null 2>&1; then echo -e "Port ${SERVER_PORT} is in use." >&2 ask_server_port return fi echo "Using port ${SERVER_PORT}" if $IS_LINUX; then echo Port will be automatically opened during the installation. It has to be manually closed on firewall. fi } validate_server_info() { local result result=$( curl -fs -X POST \ -w "%{http_code}" \ -o /dev/null \ -H "${AUTH_HEADER}" \ -F ip="${SERVER_IP}" \ -F port="${SERVER_PORT}" \ "${ENDPOINT_INSTALL}/validate" ) case ${result} in 200) return 0 ;; 400) echo "Please enter valid ip and port" ;; 401) fail "Authentication failed. Please make sure you have downloaded the latest version of this installation script and try again" ;; 403) echo "Server ${SERVER_IP}:${SERVER_PORT} is already registered. Please choose a different ip and/or port" ;; 409) echo "Server ${SERVER_IP}:${SERVER_PORT} already exists." if confirm "Do you want to replace it with current installation?"; then return 0 fi ;; 429) fail "Server limit reached. You can't have any more servers, retire some and try again." ;; *) fail "Failed to connect to Speed Test API with an error code ${result:-"unknown"}" esac return 1 } gather_server_info() { ask_server_ip ask_server_port if ! validate_server_info; then gather_server_info; fi } get_docker_network() { if $IS_LINUX; then if ! ufw allow $SERVER_PORT > /dev/null 2>&1; then iptables -A INPUT -p tcp --dport $SERVER_PORT -j ACCEPT fi echo --network host -e PORT="${SERVER_PORT}" else echo --publish "${SERVER_PORT}:8050" fi } start_docker_containers() { echo "Starting docker containers." docker run -d \ --name "${WATCHTOWER_CONTAINER}" \ --restart unless-stopped \ -v /var/run/docker.sock:/var/run/docker.sock \ v2tec/watchtower --label-enable --cleanup docker run -d \ --name "${SPEEDTEST_CONTAINER}" \ $(get_docker_network) \ -e API_KEY="${API_KEY}" \ -e DIRECTORY_SERVER_URL="${DIRECTORY_SERVER_URL}" \ -l "com.centurylinklabs.watchtower.enable=true" \ --restart unless-stopped \ "${SPEEDTEST_IMAGE}" } confirm_success() { echo "Waiting for Speed Test Server to start" local n=0 local running=false until [ ${n} -ge 10 ] do sleep 3s running=true # env -i is to ensure that http[s]_proxy variables are not set # Otherwise the check would go through proxy. local status status=$(env -i curl -skL -H 'Accept: text/plain' "http://${SERVER_IP}:${SERVER_PORT}/diagnostics" | cut -f1) if [ "${status}" = "1" ]; then break fi echo "." running=false n=$((n+1)) done docker ps if [ "${running}" = true ]; then echo "Speed Test Server is running. In next couple of minutes, it will receive an unique subdomain at wifiman.me with key & certificate." else echo "" echo "Server failed to start or it can't connect to the Speed Test Network." echo " - Make sure your installation script is up to date. You can always download the latest version from https://console.wifiman.com/" echo " - Make sure URL http://${SERVER_IP}:${SERVER_PORT} is publicly accessible" echo " - Contact our support with output of http://localhost:${SERVER_PORT}/diagnostics and 'docker logs ${SPEEDTEST_CONTAINER}'" echo "" fail "Speed Test Server installation failed" fi } do_install() { accept_terms install_jq install_docker pull_docker_images stop_docker_containers remove_old_docker_containers gather_server_info fetch_api_key start_docker_containers confirm_success echo ServerID: $SERVER_ID } # wrapped up in a function so that we have some protection against only getting # half the file during "curl | sh" do_install