#!/usr/bin/env sh # shellcheck disable=SC3043 #https://github.com/koalaman/shellcheck/wiki/SC3043#exceptions # Pi-hole: A black hole for Internet advertisements # (c) 2017 Pi-hole, LLC (https://pi-hole.net) # Network-wide ad blocking via your own hardware. # # Script to hold api functions for use in other scripts # # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. # The basic usage steps are # 1) Test Availability of the API # 2) Try to authenticate (read password if needed) # 3) Get the data from the API endpoint # 4) Delete the session TestAPIAvailability() { # as we are running locally, we can get the port value from FTL directly local chaos_api_list availabilityResponse # Query the API URLs from FTL using CHAOS TXT local.api.ftl # The result is a space-separated enumeration of full URLs # e.g., "http://localhost:80/api/" "https://localhost:443/api/" chaos_api_list="$(dig +short chaos txt local.api.ftl @127.0.0.1)" # If the query was not successful, the variable is empty if [ -z "${chaos_api_list}" ]; then echo "API not available. Please check connectivity" exit 1 fi # 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 -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 if [ "${availabilityResponse}" = 200 ]; then # API is available without authentication needAuth=false fi break fi # Remove the first URL from the list local last_api_list last_api_list="${chaos_api_list}" chaos_api_list="${chaos_api_list#* }" # 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 # if API_PORT is empty, no working API port was found if [ -n "${API_PORT}" ]; then echo "API not available at: ${API_URL}" echo "Exiting." exit 1 fi } LoginAPI() { # If the API URL is not set, test the availability if [ -z "${API_URL}" ]; then TestAPIAvailability fi # Exit early if authentication is not needed if [ "${needAuth}" = false ]; then if [ "${1}" = "verbose" ]; then echo "API Authentication: Not needed" fi return fi # 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) if [ "${1}" = "verbose" ]; then echo "API Authentication: Trying to use CLI password" fi # Try to authenticate using the CLI password Authentication "${1}" elif [ "${1}" = "verbose" ]; then echo "API Authentication: CLI password not available" fi # If this did not work, ask the user for the password while [ "${validSession}" = false ] || [ -z "${validSession}" ] ; do echo "Authentication failed. Please enter your Pi-hole password" # secretly read the password secretRead; printf '\n' # Try to authenticate again Authentication "${1}" done } Authentication() { sessionResponse="$(curl -skS -X POST "${API_URL}auth" --user-agent "Pi-hole cli " --data "{\"password\":\"${password}\"}" )" if [ -z "${sessionResponse}" ]; then echo "No response from FTL server. Please check connectivity" 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) if [ "${1}" = "verbose" ]; then if [ "${validSession}" = true ]; then echo "API Authentication: ${COL_GREEN}Success${COL_NC}" else echo "API Authentication: ${COL_RED}Failed${COL_NC}" fi fi } LogoutAPI() { # if a valid Session exists (no password required or successful Authentication) and # SID is not null (successful Authentication 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 -skS -o /dev/null -w "%{http_code}" -X DELETE "${API_URL}auth" -H "Accept: application/json" -H "sid: ${SID}") case "${deleteResponse}" in "401") echo "Logout attempt without a valid session. Unauthorized!";; "204") if [ "${1}" = "verbose" ]; then echo "API Logout: ${COL_GREEN}Success${COL_NC} (session deleted)"; fi;; esac; elif [ "${1}" = "verbose" ]; then echo "API Logout: ${COL_GREEN}Success${COL_NC} (no valid session)" fi } GetFTLData() { local data response status # get the data from querying the API as well as the http status code response=$(curl -skS -w "%{http_code}" -X GET "${API_URL}$1" -H "Accept: application/json" -H "sid: ${SID}" ) # status are the last 3 characters status="${response#"${response%???}"}" # data is everything from response without the last 3 characters data="${response%???}" if [ "${2}" = "raw" ]; then # return the raw response echo "${response}" else # return only the data if [ "${status}" = 200 ]; then # response OK echo "${data}" else # connection lost echo "${status}" fi fi } PostFTLData() { local data response status # send the data to the API response=$(curl -skS -w "%{http_code}" -X POST "${API_URL}$1" --data-raw "$2" -H "Accept: application/json" -H "sid: ${SID}" ) # data is everything from response without the last 3 characters if [ "${3}" = "status" ]; then # Keep the status code appended if requested printf %s "${response}" else # Strip the status code printf %s "${response%???}" fi } 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 # Save current terminal settings (needed for later restore after password prompt) stty_orig=$(stty -g) 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}" } apiFunc() { local data response status status_col # Authenticate with the API LoginAPI verbose echo "" echo "Requesting: ${COL_PURPLE}GET ${COL_CYAN}${API_URL}${COL_YELLOW}$1${COL_NC}" echo "" # Get the data from the API response=$(GetFTLData "$1" raw) # status are the last 3 characters status="${response#"${response%???}"}" # data is everything from response without the last 3 characters data="${response%???}" # Output the status (200 -> green, else red) if [ "${status}" = 200 ]; then status_col="${COL_GREEN}" else status_col="${COL_RED}" fi echo "Status: ${status_col}${status}${COL_NC}" # Output the data. Format it with jq if available and data is actually JSON. # Otherwise just print it echo "Data:" if command -v jq >/dev/null && echo "${data}" | jq . >/dev/null 2>&1; then echo "${data}" | jq . else echo "${data}" fi # Delete the session LogoutAPI verbose }