diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..58192bc1 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# see https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-syntax + +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +* @pi-hole/core-maintainers diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e140f792..7a5d2064 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,8 +8,10 @@ updates: time: "10:00" open-pull-requests-limit: 10 target-branch: development - reviewers: - - "pi-hole/core-maintainers" + groups: + github-actions-dependencies: + patterns: + - "*" - package-ecosystem: pip directory: "/test" schedule: @@ -18,5 +20,7 @@ updates: time: "10:00" open-pull-requests-limit: 10 target-branch: development - reviewers: - - "pi-hole/core-maintainers" + groups: + python-dependencies: + patterns: + - "*" diff --git a/.github/release.yml b/.github/release.yml index 2e8776e9..e12a3ff7 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -2,6 +2,7 @@ changelog: exclude: labels: - internal + - dependencies authors: - dependabot - github-actions diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 77aacbec..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@v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 #v4.32.3 with: languages: 'python' - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 #v4.32.3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 #v4.32.3 diff --git a/.github/workflows/merge-conflict.yml b/.github/workflows/merge-conflict.yml index f169ab6f..5dca98ba 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@v3.0.3 + uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 #v3.0.3 with: dirtyLabel: "PR: Merge Conflict" repoToken: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index a0dd9a31..1e044ec9 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,14 +17,14 @@ jobs: issues: write steps: - - uses: actions/stale@v9.1.0 + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d #v10.1.1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 30 days-before-close: 5 stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Please comment or update this issue or it will be closed in 5 days.' stale-issue-label: '${{ env.stale_label }}' - exempt-issue-labels: 'Internal, Fixed in next release, Bug: Confirmed, Documentation Needed' + exempt-issue-labels: 'Internal, Fixed in next release, Bug: Confirmed, Documentation Needed, never-stale' exempt-all-issue-assignees: true operations-per-run: 300 close-issue-reason: 'not_planned' @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4.2.2 + 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 6952dcab..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@v9.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 7df1a32d..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@v4.2.2 + 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 e96bf7d5..6482e242 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + with: + fetch-depth: 0 # Differential ShellCheck requires full git history - name: Check scripts in repository are executable run: | @@ -28,26 +30,26 @@ jobs: # If FAIL is 1 then we fail. [[ $FAIL == 1 ]] && exit 1 || echo "Scripts are executable!" - - name: Run shellcheck - uses: ludeeus/action-shellcheck@master + - name: Differential ShellCheck + uses: redhat-plumbers-in-action/differential-shellcheck@d965e66ec0b3b2f821f75c8eff9b12442d9a7d1e #v5.5.6 with: - check_together: 'yes' - format: tty - severity: error + severity: warning + display-engine: sarif-fmt + - name: Spell-Checking - uses: codespell-project/actions-codespell@master + uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 #v2.2 with: ignore_words_file: .codespellignore - name: Get editorconfig-checker - uses: editorconfig-checker/action-editorconfig-checker@main # tag v1.0.0 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@stable + uses: psf/black@6305bf1ae645ab7541be4f5028a86239316178eb #26.1.0 with: src: "./test" options: "--check --diff --color" @@ -63,23 +65,30 @@ jobs: [ debian_11, debian_12, + debian_13, ubuntu_20, ubuntu_22, ubuntu_24, centos_9, + centos_10, 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@v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 - - name: Set up Python 3.10 - uses: actions/setup-python@v5.4.0 + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #v6.2.0 with: - python-version: "3.10" + python-version: "3.13" - name: Install wheel run: pip install wheel diff --git a/.gitignore b/.gitignore index 8016472b..6322fd3e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ __pycache__ .idea/ *.iml .vscode/ +.venv/ +.fleet/ +.cache/ diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 00000000..c4711a8f --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,6 @@ +external-sources=true # allow shellcheck to read external sources +disable=SC3043 #disable SC3043: In POSIX sh, local is undefined. +enable=useless-use-of-cat # disabled by default as of shellcheck 0.11.0 +enable=avoid-negated-conditions # avoid-negated-conditions is optional as of shellcheck 0.11.0 +enable=require-variable-braces +enable=deprecate-which diff --git a/README.md b/README.md index 7eb1fb3a..622ff202 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,9 @@ #

- - - - Pi-hole website - -
- Network-wide ad blocking via your own Linux hardware + Pi-hole website +
+ Network-wide ad blocking via your own Linux hardware

@@ -132,7 +128,10 @@ Some of the statistics you can integrate include: - Queries cached - Unique clients -Access the API via [`telnet`](https://github.com/pi-hole/FTL), the Web (`admin/api.php`) and Command Line (`pihole -c -j`). You can find out [more details over here](https://discourse.pi-hole.net/t/pi-hole-api/1863). +Access the API using: +- your browser: http://pi.hole/api/docs +- `curl`: `curl --connect-timeout 2 -ks "https://pi.hole/api/stats/summary" -H "Accept: application/json"`; +- the command line - examples: `pihole api config/webserver/port` or `pihole api stats/summary`. ### The Command-Line Interface @@ -140,7 +139,7 @@ The [pihole](https://docs.pi-hole.net/core/pihole-command/) command has all the Some notable features include: -- [Whitelisting, Blacklisting, and Regex](https://docs.pi-hole.net/core/pihole-command/#whitelisting-blacklisting-and-regex) +- [Allowlisting, Denylisting (fka Whitelisting, Blacklisting), and Regex](https://docs.pi-hole.net/core/pihole-command/#allowlisting-denylisting-and-regex) - [Debugging utility](https://docs.pi-hole.net/core/pihole-command/#debugger) - [Viewing the live log file](https://docs.pi-hole.net/core/pihole-command/#tail) - [Updating Ad Lists](https://docs.pi-hole.net/core/pihole-command/#gravity) diff --git a/advanced/Scripts/COL_TABLE b/advanced/Scripts/COL_TABLE index 4344c7b8..7d339c0e 100644 --- a/advanced/Scripts/COL_TABLE +++ b/advanced/Scripts/COL_TABLE @@ -1,10 +1,12 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 # Disable warning about unused variables + # Determine if terminal is capable of showing colors -if ([ -t 1 ] && [ $(tput colors) -ge 8 ]) || [ "${WEBCALL}" ]; then +# When COL_TABLE is sourced via gravity invoked by FTL, FORCE_COLOR is set to true +if { [ -t 1 ] && [ "$(tput colors)" -ge 8 ]; } || [ "${FORCE_COLOR}" ]; then # Bold and underline may not show up on all clients # If something MUST be emphasized, use both COL_BOLD='' - COL_ULINE='' - COL_NC='' COL_GRAY='' COL_RED='' @@ -16,8 +18,6 @@ if ([ -t 1 ] && [ $(tput colors) -ge 8 ]) || [ "${WEBCALL}" ]; then else # Provide empty variables for `set -u` COL_BOLD="" - COL_ULINE="" - COL_NC="" COL_GRAY="" COL_RED="" @@ -28,22 +28,8 @@ else COL_CYAN="" fi -# Deprecated variables -COL_WHITE="${COL_BOLD}" -COL_BLACK="${COL_NC}" -COL_LIGHT_BLUE="${COL_BLUE}" -COL_LIGHT_GREEN="${COL_GREEN}" -COL_LIGHT_CYAN="${COL_CYAN}" -COL_LIGHT_RED="${COL_RED}" -COL_URG_RED="${COL_RED}${COL_BOLD}${COL_ULINE}" -COL_LIGHT_PURPLE="${COL_PURPLE}" -COL_BROWN="${COL_YELLOW}" -COL_LIGHT_GRAY="${COL_GRAY}" -COL_DARK_GRAY="${COL_GRAY}" - TICK="[${COL_GREEN}✓${COL_NC}]" CROSS="[${COL_RED}✗${COL_NC}]" INFO="[i]" QST="[?]" -DONE="${COL_GREEN} done!${COL_NC}" OVER="\\r" diff --git a/advanced/Scripts/api.sh b/advanced/Scripts/api.sh index b8c7fe99..7589d3ff 100755 --- a/advanced/Scripts/api.sh +++ b/advanced/Scripts/api.sh @@ -1,5 +1,4 @@ #!/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) @@ -20,13 +19,20 @@ TestAPIAvailability() { + local chaos_api_list authResponse authStatus authData apiAvailable DNSport + # as we are running locally, we can get the port value from FTL directly - local chaos_api_list availabilityResponse + PI_HOLE_SCRIPT_DIR="/opt/pihole" + utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" + # shellcheck source=./advanced/Scripts/utils.sh + . "${utilsfile}" + + DNSport=$(getFTLConfigValue dns.port) # 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)" + chaos_api_list="$(dig +short -p "${DNSport}" 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 @@ -48,39 +54,50 @@ TestAPIAvailability() { 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 the API is available at this URL, include delimiter for ease in splitting payload + authResponse=$(curl --connect-timeout 2 -skS -w ">>%{http_code}" "${API_URL}auth") + + # authStatus is the response http_code, eg. 200, 401. + # Shell parameter expansion, remove everything up to and including the >> delim + authStatus=${authResponse#*>>} + # data is everything from response + # Shell parameter expansion, remove the >> delim and everything after + authData=${authResponse%>>*} # 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 - + if [ "${authStatus}" = 200 ]; then + # API is available without authentication + apiAvailable=true + needAuth=false 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#* }" + elif [ "${authStatus}" = 401 ]; then + # API is available with authentication + apiAvailable=true + needAuth=true + # Check if 2FA is required + needTOTP=$(echo "${authData}"| jq --raw-output .session.totp 2>/dev/null) + break - # 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="" + else + # API is not available at this port/protocol combination + apiAvailable=false + # 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 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}" + # if apiAvailable is false, no working API was found + if [ "${apiAvailable}" = false ]; then + echo "API not available. Please check FTL.log" echo "Exiting." exit 1 fi @@ -108,22 +125,58 @@ LoginAPI() { echo "API Authentication: Trying to use CLI password" fi - # Try to authenticate using the CLI password - Authentication "${1}" - + # If we can read the CLI password, we can skip 2FA even when it's required otherwise + needTOTP=false elif [ "${1}" = "verbose" ]; then echo "API Authentication: CLI password not available" fi + if [ -z "${password}" ]; then + # no password read from CLI file + echo "Please enter your password:" + # secretly read the password + secretRead; printf '\n' + fi + if [ "${needTOTP}" = true ]; then + # 2FA required + echo "Please enter the correct second factor." + echo "(Can be any number if you used the app password)" + read -r totp + 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" + # Try to authenticate using the supplied password (CLI file or user input) and TOTP + Authentication "${1}" + + # Try to login again until the session is valid + while [ ! "${validSession}" = true ] ; do + + # Print the error message if there is one + if [ ! "${sessionError}" = "null" ] && [ "${1}" = "verbose" ]; then + echo "Error: ${sessionError}" + fi + # Print the session message if there is one + if [ ! "${sessionMessage}" = "null" ] && [ "${1}" = "verbose" ]; then + echo "Error: ${sessionMessage}" + fi + + if [ "${1}" = "verbose" ]; then + # If we are not in verbose mode, no need to print the error message again + echo "Please enter your Pi-hole password" + else + + echo "Authentication failed. Please enter your Pi-hole password" + fi # secretly read the password secretRead; printf '\n' + if [ "${needTOTP}" = true ]; then + echo "Please enter the correct second factor:" + echo "(Can be any number if you used the app password)" + read -r totp + fi + # Try to authenticate again Authentication "${1}" done @@ -131,23 +184,34 @@ LoginAPI() { } Authentication() { - sessionResponse="$(curl -skS -X POST "${API_URL}auth" --user-agent "Pi-hole cli " --data "{\"password\":\"${password}\"}" )" + sessionResponse="$(curl --connect-timeout 2 -skS -X POST "${API_URL}auth" --user-agent "Pi-hole cli" --data "{\"password\":\"${password}\", \"totp\":${totp:-null}}" )" - 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}" + if [ -z "${sessionResponse}" ]; then + echo "No response from FTL server. Please check connectivity" + exit 1 + fi + + # obtain validity, session ID, sessionMessage and error message from + # session response, apply default values if none returned + result=$(echo "${sessionResponse}" | jq -r ' + (.session.valid // false), + (.session.sid // null), + (.session.message // null), + (.error.message // null) + ' 2>/dev/null) + + validSession=$(echo "${result}" | sed -n '1p') + SID=$(echo "${result}" | sed -n '2p') + sessionMessage=$(echo "${result}" | sed -n '3p') + sessionError=$(echo "${result}" | sed -n '4p') + + 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 - fi } LogoutAPI() { @@ -185,7 +249,7 @@ GetFTLData() { # return only the data if [ "${status}" = 200 ]; then # response OK - echo "${data}" + printf %s "${data}" else # connection lost echo "${status}" @@ -259,14 +323,23 @@ secretRead() { } apiFunc() { - local data response status status_col + local data response status status_col verbosity + + # Define if the output will be silent (default) or verbose + verbosity="silent" + if [ "$1" = "verbose" ]; then + verbosity="verbose" + shift + fi # Authenticate with the API - LoginAPI verbose - echo "" + LoginAPI "${verbosity}" - echo "Requesting: ${COL_PURPLE}GET ${COL_CYAN}${API_URL}${COL_YELLOW}$1${COL_NC}" - echo "" + if [ "${verbosity}" = "verbose" ]; then + echo "" + echo "Requesting: ${COL_PURPLE}GET ${COL_CYAN}${API_URL}${COL_YELLOW}$1${COL_NC}" + echo "" + fi # Get the data from the API response=$(GetFTLData "$1" raw) @@ -283,17 +356,21 @@ apiFunc() { else status_col="${COL_RED}" fi - echo "Status: ${status_col}${status}${COL_NC}" + + # Only print the status in verbose mode or if the status is not 200 + if [ "${verbosity}" = "verbose" ] || [ "${status}" != 200 ]; then + echo "Status: ${status_col}${status}${COL_NC}" + fi # 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}" + if [ "${verbosity}" = "verbose" ]; then + echo "Data:" fi + # Attempt to print the data with jq, if it is not valid JSON, or not installed + # then print the plain text. + echo "${data}" | jq . 2>/dev/null || echo "${data}" # Delete the session - LogoutAPI verbose + LogoutAPI "${verbosity}" } diff --git a/advanced/Scripts/database_migration/gravity-db.sh b/advanced/Scripts/database_migration/gravity-db.sh index b0982bcc..5cf4cc34 100755 --- a/advanced/Scripts/database_migration/gravity-db.sh +++ b/advanced/Scripts/database_migration/gravity-db.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# shellcheck disable=SC1090 + # Pi-hole: A black hole for Internet advertisements # (c) 2019 Pi-hole, LLC (https://pi-hole.net) @@ -13,9 +13,8 @@ readonly scriptPath="/etc/.pihole/advanced/Scripts/database_migration/gravity" upgrade_gravityDB(){ - local database piholeDir version + local database version database="${1}" - piholeDir="${2}" # Exit early if the database does not exist (e.g. in CI tests) if [[ ! -f "${database}" ]]; then @@ -151,4 +150,10 @@ upgrade_gravityDB(){ pihole-FTL sqlite3 -ni "${database}" < "${scriptPath}/18_to_19.sql" version=19 fi + if [[ "$version" == "19" ]]; then + # Update views to use new allowlist/denylist names + echo -e " ${INFO} Upgrading gravity database from version 19 to 20" + pihole-FTL sqlite3 -ni "${database}" < "${scriptPath}/19_to_20.sql" + version=20 + fi } diff --git a/advanced/Scripts/database_migration/gravity/19_to_20.sql b/advanced/Scripts/database_migration/gravity/19_to_20.sql new file mode 100644 index 00000000..1867615d --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/19_to_20.sql @@ -0,0 +1,43 @@ +.timeout 30000 + +BEGIN TRANSACTION; + +DROP VIEW vw_whitelist; +CREATE VIEW vw_allowlist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id + FROM domainlist + LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id + LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id + WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1) + AND domainlist.type = 0 + ORDER BY domainlist.id; + +DROP VIEW vw_blacklist; +CREATE VIEW vw_denylist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id + FROM domainlist + LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id + LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id + WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1) + AND domainlist.type = 1 + ORDER BY domainlist.id; + +DROP VIEW vw_regex_whitelist; +CREATE VIEW vw_regex_allowlist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id + FROM domainlist + LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id + LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id + WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1) + AND domainlist.type = 2 + ORDER BY domainlist.id; + +DROP VIEW vw_regex_blacklist; +CREATE VIEW vw_regex_denylist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id + FROM domainlist + LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id + LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id + WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1) + AND domainlist.type = 3 + ORDER BY domainlist.id; + +UPDATE info SET value = 20 WHERE property = 'version'; + +COMMIT; diff --git a/advanced/Scripts/list.sh b/advanced/Scripts/list.sh index 5c57f878..c5a80ad2 100755 --- a/advanced/Scripts/list.sh +++ b/advanced/Scripts/list.sh @@ -1,5 +1,4 @@ #!/usr/bin/env bash -# shellcheck disable=SC1090 # Pi-hole: A black hole for Internet advertisements # (c) 2017 Pi-hole, LLC (https://pi-hole.net) @@ -10,11 +9,13 @@ # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. -readonly PI_HOLE_SCRIPT_DIR="/opt/pihole" -readonly utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" +PI_HOLE_SCRIPT_DIR="/opt/pihole" +utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" +# shellcheck source="./advanced/Scripts/utils.sh" source "${utilsfile}" -readonly apifile="${PI_HOLE_SCRIPT_DIR}/api.sh" +apifile="${PI_HOLE_SCRIPT_DIR}/api.sh" +# shellcheck source="./advanced/Scripts/api.sh" source "${apifile}" # Determine database location @@ -39,6 +40,7 @@ typeId="" comment="" colfile="/opt/pihole/COL_TABLE" +# shellcheck source="./advanced/Scripts/COL_TABLE" source ${colfile} helpFunc() { diff --git a/advanced/Scripts/piholeARPTable.sh b/advanced/Scripts/piholeARPTable.sh deleted file mode 100755 index f55b1320..00000000 --- a/advanced/Scripts/piholeARPTable.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC1090 - -# Pi-hole: A black hole for Internet advertisements -# (c) 2019 Pi-hole, LLC (https://pi-hole.net) -# Network-wide ad blocking via your own hardware. -# -# ARP table interaction -# -# This file is copyright under the latest version of the EUPL. -# Please see LICENSE file for your rights under this license. - -coltable="/opt/pihole/COL_TABLE" -if [[ -f ${coltable} ]]; then - source ${coltable} -fi - -readonly PI_HOLE_SCRIPT_DIR="/opt/pihole" -utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" -source "${utilsfile}" - -# Determine database location -DBFILE=$(getFTLConfigValue "files.database") -if [ -z "$DBFILE" ]; then - DBFILE="/etc/pihole/pihole-FTL.db" -fi - -flushARP(){ - local output - if [[ "${args[1]}" != "quiet" ]]; then - echo -ne " ${INFO} Flushing network table ..." - fi - - # Stop FTL to prevent database access - if ! output=$(service pihole-FTL stop 2>&1); then - echo -e "${OVER} ${CROSS} Failed to stop FTL" - echo " Output: ${output}" - return 1 - fi - - # Truncate network_addresses table in pihole-FTL.db - # This needs to be done before we can truncate the network table due to - # foreign key constraints - if ! output=$(pihole-FTL sqlite3 -ni "${DBFILE}" "DELETE FROM network_addresses" 2>&1); then - echo -e "${OVER} ${CROSS} Failed to truncate network_addresses table" - echo " Database location: ${DBFILE}" - echo " Output: ${output}" - return 1 - fi - - # Truncate network table in pihole-FTL.db - if ! output=$(pihole-FTL sqlite3 -ni "${DBFILE}" "DELETE FROM network" 2>&1); then - echo -e "${OVER} ${CROSS} Failed to truncate network table" - echo " Database location: ${DBFILE}" - echo " Output: ${output}" - return 1 - fi - - # Flush ARP cache of the host - if ! output=$(ip -s -s neigh flush all 2>&1); then - echo -e "${OVER} ${CROSS} Failed to flush ARP cache" - echo " Output: ${output}" - return 1 - fi - - # Start FTL again - if ! output=$(service pihole-FTL restart 2>&1); then - echo -e "${OVER} ${CROSS} Failed to restart FTL" - echo " Output: ${output}" - return 1 - fi - - if [[ "${args[1]}" != "quiet" ]]; then - echo -e "${OVER} ${TICK} Flushed network table" - fi -} - -args=("$@") - -case "${args[0]}" in - "arpflush" ) flushARP;; -esac diff --git a/advanced/Scripts/piholeCheckout.sh b/advanced/Scripts/piholeCheckout.sh index 84c966df..deb07172 100755 --- a/advanced/Scripts/piholeCheckout.sh +++ b/advanced/Scripts/piholeCheckout.sh @@ -10,6 +10,7 @@ readonly PI_HOLE_FILES_DIR="/etc/.pihole" SKIP_INSTALL="true" +# shellcheck source="./automated install/basic-install.sh" source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" # webInterfaceGitUrl set in basic-install.sh @@ -25,7 +26,7 @@ source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" warning1() { echo " Please note that changing branches severely alters your Pi-hole subsystems" echo " Features that work on the master branch, may not on a development branch" - echo -e " ${COL_LIGHT_RED}This feature is NOT supported unless a Pi-hole developer explicitly asks!${COL_NC}" + echo -e " ${COL_RED}This feature is NOT supported unless a Pi-hole developer explicitly asks!${COL_NC}" read -r -p " Have you read and understood this? [y/N] " response case "${response}" in [yY][eE][sS]|[yY]) @@ -40,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 @@ -54,19 +71,19 @@ checkout() { # This is unlikely if ! is_repo "${PI_HOLE_FILES_DIR}" ; then - echo -e " ${COL_LIGHT_RED}Error: Core Pi-hole repo is missing from system!" + echo -e " ${COL_RED}Error: Core Pi-hole repo is missing from system!" echo -e " Please re-run install script from https://github.com/pi-hole/pi-hole${COL_NC}" exit 1; fi if ! is_repo "${webInterfaceDir}" ; then - echo -e " ${COL_LIGHT_RED}Error: Web Admin repo is missing from system!" + echo -e " ${COL_RED}Error: Web Admin repo is missing from system!" echo -e " Please re-run install script from https://github.com/pi-hole/pi-hole${COL_NC}" exit 1; fi if [[ -z "${1}" ]]; then - echo -e " ${COL_LIGHT_RED}Invalid option${COL_NC}" + echo -e " ${COL_RED}Invalid option${COL_NC}" echo -e " Try 'pihole checkout --help' for more information." exit 1 fi @@ -109,7 +126,7 @@ checkout() { echo -e "${OVER} ${CROSS} $str" exit 1 fi - corebranches=($(get_available_branches "${PI_HOLE_FILES_DIR}")) + mapfile -t corebranches < <(get_available_branches "${PI_HOLE_FILES_DIR}") if [[ "${corebranches[*]}" == *"master"* ]]; then echo -e "${OVER} ${TICK} $str" @@ -136,7 +153,7 @@ checkout() { echo -e "${OVER} ${CROSS} $str" exit 1 fi - webbranches=($(get_available_branches "${webInterfaceDir}")) + mapfile -t webbranches < <(get_available_branches "${webInterfaceDir}") if [[ "${webbranches[*]}" == *"master"* ]]; then echo -e "${OVER} ${TICK} $str" @@ -167,7 +184,7 @@ checkout() { # Check if requested branch is available echo -e " ${INFO} Checking for availability of branch ${COL_CYAN}${2}${COL_NC} on GitHub" - ftlbranches=( $(git ls-remote https://github.com/pi-hole/ftl | grep "refs/heads" | cut -d'/' -f3- -) ) + mapfile -t ftlbranches < <(git ls-remote https://github.com/pi-hole/ftl | grep "refs/heads" | cut -d'/' -f3- -) # If returned array is empty -> connectivity issue if [[ ${#ftlbranches[@]} -eq 0 ]]; then echo -e " ${CROSS} Unable to fetch branches from GitHub. Please check your Internet connection and try again later." @@ -209,13 +226,15 @@ checkout() { # Update local and remote versions via updatechecker /opt/pihole/updatecheck.sh else - if [ $? -eq 1 ]; then + local status + status=$? + if [ $status -eq 1 ]; then # Binary for requested branch is not available, may still be # int he process of being built or CI build job failed - printf " %b Binary for requested branch is not available, please try again later.\\n" ${CROSS} + printf " %b Binary for requested branch is not available, please try again later.\\n" "${CROSS}" printf " If the issue persists, please contact Pi-hole Support and ask them to re-generate the binary.\\n" exit 1 - elif [ $? -eq 2 ]; then + elif [ $status -eq 2 ]; then printf " %b Unable to download from ftl.pi-hole.net. Please check your Internet connection and try again later.\\n" "${CROSS}" exit 1 else @@ -232,10 +251,10 @@ 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_LIGHT_RED} Error: Unable to complete update, please contact support${COL_NC}" + echo -e " ${COL_RED} Error: Unable to complete update, please contact support${COL_NC}" exit 1 fi fi diff --git a/advanced/Scripts/piholeDebug.sh b/advanced/Scripts/piholeDebug.sh index f4226299..964fff9b 100755 --- a/advanced/Scripts/piholeDebug.sh +++ b/advanced/Scripts/piholeDebug.sh @@ -8,7 +8,6 @@ # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. -# shellcheck source=/dev/null # -e option instructs bash to immediately exit if any command [1] has a non-zero exit status # -u a reference to any variable you haven't previously defined @@ -27,6 +26,7 @@ PIHOLE_COLTABLE_FILE="${PIHOLE_SCRIPTS_DIRECTORY}/COL_TABLE" # These provide the colors we need for making the log more readable if [[ -f ${PIHOLE_COLTABLE_FILE} ]]; then +# shellcheck source=./advanced/Scripts/COL_TABLE source ${PIHOLE_COLTABLE_FILE} else COL_NC='\e[0m' # No Color @@ -41,7 +41,7 @@ else #OVER="\r\033[K" fi -# shellcheck disable=SC1091 +# shellcheck source=/dev/null . /etc/pihole/versions # Read the value of an FTL config key. The value is printed to stdout. @@ -213,7 +213,7 @@ compare_local_version_to_git_version() { local local_status local_status=$(git status -s) # echo this information out to the user in a nice format - if [ ${local_version} ]; then + if [ "${local_version}" ]; then log_write "${TICK} Version: ${local_version}" elif [ -n "${DOCKER_VERSION}" ]; then log_write "${TICK} Version: Pi-hole Docker Container ${COL_BOLD}${DOCKER_VERSION}${COL_NC}" @@ -296,91 +296,12 @@ check_component_versions() { check_ftl_version } -os_check() { - # This function gets a list of supported OS versions from a TXT record at versions.pi-hole.net - # and determines whether or not the script is running on one of those systems - local remote_os_domain valid_os valid_version detected_os detected_version cmdResult digReturnCode response - remote_os_domain=${OS_CHECK_DOMAIN_NAME:-"versions.pi-hole.net"} - - detected_os=$(grep "\bID\b" /etc/os-release | cut -d '=' -f2 | tr -d '"') - detected_version=$(grep VERSION_ID /etc/os-release | cut -d '=' -f2 | tr -d '"') - - cmdResult="$(dig -4 +short -t txt "${remote_os_domain}" @ns1.pi-hole.net 2>&1; echo $?)" - #Get the return code of the previous command (last line) - digReturnCode="${cmdResult##*$'\n'}" - - # Extract dig response - response="${cmdResult%%$'\n'*}" - - if [ "${digReturnCode}" -ne 0 ]; then - log_write "${INFO} Distro: ${detected_os^}" - log_write "${INFO} Version: ${detected_version}" - log_write "${CROSS} dig IPv4 return code: ${COL_RED}${digReturnCode}${COL_NC}" - log_write "${CROSS} dig response: ${response}" - log_write "${INFO} Retrying via IPv6" - - cmdResult="$(dig -6 +short -t txt "${remote_os_domain}" @ns1.pi-hole.net 2>&1; echo $?)" - #Get the return code of the previous command (last line) - digReturnCode="${cmdResult##*$'\n'}" - - # Extract dig response - response="${cmdResult%%$'\n'*}" - fi - # If also no success via IPv6 - if [ "${digReturnCode}" -ne 0 ]; then - log_write "${CROSS} dig IPv6 return code: ${COL_RED}${digReturnCode}${COL_NC}" - log_write "${CROSS} dig response: ${response}" - log_write "${CROSS} Error: ${COL_RED}dig command failed - Unable to check OS${COL_NC}" - else - IFS=" " read -r -a supportedOS < <(echo "${response}" | tr -d '"') - for distro_and_versions in "${supportedOS[@]}" - do - distro_part="${distro_and_versions%%=*}" - versions_part="${distro_and_versions##*=}" - - if [[ "${detected_os^^}" =~ ${distro_part^^} ]]; then - valid_os=true - IFS="," read -r -a supportedVer <<<"${versions_part}" - for version in "${supportedVer[@]}" - do - if [[ "${detected_version}" =~ $version ]]; then - valid_version=true - break - fi - done - break - fi - done - - # If it is a docker container, we can assume the OS is supported - [ -n "${DOCKER_VERSION}" ] && valid_os=true && valid_version=true - - local finalmsg - if [ "$valid_os" = true ]; then - log_write "${TICK} Distro: ${COL_GREEN}${detected_os^}${COL_NC}" - - if [ "$valid_version" = true ]; then - log_write "${TICK} Version: ${COL_GREEN}${detected_version}${COL_NC}" - finalmsg="${TICK} ${COL_GREEN}Distro and version supported${COL_NC}" - else - log_write "${CROSS} Version: ${COL_RED}${detected_version}${COL_NC}" - finalmsg="${CROSS} Error: ${COL_RED}${detected_os^} is supported but version ${detected_version} is currently unsupported ${COL_NC}(${FAQ_HARDWARE_REQUIREMENTS})${COL_NC}" - fi - else - log_write "${CROSS} Distro: ${COL_RED}${detected_os^}${COL_NC}" - finalmsg="${CROSS} Error: ${COL_RED}${detected_os^} is not a supported distro ${COL_NC}(${FAQ_HARDWARE_REQUIREMENTS})${COL_NC}" - fi - - # Print dig response and the final check result - log_write "${TICK} dig return code: ${COL_GREEN}${digReturnCode}${COL_NC}" - log_write "${INFO} dig response: ${response}" - log_write "${finalmsg}" - fi -} - diagnose_operating_system() { # error message in a variable so we can easily modify it later (or reuse it) local error_msg="Distribution unknown -- most likely you are on an unsupported platform and may run into issues." + local detected_os + local detected_version + # Display the current test that is running echo_current_diagnostic "Operating system" @@ -389,8 +310,13 @@ diagnose_operating_system() { # If there is a /etc/*release file, it's probably a supported operating system, so we can if ls /etc/*release 1> /dev/null 2>&1; then - # display the attributes to the user from the function made earlier - os_check + # display the attributes to the user + + detected_os=$(grep "\bID\b" /etc/os-release | cut -d '=' -f2 | tr -d '"') + detected_version=$(grep VERSION_ID /etc/os-release | cut -d '=' -f2 | tr -d '"') + + log_write "${INFO} Distro: ${detected_os^}" + log_write "${INFO} Version: ${detected_version}" else # If it doesn't exist, it's not a system we currently support and link to FAQ log_write "${CROSS} ${COL_RED}${error_msg}${COL_NC} (${FAQ_HARDWARE_REQUIREMENTS})" @@ -441,7 +367,7 @@ check_firewalld() { # test common required service ports local firewalld_enabled_services firewalld_enabled_services=$(firewall-cmd --list-services) - local firewalld_expected_services=("http" "dns" "dhcp" "dhcpv6") + local firewalld_expected_services=("http" "https" "dns" "dhcp" "dhcpv6" "ntp") for i in "${firewalld_expected_services[@]}"; do if [[ "${firewalld_enabled_services}" =~ ${i} ]]; then log_write "${TICK} ${COL_GREEN} Allow Service: ${i}${COL_NC}"; @@ -449,30 +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 - # check FTL custom zone port: 4711 - local firewalld_ftl_zone_ports - firewalld_ftl_zone_ports=$(firewall-cmd --zone=ftl --list-ports) - if [[ "${firewalld_ftl_zone_ports}" =~ "4711/tcp" ]]; then - log_write "${TICK} ${COL_GREEN} FTL Port 4711/tcp Detected${COL_NC}"; - else - log_write "${CROSS} ${COL_RED} FTL Port 4711/tcp 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}"; @@ -488,7 +390,9 @@ run_and_print_command() { local output output=$(${cmd} 2>&1) # If the command was successful, - if [[ $? -eq 0 ]]; then + local return_code + return_code=$? + if [[ "${return_code}" -eq 0 ]]; then # show the output log_write "${output}" else @@ -569,16 +473,25 @@ ping_gateway() { ping_ipv4_or_ipv6 "${protocol}" # Check if we are using IPv4 or IPv6 # Find the default gateways using IPv4 or IPv6 - local gateway gateway_addr gateway_iface + local gateway gateway_addr gateway_iface default_route log_write "${INFO} Default IPv${protocol} gateway(s):" - while IFS= read -r gateway; do - log_write " $(cut -d ' ' -f 3 <<< "${gateway}")%$(cut -d ' ' -f 5 <<< "${gateway}")" - done < <(ip -"${protocol}" route | grep default) + while IFS= read -r default_route; do + gateway_addr=$(jq -r '.gateway' <<< "${default_route}") + gateway_iface=$(jq -r '.dev' <<< "${default_route}") + log_write " ${gateway_addr}%${gateway_iface}" + done < <(ip -j -"${protocol}" route | jq -c '.[] | select(.dst == "default")') + + # Find the first default route + default_route=$(ip -j -"${protocol}" route show default) + if echo "$default_route" | grep 'gateway' | grep -q 'dev'; then + gateway_addr=$(echo "$default_route" | jq -r -c '.[0].gateway') + gateway_iface=$(echo "$default_route" | jq -r -c '.[0].dev') + else + log_write " Unable to determine gateway address for IPv${protocol}" + fi - gateway_addr=$(ip -"${protocol}" route | grep default | cut -d ' ' -f 3 | head -n 1) - gateway_iface=$(ip -"${protocol}" route | grep default | cut -d ' ' -f 5 | head -n 1) # If there was at least one gateway if [ -n "${gateway_addr}" ]; then # Append the interface to the gateway address if it is a link-local address @@ -664,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 @@ -743,7 +659,7 @@ dig_at() { local record_type="A" fi - # Find a random blocked url that has not been whitelisted and is not ABP style. + # Find a random blocked url that has not been allowlisted and is not ABP style. # This helps emulate queries to different domains that a user might query # It will also give extra assurance that Pi-hole is correctly resolving and blocking domains local random_url @@ -793,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" @@ -849,7 +765,7 @@ process_status(){ : else # non-Docker system - if service "${i}" status | grep -E 'is\srunning' &> /dev/null; then + if service "${i}" status | grep -q -E 'is\srunning|started'; then status_of_process="active" else status_of_process="inactive" @@ -887,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() { @@ -933,7 +834,6 @@ parse_file() { # Get the lines that are in the file(s) and store them in an array for parsing later local file_info if [[ -f "$filename" ]]; then - #shellcheck disable=SC2016 IFS=$'\r\n' command eval 'file_info=( $(cat "${filename}") )' else read -r -a file_info <<< "$filename" @@ -996,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 @@ -1063,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 @@ -1071,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 @@ -1105,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 @@ -1152,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 white-/blacklist, 2/3 = regex white-/blacklist)" "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/piholeLogFlush.sh b/advanced/Scripts/piholeLogFlush.sh index 34d96318..10b4f320 100755 --- a/advanced/Scripts/piholeLogFlush.sh +++ b/advanced/Scripts/piholeLogFlush.sh @@ -9,17 +9,14 @@ # Please see LICENSE file for your rights under this license. colfile="/opt/pihole/COL_TABLE" +# shellcheck source="./advanced/Scripts/COL_TABLE" source ${colfile} readonly PI_HOLE_SCRIPT_DIR="/opt/pihole" utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" +# shellcheck source="./advanced/Scripts/utils.sh" source "${utilsfile}" -# In case we're running at the same time as a system logrotate, use a -# separate logrotate state file to prevent stepping on each other's -# toes. -STATEFILE="/var/lib/logrotate/pihole" - # Determine database location DBFILE=$(getFTLConfigValue "files.database") if [ -z "$DBFILE" ]; then @@ -35,88 +32,45 @@ FTLFILE=$(getFTLConfigValue "files.log.ftl") if [ -z "$FTLFILE" ]; then FTLFILE="/var/log/pihole/FTL.log" fi - -if [[ "$*" == *"once"* ]]; then - # Nightly logrotation - if command -v /usr/sbin/logrotate >/dev/null; then - # Logrotate once - - if [[ "$*" != *"quiet"* ]]; then - echo -ne " ${INFO} Running logrotate ..." - fi - /usr/sbin/logrotate --force --state "${STATEFILE}" /etc/pihole/logrotate - else - # Copy pihole.log over to pihole.log.1 - # and empty out pihole.log - # Note that moving the file is not an option, as - # dnsmasq would happily continue writing into the - # moved file (it will have the same file handler) - if [[ "$*" != *"quiet"* ]]; then - echo -ne " ${INFO} Rotating ${LOGFILE} ..." - fi - cp -p "${LOGFILE}" "${LOGFILE}.1" - echo " " > "${LOGFILE}" - chmod 640 "${LOGFILE}" - if [[ "$*" != *"quiet"* ]]; then - echo -e "${OVER} ${TICK} Rotated ${LOGFILE} ..." - fi - # Copy FTL.log over to FTL.log.1 - # and empty out FTL.log - if [[ "$*" != *"quiet"* ]]; then - echo -ne " ${INFO} Rotating ${FTLFILE} ..." - fi - cp -p "${FTLFILE}" "${FTLFILE}.1" - echo " " > "${FTLFILE}" - chmod 640 "${FTLFILE}" - if [[ "$*" != *"quiet"* ]]; then - echo -e "${OVER} ${TICK} Rotated ${FTLFILE} ..." - fi - fi -else - # Manual flushing - - # Flush both pihole.log and pihole.log.1 (if existing) - if [[ "$*" != *"quiet"* ]]; then - echo -ne " ${INFO} Flushing ${LOGFILE} ..." - fi - echo " " > "${LOGFILE}" - chmod 640 "${LOGFILE}" - if [ -f "${LOGFILE}.1" ]; then - echo " " > "${LOGFILE}.1" - chmod 640 "${LOGFILE}.1" - fi - if [[ "$*" != *"quiet"* ]]; then - echo -e "${OVER} ${TICK} Flushed ${LOGFILE} ..." - fi - - # Flush both FTL.log and FTL.log.1 (if existing) - if [[ "$*" != *"quiet"* ]]; then - echo -ne " ${INFO} Flushing ${FTLFILE} ..." - fi - echo " " > "${FTLFILE}" - chmod 640 "${FTLFILE}" - if [ -f "${FTLFILE}.1" ]; then - echo " " > "${FTLFILE}.1" - chmod 640 "${FTLFILE}.1" - fi - if [[ "$*" != *"quiet"* ]]; then - echo -e "${OVER} ${TICK} Flushed ${FTLFILE} ..." - fi - - if [[ "$*" != *"quiet"* ]]; then - echo -ne " ${INFO} Flushing database, DNS resolution temporarily unavailable ..." - fi - - # Stop FTL to make sure it doesn't write to the database while we're deleting data - service pihole-FTL stop - - # Delete most recent 24 hours from FTL's database, leave even older data intact (don't wipe out all history) - deleted=$(pihole-FTL sqlite3 -ni "${DBFILE}" "DELETE FROM query_storage WHERE timestamp >= strftime('%s','now')-86400; select changes() from query_storage limit 1") - - # Restart FTL - service pihole-FTL restart - if [[ "$*" != *"quiet"* ]]; then - echo -e "${OVER} ${TICK} Deleted ${deleted} queries from long-term query database" - fi +WEBFILE=$(getFTLConfigValue "files.log.webserver") +if [ -z "$WEBFILE" ]; then + WEBFILE="/var/log/pihole/webserver.log" fi +# Helper function to handle log flushing for a single file +flush_log() { + local logfile="$1" + if [[ "$*" != *"quiet"* ]]; then + echo -ne " ${INFO} Flushing ${logfile} ..." + fi + echo " " > "${logfile}" + chmod 640 "${logfile}" + if [ -f "${logfile}.1" ]; then + echo " " > "${logfile}.1" + chmod 640 "${logfile}.1" + fi + if [[ "$*" != *"quiet"* ]]; then + echo -e "${OVER} ${TICK} Flushed ${logfile} ..." + fi +} + +# Manual flushing +flush_log "${LOGFILE}" +flush_log "${FTLFILE}" +flush_log "${WEBFILE}" + +if [[ "$*" != *"quiet"* ]]; then + echo -ne " ${INFO} Flushing database, DNS resolution temporarily unavailable ..." +fi + +# Stop FTL to make sure it doesn't write to the database while we're deleting data +service pihole-FTL stop + +# Delete most recent 24 hours from FTL's database, leave even older data intact (don't wipe out all history) +deleted=$(pihole-FTL sqlite3 -ni "${DBFILE}" "DELETE FROM query_storage WHERE timestamp >= strftime('%s','now')-86400; select changes() from query_storage limit 1") + +# Restart FTL +service pihole-FTL restart +if [[ "$*" != *"quiet"* ]]; then + echo -e "${OVER} ${TICK} Deleted ${deleted} queries from long-term query database" +fi diff --git a/advanced/Scripts/piholeLogRotate.sh b/advanced/Scripts/piholeLogRotate.sh new file mode 100755 index 00000000..b7de90ee --- /dev/null +++ b/advanced/Scripts/piholeLogRotate.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# Pi-hole: A black hole for Internet advertisements +# (c) 2025 Pi-hole, LLC (https://pi-hole.net) +# Network-wide ad blocking via your own hardware. +# +# Rotate Pi-hole's log file +# +# This file is copyright under the latest version of the EUPL. +# Please see LICENSE file for your rights under this license. + +colfile="/opt/pihole/COL_TABLE" +# shellcheck source="./advanced/Scripts/COL_TABLE" +source ${colfile} + +readonly PI_HOLE_SCRIPT_DIR="/opt/pihole" +utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" +# shellcheck source="./advanced/Scripts/utils.sh" +source "${utilsfile}" + +# In case we're running at the same time as a system logrotate, use a +# separate logrotate state file to prevent stepping on each other's +# toes. +STATEFILE="/var/lib/logrotate/pihole" + + +# Determine log file location +LOGFILE=$(getFTLConfigValue "files.log.dnsmasq") +if [ -z "$LOGFILE" ]; then + LOGFILE="/var/log/pihole/pihole.log" +fi +FTLFILE=$(getFTLConfigValue "files.log.ftl") +if [ -z "$FTLFILE" ]; then + FTLFILE="/var/log/pihole/FTL.log" +fi +WEBFILE=$(getFTLConfigValue "files.log.webserver") +if [ -z "$WEBFILE" ]; then + WEBFILE="/var/log/pihole/webserver.log" +fi + +# Helper function to handle log rotation for a single file +rotate_log() { + # This function copies x.log over to x.log.1 + # and then empties x.log + # Note that moving the file is not an option, as + # dnsmasq would happily continue writing into the + # moved file (it will have the same file handler) + local logfile="$1" + if [[ "$*" != *"quiet"* ]]; then + echo -ne " ${INFO} Rotating ${logfile} ..." + fi + cp -p "${logfile}" "${logfile}.1" + echo " " > "${logfile}" + chmod 640 "${logfile}" + if [[ "$*" != *"quiet"* ]]; then + echo -e "${OVER} ${TICK} Rotated ${logfile} ..." + fi +} + +# Nightly logrotation +if command -v /usr/sbin/logrotate >/dev/null; then + # Logrotate once + if [[ "$*" != *"quiet"* ]]; then + echo -ne " ${INFO} Running logrotate ..." + fi + mkdir -p "${STATEFILE%/*}" + /usr/sbin/logrotate --force --state "${STATEFILE}" /etc/pihole/logrotate +else + # Handle rotation for each log file + rotate_log "${LOGFILE}" + rotate_log "${FTLFILE}" + rotate_log "${WEBFILE}" +fi diff --git a/advanced/Scripts/piholeNetworkFlush.sh b/advanced/Scripts/piholeNetworkFlush.sh new file mode 100755 index 00000000..a8721476 --- /dev/null +++ b/advanced/Scripts/piholeNetworkFlush.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +# Pi-hole: A black hole for Internet advertisements +# (c) 2019 Pi-hole, LLC (https://pi-hole.net) +# Network-wide ad blocking via your own hardware. +# +# Network table flush +# +# This file is copyright under the latest version of the EUPL. +# Please see LICENSE file for your rights under this license. + +coltable="/opt/pihole/COL_TABLE" +if [[ -f ${coltable} ]]; then +# shellcheck source="./advanced/Scripts/COL_TABLE" + source ${coltable} +fi + +readonly PI_HOLE_SCRIPT_DIR="/opt/pihole" +utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" +# shellcheck source=./advanced/Scripts/utils.sh +source "${utilsfile}" + +# Source api functions +# shellcheck source="./advanced/Scripts/api.sh" +. "${PI_HOLE_SCRIPT_DIR}/api.sh" + +flushNetwork(){ + local output + + echo -ne " ${INFO} Flushing network table ..." + + local data status error + # Authenticate with FTL + LoginAPI + + # send query again + data=$(PostFTLData "action/flush/network" "" "status") + + # Separate the status from the data + status=$(printf %s "${data#"${data%???}"}") + data=$(printf %s "${data%???}") + + # If there is an .error object in the returned data, display it + local error + error=$(jq --compact-output <<< "${data}" '.error') + if [[ $error != "null" && $error != "" ]]; then + echo -e "${OVER} ${CROSS} Failed to flush the network table:" + echo -e " $(jq <<< "${data}" '.error')" + LogoutAPI + exit 1 + elif [[ "${status}" == "200" ]]; then + echo -e "${OVER} ${TICK} Flushed network table" + fi + + # Delete session + LogoutAPI +} + +flushArp(){ + # Flush ARP cache of the host + if ! output=$(ip -s -s neigh flush all 2>&1); then + echo -e "${OVER} ${CROSS} Failed to flush ARP cache" + echo " Output: ${output}" + return 1 + fi +} + +# Process all options (if present) +while [ "$#" -gt 0 ]; do + case "$1" in + "--arp" ) doARP=true ;; + esac + shift +done + +flushNetwork + +if [[ "${doARP}" == true ]]; then + echo -ne " ${INFO} Flushing ARP cache" + if flushArp; then + echo -e "${OVER} ${TICK} Flushed ARP cache" + fi +fi + diff --git a/advanced/Scripts/query.sh b/advanced/Scripts/query.sh index 3340bdd2..d0dfd6e9 100755 --- a/advanced/Scripts/query.sh +++ b/advanced/Scripts/query.sh @@ -1,9 +1,4 @@ #!/usr/bin/env sh -# shellcheck disable=SC1090 - -# Ignore warning about `local` being undefinded in POSIX -# shellcheck disable=SC3043 -# https://github.com/koalaman/shellcheck/wiki/SC3043#exceptions # Pi-hole: A black hole for Internet advertisements # (c) 2023 Pi-hole, LLC (https://pi-hole.net) @@ -22,9 +17,11 @@ domain="" # Source color table colfile="/opt/pihole/COL_TABLE" +# shellcheck source="./advanced/Scripts/COL_TABLE" . "${colfile}" # Source api functions +# shellcheck source="./advanced/Scripts/api.sh" . "${PI_HOLE_INSTALL_DIR}/api.sh" Help() { @@ -40,19 +37,16 @@ Options: } GenerateOutput() { - local data gravity_data lists_data num_gravity num_lists search_type_str - local gravity_data_csv lists_data_csv line current_domain url type color + local counts data num_gravity num_lists search_type_str + local gravity_data_csv lists_data_csv line url type color data="${1}" - # construct a new json for the list results where each object contains the domain and the related type - lists_data=$(printf %s "${data}" | jq '.search.domains | [.[] | {domain: .domain, type: .type}]') - - # construct a new json for the gravity results where each object contains the adlist URL and the related domains - gravity_data=$(printf %s "${data}" | jq '.search.gravity | group_by(.address,.type) | map({ address: (.[0].address), type: (.[0].type), domains: [.[] | .domain] })') - - # number of objects in each json - num_gravity=$(printf %s "${gravity_data}" | jq length) - num_lists=$(printf %s "${lists_data}" | jq length) + # Get count of list and gravity matches + # Use JQ to count number of entries in lists and gravity + # (output is number of list matches then number of gravity matches) + counts=$(printf %s "${data}" | jq --raw-output '(.search.domains | length), (.search.gravity | group_by(.address,.type) | length)') + num_lists=$(echo "$counts" | sed -n '1p') + num_gravity=$(echo "$counts" | sed -n '2p') if [ "${partial}" = true ]; then search_type_str="partially" @@ -65,7 +59,7 @@ GenerateOutput() { if [ "${num_lists}" -gt 0 ]; then # Convert the data to a csv, each line is a "domain,type" string # not using jq's @csv here as it quotes each value individually - lists_data_csv=$(printf %s "${lists_data}" | jq --raw-output '.[] | [.domain, .type] | join(",")') + lists_data_csv=$(printf %s "${data}" | jq --raw-output '.search.domains | map([.domain, .type] | join(",")) | join("\n")') # Generate output for each csv line, separating line in a domain and type substring at the ',' echo "${lists_data_csv}" | while read -r line; do @@ -74,11 +68,11 @@ GenerateOutput() { fi # Results from gravity - printf "%s\n\n" "Found ${num_gravity} adlists ${search_type_str} matching '${COL_BLUE}${domain}${COL_NC}'." + printf "%s\n\n" "Found ${num_gravity} lists ${search_type_str} matching '${COL_BLUE}${domain}${COL_NC}'." if [ "${num_gravity}" -gt 0 ]; then - # Convert the data to a csv, each line is a "URL,domain,domain,...." string + # Convert the data to a csv, each line is a "URL,type,domain,domain,...." string # not using jq's @csv here as it quotes each value individually - gravity_data_csv=$(printf %s "${gravity_data}" | jq --raw-output '.[] | [.address, .type, .domains[]] | join(",")') + gravity_data_csv=$(printf %s "${data}" | jq --raw-output '.search.gravity | group_by(.address,.type) | map([.[0].address, .[0].type, (.[] | .domain)] | join(",")) | join("\n")') # Generate line-by-line output for each csv line echo "${gravity_data_csv}" | while read -r line; do @@ -100,15 +94,8 @@ GenerateOutput() { # cut off type, leaving "domain,domain,...." line=${line#*,} - # print each domain and remove it from the string until nothing is left - while [ ${#line} -gt 0 ]; do - current_domain=${line%%,*} - printf ' - %s\n' "${COL_GREEN}${current_domain}${COL_NC}" - # we need to remove the current_domain and the comma in two steps because - # the last domain won't have a trailing comma and the while loop wouldn't exit - line=${line#"${current_domain}"} - line=${line#,} - done + # Replace commas with newlines and format output + echo "${line}" | sed 's/,/\n/g' | sed "s/^/ - ${COL_GREEN}/" | sed "s/$/${COL_NC}/" printf "\n\n" done fi diff --git a/advanced/Scripts/update.sh b/advanced/Scripts/update.sh index efdcd9d6..50602d2d 100755 --- a/advanced/Scripts/update.sh +++ b/advanced/Scripts/update.sh @@ -12,26 +12,31 @@ # Variables readonly ADMIN_INTERFACE_GIT_URL="https://github.com/pi-hole/web.git" -readonly ADMIN_INTERFACE_DIR="/var/www/html/admin" readonly PI_HOLE_GIT_URL="https://github.com/pi-hole/pi-hole.git" readonly PI_HOLE_FILES_DIR="/etc/.pihole" -# shellcheck disable=SC2034 SKIP_INSTALL=true # when --check-only is passed to this script, it will not perform the actual update CHECK_ONLY=false -# shellcheck disable=SC1090 +# shellcheck source="./automated install/basic-install.sh" source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" -# shellcheck disable=SC1091 +# shellcheck source=./advanced/Scripts/COL_TABLE source "/opt/pihole/COL_TABLE" +# shellcheck source="./advanced/Scripts/utils.sh" +source "${PI_HOLE_INSTALL_DIR}/utils.sh" # is_repo() sourced from basic-install.sh # make_repo() sourced from basic-install.sh # update_repo() source from basic-install.sh # getGitFiles() sourced from basic-install.sh # FTLcheckUpdate() sourced from basic-install.sh +# getFTLConfigValue() sourced from utils.sh + +# Honour configured paths for the web application. +ADMIN_INTERFACE_DIR=$(getFTLConfigValue "webserver.paths.webroot")$(getFTLConfigValue "webserver.paths.webhome") +readonly ADMIN_INTERFACE_DIR GitCheckUpdateAvail() { local directory @@ -42,7 +47,7 @@ GitCheckUpdateAvail() { # Fetch latest changes in this repo if ! git fetch --quiet origin ; then - echo -e "\\n ${COL_LIGHT_RED}Error: Unable to update local repository. Contact Pi-hole Support.${COL_NC}" + echo -e "\\n ${COL_RED}Error: Unable to update local repository. Contact Pi-hole Support.${COL_NC}" exit 1 fi @@ -71,13 +76,13 @@ GitCheckUpdateAvail() { if [[ "${#LOCAL}" == 0 ]]; then - echo -e "\\n ${COL_LIGHT_RED}Error: Local revision could not be obtained, please contact Pi-hole Support" + echo -e "\\n ${COL_RED}Error: Local revision could not be obtained, please contact Pi-hole Support" echo -e " Additional debugging output:${COL_NC}" git status exit 1 fi if [[ "${#REMOTE}" == 0 ]]; then - echo -e "\\n ${COL_LIGHT_RED}Error: Remote revision could not be obtained, please contact Pi-hole Support" + echo -e "\\n ${COL_RED}Error: Remote revision could not be obtained, please contact Pi-hole Support" echo -e " Additional debugging output:${COL_NC}" git status exit 1 @@ -142,7 +147,7 @@ Please confirm you want to start the update process." \ } main() { - local basicError="\\n ${COL_LIGHT_RED}Unable to complete update, please contact Pi-hole Support${COL_NC}" + local basicError="\\n ${COL_RED}Unable to complete update, please contact Pi-hole Support${COL_NC}" local core_update local web_update local FTL_update @@ -151,8 +156,6 @@ main() { web_update=false FTL_update=false - # Perform an OS check to ensure we're on an appropriate operating system - os_check # Install packages used by this installation script (necessary if users have removed e.g. git from their systems) package_manager_detect @@ -161,7 +164,7 @@ main() { # This is unlikely if ! is_repo "${PI_HOLE_FILES_DIR}" ; then - echo -e "\\n ${COL_LIGHT_RED}Error: Core Pi-hole repo is missing from system!" + echo -e "\\n ${COL_RED}Error: Core Pi-hole repo is missing from system!" echo -e " Please re-run install script from https://pi-hole.net${COL_NC}" exit 1; fi @@ -173,11 +176,11 @@ main() { echo -e " ${INFO} Pi-hole Core:\\t${COL_YELLOW}update available${COL_NC}" else core_update=false - echo -e " ${INFO} Pi-hole Core:\\t${COL_LIGHT_GREEN}up to date${COL_NC}" + echo -e " ${INFO} Pi-hole Core:\\t${COL_GREEN}up to date${COL_NC}" fi if ! is_repo "${ADMIN_INTERFACE_DIR}" ; then - echo -e "\\n ${COL_LIGHT_RED}Error: Web Admin repo is missing from system!" + echo -e "\\n ${COL_RED}Error: Web Admin repo is missing from system!" echo -e " Please re-run install script from https://pi-hole.net${COL_NC}" exit 1; fi @@ -187,33 +190,40 @@ main() { echo -e " ${INFO} Web Interface:\\t${COL_YELLOW}update available${COL_NC}" else web_update=false - echo -e " ${INFO} Web Interface:\\t${COL_LIGHT_GREEN}up to date${COL_NC}" + 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_LIGHT_GREEN}up to date${COL_NC}" - ;; - 2) - echo -e " ${INFO} FTL:\\t\\t${COL_LIGHT_RED}Branch is not available.${COL_NC}\\n\\t\\t\\tUse ${COL_LIGHT_GREEN}pihole checkout ftl [branchname]${COL_NC} to switch to a valid branch." - ;; - 3) - echo -e " ${INFO} FTL:\\t\\t${COL_LIGHT_RED}Something has gone wrong, cannot reach download server${COL_NC}" - exit 1 - ;; - *) - echo -e " ${INFO} FTL:\\t\\t${COL_LIGHT_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 @@ -228,7 +238,7 @@ main() { if [[ ! "${ftlBranch}" == "master" && ! "${ftlBranch}" == "development" ]]; then # Notify user that they are on a custom branch which might mean they they are lost # behind if a branch was merged to development and got abandoned - printf " %b %bWarning:%b You are using FTL from a custom branch (%s) and might be missing future releases.\\n" "${INFO}" "${COL_LIGHT_RED}" "${COL_NC}" "${ftlBranch}" + printf " %b %bWarning:%b You are using FTL from a custom branch (%s) and might be missing future releases.\\n" "${INFO}" "${COL_RED}" "${COL_NC}" "${ftlBranch}" fi if [[ "${core_update}" == false && "${web_update}" == false && "${FTL_update}" == false ]]; then @@ -258,7 +268,7 @@ main() { echo "" echo -e " ${INFO} Pi-hole Web Admin files out of date, updating local repo." getGitFiles "${ADMIN_INTERFACE_DIR}" "${ADMIN_INTERFACE_GIT_URL}" - echo -e " ${INFO} If you had made any changes in '/var/www/html/admin/', they have been stashed using 'git stash'" + echo -e " ${INFO} If you had made any changes in '${ADMIN_INTERFACE_DIR}', they have been stashed using 'git stash'" fi if [[ "${FTL_update}" == true ]]; then @@ -267,7 +277,14 @@ main() { fi if [[ "${FTL_update}" == true || "${core_update}" == true ]]; then - ${PI_HOLE_FILES_DIR}/automated\ install/basic-install.sh --reconfigure --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 @@ -287,8 +304,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 b325ee9c..23abf7a6 100755 --- a/advanced/Scripts/updatecheck.sh +++ b/advanced/Scripts/updatecheck.sh @@ -39,17 +39,21 @@ function get_remote_hash() { } # Source the utils file for addOrEditKeyValPair() -# shellcheck disable=SC1091 +# shellcheck source="./advanced/Scripts/utils.sh" . /opt/pihole/utils.sh +ADMIN_INTERFACE_DIR=$(getFTLConfigValue "webserver.paths.webroot")$(getFTLConfigValue "webserver.paths.webhome") +readonly ADMIN_INTERFACE_DIR + # Remove the below three legacy files if they exist 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 @@ -85,13 +89,13 @@ addOrEditKeyValPair "${VERSION_FILE}" "GITHUB_CORE_HASH" "${GITHUB_CORE_HASH}" # get Web versions -WEB_VERSION="$(get_local_version /var/www/html/admin)" +WEB_VERSION="$(get_local_version "${ADMIN_INTERFACE_DIR}")" addOrEditKeyValPair "${VERSION_FILE}" "WEB_VERSION" "${WEB_VERSION}" -WEB_BRANCH="$(get_local_branch /var/www/html/admin)" +WEB_BRANCH="$(get_local_branch "${ADMIN_INTERFACE_DIR}")" addOrEditKeyValPair "${VERSION_FILE}" "WEB_BRANCH" "${WEB_BRANCH}" -WEB_HASH="$(get_local_hash /var/www/html/admin)" +WEB_HASH="$(get_local_hash "${ADMIN_INTERFACE_DIR}")" addOrEditKeyValPair "${VERSION_FILE}" "WEB_HASH" "${WEB_HASH}" GITHUB_WEB_VERSION="$(get_remote_version web "${WEB_BRANCH}")" diff --git a/advanced/Scripts/utils.sh b/advanced/Scripts/utils.sh index 63d51f87..51f5db36 100755 --- a/advanced/Scripts/utils.sh +++ b/advanced/Scripts/utils.sh @@ -1,5 +1,4 @@ #!/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) @@ -31,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}" @@ -74,7 +70,9 @@ getFTLPID() { # Example getFTLConfigValue dns.piholePTR ####################### getFTLConfigValue(){ - pihole-FTL --config -q "${1}" + # Pipe to cat to avoid pihole-FTL assuming this is an interactive command + # returning colored output. + pihole-FTL --config -q "${1}" | cat } ####################### @@ -87,9 +85,17 @@ getFTLConfigValue(){ # setFTLConfigValue dns.upstreams '[ "8.8.8.8" , "8.8.4.4" ]' ####################### setFTLConfigValue(){ - pihole-FTL --config "${1}" "${2}" >/dev/null - if [[ $? -eq 5 ]]; then - echo -e " ${CROSS} ${1} set by environment variable. Please unset it to use this function" - exit 5 - fi + local err + { pihole-FTL --config "${1}" "${2}" >/dev/null; err="$?"; } || true + + case $err in + 0) ;; + 5) + # FTL returns 5 if the value was set by an environment variable and is therefore read-only + printf " %s %s set by environment variable. Please unset it to use this function\n" "${CROSS}" "${1}"; + exit 5;; + *) + printf " %s Failed to set %s. Try with sudo power\n" "${CROSS}" "${1}" + exit 1 + esac } diff --git a/advanced/Scripts/version.sh b/advanced/Scripts/version.sh index 2b0883af..e932fe63 100755 --- a/advanced/Scripts/version.sh +++ b/advanced/Scripts/version.sh @@ -8,20 +8,16 @@ # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. -# Ignore warning about `local` being undefinded in POSIX -# shellcheck disable=SC3043 -# https://github.com/koalaman/shellcheck/wiki/SC3043#exceptions - -# Source the versions file poupulated by updatechecker.sh +# Source the versions file populated by updatechecker.sh cachedVersions="/etc/pihole/versions" if [ -f ${cachedVersions} ]; then - # shellcheck disable=SC1090 + # shellcheck source=/dev/null . "$cachedVersions" else echo "Could not find /etc/pihole/versions. Running update now." pihole updatechecker - # shellcheck disable=SC1090 + # shellcheck source=/dev/null . "$cachedVersions" fi diff --git a/advanced/Templates/gravity.db.sql b/advanced/Templates/gravity.db.sql index 021f6f67..9aad6113 100644 --- a/advanced/Templates/gravity.db.sql +++ b/advanced/Templates/gravity.db.sql @@ -43,8 +43,8 @@ CREATE TABLE adlist CREATE TABLE adlist_by_group ( - adlist_id INTEGER NOT NULL REFERENCES adlist (id), - group_id INTEGER NOT NULL REFERENCES "group" (id), + adlist_id INTEGER NOT NULL REFERENCES adlist (id) ON DELETE CASCADE, + group_id INTEGER NOT NULL REFERENCES "group" (id) ON DELETE CASCADE, PRIMARY KEY (adlist_id, group_id) ); @@ -66,7 +66,7 @@ CREATE TABLE info value TEXT NOT NULL ); -INSERT INTO "info" VALUES('version','19'); +INSERT INTO "info" VALUES('version','20'); /* This is a flag to indicate if gravity was restored from a backup false = not restored, failed = restoration failed due to no backup @@ -75,8 +75,8 @@ INSERT INTO "info" VALUES('gravity_restored','false'); CREATE TABLE domainlist_by_group ( - domainlist_id INTEGER NOT NULL REFERENCES domainlist (id), - group_id INTEGER NOT NULL REFERENCES "group" (id), + domainlist_id INTEGER NOT NULL REFERENCES domainlist (id) ON DELETE CASCADE, + group_id INTEGER NOT NULL REFERENCES "group" (id) ON DELETE CASCADE, PRIMARY KEY (domainlist_id, group_id) ); @@ -91,8 +91,8 @@ CREATE TABLE client CREATE TABLE client_by_group ( - client_id INTEGER NOT NULL REFERENCES client (id), - group_id INTEGER NOT NULL REFERENCES "group" (id), + client_id INTEGER NOT NULL REFERENCES client (id) ON DELETE CASCADE, + group_id INTEGER NOT NULL REFERENCES "group" (id) ON DELETE CASCADE, PRIMARY KEY (client_id, group_id) ); @@ -111,7 +111,7 @@ CREATE TRIGGER tr_domainlist_update AFTER UPDATE ON domainlist UPDATE domainlist SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE domain = NEW.domain; END; -CREATE VIEW vw_whitelist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id +CREATE VIEW vw_allowlist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id FROM domainlist LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id @@ -119,7 +119,7 @@ CREATE VIEW vw_whitelist AS SELECT domain, domainlist.id AS id, domainlist_by_gr AND domainlist.type = 0 ORDER BY domainlist.id; -CREATE VIEW vw_blacklist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id +CREATE VIEW vw_denylist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id FROM domainlist LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id @@ -127,7 +127,7 @@ CREATE VIEW vw_blacklist AS SELECT domain, domainlist.id AS id, domainlist_by_gr AND domainlist.type = 1 ORDER BY domainlist.id; -CREATE VIEW vw_regex_whitelist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id +CREATE VIEW vw_regex_allowlist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id FROM domainlist LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id @@ -135,7 +135,7 @@ CREATE VIEW vw_regex_whitelist AS SELECT domain, domainlist.id AS id, domainlist AND domainlist.type = 2 ORDER BY domainlist.id; -CREATE VIEW vw_regex_blacklist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id +CREATE VIEW vw_regex_denylist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id FROM domainlist LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id diff --git a/advanced/Templates/pihole-FTL-poststop.sh b/advanced/Templates/pihole-FTL-poststop.sh index b5ddbc97..504e2382 100755 --- a/advanced/Templates/pihole-FTL-poststop.sh +++ b/advanced/Templates/pihole-FTL-poststop.sh @@ -3,7 +3,7 @@ # Source utils.sh for getFTLConfigValue() PI_HOLE_SCRIPT_DIR='/opt/pihole' utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" -# shellcheck disable=SC1090 +# shellcheck source="./advanced/Scripts/utils.sh" . "${utilsfile}" # Get file paths diff --git a/advanced/Templates/pihole-FTL-prestart.sh b/advanced/Templates/pihole-FTL-prestart.sh index f0bbe09a..095c0d2d 100755 --- a/advanced/Templates/pihole-FTL-prestart.sh +++ b/advanced/Templates/pihole-FTL-prestart.sh @@ -3,32 +3,40 @@ # Source utils.sh for getFTLConfigValue() PI_HOLE_SCRIPT_DIR='/opt/pihole' utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" -# shellcheck disable=SC1090 +# shellcheck source="./advanced/Scripts/utils.sh" . "${utilsfile}" # 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 -# shellcheck disable=SC2174 -mkdir -pm 0640 /var/log/pihole -chown -R pihole:pihole /etc/pihole /var/log/pihole -chmod -R 0640 /var/log/pihole -chmod -R 0660 /etc/pihole +mkdir -p /var/log/pihole +chown -R pihole:pihole /etc/pihole/ /var/log/pihole/ -# Logrotate config file need to be owned by root and must not be writable by group and others -chown root:root /etc/pihole/logrotate -chmod 0644 /etc/pihole/logrotate - -# allow all users to enter the directories -chmod 0755 /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) -# credits https://stackoverflow.com/a/11512211 -find /etc/pihole/ -type d -exec chmod 0755 {} \; +find /etc/pihole/ /var/log/pihole/ -type d -exec chmod 0755 {} + +# Set all files (except TLS-related ones) to u+rw g+r +find /etc/pihole/ /var/log/pihole/ -type f ! \( -name '*.pem' -o -name '*.crt' \) -exec chmod 0640 {} + +# Set TLS-related files to a more restrictive u+rw *only* (they may contain private keys) +find /etc/pihole/ -type f \( -name '*.pem' -o -name '*.crt' \) -exec chmod 0600 {} + + +# Logrotate config file need to be owned by root +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 "${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 new file mode 100644 index 00000000..2207273e --- /dev/null +++ b/advanced/Templates/pihole-FTL.openrc @@ -0,0 +1,40 @@ +#!/sbin/openrc-run +# shellcheck shell=sh disable=SC2034 + +: "${PI_HOLE_SCRIPT_DIR:=/opt/pihole}" + +command="/usr/bin/pihole-FTL" +command_user="pihole:pihole" +supervisor=supervise-daemon +command_args_foreground="-f" +command_background=true +pidfile="/run/${RC_SVCNAME}_openrc.pid" +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" + +depend() { + want net + provide dns +} + +checkconfig() { + $command -f test +} + +start_pre() { + sh "${PI_HOLE_SCRIPT_DIR}/pihole-FTL-prestart.sh" +} + +stop_post() { + sh "${PI_HOLE_SCRIPT_DIR}/pihole-FTL-poststop.sh" +} + +reload() { + checkconfig || return $? + ebegin "Reloading ${RC_SVCNAME}" + start-stop-daemon --signal HUP --pidfile "${pidfile}" + eend $? +} diff --git a/advanced/Templates/pihole-FTL.service b/advanced/Templates/pihole-FTL.service index 151d4f90..9cdad7a0 100644 --- a/advanced/Templates/pihole-FTL.service +++ b/advanced/Templates/pihole-FTL.service @@ -12,7 +12,7 @@ # Source utils.sh for getFTLConfigValue(), getFTLPID() PI_HOLE_SCRIPT_DIR="/opt/pihole" utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" -# shellcheck disable=SC1090 +# shellcheck source="./advanced/Scripts/utils.sh" . "${utilsfile}" @@ -57,13 +57,16 @@ start() { stop() { if is_running; then kill "${FTL_PID}" - for i in 1 2 3 4 5; do + # Give FTL 120 seconds to gracefully stop + i=1 + while [ "${i}" -le 120 ]; do if ! is_running; then break fi printf "." sleep 1 + i=$((i + 1)) done echo diff --git a/advanced/Templates/pihole-FTL.systemd b/advanced/Templates/pihole-FTL.systemd index 0a3d270e..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=10s +TimeoutStopSec=120s # Make /usr, /boot, /etc and possibly some more folders read-only... ProtectSystem=full diff --git a/advanced/Templates/pihole.cron b/advanced/Templates/pihole.cron index c62d31ab..3b71cbff 100644 --- a/advanced/Templates/pihole.cron +++ b/advanced/Templates/pihole.cron @@ -24,7 +24,7 @@ # The flush script will use logrotate if available # parameter "once": logrotate only once (default is twice) # parameter "quiet": don't print messages -00 00 * * * root PATH="$PATH:/usr/sbin:/usr/local/bin/" pihole flush once quiet +00 00 * * * root PATH="$PATH:/usr/sbin:/usr/local/bin/" pihole logrotate quiet @reboot root /usr/sbin/logrotate --state /var/lib/logrotate/pihole /etc/pihole/logrotate diff --git a/advanced/bash-completion/pihole b/advanced/bash-completion/pihole deleted file mode 100644 index 4343cf92..00000000 --- a/advanced/bash-completion/pihole +++ /dev/null @@ -1,51 +0,0 @@ -_pihole() { - local cur prev opts opts_checkout opts_debug opts_logging opts_query opts_update opts_version - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" - prev2="${COMP_WORDS[COMP_CWORD-2]}" - - case "${prev}" in - "pihole") - opts="allow allow-regex allow-wild deny checkout debug disable enable flush help logging query reconfigure regex reloaddns reloadlists status tail uninstall updateGravity updatePihole version wildcard arpflush api" - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - ;; - "allow"|"deny"|"wildcard"|"regex"|"allow-regex"|"allow-wild") - opts_lists="\not \--delmode \--quiet \--list \--help" - COMPREPLY=( $(compgen -W "${opts_lists}" -- ${cur}) ) - ;; - "checkout") - opts_checkout="core ftl web master dev" - COMPREPLY=( $(compgen -W "${opts_checkout}" -- ${cur}) ) - ;; - "debug") - opts_debug="-a" - COMPREPLY=( $(compgen -W "${opts_debug}" -- ${cur}) ) - ;; - "logging") - opts_logging="on off 'off noflush'" - COMPREPLY=( $(compgen -W "${opts_logging}" -- ${cur}) ) - ;; - "query") - opts_query="--partial --all" - COMPREPLY=( $(compgen -W "${opts_query}" -- ${cur}) ) - ;; - "updatePihole"|"-up") - opts_update="--check-only" - COMPREPLY=( $(compgen -W "${opts_update}" -- ${cur}) ) - ;; - "core"|"admin"|"ftl") - if [[ "$prev2" == "checkout" ]]; then - opts_checkout="master dev" - COMPREPLY=( $(compgen -W "${opts_checkout}" -- ${cur}) ) - else - return 1 - fi - ;; - *) - return 1 - ;; - esac - return 0 -} -complete -F _pihole pihole diff --git a/advanced/bash-completion/pihole-ftl.bash b/advanced/bash-completion/pihole-ftl.bash new file mode 100644 index 00000000..d652f007 --- /dev/null +++ b/advanced/bash-completion/pihole-ftl.bash @@ -0,0 +1,9 @@ +#!/bin/bash +# +# Bash completion script for pihole-FTL +# +# This completion script provides tab completion for pihole-FTL CLI flags and commands. +# It uses the `pihole-FTL --complete` command to generate the completion options. +_complete_FTL() { mapfile -t COMPREPLY < <(pihole-FTL --complete "${COMP_WORDS[@]}"); } + +complete -F _complete_FTL pihole-FTL diff --git a/advanced/bash-completion/pihole.bash b/advanced/bash-completion/pihole.bash new file mode 100644 index 00000000..7f6c7ab4 --- /dev/null +++ b/advanced/bash-completion/pihole.bash @@ -0,0 +1,59 @@ +#!/bin/bash +# +# Bash completion script for pihole +# +_pihole() { + local cur prev prev2 opts opts_lists opts_checkout opts_debug opts_logging opts_query opts_update opts_networkflush + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + prev2="${COMP_WORDS[COMP_CWORD-2]}" + + case "${prev}" in + "pihole") + opts="allow allow-regex allow-wild deny checkout debug disable enable flush help logging query repair regex reloaddns reloadlists setpassword status tail uninstall updateGravity updatePihole version wildcard networkflush api" + mapfile -t COMPREPLY < <(compgen -W "${opts}" -- "${cur}") + ;; + "allow"|"deny"|"wildcard"|"regex"|"allow-regex"|"allow-wild") + opts_lists="\not \--delmode \--quiet \--list \--help" + mapfile -t COMPREPLY < <(compgen -W "${opts_lists}" -- "${cur}") + ;; + "checkout") + opts_checkout="core ftl web master dev" + mapfile -t COMPREPLY < <(compgen -W "${opts_checkout}" -- "${cur}") + ;; + "debug") + opts_debug="-a" + mapfile -t COMPREPLY < <(compgen -W "${opts_debug}" -- "${cur}") + ;; + "logging") + opts_logging="on off 'off noflush'" + mapfile -t COMPREPLY < <(compgen -W "${opts_logging}" -- "${cur}") + ;; + "query") + opts_query="--partial --all" + mapfile -t COMPREPLY < <(compgen -W "${opts_query}" -- "${cur}") + ;; + "updatePihole"|"-up") + opts_update="--check-only" + mapfile -t COMPREPLY < <(compgen -W "${opts_update}" -- "${cur}") + ;; + "networkflush") + opts_networkflush="--arp" + mapfile -t COMPREPLY < <(compgen -W "${opts_networkflush}" -- "${cur}") + ;; + "core"|"web"|"ftl") + if [[ "$prev2" == "checkout" ]]; then + opts_checkout="master development" + mapfile -t COMPREPLY < <(compgen -W "${opts_checkout}" -- "${cur}") + else + return 1 + fi + ;; + *) + return 1 + ;; + esac + return 0 +} +complete -F _pihole pihole diff --git a/automated install/basic-install.sh b/automated install/basic-install.sh index 4a1df70c..b12a2714 100755 --- a/automated install/basic-install.sh +++ b/automated install/basic-install.sh @@ -1,5 +1,4 @@ #!/usr/bin/env bash -# shellcheck disable=SC1090 # Pi-hole: A black hole for Internet advertisements # (c) Pi-hole (https://pi-hole.net) @@ -49,7 +48,6 @@ Google (ECS, DNSSEC);8.8.8.8;8.8.4.4;2001:4860:4860:0:0:0:0:8888;2001:4860:4860: OpenDNS (ECS, DNSSEC);208.67.222.222;208.67.220.220;2620:119:35::35;2620:119:53::53 Level3;4.2.2.1;4.2.2.2;; Comodo;8.26.56.26;8.20.247.20;; -DNS.WATCH (DNSSEC);84.200.69.80;84.200.70.40;2001:1608:10:25:0:0:1c04:b12f;2001:1608:10:25:0:0:9249:d69b Quad9 (filtered, DNSSEC);9.9.9.9;149.112.112.112;2620:fe::fe;2620:fe::9 Quad9 (unfiltered, no DNSSEC);9.9.9.10;149.112.112.10;2620:fe::10;2620:fe::fe:10 Quad9 (filtered, ECS, DNSSEC);9.9.9.11;149.112.112.11;2620:fe::11;2620:fe::fe:11 @@ -57,6 +55,17 @@ Cloudflare (DNSSEC);1.1.1.1;1.0.0.1;2606:4700:4700::1111;2606:4700:4700::1001 EOM ) +DNS_SERVERS_IPV6_ONLY=$( + cat < Architecture: all Description: Pi-hole dependency meta package -Depends: grep,dnsutils,binutils,git,iproute2,dialog,ca-certificates,cron,curl,iputils-ping,psmisc,sudo,unzip,libcap2-bin,dns-root-data,libcap2,netcat-openbsd,procps,jq,lshw,bash-completion +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 ) @@ -122,12 +130,12 @@ EOM PIHOLE_META_PACKAGE_CONTROL_RPM=$( cat </dev/null 2>&1 } -os_check_dig(){ - local protocol="$1" - local domain="$2" - local nameserver="$3" - local response - - response="$(dig -"${protocol}" +short -t txt "${domain}" "${nameserver}" 2>&1 - echo $? - )" - echo "${response}" -} - -os_check_dig_response(){ - # Checks the reply from the dig command to determine if it's a valid response - local digReply="$1" - local response - - # Dig returned 0 (success), so get the actual response, and loop through it to determine if the detected variables above are valid - response="${digReply%%$'\n'*}" - # If the value of ${response} is a single 0, then this is the return code, not an actual response. - if [ "${response}" == 0 ]; then - echo false - else - echo true - fi -} - -os_check() { - if [ "$PIHOLE_SKIP_OS_CHECK" != true ]; then - # This function gets a list of supported OS versions from a TXT record at versions.pi-hole.net - # and determines whether or not the script is running on one of those systems - local remote_os_domain valid_os valid_version valid_response detected_os detected_version display_warning cmdResult digReturnCode response - local piholeNameserver="@ns1.pi-hole.net" - remote_os_domain=${OS_CHECK_DOMAIN_NAME:-"versions.pi-hole.net"} - - detected_os=$(grep '^ID=' /etc/os-release | cut -d '=' -f2 | tr -d '"') - detected_version=$(grep VERSION_ID /etc/os-release | cut -d '=' -f2 | tr -d '"') - - # Test via IPv4 and hardcoded nameserver ns1.pi-hole.net - cmdResult=$(os_check_dig 4 "${remote_os_domain}" "${piholeNameserver}") - - # Gets the return code of the previous command (last line) - digReturnCode="${cmdResult##*$'\n'}" - - if [ ! "${digReturnCode}" == "0" ]; then - valid_response=false - else - valid_response=$(os_check_dig_response cmdResult) - fi - - # Try again via IPv6 and hardcoded nameserver ns1.pi-hole.net - if [ "$valid_response" = false ]; then - unset valid_response - unset cmdResult - unset digReturnCode - - cmdResult=$(os_check_dig 6 "${remote_os_domain}" "${piholeNameserver}") - # Gets the return code of the previous command (last line) - digReturnCode="${cmdResult##*$'\n'}" - - if [ ! "${digReturnCode}" == "0" ]; then - valid_response=false - else - valid_response=$(os_check_dig_response cmdResult) - fi - fi - - # Try again without hardcoded nameserver - if [ "$valid_response" = false ]; then - unset valid_response - unset cmdResult - unset digReturnCode - - cmdResult=$(os_check_dig 4 "${remote_os_domain}") - # Gets the return code of the previous command (last line) - digReturnCode="${cmdResult##*$'\n'}" - - if [ ! "${digReturnCode}" == "0" ]; then - valid_response=false - else - valid_response=$(os_check_dig_response cmdResult) - fi - fi - - if [ "$valid_response" = false ]; then - unset valid_response - unset cmdResult - unset digReturnCode - - cmdResult=$(os_check_dig 6 "${remote_os_domain}") - # Gets the return code of the previous command (last line) - digReturnCode="${cmdResult##*$'\n'}" - - if [ ! "${digReturnCode}" == "0" ]; then - valid_response=false - else - valid_response=$(os_check_dig_response cmdResult) - fi - fi - - if [ "$valid_response" = true ]; then - response="${cmdResult%%$'\n'*}" - IFS=" " read -r -a supportedOS < <(echo "${response}" | tr -d '"') - for distro_and_versions in "${supportedOS[@]}"; do - distro_part="${distro_and_versions%%=*}" - versions_part="${distro_and_versions##*=}" - - # If the distro part is a (case-insensitive) substring of the computer OS - if [[ "${detected_os^^}" =~ ${distro_part^^} ]]; then - valid_os=true - IFS="," read -r -a supportedVer <<<"${versions_part}" - for version in "${supportedVer[@]}"; do - if [[ "${detected_version}" =~ $version ]]; then - valid_version=true - break - fi - done - break - fi - done - fi - - if [ "$valid_os" = true ] && [ "$valid_version" = true ] && [ "$valid_response" = true ]; then - display_warning=false - fi - - if [ "$display_warning" != false ]; then - if [ "$valid_response" = false ]; then - - if [ "${digReturnCode}" -eq 0 ]; then - errStr="dig succeeded, but response was blank. Please contact support" - else - errStr="dig failed with return code ${digReturnCode}" - fi - printf " %b %bRetrieval of supported OS list failed. %s. %b\\n" "${CROSS}" "${COL_LIGHT_RED}" "${errStr}" "${COL_NC}" - printf " %bUnable to determine if the detected OS (%s %s) is supported%b\\n" "${COL_LIGHT_RED}" "${detected_os^}" "${detected_version}" "${COL_NC}" - printf " Possible causes for this include:\\n" - printf " - Firewall blocking DNS lookups from Pi-hole device to ns1.pi-hole.net\\n" - printf " - DNS resolution issues of the host system\\n" - printf " - Other internet connectivity issues\\n" - else - printf " %b %bUnsupported OS detected: %s %s%b\\n" "${CROSS}" "${COL_LIGHT_RED}" "${detected_os^}" "${detected_version}" "${COL_NC}" - printf " If you are seeing this message and you do have a supported OS, please contact support.\\n" - fi - printf "\\n" - printf " %bhttps://docs.pi-hole.net/main/prerequisites/#supported-operating-systems%b\\n" "${COL_LIGHT_GREEN}" "${COL_NC}" - printf "\\n" - printf " If you wish to attempt to continue anyway, you can try one of the following commands to skip this check:\\n" - printf "\\n" - printf " e.g: If you are seeing this message on a fresh install, you can run:\\n" - printf " %bcurl -sSL https://install.pi-hole.net | sudo PIHOLE_SKIP_OS_CHECK=true bash%b\\n" "${COL_LIGHT_GREEN}" "${COL_NC}" - printf "\\n" - printf " If you are seeing this message after having run pihole -up:\\n" - printf " %bsudo PIHOLE_SKIP_OS_CHECK=true pihole -r%b\\n" "${COL_LIGHT_GREEN}" "${COL_NC}" - printf " (In this case, your previous run of pihole -up will have already updated the local repository)\\n" - printf "\\n" - printf " It is possible that the installation will still fail at this stage due to an unsupported configuration.\\n" - printf " If that is the case, you can feel free to ask the community on Discourse with the %bCommunity Help%b category:\\n" "${COL_LIGHT_RED}" "${COL_NC}" - printf " %bhttps://discourse.pi-hole.net/c/bugs-problems-issues/community-help/%b\\n" "${COL_LIGHT_GREEN}" "${COL_NC}" - printf "\\n" - exit 1 - - else - printf " %b %bSupported OS detected%b\\n" "${TICK}" "${COL_LIGHT_GREEN}" "${COL_NC}" - fi - else - printf " %b %bPIHOLE_SKIP_OS_CHECK env variable set to true - installer will continue%b\\n" "${INFO}" "${COL_LIGHT_GREEN}" "${COL_NC}" +check_fresh_install() { + # in case of an update (can be a v5 -> v6 or v6 -> v6 update) or repair + if [[ -f "${PI_HOLE_V6_CONFIG}" ]] || [[ -f "/etc/pihole/setupVars.conf" ]]; then + fresh_install=false fi } @@ -405,8 +298,6 @@ package_manager_detect() { PKG_COUNT="${PKG_MANAGER} -s -o Debug::NoLocking=true upgrade | grep -c ^Inst || true" # The command we will use to remove packages (used in the uninstaller) PKG_REMOVE="${PKG_MANAGER} -y remove --purge" - # Update package cache - update_package_cache || exit 1 # If apt-get is not found, check for rpm. elif is_command rpm; then @@ -423,7 +314,15 @@ package_manager_detect() { PKG_COUNT="${PKG_MANAGER} check-update | grep -E '(.i686|.x86|.noarch|.arm|.src|.riscv64)' | wc -l || true" # The command we will use to remove packages (used in the uninstaller) PKG_REMOVE="${PKG_MANAGER} remove -y" - # If neither apt-get or yum/dnf package managers were found + + # If neither apt-get or yum/dnf package managers were found, check for apk. + elif is_command apk; then + PKG_MANAGER="apk" + UPDATE_PKG_CACHE="${PKG_MANAGER} update" + PKG_INSTALL="${PKG_MANAGER} add" + PKG_COUNT="${PKG_MANAGER} list --upgradable -q | wc -l" + PKG_REMOVE="${PKG_MANAGER} del" + else # we cannot install required packages printf " %b No supported package manager found\\n" "${CROSS}" @@ -434,13 +333,20 @@ package_manager_detect() { build_dependency_package(){ # This function will build a package that contains all the dependencies needed for Pi-hole + if is_command apk ; then + local str="APK based system detected. Dependencies will be installed using a virtual package named pihole-meta" + printf " %b %s...\\n" "${INFO}" "${str}" + return 0 + fi # remove any leftover build directory that may exist rm -rf /tmp/pihole-meta_* # Create a fresh build directory with random name + # Busybox Compat: `mktemp` long flags unsupported + # -d flag is short form of --directory local tempdir - tempdir="$(mktemp --directory /tmp/pihole-meta_XXXXX)" + tempdir="$(mktemp -d /tmp/pihole-meta_XXXXX)" chmod 0755 "${tempdir}" if is_command apt-get; then @@ -466,7 +372,7 @@ build_dependency_package(){ printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" else printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" - printf "%b Error: Building pihole-meta.deb failed. %b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + printf "%b Error: Building pihole-meta.deb failed. %b\\n" "${COL_RED}" "${COL_NC}" return 1 fi @@ -499,7 +405,7 @@ build_dependency_package(){ printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" else printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" - printf "%b Error: Building pihole-meta.rpm failed. %b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + printf "%b Error: Building pihole-meta.rpm failed. %b\\n" "${COL_RED}" "${COL_NC}" return 1 fi @@ -641,7 +547,7 @@ getGitFiles() { printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" # Update the repo, returning an error message on failure update_repo "${directory}" || { - printf "\\n %b: Could not update local repository. Contact support.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + printf "\\n %b: Could not update local repository. Contact support.%b\\n" "${COL_RED}" "${COL_NC}" exit 1 } # If it's not a .git repo, @@ -650,7 +556,7 @@ getGitFiles() { printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" # Attempt to make the repository, showing an error on failure make_repo "${directory}" "${remoteRepo}" || { - printf "\\n %bError: Could not update local repository. Contact support.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + printf "\\n %bError: Could not update local repository. Contact support.%b\\n" "${COL_RED}" "${COL_NC}" exit 1 } fi @@ -690,7 +596,11 @@ find_IPv4_information() { local IPv4bare # Find IP used to route to outside world by checking the route to Google's public DNS server - route=$(ip route get 8.8.8.8) + if ! route="$(ip route get 8.8.8.8 2> /dev/null)"; then + printf " %b No IPv4 route was detected.\n" "${INFO}" + IPV4_ADDRESS="" + return + fi # Get just the interface IPv4 address # shellcheck disable=SC2059,SC2086 @@ -706,10 +616,35 @@ find_IPv4_information() { IPV4_ADDRESS=$(ip -oneline -family inet address show | grep "${IPv4bare}/" | awk '{print $4}' | awk 'END {print}') } +confirm_ipv6_only() { + # Confirm from user before IPv6 only install + + dialog --no-shadow --output-fd 1 \ +--no-button "Exit" --yes-button "Install IPv6 ONLY" \ +--yesno "\\n\\nWARNING - no valid IPv4 route detected.\\n\\n\ +This may be due to a temporary connectivity issue,\\n\ +or you may be installing on an IPv6 only system.\\n\\n\ +Do you wish to continue with an IPv6-only installation?\\n\\n" \ + "${r}" "${c}" && result=0 || result="$?" + + case "${result}" in + "${DIALOG_CANCEL}" | "${DIALOG_ESC}") + printf " %b Installer exited at IPv6 only message.\\n" "${INFO}" + exit 1 + ;; + esac + + DNS_SERVERS="${DNS_SERVERS_IPV6_ONLY}" + printf " %b Proceeding with IPv6 only installation.\\n" "${INFO}" +} + # Get available interfaces that are UP get_available_interfaces() { # There may be more than one so it's all stored in a variable - availableInterfaces=$(ip --oneline link show up | grep -v "lo" | awk '{print $2}' | cut -d':' -f1 | cut -d'@' -f1) + # The ip command list all interfaces that are in the up state + # The awk command filters out any interfaces that have the LOOPBACK flag set + # while using the characters ": " or "@" as a field separator for awk + availableInterfaces=$(ip --oneline link show up | awk -F ': |@' '!/<.*LOOPBACK.*>/ {print $2}') } # A function for displaying the dialogs the user sees when first running the installer @@ -771,8 +706,8 @@ chooseInterface() { # All further interfaces are deselected status="OFF" done - # shellcheck disable=SC2086 # 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)" \ @@ -798,9 +733,9 @@ testIPv6() { # first will contain fda2 (ULA) printf -v first "%s" "${1%%:*}" # value1 will contain 253 which is the decimal value corresponding to 0xFD - value1=$(((0x$first) / 256)) + value1=$(((0x${first}) / 256)) # value2 will contain 162 which is the decimal value corresponding to 0xA2 - value2=$(((0x$first) % 256)) + value2=$(((0x${first}) % 256)) # the ULA test is testing for fc00::/7 according to RFC 4193 if (((value1 & 254) == 252)); then # echoing result to calling function as return value @@ -825,7 +760,7 @@ find_IPv6_information() { # For each address in the array above, determine the type of IPv6 address it is for i in "${IPV6_ADDRESSES[@]}"; do # Check if it's ULA, GUA, or LL by using the function created earlier - result=$(testIPv6 "$i") + result=$(testIPv6 "${i}") # If it's a ULA address, use it and store it as a global variable [[ "${result}" == "ULA" ]] && ULA_ADDRESS="${i%/*}" # If it's a GUA address, use it and store it as a global variable @@ -860,6 +795,9 @@ collect_v4andv6_information() { printf " %b IPv4 address: %s\\n" "${INFO}" "${IPV4_ADDRESS}" find_IPv6_information printf " %b IPv6 address: %s\\n" "${INFO}" "${IPV6_ADDRESS}" + if [ "${IPV4_ADDRESS}" == "" ] && [ "${IPV6_ADDRESS}" != "" ]; then + confirm_ipv6_only + fi } # Check an IP address to see if it is a valid one @@ -877,7 +815,7 @@ valid_ip() { local regex="^${ipv4elem}\\.${ipv4elem}\\.${ipv4elem}\\.${ipv4elem}${portelem}$" # Evaluate the regex, and return the result - [[ $ip =~ ${regex} ]] + [[ ${ip} =~ ${regex} ]] stat=$? return "${stat}" @@ -912,7 +850,7 @@ setDNS() { DNSChooseOptions=() local DNSServerCount=0 # Save the old Internal Field Separator in a variable, - OIFS=$IFS + OIFS=${IFS} # and set the new one to newline IFS=$'\n' # Put the DNS Servers into an array @@ -937,7 +875,7 @@ setDNS() { result=$? case ${result} in "${DIALOG_CANCEL}" | "${DIALOG_ESC}") - printf " %b Cancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + printf " %b Cancel was selected, exiting installer%b\\n" "${COL_RED}" "${COL_NC}" exit 1 ;; esac @@ -974,13 +912,13 @@ If you want to specify a port other than 53, separate it with a hash.\ result=$? case ${result} in "${DIALOG_CANCEL}" | "${DIALOG_ESC}") - printf " %b Cancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + printf " %b Cancel was selected, exiting installer%b\\n" "${COL_RED}" "${COL_NC}" exit 1 ;; esac # Clean user input and replace whitespace with comma. - piholeDNS=$(sed 's/[, \t]\+/,/g' <<<"${piholeDNS}") + piholeDNS="${piholeDNS//[[:blank:]]/,}" # Separate the user input into the two DNS values (separated by a comma) printf -v PIHOLE_DNS_1 "%s" "${piholeDNS%%,*}" @@ -1028,7 +966,7 @@ If you want to specify a port other than 53, separate it with a hash.\ DNSSettingsCorrect=False ;; "${DIALOG_ESC}") - printf " %b Escape pressed, exiting installer at DNS Settings%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + printf " %b Escape pressed, exiting installer at DNS Settings%b\\n" "${COL_RED}" "${COL_NC}" exit 1 ;; esac @@ -1036,7 +974,7 @@ If you want to specify a port other than 53, separate it with a hash.\ done else # Save the old Internal Field Separator in a variable, - OIFS=$IFS + OIFS=${IFS} # and set the new one to newline IFS=$'\n' for DNSServer in ${DNS_SERVERS}; do @@ -1079,7 +1017,7 @@ setLogging() { ;; "${DIALOG_ESC}") # User pressed - printf " %b Escape pressed, exiting installer at Query Logging choice.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + printf " %b Escape pressed, exiting installer at Query Logging choice.%b\\n" "${COL_RED}" "${COL_NC}" exit 1 ;; esac @@ -1104,7 +1042,7 @@ setPrivacyLevel() { printf " %b Using privacy level: %s\\n" "${INFO}" "${PRIVACY_LEVEL}" ;; "${DIALOG_CANCEL}" | "${DIALOG_ESC}") - printf " %b Cancelled privacy level selection.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + printf " %b Cancelled privacy level selection.%b\\n" "${COL_RED}" "${COL_NC}" exit 1 ;; esac @@ -1112,7 +1050,7 @@ setPrivacyLevel() { # A function to display a list of example blocklists for users to select chooseBlocklists() { - # Back up any existing adlist file, on the off chance that it exists. Useful in case of a reconfigure. + # Back up any existing adlist file, on the off chance that it exists. if [[ -f "${adlistFile}" ]]; then mv "${adlistFile}" "${adlistFile}.old" fi @@ -1138,7 +1076,7 @@ chooseBlocklists() { ;; "${DIALOG_ESC}") # User pressed - printf " %b Escape pressed, exiting installer at blocklist choice.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + printf " %b Escape pressed, exiting installer at blocklist choice.%b\\n" "${COL_RED}" "${COL_NC}" exit 1 ;; esac @@ -1258,13 +1196,14 @@ installScripts() { install -o "${USER}" -Dm755 -t "${PI_HOLE_INSTALL_DIR}" ./automated\ install/uninstall.sh install -o "${USER}" -Dm755 -t "${PI_HOLE_INSTALL_DIR}" ./advanced/Scripts/COL_TABLE install -o "${USER}" -Dm755 -t "${PI_HOLE_BIN_DIR}" pihole - install -Dm644 ./advanced/bash-completion/pihole /etc/bash_completion.d/pihole + install -Dm644 ./advanced/bash-completion/pihole.bash /etc/bash_completion.d/pihole + install -Dm644 ./advanced/bash-completion/pihole-ftl.bash /etc/bash_completion.d/pihole-FTL printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" else # Otherwise, show an error and exit printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" - printf "\\t\\t%bError: Local repo %s not found, exiting installer%b\\n" "${COL_LIGHT_RED}" "${PI_HOLE_LOCAL_REPO}" "${COL_NC}" + printf "\\t\\t%bError: Local repo %s not found, exiting installer%b\\n" "${COL_RED}" "${PI_HOLE_LOCAL_REPO}" "${COL_NC}" return 1 fi } @@ -1279,14 +1218,13 @@ installConfigs() { # Install empty custom.list file if it does not exist if [[ ! -r "${PI_HOLE_CONFIG_DIR}/hosts/custom.list" ]]; then if ! install -D -T -o pihole -g pihole -m 660 /dev/null "${PI_HOLE_CONFIG_DIR}/hosts/custom.list" &>/dev/null; then - printf " %b Error: Unable to initialize configuration file %s/custom.list\\n" "${COL_LIGHT_RED}" "${PI_HOLE_CONFIG_DIR}/hosts" + printf " %b Error: Unable to initialize configuration file %s/custom.list\\n" "${COL_RED}" "${PI_HOLE_CONFIG_DIR}/hosts" return 1 fi fi # Install pihole-FTL systemd or init.d service, based on whether systemd is the init system or not - # Follow debhelper logic, which checks for /run/systemd/system to derive whether systemd is the init system - if [[ -d '/run/systemd/system' ]]; then + if ps -p 1 -o comm= | grep -q systemd; then install -T -m 0644 "${PI_HOLE_LOCAL_REPO}/advanced/Templates/pihole-FTL.systemd" '/etc/systemd/system/pihole-FTL.service' # Remove init.d service if present @@ -1298,7 +1236,12 @@ installConfigs() { # Load final service systemctl daemon-reload else - install -T -m 0755 "${PI_HOLE_LOCAL_REPO}/advanced/Templates/pihole-FTL.service" '/etc/init.d/pihole-FTL' + local INIT="service" + if is_command openrc; then + INIT="openrc" + fi + + install -T -m 0755 "${PI_HOLE_LOCAL_REPO}/advanced/Templates/pihole-FTL.${INIT}" '/etc/init.d/pihole-FTL' fi install -T -m 0755 "${PI_HOLE_LOCAL_REPO}/advanced/Templates/pihole-FTL-prestart.sh" "${PI_HOLE_INSTALL_DIR}/pihole-FTL-prestart.sh" install -T -m 0755 "${PI_HOLE_LOCAL_REPO}/advanced/Templates/pihole-FTL-poststop.sh" "${PI_HOLE_INSTALL_DIR}/pihole-FTL-poststop.sh" @@ -1322,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 @@ -1355,9 +1294,9 @@ stop_service() { local str="Stopping ${1} service" printf " %b %s..." "${INFO}" "${str}" if is_command systemctl; then - systemctl stop "${1}" &>/dev/null || true + systemctl -q stop "${1}" || true else - service "${1}" stop &>/dev/null || true + service "${1}" stop >/dev/null || true fi printf "%b %b %s...\\n" "${OVER}" "${TICK}" "${str}" } @@ -1370,10 +1309,10 @@ restart_service() { # If systemctl exists, if is_command systemctl; then # use that to restart the service - systemctl restart "${1}" &>/dev/null + systemctl -q restart "${1}" else # Otherwise, fall back to the service command - service "${1}" restart &>/dev/null + service "${1}" restart >/dev/null fi printf "%b %b %s...\\n" "${OVER}" "${TICK}" "${str}" } @@ -1386,10 +1325,12 @@ enable_service() { # If systemctl exists, if is_command systemctl; then # use that to enable the service - systemctl enable "${1}" &>/dev/null + systemctl -q enable "${1}" + elif is_command openrc; then + rc-update add "${1}" "${2:-default}" &> /dev/null else # Otherwise, use update-rc.d to accomplish this - update-rc.d "${1}" defaults &>/dev/null + update-rc.d "${1}" defaults >/dev/null fi printf "%b %b %s...\\n" "${OVER}" "${TICK}" "${str}" } @@ -1402,10 +1343,13 @@ disable_service() { # If systemctl exists, if is_command systemctl; then # use that to disable the service - systemctl disable "${1}" &>/dev/null + systemctl -q disable --now "${1}" + elif is_command openrc; then + rc-update del "${1}" "${2:-default}" &> /dev/null + else # Otherwise, use update-rc.d to accomplish this - update-rc.d "${1}" disable &>/dev/null + update-rc.d "${1}" disable >/dev/null fi printf "%b %b %s...\\n" "${OVER}" "${TICK}" "${str}" } @@ -1414,7 +1358,9 @@ check_service_active() { # If systemctl exists, if is_command systemctl; then # use that to check the status of the service - systemctl is-enabled "${1}" &>/dev/null + systemctl -q is-enabled "${1}" 2>/dev/null + elif is_command openrc; then + rc-status default boot | grep -q "${1}" else # Otherwise, fall back to service command service "${1}" status &>/dev/null @@ -1459,7 +1405,7 @@ update_package_cache() { UPDATE_PKG_CACHE="apt update" fi printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" - printf " %b Error: Unable to update package cache. Please try \"%s\"%b\\n" "${COL_LIGHT_RED}" "sudo ${UPDATE_PKG_CACHE}" "${COL_NC}" + printf " %b Error: Unable to update package cache. Please try \"%s\"%b\\n" "${COL_RED}" "sudo ${UPDATE_PKG_CACHE}" "${COL_NC}" return 1 fi } @@ -1473,16 +1419,11 @@ notify_package_updates_available() { # Store the list of packages in a variable updatesToInstall=$(eval "${PKG_COUNT}") - if [[ -d "/lib/modules/$(uname -r)" ]]; then - if [[ "${updatesToInstall}" -eq 0 ]]; then - printf "%b %b %s... up to date!\\n\\n" "${OVER}" "${TICK}" "${str}" - else - printf "%b %b %s... %s updates available\\n" "${OVER}" "${TICK}" "${str}" "${updatesToInstall}" - printf " %b %bIt is recommended to update your OS after installing the Pi-hole!%b\\n\\n" "${INFO}" "${COL_LIGHT_GREEN}" "${COL_NC}" - fi + if [[ "${updatesToInstall}" -eq 0 ]]; then + printf "%b %b %s... up to date!\\n\\n" "${OVER}" "${TICK}" "${str}" else - printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" - printf " Kernel update detected. If the install fails, please reboot and try again\\n" + printf "%b %b %s... %s updates available\\n" "${OVER}" "${TICK}" "${str}" "${updatesToInstall}" + printf " %b %bIt is recommended to update your OS after installing the Pi-hole!%b\\n\\n" "${INFO}" "${COL_GREEN}" "${COL_NC}" fi } @@ -1499,11 +1440,11 @@ install_dependent_packages() { rm /tmp/pihole-meta.deb else printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" - printf " %b Error: Unable to install Pi-hole dependency package.\\n" "${COL_LIGHT_RED}" + printf " %b Error: Unable to install Pi-hole dependency package.\\n" "${COL_RED}" return 1 fi else - printf " %b Error: Unable to find Pi-hole dependency package.\\n" "${COL_LIGHT_RED}" + printf " %b Error: Unable to find Pi-hole dependency package.\\n" "${COL_RED}" return 1 fi # Install Fedora/CentOS packages @@ -1514,15 +1455,34 @@ install_dependent_packages() { rm /tmp/pihole-meta.rpm else printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" - printf " %b Error: Unable to install Pi-hole dependency package.\\n" "${COL_LIGHT_RED}" + printf " %b Error: Unable to install Pi-hole dependency package.\\n" "${COL_RED}" return 1 fi else - printf " %b Error: Unable to find Pi-hole dependency package.\\n" "${COL_LIGHT_RED}" + printf " %b Error: Unable to find Pi-hole dependency package.\\n" "${COL_RED}" return 1 fi + # Install Alpine packages + elif is_command apk; then + local repo_str="Ensuring alpine 'community' repo is enabled." + printf "%b %b %s" "${OVER}" "${INFO}" "${repo_str}" - # If neither apt-get or yum/dnf package managers were found + local pattern='^\s*#(.*/community/?)\s*$' + sed -Ei "s:${pattern}:\1:" /etc/apk/repositories + if grep -Eq "${pattern}" /etc/apk/repositories; then + # Repo still commented out = Failure + printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${repo_str}" + else + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${repo_str}" + fi + printf " %b %s..." "${INFO}" "${str}" + if { ${PKG_INSTALL} -q -t "pihole-meta=${PIHOLE_META_VERSION_APK}" "${PIHOLE_META_DEPS_APK[@]}" &>/dev/null; }; then + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" + else + printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" + printf " %b Error: Unable to install Pi-hole dependency package.\\n" "${COL_RED}" + return 1 + fi else # we cannot install the dependency package printf " %b No supported package manager found\\n" "${CROSS}" @@ -1547,6 +1507,15 @@ installCron() { # Randomize update checker time sed -i "s/59 17/$((1 + RANDOM % 58)) $((12 + RANDOM % 8))/" /etc/cron.d/pihole printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" + + # Switch off of busybox cron on alpine + if is_command openrc; then + printf " %b Switching from busybox crond to cronie...\\n" "${INFO}" + stop_service crond + disable_service crond + enable_service cronie + restart_service cronie + fi } # Gravity is a very important script as it aggregates all of the domains into a single HOSTS formatted list, @@ -1596,7 +1565,7 @@ create_pihole_user() { # then create and add her to the pihole group local str="Creating user 'pihole'" printf "%b %b %s..." "${OVER}" "${INFO}" "${str}" - if useradd -r --no-user-group -g pihole -s /usr/sbin/nologin pihole; then + if useradd -r --no-user-group -g pihole -s "$(command -v nologin)" pihole; then printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" else printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" @@ -1611,7 +1580,7 @@ create_pihole_user() { # create and add pihole user to the pihole group local str="Creating user 'pihole'" printf "%b %b %s..." "${OVER}" "${INFO}" "${str}" - if useradd -r --no-user-group -g pihole -s /usr/sbin/nologin pihole; then + if useradd -r --no-user-group -g pihole -s "$(command -v nologin)" pihole; then printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" else printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" @@ -1747,101 +1716,25 @@ checkSelinux() { if [[ "${SELINUX_ENFORCING}" -eq 1 ]] && [[ -z "${PIHOLE_SELINUX}" ]]; then printf " Pi-hole does not provide an SELinux policy as the required changes modify the security of your system.\\n" printf " Please refer to https://wiki.centos.org/HowTos/SELinux if SELinux is required for your deployment.\\n" - printf " This check can be skipped by setting the environment variable %bPIHOLE_SELINUX%b to %btrue%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" "${COL_LIGHT_RED}" "${COL_NC}" + printf " This check can be skipped by setting the environment variable %bPIHOLE_SELINUX%b to %btrue%b\\n" "${COL_RED}" "${COL_NC}" "${COL_RED}" "${COL_NC}" printf " e.g: export PIHOLE_SELINUX=true\\n" printf " By setting this variable to true you acknowledge there may be issues with Pi-hole during or after the install\\n" - printf "\\n %bSELinux Enforcing detected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + printf "\\n %bSELinux Enforcing detected, exiting installer%b\\n" "${COL_RED}" "${COL_NC}" exit 1 elif [[ "${SELINUX_ENFORCING}" -eq 1 ]] && [[ -n "${PIHOLE_SELINUX}" ]]; then - printf " %b %bSELinux Enforcing detected%b. PIHOLE_SELINUX env variable set - installer will continue\\n" "${INFO}" "${COL_LIGHT_RED}" "${COL_NC}" + printf " %b %bSELinux Enforcing detected%b. PIHOLE_SELINUX env variable set - installer will continue\\n" "${INFO}" "${COL_RED}" "${COL_NC}" fi } -# Installation complete message with instructions for the user -displayFinalMessage() { - # TODO: COME BACK TO THIS, WHAT IS GOING ON? - # If the number of arguments is > 0, - if [[ "${#1}" -gt 0 ]]; then - # set the password to the first argument. - pwstring="$1" - elif [[ -n $(pihole-FTL --config webserver.api.pwhash) ]]; then - # Else if the password exists from previous setup, we'll load it later - pwstring="unchanged" - else - # Else, inform the user that there is no set password. - pwstring="NOT SET" - fi - - # Store a message in a variable and display it - additional="View the web interface at http://pi.hole/admin:${WEBPORT} or http://${IPV4_ADDRESS%/*}:${WEBPORT}/admin\\n\\nYour Admin Webpage login password is ${pwstring}" - - # Final completion message to user - dialog --no-shadow --keep-tite \ - --title "Installation Complete!" \ - --msgbox "Configure your devices to use the Pi-hole as their DNS server using:\ -\\n\\nIPv4: ${IPV4_ADDRESS%/*}\ -\\nIPv6: ${IPV6_ADDRESS:-"Not Configured"}\ -\\nIf you have not done so already, the above IP should be set to static.\ -\\n${additional}" "${r}" "${c}" -} - -update_dialogs() { - # If pihole -r "reconfigure" option was selected, - if [[ "${reconfigure}" = true ]]; then - # set some variables that will be used - opt1a="Repair" - opt1b="This will retain existing settings" - strAdd="You will remain on the same version" - else - # Otherwise, set some variables with different values - opt1a="Update" - opt1b="This will retain existing settings." - strAdd="You will be updated to the latest version." - fi - opt2a="Reconfigure" - opt2b="Resets Pi-hole and allows re-selecting settings." - - # Display the information to the user - UpdateCmd=$(dialog --no-shadow --keep-tite --output-fd 1 \ - --cancel-label Exit \ - --title "Existing Install Detected!" \ - --menu "\\n\\nWe have detected an existing install.\ -\\n\\nPlease choose from the following options:\ -\\n($strAdd)" \ - "${r}" "${c}" 2 \ - "${opt1a}" "${opt1b}" \ - "${opt2a}" "${opt2b}") || result=$? - - case ${result} in - "${DIALOG_CANCEL}" | "${DIALOG_ESC}") - printf " %b Cancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" - exit 1 - ;; - esac - - # Set the variable based on if the user chooses - case ${UpdateCmd} in - # repair, or - "${opt1a}") - printf " %b %s option selected\\n" "${INFO}" "${opt1a}" - useUpdateVars=true - ;; - # reconfigure, - "${opt2a}") - printf " %b %s option selected\\n" "${INFO}" "${opt2a}" - useUpdateVars=false - ;; - esac -} - check_download_exists() { # Check if the download exists and we can reach the server - local status=$(curl --head --silent "https://ftl.pi-hole.net/${1}" | head -n 1) + local status + status=$(curl --head --silent "https://ftl.pi-hole.net/${1}" | head -n 1) # Check the status code - if grep -q "200" <<<"$status"; then + if grep -q "200" <<<"${status}"; then return 0 - elif grep -q "404" <<<"$status"; then + elif grep -q "404" <<<"${status}"; then return 1 fi @@ -1874,7 +1767,7 @@ get_available_branches() { # Get reachable remote branches, but store STDERR as STDOUT variable output=$({ git ls-remote --heads --quiet | cut -d'/' -f3- -; } 2>&1) # echo status for calling function to capture - echo "$output" + echo "${output}" return } @@ -1907,9 +1800,9 @@ checkout_pull_branch() { oldbranch="$(git symbolic-ref HEAD)" str="Switching to branch: '${branch}' from '${oldbranch}'" - printf " %b %s" "${INFO}" "$str" + printf " %b %s" "${INFO}" "${str}" git checkout "${branch}" --quiet || return 1 - printf "%b %b %s\\n" "${OVER}" "${TICK}" "$str" + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" # Data in the repositories is public anyway so we can make it readable by everyone (+r to keep executable permission if already set by git) chmod -R a+rX "${directory}" @@ -1920,34 +1813,40 @@ checkout_pull_branch() { return 0 } -clone_or_update_repos() { - # If the user wants to reconfigure, - if [[ "${reconfigure}" == true ]]; then - printf " %b Performing reconfiguration, skipping download of local repos\\n" "${INFO}" +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} || { - printf " %b Unable to reset %s, exiting installer%b\\n" "${COL_LIGHT_RED}" "${PI_HOLE_LOCAL_REPO}" "${COL_NC}" + printf " %b Unable to reset %s, exiting installer%b\\n" "${COL_RED}" "${PI_HOLE_LOCAL_REPO}" "${COL_NC}" exit 1 } # Reset the Web repo resetRepo ${webInterfaceDir} || { - printf " %b Unable to reset %s, exiting installer%b\\n" "${COL_LIGHT_RED}" "${webInterfaceDir}" "${COL_NC}" + printf " %b Unable to reset %s, exiting installer%b\\n" "${COL_RED}" "${webInterfaceDir}" "${COL_NC}" exit 1 } - # Otherwise, a repair is happening + # Otherwise, a fresh installation is happening else # so get git files for Core getGitFiles ${PI_HOLE_LOCAL_REPO} ${piholeGitUrl} || { - printf " %b Unable to clone %s into %s, unable to continue%b\\n" "${COL_LIGHT_RED}" "${piholeGitUrl}" "${PI_HOLE_LOCAL_REPO}" "${COL_NC}" + printf " %b Unable to clone %s into %s, unable to continue%b\\n" "${COL_RED}" "${piholeGitUrl}" "${PI_HOLE_LOCAL_REPO}" "${COL_NC}" exit 1 } # get the Web git files getGitFiles ${webInterfaceDir} ${webInterfaceGitUrl} || { - printf " %b Unable to clone %s into ${webInterfaceDir}, exiting installer%b\\n" "${COL_LIGHT_RED}" "${webInterfaceGitUrl}" "${COL_NC}" + printf " %b Unable to clone %s into ${webInterfaceDir}, exiting installer%b\\n" "${COL_RED}" "${webInterfaceGitUrl}" "${COL_NC}" exit 1 } fi @@ -1955,7 +1854,6 @@ clone_or_update_repos() { # Download FTL binary to random temp directory and install FTL binary # Disable directive for SC2120 a value _can_ be passed to this function, but it is passed from an external script that sources this one -# shellcheck disable=SC2120 FTLinstall() { # Local, named variables local str="Downloading and Installing FTL" @@ -1998,8 +1896,12 @@ FTLinstall() { # Before stopping FTL, we download the macvendor database curl -sSL "https://ftl.pi-hole.net/macvendor.db" -o "${PI_HOLE_CONFIG_DIR}/macvendor.db" || true - # Stop pihole-FTL service if available - stop_service pihole-FTL &>/dev/null + + # If the binary already exists in /usr/bin, then we need to stop the service + # If the binary does not exist (fresh installs), then we can skip this step. + if [[ -f /usr/bin/pihole-FTL ]]; then + stop_service pihole-FTL >/dev/null + fi # Install the new version with the correct permissions install -T -m 0755 "${binary}" /usr/bin/pihole-FTL @@ -2024,7 +1926,7 @@ FTLinstall() { return 1 } printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" - printf " %b Error: Download of %s/%s failed (checksum error)%b\\n" "${COL_LIGHT_RED}" "${url}" "${binary}" "${COL_NC}" + printf " %b Error: Download of %s/%s failed (checksum error)%b\\n" "${COL_RED}" "${url}" "${binary}" "${COL_NC}" # Remove temp dir remove_dir "${tempdir}" @@ -2038,7 +1940,7 @@ FTLinstall() { } printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" # The URL could not be found - printf " %b Error: URL %s/%s not found%b\\n" "${COL_LIGHT_RED}" "${url}" "${binary}" "${COL_NC}" + printf " %b Error: URL %s/%s not found%b\\n" "${COL_RED}" "${url}" "${binary}" "${COL_NC}" # Remove temp dir remove_dir "${tempdir}" @@ -2062,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 @@ -2113,9 +2024,9 @@ get_binary_name() { l_binary="pihole-FTL-riscv64" else # Something else - we try to use 32bit executable and warn the user - if [[ ! "${machine}" == "i686" ]]; then + if [[ "${machine}" != "i686" ]]; then printf "%b %b %s...\\n" "${OVER}" "${CROSS}" "${str}" - printf " %b %bNot able to detect architecture (unknown: %s), trying x86 (32bit) executable%b\\n" "${INFO}" "${COL_LIGHT_RED}" "${machine}" "${COL_NC}" + printf " %b %bNot able to detect architecture (unknown: %s), trying x86 (32bit) executable%b\\n" "${INFO}" "${COL_RED}" "${machine}" "${COL_NC}" printf " %b Contact Pi-hole Support if you experience issues (e.g: FTL not running)\\n" "${INFO}" else printf "%b %b Detected 32bit (i686) architecture\\n" "${OVER}" "${TICK}" @@ -2147,19 +2058,19 @@ FTLcheckUpdate() { local remoteSha1 local localSha1 - if [[ ! "${ftlBranch}" == "master" ]]; then + if [[ "${ftlBranch}" != "master" ]]; then # This is not the master branch local path path="${ftlBranch}/${binary}" # Check whether or not the binary for this FTL branch actually exists. If not, then there is no update! - # shellcheck disable=SC1090 - if ! check_download_exists "$path"; then - if [ $? -eq 1 ]; then + local status + if ! check_download_exists "${path}"; then + status=$? + if [ "${status}" -eq 1 ]; then printf " %b Branch \"%s\" is not available.\\n" "${INFO}" "${ftlBranch}" - printf " %b Use %bpihole checkout ftl [branchname]%b to switch to a valid branch.\\n" "${INFO}" "${COL_LIGHT_GREEN}" "${COL_NC}" - return 2 - elif [ $? -eq 2 ]; then + printf " %b Use %bpihole checkout ftl [branchname]%b to switch to a valid branch.\\n" "${INFO}" "${COL_GREEN}" "${COL_NC}" + elif [ "${status}" -eq 2 ]; then printf " %b Unable to download from ftl.pi-hole.net. Please check your Internet connection and try again later.\\n" "${CROSS}" return 3 else @@ -2187,12 +2098,14 @@ FTLcheckUpdate() { # same as the remote one local FTLversion FTLversion=$(/usr/bin/pihole-FTL tag) - local FTLlatesttag # Get the latest version from the GitHub API - if ! FTLlatesttag=$(curl -sI https://github.com/pi-hole/FTL/releases/latest | grep --color=never -i Location: | awk -F / '{print $NF}' | tr -d '[:cntrl:]'); then + local FTLlatesttag + FTLlatesttag=$(curl -s https://api.github.com/repos/pi-hole/FTL/releases/latest | jq -sRr 'fromjson? | .tag_name | values') + + if [ -z "${FTLlatesttag}" ]; then # There was an issue while retrieving the latest version - printf " %b Failed to retrieve latest FTL release metadata" "${CROSS}" + printf " %b Failed to retrieve latest FTL release metadata\\n" "${CROSS}" return 3 fi @@ -2210,6 +2123,7 @@ FTLcheckUpdate() { # Continue further down... fi else + # FTL not installed, then download return 0 fi fi @@ -2241,6 +2155,11 @@ FTLdetect() { if FTLcheckUpdate "${1}"; then FTLinstall "${1}" || return 1 + else + case $? in + 1) :;; # FTL is up-to-date + *) exit 1;; # 404 (2), other HTTP or curl error (3), unknown (4) + esac fi } @@ -2249,11 +2168,11 @@ make_temporary_log() { TEMPLOG=$(mktemp /tmp/pihole_temp.XXXXXX) # Open handle 3 for templog # https://stackoverflow.com/questions/18460186/writing-outputs-to-log-file-and-console - exec 3>"$TEMPLOG" + exec 3>"${TEMPLOG}" # Delete templog, but allow for addressing via file handle # This lets us write to the log without having a temporary file on the drive, which # is meant to be a security measure so there is not a lingering file on the drive during the install process - rm "$TEMPLOG" + rm "${TEMPLOG}" } copy_to_install_log() { @@ -2339,8 +2258,6 @@ migrate_dnsmasq_configs() { # Print a blank line for separation printf "\\n" - - v5_to_v6_update=true } # Check for availability of either the "service" or "systemctl" commands @@ -2370,7 +2287,7 @@ main() { else # Otherwise, they do not have enough privileges, so let the user know printf " %b %s\\n" "${INFO}" "${str}" - printf " %b %bScript called with non-root privileges%b\\n" "${INFO}" "${COL_LIGHT_RED}" "${COL_NC}" + printf " %b %bScript called with non-root privileges%b\\n" "${INFO}" "${COL_RED}" "${COL_NC}" printf " The Pi-hole requires elevated privileges to install and run\\n" printf " Please check the installer for any concerns regarding this requirement\\n" printf " Make sure to download this script from a trusted source\\n\\n" @@ -2394,7 +2311,7 @@ main() { # Otherwise, tell the user they need to run the script as root, and bail printf "%b %b Sudo utility check\\n" "${OVER}" "${CROSS}" printf " %b Sudo is needed for the Web Interface to run pihole commands\\n\\n" "${INFO}" - printf " %b %bPlease re-run this installer as root${COL_NC}\\n" "${INFO}" "${COL_LIGHT_RED}" + printf " %b %bPlease re-run this installer as root${COL_NC}\\n" "${INFO}" "${COL_RED}" exit 1 fi fi @@ -2405,9 +2322,17 @@ main() { # Check for availability of either the "service" or "systemctl" commands check_service_command + # Check if this is a fresh install or an update/repair + check_fresh_install + # Check for supported package managers so that we may install dependencies package_manager_detect + # Update package cache only on apt based systems + if is_command apt-get; then + update_package_cache || exit 1 + fi + # Notify user of package availability notify_package_updates_available @@ -2417,35 +2342,24 @@ main() { # Install Pi-hole dependencies install_dependent_packages - # Check that the installed OS is officially supported - display warning if not - os_check # 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 - - # in case of an update (can be a v5 -> v6 or v6 -> v6 update) - if [[ -f "${PI_HOLE_V6_CONFIG}" ]] || [[ -f "/etc/pihole/setupVars.conf" ]]; then - # if it's running unattended, - if [[ "${runUnattended}" == true ]]; then - printf " %b Performing unattended setup, no dialogs will be displayed\\n" "${INFO}" - # Use the setup variables - useUpdateVars=true - # also disable debconf-apt-progress dialogs - export DEBIAN_FRONTEND="noninteractive" - else - # If running attended, show the available options (repair/reconfigure) - update_dialogs + # 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 [[ "${useUpdateVars}" == false ]]; then + if [[ "${fresh_install}" == true ]]; then # Display welcome dialogs welcomeDialogs # Create directory for Pi-hole storage (/etc/pihole/) @@ -2468,21 +2382,25 @@ main() { # Setup adlist file if not exists installDefaultBlocklists fi - # Download or update the scripts by updating the appropriate git repos - clone_or_update_repos - + # Download or reset the appropriate git repos depending on the 'repair' flag + clone_or_reset_repos # Create the pihole user 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 @@ -2490,7 +2408,7 @@ main() { # /opt/pihole/utils.sh should be installed by installScripts now, so we can use it if [ -f "${PI_HOLE_INSTALL_DIR}/utils.sh" ]; then - # shellcheck disable=SC1091 + # shellcheck source="./advanced/Scripts/utils.sh" source "${PI_HOLE_INSTALL_DIR}/utils.sh" else printf " %b Failure: /opt/pihole/utils.sh does not exist .\\n" "${CROSS}" @@ -2500,10 +2418,17 @@ main() { # Copy the temp log file into final log location for storage copy_to_install_log - # Migrate existing install to v6.0 migrate_dnsmasq_configs + # Cleanup old v5 sudoers file if it exists + sudoers_file="/etc/sudoers.d/pihole" + if [[ -f "${sudoers_file}" ]]; then + # only remove the file if it contains the Pi-hole header + if grep -q "Pi-hole: A black hole for Internet advertisements" "${sudoers_file}"; then + rm -f "${sudoers_file}" + fi + fi # Check for and disable systemd-resolved-DNSStubListener before reloading resolved # DNSStubListener needs to remain in place for installer to download needed files, @@ -2511,10 +2436,13 @@ main() { # but before starting or resttarting the ftl service disable_resolved_stublistener - # Check if gravity database needs to be upgraded. If so, do it without rebuilding - # gravity altogether. This may be a very long running task needlessly blocking - # the update process. - /opt/pihole/gravity.sh --upgrade + if [[ "${fresh_install}" == false ]]; then + # Check if gravity database needs to be upgraded. If so, do it without rebuilding + # gravity altogether. This may be a very long running task needlessly blocking + # the update process. + # Only do this on updates, not on fresh installs as the database does not exit yet + /opt/pihole/gravity.sh --upgrade + fi printf " %b Restarting services...\\n" "${INFO}" # Start services @@ -2527,28 +2455,29 @@ main() { restart_service pihole-FTL - # Add password to web UI if there is none - pw="" - # If this is a fresh installation and no password is set, - if [[ ${v5_to_v6_update} = false && -z $(getFTLConfigValue webserver.api.pwhash) ]]; then - # generate a random password - pw=$(tr -dc _A-Z-a-z-0-9 0)); then - # display the password - printf " %b Web Interface password: %b%s%b\\n" "${INFO}" "${COL_LIGHT_GREEN}" "${pw}" "${COL_NC}" - printf " %b This can be changed using 'pihole setpassword'\\n\\n" "${INFO}" - fi + if [[ "${fresh_install}" == true ]]; then - if [[ "${useUpdateVars}" == false ]]; then # Get the Web interface port, return only the first port and strip all non-numeric characters WEBPORT=$(getFTLConfigValue webserver.port|cut -d, -f1 | tr -cd '0-9') - # Display the completion dialog - displayFinalMessage "${pw}" - - # If the Web interface was installed, - printf " %b View the web interface at http://pi.hole:${WEBPORT}/admin or http://%s/admin\\n\\n" "${INFO}" "${IPV4_ADDRESS%/*}:${WEBPORT}" + # If this is a fresh install, we will set a random password. + # Users can change this password after installation if they wish + pw=$(tr -dc _A-Z-a-z-0-9 /dev/null # Explain to the user how to use Pi-hole as their DNS server - printf " %b You may now configure your devices to use the Pi-hole as their DNS server\\n" "${INFO}" + printf "\\n %b You may now configure your devices to use the Pi-hole as their DNS server\\n" "${INFO}" [[ -n "${IPV4_ADDRESS%/*}" ]] && printf " %b Pi-hole DNS (IPv4): %s\\n" "${INFO}" "${IPV4_ADDRESS%/*}" [[ -n "${IPV6_ADDRESS}" ]] && printf " %b Pi-hole DNS (IPv6): %s\\n" "${INFO}" "${IPV6_ADDRESS}" printf " %b If you have not done so already, the above IP should be set to static.\\n" "${INFO}" + + printf " %b View the web interface at http://pi.hole:${WEBPORT}/admin or http://%s/admin\\n\\n" "${INFO}" "${IPV4_ADDRESS%/*}:${WEBPORT}" + printf " %b Web Interface password: %b%s%b\\n" "${INFO}" "${COL_GREEN}" "${pw}" "${COL_NC}" + printf " %b This can be changed using 'pihole setpassword'\\n\\n" "${INFO}" + printf " %b To allow your user to use all CLI functions without authentication, refer to\\n" "${INFO}" + printf " our documentation at: https://docs.pi-hole.net/main/post-install/\\n\\n" + + # Final dialog message to the user + dialog --no-shadow --keep-tite \ + --title "Installation Complete!" \ + --msgbox "Configure your devices to use the Pi-hole as their DNS server using:\ +\\n\\nIPv4: ${IPV4_ADDRESS%/*}\ +\\nIPv6: ${IPV6_ADDRESS:-"Not Configured"}\ +\\nIf you have not done so already, the above IP should be set to static.\ +\\nView the web interface at http://pi.hole:${WEBPORT}/admin or http://${IPV4_ADDRESS%/*}:${WEBPORT}/admin\\n\\nYour Admin Webpage login password is ${pw}\ +\\n +\\n +\\nTo allow your user to use all CLI functions without authentication,\ +\\nrefer to https://docs.pi-hole.net/main/post-install/" "${r}" "${c}" + INSTALL_TYPE="Installation" else INSTALL_TYPE="Update" @@ -2586,7 +2528,7 @@ main() { # Display where the log file is printf "\\n %b The install log is located at: %s\\n" "${INFO}" "${installLogLoc}" - printf " %b %b%s complete! %b\\n" "${TICK}" "${COL_LIGHT_GREEN}" "${INSTALL_TYPE}" "${COL_NC}" + printf " %b %b%s complete! %b\\n" "${TICK}" "${COL_GREEN}" "${INSTALL_TYPE}" "${COL_NC}" if [[ "${INSTALL_TYPE}" == "Update" ]]; then printf "\\n" diff --git a/automated install/uninstall.sh b/automated install/uninstall.sh index 39c13037..6d307de4 100755 --- a/automated install/uninstall.sh +++ b/automated install/uninstall.sh @@ -8,13 +8,17 @@ # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. +# shellcheck source="./advanced/Scripts/COL_TABLE" source "/opt/pihole/COL_TABLE" +# shellcheck source="./advanced/Scripts/utils.sh" +source "/opt/pihole/utils.sh" +# getFTLConfigValue() from utils.sh while true; do - read -rp " ${QST} Are you sure you would like to remove ${COL_WHITE}Pi-hole${COL_NC}? [y/N] " answer + read -rp " ${QST} Are you sure you would like to remove ${COL_BOLD}Pi-hole${COL_NC}? [y/N] " answer case ${answer} in [Yy]* ) break;; - * ) echo -e "${OVER} ${COL_LIGHT_GREEN}Uninstall has been canceled${COL_NC}"; exit 0;; + * ) echo -e "${OVER} ${COL_GREEN}Uninstall has been canceled${COL_NC}"; exit 0;; esac done @@ -23,141 +27,200 @@ str="Root user check" if [[ ${EUID} -eq 0 ]]; then echo -e " ${TICK} ${str}" else - # Check if sudo is actually installed - # If it isn't, exit because the uninstall can not complete - if [ -x "$(command -v sudo)" ]; then - export SUDO="sudo" - else - echo -e " ${CROSS} ${str} - Script called with non-root privileges - The Pi-hole requires elevated privileges to uninstall" - exit 1 - fi + echo -e " ${CROSS} ${str} + Script called with non-root privileges + The Pi-hole requires elevated privileges to uninstall" + exit 1 fi -readonly PI_HOLE_FILES_DIR="/etc/.pihole" +# Get paths for admin interface, log files and database files, +# to allow deletion where user has specified a non-default location +ADMIN_INTERFACE_DIR=$(getFTLConfigValue "webserver.paths.webroot")$(getFTLConfigValue "webserver.paths.webhome") +FTL_LOG=$(getFTLConfigValue "files.log.ftl") +DNSMASQ_LOG=$(getFTLConfigValue "files.log.dnsmasq") +WEBSERVER_LOG=$(getFTLConfigValue "files.log.webserver") +PIHOLE_DB=$(getFTLConfigValue "files.database") +GRAVITY_DB=$(getFTLConfigValue "files.gravity") +MACVENDOR_DB=$(getFTLConfigValue "files.macvendor") + +PI_HOLE_LOCAL_REPO="/etc/.pihole" +# Setting SKIP_INSTALL="true" to source the installer functions without running them SKIP_INSTALL="true" -source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" - -# package_manager_detect() sourced from basic-install.sh -package_manager_detect - +# shellcheck source="./automated install/basic-install.sh" +source "${PI_HOLE_LOCAL_REPO}/automated install/basic-install.sh" +# Functions and Variables sources from basic-install: +# package_manager_detect(), disable_service(), stop_service(), +# restart service() and is_command() +# PI_HOLE_CONFIG_DIR PI_HOLE_INSTALL_DIR PI_HOLE_LOCAL_REPO removeMetaPackage() { # Purge Pi-hole meta package echo "" echo -ne " ${INFO} Removing Pi-hole meta package..."; - eval "${SUDO}" "${PKG_REMOVE}" "pihole-meta" &> /dev/null; + eval "${PKG_REMOVE}" "pihole-meta" &> /dev/null; echo -e "${OVER} ${INFO} Removed Pi-hole meta package"; - } -removePiholeFiles() { - # Only web directories/files that are created by Pi-hole should be removed +removeWebInterface() { + # Remove the web interface of Pi-hole echo -ne " ${INFO} Removing Web Interface..." - ${SUDO} rm -rf /var/www/html/admin &> /dev/null - - - # If the web directory is empty after removing these files, then the parent html directory can be removed. - if [ -d "/var/www/html" ]; then - if [[ ! "$(ls -A /var/www/html)" ]]; then - ${SUDO} rm -rf /var/www/html &> /dev/null - fi - fi + rm -rf "${ADMIN_INTERFACE_DIR:-/var/www/html/admin/}" &> /dev/null echo -e "${OVER} ${TICK} Removed Web Interface" +} - # Attempt to preserve backwards compatibility with older versions - # to guarantee no additional changes were made to /etc/crontab after - # the installation of pihole, /etc/crontab.pihole should be permanently - # preserved. - if [[ -f /etc/crontab.orig ]]; then - ${SUDO} mv /etc/crontab /etc/crontab.pihole - ${SUDO} mv /etc/crontab.orig /etc/crontab - ${SUDO} service cron restart - echo -e " ${TICK} Restored the default system cron" - fi +removeFTL() { + # Remove FTL and stop any running FTL service + if is_command "pihole-FTL"; then + # service stop & disable from basic_install.sh + stop_service pihole-FTL + disable_service pihole-FTL - # Attempt to preserve backwards compatibility with older versions - if [[ -f /etc/cron.d/pihole ]];then - ${SUDO} rm -f /etc/cron.d/pihole &> /dev/null - echo -e " ${TICK} Removed /etc/cron.d/pihole" - fi - - ${SUDO} rm -rf /var/log/*pihole* &> /dev/null - ${SUDO} rm -rf /var/log/pihole/*pihole* &> /dev/null - ${SUDO} rm -rf /etc/pihole/ &> /dev/null - ${SUDO} rm -rf /etc/.pihole/ &> /dev/null - ${SUDO} rm -rf /opt/pihole/ &> /dev/null - ${SUDO} rm -f /usr/local/bin/pihole &> /dev/null - ${SUDO} rm -f /etc/bash_completion.d/pihole &> /dev/null - ${SUDO} rm -f /etc/sudoers.d/pihole &> /dev/null - echo -e " ${TICK} Removed config files" - - # Restore Resolved - if [[ -e /etc/systemd/resolved.conf.orig ]] || [[ -e /etc/systemd/resolved.conf.d/90-pi-hole-disable-stub-listener.conf ]]; then - ${SUDO} cp -p /etc/systemd/resolved.conf.orig /etc/systemd/resolved.conf &> /dev/null || true - ${SUDO} rm -f /etc/systemd/resolved.conf.d/90-pi-hole-disable-stub-listener.conf - systemctl reload-or-restart systemd-resolved - fi - - # Remove FTL - if command -v pihole-FTL &> /dev/null; then echo -ne " ${INFO} Removing pihole-FTL..." - if [[ -x "$(command -v systemctl)" ]]; then - systemctl stop pihole-FTL - else - service pihole-FTL stop - fi - ${SUDO} rm -f /etc/systemd/system/pihole-FTL.service + rm -f /etc/systemd/system/pihole-FTL.service &> /dev/null if [[ -d '/etc/systemd/system/pihole-FTL.service.d' ]]; then read -rp " ${QST} FTL service override directory /etc/systemd/system/pihole-FTL.service.d detected. Do you wish to remove this from your system? [y/N] " answer case $answer in [yY]*) echo -ne " ${INFO} Removing /etc/systemd/system/pihole-FTL.service.d..." - ${SUDO} rm -R /etc/systemd/system/pihole-FTL.service.d + rm -R /etc/systemd/system/pihole-FTL.service.d &> /dev/null echo -e "${OVER} ${INFO} Removed /etc/systemd/system/pihole-FTL.service.d" ;; *) echo -e " ${INFO} Leaving /etc/systemd/system/pihole-FTL.service.d in place.";; esac fi - ${SUDO} rm -f /etc/init.d/pihole-FTL - ${SUDO} rm -f /usr/bin/pihole-FTL + rm -f /etc/init.d/pihole-FTL &> /dev/null + rm -f /usr/bin/pihole-FTL &> /dev/null echo -e "${OVER} ${TICK} Removed pihole-FTL" + + # Force systemd reload after service files are removed + if is_command "systemctl"; then + echo -ne " ${INFO} Restarting systemd..." + systemctl daemon-reload + echo -e "${OVER} ${TICK} Restarted systemd..." + fi + fi +} + +removeCronFiles() { + # Attempt to preserve backwards compatibility with older versions + # to guarantee no additional changes were made to /etc/crontab after + # the installation of pihole, /etc/crontab.pihole should be permanently + # preserved. + if [[ -f /etc/crontab.orig ]]; then + mv /etc/crontab /etc/crontab.pihole + mv /etc/crontab.orig /etc/crontab + restart_service cron + echo -e " ${TICK} Restored the default system cron" + echo -e " ${INFO} A backup of the most recent crontab is saved at /etc/crontab.pihole" fi - # If the pihole manpage exists, then delete and rebuild man-db + # Attempt to preserve backwards compatibility with older versions + if [[ -f /etc/cron.d/pihole ]];then + rm -f /etc/cron.d/pihole &> /dev/null + echo -e " ${TICK} Removed /etc/cron.d/pihole" + fi +} + +removePiholeFiles() { + # Remove databases (including user specified non-default paths) + rm -f "${PIHOLE_DB:-/etc/pihole/pihole-FTL.db}" &> /dev/null + rm -f "${GRAVITY_DB:-/etc/pihole/gravity.db}" &> /dev/null + rm -f "${MACVENDOR_DB:-/etc/pihole/macvendor.db}" &> /dev/null + + # Remove pihole config, repo and local files + rm -rf "${PI_HOLE_CONFIG_DIR:-/etc/pihole}" &> /dev/null + rm -rf "${PI_HOLE_LOCAL_REPO:-/etc/.pihole}" &> /dev/null + rm -rf "${PI_HOLE_INSTALL_DIR:-/opt/pihole}" &> /dev/null + + # Remove log files (including user specified non-default paths) + # and rotated logs + # Explicitly escape spaces, in case of trailing space in path before wildcard + rm -f "$(printf '%q' "${FTL_LOG:-/var/log/pihole/FTL.log}")*" &> /dev/null + rm -f "$(printf '%q' "${DNSMASQ_LOG:-/var/log/pihole/pihole.log}")*" &> /dev/null + rm -f "$(printf '%q' "${WEBSERVER_LOG:-/var/log/pihole/webserver.log}")*" &> /dev/null + + # remove any remnant log-files from old versions + rm -rf /var/log/*pihole* &> /dev/null + + # remove log directory + rm -rf /var/log/pihole &> /dev/null + + # remove the pihole command + rm -f /usr/local/bin/pihole &> /dev/null + + # remove Pi-hole's bash completion + rm -f /etc/bash_completion.d/pihole &> /dev/null + rm -f /etc/bash_completion.d/pihole-FTL &> /dev/null + + # Remove pihole from sudoers for compatibility with old versions + rm -f /etc/sudoers.d/pihole &> /dev/null + + echo -e " ${TICK} Removed config files" +} + +removeManPage() { + # If the pihole manpage exists, then delete if [[ -f /usr/local/share/man/man8/pihole.8 ]]; then - ${SUDO} rm -f /usr/local/share/man/man8/pihole.8 /usr/local/share/man/man8/pihole-FTL.8 /usr/local/share/man/man5/pihole-FTL.conf.5 - ${SUDO} mandb -q &>/dev/null + rm -f /usr/local/share/man/man8/pihole.8 /usr/local/share/man/man8/pihole-FTL.8 /usr/local/share/man/man5/pihole-FTL.conf.5 + # Rebuild man-db if present + if is_command "mandb"; then + mandb -q &>/dev/null + fi echo -e " ${TICK} Removed pihole man page" fi +} +removeUser() { # If the pihole user exists, then remove if id "pihole" &> /dev/null; then - if ${SUDO} userdel -r pihole 2> /dev/null; then + if userdel -r pihole 2> /dev/null; then echo -e " ${TICK} Removed 'pihole' user" else echo -e " ${CROSS} Unable to remove 'pihole' user" fi fi + # If the pihole group exists, then remove if getent group "pihole" &> /dev/null; then - if ${SUDO} groupdel pihole 2> /dev/null; then + if groupdel pihole 2> /dev/null; then echo -e " ${TICK} Removed 'pihole' group" else echo -e " ${CROSS} Unable to remove 'pihole' group" fi fi +} +restoreResolved() { + # Restore Resolved from saved configuration, if present + if [[ -e /etc/systemd/resolved.conf.orig ]] || [[ -e /etc/systemd/resolved.conf.d/90-pi-hole-disable-stub-listener.conf ]]; then + cp -p /etc/systemd/resolved.conf.orig /etc/systemd/resolved.conf &> /dev/null || true + rm -f /etc/systemd/resolved.conf.d/90-pi-hole-disable-stub-listener.conf &> /dev/null + systemctl reload-or-restart systemd-resolved + fi +} + +completionMessage() { echo -e "\\n We're sorry to see you go, but thanks for checking out Pi-hole! If you need help, reach out to us on GitHub, Discourse, Reddit or Twitter - Reinstall at any time: ${COL_WHITE}curl -sSL https://install.pi-hole.net | bash${COL_NC} + Reinstall at any time: ${COL_BOLD}curl -sSL https://install.pi-hole.net | bash${COL_NC} - ${COL_LIGHT_RED}Please reset the DNS on your router/clients to restore internet connectivity${COL_NC} + ${COL_RED}Please reset the DNS on your router/clients to restore internet connectivity${COL_NC} ${INFO} Pi-hole's meta package has been removed, use the 'autoremove' function from your package manager to remove unused dependencies${COL_NC} - ${COL_LIGHT_GREEN}Uninstallation Complete! ${COL_NC}" + ${COL_GREEN}Uninstallation Complete! ${COL_NC}" } ######### SCRIPT ########### +# The ordering here allows clean uninstallation with nothing +# removed before anything that depends upon it. +# eg removeFTL relies on scripts removed by removePiholeFiles +# removeUser relies on commands removed by removeMetaPackage +package_manager_detect +removeWebInterface +removeCronFiles +restoreResolved +removeManPage +removeFTL +removeUser removeMetaPackage removePiholeFiles +completionMessage diff --git a/gravity.sh b/gravity.sh index 493f2b15..b772dc61 100755 --- a/gravity.sh +++ b/gravity.sh @@ -1,5 +1,4 @@ #!/usr/bin/env bash -# shellcheck disable=SC1090 # Pi-hole: A black hole for Internet advertisements # (c) 2017 Pi-hole, LLC (https://pi-hole.net) @@ -16,13 +15,13 @@ export LC_ALL=C PI_HOLE_SCRIPT_DIR="/opt/pihole" # Source utils.sh for GetFTLConfigValue utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" -# shellcheck disable=SC1090 +# shellcheck source=./advanced/Scripts/utils.sh . "${utilsfile}" coltable="${PI_HOLE_SCRIPT_DIR}/COL_TABLE" -# shellcheck disable=SC1090 +# shellcheck source=./advanced/Scripts/COL_TABLE . "${coltable}" -# shellcheck disable=SC1091 +# shellcheck source=./advanced/Scripts/database_migration/gravity-db.sh . "/etc/.pihole/advanced/Scripts/database_migration/gravity-db.sh" basename="pihole" @@ -51,14 +50,14 @@ etag_support=false # Check gravity temp directory if [ ! -d "${GRAVITY_TMPDIR}" ] || [ ! -w "${GRAVITY_TMPDIR}" ]; then - echo -e " ${COL_LIGHT_RED}Gravity temporary directory does not exist or is not a writeable directory, falling back to /tmp. ${COL_NC}" + echo -e " ${COL_RED}Gravity temporary directory does not exist or is not a writeable directory, falling back to /tmp. ${COL_NC}" GRAVITY_TMPDIR="/tmp" fi # Set this only after sourcing pihole-FTL.conf as the gravity database path may # have changed gravityDBfile="${GRAVITYDB}" -gravityDBfile_default="/etc/pihole/gravity.db" +gravityDBfile_default="${piholeDir}/gravity.db" gravityTEMPfile="${GRAVITYDB}_temp" gravityDIR="$(dirname -- "${gravityDBfile}")" gravityOLDfile="${gravityDIR}/gravity_old.db" @@ -119,15 +118,18 @@ gravity_swap_databases() { # Swap databases and remove or conditionally rename old database # Number of available blocks on disk - availableBlocks=$(stat -f --format "%a" "${gravityDIR}") + # Busybox Compat: `stat` long flags unsupported + # -f flag is short form of --file-system. + # -c flag is short form of --format. + availableBlocks=$(stat -f -c "%a" "${gravityDIR}") # Number of blocks, used by gravity.db - gravityBlocks=$(stat --format "%b" "${gravityDBfile}") + gravityBlocks=$(stat -c "%b" "${gravityDBfile}") # Only keep the old database if available disk space is at least twice the size of the existing gravity.db. # Better be safe than sorry... oldAvail=false if [ "${availableBlocks}" -gt "$((gravityBlocks * 2))" ] && [ -f "${gravityDBfile}" ]; then oldAvail=true - cp "${gravityDBfile}" "${gravityOLDfile}" + cp -p "${gravityDBfile}" "${gravityOLDfile}" fi # Drop the gravity and antigravity tables + subsequent VACUUM the current @@ -140,7 +142,7 @@ gravity_swap_databases() { else # Check if the backup directory exists if [ ! -d "${gravityBCKdir}" ]; then - mkdir -p "${gravityBCKdir}" + mkdir -p "${gravityBCKdir}" && chown pihole:pihole "${gravityBCKdir}" fi # If multiple gravityBCKfile's are present (appended with a number), rotate them @@ -306,7 +308,7 @@ migrate_to_database() { fi # Check if gravity database needs to be updated - upgrade_gravityDB "${gravityDBfile}" "${piholeDir}" + upgrade_gravityDB "${gravityDBfile}" # Migrate list files to new database if [ -e "${adListFile}" ]; then @@ -334,7 +336,7 @@ migrate_to_database() { fi # Check if gravity database needs to be updated - upgrade_gravityDB "${gravityDBfile}" "${piholeDir}" + upgrade_gravityDB "${gravityDBfile}" } # Determine if DNS resolution is available before proceeding @@ -349,17 +351,24 @@ gravity_CheckDNSResolutionAvailable() { echo -e " ${CROSS} DNS resolution is currently unavailable" fi - str="Waiting until DNS resolution is available..." + str="Waiting up to 120 seconds for DNS resolution..." echo -ne " ${INFO} ${str}" - until getent hosts github.com &> /dev/null; do - # Append one dot for each second waiting - str="${str}." - echo -ne " ${OVER} ${INFO} ${str}" - sleep 1 + + # Default DNS timeout is two seconds, plus 1 second for each dot > 120 seconds + for ((i = 0; i < 40; i++)); do + if getent hosts github.com &> /dev/null; then + # If we reach this point, DNS resolution is available + echo -e "${OVER} ${TICK} DNS resolution is available" + return 0 + fi + # Append one dot for each second waiting + echo -ne "." + sleep 1 done - # If we reach this point, DNS resolution is available - echo -e "${OVER} ${TICK} DNS resolution is available" + # DNS resolution is still unavailable after 120 seconds + return 1 + } # Function: try_restore_backup @@ -418,7 +427,7 @@ gravity_DownloadBlocklists() { echo -e " ${INFO} Storing gravity database in ${COL_BOLD}${gravityDBfile}${COL_NC}" fi - local url domain str target compression adlist_type directory success + local url domain str compression adlist_type directory success echo "" # Prepare new gravity database @@ -567,12 +576,12 @@ gravity_DownloadBlocklists() { if [[ "${check_url}" =~ ${regex} ]]; then echo -e " ${CROSS} Invalid Target" else - timeit gravity_DownloadBlocklistFromUrl "${url}" "${sourceIDs[$i]}" "${saveLocation}" "${target}" "${compression}" "${adlist_type}" "${domain}" + timeit gravity_DownloadBlocklistFromUrl "${url}" "${sourceIDs[$i]}" "${saveLocation}" "${compression}" "${adlist_type}" "${domain}" fi echo "" done - gravity_Blackbody=true + DownloadBlocklists_done=true } compareLists() { @@ -601,9 +610,11 @@ compareLists() { # Download specified URL and perform checks on HTTP status and file content gravity_DownloadBlocklistFromUrl() { - local url="${1}" adlistID="${2}" saveLocation="${3}" target="${4}" compression="${5}" gravity_type="${6}" domain="${7}" - local modifiedOptions="" listCurlBuffer str httpCode success="" ip cmd_ext - local file_path permissions ip_addr port blocked=false download=true + local url="${1}" adlistID="${2}" saveLocation="${3}" compression="${4}" gravity_type="${5}" domain="${6}" + local listCurlBuffer str httpCode success="" ip customUpstreamResolver="" + 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=() # Create temp file to store content on disk instead of RAM # We don't use '--suffix' here because not all implementations of mktemp support it, e.g. on Alpine @@ -620,14 +631,14 @@ gravity_DownloadBlocklistFromUrl() { # Save HTTP ETag to the specified file. An ETag is a caching related header, # usually returned in a response. If no ETag is sent by the server, an empty # file is created and can later be used consistently. - modifiedOptions="--etag-save ${saveLocation}.etag" + modifiedOptions=("${modifiedOptions[@]}" --etag-save "${saveLocation}".etag) if [[ -f "${saveLocation}.etag" ]]; then # This option makes a conditional HTTP request for the specific ETag read # from the given file by sending a custom If-None-Match header using the # stored ETag. This way, the server will only send the file if it has # changed since the last request. - modifiedOptions="${modifiedOptions} --etag-compare ${saveLocation}.etag" + modifiedOptions=("${modifiedOptions[@]}" --etag-compare "${saveLocation}".etag) fi fi @@ -640,39 +651,13 @@ gravity_DownloadBlocklistFromUrl() { # Interstingly, this option is not supported by raw.githubusercontent.com # URLs, however, it is still supported by many older web servers which may # not support the HTTP ETag method so we keep it as a fallback. - modifiedOptions="${modifiedOptions} -z ${saveLocation}" + modifiedOptions=("${modifiedOptions[@]}" -z "${saveLocation}") fi fi str="Status:" echo -ne " ${INFO} ${str} Pending..." blocked=false - case $(getFTLConfigValue dns.blocking.mode) in - "IP-NODATA-AAAA" | "IP") - # Get IP address of this domain - ip="$(dig "${domain}" +short)" - # Check if this IP matches any IP of the system - if [[ -n "${ip}" && $(grep -Ec "inet(|6) ${ip}" <<<"$(ip a)") -gt 0 ]]; then - blocked=true - fi - ;; - "NXDOMAIN") - if [[ $(dig "${domain}" | grep "NXDOMAIN" -c) -ge 1 ]]; then - blocked=true - fi - ;; - "NODATA") - if [[ $(dig "${domain}" | grep "NOERROR" -c) -ge 1 ]] && [[ -z $(dig +short "${domain}") ]]; then - blocked=true - fi - ;; - "NULL" | *) - if [[ $(dig "${domain}" +short | grep "0.0.0.0" -c) -ge 1 ]]; then - blocked=true - fi - ;; - esac - # Check if this domain is blocked by Pi-hole but only if the domain is not a # local file or empty if [[ $url != "file"* ]] && [[ -n "${domain}" ]]; then @@ -732,46 +717,56 @@ gravity_DownloadBlocklistFromUrl() { fi echo -e "${OVER} ${CROSS} ${str} ${domain} is blocked by one of your lists. Using DNS server ${upstream} instead" echo -ne " ${INFO} ${str} Pending..." - cmd_ext="--resolve $domain:$port:$ip" + customUpstreamResolver="--resolve $domain:$port:$ip" 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 # Check for allowed protocols if [[ $url != "http"* && $url != "https"* && $url != "file"* && $url != "ftp"* && $url != "ftps"* && $url != "sftp"* ]]; then echo -e "${OVER} ${CROSS} ${str} Invalid protocol specified. Ignoring list." - echo -e "Ensure your URL starts with a valid protocol like http:// , https:// or file:// ." + echo -e " Ensure your URL starts with a valid protocol like http:// , https:// or file:// ." download=false fi if [[ "${download}" == true ]]; then - # shellcheck disable=SC2086 - httpCode=$(curl --connect-timeout ${curl_connect_timeout} -s -L ${compression} ${cmd_ext} ${modifiedOptions} -w "%{http_code}" "${url}" -o "${listCurlBuffer}" 2>/dev/null) + httpCode=$(curl --connect-timeout ${curl_connect_timeout} -s -L ${compression:+${compression}} ${customUpstreamResolver:+${customUpstreamResolver}} "${modifiedOptions[@]}" -w "%{http_code}" "${url}" -o "${listCurlBuffer}" 2>/dev/null) fi case $url in @@ -821,12 +816,16 @@ gravity_DownloadBlocklistFromUrl() { done="true" # Check if $listCurlBuffer is a non-zero length file elif [[ -s "${listCurlBuffer}" ]]; then - # Determine if blocklist is non-standard and parse as appropriate - gravity_ParseFileIntoDomains "${listCurlBuffer}" "${saveLocation}" - # Remove curl buffer file after its use - rm "${listCurlBuffer}" - # Compare lists if are they identical + # Move the downloaded list to the final location + mv "${listCurlBuffer}" "${saveLocation}" + # Ensure the file has the correct permissions + 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" @@ -840,13 +839,13 @@ gravity_DownloadBlocklistFromUrl() { if [[ "${done}" != "true" ]]; then # Determine if cached list has read permission if [[ -r "${saveLocation}" ]]; then - echo -e " ${CROSS} List download failed: ${COL_LIGHT_GREEN}using previously cached list${COL_NC}" + echo -e " ${CROSS} List download failed: ${COL_GREEN}using previously cached list${COL_NC}" # Set list status to "download-failed/cached" database_adlist_status "${adlistID}" "3" # Add domains to database table file pihole-FTL "${gravity_type}" parseList "${saveLocation}" "${gravityTEMPfile}" "${adlistID}" else - echo -e " ${CROSS} List download failed: ${COL_LIGHT_RED}no cached list available${COL_NC}" + echo -e " ${CROSS} List download failed: ${COL_RED}no cached list available${COL_NC}" # Manually reset these two numbers because we do not call parseList here database_adlist_number "${adlistID}" 0 0 database_adlist_status "${adlistID}" "4" @@ -854,37 +853,6 @@ gravity_DownloadBlocklistFromUrl() { fi } -# Parse source files into domains format -gravity_ParseFileIntoDomains() { - local src="${1}" destination="${2}" - - # Remove comments and print only the domain name - # Most of the lists downloaded are already in hosts file format but the spacing/formatting is not contiguous - # This helps with that and makes it easier to read - # It also helps with debugging so each stage of the script can be researched more in depth - # 1) Convert all characters to lowercase - tr '[:upper:]' '[:lower:]' <"${src}" >"${destination}" - - # 2) Remove carriage returns - # 3) Remove lines starting with ! (ABP Comments) - # 4) Remove lines starting with [ (ABP Header) - # 5) Remove lines containing ABP extended CSS selectors ("##", "#$#", "#@#", "#?#") and Adguard JavaScript (#%#) preceded by a letter - # 6) Remove comments (text starting with "#", include possible spaces before the hash sign) - # 7) Remove leading tabs, spaces, etc. (Also removes leading IP addresses) - # 8) Remove empty lines - - sed -i -r \ - -e 's/\r$//' \ - -e 's/\s*!.*//g' \ - -e 's/\s*\[.*//g' \ - -e '/[a-z]\#[$?@%]{0,3}\#/d' \ - -e 's/\s*#.*//g' \ - -e 's/^.*\s+//g' \ - -e '/^$/d' "${destination}" - - fix_owner_permissions "${destination}" -} - # Report number of entries in a table gravity_Table_Count() { local table="${1}" @@ -901,7 +869,7 @@ gravity_Table_Count() { fi } -# Output count of blacklisted domains and regex filters +# Output count of denied and allowed domains and regex filters gravity_ShowCount() { # Here we use the table "gravity" instead of the view "vw_gravity" for speed. # It's safe to replace it here, because right after a gravity run both will show the exactly same number of domains. @@ -914,7 +882,7 @@ gravity_ShowCount() { # Trap Ctrl-C gravity_Trap() { - trap '{ echo -e "\\n\\n ${INFO} ${COL_LIGHT_RED}User-abort detected${COL_NC}"; gravity_Cleanup "error"; }' INT + trap '{ echo -e "\\n\\n ${INFO} ${COL_RED}User-abort detected${COL_NC}"; gravity_Cleanup "error"; }' INT } # Clean up after Gravity upon exit or cancellation @@ -932,13 +900,13 @@ gravity_Cleanup() { # invalid_domains location rm "${GRAVITY_TMPDIR}"/*.ph-non-domains 2>/dev/null - # Ensure this function only runs when gravity_SetDownloadOptions() has completed - if [[ "${gravity_Blackbody:-}" == true ]]; then - # Remove any unused .domains files - for file in "${piholeDir}"/*."${domainsExtension}"; do - # If list is not in active array, then remove it + # Ensure this function only runs when gravity_DownloadBlocklists() has completed + if [[ "${DownloadBlocklists_done:-}" == true ]]; then + # Remove any unused .domains/.etag/.sha files + for file in "${listsCacheDir}"/*."${domainsExtension}"; do + # If list is not in active array, then remove it and all associated files if [[ ! "${activeDomains[*]}" == *"${file}"* ]]; then - rm -f "${file}" 2>/dev/null || + rm -f "${file}"* 2>/dev/null || echo -e " ${CROSS} Failed to remove ${file##*/}" fi done @@ -994,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 "" @@ -1072,7 +1040,7 @@ migrate_to_listsCache_dir() { # If not, we need to migrate the old files to the new directory local str="Migrating the list's cache directory to new location" echo -ne " ${INFO} ${str}..." - mkdir -p "${listsCacheDir}" + mkdir -p "${listsCacheDir}" && chown pihole:pihole "${listsCacheDir}" # Move the old files to the new directory if mv "${piholeDir}"/list.* "${listsCacheDir}/" 2>/dev/null; then @@ -1131,13 +1099,19 @@ for var in "$@"; do "-t" | "--timeit") timed=true ;; "-r" | "--repair") repairSelector "$3" ;; "-u" | "--upgrade") - upgrade_gravityDB "${gravityDBfile}" "${piholeDir}" + upgrade_gravityDB "${gravityDBfile}" exit 0 ;; "-h" | "--help") helpFunc ;; esac done +# Check if DNS is available, no need to do any database manipulation if we're not able to download adlists +if ! timeit gravity_CheckDNSResolutionAvailable; then + echo -e " ${CROSS} No DNS resolution available. Please contact support." + exit 1 +fi + # Remove OLD (backup) gravity file, if it exists if [[ -f "${gravityOLDfile}" ]]; then rm "${gravityOLDfile}" @@ -1171,18 +1145,13 @@ fi if [[ "${forceDelete:-}" == true ]]; then str="Deleting existing list cache" - echo -ne "${INFO} ${str}..." + echo -ne " ${INFO} ${str}..." rm "${listsCacheDir}/list.*" 2>/dev/null || true echo -e "${OVER} ${TICK} ${str}" fi # Gravity downloads blocklists next -if ! timeit gravity_CheckDNSResolutionAvailable; then - echo -e " ${CROSS} Can not complete gravity update, no DNS is available. Please contact support." - exit 1 -fi - if ! gravity_DownloadBlocklists; then echo -e " ${CROSS} Unable to create gravity database. Please try again later. If the problem persists, please contact support." exit 1 diff --git a/manpages/pihole.8 b/manpages/pihole.8 index 97a6ec68..191691ee 100644 --- a/manpages/pihole.8 +++ b/manpages/pihole.8 @@ -23,7 +23,7 @@ pihole -r .br \fBpihole -g\fR .br -\fBpihole\fR -\fBq\fR [options] +\fBpihole\fR \fB-q\fR [options] .br \fBpihole\fR \fB-l\fR (\fBon|off|off noflush\fR) .br @@ -43,7 +43,7 @@ pihole -r .br \fBpihole\fR \fBcheckout\fR repo [branch] .br -\fBpihole\fR \api\fR endpoint +\fBpihole\fR \fBapi\fR [verbose] endpoint .br \fBpihole\fR \fBhelp\fR .br @@ -100,14 +100,17 @@ Available commands and options: -c Include a Pi-hole database integrity check .br -\fB-f, flush\fR +\fB-f, flush\fR [quite] .br - Flush the Pi-hole log + Flush the Pi-hole log and last 24h from the query database .br -\fB-r, reconfigure\fR + quite Suppress output .br - Reconfigure or Repair Pi-hole subsystems + +\fB-r, repair\fR +.br + Repair Pi-hole subsystems .br \fB-t, tail\fR [arg] @@ -234,10 +237,22 @@ Available commands and options: branchname Update subsystems to the specified branchname .br -\fBapi\fR endpoint +\fBapi\fR [verbose] endpoint .br Query the Pi-hole API at .br + + verbose Show authentication and status messages +.br + +\fBlogrotate\fR [quite] +.br + Rotate Pi-hole's log files +.br + + quite Suppress output +.br + .SH "EXAMPLE" Some usage examples @@ -264,7 +279,7 @@ Allow-/denylist manipulation \fBpihole --regex "ad.*\\.example\\.com$"\fR .br - Adds "ad.*\\.example\\.com$" to the regex blacklist. + Adds "ad.*\\.example\\.com$" to the regex denylist. Would block all subdomains of example.com which start with "ad" .br @@ -313,9 +328,10 @@ Switching Pi-hole subsystem branches Switch to core development branch .br -\fBpihole arpflush\fR +\fBpihole networkflush\fR .br - Flush information stored in Pi-hole's network tables + Flush information stored in Pi-hole's network table + Add '--arp' to additionally flush the ARP table .br \fBpihole api stats/summary\fR @@ -323,6 +339,11 @@ Switching Pi-hole subsystem branches Queries FTL for the stats/summary endpoint .br +\fBpihole api verbose stats/summary\fR +.br + Same as above, but shows authentication and status messages +.br + .SH "COLOPHON" Get sucked into the latest news and community activity by entering Pi-hole's orbit. Information about Pi-hole, and the latest version of the software can be found at https://pi-hole.net. diff --git a/pihole b/pihole index b5fae016..edebf48c 100755 --- a/pihole +++ b/pihole @@ -9,7 +9,7 @@ # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. -readonly PI_HOLE_SCRIPT_DIR="/opt/pihole" +PI_HOLE_SCRIPT_DIR="/opt/pihole" # PI_HOLE_BIN_DIR is not readonly here because in some functions (checkout), # they might get set again when the installer is sourced. This causes an @@ -17,13 +17,16 @@ readonly PI_HOLE_SCRIPT_DIR="/opt/pihole" PI_HOLE_BIN_DIR="/usr/local/bin" readonly colfile="${PI_HOLE_SCRIPT_DIR}/COL_TABLE" +# shellcheck source=./advanced/Scripts/COL_TABLE source "${colfile}" -readonly utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" +utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" +# shellcheck source=./advanced/Scripts/utils.sh source "${utilsfile}" # Source api functions readonly apifile="${PI_HOLE_SCRIPT_DIR}/api.sh" +# shellcheck source=./advanced/Scripts/api.sh source "${apifile}" versionsfile="/etc/pihole/versions" @@ -31,6 +34,7 @@ if [ -f "${versionsfile}" ]; then # Only source versionsfile if the file exits # fixes a warning during installation where versionsfile does not exist yet # but gravity calls `pihole -status` and thereby sourcing the file + # shellcheck source=/dev/null source "${versionsfile}" fi @@ -88,12 +92,32 @@ debugFunc() { } flushFunc() { - "${PI_HOLE_SCRIPT_DIR}"/piholeLogFlush.sh "$@" + # unsupported in docker because it requires restarting FTL + if [ -n "${DOCKER_VERSION}" ]; then + unsupportedFunc + else + "${PI_HOLE_SCRIPT_DIR}"/piholeLogFlush.sh "$@" + exit 0 + fi +} + +# Deprecated function, should be removed in the future +# use networkFlush instead +arpFunc() { + shift + echo -e " ${INFO} The 'arpflush' command is deprecated, use 'networkflush' instead" + "${PI_HOLE_SCRIPT_DIR}"/piholeNetworkFlush.sh "$@" exit 0 } -arpFunc() { - "${PI_HOLE_SCRIPT_DIR}"/piholeARPTable.sh "$@" +logrotateFunc() { + "${PI_HOLE_SCRIPT_DIR}"/piholeLogRotate.sh "$@" + exit 0 +} + +networkFlush() { + shift + "${PI_HOLE_SCRIPT_DIR}"/piholeNetworkFlush.sh "$@" exit 0 } @@ -107,11 +131,26 @@ updatePiholeFunc() { fi } -reconfigurePiholeFunc() { +repairPiholeFunc() { if [ -n "${DOCKER_VERSION}" ]; then unsupportedFunc else - /etc/.pihole/automated\ install/basic-install.sh --reconfigure + 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 } @@ -143,10 +182,11 @@ uninstallFunc() { versionFunc() { exec "${PI_HOLE_SCRIPT_DIR}"/version.sh + exit 0 } reloadDNS() { - local svcOption svc str output status pid icon FTL_PID_FILE + local svcOption svc str output status pid icon FTL_PID_FILE sigrtmin svcOption="${1:-reload}" # get the current path to the pihole-FTL.pid @@ -165,7 +205,10 @@ reloadDNS() { str="FTL is not running" icon="${INFO}" else - svc="kill -RTMIN ${pid}" + sigrtmin="$(pihole-FTL sigrtmin 2>/dev/null)" + # Make sure sigrtmin is a number, otherwise fallback to RTMIN + [[ "${sigrtmin}" =~ ^[0-9]+$ ]] || unset sigrtmin + svc="kill -${sigrtmin:-RTMIN} ${pid}" str="Reloading DNS lists" icon="${TICK}" fi @@ -234,7 +277,7 @@ Time: fi if [[ ${error} == true ]];then - echo -e " ${COL_LIGHT_RED}Unknown format for blocking timer!${COL_NC}" + echo -e " ${COL_RED}Unknown format for blocking timer!${COL_NC}" echo -e " Try 'pihole disable --help' for more information." exit 1 fi @@ -247,17 +290,20 @@ Time: data=$(PostFTLData "dns/blocking" "{ \"blocking\": ${1}, \"timer\": ${tt} }") # Check the response - local extra=" forever" - local timer="$(echo "${data}"| jq --raw-output '.timer' )" + local extra timer + extra=" forever" + timer="$(echo "${data}"| jq --raw-output '.timer' )" if [[ "${timer}" != "null" ]]; then extra=" for ${timer}s" fi - local str="Pi-hole $(echo "${data}" | jq --raw-output '.blocking')${extra}" + local str + str="Pi-hole $(echo "${data}" | jq --raw-output '.blocking')${extra}" # Logout from the API LogoutAPI echo -e "${OVER} ${TICK} ${str}" + exit 0 } piholeLogging() { @@ -287,7 +333,7 @@ Options: echo -e " ${INFO} Enabling logging..." local str="Logging has been enabled!" else - echo -e " ${COL_LIGHT_RED}Invalid option${COL_NC} + echo -e " ${COL_RED}Invalid option${COL_NC} Try 'pihole logging --help' for more information." exit 1 fi @@ -375,20 +421,22 @@ statusFunc() { tailFunc() { # Warn user if Pi-hole's logging is disabled - local logging_enabled=$(getFTLConfigValue dns.queryLogging) + local logging_enabled + logging_enabled=$(getFTLConfigValue dns.queryLogging) if [[ "${logging_enabled}" != "true" ]]; then echo " ${CROSS} Warning: Query logging is disabled" fi echo -e " ${INFO} Press Ctrl-C to exit" # Get logfile path - readonly LOGFILE=$(getFTLConfigValue files.log.dnsmasq) + LOGFILE=$(getFTLConfigValue files.log.dnsmasq) + readonly LOGFILE # Strip date from each line # Color blocklist/denylist/wildcard entries as red # Color A/AAAA/DHCP strings as white # Color everything else as gray - tail -f $LOGFILE | grep --line-buffered "${1}" | sed -E \ + tail -f $LOGFILE | grep --line-buffered -- "${1}" | sed -E \ -e "s,($(date +'%b %d ')| dnsmasq\[[0-9]*\]),,g" \ -e "s,(.*(denied |gravity blocked ).*),${COL_RED}&${COL_NC}," \ -e "s,.*(query\\[A|DHCP).*,${COL_NC}&${COL_NC}," \ @@ -423,6 +471,7 @@ piholeCheckoutFunc() { exit 0 fi + #shellcheck source=./advanced/Scripts/piholeCheckout.sh source "${PI_HOLE_SCRIPT_DIR}"/piholeCheckout.sh shift checkout "$@" @@ -478,12 +527,14 @@ Debugging Options: -d, debug Start a debugging session Add '-c' or '--check-database' to include a Pi-hole database integrity check Add '-a' to automatically upload the log to tricorder.pi-hole.net - -f, flush Flush the Pi-hole log - -r, reconfigure Reconfigure or Repair Pi-hole subsystems + -f, flush Flush the Pi-hole logs and last 24h from the query database + Add 'quiet' to suppress output messages + -r, repair Repair Pi-hole subsystems -t, tail [arg] View the live output of the Pi-hole log. Add an optional argument to filter the log (regular expressions are supported) api Query the Pi-hole API at + Precede with 'verbose' option to show authentication and status messages Options: @@ -509,7 +560,10 @@ Options: reloadlists Update the lists WITHOUT flushing the cache or restarting the DNS server checkout Switch Pi-hole subsystems to a different GitHub branch Add '-h' for more info on checkout usage - arpflush Flush information stored in Pi-hole's network tables"; + networkflush Flush information stored in Pi-hole's network tables + Add '--arp' to additionally flush the ARP table + logrotate Rotate Pi-hole's log files + Add 'quiet' to suppress output messages"; exit 0 } @@ -518,7 +572,7 @@ if [[ $# = 0 ]]; then fi # functions that do not require sudo power -need_root=1 +need_root= case "${1}" in "-h" | "help" | "--help" ) helpFunc;; "-v" | "version" ) versionFunc;; @@ -526,31 +580,33 @@ case "${1}" in "-q" | "query" ) queryFunc "$@";; "status" ) statusFunc "$2";; "tricorder" ) tricorderFunc;; + "allow" | "allowlist" ) listFunc "$@";; + "deny" | "denylist" ) listFunc "$@";; + "--wild" | "wildcard" ) listFunc "$@";; + "--regex" | "regex" ) listFunc "$@";; + "--allow-regex" | "allow-regex" ) listFunc "$@";; + "--allow-wild" | "allow-wild" ) listFunc "$@";; + "enable" ) piholeEnable true "$2";; + "disable" ) piholeEnable false "$2";; + "api" ) shift; apiFunc "$@"; exit 0;; # we need to add all arguments that require sudo power to not trigger the * argument - "allow" | "allowlist" ) need_root=0;; - "deny" | "denylist" ) need_root=0;; - "--wild" | "wildcard" ) need_root=0;; - "--regex" | "regex" ) need_root=0;; - "--allow-regex" | "allow-regex" ) need_root=0;; - "--allow-wild" | "allow-wild" ) need_root=0;; - "-f" | "flush" ) ;; - "-up" | "updatePihole" ) ;; - "-r" | "reconfigure" ) ;; - "-l" | "logging" ) ;; - "uninstall" ) ;; - "enable" ) need_root=0;; - "disable" ) need_root=0;; - "-d" | "debug" ) ;; - "-g" | "updateGravity" ) ;; - "reloaddns" ) ;; - "reloadlists" ) ;; - "setpassword" ) ;; - "checkout" ) ;; - "updatechecker" ) ;; - "arpflush" ) ;; - "-t" | "tail" ) ;; - "api" ) need_root=0;; + "-f" | "flush" ) need_root=true;; + "-up" | "updatePihole" ) need_root=true;; + "-r" | "repair" ) need_root=true;; + "-l" | "logging" ) need_root=true;; + "uninstall" ) need_root=true;; + "-d" | "debug" ) need_root=true;; + "-g" | "updateGravity" ) need_root=true;; + "reloaddns" ) need_root=true;; + "reloadlists" ) need_root=true;; + "setpassword" ) need_root=true;; + "checkout" ) need_root=true;; + "updatechecker" ) need_root=true;; + "arpflush" ) need_root=true;; # Deprecated, use networkflush instead + "networkflush" ) need_root=true;; + "-t" | "tail" ) need_root=true;; + "logrotate" ) need_root=true;; * ) helpFunc;; esac @@ -560,38 +616,32 @@ if [[ -z ${USER} ]]; then USER=$(whoami) fi -# Check if the current user is neither root nor pihole and if the command +# Check if the current user is not root and if the command # requires root. If so, exit with an error message. -if [[ $EUID -ne 0 && ${USER} != "pihole" && need_root -eq 1 ]];then - echo -e " ${CROSS} The Pi-hole command requires root privileges, try:" +# Add an exception for the user "pihole" to allow the webserver running gravity +if [[ ( $EUID -ne 0 && ${USER} != "pihole" ) && -n "${need_root}" ]]; then + echo -e " ${CROSS} This Pi-hole command requires root privileges, try:" echo -e " ${COL_GREEN}sudo pihole $*${COL_NC}" exit 1 fi # Handle redirecting to specific functions based on arguments case "${1}" in - "allow" | "allowlist" ) listFunc "$@";; - "deny" | "denylist" ) listFunc "$@";; - "--wild" | "wildcard" ) listFunc "$@";; - "--regex" | "regex" ) listFunc "$@";; - "--allow-regex" | "allow-regex" ) listFunc "$@";; - "--allow-wild" | "allow-wild" ) listFunc "$@";; "-d" | "debug" ) debugFunc "$@";; "-f" | "flush" ) flushFunc "$@";; "-up" | "updatePihole" ) updatePiholeFunc "$@";; - "-r" | "reconfigure" ) reconfigurePiholeFunc;; + "-r" | "repair" ) repairPiholeFunc "$@";; "-g" | "updateGravity" ) updateGravityFunc "$@";; "-l" | "logging" ) piholeLogging "$@";; "uninstall" ) uninstallFunc;; - "enable" ) piholeEnable true "$2";; - "disable" ) piholeEnable false "$2";; "reloaddns" ) reloadDNS "reload";; "reloadlists" ) reloadDNS "reload-lists";; "setpassword" ) SetWebPassword "$@";; "checkout" ) piholeCheckoutFunc "$@";; "updatechecker" ) shift; updateCheckFunc "$@";; - "arpflush" ) arpFunc "$@";; + "arpflush" ) arpFunc "$@";; # Deprecated, use networkflush instead + "networkflush" ) networkFlush "$@";; "-t" | "tail" ) tailFunc "$2";; - "api" ) apiFunc "$2";; + "logrotate" ) logrotateFunc "$@";; * ) helpFunc;; esac diff --git a/test/_alpine_3_21.Dockerfile b/test/_alpine_3_21.Dockerfile new file mode 100644 index 00000000..d7b88f20 --- /dev/null +++ b/test/_alpine_3_21.Dockerfile @@ -0,0 +1,18 @@ +FROM alpine:3.21 + +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/_alpine_3_22.Dockerfile b/test/_alpine_3_22.Dockerfile new file mode 100644 index 00000000..25beb4e0 --- /dev/null +++ b/test/_alpine_3_22.Dockerfile @@ -0,0 +1,18 @@ +FROM alpine:3.22 + +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/_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/_centos_10.Dockerfile b/test/_centos_10.Dockerfile new file mode 100644 index 00000000..78a89789 --- /dev/null +++ b/test/_centos_10.Dockerfile @@ -0,0 +1,19 @@ +FROM quay.io/centos/centos:stream10 +# Disable SELinux +RUN echo "SELINUX=disabled" > /etc/selinux/config +RUN yum install -y --allowerasing curl 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/_centos_9.Dockerfile b/test/_centos_9.Dockerfile index a5e7cf0b..73f53fa5 100644 --- a/test/_centos_9.Dockerfile +++ b/test/_centos_9.Dockerfile @@ -15,6 +15,5 @@ RUN true && \ chmod +x $SCRIPTDIR/* ENV SKIP_INSTALL=true -ENV OS_CHECK_DOMAIN_NAME=dev-supportedos.pi-hole.net #sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_debian_11.Dockerfile b/test/_debian_11.Dockerfile index b8107244..2389063c 100644 --- a/test/_debian_11.Dockerfile +++ b/test/_debian_11.Dockerfile @@ -12,6 +12,5 @@ RUN true && \ chmod +x $SCRIPTDIR/* ENV SKIP_INSTALL=true -ENV OS_CHECK_DOMAIN_NAME=dev-supportedos.pi-hole.net #sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_debian_12.Dockerfile b/test/_debian_12.Dockerfile index 7446711a..a6c5f1ed 100644 --- a/test/_debian_12.Dockerfile +++ b/test/_debian_12.Dockerfile @@ -12,6 +12,5 @@ RUN true && \ chmod +x $SCRIPTDIR/* ENV SKIP_INSTALL=true -ENV OS_CHECK_DOMAIN_NAME=dev-supportedos.pi-hole.net #sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_debian_13.Dockerfile b/test/_debian_13.Dockerfile new file mode 100644 index 00000000..cfff2235 --- /dev/null +++ b/test/_debian_13.Dockerfile @@ -0,0 +1,16 @@ +FROM buildpack-deps:trixie-scm + +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/_fedora_40.Dockerfile b/test/_fedora_40.Dockerfile index 20102a10..43913895 100644 --- a/test/_fedora_40.Dockerfile +++ b/test/_fedora_40.Dockerfile @@ -13,6 +13,5 @@ RUN true && \ chmod +x $SCRIPTDIR/* ENV SKIP_INSTALL=true -ENV OS_CHECK_DOMAIN_NAME=dev-supportedos.pi-hole.net #sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_fedora_41.Dockerfile b/test/_fedora_41.Dockerfile index bf5fe5d5..c03371a5 100644 --- a/test/_fedora_41.Dockerfile +++ b/test/_fedora_41.Dockerfile @@ -13,6 +13,5 @@ RUN true && \ chmod +x $SCRIPTDIR/* ENV SKIP_INSTALL=true -ENV OS_CHECK_DOMAIN_NAME=dev-supportedos.pi-hole.net #sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_fedora_42.Dockerfile b/test/_fedora_42.Dockerfile new file mode 100644 index 00000000..90b17c0b --- /dev/null +++ b/test/_fedora_42.Dockerfile @@ -0,0 +1,17 @@ +FROM fedora:42 +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/_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/_ubuntu_20.Dockerfile b/test/_ubuntu_20.Dockerfile index 75c12673..5b8deb5d 100644 --- a/test/_ubuntu_20.Dockerfile +++ b/test/_ubuntu_20.Dockerfile @@ -12,6 +12,5 @@ RUN true && \ chmod +x $SCRIPTDIR/* ENV SKIP_INSTALL=true -ENV OS_CHECK_DOMAIN_NAME=dev-supportedos.pi-hole.net #sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_ubuntu_22.Dockerfile b/test/_ubuntu_22.Dockerfile index 9206a46a..c3be89e1 100644 --- a/test/_ubuntu_22.Dockerfile +++ b/test/_ubuntu_22.Dockerfile @@ -13,6 +13,5 @@ RUN true && \ chmod +x $SCRIPTDIR/* ENV SKIP_INSTALL=true -ENV OS_CHECK_DOMAIN_NAME=dev-supportedos.pi-hole.net #sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_ubuntu_24.Dockerfile b/test/_ubuntu_24.Dockerfile index 4cab43de..cf57c2aa 100644 --- a/test/_ubuntu_24.Dockerfile +++ b/test/_ubuntu_24.Dockerfile @@ -13,6 +13,5 @@ RUN true && \ chmod +x $SCRIPTDIR/* ENV SKIP_INSTALL=true -ENV OS_CHECK_DOMAIN_NAME=dev-supportedos.pi-hole.net #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 7b7e679f..61f18c4e 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -1,6 +1,6 @@ -pyyaml == 6.0.2 -pytest == 8.3.4 -pytest-xdist == 3.6.1 -pytest-testinfra == 10.1.1 -tox == 4.24.1 +pyyaml == 6.0.3 +pytest == 9.0.2 +pytest-xdist == 3.8.0 +pytest-testinfra == 10.2.2 +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 73da7eef..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" @@ -22,12 +20,11 @@ def test_supported_package_manager(host): # break supported package managers host.run("rm -rf /usr/bin/apt-get") host.run("rm -rf /usr/bin/rpm") - package_manager_detect = host.run( - """ + host.run("rm -rf /sbin/apk") + 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 @@ -37,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 @@ -77,26 +72,33 @@ def test_installPihole_fresh_install_readableFiles(host): }, host, ) + mock_command_2( + "rc-service", + { + "rc-service pihole-FTL enable": ("", "0"), + "rc-service pihole-FTL restart": ("", "0"), + "rc-service pihole-FTL start": ("", "0"), + "*": ('echo "rc-service call with $@"', "0"), + }, + host, + ) # try to install man host.run("command -v apt-get > /dev/null && apt-get install -qq man") host.run("command -v dnf > /dev/null && dnf install -y man") host.run("command -v yum > /dev/null && yum install -y man") + 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 runUnattended=true - useUpdateVars=true source /opt/pihole/basic-install.sh > /dev/null runUnattended=true - useUpdateVars=true main /opt/pihole/pihole-FTL-prestart.sh - """ - ) + """) assert 0 == install.rc maninstalled = True if (info_box + " man not installed") in install.stdout: @@ -105,7 +107,7 @@ def test_installPihole_fresh_install_readableFiles(host): maninstalled = False piholeuser = "pihole" exit_status_success = 0 - test_cmd = 'su --shell /bin/bash --command "test -{0} {1}" -p {2}' + test_cmd = 'su -s /bin/bash -c "test -{0} {1}" -p {2}' # check files in /etc/pihole for read, write and execute permission check_etc = test_cmd.format("r", "/etc/pihole", piholeuser) actual_rc = host.run(check_etc).rc @@ -127,10 +129,6 @@ def test_installPihole_fresh_install_readableFiles(host): check_localversion = test_cmd.format("r", "/etc/pihole/versions", piholeuser) actual_rc = host.run(check_localversion).rc assert exit_status_success == actual_rc - # readable logrotate - check_logrotate = test_cmd.format("r", "/etc/pihole/logrotate", piholeuser) - actual_rc = host.run(check_logrotate).rc - assert exit_status_success == actual_rc # readable macvendor.db check_macvendor = test_cmd.format("r", "/etc/pihole/macvendor.db", piholeuser) actual_rc = host.run(check_macvendor).rc @@ -156,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 ) @@ -195,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() @@ -212,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 @@ -249,20 +237,19 @@ def test_FTL_detect_no_errors(host, arch, detected_string, supported): { "-A /bin/sh": ("Tag_CPU_arch: " + arch, "0"), "-A /usr/bin/sh": ("Tag_CPU_arch: " + arch, "0"), + "-A /usr/sbin/sh": ("Tag_CPU_arch: " + arch, "0"), }, 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 @@ -282,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 @@ -312,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 @@ -337,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 @@ -362,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 @@ -388,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 @@ -414,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 @@ -430,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 @@ -469,61 +438,16 @@ def test_validate_ip(host): test_address("0.0.0.0#00001", False) -def test_os_check_fails(host): - """Confirms install fails on unsupported OS""" - host.run( - """ - source /opt/pihole/basic-install.sh - package_manager_detect - build_dependency_package - install_dependent_packages - cat < /etc/os-release -ID=UnsupportedOS -VERSION_ID="2" -EOT - """ - ) - detectOS = host.run( - """t - source /opt/pihole/basic-install.sh - os_check - """ - ) - expected_stdout = "Unsupported OS detected: UnsupportedOS" - assert expected_stdout in detectOS.stdout - - -def test_os_check_passes(host): - """Confirms OS meets the requirements""" - host.run( - """ - source /opt/pihole/basic-install.sh - package_manager_detect - build_dependency_package - install_dependent_packages - """ - ) - detectOS = host.run( - """ - source /opt/pihole/basic-install.sh - os_check - """ - ) - expected_stdout = "Supported OS detected" - assert expected_stdout in detectOS.stdout - - 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 @@ -532,20 +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_21.ini b/test/tox.alpine_3_21.ini new file mode 100644 index 00000000..b0465f6c --- /dev/null +++ b/test/tox.alpine_3_21.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_21.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.alpine_3_22.ini b/test/tox.alpine_3_22.ini new file mode 100644 index 00000000..38f66c4f --- /dev/null +++ b/test/tox.alpine_3_22.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_22.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.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.centos_10.ini b/test/tox.centos_10.ini new file mode 100644 index 00000000..1a15c766 --- /dev/null +++ b/test/tox.centos_10.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 _centos_10.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 diff --git a/test/tox.debian_13.ini b/test/tox.debian_13.ini new file mode 100644 index 00000000..dcfbf816 --- /dev/null +++ b/test/tox.debian_13.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 _debian_13.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_42.ini b/test/tox.fedora_42.ini new file mode 100644 index 00000000..67eb77e4 --- /dev/null +++ b/test/tox.fedora_42.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_42.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 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