diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 8f5f6aac..f81d0e56 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -10,7 +10,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4.1.0 + uses: actions/checkout@v4.2.2 - name: Spell-Checking uses: codespell-project/actions-codespell@master diff --git a/.github/workflows/editorconfig-checker.yml b/.github/workflows/editorconfig-checker.yml index 9eff6db2..8f104c24 100644 --- a/.github/workflows/editorconfig-checker.yml +++ b/.github/workflows/editorconfig-checker.yml @@ -9,6 +9,6 @@ jobs: name: editorconfig-checker runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.0 + - uses: actions/checkout@v4.2.2 - uses: editorconfig-checker/action-editorconfig-checker@main # current tag v1.0.0 is really out-of-date - run: editorconfig-checker diff --git a/.github/workflows/merge-conflict.yml b/.github/workflows/merge-conflict.yml index 424a1c0e..ea30b655 100644 --- a/.github/workflows/merge-conflict.yml +++ b/.github/workflows/merge-conflict.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check if PRs are have merge conflicts - uses: eps1lon/actions-label-merge-conflict@v2.1.0 + uses: eps1lon/actions-label-merge-conflict@v3.0.3 with: dirtyLabel: "Merge Conflict" repoToken: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 5535c562..558223f5 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,7 +17,7 @@ jobs: issues: write steps: - - uses: actions/stale@v8.0.0 + - uses: actions/stale@v9.1.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 30 @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4.1.0 + uses: actions/checkout@v4.2.2 - name: Remove 'stale' label run: gh issue edit ${{ github.event.issue.number }} --remove-label ${{ env.stale_label }} env: diff --git a/.github/workflows/stale_pr.yml b/.github/workflows/stale_pr.yml index 4a151100..2441271e 100644 --- a/.github/workflows/stale_pr.yml +++ b/.github/workflows/stale_pr.yml @@ -17,7 +17,7 @@ jobs: pull-requests: write steps: - - uses: actions/stale@v8.0.0 + - uses: actions/stale@v9.1.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} # Do not automatically mark PR/issue as stale diff --git a/.github/workflows/sync-back-to-dev.yml b/.github/workflows/sync-back-to-dev.yml index 6d59401f..d9c7ed19 100644 --- a/.github/workflows/sync-back-to-dev.yml +++ b/.github/workflows/sync-back-to-dev.yml @@ -11,7 +11,7 @@ jobs: name: Syncing branches steps: - name: Checkout - uses: actions/checkout@v4.1.0 + uses: actions/checkout@v4.2.2 - name: Opening pull request run: gh pr create -B development -H master --title 'Sync master back into development' --body 'Created by Github action' --label 'Internal' env: diff --git a/.github/workflows/version_bump.yml b/.github/workflows/version_bump.yml index 9d1d893f..8aeecab5 100644 --- a/.github/workflows/version_bump.yml +++ b/.github/workflows/version_bump.yml @@ -17,9 +17,10 @@ jobs: LATEST_TAG=$(gh -R $REPO release list -L 1 | awk '{printf $3}') echo "Latest version tag for releases in $REPO is $LATEST_TAG" echo "latest_tag=$LATEST_TAG" >> $GITHUB_ENV + [[ -z ${LATEST_TAG} ]] && echo "Error: LATEST_TAG is empty, aborting" && exit 1 - name: Checkout code - uses: actions/checkout@v4.1.0 + uses: actions/checkout@v4.2.2 with: ref: 'development' diff --git a/padd.sh b/padd.sh index edb1d527..f4524dad 100755 --- a/padd.sh +++ b/padd.sh @@ -15,18 +15,15 @@ export LC_NUMERIC=C ############################################ VARIABLES ############################################# # VERSION -padd_version="v3.11.1" +padd_version="v4.0.0" # LastChecks -LastCheckVersionInformation=$(date +%s) -LastCheckNetworkInformation=$(date +%s) -LastCheckSummaryInformation=$(date +%s) -LastCheckPiholeInformation=$(date +%s) -LastCheckSystemInformation=$(date +%s) LastCheckPADDInformation=$(date +%s) +LastCheckFullInformation=$(date +%s) +LastCheckNetworkInformation=$(date +%s) -# CORES -core_count=$(nproc --all 2> /dev/null) +# padd_data holds the data returned by FTL's /padd endpoint globally +padd_data="" # COLORS CSI="$(printf '\033')[" # Control Sequence Introducer @@ -55,45 +52,40 @@ pico_status_ok="${check_box_good} Sys. OK" pico_status_update="${check_box_info} Update" pico_status_hot="${check_box_bad} Sys. Hot!" pico_status_off="${check_box_info} No blck" -pico_status_ftl_down="${check_box_bad} FTL Down" +pico_status_ftl_down="${check_box_bad} No CXN" pico_status_dns_down="${check_box_bad} DNS Down" -pico_status_unknown="${check_box_question} Stat. Unk." # MINI STATUS mini_status_ok="${check_box_good} System OK" mini_status_update="${check_box_info} Update avail." mini_status_hot="${check_box_bad} System is hot!" mini_status_off="${check_box_info} No blocking!" -mini_status_ftl_down="${check_box_bad} FTL down!" +mini_status_ftl_down="${check_box_bad} No connection!" mini_status_dns_down="${check_box_bad} DNS off!" -mini_status_unknown="${check_box_question} Status unknown" # REGULAR STATUS full_status_ok="${check_box_good} System is healthy" full_status_update="${check_box_info} Updates are available" full_status_hot="${check_box_bad} System is hot!" full_status_off="${check_box_info} Blocking is disabled" -full_status_ftl_down="${check_box_bad} FTL is down!" +full_status_ftl_down="${check_box_bad} No connection!" full_status_dns_down="${check_box_bad} DNS is off!" -full_status_unknown="${check_box_question} Status unknown!" # MEGA STATUS mega_status_ok="${check_box_good} Your system is healthy" mega_status_update="${check_box_info} Updates are available" mega_status_hot="${check_box_bad} Your system is hot!" mega_status_off="${check_box_info} Blocking is disabled!" -mega_status_ftl_down="${check_box_bad} FTLDNS service is not running!" +mega_status_ftl_down="${check_box_bad} No connection to FTL!" mega_status_dns_down="${check_box_bad} Pi-hole's DNS server is off!" -mega_status_unknown="${check_box_question} Unable to determine Pi-hole status!" # TINY STATUS tiny_status_ok="${check_box_good} System is healthy" tiny_status_update="${check_box_info} Updates are available" tiny_status_hot="${check_box_bad} System is hot!" tiny_status_off="${check_box_info} Blocking is disabled" -tiny_status_ftl_down="${check_box_bad} FTL is down!" +tiny_status_ftl_down="${check_box_bad} No connection to FTL!" tiny_status_dns_down="${check_box_bad} DNS is off!" -tiny_status_unknown="${check_box_question} Status unknown!" # Text only "logos" padd_text="${green_text}${bold_text}PADD${reset_text}" @@ -106,321 +98,550 @@ padd_logo_retro_1="${bold_text} ${yellow_text}_${green_text}_ ${blue_text}_ padd_logo_retro_2="${bold_text}${yellow_text}|${green_text}_${blue_text}_${cyan_text}) ${red_text}/${yellow_text}\\ ${blue_text}| ${red_text}\\${yellow_text}| ${cyan_text}\\ ${reset_text}" padd_logo_retro_3="${bold_text}${green_text}| ${red_text}/${yellow_text}-${green_text}-${blue_text}\\${cyan_text}|${magenta_text}_${red_text}_${yellow_text}/${green_text}|${blue_text}_${cyan_text}_${magenta_text}/ ${reset_text}" +############################################# FTL ################################################## -############################################# GETTERS ############################################## +TestAPIAvailability() { -GetFTLData() { - local ftl_port data - ftl_port=$(getFTLAPIPort) - if [ -n "$ftl_port" ]; then - # Send command to FTL and ask to quit when finished - data="$(echo ">$1 >quit" | nc 127.0.0.1 "${ftl_port}")" - echo "${data}" + local chaos_api_list availabilityResponse cmdResult digReturnCode + + # Query the API URLs from FTL using CHAOS TXT + # The result is a space-separated enumeration of full URLs + # e.g., "http://localhost:80/api" or "https://domain.com:443/api" + if [ -z "${SERVER}" ] || [ "${SERVER}" = "localhost" ] || [ "${SERVER}" = "127.0.0.1" ]; then + # --server was not set or set to local, assuming we're running locally + cmdResult="$(dig +short chaos txt local.api.ftl @localhost 2>&1; echo $?)" + else + # --server was set, try to get response from there + cmdResult="$(dig +short chaos txt domain.api.ftl @"${SERVER}" 2>&1; echo $?)" fi -} -GetSummaryInformation() { - summary=$(GetFTLData "stats") - cache_info=$(GetFTLData "cacheinfo") + # Gets the return code of the dig command (last line) + # We can't use${cmdResult##*$'\n'*} here as $'..' is not POSIX + digReturnCode="$(echo "${cmdResult}" | tail -n 1)" - clients=$(echo "${summary}" | grep "unique_clients" | grep -Eo "[0-9]+$") + if [ ! "${digReturnCode}" = "0" ]; then + # If the query was not successful + moveXOffset; echo "API not available. Please check server address and connectivity" + exit 1 + else + # Dig returned 0 (success), so get the actual response (first line) + chaos_api_list="$(echo "${cmdResult}" | head -n 1)" + fi - blocking_status=$(echo "${summary}" | grep "status" | grep -Eo "enabled|disabled|unknown" ) + # Iterate over space-separated list of URLs + while [ -n "${chaos_api_list}" ]; do + # Get the first URL + API_URL="${chaos_api_list%% *}" + # Strip leading and trailing quotes + API_URL="${API_URL%\"}" + API_URL="${API_URL#\"}" + + # Test if the API is available at this URL + availabilityResponse=$(curl --connect-timeout 2 -skS -o /dev/null -w "%{http_code}" "${API_URL}auth") + + # Test if http status code was 200 (OK) or 401 (authentication required) + if [ ! "${availabilityResponse}" = 200 ] && [ ! "${availabilityResponse}" = 401 ]; then + # API is not available at this port/protocol combination + API_PORT="" + else + # API is available at this URL combination - domains_being_blocked_raw=$(echo "${summary}" | grep "domains_being_blocked" | grep -Eo "[0-9]+$") - domains_being_blocked=$(printf "%.f" "${domains_being_blocked_raw}") + if [ "${availabilityResponse}" = 200 ]; then + # API is available without authentication + needAuth=false + fi - dns_queries_today_raw=$(echo "$summary" | grep "dns_queries_today" | grep -Eo "[0-9]+$") - dns_queries_today=$(printf "%.f" "${dns_queries_today_raw}") + break + fi - ads_blocked_today_raw=$(echo "$summary" | grep "ads_blocked_today" | grep -Eo "[0-9]+$") - ads_blocked_today=$(printf "%.f" "${ads_blocked_today_raw}") + # Remove the first URL from the list + local last_api_list + last_api_list="${chaos_api_list}" + chaos_api_list="${chaos_api_list#* }" - ads_percentage_today_raw=$(echo "$summary" | grep "ads_percentage_today" | grep -Eo "[0-9.]+$") - ads_percentage_today=$(printf "%.1f" "${ads_percentage_today_raw}") + # If the list did not change, we are at the last element + if [ "${last_api_list}" = "${chaos_api_list}" ]; then + # Remove the last element + chaos_api_list="" + fi + done - cache_size=$(echo "$cache_info" | grep "cache-size" | grep -Eo "[0-9.]+$") - cache_deletes=$(echo "$cache_info" | grep "cache-live-freed" | grep -Eo "[0-9.]+$") - cache_inserts=$(echo "$cache_info"| grep "cache-inserted" | grep -Eo "[0-9.]+$") + # if API_PORT is empty, no working API port was found + if [ -n "${API_PORT}" ]; then + moveXOffset; echo "API not available at: ${API_URL}" + moveXOffset; echo "Exiting." + exit 1 + fi +} - latest_blocked_raw=$(GetFTLData recentBlocked) +LoginAPI() { + # Exit early if no authentication is required + if [ "${needAuth}" = false ]; then + moveXOffset; echo "No password required." + return + fi - top_blocked_raw=$(GetFTLData "top-ads (1)" | awk '{print $3}') + # Try to read the CLI password (if enabled and readable by the current user) + if [ -r /etc/pihole/cli_pw ]; then + password=$(cat /etc/pihole/cli_pw) - top_domain_raw=$(GetFTLData "top-domains (1)" | awk '{print $3}') + # Try to authenticate using the CLI password + Authenticate + fi + + # If this did not work, ask the user for the password + while [ "${validSession}" = false ] || [ -z "${validSession}" ] ; do + moveXOffset; echo "Authentication failed." + + # no password was supplied as argument + if [ -z "${password}" ]; then + moveXOffset; echo "No password supplied. Please enter your password:" + else + moveXOffset; echo "Wrong password supplied, please enter the correct password:" + fi + + # secretly read the password + moveXOffset; secretRead; printf '\n' + + # Try to authenticate again + Authenticate + done + + # Loop exited, authentication was successful + moveXOffset; echo "Authentication successful." - top_client_raw=$(GetFTLData "top-clients (1)" | awk '{print $4}') - if [ -z "${top_client_raw}" ]; then - # if no hostname was supplied, use IP - top_client_raw=$(GetFTLData "top-clients (1)" | awk '{print $3}') - fi } -GetSystemInformation() { - # System uptime - system_uptime_raw=$(uptime) +DeleteSession() { + # if a valid Session exists (no password required or successful authenthication) and + # SID is not null (successful authenthication only), delete the session + if [ "${validSession}" = true ] && [ ! "${SID}" = null ]; then + # Try to delete the session. Omit the output, but get the http status code + deleteResponse=$(curl --connect-timeout 2 -skS -o /dev/null -w "%{http_code}" -X DELETE "${API_URL}auth" -H "Accept: application/json" -H "sid: ${SID}") + + printf "\n\n" + case "${deleteResponse}" in + "204") moveXOffset; printf "%b" "Session successfully deleted.\n";; + "401") moveXOffset; printf "%b" "Logout attempt without a valid session. Unauthorized!\n";; + esac; + else + # no session to delete, just print a newline for nicer output + echo + fi - # reset $cpu variable - unset cpu +} - # CPU temperature - if [ -d "/sys/devices/platform/coretemp.0/hwmon/" ]; then - cpu=$(cat "$(find /sys/devices/platform/coretemp.0/hwmon/ -maxdepth 2 -name "temp1_input" 2>/dev/null | head -1)" 2>/dev/null) - fi - if [ -z "${cpu}" ] && [ -f /sys/class/thermal/thermal_zone0/temp ]; then - cpu=$(cat /sys/class/thermal/thermal_zone0/temp) - fi - if [ -z "${cpu}" ] && [ -f /sys/class/hwmon/hwmon0/temp1_input ]; then - cpu=$(cat /sys/class/hwmon/hwmon0/temp1_input) - fi - if [ -z "${cpu}" ]; then - cpu=0 +Authenticate() { + sessionResponse="$(curl --connect-timeout 2 -skS -X POST "${API_URL}auth" --user-agent "PADD ${padd_version}" --data "{\"password\":\"${password}\"}" )" + + if [ -z "${sessionResponse}" ]; then + moveXOffset; echo "No response from FTL server. Please check connectivity and use the options to set the API URL" + moveXOffset; echo "Usage: $0 [--server ]" + exit 1 fi + # obtain validity and session ID from session response + validSession=$(echo "${sessionResponse}"| jq .session.valid 2>/dev/null) + SID=$(echo "${sessionResponse}"| jq --raw-output .session.sid 2>/dev/null) +} - # Convert CPU temperature to correct unit - if [ "${TEMPERATUREUNIT}" = "F" ]; then - temperature="$(printf %.1f "$(echo "${cpu}" | awk '{print $1 * 9 / 5000 + 32}')")°F" - elif [ "${TEMPERATUREUNIT}" = "K" ]; then - temperature="$(printf %.1f "$(echo "${cpu}" | awk '{print $1 / 1000 + 273.15}')")°K" - else - temperature="$(printf %.1f "$(echo "${cpu}" | awk '{print $1 / 1000}')")°C" +GetFTLData() { + local response + local data + local status + + # get the data from querying the API as well as the http status code + response=$(curl --connect-timeout 2 -sk -w "%{http_code}" -X GET "${API_URL}$1$2" -H "Accept: application/json" -H "sid: ${SID}" ) + + # status are the last 3 characters + # not using ${response#"${response%???}"}" here because it's extremely slow on big responses + status=$(printf "%s" "${response}" | tail -c 3) + # data is everything from response without the last 3 characters + data=$(printf %s "${response%???}") + + if [ "${status}" = 200 ]; then + echo "${data}" + elif [ "${status}" = 000 ]; then + # connection lost + echo "000" + elif [ "${status}" = 401 ]; then + # unauthorized + echo "401" fi +} - # CPU load, heatmap - cpu_load_1=$(awk '{print $1}' < /proc/loadavg) - cpu_load_5=$(awk '{print $2}' < /proc/loadavg) - cpu_load_15=$(awk '{print $3}' < /proc/loadavg) - cpu_load_1_heatmap=$(HeatmapGenerator "${cpu_load_1}" "${core_count}") - cpu_load_5_heatmap=$(HeatmapGenerator "${cpu_load_5}" "${core_count}") - cpu_load_15_heatmap=$(HeatmapGenerator "${cpu_load_15}" "${core_count}") - cpu_percent=$(printf %.1f "$(echo "${cpu_load_1} ${core_count}" | awk '{print ($1 / $2) * 100}')") - - # CPU temperature heatmap - hot_flag=false - # If we're getting close to 85°C... (https://www.raspberrypi.org/blog/introducing-turbo-mode-up-to-50-more-performance-for-free/) - if [ ${cpu} -gt 80000 ]; then - temp_heatmap=${blinking_text}${red_text} - # set flag to change the status message in SetStatusMessage() - hot_flag=true - elif [ ${cpu} -gt 70000 ]; then - temp_heatmap=${magenta_text} - elif [ ${cpu} -gt 60000 ]; then - temp_heatmap=${blue_text} + +############################################# GETTERS ############################################## + +GetPADDData() { + local response + response=$(GetFTLData "padd" "$1") + + if [ "${response}" = 000 ]; then + # connection lost + padd_data="000" + elif [ "${response}" = 401 ]; then + # unauthorized + padd_data="401" else - temp_heatmap=${cyan_text} + # Iterate over all the leaf paths in the JSON object and creates key-value + # pairs in the format "key=value". Nested objects are flattened using the dot + # notation, e.g., { "a": { "b": 1 } } becomes "a.b=1". + # We cannot use leaf_paths here as it was deprecated in jq 1.6 and removed in + # current master + # Using "paths(scalars | true)" will return null and false values. + # We also check if the value is exactly `null` and, in this case, return the + # string "null", as jq would return an empty string for nulls. + padd_data=$(echo "$response" | jq -r 'paths(scalars | true) as $p | [$p | join(".")] + [if getpath($p)!=null then getpath($p) else "null" end] | join("=")' 2>/dev/null) fi +} - # Memory use, heatmap and bar - memory_percent=$(awk '/MemTotal:/{total=$2} /MemFree:/{free=$2} /Buffers:/{buffers=$2} /^Cached:/{cached=$2} END {printf "%.1f", (total-free-buffers-cached)*100/total}' '/proc/meminfo') - memory_heatmap=$(HeatmapGenerator "${memory_percent}") +GetPADDValue() { + echo "$padd_data" | sed -n "s/^$1=//p" 2>/dev/null +} - # Get product name and family - product_name= - product_family= - if [ -f /sys/devices/virtual/dmi/id/product_name ]; then - # Get product name, remove possible null byte - product_name=$(tr -d '\0' < /sys/devices/virtual/dmi/id/product_name) - fi - if [ -f /sys/devices/virtual/dmi/id/product_family ]; then - # Get product family, remove possible null byte - product_family=$(tr -d '\0' < /sys/devices/virtual/dmi/id/product_family) - fi +GetSummaryInformation() { + if [ "${connection_down_flag}" = true ]; then + clients="N/A" + blocking_enabled="N/A" + domains_being_blocked="N/A" + dns_queries_today="N/A" + ads_blocked_today="N/A" + ads_percentage_today="N/A" + cache_size="N/A" + cache_evictions="N/A" + cache_inserts="N/A" + latest_blocked_raw="N/A" + top_blocked_raw="N/A" + top_domain_raw="N/A" + top_client_raw="N/A" + return + fi - board_vendor= - board_name= - if [ -f /sys/devices/virtual/dmi/id/board_vendor ]; then - board_vendor=$(tr -d '\0' < /sys/devices/virtual/dmi/id/board_vendor) - fi - if [ -f /sys/devices/virtual/dmi/id/board_name ]; then - board_name="$(tr -d '\0' < /sys/devices/virtual/dmi/id/board_name)" - fi + clients=$(GetPADDValue active_clients) - if [ -n "$product_name" ] || [ -n "$product_family" ]; then - if echo "$product_family" | grep -q "$product_name"; then - # If product_name is contained in product_family, only show product_family - sys_model="${product_family}" + blocking_enabled=$(GetPADDValue blocking) + + domains_being_blocked_raw=$(GetPADDValue gravity_size) + domains_being_blocked=$(printf "%.f" "${domains_being_blocked_raw}") + + dns_queries_today_raw=$(GetPADDValue queries.total) + dns_queries_today=$(printf "%.f" "${dns_queries_today_raw}") + + ads_blocked_today_raw=$(GetPADDValue queries.blocked) + ads_blocked_today=$(printf "%.f" "${ads_blocked_today_raw}") + + ads_percentage_today_raw=$(GetPADDValue queries.percent_blocked) + ads_percentage_today=$(printf "%.1f" "${ads_percentage_today_raw}") + + cache_size=$(GetPADDValue cache.size) + cache_evictions=$(GetPADDValue cache.evicted) + cache_inserts=$(echo "${padd_data}"| GetPADDValue cache.inserted) + + latest_blocked_raw=$(GetPADDValue recent_blocked) + + top_blocked_raw=$(GetPADDValue top_blocked) + + top_domain_raw=$(GetPADDValue top_domain) + + top_client_raw=$(GetPADDValue top_client) +} + +GetSystemInformation() { + + if [ "${connection_down_flag}" = true ]; then + system_uptime_raw=0 + temperature="N/A" + temp_heatmap=${reset_text} + + cpu_load_1="N/A" + cpu_load_5="N/A" + cpu_load_15="N/A" + cpu_load_1_heatmap=${reset_text} + cpu_load_5_heatmap=${reset_text} + cpu_load_15_heatmap=${reset_text} + cpu_percent=0 + + memory_percent=0 + memory_heatmap=${reset_text} + + sys_model="N/A" + return + fi + + # System uptime + system_uptime_raw=$(GetPADDValue system.uptime) + + # CPU temperature and unit + cpu_temp_raw=$(GetPADDValue sensors.cpu_temp) + cpu_temp=$(printf "%.1f" "${cpu_temp_raw}") + temp_unit=$(echo "${padd_data}" | GetPADDValue sensors.unit) + + # Temp + Unit + if [ "${temp_unit}" = "C" ]; then + temperature="${cpu_temp}°${temp_unit}" + # no conversion needed + cpu_temp_celsius="$(echo "${cpu_temp}" | awk -F '.' '{print $1}')" + elif [ "${temp_unit}" = "F" ]; then + temperature="${cpu_temp}°${temp_unit}" + # convert to Celsius for limit checking + cpu_temp_celsius="$(echo "${cpu_temp}" | awk '{print ($1-32) * 5 / 9}' | awk -F '.' '{print $1}')" + elif [ "${temp_unit}" = "K" ]; then + # no ° for Kelvin + temperature="${cpu_temp}${temp_unit}" + # convert to Celsius for limit checking + cpu_temp_celsius="$(echo "${cpu_temp}" | awk '{print $1 - 273.15}' | awk -F '.' '{print $1}')" + else # unknown unit + temperature="${cpu_temp}°?" + # no conversion needed + cpu_temp_celsius=0 + fi + + # CPU temperature heatmap + hot_flag=false + # If we're getting close to 85°C... (https://www.raspberrypi.org/blog/introducing-turbo-mode-up-to-50-more-performance-for-free/) + if [ "${cpu_temp_celsius}" -gt 80 ]; then + temp_heatmap=${blinking_text}${red_text} + # set flag to change the status message in SetStatusMessage() + hot_flag=true + elif [ "${cpu_temp_celsius}" -gt 70 ]; then + temp_heatmap=${magenta_text} + elif [ "${cpu_temp_celsius}" -gt 60 ]; then + temp_heatmap=${blue_text} else - # If product_name is not contained in product_family, both are shown - sys_model="${product_family} ${product_name}" + temp_heatmap=${cyan_text} fi - elif [ -f /sys/firmware/devicetree/base/model ]; then - sys_model=$(tr -d '\0' < /sys/firmware/devicetree/base/model) - elif [ -n "$board_vendor" ] || [ -n "$board_name" ]; then - sys_model="${board_vendor} ${board_name}" - elif [ -f /tmp/sysinfo/model ]; then - sys_model=$(tr -d '\0' < /tmp/sysinfo/model) - elif [ -n "${DOCKER_VERSION}" ]; then - # Docker image. DOCKER_VERSION is read from /etc/pihole/versions - sys_model="Container" - fi - # Cleaning device model from useless OEM information - sys_model=$(filterModel "${sys_model}") + # CPU, load, heatmap + core_count=$(GetPADDValue system.cpu.nprocs) + cpu_load_1=$(printf %.2f "$(GetPADDValue system.cpu.load.raw.[0])") + cpu_load_5=$(printf %.2f "$(GetPADDValue system.cpu.load.raw.[1])") + cpu_load_15=$(printf %.2f "$(GetPADDValue system.cpu.load.raw.[2])") + cpu_load_1_heatmap=$(HeatmapGenerator "${cpu_load_1}" "${core_count}") + cpu_load_5_heatmap=$(HeatmapGenerator "${cpu_load_5}" "${core_count}") + cpu_load_15_heatmap=$(HeatmapGenerator "${cpu_load_15}" "${core_count}") + cpu_percent=$(printf %.1f "$(GetPADDValue system.cpu.load.percent.0)") + + # Memory use, heatmap and bar + memory_percent_raw="$(GetPADDValue system.memory.ram.%used)" + memory_percent=$(printf %.1f "${memory_percent_raw}") + memory_heatmap="$(HeatmapGenerator "${memory_percent}")" + + # Get device model + sys_model="$(GetPADDValue host_model)" + + # DOCKER_VERSION is set during GetVersionInformation, so this needs to run first during startup + if [ ! "${DOCKER_VERSION}" = "null" ]; then + # Docker image + sys_model="Container" + fi - if [ -z "$sys_model" ]; then - sys_model="Unknown" - fi + # Cleaning device model from useless OEM information + sys_model=$(filterModel "${sys_model}") + + # FTL returns null if device information is not available + if [ -z "$sys_model" ] || [ "$sys_model" = "null" ]; then + sys_model="N/A" + fi } GetNetworkInformation() { - # Get pi IPv4 address - pi_ip4_addrs="$(ip addr | grep 'inet ' | grep -v '127.0.0.1/8' | awk '{print $2}' | cut -f1 -d'/' |wc -l)" - if [ "${pi_ip4_addrs}" -eq 0 ]; then - # No IPv4 address available - pi_ip4_addr="N/A" - elif [ "${pi_ip4_addrs}" -eq 1 ]; then - # One IPv4 address available - pi_ip4_addr="$(ip addr | grep 'inet ' | grep -v '127.0.0.1/8' | awk '{print $2}' | cut -f1 -d'/' | head -n 1)" - else - # More than one IPv4 address available - pi_ip4_addr="$(ip addr | grep 'inet ' | grep -v '127.0.0.1/8' | awk '{print $2}' | cut -f1 -d'/' | head -n 1)+" - fi - - # Get pi IPv6 address - pi_ip6_addrs="$(ip addr | grep 'inet6 ' | grep -v '::1/128' | awk '{print $2}' | cut -f1 -d'/' | wc -l)" - if [ "${pi_ip6_addrs}" -eq 0 ]; then - # No IPv6 address available - pi_ip6_addr="N/A" - ipv6_check_box=${check_box_bad} - elif [ "${pi_ip6_addrs}" -eq 1 ]; then - # One IPv6 address available - pi_ip6_addr="$(ip addr | grep 'inet6 ' | grep -v '::1/128' | awk '{print $2}' | cut -f1 -d'/' | head -n 1)" - ipv6_check_box=${check_box_good} - else - # More than one IPv6 address available - pi_ip6_addr="$(ip addr | grep 'inet6 ' | grep -v '::1/128' | awk '{print $2}' | cut -f1 -d'/' | head -n 1)+" - ipv6_check_box=${check_box_good} - fi + if [ "${connection_down_flag}" = true ]; then + iface_name="N/A" + pi_ip4_addr="N/A" + pi_ip6_addr="N/A" + ipv6_check_box=${check_box_question} + + dhcp_status="N/A" + dhcp_heatmap=${reset_text} + dhcp_info="" + dhcp_ipv6_status="N/A" + dhcp_ipv6_heatmap=${reset_text} + dhcp_ipv6_check_box=${check_box_question} + + pi_hostname="N/A" + full_hostname="N/A" + + dns_count="N/A" + dns_information="N/A" + + dnssec_status="N/A" + dnssec_heatmap=${reset_text} + + conditional_forwarding_status="N/A" + conditional_forwarding_heatmap=${reset_text} + + tx_bytes="N/A" + rx_bytes="N/A" + return + fi - # Get hostname and gateway - pi_hostname=$(hostname) + gateway_v4_iface=$(GetPADDValue iface.v4.name) + gateway_v6_iface=$(GetPADDValue iface.v4.name) + + # Get IPv4 address of the default interface + pi_ip4_addrs="$(GetPADDValue iface.v4.num_addrs)" + pi_ip4_addr="$(GetPADDValue iface.v4.addr)" + if [ "${pi_ip4_addrs}" -eq 0 ]; then + # No IPv4 address available + pi_ip4_addr="N/A" + #elif [ "${pi_ip4_addrs}" -eq 1 ]; then + # # One IPv4 address available + else + # More than one IPv4 address available + pi_ip4_addr="${pi_ip4_addr}+" + fi - full_hostname=${pi_hostname} - # does the Pi-hole have a domain set? - if [ -n "${PIHOLE_DOMAIN+x}" ]; then - # is Pi-hole acting as DHCP server? - if [ "${DHCP_ACTIVE}" = "true" ]; then - count=${pi_hostname}"."${PIHOLE_DOMAIN} - count=${#count} - if [ "${count}" -lt "18" ]; then - full_hostname=${pi_hostname}"."${PIHOLE_DOMAIN} - fi + # Get IPv6 address of the default interface + pi_ip6_addrs="$(GetPADDValue iface.v6.num_addrs)" + pi_ip6_addr="$(GetPADDValue iface.v6.addr)" + if [ "${pi_ip6_addrs}" -eq 0 ]; then + # No IPv6 address available + pi_ip6_addr="N/A" + ipv6_check_box=${check_box_bad} + elif [ "${pi_ip6_addrs}" -eq 1 ]; then + # One IPv6 address available + ipv6_check_box=${check_box_good} + else + # More than one IPv6 address available + pi_ip6_addr="${pi_ip6_addr}+" + ipv6_check_box=${check_box_good} fi - fi - # Get the DNS count (from pihole -c) - dns_count="0" - [ -n "${PIHOLE_DNS_1}" ] && dns_count=$((dns_count+1)) - [ -n "${PIHOLE_DNS_2}" ] && dns_count=$((dns_count+1)) - [ -n "${PIHOLE_DNS_3}" ] && dns_count=$((dns_count+1)) - [ -n "${PIHOLE_DNS_4}" ] && dns_count=$((dns_count+1)) - [ -n "${PIHOLE_DNS_5}" ] && dns_count=$((dns_count+1)) - [ -n "${PIHOLE_DNS_6}" ] && dns_count=$((dns_count+1)) - [ -n "${PIHOLE_DNS_7}" ] && dns_count=$((dns_count+1)) - [ -n "${PIHOLE_DNS_8}" ] && dns_count=$((dns_count+1)) - [ -n "${PIHOLE_DNS_9}" ] && dns_count=$((dns_count+1)) - - # if there's only one DNS server - if [ ${dns_count} -eq 1 ]; then - dns_information="1 server" - else - dns_information="${dns_count} servers" - fi + # Is Pi-Hole acting as the DHCP server? + DHCP_ACTIVE="$(GetPADDValue config.dhcp_active )" - # Is Pi-Hole acting as the DHCP server? - if [ "${DHCP_ACTIVE}" = "true" ]; then - dhcp_status="Enabled" - dhcp_info=" Range: ${DHCP_START} - ${DHCP_END}" - dhcp_heatmap=${green_text} - dhcp_check_box=${check_box_good} - - # Is DHCP handling IPv6? - # DHCP_IPv6 is set in setupVars.conf - # shellcheck disable=SC2154 - if [ "${DHCP_IPv6}" = "true" ]; then - dhcp_ipv6_status="Enabled" - dhcp_ipv6_heatmap=${green_text} - dhcp_ipv6_check_box=${check_box_good} + if [ "${DHCP_ACTIVE}" = "true" ]; then + DHCP_START="$(GetPADDValue config.dhcp_start)" + DHCP_END="$(GetPADDValue config.dhcp_end)" + + dhcp_status="Enabled" + dhcp_info=" Range: ${DHCP_START} - ${DHCP_END}" + dhcp_heatmap=${green_text} + dhcp_check_box=${check_box_good} + + # Is DHCP handling IPv6? + DHCP_IPv6="$(GetPADDValue config.dhcp_ipv6)" + if [ "${DHCP_IPv6}" = "true" ]; then + dhcp_ipv6_status="Enabled" + dhcp_ipv6_heatmap=${green_text} + dhcp_ipv6_check_box=${check_box_good} + else + dhcp_ipv6_status="Disabled" + dhcp_ipv6_heatmap=${red_text} + dhcp_ipv6_check_box=${check_box_bad} + fi else - dhcp_ipv6_status="Disabled" - dhcp_ipv6_heatmap=${red_text} - dhcp_ipv6_check_box=${check_box_bad} + dhcp_status="Disabled" + dhcp_heatmap=${red_text} + dhcp_check_box=${check_box_bad} + + # Display the gateway address if DHCP is disabled + GATEWAY="$(GetPADDValue iface.v4.gw_addr | head -n 1)" + dhcp_info=" Router: ${GATEWAY}" + dhcp_ipv6_status="N/A" + dhcp_ipv6_heatmap=${yellow_text} + dhcp_ipv6_check_box=${check_box_question} fi - else - dhcp_status="Disabled" - dhcp_heatmap=${red_text} - dhcp_check_box=${check_box_bad} - # if the DHCP Router variable isn't set - if [ -z ${DHCP_ROUTER+x} ]; then - DHCP_ROUTER=$(GetFTLData "gateway" | awk '{ printf $1 }') + # Get hostname + pi_hostname="$(GetPADDValue node_name)" + full_hostname=${pi_hostname} + # when PI-hole is the DHCP server, append the domain to the hostname + if [ "${DHCP_ACTIVE}" = "true" ]; then + PIHOLE_DOMAIN="$(GetPADDValue config.dns_domain)" + if [ -n "${PIHOLE_DOMAIN}" ]; then + count=${pi_hostname}"."${PIHOLE_DOMAIN} + count=${#count} + if [ "${count}" -lt "18" ]; then + full_hostname=${pi_hostname}"."${PIHOLE_DOMAIN} + fi + fi fi - dhcp_info=" Router: ${DHCP_ROUTER}" - dhcp_heatmap=${red_text} - dhcp_check_box=${check_box_bad} + # Get the number of configured upstream DNS servers + dns_count="$(GetPADDValue config.dns_num_upstreams)" + # if there's only one DNS server + if [ "${dns_count}" -eq 1 ]; then + dns_information="1 server" + else + dns_information="${dns_count} servers" + fi - dhcp_ipv6_status="N/A" - dhcp_ipv6_heatmap=${yellow_text} - dhcp_ipv6_check_box=${check_box_question} - fi - # DNSSEC - if [ "${DNSSEC}" = "true" ]; then - dnssec_status="Enabled" - dnssec_heatmap=${green_text} - else - dnssec_status="Disabled" - dnssec_heatmap=${red_text} - fi + # DNSSEC + DNSSEC="$(GetPADDValue config.dns_dnssec)" + if [ "${DNSSEC}" = "true" ]; then + dnssec_status="Enabled" + dnssec_heatmap=${green_text} + else + dnssec_status="Disabled" + dnssec_heatmap=${red_text} + fi - # Conditional forwarding - if [ "${CONDITIONAL_FORWARDING}" = "true" ] || [ "${REV_SERVER}" = "true" ]; then - conditional_forwarding_status="Enabled" - conditional_forwarding_heatmap=${green_text} - else - conditional_forwarding_status="Disabled" - conditional_forwarding_heatmap=${red_text} - fi + # Conditional forwarding + CONDITIONAL_FORWARDING="$(GetPADDValue config.dns_revServer_active)" + if [ "${CONDITIONAL_FORWARDING}" = "true" ]; then + conditional_forwarding_status="Enabled" + conditional_forwarding_heatmap=${green_text} + else + conditional_forwarding_status="Disabled" + conditional_forwarding_heatmap=${red_text} + fi - #Default interface data - def_iface_data=$(GetFTLData "interfaces" | head -n1) - iface_name="$(echo "$def_iface_data" | awk '{print $1}')" - tx_bytes="$(echo "$def_iface_data" | awk '{print $4}')" - rx_bytes="$(echo "$def_iface_data" | awk '{print $5}')" + # Default interface data (use IPv4 interface - we cannot show both and assume they are the same) + iface_name="${gateway_v4_iface}" + tx_bytes="$(GetPADDValue iface.v4.tx_bytes.value)" + tx_bytes_unit="$(GetPADDValue iface.v4.tx_bytes.unit)" + tx_bytes=$(printf "%.1f %b" "${tx_bytes}" "${tx_bytes_unit}") + + rx_bytes="$(GetPADDValue iface.v4.rx_bytes.value)" + rx_bytes_unit="$(GetPADDValue iface.v4.rx_bytes.unit)" + rx_bytes=$(printf "%.1f %b" "${rx_bytes}" "${rx_bytes_unit}") + + # If IPv4 and IPv6 interfaces are not the same, add a "*" to the interface + # name to highlight that there are two different interfaces and the + # displayed statistics are only for the IPv4 interface, while the IPv6 + # address correctly corresponds to the default IPv6 interface + if [ ! "${gateway_v4_iface}" = "${gateway_v6_iface}" ]; then + iface_name="${iface_name}*" + fi } GetPiholeInformation() { - # Get FTL status - - # Get FTL's current PID - ftlPID="$(getFTLPID)" + if [ "${connection_down_flag}" = true ]; then + ftl_status="No connection" + ftl_heatmap=${red_text} + ftl_check_box=${check_box_bad} + ftl_cpu="N/A" + ftl_mem_percentage="N/A" + dns_status="DNS offline" + dns_heatmap=${red_text} + dns_check_box=${check_box_bad} + ftlPID="N/A" + dns_down_flag=true + + return + fi - # If FTL is not running (getFTLPID returns -1), set all variables to "not running" - ftl_down_flag=false - if [ "${ftlPID}" = "-1" ]; then - ftl_status="Not running" - ftl_heatmap=${red_text} - ftl_check_box=${check_box_bad} - # set flag to change the status message in SetStatusMessage() - ftl_down_flag=true - ftl_cpu="N/A" - ftl_mem_percentage="N/A" - else ftl_status="Running" ftl_heatmap=${green_text} ftl_check_box=${check_box_good} # Get FTL CPU and memory usage - ftl_cpu="$(ps h -p "${ftlPID}" -o %cpu | tr -d '[:space:]')%" - ftl_mem_percentage="$(ps h -p "${ftlPID}" -o %mem | tr -d '[:space:]')%" + ftl_cpu_raw="$(GetPADDValue "%cpu")" + ftl_mem_percentage_raw="$(GetPADDValue "%mem")" + ftl_cpu="$(printf "%.1f" "${ftl_cpu_raw}")%" + ftl_mem_percentage="$(printf "%.1f" "${ftl_mem_percentage_raw}")%" # Get Pi-hole (blocking) status - ftl_dns_port=$(GetFTLData "dns-port") - fi + ftl_dns_port=$(GetPADDValue config.dns_port) + # Get FTL's current PID + ftlPID="$(GetPADDValue pid)" + - # ${ftl_dns_port} == 0 DNS server part of dnsmasq disabled, ${ftl_status} == "Not running" no ftlPID found + + # ${ftl_dns_port} == 0 DNS server part of dnsmasq disabled dns_down_flag=false - if [ "${ftl_dns_port}" = 0 ] || [ "${ftl_status}" = "Not running" ]; then + if [ "${ftl_dns_port}" = 0 ]; then dns_status="DNS offline" dns_heatmap=${red_text} dns_check_box=${check_box_bad} @@ -434,148 +655,175 @@ fi } GetVersionInformation() { - # Check if version status has been saved - # all info is sourced from /etc/pihole/versions + if [ "${connection_down_flag}" = true ]; then + DOCKER_VERSION=null + CORE_VERSION="N/A" + WEB_VERSION="N/A" + FTL_VERSION="N/A" + core_version_heatmap=${reset_text} + web_version_heatmap=${reset_text} + ftl_version_heatmap=${reset_text} + return + fi - out_of_date_flag=false + out_of_date_flag=false - # If PADD is running inside docker, immediately return without checking for updated component versions - if [ -n "${DOCKER_VERSION}" ]; then - docker_version_converted="$(VersionConverter "${DOCKER_VERSION}")" - docker_version_latest_converted="$(VersionConverter "${GITHUB_DOCKER_VERSION}")" + # Gather DOCKER version information... + # returns "null" if not running Pi-hole in Docker container + DOCKER_VERSION="$(GetPADDValue version.docker.local)" + + # If PADD is running inside docker, immediately return without checking for updated component versions + if [ ! "${DOCKER_VERSION}" = "null" ] ; then + GITHUB_DOCKER_VERSION="$(GetPADDValue version.docker.remote)" + docker_version_converted="$(VersionConverter "${DOCKER_VERSION}")" + docker_version_latest_converted="$(VersionConverter "${GITHUB_DOCKER_VERSION}")" # Note: the version comparison will fail for any Docker tag not following a 'YYYY.MM.VV' scheme # e.g. 'nightly', 'beta', 'v6-pre-alpha' and might set a false out_of_date_flag # As those versions are not meant to be used in production, we ignore this small bug - if [ "${docker_version_converted}" -lt "${docker_version_latest_converted}" ]; then - out_of_date_flag="true" - docker_version_heatmap=${red_text} - else - docker_version_heatmap=${green_text} + if [ "${docker_version_converted}" -lt "${docker_version_latest_converted}" ]; then + out_of_date_flag="true" + docker_version_heatmap=${red_text} + else + docker_version_heatmap=${green_text} + fi + return fi - return - fi - # Gather CORE version information... - # Extract vx.xx or vx.xx.xxx version - CORE_VERSION="$(echo "${CORE_VERSION}" | grep -oE '^v[0-9]+([.][0-9]+){1,2}')" - if [ "${CORE_BRANCH}" = "master" ]; then - core_version_converted="$(VersionConverter "${CORE_VERSION}")" - core_version_latest_converted=$(VersionConverter "${GITHUB_CORE_VERSION}") + # Gather core version information... + CORE_BRANCH="$(GetPADDValue version.core.local.branch)" + CORE_VERSION="$(GetPADDValue version.core.local.version | tr -d '[:alpha:]' | awk -F '-' '{printf $1}')" + GITHUB_CORE_VERSION="$(GetPADDValue version.core.remmote.version | tr -d '[:alpha:]' | awk -F '-' '{printf $1}')" + CORE_HASH="$(GetPADDValue version.core.local.hash)" + GITHUB_CORE_HASH="$(GetPADDValue version.core.remote.hash)" - if [ "${core_version_converted}" -lt "${core_version_latest_converted}" ]; then - out_of_date_flag="true" - core_version_heatmap=${red_text} - else - core_version_heatmap=${green_text} - fi + if [ "${CORE_BRANCH}" = "master" ]; then + core_version_converted="$(VersionConverter "${CORE_VERSION}")" + core_version_latest_converted=$(VersionConverter "${GITHUB_CORE_VERSION}") - else - # Custom branch - if [ -z "${CORE_BRANCH}" ]; then - # Branch name is empty, something went wrong - core_version_heatmap=${red_text} - CORE_VERSION="?" + if [ "${core_version_converted}" -lt "${core_version_latest_converted}" ]; then + out_of_date_flag="true" + core_version_heatmap=${red_text} + else + core_version_heatmap=${green_text} + fi else - if [ "${CORE_HASH}" = "${GITHUB_CORE_HASH}" ]; then - # up-to-date - core_version_heatmap=${green_text} - else - # out-of-date - out_of_date_flag="true" - core_version_heatmap=${red_text} - fi - # shorten common branch names (fix/, tweak/, new/) - # use the first 7 characters of the branch name as version - CORE_VERSION="$(printf '%s' "$CORE_BRANCH" | sed 's/fix\//f\//;s/new\//n\//;s/tweak\//t\//' | cut -c 1-7)" + # Custom branch + if [ -z "${CORE_BRANCH}" ]; then + # Branch name is empty, something went wrong + core_version_heatmap=${red_text} + CORE_VERSION="?" + else + if [ "${CORE_HASH}" = "${GITHUB_CORE_HASH}" ]; then + # up-to-date + core_version_heatmap=${green_text} + else + # out-of-date + out_of_date_flag="true" + core_version_heatmap=${red_text} + fi + # shorten common branch names (fix/, tweak/, new/) + # use the first 7 characters of the branch name as version + CORE_VERSION="$(printf '%s' "$CORE_BRANCH" | sed 's/fix\//f\//;s/new\//n\//;s/tweak\//t\//' | cut -c 1-7)" + fi fi - fi # Gather web version information... - # Extract vx.xx or vx.xx.xxx version - if [ "$INSTALL_WEB_INTERFACE" = true ]; then - WEB_VERSION="$(echo "${WEB_VERSION}" | grep -oE '^v[0-9]+([.][0-9]+){1,2}')" - if [ "${WEB_BRANCH}" = "master" ]; then - web_version_converted="$(VersionConverter "${WEB_VERSION}")" - web_version_latest_converted=$(VersionConverter "${GITHUB_WEB_VERSION}") - - if [ "${web_version_converted}" -lt "${web_version_latest_converted}" ]; then - out_of_date_flag="true" - web_version_heatmap=${red_text} - else - web_version_heatmap=${green_text} - fi + WEB_VERSION="$(GetPADDValue version.web.local.version)" + + if [ ! "$WEB_VERSION" = "null" ]; then + WEB_BRANCH="$(GetPADDValue version.web.local.branch)" + WEB_VERSION="$(GetPADDValue version.web.local.version | tr -d '[:alpha:]' | awk -F '-' '{printf $1}')" + GITHUB_WEB_VERSION="$(GetPADDValue version.web.remmote.version | tr -d '[:alpha:]' | awk -F '-' '{printf $1}')" + WEB_HASH="$(GetPADDValue version.web.local.hash)" + GITHUB_WEB_HASH="$(GetPADDValue version.web.remote.hash)" + + if [ "${WEB_BRANCH}" = "master" ]; then + web_version_converted="$(VersionConverter "${WEB_VERSION}")" + web_version_latest_converted=$(VersionConverter "${GITHUB_WEB_VERSION}") + + if [ "${web_version_converted}" -lt "${web_version_latest_converted}" ]; then + out_of_date_flag="true" + web_version_heatmap=${red_text} + else + web_version_heatmap=${green_text} + fi - else - # Custom branch - if [ -z "${WEB_BRANCH}" ]; then - # Branch name is empty, something went wrong - web_version_heatmap=${red_text} - WEB_VERSION="?" - else - if [ "${WEB_HASH}" = "${GITHUB_WEB_HASH}" ]; then - # up-to-date - web_version_heatmap=${green_text} else - # out-of-date - out_of_date_flag="true" - web_version_heatmap=${red_text} + # Custom branch + if [ -z "${WEB_BRANCH}" ]; then + # Branch name is empty, something went wrong + web_version_heatmap=${red_text} + WEB_VERSION="?" + else + if [ "${WEB_HASH}" = "${GITHUB_WEB_HASH}" ]; then + # up-to-date + web_version_heatmap=${green_text} + else + # out-of-date + out_of_date_flag="true" + web_version_heatmap=${red_text} + fi + # shorten common branch names (fix/, tweak/, new/) + # use the first 7 characters of the branch name as version + WEB_VERSION="$(printf '%s' "$WEB_BRANCH" | sed 's/fix\//f\//;s/new\//n\//;s/tweak\//t\//' | cut -c 1-7)" + fi fi - # shorten common branch names (fix/, tweak/, new/) - # use the first 7 characters of the branch name as version - WEB_VERSION="$(printf '%s' "$WEB_BRANCH" | sed 's/fix\//f\//;s/new\//n\//;s/tweak\//t\//' | cut -c 1-7)" - fi + else + # Web interface not installed + WEB_VERSION="N/A" + web_version_heatmap=${yellow_text} fi - else - # Web interface not installed - WEB_VERSION="N/A" - web_version_heatmap=${yellow_text} - fi - # Gather FTL version information... - # Extract vx.xx or vx.xx.xxx version - FTL_VERSION="$(echo "${FTL_VERSION}" | grep -oE '^v[0-9]+([.][0-9]+){1,2}')" - if [ "${FTL_BRANCH}" = "master" ]; then - ftl_version_converted="$(VersionConverter "${FTL_VERSION}")" - ftl_version_latest_converted=$(VersionConverter "${GITHUB_FTL_VERSION}") + # Gather FTL version information... + FTL_BRANCH="$(GetPADDValue version.ftl.local.branch)" + FTL_VERSION="$(GetPADDValue version.ftl.local.version | tr -d '[:alpha:]' | awk -F '-' '{printf $1}')" + GITHUB_FTL_VERSION="$(GetPADDValue version.ftl.remmote.version | tr -d '[:alpha:]' | awk -F '-' '{printf $1}')" + FTL_HASH="$(GetPADDValue version.ftl.local.hash)" + GITHUB_FTL_HASH="$(GetPADDValue version.ftl.remote.hash)" - if [ "${ftl_version_converted}" -lt "${ftl_version_latest_converted}" ]; then - out_of_date_flag="true" - ftl_version_heatmap=${red_text} - else - ftl_version_heatmap=${green_text} - fi - else - # Custom branch - if [ -z "${FTL_BRANCH}" ]; then - # Branch name is empty, something went wrong - ftl_version_heatmap=${red_text} - FTL_VERSION="?" + + if [ "${FTL_BRANCH}" = "master" ]; then + ftl_version_converted="$(VersionConverter "${FTL_VERSION}")" + ftl_version_latest_converted=$(VersionConverter "${GITHUB_FTL_VERSION}") + + if [ "${ftl_version_converted}" -lt "${ftl_version_latest_converted}" ]; then + out_of_date_flag="true" + ftl_version_heatmap=${red_text} + else + ftl_version_heatmap=${green_text} + fi else - if [ "${FTL_HASH}" = "${GITHUB_FTL_HASH}" ]; then - # up-to-date - ftl_version_heatmap=${green_text} - else - # out-of-date - out_of_date_flag="true" - ftl_version_heatmap=${red_text} - fi - # shorten common branch names (fix/, tweak/, new/) - # use the first 7 characters of the branch name as version - FTL_VERSION="$(printf '%s' "$FTL_BRANCH" | sed 's/fix\//f\//;s/new\//n\//;s/tweak\//t\//' | cut -c 1-7)" + # Custom branch + if [ -z "${FTL_BRANCH}" ]; then + # Branch name is empty, something went wrong + ftl_version_heatmap=${red_text} + FTL_VERSION="?" + else + if [ "${FTL_HASH}" = "${GITHUB_FTL_HASH}" ]; then + # up-to-date + ftl_version_heatmap=${green_text} + else + # out-of-date + out_of_date_flag="true" + ftl_version_heatmap=${red_text} + fi + # shorten common branch names (fix/, tweak/, new/) + # use the first 7 characters of the branch name as version + FTL_VERSION="$(printf '%s' "$FTL_BRANCH" | sed 's/fix\//f\//;s/new\//n\//;s/tweak\//t\//' | cut -c 1-7)" + fi fi - fi } GetPADDInformation() { # If PADD is running inside docker, immediately return without checking for an update - if [ -n "${DOCKER_VERSION}" ]; then + if [ ! "${DOCKER_VERSION}" = "null" ]; then return fi # PADD version information... - padd_version_latest="$(curl --silent https://api.github.com/repos/pi-hole/PADD/releases/latest | grep '"tag_name":' | awk -F \" '{print $4}')" + padd_version_latest="$(curl --connect-timeout 5 --silent https://api.github.com/repos/pi-hole/PADD/releases/latest | grep '"tag_name":' | awk -F \" '{print $4}')" # is PADD up-to-date? padd_out_of_date_flag=false if [ -z "${padd_version_latest}" ]; then @@ -636,9 +884,9 @@ GenerateSizeDependendOutput() { # System uptime if [ "$1" = "pico" ] || [ "$1" = "nano" ] || [ "$1" = "micro" ]; then - system_uptime=$(echo "${system_uptime_raw}" | awk -F'( |,|:)+' '{if ($7=="min") m=$6; else {if ($7~/^day/){if ($9=="min") {d=$6;m=$8} else {d=$6;h=$8;m=$9}} else {h=$6;m=$7}}} {print d+0,"days,",h+0,"hours"}') + system_uptime="$(convertUptime "${system_uptime_raw}" | awk -F ',' '{print $1 "," $2}')" else - system_uptime=$(echo "${system_uptime_raw}" | awk -F'( |,|:)+' '{if ($7=="min") m=$6; else {if ($7~/^day/){if ($9=="min") {d=$6;m=$8} else {d=$6;h=$8;m=$9}} else {h=$6;m=$7}}} {print d+0,"days,",h+0,"hours,",m+0,"minutes"}') + system_uptime="$(convertUptime "${system_uptime_raw}")" fi # Bar generations @@ -675,7 +923,7 @@ SetStatusMessage() { full_status="${full_status_hot} ${blinking_text}${red_text}${temperature}${reset_text}" mega_status="${mega_status_hot} ${blinking_text}${red_text}${temperature}${reset_text}" - elif [ "${ftl_down_flag}" = true ]; then + elif [ "${connection_down_flag}" = true ]; then # Check if FTL is down pico_status=${pico_status_ftl_down} mini_status=${mini_status_ftl_down} @@ -691,15 +939,7 @@ SetStatusMessage() { full_status=${full_status_dns_down} mega_status=${mega_status_dns_down} - elif [ "${blocking_status}" = "unknown" ]; then - # Check if blocking status is unknown - pico_status=${pico_status_unknown} - mini_status=${mini_status_unknown} - tiny_status=${tiny_status_unknown} - full_status=${full_status_unknown} - mega_status=${mega_status_unknown} - - elif [ "${blocking_status}" = "disabled" ]; then + elif [ "${blocking_enabled}" = "disabled" ]; then # Check if blocking status is disabled pico_status=${pico_status_off} mini_status=${mini_status_off} @@ -715,7 +955,7 @@ SetStatusMessage() { full_status=${full_status_update} mega_status=${mega_status_update} - elif [ "${blocking_status}" = "enabled" ]; then + elif [ "${blocking_enabled}" = "enabled" ]; then # if we reach this point and blocking is enabled, everything is fine pico_status=${pico_status_ok} mini_status=${mini_status_ok} @@ -728,7 +968,7 @@ SetStatusMessage() { ############################################# PRINTERS ############################################# PrintLogo() { - if [ -n "${DOCKER_VERSION}" ]; then + if [ ! "${DOCKER_VERSION}" = "null" ]; then version_info="Docker ${docker_version_heatmap}${DOCKER_VERSION}${reset_text}" else version_info="Pi-hole® ${core_version_heatmap}${CORE_VERSION}${reset_text}, Web ${web_version_heatmap}${WEB_VERSION}${reset_text}, FTL ${ftl_version_heatmap}${FTL_VERSION}${reset_text}" @@ -761,7 +1001,7 @@ PrintLogo() { } PrintDashboard() { - if [ -n "${DOCKER_VERSION}" ]; then + if [ ! "${DOCKER_VERSION}" = "null" ]; then version_info="Docker ${docker_version_heatmap}${DOCKER_VERSION}${reset_text}" else version_info="Pi-hole® ${core_version_heatmap}${CORE_VERSION}${reset_text}, Web ${web_version_heatmap}${WEB_VERSION}${reset_text}, FTL ${ftl_version_heatmap}${FTL_VERSION}${reset_text}" @@ -921,7 +1161,7 @@ PrintDashboard() { moveXOffset; printf " %-10s%-39s${clear_line}\n" "Top Clnt:" "${top_client}" moveXOffset; printf "%s${clear_line}\n" "${bold_text}FTL ============================================================================${reset_text}" moveXOffset; printf " %-10s%-9s %-10s%-9s %-10s%-9s${clear_line}\n" "PID:" "${ftlPID}" "CPU Use:" "${ftl_cpu}" "Mem. Use:" "${ftl_mem_percentage}" - moveXOffset; printf " %-10s%-69s${clear_line}\n" "DNSCache:" "${cache_inserts} insertions, ${cache_deletes} deletions, ${cache_size} total entries" + moveXOffset; printf " %-10s%-69s${clear_line}\n" "DNSCache:" "${cache_inserts} insertions, ${cache_evictions} deletions, ${cache_size} total entries" moveXOffset; printf "%s${clear_line}\n" "${bold_text}NETWORK ========================================================================${reset_text}" moveXOffset; printf " %-10s%-19s${clear_line}\n" "Hostname:" "${full_hostname}" moveXOffset; printf " %-10s%-15s %-4s%-9s %-4s%-9s${clear_line}\n" "Interfce:" "${iface_name}" "TX:" "${tx_bytes}" "RX:" "${rx_bytes}" @@ -1002,7 +1242,7 @@ BarGenerator() { echo "$out" } -# Checks the size of the screen and sets the value of $padd_size +# Checks the size of the screen and sets the value of ${padd_data}_size SizeChecker(){ # adding a tiny delay here to to give the kernel a bit time to # report new sizes correctly after a terminal resize @@ -1090,59 +1330,6 @@ VersionConverter() { echo "$@" | tr -d '[:alpha:]' | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; } -# get the Telnet API Port FTL is using by parsing `pihole-FTL.conf` -# same implementation as https://github.com/pi-hole/pi-hole/pull/4945 -getFTLAPIPort(){ - local FTLCONFFILE="/etc/pihole/pihole-FTL.conf" - local DEFAULT_FTL_PORT=4711 - local ftl_api_port - - if [ -s "$FTLCONFFILE" ]; then - # if FTLPORT is not set in pihole-FTL.conf, use the default port - ftl_api_port="$({ grep '^FTLPORT=' "${FTLCONFFILE}" || echo "${DEFAULT_FTL_PORT}"; } | cut -d'=' -f2-)" - # Exploit prevention: set the port to the default port if there is malicious (non-numeric) - # content set in pihole-FTL.conf - expr "${ftl_api_port}" : "[^[:digit:]]" > /dev/null && ftl_api_port="${DEFAULT_FTL_PORT}" - else - # if there is no pihole-FTL.conf, use the default port - ftl_api_port="${DEFAULT_FTL_PORT}" - fi - - echo "${ftl_api_port}" - -} - -# returns FTL's PID based on the content of the pihole-FTL.pid file -# honor PIDFILE setting in `pihole-FTL.conf` -getFTLPID() { - local FTLCONFFILE="/etc/pihole/pihole-FTL.conf" - local DEFAULT_PID_FILE="/run/pihole-FTL.pid" - local FTL_PID_FILE - local FTL_PID - - if [ -s "${FTLCONFFILE}" ]; then - # if PIDFILE is not set in pihole-FTL.conf, use the default path - FTL_PID_FILE="$({ grep '^PIDFILE=' "${FTLCONFFILE}" || echo "${DEFAULT_PID_FILE}"; } | cut -d'=' -f2-)" - else - # if there is no pihole-FTL.conf, use the default path - FTL_PID_FILE="${DEFAULT_PID_FILE}" - fi - - if [ -s "${FTL_PID_FILE}" ]; then - # -s: FILE exists and has a size greater than zero - FTL_PID="$(cat "${FTL_PID_FILE}")" - # Exploit prevention: unset the variable if there is malicious content - # Verify that the value read from the file is numeric - expr "${FTL_PID}" : "[^[:digit:]]" > /dev/null && unset FTL_PID - fi - - # If FTL is not running, or the PID file contains malicious stuff, substitute - # negative PID to signal this - FTL_PID=${FTL_PID:=-1} - echo "${FTL_PID}" -} - - moveYOffset(){ # moves the cursor yOffset-times down # https://vt100.net/docs/vt510-rm/CUD.html @@ -1195,35 +1382,131 @@ truncateString() { fi } +# Converts seconds to days, hours, minutes +# https://unix.stackexchange.com/a/338844 +convertUptime() { + # shellcheck disable=SC2016 + eval "echo $(date -ud "@$1" +'$((%s/3600/24)) days, %H hours, %M minutes')" +} + +secretRead() { + + # POSIX compliant function to read user-input and + # mask every character entered by (*) + # + # This is challenging, because in POSIX, `read` does not support + # `-s` option (suppressing the input) or + # `-n` option (reading n chars) + + + # This workaround changes the terminal characteristics to not echo input and later resets this option + # credits https://stackoverflow.com/a/4316765 + # showing asterisk instead of password + # https://stackoverflow.com/a/24600839 + # https://unix.stackexchange.com/a/464963 + + stty -echo # do not echo user input + stty -icanon min 1 time 0 # disable canonical mode https://man7.org/linux/man-pages/man3/termios.3.html + + unset password + unset key + unset charcount + charcount=0 + while key=$(dd ibs=1 count=1 2>/dev/null); do #read one byte of input + if [ "${key}" = "$(printf '\0' | tr -d '\0')" ] ; then + # Enter - accept password + break + fi + if [ "${key}" = "$(printf '\177')" ] ; then + # Backspace + if [ $charcount -gt 0 ] ; then + charcount=$((charcount-1)) + printf '\b \b' + password="${password%?}" + fi + else + # any other character + charcount=$((charcount+1)) + printf '*' + password="$password$key" + fi + done + + # restore original terminal settings + stty "${stty_orig}" +} + +check_dependencies() { + # Check for required dependencies + if ! command -v curl >/dev/null 2>&1; then + printf "%b" "${check_box_bad} Error!\n 'curl' is missing but required.\n" + exit 1 + fi + if ! command -v jq >/dev/null 2>&1; then + printf "%b" "${check_box_bad} Error!\n 'jq' is missing but required.\n" + exit 1 + fi +} ########################################## MAIN FUNCTIONS ########################################## OutputJSON() { - GetSummaryInformation - echo "{\"domains_being_blocked\":${domains_being_blocked_raw},\"dns_queries_today\":${dns_queries_today_raw},\"ads_blocked_today\":${ads_blocked_today_raw},\"ads_percentage_today\":${ads_percentage_today_raw},\"clients\": ${clients}}" + # Hiding the cursor. + # https://vt100.net/docs/vt510-rm/DECTCEM.html + printf '\e[?25l' + # Traps for graceful shutdown + # https://unix.stackexchange.com/a/681201 + trap CleanExit EXIT + trap sig_cleanup INT QUIT TERM + # Save current terminal settings (needed for later restore after password prompt) + stty_orig=$(stty -g) + + # Test if the authentication endpoint is available + TestAPIAvailability + # Authenticate with the FTL server + printf "%b" "Establishing connection with FTL...\n" + LoginAPI + + GetPADDData + GetSummaryInformation + printf "%b" "{\"domains_being_blocked\":${domains_being_blocked_raw},\"dns_queries_today\":${dns_queries_today_raw},\"ads_blocked_today\":${ads_blocked_today_raw},\"ads_percentage_today\":${ads_percentage_today},\"clients\": ${clients}}" } ShowVersion() { - # source version file to check if $DOCKER_VERSION is set - . /etc/pihole/versions - GetPADDInformation - if [ -z "${padd_version_latest}" ]; then - padd_version_latest="N/A" - fi - if [ -n "${DOCKER_VERSION}" ]; then - # Check for latest Docker version + # Hiding the cursor. + # https://vt100.net/docs/vt510-rm/DECTCEM.html + printf '\e[?25l' + # Traps for graceful shutdown + # https://unix.stackexchange.com/a/681201 + trap CleanExit EXIT + trap sig_cleanup INT QUIT TERM + + # Save current terminal settings (needed for later restore after password prompt) + stty_orig=$(stty -g) + + # Test if the authentication endpoint is available + TestAPIAvailability + # Authenticate with the FTL server + printf "%b" "Establishing connection with FTL...\n" + LoginAPI + + GetPADDData GetVersionInformation - printf "%s${clear_line}\n" " PADD version is ${padd_version} as part of Docker ${docker_version_heatmap}${DOCKER_VERSION}${reset_text} (Latest Docker: ${GITHUB_DOCKER_VERSION})" - version_info="Docker ${docker_version_heatmap}${DOCKER_VERSION}${reset_text}" - else - printf "%s${clear_line}\n" " PADD version is ${padd_version_heatmap}${padd_version}${reset_text} (Latest: ${padd_version_latest})" + GetPADDInformation + if [ -z "${padd_version_latest}" ]; then + padd_version_latest="N/A" + fi + if [ ! "${DOCKER_VERSION}" = "null" ]; then + # Check for latest Docker version + printf "%s${clear_line}\n" "PADD version is ${padd_version} as part of Docker ${docker_version_heatmap}${DOCKER_VERSION}${reset_text} (Latest Docker: ${GITHUB_DOCKER_VERSION})" + version_info="Docker ${docker_version_heatmap}${DOCKER_VERSION}${reset_text}" + else + printf "%s${clear_line}\n" "PADD version is ${padd_version_heatmap}${padd_version}${reset_text} (Latest: ${padd_version_latest})" fi } StartupRoutine(){ - # Get config variables - . /etc/pihole/setupVars.conf # Clear the screen and move cursor to (0,0). # This mimics the 'clear' command. @@ -1235,22 +1518,31 @@ StartupRoutine(){ # adds the y-offset moveYOffset - # Get versions information - . /etc/pihole/versions - if [ "$1" = "pico" ] || [ "$1" = "nano" ] || [ "$1" = "micro" ]; then moveXOffset; PrintLogo "$1" moveXOffset; printf "%b" "START-UP ===========\n" + # Test if the authentication endpoint is available + TestAPIAvailability + + # Authenticate with the FTL server + moveXOffset; printf "%b" "Establishing connection with FTL...\n" + LoginAPI + + moveXOffset; printf "%b" "Starting PADD...\n" + moveXOffset; printf "%b" " [■·········] 10%\r" + # Request PADD data + GetPADDData + # Check for updates moveXOffset; printf "%b" " [■■········] 20%\r" moveXOffset; printf "%b" " [■■■·······] 30%\r" # Get our information for the first time moveXOffset; printf "%b" " [■■■■······] 40%\r" - GetSystemInformation + GetVersionInformation moveXOffset; printf "%b" " [■■■■■·····] 50%\r" GetSummaryInformation moveXOffset; printf "%b" " [■■■■■■····] 60%\r" @@ -1258,7 +1550,7 @@ StartupRoutine(){ moveXOffset; printf "%b" " [■■■■■■■···] 70%\r" GetNetworkInformation moveXOffset; printf "%b" " [■■■■■■■■··] 80%\r" - GetVersionInformation + GetSystemInformation moveXOffset; printf "%b" " [■■■■■■■■■·] 90%\r" GetPADDInformation moveXOffset; printf "%b" " [■■■■■■■■■■] 100%\n" @@ -1266,23 +1558,32 @@ StartupRoutine(){ elif [ "$1" = "mini" ]; then moveXOffset; PrintLogo "$1" moveXOffset; echo "START UP =====================" + # Test if the authentication endpoint is available + TestAPIAvailability + # Authenticate with the FTL server + moveXOffset; printf "%b" "Establishing connection with FTL...\n" + LoginAPI + + # Request PADD data + moveXOffset; echo "- Requesting PADD information..." + GetPADDData # Get our information for the first time + moveXOffset; echo "- Gathering version info." + GetVersionInformation moveXOffset; echo "- Gathering system info." GetSystemInformation - moveXOffset; echo "- Gathering Pi-hole info." + moveXOffset; echo "- Gathering CPU/DNS info." GetPiholeInformation GetSummaryInformation moveXOffset; echo "- Gathering network info." GetNetworkInformation - moveXOffset; echo "- Gathering version info." - GetVersionInformation GetPADDInformation - if [ -n "${DOCKER_VERSION}" ]; then + if [ ! "${DOCKER_VERSION}" = "null" ]; then moveXOffset; echo " - Docker Tag ${DOCKER_VERSION}" else moveXOffset; echo " - Core $CORE_VERSION, Web $WEB_VERSION" - moveXOffset; echo " - FTL $FTL_VERSION, PADD $padd_version" + moveXOffset; echo " - FTL $FTL_VERSION, PADD ${padd_data}_version" fi else @@ -1295,24 +1596,36 @@ StartupRoutine(){ moveXOffset; echo "START UP ===================================================" fi + # Test if the authentication endpoint is available + TestAPIAvailability + + # Authenticate with the FTL server + moveXOffset; printf "%b" "Establishing connection with FTL...\n" + LoginAPI + + # Request PADD data + moveXOffset; echo "- Requesting PADD information..." + GetPADDData + # Get our information for the first time + moveXOffset; echo "- Gathering version information..." + GetVersionInformation moveXOffset; echo "- Gathering system information..." GetSystemInformation - moveXOffset; echo "- Gathering Pi-hole information..." + moveXOffset; echo "- Gathering CPU/DNS information..." GetSummaryInformation GetPiholeInformation moveXOffset; echo "- Gathering network information..." GetNetworkInformation - moveXOffset; echo "- Gathering version information..." - GetVersionInformation + GetPADDInformation - if [ -n "${DOCKER_VERSION}" ]; then + if [ ! "${DOCKER_VERSION}" = "null" ]; then moveXOffset; echo " - Docker Tag ${DOCKER_VERSION}" else moveXOffset; echo " - Pi-hole Core $CORE_VERSION" moveXOffset; echo " - Web Admin $WEB_VERSION" moveXOffset; echo " - FTL $FTL_VERSION" - moveXOffset; echo " - PADD $padd_version" + moveXOffset; echo " - PADD ${padd_version}" fi fi @@ -1355,42 +1668,52 @@ NormalPADD() { # Start getting our information for next round now=$(date +%s) - # Get uptime, CPU load, temp, etc. every 5 seconds - if [ $((now - LastCheckSystemInformation)) -ge 5 ]; then - . /etc/pihole/setupVars.conf - GetSystemInformation - LastCheckSystemInformation="${now}" - fi - - # Get cache info, last ad domain, blocking percentage, etc. every 5 seconds - if [ $((now - LastCheckSummaryInformation)) -ge 5 ]; then - GetSummaryInformation - LastCheckSummaryInformation="${now}" - fi - - # Get FTL status every 5 seconds - if [ $((now - LastCheckPiholeInformation)) -ge 5 ]; then - GetPiholeInformation - LastCheckPiholeInformation="${now}" + # check if a new authentication is required (e.g. after connection to FTL has re-established) + # GetFTLData() will return a 401 if a 401 http status code is returned + # as $password should be set already, PADD should automatically re-authenticate + authenthication_required=$(GetFTLData "info/ftl") + if [ "${authenthication_required}" = 401 ]; then + Authenticate fi - # Get IPv4 address, DNS servers, DNSSEC, hostname, DHCP status, interface traffic, etc. every 30 seconds - if [ $((now - LastCheckNetworkInformation)) -ge 30 ]; then - GetNetworkInformation - LastCheckNetworkInformation="${now}" + # Request PADD data after 30 seconds or if the connection was lost + if [ $((now - LastCheckFullInformation)) -ge 30 ] || [ "${connection_down_flag}" = true ] ; then + GetPADDData + LastCheckFullInformation="${now}" + else + # Request only a subset of the data + GetPADDData "?full=false" fi - # Get Pi-hole components version information every 30 seconds - if [ $((now - LastCheckVersionInformation)) -ge 30 ]; then - . /etc/pihole/versions - GetVersionInformation - LastCheckVersionInformation="${now}" - fi + connection_down_flag=false + # If the connection was lost, set connection_down_flag + if [ "${padd_data}" = "000" ]; then + connection_down_flag=true + GetSystemInformation + GetSummaryInformation + GetPiholeInformation + GetNetworkInformation + GetVersionInformation + # set flag to update network information in the next loop in case the connection is re-established + get_network_information_requried=true + else + # Get uptime, CPU load, temp, etc. every 5 seconds + GetSystemInformation + GetSummaryInformation + GetPiholeInformation + + if [ $((now - LastCheckNetworkInformation)) -ge 30 ] || [ "${get_network_information_requried}" = true ]; then + GetNetworkInformation + GetVersionInformation + LastCheckNetworkInformation="${now}" + get_network_information_requried=false + fi - # Get PADD version information every 24hours - if [ $((now - LastCheckPADDInformation)) -ge 86400 ]; then - GetPADDInformation - LastCheckPADDInformation="${now}" + # Get PADD version information every 24hours + if [ $((now - LastCheckPADDInformation)) -ge 86400 ]; then + GetPADDInformation + LastCheckPADDInformation="${now}" + fi fi done @@ -1412,26 +1735,12 @@ Update() { padd_script_path=$(realpath "$0") - if which wget > /dev/null 2>&1; then - echo "${check_box_info} Downloading PADD update via wget ..." - if wget -qO "${padd_script_path}" https://install.padd.sh > /dev/null 2>&1; then - echo "${check_box_good} ... done. Restart PADD for the update to take effect" - else - echo "${check_box_bad} Cannot download PADD update via wget" - echo "${check_box_info} Go to https://install.padd.sh to download the update manually" - exit 1 - fi - elif which curl > /dev/null 2>&1; then - echo "${check_box_info} Downloading PADD update via curl ..." - if curl -sSL https://install.padd.sh -o "${padd_script_path}" > /dev/null 2>&1; then - echo "${check_box_good} ... done. Restart PADD for the update to take effect" - else - echo "${check_box_bad} Cannot download PADD update via curl" - echo "${check_box_info} Go to https://install.padd.sh to download the update manually" - exit 1 - fi + echo "${check_box_info} Downloading PADD update ..." + + if curl --connect-timeout 5 -sSL https://install.padd.sh -o "${padd_script_path}" > /dev/null 2>&1; then + echo "${check_box_good} ... done. Restart PADD for the update to take effect" else - echo "${check_box_bad} Cannot download, neither wget nor curl are available" + echo "${check_box_bad} Cannot download PADD update" echo "${check_box_info} Go to https://install.padd.sh to download the update manually" exit 1 fi @@ -1447,24 +1756,46 @@ DisplayHelp() { ::: PADD displays stats about your Pi-hole! ::: -::: Note: If no option is passed, then stats are displayed on screen, updated every 5 seconds ::: ::: Options: -::: -xoff [num] set the x-offset, reference is the upper left corner, disables auto-centering -::: -yoff [num] set the y-offset, reference is the upper left corner, disables auto-centering -::: -j, --json output stats as JSON formatted string and exit -::: -u, --update update to the latest version -::: -v, --version show PADD version info -::: -h, --help display this help text +::: --xoff [num] set the x-offset, reference is the upper left corner, disables auto-centering +::: --yoff [num] set the y-offset, reference is the upper left corner, disables auto-centering +::: +::: --server domain or IP of your Pi-hole (default: localhost) +::: --secret your Pi-hole's password, required to access the API +::: -j, --json output stats as JSON formatted string and exit and exit +::: -u, --update update to the latest version +::: -v, --version show PADD version info +::: -h, --help display this help text EOM } -CleanExit(){ +# Called on signals INT QUIT TERM +sig_cleanup() { + # save error code (130 for SIGINT, 143 for SIGTERM, 131 for SIGQUIT) + err=$? + + # some shells will call EXIT after the INT signal + # causing EXIT trap to be executed, so we trap EXIT after INT + trap '' EXIT + + (exit $err) # execute in a subshell just to pass $? to CleanExit() + CleanExit +} + +# Called on signal EXIT, or indirectly on INT QUIT TERM +CleanExit() { # save the return code of the script err=$? - #clear the line - printf '\e[0K\n' + + # reset trap for all signals to not interrupt clean_tempfiles() on any next signal + trap '' EXIT INT QUIT TERM + + # restore terminal settings if they have been changed (e.g. user canceled script while at password input prompt) + if [ "$(stty -g)" != "${stty_orig}" ]; then + stty "${stty_orig}" + fi # Show the cursor # https://vt100.net/docs/vt510-rm/DECTCEM.html @@ -1472,8 +1803,10 @@ CleanExit(){ # if background sleep is running, kill it # http://mywiki.wooledge.org/SignalTrap#When_is_the_signal_handled.3F - kill $sleepPID > /dev/null 2>&1 + kill "{$sleepPID}" > /dev/null 2>&1 + # Delete session from FTL server + DeleteSession exit $err # exit the script with saved $? } @@ -1491,23 +1824,25 @@ TerminalResize(){ printf '\e[H\e[2J\e[3J' - kill $sleepPID > /dev/null 2>&1 + kill "{$sleepPID}" > /dev/null 2>&1 } main(){ + + check_dependencies + # Hiding the cursor. # https://vt100.net/docs/vt510-rm/DECTCEM.html printf '\e[?25l' - # Trap on exit - trap 'CleanExit' INT TERM EXIT + # Traps for graceful shutdown + # https://unix.stackexchange.com/a/681201 + trap CleanExit EXIT + trap sig_cleanup INT QUIT TERM + + # Save current terminal settings (needed for later restore after password prompt) + stty_orig=$(stty -g) - # If setupVars.conf is not present, then PADD is not running on a Pi-hole - # and we are not able to start as StartupRoutine() will fail below - if [ ! -f /etc/pihole/setupVars.conf ]; then - printf "%b" "${check_box_bad} Error!\n PADD only works in conjunction with Pi-hole!\n" - exit 1 - fi SizeChecker @@ -1520,12 +1855,14 @@ main(){ # Process all options (if present) while [ "$#" -gt 0 ]; do case "$1" in - "-j" | "--json" ) OutputJSON; exit 0;; + "-j" | "--json" ) xOffset=0; OutputJSON; exit 0;; "-u" | "--update" ) Update;; "-h" | "--help" ) DisplayHelp; exit 0;; - "-v" | "--version" ) ShowVersion; exit 0;; - "-xoff" ) xOffset="$2"; xOffOrig="$2"; shift;; - "-yoff" ) yOffset="$2"; yOffOrig="$2"; shift;; + "-v" | "--version" ) xOffset=0; ShowVersion; exit 0;; + "--xoff" ) xOffset="$2"; xOffOrig="$2"; shift;; + "--yoff" ) yOffset="$2"; yOffOrig="$2"; shift;; + "--server" ) SERVER="$2"; shift;; + "--secret" ) password="$2"; shift;; * ) DisplayHelp; exit 1;; esac shift