diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index cf17767c..ee923724 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -25,16 +25,16 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 #v4.30.9 + uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 #v4.32.3 with: languages: 'python' - name: Autobuild - uses: github/codeql-action/autobuild@16140ae1a102900babc80a33c44059580f687047 #v4.30.9 + uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 #v4.32.3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 #v4.30.9 + uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 #v4.32.3 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 2fdf9291..1e044ec9 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,7 +17,7 @@ jobs: issues: write steps: - - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 #v10.1.0 + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d #v10.1.1 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.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 7d68df6a..6dfcbe99 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@5f858e3efba33a5ca4407a664cc011ad407f2008 #v10.1.0 + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d #v10.1.1 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 b8546b64..058bbcab 100644 --- a/.github/workflows/sync-back-to-dev.yml +++ b/.github/workflows/sync-back-to-dev.yml @@ -33,7 +33,7 @@ jobs: name: Syncing branches steps: - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.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/test.yml b/.github/workflows/test.yml index 085a3cc7..6482e242 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: fetch-depth: 0 # Differential ShellCheck requires full git history @@ -31,25 +31,25 @@ jobs: [[ $FAIL == 1 ]] && exit 1 || echo "Scripts are executable!" - name: Differential ShellCheck - uses: redhat-plumbers-in-action/differential-shellcheck@0d9e5b29625f871e6a4215380486d6f1a7cb6cdd #v5.5.5 + uses: redhat-plumbers-in-action/differential-shellcheck@d965e66ec0b3b2f821f75c8eff9b12442d9a7d1e #v5.5.6 with: severity: warning display-engine: sarif-fmt - name: Spell-Checking - uses: codespell-project/actions-codespell@406322ec52dd7b488e48c1c4b82e2a8b3a1bf630 #v2.1 + uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 #v2.2 with: ignore_words_file: .codespellignore - name: Get editorconfig-checker - uses: editorconfig-checker/action-editorconfig-checker@5ecdd656fe347c26f76b1b435b90e1d74fb5e787 # tag v2. is really out of date + uses: editorconfig-checker/action-editorconfig-checker@4b6cd6190d435e7e084fb35e36a096e98506f7b9 #v2.1.0 - name: Run editorconfig-checker run: editorconfig-checker - name: Check python code formatting with black - uses: psf/black@af0ba72a73598c76189d6dd1b21d8532255d5942 #25.9.0 + uses: psf/black@6305bf1ae645ab7541be4f5028a86239316178eb #26.1.0 with: src: "./test" options: "--check --diff --color" @@ -74,17 +74,19 @@ jobs: fedora_40, fedora_41, fedora_42, + fedora_43, alpine_3_21, alpine_3_22, + alpine_3_23, ] env: DISTRO: ${{matrix.distro}} steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 - name: Set up Python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c #v6.0.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #v6.2.0 with: python-version: "3.13" diff --git a/advanced/Scripts/piholeCheckout.sh b/advanced/Scripts/piholeCheckout.sh index a6df46f2..deb07172 100755 --- a/advanced/Scripts/piholeCheckout.sh +++ b/advanced/Scripts/piholeCheckout.sh @@ -41,6 +41,22 @@ warning1() { } checkout() { + + local skipFTL additionalFlag + skipFTL=false + # Check arguments + for var in "$@"; do + case "$var" in + "--skipFTL") skipFTL=true ;; + esac + done + + if [ "${skipFTL}" == true ]; then + additionalFlag="--skipFTL" + else + additionalFlag="" + fi + local corebranches local webbranches @@ -235,7 +251,7 @@ checkout() { # Force updating everything if [[ ! "${1}" == "web" && ! "${1}" == "ftl" ]]; then echo -e " ${INFO} Running installer to upgrade your installation" - if "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" --unattended; then + if "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" --unattended ${additionalFlag}; then exit 0 else echo -e " ${COL_RED} Error: Unable to complete update, please contact support${COL_NC}" diff --git a/advanced/Scripts/piholeDebug.sh b/advanced/Scripts/piholeDebug.sh index 7cc31e29..964fff9b 100755 --- a/advanced/Scripts/piholeDebug.sh +++ b/advanced/Scripts/piholeDebug.sh @@ -375,22 +375,6 @@ check_firewalld() { log_write "${CROSS} ${COL_RED} Allow Service: ${i}${COL_NC} (${FAQ_HARDWARE_REQUIREMENTS_FIREWALLD})" fi done - # check for custom FTL FirewallD zone - local firewalld_zones - firewalld_zones=$(firewall-cmd --get-zones) - if [[ "${firewalld_zones}" =~ "ftl" ]]; then - log_write "${TICK} ${COL_GREEN}FTL Custom Zone Detected${COL_NC}"; - # check FTL custom zone interface: lo - local firewalld_ftl_zone_interfaces - firewalld_ftl_zone_interfaces=$(firewall-cmd --zone=ftl --list-interfaces) - if [[ "${firewalld_ftl_zone_interfaces}" =~ "lo" ]]; then - log_write "${TICK} ${COL_GREEN} Local Interface Detected${COL_NC}"; - else - log_write "${CROSS} ${COL_RED} Local Interface Not Detected${COL_NC} (${FAQ_HARDWARE_REQUIREMENTS_FIREWALLD})" - fi - else - log_write "${CROSS} ${COL_RED}FTL Custom Zone Not Detected${COL_NC} (${FAQ_HARDWARE_REQUIREMENTS_FIREWALLD})" - fi fi else log_write "${TICK} ${COL_GREEN}Firewalld service not detected${COL_NC}"; @@ -593,18 +577,21 @@ check_required_ports() { # Add port 53 ports_configured+=("53") + local protocol_type port_number service_name # Now that we have the values stored, for i in "${!ports_in_use[@]}"; do # loop through them and assign some local variables - local service_name - service_name=$(echo "${ports_in_use[$i]}" | awk '{gsub(/users:\(\("/,"",$7);gsub(/".*/,"",$7);print $7}') - local protocol_type - protocol_type=$(echo "${ports_in_use[$i]}" | awk '{print $1}') - local port_number - port_number="$(echo "${ports_in_use[$i]}" | awk '{print $5}')" # | awk '{gsub(/^.*:/,"",$5);print $5}') + read -r protocol_type port_number service_name <<< "$( + awk '{ + p=$1; n=$5; s=$7 + gsub(/users:\(\("/,"",s) + gsub(/".*/,"",s) + print p, n, s + }' <<< "${ports_in_use[$i]}" + )" # Check if the right services are using the right ports - if [[ ${ports_configured[*]} =~ $(echo "${port_number}" | rev | cut -d: -f1 | rev) ]]; then + if [[ ${ports_configured[*]} =~ ${port_number##*:} ]]; then compare_port_to_service_assigned "${ftl}" "${service_name}" "${protocol_type}:${port_number}" else # If it's not a default port that Pi-hole needs, just print it out for the user to see @@ -722,7 +709,7 @@ dig_at() { fi # Check if Pi-hole can use itself to block a domain - if local_dig="$(dig +tries=1 +time=2 -"${protocol}" "${random_url}" @"${local_address}" "${record_type}")"; then + if local_dig="$(dig +tries=1 +time=2 -"${protocol}" "${random_url}" @"${local_address}" "${record_type}" -p "$(get_ftl_conf_value "dns.port")")"; then # If it can, show success if [[ "${local_dig}" == *"status: NOERROR"* ]]; then local_dig="NOERROR" @@ -816,42 +803,27 @@ ftl_full_status(){ make_array_from_file() { local filename="${1}" + + # If the file is a directory do nothing since it cannot be parsed + [[ -d "${filename}" ]] && return + # The second argument can put a limit on how many line should be read from the file # Since some of the files are so large, this is helpful to limit the output local limit=${2} # A local iterator for testing if we are at the limit above local i=0 - # If the file is a directory - if [[ -d "${filename}" ]]; then - # do nothing since it cannot be parsed - : - else - # Otherwise, read the file line by line - while IFS= read -r line;do - # Otherwise, strip out comments and blank lines - new_line=$(echo "${line}" | sed -e 's/^\s*#.*$//' -e '/^$/d') - # If the line still has content (a non-zero value) - if [[ -n "${new_line}" ]]; then - # If the string contains "### CHANGED", highlight this part in red - if [[ "${new_line}" == *"### CHANGED"* ]]; then - new_line="${new_line//### CHANGED/${COL_RED}### CHANGED${COL_NC}}" - fi + # Process the file, strip out comments and blank lines + local processed + processed=$(sed -e 's/^\s*#.*$//' -e '/^$/d' "${filename}") - # Finally, write this line to the log - log_write " ${new_line}" - fi - # Increment the iterator +1 - i=$((i+1)) - # but if the limit of lines we want to see is exceeded - if [[ -z ${limit} ]]; then - # do nothing - : - elif [[ $i -eq ${limit} ]]; then - break - fi - done < "${filename}" - fi + while IFS= read -r line; do + # If the string contains "### CHANGED", highlight this part in red + log_write " ${line//### CHANGED/${COL_RED}### CHANGED${COL_NC}}" + ((i++)) + # if the limit of lines we want to see is exceeded do nothing + [[ -n ${limit} && $i -eq ${limit} ]] && break + done <<< "$processed" } parse_file() { @@ -924,38 +896,38 @@ list_files_in_dir() { fi # Store the files found in an array - mapfile -t files_found < <(ls "${dir_to_parse}") + local files_found=("${dir_to_parse}"/*) # For each file in the array, for each_file in "${files_found[@]}"; do - if [[ -d "${dir_to_parse}/${each_file}" ]]; then + if [[ -d "${each_file}" ]]; then # If it's a directory, do nothing : - elif [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_DEBUG_LOG}" ]] || \ - [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_RAW_BLOCKLIST_FILES}" ]] || \ - [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_INSTALL_LOG_FILE}" ]] || \ - [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_LOG}" ]] || \ - [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_LOG_GZIPS}" ]]; then + elif [[ "${each_file}" == "${PIHOLE_DEBUG_LOG}" ]] || \ + [[ "${each_file}" == "${PIHOLE_RAW_BLOCKLIST_FILES}" ]] || \ + [[ "${each_file}" == "${PIHOLE_INSTALL_LOG_FILE}" ]] || \ + [[ "${each_file}" == "${PIHOLE_LOG}" ]] || \ + [[ "${each_file}" == "${PIHOLE_LOG_GZIPS}" ]]; then : elif [[ "${dir_to_parse}" == "${DNSMASQ_D_DIRECTORY}" ]]; then # in case of the dnsmasq directory include all files in the debug output - log_write "\\n${COL_GREEN}$(ls -lhd "${dir_to_parse}"/"${each_file}")${COL_NC}" - make_array_from_file "${dir_to_parse}/${each_file}" + log_write "\\n${COL_GREEN}$(ls -lhd "${each_file}")${COL_NC}" + make_array_from_file "${each_file}" else # Then, parse the file's content into an array so each line can be analyzed if need be for i in "${!REQUIRED_FILES[@]}"; do - if [[ "${dir_to_parse}/${each_file}" == "${REQUIRED_FILES[$i]}" ]]; then + if [[ "${each_file}" == "${REQUIRED_FILES[$i]}" ]]; then # display the filename - log_write "\\n${COL_GREEN}$(ls -lhd "${dir_to_parse}"/"${each_file}")${COL_NC}" + log_write "\\n${COL_GREEN}$(ls -lhd "${each_file}")${COL_NC}" # Check if the file we want to view has a limit (because sometimes we just need a little bit of info from the file, not the entire thing) - case "${dir_to_parse}/${each_file}" in + case "${each_file}" in # If it's Web server log, give the first and last 25 lines - "${PIHOLE_WEBSERVER_LOG}") head_tail_log "${dir_to_parse}/${each_file}" 25 + "${PIHOLE_WEBSERVER_LOG}") head_tail_log "${each_file}" 25 ;; # Same for the FTL log - "${PIHOLE_FTL_LOG}") head_tail_log "${dir_to_parse}/${each_file}" 35 + "${PIHOLE_FTL_LOG}") head_tail_log "${each_file}" 35 ;; # parse the file into an array in case we ever need to analyze it line-by-line - *) make_array_from_file "${dir_to_parse}/${each_file}"; + *) make_array_from_file "${each_file}"; esac else # Otherwise, do nothing since it's not a file needed for Pi-hole so we don't care about it @@ -991,6 +963,7 @@ head_tail_log() { local filename="${1}" # The number of lines to use for head and tail local qty="${2}" + local filebasename="${filename##*/}" local head_line local tail_line # Put the current Internal Field Separator into another variable so it can be restored later @@ -999,14 +972,14 @@ head_tail_log() { IFS=$'\r\n' local log_head=() mapfile -t log_head < <(head -n "${qty}" "${filename}") - log_write " ${COL_CYAN}-----head of $(basename "${filename}")------${COL_NC}" + log_write " ${COL_CYAN}-----head of ${filebasename}------${COL_NC}" for head_line in "${log_head[@]}"; do log_write " ${head_line}" done log_write "" local log_tail=() mapfile -t log_tail < <(tail -n "${qty}" "${filename}") - log_write " ${COL_CYAN}-----tail of $(basename "${filename}")------${COL_NC}" + log_write " ${COL_CYAN}-----tail of ${filebasename}------${COL_NC}" for tail_line in "${log_tail[@]}"; do log_write " ${tail_line}" done @@ -1033,6 +1006,24 @@ show_db_entries() { ) for line in "${entries[@]}"; do + # Use gray color for "no". Normal color for "yes" + line=${line//--no---/${COL_GRAY} no ${COL_NC}} + line=${line//--yes--/ yes } + + # Use red for "deny" and green for "allow" + if [ "$title" = "Domainlist" ]; then + line=${line//regex-deny/${COL_RED}regex-deny${COL_NC}} + line=${line//regex-allow/${COL_GREEN}regex-allow${COL_NC}} + line=${line//exact-deny/${COL_RED}exact-deny${COL_NC}} + line=${line//exact-allow/${COL_GREEN}exact-allow${COL_NC}} + fi + + # Use red for "block" and green for "allow" + if [ "$title" = "Adlists" ]; then + line=${line//-BLOCK-/${COL_RED} Block ${COL_NC}} + line=${line//-ALLOW-/${COL_GREEN} Allow ${COL_NC}} + fi + log_write " ${line}" done @@ -1080,15 +1071,15 @@ check_dhcp_servers() { } show_groups() { - show_db_entries "Groups" "SELECT id,CASE enabled WHEN '0' THEN ' 0' WHEN '1' THEN ' 1' ELSE enabled END enabled,name,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,description FROM \"group\"" "4 7 50 19 19 50" + show_db_entries "Groups" "SELECT id,CASE enabled WHEN '0' THEN '--no---' WHEN '1' THEN '--yes--' ELSE enabled END enabled,name,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,description FROM \"group\"" "4 7 50 19 19 50" } show_adlists() { - show_db_entries "Adlists" "SELECT id,CASE enabled WHEN '0' THEN ' 0' WHEN '1' THEN ' 1' ELSE enabled END enabled,GROUP_CONCAT(adlist_by_group.group_id) group_ids,address,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,comment FROM adlist LEFT JOIN adlist_by_group ON adlist.id = adlist_by_group.adlist_id GROUP BY id;" "5 7 12 100 19 19 50" + show_db_entries "Adlists" "SELECT id,CASE enabled WHEN '0' THEN '--no---' WHEN '1' THEN '--yes--' ELSE enabled END enabled,GROUP_CONCAT(adlist_by_group.group_id) group_ids, CASE type WHEN '0' THEN '-BLOCK-' WHEN '1' THEN '-ALLOW-' ELSE type END type, address,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,comment FROM adlist LEFT JOIN adlist_by_group ON adlist.id = adlist_by_group.adlist_id GROUP BY id;" "5 7 12 7 100 19 19 50" } show_domainlist() { - show_db_entries "Domainlist (0/1 = exact allow-/denylist, 2/3 = regex allow-/denylist)" "SELECT id,CASE type WHEN '0' THEN '0 ' WHEN '1' THEN ' 1 ' WHEN '2' THEN ' 2 ' WHEN '3' THEN ' 3' ELSE type END type,CASE enabled WHEN '0' THEN ' 0' WHEN '1' THEN ' 1' ELSE enabled END enabled,GROUP_CONCAT(domainlist_by_group.group_id) group_ids,domain,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,comment FROM domainlist LEFT JOIN domainlist_by_group ON domainlist.id = domainlist_by_group.domainlist_id GROUP BY id;" "5 4 7 12 100 19 19 50" + show_db_entries "Domainlist" "SELECT id,CASE type WHEN '0' THEN 'exact-allow' WHEN '1' THEN 'exact-deny' WHEN '2' THEN 'regex-allow' WHEN '3' THEN 'regex-deny' ELSE type END type,CASE enabled WHEN '0' THEN '--no---' WHEN '1' THEN '--yes--' ELSE enabled END enabled,GROUP_CONCAT(domainlist_by_group.group_id) group_ids,domain,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,comment FROM domainlist LEFT JOIN domainlist_by_group ON domainlist.id = domainlist_by_group.domainlist_id GROUP BY id;" "5 11 7 12 90 19 19 50" } show_clients() { diff --git a/advanced/Scripts/update.sh b/advanced/Scripts/update.sh index 4e0d973e..67ac9693 100755 --- a/advanced/Scripts/update.sh +++ b/advanced/Scripts/update.sh @@ -149,31 +149,37 @@ main() { echo -e " ${INFO} Web Interface:\\t${COL_GREEN}up to date${COL_NC}" fi - local funcOutput - funcOutput=$(get_binary_name) #Store output of get_binary_name here - local binary - binary="pihole-FTL${funcOutput##*pihole-FTL}" #binary name will be the last line of the output of get_binary_name (it always begins with pihole-FTL) + # Allow the user to skip this check if they are using a self-compiled FTL binary from an unsupported architecture + if [ "${skipFTL}" != true ]; then + local funcOutput + funcOutput=$(get_binary_name) #Store output of get_binary_name here + local binary + binary="pihole-FTL${funcOutput##*pihole-FTL}" #binary name will be the last line of the output of get_binary_name (it always begins with pihole-FTL) - if FTLcheckUpdate "${binary}" &>/dev/null; then - FTL_update=true - echo -e " ${INFO} FTL:\\t\\t${COL_YELLOW}update available${COL_NC}" + if FTLcheckUpdate "${binary}" &>/dev/null; then + FTL_update=true + echo -e " ${INFO} FTL:\\t\\t${COL_YELLOW}update available${COL_NC}" + else + case $? in + 1) + echo -e " ${INFO} FTL:\\t\\t${COL_GREEN}up to date${COL_NC}" + ;; + 2) + echo -e " ${INFO} FTL:\\t\\t${COL_RED}Branch is not available.${COL_NC}\\n\\t\\t\\tUse ${COL_GREEN}pihole checkout ftl [branchname]${COL_NC} to switch to a valid branch." + exit 1 + ;; + 3) + echo -e " ${INFO} FTL:\\t\\t${COL_RED}Something has gone wrong, cannot reach download server${COL_NC}" + exit 1 + ;; + *) + echo -e " ${INFO} FTL:\\t\\t${COL_RED}Something has gone wrong, contact support${COL_NC}" + exit 1 + esac + FTL_update=false + fi else - case $? in - 1) - echo -e " ${INFO} FTL:\\t\\t${COL_GREEN}up to date${COL_NC}" - ;; - 2) - echo -e " ${INFO} FTL:\\t\\t${COL_RED}Branch is not available.${COL_NC}\\n\\t\\t\\tUse ${COL_GREEN}pihole checkout ftl [branchname]${COL_NC} to switch to a valid branch." - exit 1 - ;; - 3) - echo -e " ${INFO} FTL:\\t\\t${COL_RED}Something has gone wrong, cannot reach download server${COL_NC}" - exit 1 - ;; - *) - echo -e " ${INFO} FTL:\\t\\t${COL_RED}Something has gone wrong, contact support${COL_NC}" - exit 1 - esac + echo -e " ${INFO} FTL:\\t\\t${COL_YELLOW}--skipFTL set - update check skipped${COL_NC}" FTL_update=false fi @@ -222,7 +228,14 @@ main() { fi if [[ "${FTL_update}" == true || "${core_update}" == true ]]; then - ${PI_HOLE_FILES_DIR}/automated\ install/basic-install.sh --repair --unattended || \ + local addionalFlag + + if [[ ${skipFTL} == true ]]; then + addionalFlag="--skipFTL" + else + addionalFlag="" + fi + ${PI_HOLE_FILES_DIR}/automated\ install/basic-install.sh --repair --unattended ${addionalFlag} || \ echo -e "${basicError}" && exit 1 fi @@ -242,8 +255,15 @@ main() { exit 0 } -if [[ "$1" == "--check-only" ]]; then - CHECK_ONLY=true -fi +CHECK_ONLY=false +skipFTL=false + +# Check arguments +for var in "$@"; do + case "$var" in + "--check-only") CHECK_ONLY=true ;; + "--skipFTL") skipFTL=true ;; + esac +done main diff --git a/advanced/Scripts/updatecheck.sh b/advanced/Scripts/updatecheck.sh index 44f21419..23abf7a6 100755 --- a/advanced/Scripts/updatecheck.sh +++ b/advanced/Scripts/updatecheck.sh @@ -50,9 +50,10 @@ rm -f "/etc/pihole/GitHubVersions" rm -f "/etc/pihole/localbranches" rm -f "/etc/pihole/localversions" -# Create new versions file if it does not exist VERSION_FILE="/etc/pihole/versions" -touch "${VERSION_FILE}" + +# Truncates the file to zero length if it exists to clear it up, otherwise creates an empty file. +truncate -s 0 "${VERSION_FILE}" chmod 644 "${VERSION_FILE}" # if /pihole.docker.tag file exists, we will use it's value later in this script diff --git a/advanced/Scripts/utils.sh b/advanced/Scripts/utils.sh index d000a6db..51f5db36 100755 --- a/advanced/Scripts/utils.sh +++ b/advanced/Scripts/utils.sh @@ -30,9 +30,6 @@ addOrEditKeyValPair() { local key="${2}" local value="${3}" - # touch file to prevent grep error if file does not exist yet - touch "${file}" - if grep -q "^${key}=" "${file}"; then # Key already exists in file, modify the value sed -i "/^${key}=/c\\${key}=${value}" "${file}" diff --git a/advanced/Templates/pihole-FTL-prestart.sh b/advanced/Templates/pihole-FTL-prestart.sh index 579309d3..095c0d2d 100755 --- a/advanced/Templates/pihole-FTL-prestart.sh +++ b/advanced/Templates/pihole-FTL-prestart.sh @@ -8,12 +8,20 @@ utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" # Get file paths FTL_PID_FILE="$(getFTLConfigValue files.pid)" +FTL_LOG_FILE="$(getFTLConfigValue files.log.ftl)" +PIHOLE_LOG_FILE="$(getFTLConfigValue files.log.dnsmasq)" +WEBSERVER_LOG_FILE="$(getFTLConfigValue files.log.webserver)" +FTL_PID_FILE="${FTL_PID_FILE:-/run/pihole-FTL.pid}" +FTL_LOG_FILE="${FTL_LOG_FILE:-/var/log/pihole/FTL.log}" +PIHOLE_LOG_FILE="${PIHOLE_LOG_FILE:-/var/log/pihole/pihole.log}" +WEBSERVER_LOG_FILE="${WEBSERVER_LOG_FILE:-/var/log/pihole/webserver.log}" # Ensure that permissions are set so that pihole-FTL can edit all necessary files mkdir -p /var/log/pihole chown -R pihole:pihole /etc/pihole/ /var/log/pihole/ # allow all users read version file (and use pihole -v) +touch /etc/pihole/versions chmod 0644 /etc/pihole/versions # allow pihole to access subdirs in /etc/pihole (sets execution bit on dirs) @@ -28,7 +36,7 @@ chown root:root /etc/pihole/logrotate # Touch files to ensure they exist (create if non-existing, preserve if existing) [ -f "${FTL_PID_FILE}" ] || install -D -m 644 -o pihole -g pihole /dev/null "${FTL_PID_FILE}" -[ -f /var/log/pihole/FTL.log ] || install -m 640 -o pihole -g pihole /dev/null /var/log/pihole/FTL.log -[ -f /var/log/pihole/pihole.log ] || install -m 640 -o pihole -g pihole /dev/null /var/log/pihole/pihole.log -[ -f /var/log/pihole/webserver.log ] || install -m 640 -o pihole -g pihole /dev/null /var/log/pihole/webserver.log +[ -f "${FTL_LOG_FILE}" ] || install -m 640 -o pihole -g pihole /dev/null "${FTL_LOG_FILE}" +[ -f "${PIHOLE_LOG_FILE}" ] || install -m 640 -o pihole -g pihole /dev/null "${PIHOLE_LOG_FILE}" +[ -f "${WEBSERVER_LOG_FILE}" ] || install -m 640 -o pihole -g pihole /dev/null "${WEBSERVER_LOG_FILE}" [ -f /etc/pihole/dhcp.leases ] || install -m 644 -o pihole -g pihole /dev/null /etc/pihole/dhcp.leases diff --git a/advanced/Templates/pihole-FTL.openrc b/advanced/Templates/pihole-FTL.openrc index 34a30a0b..2207273e 100644 --- a/advanced/Templates/pihole-FTL.openrc +++ b/advanced/Templates/pihole-FTL.openrc @@ -13,7 +13,7 @@ extra_started_commands="reload" respawn_max=5 respawn_period=60 -capabilities="^CAP_NET_BIND_SERVICE,CAP_NET_RAW,CAP_NET_ADMIN,CAP_SYS_NICE,CAP_IPC_LOCK,CAP_CHOWN,CAP_SYS_TIME" +capabilities="^CAP_NET_BIND_SERVICE,^CAP_NET_RAW,^CAP_NET_ADMIN,^CAP_SYS_NICE,^CAP_IPC_LOCK,^CAP_CHOWN,^CAP_SYS_TIME" depend() { want net diff --git a/advanced/Templates/pihole-FTL.service b/advanced/Templates/pihole-FTL.service index 7c7e9962..9cdad7a0 100644 --- a/advanced/Templates/pihole-FTL.service +++ b/advanced/Templates/pihole-FTL.service @@ -57,9 +57,9 @@ start() { stop() { if is_running; then kill "${FTL_PID}" - # Give FTL 60 seconds to gracefully stop + # Give FTL 120 seconds to gracefully stop i=1 - while [ "${i}" -le 60 ]; do + while [ "${i}" -le 120 ]; do if ! is_running; then break fi diff --git a/advanced/Templates/pihole-FTL.systemd b/advanced/Templates/pihole-FTL.systemd index fcbb8d8d..5024b9a2 100644 --- a/advanced/Templates/pihole-FTL.systemd +++ b/advanced/Templates/pihole-FTL.systemd @@ -17,18 +17,18 @@ StartLimitIntervalSec=60s [Service] User=pihole -PermissionsStartOnly=true AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_NICE CAP_IPC_LOCK CAP_CHOWN CAP_SYS_TIME -ExecStartPre=/opt/pihole/pihole-FTL-prestart.sh +# Run prestart with elevated permissions +ExecStartPre=+/opt/pihole/pihole-FTL-prestart.sh ExecStart=/usr/bin/pihole-FTL -f Restart=on-failure RestartSec=5s ExecReload=/bin/kill -HUP $MAINPID -ExecStopPost=/opt/pihole/pihole-FTL-poststop.sh +ExecStopPost=+/opt/pihole/pihole-FTL-poststop.sh # Use graceful shutdown with a reasonable timeout -TimeoutStopSec=60s +TimeoutStopSec=120s # Make /usr, /boot, /etc and possibly some more folders read-only... ProtectSystem=full diff --git a/automated install/basic-install.sh b/automated install/basic-install.sh index 65464b9e..b12a2714 100755 --- a/automated install/basic-install.sh +++ b/automated install/basic-install.sh @@ -116,11 +116,11 @@ c=70 PIHOLE_META_PACKAGE_CONTROL_APT=$( cat < Architecture: all Description: Pi-hole dependency meta package -Depends: awk,bash-completion,binutils,ca-certificates,cron|cron-daemon,curl,dialog,dnsutils,dns-root-data,git,grep,iproute2,iputils-ping,jq,libcap2,libcap2-bin,lshw,procps,psmisc,sudo,unzip +Depends: awk,bash-completion,binutils,ca-certificates,cron|cron-daemon,curl,dialog,bind9-dnsutils|dnsutils,dns-root-data,git,grep,iproute2,iputils-ping,jq,libcap2,libcap2-bin,lshw,procps,psmisc,sudo,unzip Section: contrib/metapackages Priority: optional EOM @@ -155,7 +155,7 @@ EOM ) # List of required packages on APK based systems -PIHOLE_META_VERSION_APK=0.1 +PIHOLE_META_VERSION_APK=0.2 PIHOLE_META_DEPS_APK=( bash bash-completion @@ -165,6 +165,8 @@ PIHOLE_META_DEPS_APK=( cronie curl dialog + doas # sudo replacement + doas-sudo-shim git grep iproute2-minimal # piholeARPTable.sh @@ -178,10 +180,8 @@ PIHOLE_META_DEPS_APK=( procps-ng psmisc shadow - sudo tzdata unzip - wget ) ######## Undocumented Flags. Shhh ######## @@ -189,14 +189,27 @@ PIHOLE_META_DEPS_APK=( # The runUnattended flag is one example of this repair=false runUnattended=false +skipFTL=false # Check arguments for the undocumented flags for var in "$@"; do case "${var}" in "--repair") repair=true ;; "--unattended") runUnattended=true ;; + "--skipFTL") skipFTL=true ;; esac done +if [[ "${runUnattended}" == true ]]; then + # In order to run an unattended setup, a pre-seeded /etc/pihole/pihole.toml must exist + if [[ ! -f "${PI_HOLE_CONFIG_DIR}/pihole.toml" ]]; then + printf " %b Error: \"%s\" not found. Cannot run unattended setup\\n" "${CROSS}" "${PI_HOLE_CONFIG_DIR}/pihole.toml" + exit 1 + fi + printf " %b Performing unattended setup, no dialogs will be displayed\\n" "${INFO}" + # also disable debconf-apt-progress dialogs + export DEBIAN_FRONTEND="noninteractive" +fi + # If the color table file exists, if [[ -f "${coltable}" ]]; then # source it @@ -694,10 +707,11 @@ chooseInterface() { status="OFF" done # Disable check for double quote here as we are passing a string with spaces + # shellcheck disable=SC2086 PIHOLE_INTERFACE=$(dialog --no-shadow --keep-tite --output-fd 1 \ --cancel-label "Exit" --ok-label "Select" \ --radiolist "Choose An Interface (press space to toggle selection)" \ - ${r} ${c} "${interfaceCount}" "${interfacesList}") + ${r} ${c} "${interfaceCount}" ${interfacesList}) result=$? case ${result} in @@ -1251,10 +1265,6 @@ install_manpage() { # if not present, create man8 directory install -d -m 755 /usr/local/share/man/man8 fi - if [[ ! -d "/usr/local/share/man/man5" ]]; then - # if not present, create man5 directory - install -d -m 755 /usr/local/share/man/man5 - fi # Testing complete, copy the files & update the man db install -D -m 644 -T ${PI_HOLE_LOCAL_REPO}/manpages/pihole.8 /usr/local/share/man/man8/pihole.8 @@ -1807,6 +1817,12 @@ clone_or_reset_repos() { # If the user wants to repair/update, if [[ "${repair}" == true ]]; then printf " %b Resetting local repos\\n" "${INFO}" + + # import getFTLConfigValue from utils.sh + source "/opt/pihole/utils.sh" + # Use the configured Web repo location on repair/update + webInterfaceDir=$(getFTLConfigValue "webserver.paths.webroot")$(getFTLConfigValue "webserver.paths.webhome") + # Reset the Core repo resetRepo ${PI_HOLE_LOCAL_REPO} || { @@ -1948,9 +1964,18 @@ get_binary_name() { # If the machine is aarch64 (armv8) if [[ "${machine}" == "aarch64" ]]; then - # If AArch64 is found (e.g., BCM2711 in Raspberry Pi 4) - printf "%b %b Detected AArch64 (64 Bit ARM) architecture\\n" "${OVER}" "${TICK}" - l_binary="pihole-FTL-arm64" + if [[ "$(getconf LONG_BIT)" == "64" ]]; then + # If the OS is 64 bit, we use the arm64 binary + printf "%b %b Detected AArch64 (64 Bit ARM) architecture\\n" "${OVER}" "${TICK}" + l_binary="pihole-FTL-arm64" + else + # If the OS is 32 bit, we use the armv7 binary (aarch64 is actually armv8) + # Even though the machine is 64 bit capable, this makes debugging + # very hard as 32bit tools like gdb, etc. cannot analyze the 64 bit + # binary. See FTL issue #2494 for such an example. + printf "%b %b Detected AArch64 (64 Bit ARM) architecture with 32 bit OS\\n" "${OVER}" "${TICK}" + l_binary="pihole-FTL-armv7" + fi elif [[ "${machine}" == "arm"* ]]; then # ARM 32 bit # Get supported processor from other binaries installed on the system @@ -2320,21 +2345,18 @@ main() { # Check if there is a usable FTL binary available on this architecture - do # this early on as FTL is a hard dependency for Pi-hole - local funcOutput - funcOutput=$(get_binary_name) #Store output of get_binary_name here - # Abort early if this processor is not supported (get_binary_name returns empty string) - if [[ "${funcOutput}" == "" ]]; then - printf " %b Upgrade/install aborted\\n" "${CROSS}" "${DISTRO_NAME}" - exit 1 - fi - - if [[ "${fresh_install}" == false ]]; then - # if it's running unattended, - if [[ "${runUnattended}" == true ]]; then - printf " %b Performing unattended setup, no dialogs will be displayed\\n" "${INFO}" - # also disable debconf-apt-progress dialogs - export DEBIAN_FRONTEND="noninteractive" + # Allow the user to skip this check if they are using a self-compiled FTL binary from an unsupported architecture + if [ "${skipFTL}" != true ]; then + # Get the binary name for the current architecture + local funcOutput + funcOutput=$(get_binary_name) #Store output of get_binary_name here + # Abort early if this processor is not supported (get_binary_name returns empty string) + if [[ "${funcOutput}" == "" ]]; then + printf " %b Upgrade/install aborted\\n" "${CROSS}" "${DISTRO_NAME}" + exit 1 fi + else + printf " %b %b--skipFTL set - skipping architecture check%b\\n" "${INFO}" "${COL_YELLOW}" "${COL_NC}" fi if [[ "${fresh_install}" == true ]]; then @@ -2367,13 +2389,18 @@ main() { create_pihole_user # Download and install FTL - local binary - binary="pihole-FTL${funcOutput##*pihole-FTL}" #binary name will be the last line of the output of get_binary_name (it always begins with pihole-FTL) - local theRest - theRest="${funcOutput%pihole-FTL*}" # Print the rest of get_binary_name's output to display (cut out from first instance of "pihole-FTL") - if ! FTLdetect "${binary}" "${theRest}"; then - printf " %b FTL Engine not installed\\n" "${CROSS}" - exit 1 + # Allow the user to skip this check if they are using a self-compiled FTL binary from an unsupported architecture + if [ "${skipFTL}" != true ]; then + local binary + binary="pihole-FTL${funcOutput##*pihole-FTL}" #binary name will be the last line of the output of get_binary_name (it always begins with pihole-FTL) + local theRest + theRest="${funcOutput%pihole-FTL*}" # Print the rest of get_binary_name's output to display (cut out from first instance of "pihole-FTL") + if ! FTLdetect "${binary}" "${theRest}"; then + printf " %b FTL Engine not installed\\n" "${CROSS}" + exit 1 + fi + else + printf " %b %b--skipFTL set - skipping FTL binary installation%b\\n" "${INFO}" "${COL_YELLOW}" "${COL_NC}" fi # Install and log everything to a file diff --git a/gravity.sh b/gravity.sh index 5720ca41..b772dc61 100755 --- a/gravity.sh +++ b/gravity.sh @@ -612,7 +612,7 @@ compareLists() { gravity_DownloadBlocklistFromUrl() { local url="${1}" adlistID="${2}" saveLocation="${3}" compression="${4}" gravity_type="${5}" domain="${6}" local listCurlBuffer str httpCode success="" ip customUpstreamResolver="" - local file_path permissions ip_addr port blocked=false download=true + local file_path ip_addr port blocked=false download=true # modifiedOptions is an array to store all the options used to check if the adlist has been changed upstream local modifiedOptions=() @@ -721,29 +721,40 @@ gravity_DownloadBlocklistFromUrl() { fi fi - # If we are going to "download" a local file, we first check if the target - # file has a+r permission. We explicitly check for all+read because we want - # to make sure that the file is readable by everyone and not just the user - # running the script. - if [[ $url == "file://"* ]]; then + # If we "download" a local file (file://), verify read access before using it. + # When running as root (e.g., via pihole -g), check that the 'pihole' user can read the file + # to match the effective runtime user of FTL; otherwise, check the current user's read access + # (e.g., in Docker or when invoked by a non-root user). The target must + # resolve to a regular file and be readable by the evaluated user. + if [[ "${url}" == "file:/"* ]]; then # Get the file path - file_path=$(echo "$url" | cut -d'/' -f3-) + file_path=$(echo "${url}" | cut -d'/' -f3-) # Check if the file exists and is a regular file (i.e. not a socket, fifo, tty, block). Might still be a symlink. - if [[ ! -f $file_path ]]; then - # Output that the file does not exist - echo -e "${OVER} ${CROSS} ${file_path} does not exist" - download=false - else - # Check if the file or a file referenced by the symlink has a+r permissions - permissions=$(stat -L -c "%a" "$file_path") - if [[ $permissions == *4 || $permissions == *5 || $permissions == *6 || $permissions == *7 ]]; then - # Output that we are using the local file - echo -e "${OVER} ${INFO} Using local file ${file_path}" - else - # Output that the file does not have the correct permissions - echo -e "${OVER} ${CROSS} Cannot read file (file needs to have a+r permission)" + if [[ ! -f ${file_path} ]]; then + # Output that the file does not exist + echo -e "${OVER} ${CROSS} ${file_path} does not exist" download=false - fi + else + if [ "$(id -un)" == "root" ]; then + # If we are root, we need to check if the pihole user has read permission + # otherwise, we might read files that the pihole user should not be able to read + if sudo -u pihole test -r "${file_path}"; then + echo -e "${OVER} ${INFO} Using local file ${file_path}" + else + echo -e "${OVER} ${CROSS} Cannot read file (user 'pihole' lacks read permission)" + download=false + fi + else + # If we are not root, we just check if the current user has read permission + if [[ -r "${file_path}" ]]; then + # Output that we are using the local file + echo -e "${OVER} ${INFO} Using local file ${file_path}" + else + # Output that the file is not readable by the current user + echo -e "${OVER} ${CROSS} Cannot read file (current user '$(id -un)' lacks read permission)" + download=false + fi + fi fi fi @@ -811,6 +822,10 @@ gravity_DownloadBlocklistFromUrl() { fix_owner_permissions "${saveLocation}" # Compare lists if they are identical compareLists "${adlistID}" "${saveLocation}" + # Set permissions for the *.etag file + if [[ -f "${saveLocation}.etag" ]]; then + fix_owner_permissions "${saveLocation}.etag" + fi # Add domains to database table file pihole-FTL "${gravity_type}" parseList "${saveLocation}" "${gravityTEMPfile}" "${adlistID}" done="true" @@ -947,7 +962,7 @@ database_recovery() { else echo -e "${OVER} ${CROSS} ${str} - the following errors happened:" while IFS= read -r line; do echo " - $line"; done <<<"$result" - echo -e " ${CROSS} Recovery failed. Try \"pihole -r recreate\" instead." + echo -e " ${CROSS} Recovery failed. Try \"pihole -g -r recreate\" instead." exit 1 fi echo "" diff --git a/pihole b/pihole index 4fe09418..edebf48c 100755 --- a/pihole +++ b/pihole @@ -135,7 +135,22 @@ repairPiholeFunc() { if [ -n "${DOCKER_VERSION}" ]; then unsupportedFunc else - /etc/.pihole/automated\ install/basic-install.sh --repair + local skipFTL additionalFlag + skipFTL=false + # Check arguments + for var in "$@"; do + case "$var" in + "--skipFTL") skipFTL=true ;; + esac + done + + if [ "${skipFTL}" == true ]; then + additionalFlag="--skipFTL" + else + additionalFlag="" + fi + + /etc/.pihole/automated\ install/basic-install.sh --repair ${additionalFlag} exit 0; fi } @@ -615,7 +630,7 @@ case "${1}" in "-d" | "debug" ) debugFunc "$@";; "-f" | "flush" ) flushFunc "$@";; "-up" | "updatePihole" ) updatePiholeFunc "$@";; - "-r" | "repair" ) repairPiholeFunc;; + "-r" | "repair" ) repairPiholeFunc "$@";; "-g" | "updateGravity" ) updateGravityFunc "$@";; "-l" | "logging" ) piholeLogging "$@";; "uninstall" ) uninstallFunc;; diff --git a/test/_alpine_3_23.Dockerfile b/test/_alpine_3_23.Dockerfile new file mode 100644 index 00000000..2cb34137 --- /dev/null +++ b/test/_alpine_3_23.Dockerfile @@ -0,0 +1,18 @@ +FROM alpine:3.23 + +ENV GITDIR=/etc/.pihole +ENV SCRIPTDIR=/opt/pihole +RUN sed -i 's/#\(.*\/community\)/\1/' /etc/apk/repositories +RUN apk --no-cache add bash coreutils curl git jq openrc shadow + +RUN mkdir -p $GITDIR $SCRIPTDIR /etc/pihole +ADD . $GITDIR +RUN cp $GITDIR/advanced/Scripts/*.sh $GITDIR/gravity.sh $GITDIR/pihole $GITDIR/automated\ install/*.sh $GITDIR/advanced/Scripts/COL_TABLE $SCRIPTDIR/ +ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR + +RUN true && \ + chmod +x $SCRIPTDIR/* + +ENV SKIP_INSTALL=true + +#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_fedora_43.Dockerfile b/test/_fedora_43.Dockerfile new file mode 100644 index 00000000..85f06ff8 --- /dev/null +++ b/test/_fedora_43.Dockerfile @@ -0,0 +1,17 @@ +FROM fedora:43 +RUN dnf install -y git initscripts + +ENV GITDIR=/etc/.pihole +ENV SCRIPTDIR=/opt/pihole + +RUN mkdir -p $GITDIR $SCRIPTDIR /etc/pihole +ADD . $GITDIR +RUN cp $GITDIR/advanced/Scripts/*.sh $GITDIR/gravity.sh $GITDIR/pihole $GITDIR/automated\ install/*.sh $GITDIR/advanced/Scripts/COL_TABLE $SCRIPTDIR/ +ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR + +RUN true && \ + chmod +x $SCRIPTDIR/* + +ENV SKIP_INSTALL=true + +#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/conftest.py b/test/conftest.py index dcf49790..d4c763e7 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -51,29 +51,19 @@ def mock_command(script, args, container): in unit tests """ full_script_path = "/usr/local/bin/{}".format(script) - mock_script = dedent( - r"""\ + mock_script = dedent(r"""\ #!/bin/bash -e echo "\$0 \$@" >> /var/log/{script} - case "\$1" in""".format( - script=script - ) - ) + case "\$1" in""".format(script=script)) for k, v in args.items(): - case = dedent( - """ + case = dedent(""" {arg}) echo {res} exit {retcode} - ;;""".format( - arg=k, res=v[0], retcode=v[1] - ) - ) + ;;""".format(arg=k, res=v[0], retcode=v[1])) mock_script += case - mock_script += dedent( - """ - esac""" - ) + mock_script += dedent(""" + esac""") container.run( """ cat < {script}\n{content}\nEOF @@ -94,37 +84,23 @@ def mock_command_passthrough(script, args, container): """ orig_script_path = container.check_output("command -v {}".format(script)) full_script_path = "/usr/local/bin/{}".format(script) - mock_script = dedent( - r"""\ + mock_script = dedent(r"""\ #!/bin/bash -e echo "\$0 \$@" >> /var/log/{script} - case "\$1" in""".format( - script=script - ) - ) + case "\$1" in""".format(script=script)) for k, v in args.items(): - case = dedent( - """ + case = dedent(""" {arg}) echo {res} exit {retcode} - ;;""".format( - arg=k, res=v[0], retcode=v[1] - ) - ) + ;;""".format(arg=k, res=v[0], retcode=v[1])) mock_script += case - mock_script += dedent( - r""" + mock_script += dedent(r""" *) {orig_script_path} "\$@" - ;;""".format( - orig_script_path=orig_script_path - ) - ) - mock_script += dedent( - """ - esac""" - ) + ;;""".format(orig_script_path=orig_script_path)) + mock_script += dedent(""" + esac""") container.run( """ cat < {script}\n{content}\nEOF @@ -141,29 +117,19 @@ def mock_command_run(script, args, container): in unit tests """ full_script_path = "/usr/local/bin/{}".format(script) - mock_script = dedent( - r"""\ + mock_script = dedent(r"""\ #!/bin/bash -e echo "\$0 \$@" >> /var/log/{script} - case "\$1 \$2" in""".format( - script=script - ) - ) + case "\$1 \$2" in""".format(script=script)) for k, v in args.items(): - case = dedent( - """ + case = dedent(""" \"{arg}\") echo {res} exit {retcode} - ;;""".format( - arg=k, res=v[0], retcode=v[1] - ) - ) + ;;""".format(arg=k, res=v[0], retcode=v[1])) mock_script += case - mock_script += dedent( - """ - esac""" - ) + mock_script += dedent(r""" + esac""") container.run( """ cat < {script}\n{content}\nEOF @@ -180,29 +146,19 @@ def mock_command_2(script, args, container): in unit tests """ full_script_path = "/usr/local/bin/{}".format(script) - mock_script = dedent( - r"""\ + mock_script = dedent(r"""\ #!/bin/bash -e echo "\$0 \$@" >> /var/log/{script} - case "\$1 \$2" in""".format( - script=script - ) - ) + case "\$1 \$2" in""".format(script=script)) for k, v in args.items(): - case = dedent( - """ + case = dedent(""" \"{arg}\") echo \"{res}\" exit {retcode} - ;;""".format( - arg=k, res=v[0], retcode=v[1] - ) - ) + ;;""".format(arg=k, res=v[0], retcode=v[1])) mock_script += case - mock_script += dedent( - """ - esac""" - ) + mock_script += dedent(r""" + esac""") container.run( """ cat < {script}\n{content}\nEOF diff --git a/test/requirements.txt b/test/requirements.txt index 889a9be3..61f18c4e 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -1,6 +1,6 @@ pyyaml == 6.0.3 -pytest == 8.4.2 +pytest == 9.0.2 pytest-xdist == 3.8.0 pytest-testinfra == 10.2.2 -tox == 4.31.0 +tox == 4.35.0 pytest-clarity == 1.0.1 diff --git a/test/test_any_automated_install.py b/test/test_any_automated_install.py index 0a561b36..aa48fd32 100644 --- a/test/test_any_automated_install.py +++ b/test/test_any_automated_install.py @@ -6,10 +6,8 @@ from .conftest import ( info_box, cross_box, mock_command, - mock_command_run, mock_command_2, mock_command_passthrough, - run_script, ) FTL_BRANCH = "development" @@ -23,12 +21,10 @@ def test_supported_package_manager(host): host.run("rm -rf /usr/bin/apt-get") host.run("rm -rf /usr/bin/rpm") host.run("rm -rf /sbin/apk") - package_manager_detect = host.run( - """ + package_manager_detect = host.run(""" source /opt/pihole/basic-install.sh package_manager_detect - """ - ) + """) expected_stdout = cross_box + " No supported package manager found" assert expected_stdout in package_manager_detect.stdout # assert package_manager_detect.rc == 1 @@ -38,13 +34,11 @@ def test_selinux_not_detected(host): """ confirms installer continues when SELinux configuration file does not exist """ - check_selinux = host.run( - """ + check_selinux = host.run(""" rm -f /etc/selinux/config source /opt/pihole/basic-install.sh checkSelinux - """ - ) + """) expected_stdout = info_box + " SELinux not detected" assert expected_stdout in check_selinux.stdout assert check_selinux.rc == 0 @@ -95,8 +89,7 @@ def test_installPihole_fresh_install_readableFiles(host): host.run("command -v apk > /dev/null && apk add mandoc man-pages") # Workaround to get FTLv6 installed until it reaches master branch host.run('echo "' + FTL_BRANCH + '" > /etc/pihole/ftlbranch') - install = host.run( - """ + install = host.run(""" export TERM=xterm export DEBIAN_FRONTEND=noninteractive umask 0027 @@ -105,8 +98,7 @@ def test_installPihole_fresh_install_readableFiles(host): runUnattended=true main /opt/pihole/pihole-FTL-prestart.sh - """ - ) + """) assert 0 == install.rc maninstalled = True if (info_box + " man not installed") in install.stdout: @@ -162,12 +154,6 @@ def test_installPihole_fresh_install_readableFiles(host): check_man = test_cmd.format("r", "/usr/local/share/man/man8", piholeuser) actual_rc = host.run(check_man).rc assert exit_status_success == actual_rc - check_man = test_cmd.format("x", "/usr/local/share/man/man5", piholeuser) - actual_rc = host.run(check_man).rc - assert exit_status_success == actual_rc - check_man = test_cmd.format("r", "/usr/local/share/man/man5", piholeuser) - actual_rc = host.run(check_man).rc - assert exit_status_success == actual_rc check_man = test_cmd.format( "r", "/usr/local/share/man/man8/pihole.8", piholeuser ) @@ -201,13 +187,11 @@ def test_update_package_cache_success_no_errors(host): """ confirms package cache was updated without any errors """ - updateCache = host.run( - """ + updateCache = host.run(""" source /opt/pihole/basic-install.sh package_manager_detect update_package_cache - """ - ) + """) expected_stdout = tick_box + " Update local cache of available packages" assert expected_stdout in updateCache.stdout assert "error" not in updateCache.stdout.lower() @@ -218,13 +202,11 @@ def test_update_package_cache_failure_no_errors(host): confirms package cache was not updated """ mock_command("apt-get", {"update": ("", "1")}, host) - updateCache = host.run( - """ + updateCache = host.run(""" source /opt/pihole/basic-install.sh package_manager_detect update_package_cache - """ - ) + """) expected_stdout = cross_box + " Update local cache of available packages" assert expected_stdout in updateCache.stdout assert "Error: Unable to update package cache." in updateCache.stdout @@ -260,16 +242,14 @@ def test_FTL_detect_no_errors(host, arch, detected_string, supported): host, ) host.run('echo "' + FTL_BRANCH + '" > /etc/pihole/ftlbranch') - detectPlatform = host.run( - """ + detectPlatform = host.run(""" source /opt/pihole/basic-install.sh create_pihole_user funcOutput=$(get_binary_name) binary="pihole-FTL${funcOutput##*pihole-FTL}" theRest="${funcOutput%pihole-FTL*}" FTLdetect "${binary}" "${theRest}" - """ - ) + """) if supported: expected_stdout = info_box + " FTL Checks..." assert expected_stdout in detectPlatform.stdout @@ -289,22 +269,18 @@ def test_FTL_development_binary_installed_and_responsive_no_errors(host): confirms FTL development binary is copied and functional in installed location """ host.run('echo "' + FTL_BRANCH + '" > /etc/pihole/ftlbranch') - host.run( - """ + host.run(""" source /opt/pihole/basic-install.sh create_pihole_user funcOutput=$(get_binary_name) binary="pihole-FTL${funcOutput##*pihole-FTL}" theRest="${funcOutput%pihole-FTL*}" FTLdetect "${binary}" "${theRest}" - """ - ) - version_check = host.run( - """ + """) + version_check = host.run(""" VERSION=$(pihole-FTL version) echo ${VERSION:0:1} - """ - ) + """) expected_stdout = "v" assert expected_stdout in version_check.stdout @@ -319,12 +295,10 @@ def test_IPv6_only_link_local(host): {"-6 address": ("inet6 fe80::d210:52fa:fe00:7ad7/64 scope link", "0")}, host, ) - detectPlatform = host.run( - """ + detectPlatform = host.run(""" source /opt/pihole/basic-install.sh find_IPv6_information - """ - ) + """) expected_stdout = "Unable to find IPv6 ULA/GUA address" assert expected_stdout in detectPlatform.stdout @@ -344,12 +318,10 @@ def test_IPv6_only_ULA(host): }, host, ) - detectPlatform = host.run( - """ + detectPlatform = host.run(""" source /opt/pihole/basic-install.sh find_IPv6_information - """ - ) + """) expected_stdout = "Found IPv6 ULA address" assert expected_stdout in detectPlatform.stdout @@ -369,12 +341,10 @@ def test_IPv6_only_GUA(host): }, host, ) - detectPlatform = host.run( - """ + detectPlatform = host.run(""" source /opt/pihole/basic-install.sh find_IPv6_information - """ - ) + """) expected_stdout = "Found IPv6 GUA address" assert expected_stdout in detectPlatform.stdout @@ -395,12 +365,10 @@ def test_IPv6_GUA_ULA_test(host): }, host, ) - detectPlatform = host.run( - """ + detectPlatform = host.run(""" source /opt/pihole/basic-install.sh find_IPv6_information - """ - ) + """) expected_stdout = "Found IPv6 ULA address" assert expected_stdout in detectPlatform.stdout @@ -421,12 +389,10 @@ def test_IPv6_ULA_GUA_test(host): }, host, ) - detectPlatform = host.run( - """ + detectPlatform = host.run(""" source /opt/pihole/basic-install.sh find_IPv6_information - """ - ) + """) expected_stdout = "Found IPv6 ULA address" assert expected_stdout in detectPlatform.stdout @@ -437,14 +403,10 @@ def test_validate_ip(host): """ def test_address(addr, success=True): - output = host.run( - """ + output = host.run(""" source /opt/pihole/basic-install.sh valid_ip "{addr}" - """.format( - addr=addr - ) - ) + """.format(addr=addr)) assert output.rc == 0 if success else 1 @@ -479,15 +441,13 @@ def test_validate_ip(host): def test_package_manager_has_pihole_deps(host): """Confirms OS is able to install the required packages for Pi-hole""" mock_command("dialog", {"*": ("", "0")}, host) - output = host.run( - """ + output = host.run(""" source /opt/pihole/basic-install.sh package_manager_detect update_package_cache build_dependency_package install_dependent_packages - """ - ) + """) assert "No package" not in output.stdout assert output.rc == 0 @@ -496,21 +456,17 @@ def test_package_manager_has_pihole_deps(host): def test_meta_package_uninstall(host): """Confirms OS is able to install and uninstall the Pi-hole meta package""" mock_command("dialog", {"*": ("", "0")}, host) - install = host.run( - """ + install = host.run(""" source /opt/pihole/basic-install.sh package_manager_detect update_package_cache build_dependency_package install_dependent_packages - """ - ) + """) assert install.rc == 0 - uninstall = host.run( - """ + uninstall = host.run(""" source /opt/pihole/uninstall.sh removeMetaPackage - """ - ) + """) assert uninstall.rc == 0 diff --git a/test/test_any_utils.py b/test/test_any_utils.py index 0f9ae6d2..e4646572 100644 --- a/test/test_any_utils.py +++ b/test/test_any_utils.py @@ -1,31 +1,26 @@ def test_key_val_replacement_works(host): """Confirms addOrEditKeyValPair either adds or replaces a key value pair in a given file""" - host.run( - """ + host.run(""" source /opt/pihole/utils.sh + touch ./testoutput addOrEditKeyValPair "./testoutput" "KEY_ONE" "value1" addOrEditKeyValPair "./testoutput" "KEY_TWO" "value2" addOrEditKeyValPair "./testoutput" "KEY_ONE" "value3" addOrEditKeyValPair "./testoutput" "KEY_FOUR" "value4" - """ - ) - output = host.run( - """ + """) + output = host.run(""" cat ./testoutput - """ - ) + """) expected_stdout = "KEY_ONE=value3\nKEY_TWO=value2\nKEY_FOUR=value4\n" assert expected_stdout == output.stdout def test_getFTLPID_default(host): """Confirms getFTLPID returns the default value if FTL is not running""" - output = host.run( - """ + output = host.run(""" source /opt/pihole/utils.sh getFTLPID - """ - ) + """) expected_stdout = "-1\n" assert expected_stdout == output.stdout @@ -36,8 +31,7 @@ def test_setFTLConfigValue_getFTLConfigValue(host): Requires FTL to be installed, so we do that first (taken from test_FTL_development_binary_installed_and_responsive_no_errors) """ - host.run( - """ + host.run(""" source /opt/pihole/basic-install.sh create_pihole_user funcOutput=$(get_binary_name) @@ -45,15 +39,12 @@ def test_setFTLConfigValue_getFTLConfigValue(host): binary="pihole-FTL${funcOutput##*pihole-FTL}" theRest="${funcOutput%pihole-FTL*}" FTLdetect "${binary}" "${theRest}" - """ - ) + """) - output = host.run( - """ + output = host.run(""" source /opt/pihole/utils.sh setFTLConfigValue "dns.upstreams" '["9.9.9.9"]' > /dev/null getFTLConfigValue "dns.upstreams" - """ - ) + """) assert "[ 9.9.9.9 ]" in output.stdout diff --git a/test/test_centos_fedora_common_support.py b/test/test_centos_fedora_common_support.py index 7e0bae4e..a892db87 100644 --- a/test/test_centos_fedora_common_support.py +++ b/test/test_centos_fedora_common_support.py @@ -15,14 +15,10 @@ def mock_selinux_config(state, host): # getenforce returns the running state of SELinux mock_command("getenforce", {"*": (state.capitalize(), "0")}, host) # create mock configuration with desired content - host.run( - """ + host.run(""" mkdir /etc/selinux echo "SELINUX={state}" > /etc/selinux/config - """.format( - state=state.lower() - ) - ) + """.format(state=state.lower())) def test_selinux_enforcing_exit(host): @@ -30,12 +26,10 @@ def test_selinux_enforcing_exit(host): confirms installer prompts to exit when SELinux is Enforcing by default """ mock_selinux_config("enforcing", host) - check_selinux = host.run( - """ + check_selinux = host.run(""" source /opt/pihole/basic-install.sh checkSelinux - """ - ) + """) expected_stdout = cross_box + " Current SELinux: enforcing" assert expected_stdout in check_selinux.stdout expected_stdout = "SELinux Enforcing detected, exiting installer" @@ -48,12 +42,10 @@ def test_selinux_permissive(host): confirms installer continues when SELinux is Permissive """ mock_selinux_config("permissive", host) - check_selinux = host.run( - """ + check_selinux = host.run(""" source /opt/pihole/basic-install.sh checkSelinux - """ - ) + """) expected_stdout = tick_box + " Current SELinux: permissive" assert expected_stdout in check_selinux.stdout assert check_selinux.rc == 0 @@ -64,12 +56,10 @@ def test_selinux_disabled(host): confirms installer continues when SELinux is Disabled """ mock_selinux_config("disabled", host) - check_selinux = host.run( - """ + check_selinux = host.run(""" source /opt/pihole/basic-install.sh checkSelinux - """ - ) + """) expected_stdout = tick_box + " Current SELinux: disabled" assert expected_stdout in check_selinux.stdout assert check_selinux.rc == 0 diff --git a/test/tox.alpine_3_23.ini b/test/tox.alpine_3_23.ini new file mode 100644 index 00000000..d7208064 --- /dev/null +++ b/test/tox.alpine_3_23.ini @@ -0,0 +1,10 @@ +[tox] +envlist = py3 + +[testenv:py3] +allowlist_externals = docker +deps = -rrequirements.txt +setenv = + COLUMNS=120 +commands = docker buildx build --load --progress plain -f _alpine_3_23.Dockerfile -t pytest_pihole:test_container ../ + pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py diff --git a/test/tox.fedora_43.ini b/test/tox.fedora_43.ini new file mode 100644 index 00000000..efbb0471 --- /dev/null +++ b/test/tox.fedora_43.ini @@ -0,0 +1,10 @@ +[tox] +envlist = py3 + +[testenv] +allowlist_externals = docker +deps = -rrequirements.txt +setenv = + COLUMNS=120 +commands = docker buildx build --load --progress plain -f _fedora_43.Dockerfile -t pytest_pihole:test_container ../ + pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py ./test_centos_fedora_common_support.py