diff --git a/.codespellignore b/.codespellignore new file mode 100644 index 0000000000..7f391ce767 --- /dev/null +++ b/.codespellignore @@ -0,0 +1,4 @@ +doubleclick +wan +nwe +reenable diff --git a/.editorconfig b/.editorconfig index a50f2f70ba..714ed210b9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,26 +13,8 @@ tab_width = 4 charset = utf-8 trim_trailing_whitespace = true -# Matches multiple files with brace expansion notation -# Set default charset -[*.{js,py}] -charset = utf-8 - -# 4 space indentation -[*.py] -indent_style = space -indent_size = 4 - -# Tab indentation (no size specified) -[Makefile] -indent_style = tab +[*.yml] +tab_width = 2 -# Indentation override for all JS under lib directory -[scripts/**.js] -indent_style = space -indent_size = 2 - -# Matches the exact files either package.json or .travis.yml -[{package.json,.travis.yml}] -indent_style = space -indent_size = 2 +[*.md] +tab_width = 2 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e10beb30f8..20163f5eb9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,4 +7,37 @@ updates: day: saturday time: "10:00" open-pull-requests-limit: 10 - target-branch: developement \ No newline at end of file + target-branch: development + reviewers: + - "pi-hole/core-maintainers" +- package-ecosystem: pip + directory: "/test" + schedule: + interval: weekly + day: saturday + time: "10:00" + open-pull-requests-limit: 10 + target-branch: development + reviewers: + - "pi-hole/core-maintainers" +# As above, but for development-v6 +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + day: saturday + time: "10:00" + open-pull-requests-limit: 10 + target-branch: development-v6 + reviewers: + - "pi-hole/core-maintainers" +- package-ecosystem: pip + directory: "/test" + schedule: + interval: weekly + day: saturday + time: "10:00" + open-pull-requests-limit: 10 + target-branch: development-v6 + reviewers: + - "pi-hole/core-maintainers" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a4f67b81f4..9cfd8a61d6 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@v2 + uses: actions/checkout@v4.1.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v3 with: languages: 'python' - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/merge-conflict.yml b/.github/workflows/merge-conflict.yml new file mode 100644 index 0000000000..d86e9cd189 --- /dev/null +++ b/.github/workflows/merge-conflict.yml @@ -0,0 +1,21 @@ +name: "Check for merge conflicts" +on: + # So that PRs touching the same files as the push are updated + push: + # So that the `dirtyLabel` is removed if conflicts are resolve + # We recommend `pull_request_target` so that github secrets are available. + # In `pull_request` we wouldn't be able to change labels of fork PRs + pull_request_target: + types: [synchronize] + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Check if PRs are have merge conflicts + uses: eps1lon/actions-label-merge-conflict@v2.1.0 + with: + dirtyLabel: "PR: Merge Conflict" + repoToken: "${{ secrets.GITHUB_TOKEN }}" + commentOnDirty: "This pull request has conflicts, please resolve those before we can evaluate the pull request." + commentOnClean: "Conflicts have been resolved." diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 783f141967..c6a581ffde 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -2,24 +2,47 @@ name: Mark stale issues on: schedule: - - cron: '0 * * * *' + - cron: '0 8 * * *' workflow_dispatch: + issue_comment: -jobs: - stale: +env: + stale_label: stale +jobs: + stale_action: + if: github.event_name != 'issue_comment' runs-on: ubuntu-latest permissions: issues: write steps: - - uses: actions/stale@v4 - 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: 'stale' - exempt-issue-labels: 'Internal, Fixed in next release, Bug: Confirmed, Documentation Needed' - exempt-all-issue-assignees: true - operations-per-run: 300 + - uses: actions/stale@v9.0.0 + 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-all-issue-assignees: true + operations-per-run: 300 + close-issue-reason: 'not_planned' + + remove_stale: + # trigger "stale" removal immediately when stale issues are commented on + # we need to explicitly check that the trigger does not run on comment on a PR as + # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment-on-issues-only-or-pull-requests-only + if: ${{ !github.event.issue.pull_request && github.event_name != 'schedule' }} + permissions: + contents: read # for actions/checkout + issues: write # to edit issues label + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4.1.2 + - name: Remove 'stale' label + run: gh issue edit ${{ github.event.issue.number }} --remove-label ${{ env.stale_label }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + diff --git a/.github/workflows/stale_pr.yml b/.github/workflows/stale_pr.yml new file mode 100644 index 0000000000..9665081899 --- /dev/null +++ b/.github/workflows/stale_pr.yml @@ -0,0 +1,35 @@ +name: Close stale PR +# This action will add a `stale` label and close immediately every PR that meets the following conditions: +# - it is already marked with "merge conflict" label +# - there was no update/comment on the PR in the last 30 days. + +on: + schedule: + - cron: '0 10 * * *' + workflow_dispatch: + +jobs: + stale: + + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/stale@v9.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + # Do not automatically mark PR/issue as stale + days-before-stale: -1 + # Override 'days-before-stale' for PR only + days-before-pr-stale: 30 + # Close PRs immediately, after marking them 'stale' + days-before-pr-close: 0 + # only run the action on merge conflict PR + any-of-labels: 'PR: Merge Conflict' + exempt-pr-labels: 'internal, never-stale, ON HOLD, WIP' + exempt-all-pr-assignees: true + operations-per-run: 300 + stale-pr-message: '' + close-pr-message: 'Existing merge conflicts have not been addressed. This PR is considered abandoned.' diff --git a/.github/workflows/sync-back-to-dev.yml b/.github/workflows/sync-back-to-dev.yml index 5b9fa570e3..9b35a974d1 100644 --- a/.github/workflows/sync-back-to-dev.yml +++ b/.github/workflows/sync-back-to-dev.yml @@ -5,23 +5,36 @@ on: branches: - master +# The section is needed to drop the default write-all permissions for all jobs +# that are granted on `push` event. By specifying any permission explicitly +# all others are set to none. By using the principle of least privilege the damage a compromised +# workflow can do (because of an injection or compromised third party tool or +# action) is restricted. Adding labels to issues, commenting +# on pull-requests, etc. may need additional permissions: +# +# Syntax for this section: +# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions +# +# Reference for how to assign permissions on a job-by-job basis: +# https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs +# +# Reference for available permissions that we can enable if needed: +# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token +permissions: {} + jobs: sync-branches: + # The job needs to be able to pull the code and create a pull request. + permissions: + contents: read # for actions/checkout + pull-requests: write # to create pull request + runs-on: ubuntu-latest name: Syncing branches steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4.1.2 - name: Opening pull request - id: pull - uses: tretuna/sync-branches@1.4.0 - with: + run: gh pr create -B development -H master --title 'Sync master back into development' --body 'Created by Github action' --label 'internal' + env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - FROM_BRANCH: 'master' - TO_BRANCH: 'development' - - name: Label the pull request to ignore for release note generation - uses: actions-ecosystem/action-add-labels@v1 - with: - labels: internal - repo: ${{ github.repository }} - number: ${{ steps.pull.outputs.PULL_REQUEST_NUMBER }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d39852dc2b..d2282d2d62 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,44 +8,74 @@ permissions: contents: read jobs: - smoke-test: + smoke-tests: if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Run Smoke Tests - run: | - # Ensure scripts in repository are executable - IFS=$'\n'; - for f in $(find . -name '*.sh'); do if [[ ! -x $f ]]; then echo "$f is not executable" && FAIL=1; fi ;done - unset IFS; - # If FAIL is 1 then we fail. - [[ $FAIL == 1 ]] && exit 1 || echo "Smoke Tests Passed" + - name: Checkout repository + uses: actions/checkout@v4.1.2 + + - name: Check scripts in repository are executable + run: | + IFS=$'\n'; + for f in $(find . -name '*.sh'); do if [[ ! -x $f ]]; then echo "$f is not executable" && FAIL=1; fi ;done + unset IFS; + # If FAIL is 1 then we fail. + [[ $FAIL == 1 ]] && exit 1 || echo "Scripts are executable!" + + - name: Spell-Checking + uses: codespell-project/actions-codespell@master + 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 + + - name: Run editorconfig-checker + run: editorconfig-checker + + - name: Check python code formatting with black + uses: psf/black@stable + with: + src: "./test" + options: "--check --diff --color" distro-test: if: github.event.pull_request.draft == false runs-on: ubuntu-latest - needs: smoke-test + needs: smoke-tests strategy: + fail-fast: false matrix: - distro: [debian_9, debian_10, debian_11, ubuntu_16, ubuntu_18, ubuntu_20, ubuntu_21, centos_7, centos_8, fedora_33, fedora_34] + distro: + [ + debian_10, + debian_11, + debian_12, + ubuntu_20, + ubuntu_22, + ubuntu_23, + centos_8, + centos_9, + fedora_38, + fedora_39, + ] env: DISTRO: ${{matrix.distro}} steps: - - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Set up Python 3.8 - uses: actions/setup-python@v3 - with: - python-version: 3.8 - - - name: Install dependencies - run: pip install -r test/requirements.txt - - - name: Test with tox - run: tox -c test/tox.${DISTRO}.ini + - name: Checkout repository + uses: actions/checkout@v4.1.2 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5.0.0 + with: + python-version: "3.10" + + - name: Install wheel + run: pip install wheel + + - name: Install dependencies + run: pip install -r test/requirements.txt + + - name: Test with tox + run: tox -c test/tox.${DISTRO}.ini diff --git a/.stickler.yml b/.stickler.yml index 8a2a1ce991..5fdbbf1ec1 100644 --- a/.stickler.yml +++ b/.stickler.yml @@ -1,6 +1,10 @@ +--- linters: shellcheck: shell: bash phpcs: flake8: max-line-length: 120 + yamllint: + config: ./.yamllint.conf + remarklint: diff --git a/.yamllint.conf b/.yamllint.conf new file mode 100644 index 0000000000..d1b0953bdf --- /dev/null +++ b/.yamllint.conf @@ -0,0 +1,3 @@ +rules: + line-length: disable + document-start: disable diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 018b8c5f51..1ea98df296 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,5 +3,3 @@ Please read and understand the contribution guide before creating an issue or pull request. The guide can be found here: [https://docs.pi-hole.net/guides/github/contributing/](https://docs.pi-hole.net/guides/github/contributing/) - - diff --git a/README.md b/README.md index 8ec737123e..481c59d426 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,17 @@ -## This project is part of +
-https://github.com/arevindh/pihole-speedtest +# Pi-hole Speedtest -## About the project +[![Join the chat at https://gitter.im/pihole-speedtest/community](https://badges.gitter.im/pihole-speedtest/community.svg)](https://gitter.im/pihole-speedtest/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/TW9TfyM) -This project is just another fun project integrating speedtest to PiHole Web UI. +Test your connection speed directly in the Pi-hole web interface! -It will be using speedtest.net on background for testing. More frequent the speed tests more data will used. +
-What does this mod have in extra ? +--- -1. Speedtest results of 1/2/4/7/30 days as graph. -2. Custom speed test server selection. -3. Detailed speedtest results page. -4. Ability to schedule speedtest interval. - -## Wiki - -Wiki is available here https://github.com/arevindh/pihole-speedtest/wiki +Please go to the [main repository](https://github.com/arevindh/pihole-speedtest) for more information, including (un)installation instructions, pull requests, and issues. ## Disclaimer -We are not affiliated or endorced by [Pi-hole](https://github.com/pi-hole/AdminLTE) - -## Use Official CLI Mode for best results. - -[Uninstall Instructions](https://github.com/arevindh/pihole-speedtest/wiki/Uninstalling-Speedtest-Mod) \ No newline at end of file +We are not affiliated with or endorsed by [Pi-hole](https://github.com/pi-hole/pi-hole) diff --git a/advanced/01-pihole.conf b/advanced/01-pihole.conf index 02bc93bf35..677910f654 100644 --- a/advanced/01-pihole.conf +++ b/advanced/01-pihole.conf @@ -29,14 +29,7 @@ bogus-priv no-resolv -server=@DNS1@ -server=@DNS2@ - -interface=@INT@ - -cache-size=@CACHE_SIZE@ - log-queries -log-facility=/var/log/pihole.log +log-facility=/var/log/pihole/pihole.log log-async diff --git a/advanced/GIFs/25Bytes.gif b/advanced/GIFs/25Bytes.gif deleted file mode 100644 index 472727f293..0000000000 Binary files a/advanced/GIFs/25Bytes.gif and /dev/null differ diff --git a/advanced/GIFs/26Bytes.gif b/advanced/GIFs/26Bytes.gif deleted file mode 100644 index 264e471abd..0000000000 Binary files a/advanced/GIFs/26Bytes.gif and /dev/null differ diff --git a/advanced/GIFs/37Bytes.gif b/advanced/GIFs/37Bytes.gif deleted file mode 100644 index b3aa80d843..0000000000 Binary files a/advanced/GIFs/37Bytes.gif and /dev/null differ diff --git a/advanced/GIFs/43Bytes.gif b/advanced/GIFs/43Bytes.gif deleted file mode 100644 index 9884f476b9..0000000000 Binary files a/advanced/GIFs/43Bytes.gif and /dev/null differ diff --git a/advanced/Scripts/chronometer.sh b/advanced/Scripts/chronometer.sh index fddb393677..bb94a857d2 100755 --- a/advanced/Scripts/chronometer.sh +++ b/advanced/Scripts/chronometer.sh @@ -14,7 +14,9 @@ LC_NUMERIC=C # Retrieve stats from FTL engine pihole-FTL() { local ftl_port LINE - ftl_port=$(cat /run/pihole-FTL.port 2> /dev/null) + # shellcheck disable=SC1091 + . /opt/pihole/utils.sh + ftl_port=$(getFTLAPIPort) if [[ -n "$ftl_port" ]]; then # Open connection to FTL exec 3<>"/dev/tcp/127.0.0.1/$ftl_port" @@ -231,7 +233,7 @@ get_sys_stats() { if [[ -n "${ph_ver_raw[0]}" ]]; then ph_core_ver="${ph_ver_raw[0]}" if [[ ${#ph_ver_raw[@]} -eq 2 ]]; then - # AdminLTE not installed + # web not installed ph_lte_ver="(not installed)" ph_ftl_ver="${ph_ver_raw[1]}" else @@ -503,11 +505,11 @@ chronoFunc() { fi printFunc " Pi-hole: " "$ph_status" "$ph_info" - printFunc " Ads Today: " "$ads_percentage_today%" "$ads_info" + printFunc " Blocked: " "$ads_percentage_today%" "$ads_info" printFunc "Local Qrys: " "$queries_cached_percentage%" "$dns_info" - printFunc " Blocked: " "$recent_blocked" - printFunc "Top Advert: " "$top_ad" + printFunc "Last Block: " "$recent_blocked" + printFunc " Top Block: " "$top_ad" # Provide more stats on screens with more lines if [[ "$scr_lines" -eq 17 ]]; then diff --git a/advanced/Scripts/database_migration/gravity-db.sh b/advanced/Scripts/database_migration/gravity-db.sh index a7ba60a919..1459ecd9b7 100755 --- a/advanced/Scripts/database_migration/gravity-db.sh +++ b/advanced/Scripts/database_migration/gravity-db.sh @@ -19,13 +19,13 @@ upgrade_gravityDB(){ auditFile="${piholeDir}/auditlog.list" # Get database version - version="$(pihole-FTL sqlite3 "${database}" "SELECT \"value\" FROM \"info\" WHERE \"property\" = 'version';")" + version="$(pihole-FTL sqlite3 -ni "${database}" "SELECT \"value\" FROM \"info\" WHERE \"property\" = 'version';")" if [[ "$version" == "1" ]]; then # This migration script upgrades the gravity.db file by # adding the domain_audit table echo -e " ${INFO} Upgrading gravity database from version 1 to 2" - pihole-FTL sqlite3 "${database}" < "${scriptPath}/1_to_2.sql" + pihole-FTL sqlite3 -ni "${database}" < "${scriptPath}/1_to_2.sql" version=2 # Store audit domains in database table @@ -40,28 +40,28 @@ upgrade_gravityDB(){ # renaming the regex table to regex_blacklist, and # creating a new regex_whitelist table + corresponding linking table and views echo -e " ${INFO} Upgrading gravity database from version 2 to 3" - pihole-FTL sqlite3 "${database}" < "${scriptPath}/2_to_3.sql" + pihole-FTL sqlite3 -ni "${database}" < "${scriptPath}/2_to_3.sql" version=3 fi if [[ "$version" == "3" ]]; then # This migration script unifies the formally separated domain # lists into a single table with a UNIQUE domain constraint echo -e " ${INFO} Upgrading gravity database from version 3 to 4" - pihole-FTL sqlite3 "${database}" < "${scriptPath}/3_to_4.sql" + pihole-FTL sqlite3 -ni "${database}" < "${scriptPath}/3_to_4.sql" version=4 fi if [[ "$version" == "4" ]]; then # This migration script upgrades the gravity and list views # implementing necessary changes for per-client blocking echo -e " ${INFO} Upgrading gravity database from version 4 to 5" - pihole-FTL sqlite3 "${database}" < "${scriptPath}/4_to_5.sql" + pihole-FTL sqlite3 -ni "${database}" < "${scriptPath}/4_to_5.sql" version=5 fi if [[ "$version" == "5" ]]; then # This migration script upgrades the adlist view # to return an ID used in gravity.sh echo -e " ${INFO} Upgrading gravity database from version 5 to 6" - pihole-FTL sqlite3 "${database}" < "${scriptPath}/5_to_6.sql" + pihole-FTL sqlite3 -ni "${database}" < "${scriptPath}/5_to_6.sql" version=6 fi if [[ "$version" == "6" ]]; then @@ -69,7 +69,7 @@ upgrade_gravityDB(){ # which is automatically associated to all clients not # having their own group assignments echo -e " ${INFO} Upgrading gravity database from version 6 to 7" - pihole-FTL sqlite3 "${database}" < "${scriptPath}/6_to_7.sql" + pihole-FTL sqlite3 -ni "${database}" < "${scriptPath}/6_to_7.sql" version=7 fi if [[ "$version" == "7" ]]; then @@ -77,21 +77,21 @@ upgrade_gravityDB(){ # to ensure uniqueness on the group name # We also add date_added and date_modified columns echo -e " ${INFO} Upgrading gravity database from version 7 to 8" - pihole-FTL sqlite3 "${database}" < "${scriptPath}/7_to_8.sql" + pihole-FTL sqlite3 -ni "${database}" < "${scriptPath}/7_to_8.sql" version=8 fi if [[ "$version" == "8" ]]; then # This migration fixes some issues that were introduced # in the previous migration script. echo -e " ${INFO} Upgrading gravity database from version 8 to 9" - pihole-FTL sqlite3 "${database}" < "${scriptPath}/8_to_9.sql" + pihole-FTL sqlite3 -ni "${database}" < "${scriptPath}/8_to_9.sql" version=9 fi if [[ "$version" == "9" ]]; then # This migration drops unused tables and creates triggers to remove # obsolete groups assignments when the linked items are deleted echo -e " ${INFO} Upgrading gravity database from version 9 to 10" - pihole-FTL sqlite3 "${database}" < "${scriptPath}/9_to_10.sql" + pihole-FTL sqlite3 -ni "${database}" < "${scriptPath}/9_to_10.sql" version=10 fi if [[ "$version" == "10" ]]; then @@ -101,31 +101,31 @@ upgrade_gravityDB(){ # to keep the copying process generic (needs the same columns in both the # source and the destination databases). echo -e " ${INFO} Upgrading gravity database from version 10 to 11" - pihole-FTL sqlite3 "${database}" < "${scriptPath}/10_to_11.sql" + pihole-FTL sqlite3 -ni "${database}" < "${scriptPath}/10_to_11.sql" version=11 fi if [[ "$version" == "11" ]]; then # Rename group 0 from "Unassociated" to "Default" echo -e " ${INFO} Upgrading gravity database from version 11 to 12" - pihole-FTL sqlite3 "${database}" < "${scriptPath}/11_to_12.sql" + pihole-FTL sqlite3 -ni "${database}" < "${scriptPath}/11_to_12.sql" version=12 fi if [[ "$version" == "12" ]]; then # Add column date_updated to adlist table echo -e " ${INFO} Upgrading gravity database from version 12 to 13" - pihole-FTL sqlite3 "${database}" < "${scriptPath}/12_to_13.sql" + pihole-FTL sqlite3 -ni "${database}" < "${scriptPath}/12_to_13.sql" version=13 fi if [[ "$version" == "13" ]]; then # Add columns number and status to adlist table echo -e " ${INFO} Upgrading gravity database from version 13 to 14" - pihole-FTL sqlite3 "${database}" < "${scriptPath}/13_to_14.sql" + pihole-FTL sqlite3 -ni "${database}" < "${scriptPath}/13_to_14.sql" version=14 fi if [[ "$version" == "14" ]]; then # Changes the vw_adlist created in 5_to_6 echo -e " ${INFO} Upgrading gravity database from version 14 to 15" - pihole-FTL sqlite3 "${database}" < "${scriptPath}/14_to_15.sql" + pihole-FTL sqlite3 -ni "${database}" < "${scriptPath}/14_to_15.sql" version=15 fi } diff --git a/advanced/Scripts/database_migration/gravity/11_to_12.sql b/advanced/Scripts/database_migration/gravity/11_to_12.sql index 45fbc8451a..d480d46efc 100644 --- a/advanced/Scripts/database_migration/gravity/11_to_12.sql +++ b/advanced/Scripts/database_migration/gravity/11_to_12.sql @@ -16,4 +16,4 @@ CREATE TRIGGER tr_group_zero AFTER DELETE ON "group" UPDATE info SET value = 12 WHERE property = 'version'; -COMMIT; \ No newline at end of file +COMMIT; diff --git a/advanced/Scripts/database_migration/gravity/12_to_13.sql b/advanced/Scripts/database_migration/gravity/12_to_13.sql index d16791d60a..7d85cb05e2 100644 --- a/advanced/Scripts/database_migration/gravity/12_to_13.sql +++ b/advanced/Scripts/database_migration/gravity/12_to_13.sql @@ -15,4 +15,4 @@ CREATE TRIGGER tr_adlist_update AFTER UPDATE OF address,enabled,comment ON adlis UPDATE info SET value = 13 WHERE property = 'version'; -COMMIT; \ No newline at end of file +COMMIT; diff --git a/advanced/Scripts/database_migration/gravity/3_to_4.sql b/advanced/Scripts/database_migration/gravity/3_to_4.sql index 352b1baae3..05231f7299 100644 --- a/advanced/Scripts/database_migration/gravity/3_to_4.sql +++ b/advanced/Scripts/database_migration/gravity/3_to_4.sql @@ -93,4 +93,4 @@ CREATE VIEW vw_regex_blacklist AS SELECT domain, domainlist.id AS id, domainlist UPDATE info SET value = 4 WHERE property = 'version'; -COMMIT; \ No newline at end of file +COMMIT; diff --git a/advanced/Scripts/database_migration/gravity/4_to_5.sql b/advanced/Scripts/database_migration/gravity/4_to_5.sql index 2ad906fc12..4ae9f980fb 100644 --- a/advanced/Scripts/database_migration/gravity/4_to_5.sql +++ b/advanced/Scripts/database_migration/gravity/4_to_5.sql @@ -35,4 +35,4 @@ CREATE TABLE client_by_group UPDATE info SET value = 5 WHERE property = 'version'; -COMMIT; \ No newline at end of file +COMMIT; diff --git a/advanced/Scripts/list.sh b/advanced/Scripts/list.sh index f3f97da26f..76558e5827 100755 --- a/advanced/Scripts/list.sh +++ b/advanced/Scripts/list.sh @@ -100,21 +100,29 @@ Options: ValidateDomain() { # Convert to lowercase domain="${1,,}" + local str validDomain # Check validity of domain (don't check for regex entries) - if [[ "${#domain}" -le 253 ]]; then - if [[ ( "${typeId}" == "${regex_blacklist}" || "${typeId}" == "${regex_whitelist}" ) && "${wildcard}" == false ]]; then - validDomain="${domain}" - else + if [[ ( "${typeId}" == "${regex_blacklist}" || "${typeId}" == "${regex_whitelist}" ) && "${wildcard}" == false ]]; then + validDomain="${domain}" + else + # Check max length + if [[ "${#domain}" -le 253 ]]; then validDomain=$(grep -P "^((-|_)*[a-z\\d]((-|_)*[a-z\\d])*(-|_)*)(\\.(-|_)*([a-z\\d]((-|_)*[a-z\\d])*))*$" <<< "${domain}") # Valid chars check validDomain=$(grep -P "^[^\\.]{1,63}(\\.[^\\.]{1,63})*$" <<< "${validDomain}") # Length of each label + # set error string + str="is not a valid argument or domain name!" + else + validDomain= + str="is too long!" + fi fi if [[ -n "${validDomain}" ]]; then domList=("${domList[@]}" "${validDomain}") else - echo -e " ${CROSS} ${domain} is not a valid argument or domain name!" + echo -e " ${CROSS} ${domain} ${str}" fi domaincount=$((domaincount+1)) @@ -142,18 +150,18 @@ AddDomain() { domain="$1" # Is the domain in the list we want to add it to? - num="$(pihole-FTL sqlite3 "${gravityDBfile}" "SELECT COUNT(*) FROM domainlist WHERE domain = '${domain}';")" + num="$(pihole-FTL sqlite3 -ni "${gravityDBfile}" "SELECT COUNT(*) FROM domainlist WHERE domain = '${domain}';")" requestedListname="$(GetListnameFromTypeId "${typeId}")" if [[ "${num}" -ne 0 ]]; then - existingTypeId="$(pihole-FTL sqlite3 "${gravityDBfile}" "SELECT type FROM domainlist WHERE domain = '${domain}';")" + existingTypeId="$(pihole-FTL sqlite3 -ni "${gravityDBfile}" "SELECT type FROM domainlist WHERE domain = '${domain}';")" if [[ "${existingTypeId}" == "${typeId}" ]]; then if [[ "${verbose}" == true ]]; then echo -e " ${INFO} ${1} already exists in ${requestedListname}, no need to add!" fi else existingListname="$(GetListnameFromTypeId "${existingTypeId}")" - pihole-FTL sqlite3 "${gravityDBfile}" "UPDATE domainlist SET type = ${typeId} WHERE domain='${domain}';" + pihole-FTL sqlite3 -ni "${gravityDBfile}" "UPDATE domainlist SET type = ${typeId} WHERE domain='${domain}';" if [[ "${verbose}" == true ]]; then echo -e " ${INFO} ${1} already exists in ${existingListname}, it has been moved to ${requestedListname}!" fi @@ -169,10 +177,10 @@ AddDomain() { # Insert only the domain here. The enabled and date_added fields will be filled # with their default values (enabled = true, date_added = current timestamp) if [[ -z "${comment}" ]]; then - pihole-FTL sqlite3 "${gravityDBfile}" "INSERT INTO domainlist (domain,type) VALUES ('${domain}',${typeId});" + pihole-FTL sqlite3 -ni "${gravityDBfile}" "INSERT INTO domainlist (domain,type) VALUES ('${domain}',${typeId});" else # also add comment when variable has been set through the "--comment" option - pihole-FTL sqlite3 "${gravityDBfile}" "INSERT INTO domainlist (domain,type,comment) VALUES ('${domain}',${typeId},'${comment}');" + pihole-FTL sqlite3 -ni "${gravityDBfile}" "INSERT INTO domainlist (domain,type,comment) VALUES ('${domain}',${typeId},'${comment}');" fi } @@ -181,7 +189,7 @@ RemoveDomain() { domain="$1" # Is the domain in the list we want to remove it from? - num="$(pihole-FTL sqlite3 "${gravityDBfile}" "SELECT COUNT(*) FROM domainlist WHERE domain = '${domain}' AND type = ${typeId};")" + num="$(pihole-FTL sqlite3 -ni "${gravityDBfile}" "SELECT COUNT(*) FROM domainlist WHERE domain = '${domain}' AND type = ${typeId};")" requestedListname="$(GetListnameFromTypeId "${typeId}")" @@ -198,14 +206,14 @@ RemoveDomain() { fi reload=true # Remove it from the current list - pihole-FTL sqlite3 "${gravityDBfile}" "DELETE FROM domainlist WHERE domain = '${domain}' AND type = ${typeId};" + pihole-FTL sqlite3 -ni "${gravityDBfile}" "DELETE FROM domainlist WHERE domain = '${domain}' AND type = ${typeId};" } Displaylist() { local count num_pipes domain enabled status nicedate requestedListname requestedListname="$(GetListnameFromTypeId "${typeId}")" - data="$(pihole-FTL sqlite3 "${gravityDBfile}" "SELECT domain,enabled,date_modified FROM domainlist WHERE type = ${typeId};" 2> /dev/null)" + data="$(pihole-FTL sqlite3 -ni "${gravityDBfile}" "SELECT domain,enabled,date_modified FROM domainlist WHERE type = ${typeId};" 2> /dev/null)" if [[ -z $data ]]; then echo -e "Not showing empty list" @@ -243,10 +251,10 @@ Displaylist() { } NukeList() { - count=$(pihole-FTL sqlite3 "${gravityDBfile}" "SELECT COUNT(1) FROM domainlist WHERE type = ${typeId};") + count=$(pihole-FTL sqlite3 -ni "${gravityDBfile}" "SELECT COUNT(1) FROM domainlist WHERE type = ${typeId};") listname="$(GetListnameFromTypeId "${typeId}")" if [ "$count" -gt 0 ];then - pihole-FTL sqlite3 "${gravityDBfile}" "DELETE FROM domainlist WHERE type = ${typeId};" + pihole-FTL sqlite3 -ni "${gravityDBfile}" "DELETE FROM domainlist WHERE type = ${typeId};" echo " ${TICK} Removed ${count} domain(s) from the ${listname}" else echo " ${INFO} ${listname} already empty. Nothing to do!" diff --git a/advanced/Scripts/piholeARPTable.sh b/advanced/Scripts/piholeARPTable.sh index 5daa025d49..b92dd1242c 100755 --- a/advanced/Scripts/piholeARPTable.sh +++ b/advanced/Scripts/piholeARPTable.sh @@ -39,7 +39,7 @@ flushARP(){ # 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 "${DBFILE}" "DELETE FROM network_addresses" 2>&1); then + 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}" @@ -47,7 +47,7 @@ flushARP(){ fi # Truncate network table in pihole-FTL.db - if ! output=$(pihole-FTL sqlite3 "${DBFILE}" "DELETE FROM network" 2>&1); then + 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}" diff --git a/advanced/Scripts/piholeCheckout.sh b/advanced/Scripts/piholeCheckout.sh index 4c0a4f4042..cf57800c4c 100755 --- a/advanced/Scripts/piholeCheckout.sh +++ b/advanced/Scripts/piholeCheckout.sh @@ -9,7 +9,7 @@ # Please see LICENSE file for your rights under this license. readonly PI_HOLE_FILES_DIR="/etc/.pihole" -PH_TEST="true" +SKIP_INSTALL="true" source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" # webInterfaceGitUrl set in basic-install.sh @@ -164,6 +164,8 @@ checkout() { exit 1 fi checkout_pull_branch "${webInterfaceDir}" "${2}" + # Update local and remote versions via updatechecker + /opt/pihole/updatecheck.sh elif [[ "${1}" == "ftl" ]] ; then local path local oldbranch @@ -178,6 +180,8 @@ checkout() { FTLinstall "${binary}" restart_service pihole-FTL enable_service pihole-FTL + # Update local and remote versions via updatechecker + /opt/pihole/updatecheck.sh else echo " ${CROSS} Requested branch \"${2}\" is not available" ftlbranches=( $(git ls-remote https://github.com/pi-hole/ftl | grep 'heads' | sed 's/refs\/heads\///;s/ //g' | awk '{print $2}') ) diff --git a/advanced/Scripts/piholeDebug.sh b/advanced/Scripts/piholeDebug.sh index 844cbd237c..143ff9d76a 100755 --- a/advanced/Scripts/piholeDebug.sh +++ b/advanced/Scripts/piholeDebug.sh @@ -41,18 +41,15 @@ else #OVER="\r\033[K" fi -OBFUSCATED_PLACEHOLDER="" +# shellcheck disable=SC1091 +. /etc/pihole/versions # FAQ URLs for use in showing the debug log -FAQ_UPDATE_PI_HOLE="${COL_CYAN}https://discourse.pi-hole.net/t/how-do-i-update-pi-hole/249${COL_NC}" -FAQ_CHECKOUT_COMMAND="${COL_CYAN}https://discourse.pi-hole.net/t/the-pihole-command-with-examples/738#checkout${COL_NC}" FAQ_HARDWARE_REQUIREMENTS="${COL_CYAN}https://docs.pi-hole.net/main/prerequisites/${COL_NC}" FAQ_HARDWARE_REQUIREMENTS_PORTS="${COL_CYAN}https://docs.pi-hole.net/main/prerequisites/#ports${COL_NC}" FAQ_HARDWARE_REQUIREMENTS_FIREWALLD="${COL_CYAN}https://docs.pi-hole.net/main/prerequisites/#firewalld${COL_NC}" FAQ_GATEWAY="${COL_CYAN}https://discourse.pi-hole.net/t/why-is-a-default-gateway-important-for-pi-hole/3546${COL_NC}" -FAQ_ULA="${COL_CYAN}https://discourse.pi-hole.net/t/use-ipv6-ula-addresses-for-pi-hole/2127${COL_NC}" FAQ_FTL_COMPATIBILITY="${COL_CYAN}https://github.com/pi-hole/FTL#compatibility-list${COL_NC}" -FAQ_BAD_ADDRESS="${COL_CYAN}https://discourse.pi-hole.net/t/why-do-i-see-bad-address-at-in-pihole-log/3972${COL_NC}" # Other URLs we may use FORUMS_URL="${COL_CYAN}https://discourse.pi-hole.net${COL_NC}" @@ -66,14 +63,16 @@ PIHOLE_DIRECTORY="/etc/pihole" PIHOLE_SCRIPTS_DIRECTORY="/opt/pihole" BIN_DIRECTORY="/usr/local/bin" RUN_DIRECTORY="/run" -LOG_DIRECTORY="/var/log" -WEB_SERVER_LOG_DIRECTORY="${LOG_DIRECTORY}/lighttpd" +LOG_DIRECTORY="/var/log/pihole" +WEB_SERVER_LOG_DIRECTORY="/var/log/lighttpd" WEB_SERVER_CONFIG_DIRECTORY="/etc/lighttpd" +WEB_SERVER_CONFIG_DIRECTORY_FEDORA="${WEB_SERVER_CONFIG_DIRECTORY}/conf.d" +WEB_SERVER_CONFIG_DIRECTORY_DEBIAN="${WEB_SERVER_CONFIG_DIRECTORY}/conf-enabled" HTML_DIRECTORY="/var/www/html" WEB_GIT_DIRECTORY="${HTML_DIRECTORY}/admin" -#BLOCK_PAGE_DIRECTORY="${HTML_DIRECTORY}/pihole" SHM_DIRECTORY="/dev/shm" ETC="/etc" +SPEEDTEST_GIT_DIRECTORY="/etc/pihole-speedtest" # Files required by Pi-hole # https://discourse.pi-hole.net/t/what-files-does-pi-hole-use/1684 @@ -81,6 +80,8 @@ PIHOLE_CRON_FILE="${CRON_D_DIRECTORY}/pihole" WEB_SERVER_CONFIG_FILE="${WEB_SERVER_CONFIG_DIRECTORY}/lighttpd.conf" WEB_SERVER_CUSTOM_CONFIG_FILE="${WEB_SERVER_CONFIG_DIRECTORY}/external.conf" +WEB_SERVER_PIHOLE_CONFIG_FILE_DEBIAN="${WEB_SERVER_CONFIG_DIRECTORY_DEBIAN}/15-pihole-admin.conf" +WEB_SERVER_PIHOLE_CONFIG_FILE_FEDORA="${WEB_SERVER_CONFIG_DIRECTORY_FEDORA}/pihole-admin.conf" PIHOLE_INSTALL_LOG_FILE="${PIHOLE_DIRECTORY}/install.log" PIHOLE_RAW_BLOCKLIST_FILES="${PIHOLE_DIRECTORY}/list.*" @@ -89,6 +90,7 @@ PIHOLE_LOGROTATE_FILE="${PIHOLE_DIRECTORY}/logrotate" PIHOLE_SETUP_VARS_FILE="${PIHOLE_DIRECTORY}/setupVars.conf" PIHOLE_FTL_CONF_FILE="${PIHOLE_DIRECTORY}/pihole-FTL.conf" PIHOLE_CUSTOM_HOSTS_FILE="${PIHOLE_DIRECTORY}/custom.list" +PIHOLE_VERSIONS_FILE="${PIHOLE_DIRECTORY}/versions" # Read the value of an FTL config key. The value is printed to stdout. # @@ -124,45 +126,27 @@ PIHOLE_COMMAND="${BIN_DIRECTORY}/pihole" PIHOLE_COLTABLE_FILE="${BIN_DIRECTORY}/COL_TABLE" FTL_PID="${RUN_DIRECTORY}/pihole-FTL.pid" -FTL_PORT="${RUN_DIRECTORY}/pihole-FTL.port" PIHOLE_LOG="${LOG_DIRECTORY}/pihole.log" PIHOLE_LOG_GZIPS="${LOG_DIRECTORY}/pihole.log.[0-9].*" PIHOLE_DEBUG_LOG="${LOG_DIRECTORY}/pihole_debug.log" -PIHOLE_FTL_LOG="$(get_ftl_conf_value "LOGFILE" "${LOG_DIRECTORY}/pihole-FTL.log")" +PIHOLE_FTL_LOG="$(get_ftl_conf_value "LOGFILE" "${LOG_DIRECTORY}/FTL.log")" -PIHOLE_WEB_SERVER_ACCESS_LOG_FILE="${WEB_SERVER_LOG_DIRECTORY}/access.log" -PIHOLE_WEB_SERVER_ERROR_LOG_FILE="${WEB_SERVER_LOG_DIRECTORY}/error.log" +PIHOLE_WEB_SERVER_ACCESS_LOG_FILE="${WEB_SERVER_LOG_DIRECTORY}/access-pihole.log" +PIHOLE_WEB_SERVER_ERROR_LOG_FILE="${WEB_SERVER_LOG_DIRECTORY}/error-pihole.log" RESOLVCONF="${ETC}/resolv.conf" DNSMASQ_CONF="${ETC}/dnsmasq.conf" -# An array of operating system "pretty names" that we officially support -# We can loop through the array at any time to see if it matches a value -#SUPPORTED_OS=("Raspbian" "Ubuntu" "Fedora" "Debian" "CentOS") - # Store Pi-hole's processes in an array for easy use and parsing PIHOLE_PROCESSES=( "lighttpd" "pihole-FTL" ) -# Store the required directories in an array so it can be parsed through -#REQUIRED_DIRECTORIES=("${CORE_GIT_DIRECTORY}" -#"${CRON_D_DIRECTORY}" -#"${DNSMASQ_D_DIRECTORY}" -#"${PIHOLE_DIRECTORY}" -#"${PIHOLE_SCRIPTS_DIRECTORY}" -#"${BIN_DIRECTORY}" -#"${RUN_DIRECTORY}" -#"${LOG_DIRECTORY}" -#"${WEB_SERVER_LOG_DIRECTORY}" -#"${WEB_SERVER_CONFIG_DIRECTORY}" -#"${HTML_DIRECTORY}" -#"${WEB_GIT_DIRECTORY}" -#"${BLOCK_PAGE_DIRECTORY}") - # Store the required directories in an array so it can be parsed through REQUIRED_FILES=("${PIHOLE_CRON_FILE}" "${WEB_SERVER_CONFIG_FILE}" "${WEB_SERVER_CUSTOM_CONFIG_FILE}" +"${WEB_SERVER_PIHOLE_CONFIG_FILE_DEBIAN}" +"${WEB_SERVER_PIHOLE_CONFIG_FILE_FEDORA}" "${PIHOLE_INSTALL_LOG_FILE}" "${PIHOLE_RAW_BLOCKLIST_FILES}" "${PIHOLE_LOCAL_HOSTS_FILE}" @@ -172,7 +156,6 @@ REQUIRED_FILES=("${PIHOLE_CRON_FILE}" "${PIHOLE_COMMAND}" "${PIHOLE_COLTABLE_FILE}" "${FTL_PID}" -"${FTL_PORT}" "${PIHOLE_LOG}" "${PIHOLE_LOG_GZIPS}" "${PIHOLE_DEBUG_LOG}" @@ -181,7 +164,8 @@ REQUIRED_FILES=("${PIHOLE_CRON_FILE}" "${PIHOLE_WEB_SERVER_ERROR_LOG_FILE}" "${RESOLVCONF}" "${DNSMASQ_CONF}" -"${PIHOLE_CUSTOM_HOSTS_FILE}") +"${PIHOLE_CUSTOM_HOSTS_FILE}" +"${PIHOLE_VERSIONS_FILE}") DISCLAIMER="This process collects information from your Pi-hole, and optionally uploads it to a unique and random directory on tricorder.pi-hole.net. @@ -247,10 +231,8 @@ initialize_debug() { # This is a function for visually displaying the current test that is being run. # Accepts one variable: the name of what is being diagnosed -# Colors do not show in the dasboard, but the icons do: [i], [✓], and [✗] echo_current_diagnostic() { # Colors are used for visually distinguishing each test in the output - # These colors do not show in the GUI, but the formatting will log_write "\\n${COL_PURPLE}*** [ DIAGNOSING ]:${COL_NC} ${1}" } @@ -259,15 +241,7 @@ compare_local_version_to_git_version() { local git_dir="${1}" # The named component of the project (Core or Web) local pihole_component="${2}" - # If we are checking the Core versions, - if [[ "${pihole_component}" == "Core" ]]; then - # We need to search for "Pi-hole" when using pihole -v - local search_term="Pi-hole" - elif [[ "${pihole_component}" == "Web" ]]; then - # We need to search for "AdminLTE" so store it in a variable as well - #shellcheck disable=2034 - local search_term="AdminLTE" - fi + # Display what we are checking echo_current_diagnostic "${pihole_component} version" # Store the error message in a variable in case we want to change and/or reuse it @@ -280,43 +254,35 @@ compare_local_version_to_git_version() { log_write "${COL_RED}Could not cd into ${git_dir}$COL_NC" if git status &> /dev/null; then # The current version the user is on - local remote_version - remote_version=$(git describe --tags --abbrev=0); + local local_version + local_version=$(git describe --tags --abbrev=0); # What branch they are on - local remote_branch - remote_branch=$(git rev-parse --abbrev-ref HEAD); + local local_branch + local_branch=$(git rev-parse --abbrev-ref HEAD); # The commit they are on - local remote_commit - remote_commit=$(git describe --long --dirty --tags --always) + local local_commit + local_commit=$(git describe --long --dirty --tags --always) # Status of the repo local local_status local_status=$(git status -s) # echo this information out to the user in a nice format - # If the current version matches what pihole -v produces, the user is up-to-date - if [[ "${remote_version}" == "$(pihole -v | awk '/${search_term}/ {print $6}' | cut -d ')' -f1)" ]]; then - log_write "${TICK} ${pihole_component}: ${COL_GREEN}${remote_version}${COL_NC}" - # If not, - else - # echo the current version in yellow, signifying it's something to take a look at, but not a critical error - # Also add a URL to an FAQ - log_write "${INFO} ${pihole_component}: ${COL_YELLOW}${remote_version:-Untagged}${COL_NC} (${FAQ_UPDATE_PI_HOLE})" - fi + log_write "${TICK} Version: ${local_version}" # Print the repo upstreams remotes=$(git remote -v) log_write "${INFO} Remotes: ${remotes//$'\n'/'\n '}" # If the repo is on the master branch, they are on the stable codebase - if [[ "${remote_branch}" == "master" ]]; then + if [[ "${local_branch}" == "master" ]]; then # so the color of the text is green - log_write "${INFO} Branch: ${COL_GREEN}${remote_branch}${COL_NC}" + log_write "${INFO} Branch: ${COL_GREEN}${local_branch}${COL_NC}" # If it is any other branch, they are in a development branch else # So show that in yellow, signifying it's something to take a look at, but not a critical error - log_write "${INFO} Branch: ${COL_YELLOW}${remote_branch:-Detached}${COL_NC} (${FAQ_CHECKOUT_COMMAND})" + log_write "${INFO} Branch: ${COL_YELLOW}${local_branch:-Detached}${COL_NC}" fi # echo the current commit - log_write "${INFO} Commit: ${remote_commit}" + log_write "${INFO} Commit: ${local_commit}" # if `local_status` is non-null, then the repo is not clean, display details here if [[ ${local_status} ]]; then # Replace new lines in the status with 12 spaces to make the output cleaner @@ -350,18 +316,28 @@ compare_local_version_to_git_version() { } check_ftl_version() { - local ftl_name="FTL" - echo_current_diagnostic "${ftl_name} version" + local FTL_VERSION FTL_COMMIT FTL_BRANCH + echo_current_diagnostic "FTL version" # Use the built in command to check FTL's version FTL_VERSION=$(pihole-FTL version) - # Compare the current FTL version to the remote version - if [[ "${FTL_VERSION}" == "$(pihole -v | awk '/FTL/ {print $6}' | cut -d ')' -f1)" ]]; then - # If they are the same, FTL is up-to-date - log_write "${TICK} ${ftl_name}: ${COL_GREEN}${FTL_VERSION}${COL_NC}" + FTL_BRANCH=$(pihole-FTL branch) + FTL_COMMIT=$(pihole-FTL --hash) + + + log_write "${TICK} Version: ${FTL_VERSION}" + + # If they use the master branch, they are on the stable codebase + if [[ "${FTL_BRANCH}" == "master" ]]; then + # so the color of the text is green + log_write "${INFO} Branch: ${COL_GREEN}${FTL_BRANCH}${COL_NC}" + # If it is any other branch, they are in a development branch else - # If not, show it in yellow, signifying there is an update - log_write "${TICK} ${ftl_name}: ${COL_YELLOW}${FTL_VERSION}${COL_NC} (${FAQ_UPDATE_PI_HOLE})" + # So show that in yellow, signifying it's something to take a look at, but not a critical error + log_write "${INFO} Branch: ${COL_YELLOW}${FTL_BRANCH}${COL_NC}" fi + + # echo the current commit + log_write "${INFO} Commit: ${FTL_COMMIT}" } # Checks the core version of the Pi-hole codebase @@ -370,6 +346,8 @@ check_component_versions() { compare_local_version_to_git_version "${CORE_GIT_DIRECTORY}" "Core" # Check the Web version, branch, and commit compare_local_version_to_git_version "${WEB_GIT_DIRECTORY}" "Web" + # Check the Speedtest version, branch, and commit + compare_local_version_to_git_version "${SPEEDTEST_GIT_DIRECTORY}" "Speedtest" # Check the FTL version check_ftl_version } @@ -423,52 +401,64 @@ os_check() { # Extract dig response 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 [[ "${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 - - log_write "${INFO} dig return code: ${digReturnCode}" - log_write "${INFO} dig response: ${response}" + if [ "${digReturnCode}" -ne 0 ]; then + log_write "${INFO} Distro: ${detected_os^}" + log_write "${INFO} Version: ${detected_version}" + log_write "${CROSS} dig 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 [ "$valid_os" = true ]; then - log_write "${TICK} Distro: ${COL_GREEN}${detected_os^}${COL_NC}" + 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}" + 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} Version: ${COL_RED}${detected_version}${COL_NC}" - log_write "${CROSS} Error: ${COL_RED}${detected_os^} is supported but version ${detected_version} is currently unsupported (${FAQ_HARDWARE_REQUIREMENTS})${COL_NC}" + 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 - else - log_write "${CROSS} Distro: ${COL_RED}${detected_os^}${COL_NC}" - log_write "${CROSS} Error: ${COL_RED}${detected_os^} is not a supported distro (${FAQ_HARDWARE_REQUIREMENTS})${COL_NC}" + + # 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 re-use it) + # 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." # Display the current test that is running echo_current_diagnostic "Operating system" - # If the PIHOLE_DOCKER_TAG variable is set, include this information in the debug output - [ -n "${PIHOLE_DOCKER_TAG}" ] && log_write "${INFO} Pi-hole Docker Container: ${PIHOLE_DOCKER_TAG}" + # If DOCKER_VERSION is set (Sourced from /etc/pihole/versions at start of script), include this information in the debug output + [ -n "${DOCKER_VERSION}" ] && log_write "${INFO} Pi-hole Docker Container: ${DOCKER_VERSION}" # 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 @@ -600,10 +590,10 @@ disk_usage() { # Some lines of df might contain sensitive information like usernames and passwords. # E.g. curlftpfs filesystems (https://www.looklinux.com/mount-ftp-share-on-linux-using-curlftps/) # We are not interested in those lines so we collect keyword, to remove them from the output - # Additinal keywords can be added, separated by "|" + # Additional keywords can be added, separated by "|" hide="curlftpfs" - # only show those lines not containg a sensitive phrase + # only show those lines not containing a sensitive phrase for line in "${file_system[@]}"; do if [[ ! $line =~ $hide ]]; then log_write " ${line}" @@ -678,15 +668,20 @@ ping_gateway() { local protocol="${1}" ping_ipv4_or_ipv6 "${protocol}" # Check if we are using IPv4 or IPv6 - # Find the default gateway using IPv4 or IPv6 + # Find the default gateways using IPv4 or IPv6 local gateway - gateway="$(ip -"${protocol}" route | grep default | grep "${PIHOLE_INTERFACE}" | cut -d ' ' -f 3)" - # If the gateway variable has a value (meaning a gateway was found), - if [[ -n "${gateway}" ]]; then - log_write "${INFO} Default IPv${protocol} gateway: ${gateway}" + log_write "${INFO} Default IPv${protocol} gateway(s):" + + while IFS= read -r gateway; do + log_write " ${gateway}" + done < <(ip -"${protocol}" route | grep default | grep "${PIHOLE_INTERFACE}" | cut -d ' ' -f 3) + + gateway=$(ip -"${protocol}" route | grep default | grep "${PIHOLE_INTERFACE}" | cut -d ' ' -f 3 | head -n 1) + # If there was at least one gateway + if [ -n "${gateway}" ]; then # Let the user know we will ping the gateway for a response - log_write " * Pinging ${gateway}..." + log_write " * Pinging first gateway ${gateway}..." # Try to quietly ping the gateway 3 times, with a timeout of 3 seconds, using numeric output only, # on the pihole interface, and tail the last three lines of the output # If pinging the gateway is not successful, @@ -804,7 +799,7 @@ check_networking() { ping_gateway "6" # Skip the following check if installed in docker container. Unpriv'ed containers do not have access to the information required # to resolve the service name listening - and the container should not start if there was a port conflict anyway - [ -z "${PIHOLE_DOCKER_TAG}" ] && check_required_ports + [ -z "${DOCKER_VERSION}" ] && check_required_ports } check_x_headers() { @@ -814,39 +809,24 @@ check_x_headers() { # Similarly, it will show "X-Pi-hole: The Pi-hole Web interface is working!" if you view the header returned # when accessing the dashboard (i.e curl -I pi.hole/admin/) # server is operating correctly - echo_current_diagnostic "Dashboard and block page" + echo_current_diagnostic "Dashboard headers" # Use curl -I to get the header and parse out just the X-Pi-hole one - local block_page - block_page=$(curl -Is localhost | awk '/X-Pi-hole/' | tr -d '\r') - # Do it for the dashboard as well, as the header is different than above + local full_curl_output_dashboard local dashboard - dashboard=$(curl -Is localhost/admin/ | awk '/X-Pi-hole/' | tr -d '\r') + full_curl_output_dashboard="$(curl -Is localhost/admin/)" + dashboard=$(echo "${full_curl_output_dashboard}" | awk '/X-Pi-hole/' | tr -d '\r') # Store what the X-Header should be in variables for comparison later - local block_page_working - block_page_working="X-Pi-hole: A black hole for Internet advertisements." local dashboard_working dashboard_working="X-Pi-hole: The Pi-hole Web interface is working!" - local full_curl_output_block_page - full_curl_output_block_page="$(curl -Is localhost)" - local full_curl_output_dashboard - full_curl_output_dashboard="$(curl -Is localhost/admin/)" - # If the X-header found by curl matches what is should be, - if [[ $block_page == "$block_page_working" ]]; then - # display a success message - log_write "$TICK Block page X-Header: ${COL_GREEN}${block_page}${COL_NC}" - else - # Otherwise, show an error - log_write "$CROSS Block page X-Header: ${COL_RED}X-Header does not match or could not be retrieved.${COL_NC}" - log_write "${COL_RED}${full_curl_output_block_page}${COL_NC}" - fi - # Same logic applies to the dashboard as above, if the X-Header matches what a working system should have, + # If the X-Header matches what a working system should have, if [[ $dashboard == "$dashboard_working" ]]; then # then we can show a success log_write "$TICK Web interface X-Header: ${COL_GREEN}${dashboard}${COL_NC}" else # Otherwise, it's a failure since the X-Headers either don't exist or have been modified in some way log_write "$CROSS Web interface X-Header: ${COL_RED}X-Header does not match or could not be retrieved.${COL_NC}" + log_write "${COL_RED}${full_curl_output_dashboard}${COL_NC}" fi } @@ -884,11 +864,15 @@ dig_at() { local record_type="A" fi - # Find a random blocked url that has not been whitelisted. + # Find a random blocked url that has not been whitelisted 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 - random_url=$(pihole-FTL sqlite3 "${PIHOLE_GRAVITY_DB_FILE}" "SELECT domain FROM vw_gravity ORDER BY RANDOM() LIMIT 1") + random_url=$(pihole-FTL sqlite3 -ni "${PIHOLE_GRAVITY_DB_FILE}" "SELECT domain FROM vw_gravity WHERE domain not like '||%^' ORDER BY RANDOM() LIMIT 1") + # Fallback if no non-ABP style domains were found + if [ -z "${random_url}" ]; then + random_url="flurry.com" + fi # Next we need to check if Pi-hole can resolve a domain when the query is sent to it's IP address # This better emulates how clients will interact with Pi-hole as opposed to above where Pi-hole is @@ -964,10 +948,21 @@ process_status(){ else # Otherwise, use the service command and mock the output of `systemctl is-active` local status_of_process - if service "${i}" status | grep -E 'is\srunning' &> /dev/null; then - status_of_process="active" + + # If DOCKER_VERSION is set, the output is slightly different (s6 init system on Docker) + if [ -n "${DOCKER_VERSION}" ]; then + if service "${i}" status | grep -E '^up' &> /dev/null; then + status_of_process="active" + else + status_of_process="inactive" + fi else - status_of_process="inactive" + # non-Docker system + if service "${i}" status | grep -E 'is\srunning' &> /dev/null; then + status_of_process="active" + else + status_of_process="inactive" + fi fi fi # and print it out to the user @@ -993,6 +988,20 @@ ftl_full_status(){ fi } +lighttpd_test_configuration(){ + # let lighttpd test it's own configuration + local lighttpd_conf_test + echo_current_diagnostic "Lighttpd configuration test" + lighttpd_conf_test=$(lighttpd -tt -f /etc/lighttpd/lighttpd.conf) + if [ -z "${lighttpd_conf_test}" ]; then + # empty output + log_write "${TICK} ${COL_GREEN}No error in lighttpd configuration${COL_NC}" + else + log_write "${CROSS} ${COL_RED}Error in lighttpd configuration${COL_NC}" + log_write " ${lighttpd_conf_test}" + fi +} + make_array_from_file() { local filename="${1}" # The second argument can put a limit on how many line should be read from the file @@ -1009,7 +1018,7 @@ make_array_from_file() { else # Otherwise, read the file line by line while IFS= read -r line;do - # Othwerise, strip out comments and blank lines + # 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 @@ -1067,7 +1076,7 @@ parse_file() { } check_name_resolution() { - # Check name resolution from localhost, Pi-hole's IP, and Google's name severs + # Check name resolution from localhost, Pi-hole's IP, and Google's name servers # using the function we created earlier dig_at 4 dig_at 6 @@ -1085,10 +1094,13 @@ dir_check() { # check if exists first; if it does, if ls "${filename}" 1> /dev/null 2>&1; then # do nothing - : + true + return else # Otherwise, show an error log_write "${COL_RED}${directory} does not exist.${COL_NC}" + false + return fi done } @@ -1096,6 +1108,19 @@ dir_check() { list_files_in_dir() { # Set the first argument passed to this function as a named variable for better readability local dir_to_parse="${1}" + + # show files and sizes of some directories, don't print the file content (yet) + if [[ "${dir_to_parse}" == "${SHM_DIRECTORY}" ]]; then + # SHM file - we do not want to see the content, but we want to see the files and their sizes + log_write "$(ls -lh "${dir_to_parse}/")" + elif [[ "${dir_to_parse}" == "${WEB_SERVER_CONFIG_DIRECTORY_FEDORA}" ]]; then + # we want to see all files files in /etc/lighttpd/conf.d + log_write "$(ls -lh "${dir_to_parse}/" 2> /dev/null )" + elif [[ "${dir_to_parse}" == "${WEB_SERVER_CONFIG_DIRECTORY_DEBIAN}" ]]; then + # we want to see all files files in /etc/lighttpd/conf.d + log_write "$(ls -lh "${dir_to_parse}/"/ 2> /dev/null )" + fi + # Store the files found in an array mapfile -t files_found < <(ls "${dir_to_parse}") # For each file in the array, @@ -1111,11 +1136,8 @@ list_files_in_dir() { [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_WEB_SERVER_ACCESS_LOG_FILE}" ]] || \ [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_LOG_GZIPS}" ]]; then : - elif [[ "${dir_to_parse}" == "${SHM_DIRECTORY}" ]]; then - # SHM file - we do not want to see the content, but we want to see the files and their sizes - log_write "$(ls -lhd "${dir_to_parse}"/"${each_file}")" elif [[ "${dir_to_parse}" == "${DNSMASQ_D_DIRECTORY}" ]]; then - # in case of the dnsmasq directory inlcuede all files in the debug output + # 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}" else @@ -1148,9 +1170,10 @@ show_content_of_files_in_dir() { # Set a local variable for better readability local directory="${1}" # Check if the directory exists - dir_check "${directory}" - # if it does, list the files in it - list_files_in_dir "${directory}" + if dir_check "${directory}"; then + # if it does, list the files in it + list_files_in_dir "${directory}" + fi } show_content_of_pihole_files() { @@ -1158,6 +1181,8 @@ show_content_of_pihole_files() { show_content_of_files_in_dir "${PIHOLE_DIRECTORY}" show_content_of_files_in_dir "${DNSMASQ_D_DIRECTORY}" show_content_of_files_in_dir "${WEB_SERVER_CONFIG_DIRECTORY}" + show_content_of_files_in_dir "${WEB_SERVER_CONFIG_DIRECTORY_FEDORA}" + show_content_of_files_in_dir "${WEB_SERVER_CONFIG_DIRECTORY_DEBIAN}" show_content_of_files_in_dir "${CRON_D_DIRECTORY}" show_content_of_files_in_dir "${WEB_SERVER_LOG_DIRECTORY}" show_content_of_files_in_dir "${LOG_DIRECTORY}" @@ -1204,7 +1229,7 @@ show_db_entries() { IFS=$'\r\n' local entries=() mapfile -t entries < <(\ - pihole-FTL sqlite3 "${PIHOLE_GRAVITY_DB_FILE}" \ + pihole-FTL sqlite3 -ni "${PIHOLE_GRAVITY_DB_FILE}" \ -cmd ".headers on" \ -cmd ".mode column" \ -cmd ".width ${widths}" \ @@ -1229,7 +1254,7 @@ show_FTL_db_entries() { IFS=$'\r\n' local entries=() mapfile -t entries < <(\ - pihole-FTL sqlite3 "${PIHOLE_FTL_DB_FILE}" \ + pihole-FTL sqlite3 -ni "${PIHOLE_FTL_DB_FILE}" \ -cmd ".headers on" \ -cmd ".mode column" \ -cmd ".width ${widths}" \ @@ -1249,7 +1274,7 @@ check_dhcp_servers() { OLD_IFS="$IFS" IFS=$'\n' local entries=() - mapfile -t entries < <(pihole-FTL dhcp-discover) + mapfile -t entries < <(pihole-FTL dhcp-discover & spinner) for line in "${entries[@]}"; do log_write " ${line}" @@ -1278,15 +1303,24 @@ show_messages() { show_FTL_db_entries "Pi-hole diagnosis messages" "SELECT count (message) as count, datetime(max(timestamp),'unixepoch','localtime') as 'last timestamp', type, message, blob1, blob2, blob3, blob4, blob5 FROM message GROUP BY type, message, blob1, blob2, blob3, blob4, blob5;" "6 19 20 60 20 20 20 20 20" } +database_permissions() { + local permissions + permissions=$(ls -lhd "${1}") + log_write "${COL_GREEN}${permissions}${COL_NC}" +} + analyze_gravity_list() { echo_current_diagnostic "Gravity Database" - local gravity_permissions - gravity_permissions=$(ls -lhd "${PIHOLE_GRAVITY_DB_FILE}") - log_write "${COL_GREEN}${gravity_permissions}${COL_NC}" + database_permissions "${PIHOLE_GRAVITY_DB_FILE}" + + # if users want to check database integrity + if [[ "${CHECK_DATABASE}" = true ]]; then + database_integrity_check "${PIHOLE_GRAVITY_DB_FILE}" + fi show_db_entries "Info table" "SELECT property,value FROM info" "20 40" - gravity_updated_raw="$(pihole-FTL sqlite3 "${PIHOLE_GRAVITY_DB_FILE}" "SELECT value FROM info where property = 'updated'")" + gravity_updated_raw="$(pihole-FTL sqlite3 -ni "${PIHOLE_GRAVITY_DB_FILE}" "SELECT value FROM info where property = 'updated'")" gravity_updated="$(date -d @"${gravity_updated_raw}")" log_write " Last gravity run finished at: ${COL_CYAN}${gravity_updated}${COL_NC}" log_write "" @@ -1294,7 +1328,7 @@ analyze_gravity_list() { OLD_IFS="$IFS" IFS=$'\r\n' local gravity_sample=() - mapfile -t gravity_sample < <(pihole-FTL sqlite3 "${PIHOLE_GRAVITY_DB_FILE}" "SELECT domain FROM vw_gravity LIMIT 10") + mapfile -t gravity_sample < <(pihole-FTL sqlite3 -ni "${PIHOLE_GRAVITY_DB_FILE}" "SELECT domain FROM vw_gravity LIMIT 10") log_write " ${COL_CYAN}----- First 10 Gravity Domains -----${COL_NC}" for line in "${gravity_sample[@]}"; do @@ -1305,49 +1339,87 @@ analyze_gravity_list() { IFS="$OLD_IFS" } -obfuscated_pihole_log() { - local pihole_log=("$@") - local line - local error_to_check_for - local line_to_obfuscate - local obfuscated_line - for line in "${pihole_log[@]}"; do - # A common error in the pihole.log is when there is a non-hosts formatted file - # that the DNS server is attempting to read. Since it's not formatted - # correctly, there will be an entry for "bad address at line n" - # So we can check for that here and highlight it in red so the user can see it easily - error_to_check_for=$(echo "${line}" | grep 'bad address at') - # Some users may not want to have the domains they visit sent to us - # To that end, we check for lines in the log that would contain a domain name - line_to_obfuscate=$(echo "${line}" | grep ': query\|: forwarded\|: reply') - # If the variable contains a value, it found an error in the log - if [[ -n ${error_to_check_for} ]]; then - # So we can print it in red to make it visible to the user - log_write " ${CROSS} ${COL_RED}${line}${COL_NC} (${FAQ_BAD_ADDRESS})" +analyze_ftl_db() { + echo_current_diagnostic "Pi-hole FTL Query Database" + database_permissions "${PIHOLE_FTL_DB_FILE}" + # if users want to check database integrity + if [[ "${CHECK_DATABASE}" = true ]]; then + database_integrity_check "${PIHOLE_FTL_DB_FILE}" + fi +} + +database_integrity_check(){ + local result + local database="${1}" + + log_write "${INFO} Checking integrity of ${database} ... (this can take several minutes)" + result="$(pihole-FTL "${database}" "PRAGMA integrity_check" 2>&1 & spinner)" + if [[ ${result} = "ok" ]]; then + log_write "${TICK} Integrity of ${database} intact" + + + log_write "${INFO} Checking foreign key constraints of ${database} ... (this can take several minutes)" + unset result + result="$(pihole-FTL sqlite3 -ni "${database}" -cmd ".headers on" -cmd ".mode column" "PRAGMA foreign_key_check" 2>&1 & spinner)" + if [[ -z ${result} ]]; then + log_write "${TICK} No foreign key errors in ${database}" else - # If the variable does not a value (the current default behavior), so do not obfuscate anything - if [[ -z ${OBFUSCATE} ]]; then - log_write " ${line}" - # Othwerise, a flag was passed to this command to obfuscate domains in the log - else - # So first check if there are domains in the log that should be obfuscated - if [[ -n ${line_to_obfuscate} ]]; then - # If there are, we need to use awk to replace only the domain name (the 6th field in the log) - # so we substitute the domain for the placeholder value - obfuscated_line=$(echo "${line_to_obfuscate}" | awk -v placeholder="${OBFUSCATED_PLACEHOLDER}" '{sub($6,placeholder); print $0}') - log_write " ${obfuscated_line}" - else - log_write " ${line}" - fi - fi + log_write "${CROSS} ${COL_RED}Foreign key errors in ${database} found.${COL_NC}" + while IFS= read -r line ; do + log_write " $line" + done <<< "$result" fi - done + + else + log_write "${CROSS} ${COL_RED}Integrity errors in ${database} found.\n${COL_NC}" + while IFS= read -r line ; do + log_write " $line" + done <<< "$result" + fi + +} + +# Show a text spinner during a long process run +spinner(){ + # Show the spinner only if there is a tty + if tty -s; then + # PID of the most recent background process + _PID=$! + _spin="/-\|" + _start=0 + _elapsed=0 + _i=1 + + # Start the counter + _start=$(date +%s) + + # Hide the cursor + tput civis > /dev/tty + + # ensures cursor is visible again, in case of premature exit + trap 'tput cnorm > /dev/tty' EXIT + + while [ -d /proc/$_PID ]; do + _elapsed=$(( $(date +%s) - _start )) + # use hours only if needed + if [ "$_elapsed" -lt 3600 ]; then + printf "\r${_spin:_i++%${#_spin}:1} %02d:%02d" $((_elapsed/60)) $((_elapsed%60)) >"$(tty)" + else + printf "\r${_spin:_i++%${#_spin}:1} %02d:%02d:%02d" $((_elapsed/3600)) $(((_elapsed/60)%60)) $((_elapsed%60)) >"$(tty)" + fi + sleep 0.25 + done + + # Return to the begin of the line after completion (the spinner will be overwritten) + printf "\r" >"$(tty)" + + # Restore cursor visibility + tput cnorm > /dev/tty + fi } analyze_pihole_log() { echo_current_diagnostic "Pi-hole log" - local pihole_log_head=() - local pihole_log_tail=() local pihole_log_permissions local logging_enabled @@ -1357,22 +1429,10 @@ analyze_pihole_log() { log_write "${INFO} Query logging is disabled" log_write "" fi - # Put the current Internal Field Separator into another variable so it can be restored later - OLD_IFS="$IFS" - # Get the lines that are in the file(s) and store them in an array for parsing later - IFS=$'\r\n' + pihole_log_permissions=$(ls -lhd "${PIHOLE_LOG}") log_write "${COL_GREEN}${pihole_log_permissions}${COL_NC}" - mapfile -t pihole_log_head < <(head -n 20 ${PIHOLE_LOG}) - log_write " ${COL_CYAN}-----head of $(basename ${PIHOLE_LOG})------${COL_NC}" - obfuscated_pihole_log "${pihole_log_head[@]}" - log_write "" - mapfile -t pihole_log_tail < <(tail -n 20 ${PIHOLE_LOG}) - log_write " ${COL_CYAN}-----tail of $(basename ${PIHOLE_LOG})------${COL_NC}" - obfuscated_pihole_log "${pihole_log_tail[@]}" - log_write "" - # Set the IFS back to what it was - IFS="$OLD_IFS" + head_tail_log "${PIHOLE_LOG}" 20 } curl_to_tricorder() { @@ -1394,7 +1454,7 @@ curl_to_tricorder() { upload_to_tricorder() { local username="pihole" # Set the permissions and owner - chmod 644 ${PIHOLE_DEBUG_LOG} + chmod 640 ${PIHOLE_DEBUG_LOG} chown "$USER":"${username}" ${PIHOLE_DEBUG_LOG} # Let the user know debugging is complete with something strikingly visual @@ -1446,11 +1506,11 @@ upload_to_tricorder() { # If no token was generated else # Show an error and some help instructions - # Skip this if being called from web interface and autmatic mode was not chosen (users opt-out to upload) + # Skip this if being called from web interface and automatic mode was not chosen (users opt-out to upload) if [[ "${WEBCALL}" ]] && [[ ! "${AUTOMATED}" ]]; then : else - log_write "${CROSS} ${COL_RED}There was an error uploading your debug log.${COL_NC}" + log_write "${CROSS} ${COL_RED}There was an error uploading your debug log.${COL_NC}" log_write " * Please try again or contact the Pi-hole team for assistance." fi fi @@ -1477,8 +1537,10 @@ check_name_resolution check_dhcp_servers process_status ftl_full_status +lighttpd_test_configuration parse_setup_vars check_x_headers +analyze_ftl_db analyze_gravity_list show_groups show_domainlist diff --git a/advanced/Scripts/piholeLogFlush.sh b/advanced/Scripts/piholeLogFlush.sh index 57f901f52d..b06aac8bb6 100755 --- a/advanced/Scripts/piholeLogFlush.sh +++ b/advanced/Scripts/piholeLogFlush.sh @@ -31,7 +31,7 @@ if [ -z "$DBFILE" ]; then fi if [[ "$@" != *"quiet"* ]]; then - echo -ne " ${INFO} Flushing /var/log/pihole.log ..." + echo -ne " ${INFO} Flushing /var/log/pihole/pihole.log ..." fi if [[ "$@" == *"once"* ]]; then # Nightly logrotation @@ -44,9 +44,9 @@ if [[ "$@" == *"once"* ]]; then # 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) - cp -p /var/log/pihole.log /var/log/pihole.log.1 - echo " " > /var/log/pihole.log - chmod 644 /var/log/pihole.log + cp -p /var/log/pihole/pihole.log /var/log/pihole/pihole.log.1 + echo " " > /var/log/pihole/pihole.log + chmod 640 /var/log/pihole/pihole.log fi else # Manual flushing @@ -56,20 +56,20 @@ else /usr/sbin/logrotate --force --state "${STATEFILE}" /etc/pihole/logrotate else # Flush both pihole.log and pihole.log.1 (if existing) - echo " " > /var/log/pihole.log - if [ -f /var/log/pihole.log.1 ]; then - echo " " > /var/log/pihole.log.1 - chmod 644 /var/log/pihole.log.1 + echo " " > /var/log/pihole/pihole.log + if [ -f /var/log/pihole/pihole.log.1 ]; then + echo " " > /var/log/pihole/pihole.log.1 + chmod 640 /var/log/pihole/pihole.log.1 fi fi # Delete most recent 24 hours from FTL's database, leave even older data intact (don't wipe out all history) - deleted=$(pihole-FTL sqlite3 "${DBFILE}" "DELETE FROM query_storage WHERE timestamp >= strftime('%s','now')-86400; select changes() from query_storage limit 1") + deleted=$(pihole-FTL sqlite3 -ni "${DBFILE}" "DELETE FROM query_storage WHERE timestamp >= strftime('%s','now')-86400; select changes() from query_storage limit 1") # Restart pihole-FTL to force reloading history sudo pihole restartdns fi if [[ "$@" != *"quiet"* ]]; then - echo -e "${OVER} ${TICK} Flushed /var/log/pihole.log" + echo -e "${OVER} ${TICK} Flushed /var/log/pihole/pihole.log" echo -e " ${TICK} Deleted ${deleted} queries from database" fi diff --git a/advanced/Scripts/query.sh b/advanced/Scripts/query.sh index 8f7bfea42d..ebcc6f79c9 100755 --- a/advanced/Scripts/query.sh +++ b/advanced/Scripts/query.sh @@ -16,7 +16,6 @@ GRAVITYDB="${piholeDir}/gravity.db" options="$*" all="" exact="" -blockpage="" matchType="match" # Source pihole-FTL from install script pihole_FTL="${piholeDir}/pihole-FTL.conf" @@ -31,33 +30,6 @@ gravityDBfile="${GRAVITYDB}" colfile="/opt/pihole/COL_TABLE" source "${colfile}" -# Scan an array of files for matching strings -scanList(){ - # Escape full stops - local domain="${1}" esc_domain="${1//./\\.}" lists="${2}" type="${3:-}" - - # Prevent grep from printing file path - cd "$piholeDir" || exit 1 - - # Prevent grep -i matching slowly: https://bit.ly/2xFXtUX - export LC_CTYPE=C - - # /dev/null forces filename to be printed when only one list has been generated - case "${type}" in - "exact" ) grep -i -E -l "(^|(?/dev/null;; - # Iterate through each regexp and check whether it matches the domainQuery - # If it does, print the matching regexp and continue looping - # Input 1 - regexps | Input 2 - domainQuery - "regex" ) - for list in ${lists}; do - if [[ "${domain}" =~ ${list} ]]; then - printf "%b\n" "${list}"; - fi - done;; - * ) grep -i "${esc_domain}" ${lists} /dev/null 2>/dev/null;; - esac -} - if [[ "${options}" == "-h" ]] || [[ "${options}" == "--help" ]]; then echo "Usage: pihole -q [option] Example: 'pihole -q -exact domain.com' @@ -71,57 +43,93 @@ Options: fi # Handle valid options -if [[ "${options}" == *"-bp"* ]]; then - exact="exact"; blockpage=true -else - [[ "${options}" == *"-all"* ]] && all=true - if [[ "${options}" == *"-exact"* ]]; then - exact="exact"; matchType="exact ${matchType}" - fi +[[ "${options}" == *"-all"* ]] && all=true +if [[ "${options}" == *"-exact"* ]]; then + exact="exact"; matchType="exact ${matchType}" fi # Strip valid options, leaving only the domain and invalid options # This allows users to place the options before or after the domain -options=$(sed -E 's/ ?-(bp|adlists?|all|exact) ?//g' <<< "${options}") +options=$(sed -E 's/ ?-(all|exact) ?//g' <<< "${options}") # Handle remaining options # If $options contain non ASCII characters, convert to punycode case "${options}" in "" ) str="No domain specified";; *" "* ) str="Unknown query option specified";; - *[![:ascii:]]* ) domainQuery=$(idn2 "${options}");; - * ) domainQuery="${options}";; + *[![:ascii:]]* ) rawDomainQuery=$(idn2 "${options}");; + * ) rawDomainQuery="${options}";; esac +# convert the domain to lowercase +domainQuery=$(echo "${rawDomainQuery}" | tr '[:upper:]' '[:lower:]') + if [[ -n "${str:-}" ]]; then echo -e "${str}${COL_NC}\\nTry 'pihole -q --help' for more information." exit 1 fi +# Scan a domain again a list of RegEX +scanRegExList(){ + local domain="${1}" list="${2}" + + for entry in ${list}; do + if [[ "${domain}" =~ ${entry} ]]; then + printf "%b\n" "${entry}"; + fi + done + +} + scanDatabaseTable() { - local domain table type querystr result extra + local domain table list_type querystr result extra abpquerystr abpfound abpentry searchstr domain="$(printf "%q" "${1}")" table="${2}" - type="${3:-}" + list_type="${3:-}" # As underscores are legitimate parts of domains, we escape them when using the LIKE operator. # Underscores are SQLite wildcards matching exactly one character. We obviously want to suppress this # behavior. The "ESCAPE '\'" clause specifies that an underscore preceded by an '\' should be matched # as a literal underscore character. We pretreat the $domain variable accordingly to escape underscores. if [[ "${table}" == "gravity" ]]; then + + # Are there ABP entries on gravity? + # Return 1 if abp_domain=1 or Zero if abp_domain=0 or not set + abpquerystr="SELECT EXISTS (SELECT 1 FROM info WHERE property='abp_domains' and value='1')" + abpfound="$(pihole-FTL sqlite3 -ni "${gravityDBfile}" "${abpquerystr}")" 2> /dev/null + + # Create search string for ABP entries only if needed + if [ "${abpfound}" -eq 1 ]; then + abpentry="${domain}" + + searchstr="'||${abpentry}^'" + + # While a dot is found ... + while [ "${abpentry}" != "${abpentry/./}" ] + do + # ... remove text before the dot (including the dot) and append the result to $searchstr + abpentry=$(echo "${abpentry}" | cut -f 2- -d '.') + searchstr="$searchstr, '||${abpentry}^'" + done + + # The final search string will look like: + # "domain IN ('||sub2.sub1.domain.com^', '||sub1.domain.com^', '||domain.com^', '||com^') OR" + searchstr="domain IN (${searchstr}) OR " + fi + case "${exact}" in "exact" ) querystr="SELECT gravity.domain,adlist.address,adlist.enabled FROM gravity LEFT JOIN adlist ON adlist.id = gravity.adlist_id WHERE domain = '${domain}'";; - * ) querystr="SELECT gravity.domain,adlist.address,adlist.enabled FROM gravity LEFT JOIN adlist ON adlist.id = gravity.adlist_id WHERE domain LIKE '%${domain//_/\\_}%' ESCAPE '\\'";; + * ) querystr="SELECT gravity.domain,adlist.address,adlist.enabled FROM gravity LEFT JOIN adlist ON adlist.id = gravity.adlist_id WHERE ${searchstr} domain LIKE '%${domain//_/\\_}%' ESCAPE '\\'";; esac else case "${exact}" in - "exact" ) querystr="SELECT domain,enabled FROM domainlist WHERE type = '${type}' AND domain = '${domain}'";; - * ) querystr="SELECT domain,enabled FROM domainlist WHERE type = '${type}' AND domain LIKE '%${domain//_/\\_}%' ESCAPE '\\'";; + "exact" ) querystr="SELECT domain,enabled FROM domainlist WHERE type = '${list_type}' AND domain = '${domain}'";; + * ) querystr="SELECT domain,enabled FROM domainlist WHERE type = '${list_type}' AND domain LIKE '%${domain//_/\\_}%' ESCAPE '\\'";; esac fi # Send prepared query to gravity database - result="$(pihole-FTL sqlite3 "${gravityDBfile}" "${querystr}")" 2> /dev/null + result="$(pihole-FTL sqlite3 -ni -separator ',' "${gravityDBfile}" "${querystr}")" 2> /dev/null if [[ -z "${result}" ]]; then # Return early when there are no matches in this table return @@ -136,19 +144,13 @@ scanDatabaseTable() { wbMatch=true # Print table name - if [[ -z "${blockpage}" ]]; then - echo " ${matchType^} found in ${COL_BOLD}exact ${table}${COL_NC}" - fi + echo " ${matchType^} found in ${COL_BOLD}exact ${table}${COL_NC}" # Loop over results and print them mapfile -t results <<< "${result}" for result in "${results[@]}"; do - if [[ -n "${blockpage}" ]]; then - echo "π ${result}" - exit 0 - fi - domain="${result/|*}" - if [[ "${result#*|}" == "0" ]]; then + domain="${result/,*}" + if [[ "${result#*,}" == "0" ]]; then extra=" (disabled)" else extra="" @@ -158,20 +160,20 @@ scanDatabaseTable() { } scanRegexDatabaseTable() { - local domain list + local domain list list_type domain="${1}" list="${2}" - type="${3:-}" + list_type="${3:-}" # Query all regex from the corresponding database tables - mapfile -t regexList < <(pihole-FTL sqlite3 "${gravityDBfile}" "SELECT domain FROM domainlist WHERE type = ${type}" 2> /dev/null) + mapfile -t regexList < <(pihole-FTL sqlite3 -ni "${gravityDBfile}" "SELECT domain FROM domainlist WHERE type = ${list_type}" 2> /dev/null) # If we have regexps to process if [[ "${#regexList[@]}" -ne 0 ]]; then # Split regexps over a new line str_regexList=$(printf '%s\n' "${regexList[@]}") # Check domain against regexps - mapfile -t regexMatches < <(scanList "${domain}" "${str_regexList}" "regex") + mapfile -t regexMatches < <(scanRegExList "${domain}" "${str_regexList}") # If there were regex matches if [[ "${#regexMatches[@]}" -ne 0 ]]; then # Split matching regexps over a new line @@ -181,18 +183,13 @@ scanRegexDatabaseTable() { # Form a "results" message str_result="${COL_BOLD}${str_regexMatches}${COL_NC}" # If we are displaying more than just the source of the block - if [[ -z "${blockpage}" ]]; then - # Set the wildcard match flag - wcMatch=true - # Echo the "matched" message, indented by one space - echo " ${str_message}" - # Echo the "results" message, each line indented by three spaces - # shellcheck disable=SC2001 - echo "${str_result}" | sed 's/^/ /' - else - echo "π .wildcard" - exit 0 - fi + # Set the wildcard match flag + wcMatch=true + # Echo the "matched" message, indented by one space + echo " ${str_message}" + # Echo the "results" message, each line indented by three spaces + # shellcheck disable=SC2001 + echo "${str_result}" | sed 's/^/ /' fi fi } @@ -222,25 +219,23 @@ elif [[ -z "${all}" ]] && [[ "${#results[*]}" -ge 100 ]]; then fi # Print "Exact matches for" title -if [[ -n "${exact}" ]] && [[ -z "${blockpage}" ]]; then +if [[ -n "${exact}" ]]; then plural=""; [[ "${#results[*]}" -gt 1 ]] && plural="es" echo " ${matchType^}${plural} for ${COL_BOLD}${domainQuery}${COL_NC} found in:" fi for result in "${results[@]}"; do - match="${result/|*/}" - extra="${result#*|}" - adlistAddress="${extra/|*/}" - extra="${extra#*|}" + match="${result/,*/}" + extra="${result#*,}" + adlistAddress="${extra/,*/}" + extra="${extra#*,}" if [[ "${extra}" == "0" ]]; then extra=" (disabled)" else extra="" fi - if [[ -n "${blockpage}" ]]; then - echo "0 ${adlistAddress}" - elif [[ -n "${exact}" ]]; then + if [[ -n "${exact}" ]]; then echo " - ${adlistAddress}${extra}" else if [[ ! "${adlistAddress}" == "${adlistAddress_prev:-}" ]]; then diff --git a/advanced/Scripts/setupLCD.sh b/advanced/Scripts/setupLCD.sh deleted file mode 100755 index 8252364323..0000000000 --- a/advanced/Scripts/setupLCD.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env bash -# Pi-hole: A black hole for Internet advertisements -# (c) 2017 Pi-hole, LLC (https://pi-hole.net) -# Network-wide ad blocking via your own hardware. -# -# Automatically configures the Pi to use the 2.8 LCD screen to display stats on it (also works over ssh) -# -# This file is copyright under the latest version of the EUPL. -# Please see LICENSE file for your rights under this license. - - - -############ FUNCTIONS ########### - -# Borrowed from adafruit-pitft-helper < borrowed from raspi-config -# https://github.com/adafruit/Adafruit-PiTFT-Helper/blob/master/adafruit-pitft-helper#L324-L334 -getInitSys() { - if command -v systemctl > /dev/null && systemctl | grep -q '\-\.mount'; then - SYSTEMD=1 - elif [ -f /etc/init.d/cron ] && [ ! -h /etc/init.d/cron ]; then - SYSTEMD=0 - else - echo "Unrecognized init system" - return 1 - fi -} - -# Borrowed from adafruit-pitft-helper: -# https://github.com/adafruit/Adafruit-PiTFT-Helper/blob/master/adafruit-pitft-helper#L274-L285 -autoLoginPiToConsole() { - if [ -e /etc/init.d/lightdm ]; then - if [ ${SYSTEMD} -eq 1 ]; then - systemctl set-default multi-user.target - ln -fs /etc/systemd/system/autologin@.service /etc/systemd/system/getty.target.wants/getty@tty1.service - else - update-rc.d lightdm disable 2 - sed /etc/inittab -i -e "s/1:2345:respawn:\/sbin\/getty --noclear 38400 tty1/1:2345:respawn:\/bin\/login -f pi tty1 <\/dev\/tty1 >\/dev\/tty1 2>&1/" - fi - fi -} - -######### SCRIPT ########### -# Set pi to log in automatically -getInitSys -autoLoginPiToConsole - -# Set chronomter to run automatically when pi logs in -echo /usr/local/bin/chronometer.sh >> /home/pi/.bashrc -# OR -#$SUDO echo /usr/local/bin/chronometer.sh >> /etc/profile - -# Set up the LCD screen based on Adafruits instuctions: -# https://learn.adafruit.com/adafruit-pitft-28-inch-resistive-touchscreen-display-raspberry-pi/easy-install -curl -SLs https://apt.adafruit.com/add-pin | bash -apt-get -y install raspberrypi-bootloader -apt-get -y install adafruit-pitft-helper -adafruit-pitft-helper -t 28r - -# Download the cmdline.txt file that prevents the screen from going blank after a period of time -mv /boot/cmdline.txt /boot/cmdline.orig -curl -o /boot/cmdline.txt https://raw.githubusercontent.com/pi-hole/pi-hole/master/advanced/cmdline.txt - -# Back up the original file and download the new one -mv /etc/default/console-setup /etc/default/console-setup.orig -curl -o /etc/default/console-setup https://raw.githubusercontent.com/pi-hole/pi-hole/master/advanced/console-setup - -# Instantly apply the font change to the LCD screen -setupcon - -reboot - -# Start showing the stats on the screen by running the command on another tty: -# https://unix.stackexchange.com/questions/170063/start-a-process-on-a-different-tty -#setsid sh -c 'exec /usr/local/bin/chronometer.sh <> /dev/tty1 >&0 2>&1' diff --git a/advanced/Scripts/speedtestmod/lib.sh b/advanced/Scripts/speedtestmod/lib.sh new file mode 100755 index 0000000000..b0c7d42f67 --- /dev/null +++ b/advanced/Scripts/speedtestmod/lib.sh @@ -0,0 +1,511 @@ +#!/bin/bash +# +# The Library Script, Speedtest Mod for Pi-hole Helper Functions +# +# shellcheck disable=SC2015 +# + +declare PKG_MANAGER +PKG_MANAGER=$(command -v apt-get || command -v dnf || command -v yum) +readonly PKG_MANAGER + +####################################### +# Get the version of a repository, either from a local clone or from the installed package +# Globals: +# None +# Arguments: +# $1: The path to, or name of, the repository +# $2: Non-empty string to get the hash, empty string to get the tag if it exists +# Returns: +# The version of the repository +####################################### +getVersion() { + local found_version="" + + if [[ -d "$1" && -d "$1/.git" ]]; then + pushd "$1" &>/dev/null || exit 1 + found_version=$(git status --porcelain=2 -b | grep branch.oid | awk '{print $3;}') + [[ $found_version != *"("* ]] || found_version=$(git rev-parse HEAD 2>/dev/null) + + if [[ -z "${2:-}" ]]; then + local tags + local found_tag=$found_version + tags=$(git ls-remote -t origin || git show-ref --tags) + ! grep -q "$found_version" <<<"$tags" || found_tag=$(grep "$found_version" <<<"$tags" | awk '{print $2;}' | cut -d '/' -f 3 | sort -V | tail -n1) + [[ -z "$found_tag" ]] || found_version=$found_tag + fi + + popd &>/dev/null || exit 1 + elif [[ -x "$(command -v pihole)" ]]; then + local versions + versions=$(pihole -v | grep "$1") + found_version=$(cut -d ' ' -f 6 <<<"$versions") + [[ "$found_version" == *.* || ${#found_version} -ge 40 ]] || found_version=$(cut -d ' ' -f 7 <<<"$versions") + fi + + echo "$found_version" +} + +####################################### +# Fetch a repository, optionally a specific version +# Globals: +# None +# Arguments: +# $1: The path to download the repository to +# $2: The name of the repository +# $3: The URL of the repository +# $4: The desired version, hash or tag, to download (optional, none by default) +# $5: The branch to download (optional, master by default) +# $6: Whether to snap to the tag (optional, true by default) +# Outputs: +# The repository at the desired version +####################################### +download() { + local path=$1 + local name=$2 + local url=$3 + local desired_version="${4:-}" + local branch="${5:-master}" + local snap_to_tag="${6:-true}" + local dest=$path/$name + local aborting=false + + [[ ! -d "$dest" || -d "$dest/.git" ]] || mv -f "$dest" "$dest.old" + [[ -d "$dest" ]] || git clone --depth=1 -b "$branch" "$url" "$dest" -q + pushd "$dest" &>/dev/null || exit 1 + git config --global --add safe.directory "$dest" + + if [[ -n "$desired_version" && "$desired_version" != *.* ]]; then + local repos=("Pi-hole" "web" "speedtest") + + for repo in "${repos[@]}"; do + if [[ "$desired_version" == *"$repo"* ]]; then + aborting=true + break + fi + done + fi + + if ! $aborting; then + ! git remote -v | grep -q "old" && git remote -v | grep -q "origin" && git remote rename origin old || : + ! git remote -v | grep -q "origin" || git remote remove origin + git remote add -t "$branch" origin "$url" + elif git remote -v | grep -q "old"; then + ! git remote -v | grep -q "origin" || git remote remove origin + git remote rename old origin + url=$(git remote get-url origin) + fi + + [[ "$url" != *"ipitio"* ]] || snap_to_tag=$(grep -q "true" <<<"$snap_to_tag" && echo "false" || echo "true") + git fetch origin --depth=1 "$branch":refs/remotes/origin/"$branch" -q + git reset --hard origin/"$branch" -q + git checkout -B "$branch" -q + local current_hash + local tags + current_hash=$(getVersion "$dest" hash) + tags=$(git ls-remote -t origin || git show-ref --tags) + + if [[ -z "$desired_version" ]]; then # if empty, get the latest version + local latest_tag="" + [[ "$snap_to_tag" != "true" ]] || latest_tag=$(awk -F/ '{print $3}' <<<"$tags" | grep '^v[0-9]' | grep -v '\^{}' | sort -V | tail -n1) + [[ -n "$latest_tag" ]] && desired_version=$latest_tag || desired_version=$current_hash + elif $aborting; then + desired_version=$(getVersion "$desired_version" hash) + fi + + if [[ "$desired_version" == *.* ]]; then + grep -q "$desired_version$" <<<"$tags" && desired_version=$(grep "$desired_version$" <<<"$tags" | awk '{print $1;}') || desired_version=$current_hash + fi + + if [[ "$current_hash" != "$desired_version" ]]; then + git fetch origin --depth=1 "$desired_version" -q + git reset --hard "$desired_version" -q + fi + + popd &>/dev/null || exit 1 +} + +####################################### +# Check if the package is available +# Globals: +# PKG_MANAGER +# Arguments: +# $1: Package name +# Returns: +# 0 if available, 1 if not +####################################### +isAvailable() { + if [[ "$PKG_MANAGER" == *"apt"* ]]; then + # Check if there is a candidate and it is not "(none)" + apt-cache policy "$1" | grep -q "Candidate:" && ! apt-cache policy "$1" | grep -q "Candidate: (none)" && return 0 || return 1 + elif [[ "$PKG_MANAGER" == *"dnf"* || "$PKG_MANAGER" == *"yum"* ]]; then + $PKG_MANAGER list available "$1" &>/dev/null && return 0 || return 1 + else + echo "Unsupported package manager!" + exit 1 + fi +} + +####################################### +# Check if a package is installed, only used below when --continuous is not +# Globals: +# None +# Arguments: +# $1: The package to check +# Returns: +# 0 if the package is not installed, 1 if it is +####################################### +notInstalled() { + if [[ "$PKG_MANAGER" == *"apt"* ]]; then + dpkg -s "$1" &>/dev/null || return 0 + elif [[ "$PKG_MANAGER" == *"dnf"* || "$PKG_MANAGER" == *"yum"* ]]; then + rpm -q "$1" &>/dev/null || return 0 + else + echo "Unsupported package manager!" + exit 1 + fi + + return 1 +} + +####################################### +# Set a key-value pair in a configuration file, used below for --reinstall +# Globals: +# None +# Arguments: +# $1: The key to set +# $2: The value to set +# $3: The configuration file to set the key-value pair in +# $4: Whether to replace the value if it already exists +# Outputs: +# The configuration file with the key-value pair set +####################################### +setCnf() { + grep -q "^$1=" "$3" || echo "$1=$2" >>"$3" + [[ "${4:-false}" == "true" ]] || sed -i "s|^$1=.*|$1=$2|" "$3" +} + +####################################### +# Get a key-value pair from a configuration file, used below for --reinstall +# Globals: +# None +# Arguments: +# $1: The configuration file to get the key-value pair from +# $2: The key to get the value of +# $3: Non-empty string to get the hash, empty string to get the tag if it exists +# Returns: +# The value of the key-value pair +####################################### +getCnf() { + local keydir + local value + keydir=$(echo "$2" | sed 's/^mod-//;s/^org-//') + value=$(grep "^$2=" "$1" | cut -d '=' -f 2) + [[ -n "$value" ]] || value=$(getVersion "$keydir" "${3:-}") + echo "$value" +} + +####################################### +# Download and install librespeed +# Globals: +# PKG_MANAGERsetupVars +# Arguments: +# None +# Returns: +# 0 if the installation was successful, 1 if it was not +####################################### +libreSpeed() { + echo "Installing librespeed-cli..." + $PKG_MANAGER remove -y speedtest-cli speedtest >/dev/null 2>&1 + + if notInstalled golang; then + if grep -q "Raspbian" /etc/os-release; then + if [[ ! -f /etc/apt/sources.list.d/testing.list ]] && ! grep -q "testing" /etc/apt/sources.list; then + echo "Adding testing repo to sources.list.d" + echo "deb http://archive.raspbian.org/raspbian/ testing main" >/etc/apt/sources.list.d/testing.list + printf "Package: *\nPin: release a=testing\nPin-Priority: 50" >/etc/apt/preferences.d/limit-testing + $PKG_MANAGER update -y &>/dev/null + fi + + isAvailable golang || $PKG_MANAGER update -y &>/dev/null + $PKG_MANAGER install -y -t testing golang >/dev/null 2>&1 + else + [[ $PKG_MANAGER == *"apt"* ]] && ! isAvailable golang && $PKG_MANAGER update -y &>/dev/null || : + $PKG_MANAGER install -y golang >/dev/null 2>&1 + fi + fi + + download /etc/pihole librespeed https://github.com/librespeed/speedtest-cli + pushd /etc/pihole/librespeed &>/dev/null || return 1 + [[ ! -d out ]] || rm -rf out + ./build.sh + rm -f /usr/bin/speedtest + mv -f out/* /usr/bin/speedtest + popd &>/dev/null || return 1 + chmod +x /usr/bin/speedtest + + if [[ -x /usr/bin/speedtest ]]; then + echo "Installed librespeed-cli" + return 0 + fi + + echo "Installation of $candidate Failed!" + return 1 +} + +####################################### +# Install a package, removing a conflicting package if necessary +# Globals: +# PKG_MANAGER +# Arguments: +# None +# Outputs: +# The installed package +####################################### +swivelSpeed() { + local candidate="${1:-speedtest-cli}" + local target="${2:-speedtest}" + [[ ! -f /usr/bin/speedtest ]] || rm -f /usr/bin/speedtest + echo "Installing $candidate..." + + case "$PKG_MANAGER" in + /usr/bin/apt-get) + ! isAvailable "$candidate" && echo "And Updating Package Cache..." && $PKG_MANAGER update -y &>/dev/null || : + "$PKG_MANAGER" install -y "$candidate" "$target"- &>/dev/null + ;; + /usr/bin/dnf) "$PKG_MANAGER" install -y --allowerasing "$candidate" &>/dev/null ;; + /usr/bin/yum) "$PKG_MANAGER" install -y --allowerasing "$candidate" &>/dev/null ;; + esac + + if ! notInstalled "$candidate" && [[ -x /usr/bin/speedtest ]]; then + printf "Installed " + local version= + version=$(/usr/bin/speedtest --version) || return 1 + echo "${version%%$'\n'*}" + return 0 + fi + + echo "Installation of $candidate Failed!" + return 1 +} + +####################################### +# Add the Ookla speedtest CLI source and install the package +# Globals: +# PKG_MANAGER +# Arguments: +# None +# Outputs: +# The source for the speedtest CLI and the package +####################################### +ooklaSpeed() { + if [[ "$PKG_MANAGER" == *"yum"* || "$PKG_MANAGER" == *"dnf"* && ! -f /etc/yum.repos.d/ookla_speedtest-cli.repo ]]; then + echo "Adding speedtest source for RPM..." + curl -sSLN https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh | sudo bash + elif [[ "$PKG_MANAGER" == *"apt"* && ! -f /etc/apt/sources.list.d/ookla_speedtest-cli.list ]]; then + echo "Adding speedtest source for DEB..." + if [[ -e /etc/os-release ]]; then + # shellcheck disable=SC1091 + source /etc/os-release + local -r base="ubuntu debian" + local os=${ID} + local dist=${VERSION_CODENAME} + # shellcheck disable=SC2076 + if [[ -n "${ID_LIKE:-}" && "${base//\"/}" =~ "${ID_LIKE//\"/}" && "${os}" != "ubuntu" ]]; then + os=${ID_LIKE%% *} + [[ -z "${UBUNTU_CODENAME:-}" ]] && UBUNTU_CODENAME=$(/usr/bin/lsb_release -cs) + dist=${UBUNTU_CODENAME} + [[ -z "$dist" ]] && dist=${VERSION_CODENAME} + fi + wget -O /tmp/script.deb.sh https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh >/dev/null 2>&1 + chmod +x /tmp/script.deb.sh + os=$os dist=$dist /tmp/script.deb.sh >/dev/null 2>&1 + rm -f /tmp/script.deb.sh + else + curl -sSLN https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | sudo bash >/dev/null 2>&1 + fi + + sed -i 's/g]/g allow-insecure=yes trusted=yes]/' /etc/apt/sources.list.d/ookla_speedtest-cli.list + apt-get update -y &>/dev/null + fi + + swivelSpeed speedtest speedtest-cli +} + +####################################### +# Use an interval to generate a systemd calendar +# Globals: +# None +# Arguments: +# $1: The interval in hours, down to the minute +# Outputs: +# The systemd unit and timer files +####################################### +generate_systemd_service() { + local interval_hours="$1" + local freq_entries=() + local total_seconds + total_seconds=$(echo "$interval_hours * 3600" | bc) + + if (($(echo "$total_seconds < 60" | bc -l))); then # less than a minute + total_seconds=60 + addOrEditKeyValPair "/etc/pihole/setupVars.conf" "SPEEDTESTSCHEDULE" "0.017" + fi + + if (($(echo "$total_seconds >= 60 && $total_seconds < 3600" | bc -l))); then # less than an hour + local minute_interval + minute_interval=$(echo "$total_seconds / 60" | bc) + freq_entries+=("*-*-* *:00/$minute_interval:00") + elif (($(echo "$total_seconds == 3600" | bc -l))); then # exactly an hour + freq_entries+=("*-*-* *:00:00") + elif (($(echo "$total_seconds < 86400" | bc -l))); then # less than a day + if (($(awk "BEGIN {print ($total_seconds / 3600) % 1}") == 0)); then # divides evenly into an hour + local hour_interval + hour_interval=$(echo "$total_seconds / 3600" | bc) + freq_entries+=("*-*-* 00/$hour_interval:00:00") + else # does not divide evenly into an hour + local current_second=0 + + while (($(echo "$current_second < 86400" | bc -l))); do + local hour + hour=$(echo "$current_second / 3600" | bc) + local minute + minute=$(awk "BEGIN {print ($current_second % 3600) / 60}") + hour=${hour%.*} + minute=${minute%.*} + freq_entries+=("*-*-* $(printf "%02d:%02d:00" "$hour" "$minute")") + current_second=$(echo "$current_second + $total_seconds" | bc) + done + fi + else # more than a day + local full_days + local remaining_hours + full_days=$(echo "$interval_hours / 24" | bc) + remaining_hours=$(echo "$interval_hours - ($full_days * 24)" | bc) + + if (($(echo "$full_days > 0" | bc -l))); then + freq_entries+=("*-*-1/$(printf "%02.0f" "$full_days")") + fi + + if (($(echo "$remaining_hours > 0" | bc -l))); then # partial day + local remaining_minutes + remaining_minutes=$(echo "($remaining_hours - ($remaining_hours / 1)) * 60" | bc) + remaining_hours=${remaining_hours%.*} + remaining_minutes=${remaining_minutes%.*} + freq_entries+=("*-*-* $(printf "%02d:%02d:00" "$remaining_hours" "$remaining_minutes")") + fi + fi + + sudo bash -c 'cat > /etc/systemd/system/pihole-speedtest.service << EOF +[Unit] +Description=Pi-hole Speedtest +After=network.target + +[Service] +User=root +Type=forking +ExecStart=/usr/local/bin/pihole -a -sn + +[Install] +WantedBy=multi-user.target +EOF' + sudo bash -c 'cat > /etc/systemd/system/pihole-speedtest.timer << EOF +[Unit] +Description=Pi-hole Speedtest Timer + +[Install] +WantedBy=timers.target + +[Timer] +Persistent=true +EOF' + + for freq in "${freq_entries[@]}"; do + sudo bash -c "echo 'OnCalendar=$freq' >> /etc/systemd/system/pihole-speedtest.timer" + done + + systemctl daemon-reload + systemctl reenable pihole-speedtest.timer &>/dev/null + systemctl restart pihole-speedtest.timer +} + +####################################### +# Save the interval to a file that is run by cron every minute +# Globals: +# None +# Arguments: +# $1: The interval in hours +# Outputs: +# The interval in a file and the cron job +####################################### +generate_cron_job() { + local total_seconds="nan" + local schedule_script="/opt/pihole/speedtestmod/schedule_check.sh" + + if [[ "$1" != "nan" ]] && [[ "$1" =~ ^([0-9]+(\.[0-9]*)?|\.[0-9]+)$ ]] && (($(echo "$1 > 0" | bc -l))); then + total_seconds=$(echo "$1 * 3600" | bc) + if (($(echo "$total_seconds < 60" | bc -l))); then + total_seconds=60 + fi + + local remainder + remainder=$(awk "BEGIN {print $total_seconds % 60}") + + if (($(echo "$remainder < 30" | bc -l))); then + total_seconds=$(echo "$total_seconds - $remainder" | bc -l) + else + total_seconds=$(echo "$total_seconds + (60 - $remainder)" | bc -l) + fi + + addOrEditKeyValPair "/etc/pihole/setupVars.conf" "SPEEDTESTSCHEDULE" "$(echo "scale=3; $total_seconds / 3600" | bc)" + fi + + [ -d /opt/pihole/speedtestmod ] || return + sudo bash -c "cat > $(printf %q "$schedule_script")" < 0" | bc -l) )) || exit 0 + +if [[ -f "\$LAST_RUN_FILE" ]]; then + declare last_run + last_run=\$(<"\$LAST_RUN_FILE") + current_time=\$(date +%s) + (( \$(echo "\$current_time - \$last_run >= \$INTERVAL_SECONDS" | bc -l) )) || exit 0 +fi + +[[ \$(/usr/bin/tmux list-sessions 2>/dev/null | grep -c pimodtest) -eq 0 ]] || exit 0 +echo "\$current_time" > "\$LAST_RUN_FILE" +/usr/bin/tmux new-session -d -s pimodtest "sudo bash /opt/pihole/speedtestmod/speedtest.sh" +EOF + sudo chmod +x "$schedule_script" + + crontab -l 2>/dev/null | grep -v "$schedule_script" | crontab - + + if [[ "$total_seconds" == "nan" ]] || (($(echo "$total_seconds > 0" | bc -l))); then + crontab -l &>/dev/null || crontab -l 2>/dev/null | { + cat + echo "" + } | crontab - + ( + crontab -l + echo "* * * * * /bin/bash $schedule_script" + ) | crontab - + fi +} diff --git a/advanced/Scripts/speedtestmod/mod.sh b/advanced/Scripts/speedtestmod/mod.sh new file mode 100755 index 0000000000..e2e5e57d0a --- /dev/null +++ b/advanced/Scripts/speedtestmod/mod.sh @@ -0,0 +1,562 @@ +#!/bin/bash +# +# The Mod Script, Speedtest Mod for Pi-hole Installation Manager +# Please run this with the --help option for usage information +# +# shellcheck disable=SC2015 +# + +declare -r MOD_REPO="arevindh" +declare -r MOD_BRANCH="master" +declare -r CORE_BRANCH="master" +declare -r ADMIN_BRANCH="master" +declare -r HTML_DIR="/var/www/html" +declare -r CORE_DIR="/etc/.pihole" +declare -r OPT_DIR="/opt/pihole" +declare -r ETC_DIR="/etc/pihole" +declare -r MOD_DIR="/etc/pihole-speedtest" +declare -r CURR_WP="$OPT_DIR/webpage.sh" +declare -r CURR_DB="$ETC_DIR/speedtest.db" +declare -r LAST_DB="$CURR_DB.old" +declare -r DB_TABLE="speedtest" +declare cleanup +declare aborted +cleanup=$(mktemp) +aborted=$(mktemp) +echo "false" >"$cleanup" +echo "false" >"$aborted" +# shellcheck disable=SC2034 +SKIP_INSTALL=true +# shellcheck disable=SC1091 +source "$CORE_DIR/automated install/basic-install.sh" +# shellcheck disable=SC1090,SC1091 +[[ -f "$OPT_DIR/speedtestmod/lib.sh" ]] && source "$OPT_DIR/speedtestmod/lib.sh" || source <(curl -sSLN https://github.com/"$MOD_REPO"/pi-hole/raw/"$CORE_BRANCH"/advanced/Scripts/speedtestmod/lib.sh) + +####################################### +# Display the help message +# Globals: +# None +# Arguments: +# None +# Outputs: +# The help message +####################################### +help() { + local -r help_text=( + "The Mod Script" + "Usage: sudo bash /path/to/mod.sh [options]" + " or: curl -sSLN //link/to/mod.sh | sudo bash [-s -- options]" + " or: pihole -a -sm [options]" + "(Re)install Speedtest Mod and/or the following options:" + "" + "Installation:" + " -u, --update, up also update Pi-hole" + " -r, --reinstall repair currently installed version of the Mod" + " -t, --testing try unstable changes" + "" + "Restoration:" + " -n, --uninstall, un purge the Mod, keeping the speedtest package, logs, and database" + " -b, --backup backup Pi-hole for faster offline restore" + " -o, --online force online restore of Pi-hole" + "" + "Standalone:" + " -d, --database, db flush/restore the database if it's not/empty" + " -s, --speedtest[=] install Ookla's or the specified CLI immediately" + " -x, --verbose show the commands being run" + " -v, --version display the installed version of the Mod and exit" + " -h, --help display this help message and exit" + "" + "Examples:" + " pihole -a -sm -d -slibre" + " sudo bash /opt/pihole/speedtestmod/mod.sh --update" + " curl -sSL https://github.com/$MOD_REPO/pihole-speedtest/raw/$CORE_BRANCH/mod | sudo bash" + " curl -sSLN https://github.com/$MOD_REPO/pi-hole/raw/$CORE_BRANCH/advanced/Scripts/speedtestmod/mod.sh | sudo bash -s -- -bo" + ) + + printf "%s\n" "${help_text[@]}" +} + +####################################### +# Check if a database is empty +# Globals: +# DB_TABLE +# Arguments: +# $1: The database to check +# Returns: +# 0 if the database is empty, 1 if it is not +####################################### +isEmpty() { + [[ -f "$1" ]] && sqlite3 "$1" "select * from $DB_TABLE limit 1;" &>/dev/null && [[ -n "$(sqlite3 "$1" "select * from $DB_TABLE limit 1;")" ]] && return 1 || return 0 +} + +####################################### +# Copy scripts from the CORE to the OPT repository +# Globals: +# OPT_DIR +# Arguments: +# None +# Outputs: +# The scripts copied to the OPT repository +####################################### +swapScripts() { + set +u + installScripts >/dev/null 2>&1 + set -u +} + +####################################### +# Restore a backup, used after --backup unless --online or --install are used +# Globals: +# None +# Arguments: +# $1: The backup to restore +# Returns: +# 1 if the backup does not exist or is stale, 0 if it does and isn't +# Outputs: +# The backup restored +####################################### +restore() { + [[ -d "$1".bak && "$(getVersion "$1".bak hash)" == "$(getCnf $MOD_DIR/cnf org-"$1" hash)" ]] || return 1 + [[ ! -e "$1" ]] || rm -rf "$1" + mv -f "$1".bak "$1" +} + +####################################### +# Purge the mod, used for --uninstall +# Globals: +# CORE_DIR +# HTML_DIR +# OPT_DIR +# CURR_DB +# ETC_DIR +# Arguments: +# None +# Outputs: +# The mod purged +####################################### +purge() { + if [[ -f /etc/systemd/system/pihole-speedtest.timer ]]; then + rm -f /etc/systemd/system/pihole-speedtest.service + rm -f /etc/systemd/system/pihole-speedtest.timer + systemctl daemon-reload + fi + + rm -rf $OPT_DIR/speedtestmod + rm -rf $CORE_DIR.bak + rm -rf $HTML_DIR/admin.bak + rm -rf $CORE_DIR.mod + rm -rf $HTML_DIR/admin.mod + rm -f "$CURR_DB".* + rm -f $ETC_DIR/last_speedtest.* + ! isEmpty $CURR_DB || rm -f $CURR_DB +} + +####################################### +# Abort the process +# Globals: +# CORE_DIR +# HTML_DIR +# MOD_DIR +# OPT_DIR +# CURR_WP +# CURR_DB +# LAST_DB +# ETC_DIR +# cleanup +# aborted +# Arguments: +# None +# Outputs: +# The changes reverted +# shellcheck disable=SC2317 ########### +abort() { + if grep -q true "$cleanup" && grep -q false "$aborted"; then + echo "Process Aborting..." + echo "true" >"$aborted" + + if [[ -d "$CORE_DIR"/.git/refs/remotes/old ]]; then + download /etc .pihole "" Pi-hole + swapScripts + + if [[ -d "$CORE_DIR"/advanced/Scripts/speedtestmod ]]; then + \cp -af "$CORE_DIR"/advanced/Scripts/speedtestmod/. "$OPT_DIR"/speedtestmod/ + pihole -a -s + fi + fi + + [[ ! -d "$MOD_DIR" || ! -d "$MOD_DIR"/.git/refs/remotes/old ]] || download /etc pihole-speedtest "" speedtest + [[ ! -d "$HTML_DIR"/admin/.git/refs/remotes/old ]] || download "$HTML_DIR" admin "" web + [[ ! -f "$LAST_DB" || -f "$CURR_DB" ]] || mv "$LAST_DB" "$CURR_DB" + [[ -f "$CURR_WP" ]] && ! grep -q SpeedTest "$CURR_WP" && purge || : + printf "Please try again before reporting an issue.\n\n%s\n" "$(date)" + fi +} + +####################################### +# Commit the changes +# Globals: +# CORE_DIR +# HTML_DIR +# cleanup +# Arguments: +# None +# Outputs: +# The repositories cleaned up +# shellcheck disable=SC2317 ########### +commit() { + if grep -q true "$cleanup"; then + for dir in $CORE_DIR $HTML_DIR/admin; do + [[ ! -d "$dir" ]] && continue || pushd "$dir" &>/dev/null || exit 1 + ! git remote -v | grep -q "old" || git remote remove old + git clean -ffdx + popd &>/dev/null + done + + printf "Done!\n\n%s\n" "$(date)" + fi +} + +####################################### +# Manage the installation +# Globals: +# CORE_DIR +# HTML_DIR +# MOD_DIR +# OPT_DIR +# CURR_WP +# CURR_DB +# LAST_DB +# ETC_DIR +# cleanup +# Arguments: +# $@: The options for managing the installation +# Outputs: +# The installation managed +####################################### +main() { + set -u + + local -r short_opts=-ubortnds::vxh + local -r long_opts=update,backup,online,reinstall,testing,uninstall,database,speedtest::,version,verbose,help + local parsed_opts + + if ! parsed_opts=$(getopt --options ${short_opts} --longoptions ${long_opts} --name "$0" -- "$@"); then + help + return 1 + fi + + eval set -- "${parsed_opts}" + + declare -a POSITIONAL EXTRA_ARGS + local -i dashes=0 + local update=false + local backup=false + local online=false + local reinstall=false + local stable=true + local uninstall=false + local database=false + local verbose=false + local select_test=false + local selected_test="" + local do_main=false + local st_ver="" + local mod_core_ver="" + local mod_admin_ver="" + + while [[ $# -gt 0 ]]; do + case "$1" in + -u | --update) + update=true + do_main=true + ;; + -b | --backup) + backup=true + do_main=true + ;; + -o | --online) + online=true + do_main=true + ;; + -r | --reinstall) + reinstall=true + do_main=true + ;; + -t | --testing) + stable=false + do_main=true + ;; + -n | --uninstall) + uninstall=true + do_main=true + ;; + -d | --database) database=true ;; + -s | --speedtest) + select_test=true + + if [[ -n "$2" && ! "$2" =~ sivel|libre ]]; then + help + return 1 + fi + + selected_test=$2 + shift + ;; + -v | --version) + getVersion $MOD_DIR + return 0 + ;; + -x | --verbose) verbose=true ;; + -h | --help) + help + return 0 + ;; + --) dashes=1 ;; + *) [[ $dashes -eq 0 ]] && POSITIONAL+=("$1") || EXTRA_ARGS+=("$1") ;; + esac + shift + done + + set -- "${POSITIONAL[@]}" + + # backward compatibility + for arg in "$@"; do + case $arg in + up) update=true ;; + un) uninstall=true ;; + db) database=true ;; + *) + help + return 1 + ;; + esac + done + + echo "true" >"$cleanup" + ! $do_main && ! $database && ! $select_test && do_main=true || : + readonly update backup online reinstall stable uninstall database verbose select_test selected_test do_main + printf "%s\n\nRunning the Mod Script by @ipitio...\n" "$(date)" + ! $verbose || set -x + + if $select_test; then + case $selected_test in + sivel) swivelSpeed ;; + libre) libreSpeed ;; + *) ooklaSpeed ;; + esac + fi + + set -Eeo pipefail + trap '[ "$?" -eq "0" ] && commit || abort' EXIT + trap 'abort' INT TERM ERR + shopt -s dotglob + + if $database; then + if [[ -f $CURR_DB ]] && ! isEmpty $CURR_DB; then + echo "Flushing Database..." + mv -f $CURR_DB $LAST_DB + [[ ! -f $ETC_DIR/last_speedtest ]] || mv -f $ETC_DIR/last_speedtest $ETC_DIR/last_speedtest.old + + if [[ -f /var/log/pihole/speedtest.log ]]; then + mv -f /var/log/pihole/speedtest.log /var/log/pihole/speedtest.log.old + rm -f $ETC_DIR/speedtest.log + fi + elif [[ -f $LAST_DB ]]; then + echo "Restoring Database..." + mv -f $LAST_DB $CURR_DB + [[ ! -f $ETC_DIR/last_speedtest.old ]] || mv -f $ETC_DIR/last_speedtest.old $ETC_DIR/last_speedtest + + if [[ -f /var/log/pihole/speedtest.log.old ]]; then + mv -f /var/log/pihole/speedtest.log.old /var/log/pihole/speedtest.log + \cp -af /var/log/pihole/speedtest.log $ETC_DIR/speedtest.log + fi + fi + fi + + if $do_main; then + if [[ ! -f /usr/local/bin/pihole ]]; then + # https://discourse.pi-hole.net/t/pi-hole-as-part-of-a-post-installation-script/3523/15 + if [[ ! -f /etc/pihole/setupVars.conf ]]; then + cat </etc/pihole/setupVars.conf +WEBPASSWORD={{ pihole_admin_password | hash('sha256') | hash('sha256') }} +PIHOLE_INTERFACE=eth0 +IPV4_ADDRESS=192.168.x.y/24 +IPV6_ADDRESS=fd00::2 +QUERY_LOGGING=true +INSTALL_WEB_INTERFACE=true +LIGHTTPD_ENABLED=false +INSTALL_WEB_SERVER=false +DNSMASQ_LISTENING=single +PIHOLE_DNS_1=8.8.8.8 +PIHOLE_DNS_2=4.4.4.4 +PIHOLE_DNS_3=2001:4860:4860:0:0:0:0:8888 +PIHOLE_DNS_4=2001:4860:4860:0:0:0:0:8844 +DNS_FQDN_REQUIRED=true +DNS_BOGUS_PRIV=true +DNSSEC=false +TEMPERATUREUNIT=C +WEBUIBOXEDLAYOUT=traditional +API_EXCLUDE_DOMAINS= +API_EXCLUDE_CLIENTS= +API_QUERY_LOG_SHOW=all +API_PRIVACY_MODE=false +BLOCKING_ENABLED=true +REV_SERVER=true +REV_SERVER_CIDR=192.168.x.0/24 +REV_SERVER_TARGET=192.168.x.z +REV_SERVER_DOMAIN=your.domain +CACHE_SIZE=10000 +EOF + fi + + echo "Installing Pi-hole..." + curl -sSL https://install.pi-hole.net | sudo bash /dev/stdin --unattended + fi + + pushd ~ >/dev/null || exit 1 + pihole updatechecker + pihole -v || : + + if [[ -f $CURR_WP ]] && grep -q SpeedTest "$CURR_WP"; then + if $reinstall; then + for repo in $CORE_DIR $HTML_DIR/admin $MOD_DIR; do + if [[ -d "$repo" ]]; then + local hash_tag + hash_tag=$(getVersion "$repo") # if hashes are the same, we may be on an older tag + [[ "$(getVersion "$repo" hash)" != "$(getCnf $MOD_DIR/cnf mod-"$repo" hash)" ]] || hash_tag=$(getCnf $MOD_DIR/cnf mod-"$repo") + + case "$repo" in + "$CORE_DIR") mod_core_ver=$hash_tag ;; + "$HTML_DIR/admin") mod_admin_ver=$hash_tag ;; + "$MOD_DIR") st_ver=$hash_tag ;; + esac + fi + done + fi + + local core_ver="" + local admin_ver="" + echo "Restoring Pi-hole$($online && echo " Online..." || echo "...")" + pihole -a -s -1 + + if [[ -f $MOD_DIR/cnf ]]; then + core_ver=$(getCnf $MOD_DIR/cnf org-$CORE_DIR) + admin_ver=$(getCnf $MOD_DIR/cnf org-$HTML_DIR/admin) + fi + + readonly core_ver admin_ver + ! $online && restore $HTML_DIR/admin || download $HTML_DIR admin https://github.com/pi-hole/AdminLTE "$admin_ver" + ! $online && restore $CORE_DIR || download /etc .pihole https://github.com/pi-hole/pi-hole "$core_ver" + [[ ! -d $MOD_DIR ]] || rm -rf $MOD_DIR + swapScripts + + for repo in $CORE_DIR $HTML_DIR/admin; do + pushd "$repo" &>/dev/null || exit 1 + git tag -l | xargs git tag -d >/dev/null 2>&1 + git fetch --tags -f -q + popd &>/dev/null + done + fi + + if $uninstall; then + echo "Purging Mod..." + purge + else + echo "Checking Dependencies..." + local -r php_version=$(php -v | head -n 1 | awk '{print $2}' | cut -d "." -f 1,2) + local -r pkgs=(bc nano sqlite3 jq tar tmux wget "php$php_version-sqlite3") + local missingpkgs=() + + for pkg in "${pkgs[@]}"; do + ! notInstalled "$pkg" || missingpkgs+=("$pkg") + done + + readonly missingpkgs + if [[ ${#missingpkgs[@]} -gt 0 ]]; then + echo "Installing Missing Dependencies..." + if ! $PKG_MANAGER install -y "${missingpkgs[@]}" &>/dev/null; then + [[ "$PKG_MANAGER" == *"apt"* ]] || exit 1 + echo "And Updating Package Cache..." + $PKG_MANAGER update -y &>/dev/null + $PKG_MANAGER install -y "${missingpkgs[@]}" &>/dev/null + fi + fi + + if ! $update; then + if ! $reinstall; then + local -r installed_core_ver=$(getVersion "Pi-hole") + local -r installed_admin_ver=$(getVersion "web") + if [[ "$installed_core_ver" == *.* && "$installed_admin_ver" == *.* ]]; then + echo "Finding Latest Compatible Versions..." + local -r remote_core_ver=$(git ls-remote "https://github.com/$MOD_REPO/pi-hole") + local -r remote_admin_ver=$(git ls-remote "https://github.com/$MOD_REPO/AdminLTE") + mod_core_ver=$(grep -q "$installed_core_ver" <<<"$remote_core_ver" && grep "$installed_core_ver" <<<"$remote_core_ver" | awk '{print $2;}' | cut -d '/' -f 3 | sort -Vr | head -n1 || echo "") + mod_admin_ver=$(grep -q "$installed_admin_ver" <<<"$remote_admin_ver" && grep "$installed_admin_ver" <<<"$remote_admin_ver" | awk '{print $2;}' | cut -d '/' -f 3 | sort -Vr | head -n1 || echo "") + fi + fi + elif [[ -d /run/systemd/system ]]; then + echo "Updating Pi-hole..." + PIHOLE_SKIP_OS_CHECK=true sudo -E pihole -up + else + echo "Systemd not found. Skipping Pi-hole Update..." + fi + + if $backup; then + echo "Creating Backup..." + download /etc .pihole.mod https://github.com/"$MOD_REPO"/pi-hole "$mod_core_ver" "$CORE_BRANCH" $stable + download $HTML_DIR admin.mod https://github.com/"$MOD_REPO"/AdminLTE "$mod_admin_ver" "$ADMIN_BRANCH" $stable + fi + + $reinstall && echo "Reinstalling Mod..." || echo "Installing Mod..." + download /etc pihole-speedtest https://github.com/"$MOD_REPO"/pihole-speedtest "$st_ver" "$MOD_BRANCH" $stable + [[ -f $MOD_DIR/cnf ]] || touch $MOD_DIR/cnf + setCnf mod-$MOD_DIR "$(getVersion $MOD_DIR)" $MOD_DIR/cnf $reinstall + local stock_tag + + for repo in $CORE_DIR $HTML_DIR/admin; do + if [[ -d "$repo" ]]; then + stock_tag=$(getVersion "$repo") + setCnf org-"$repo" "$stock_tag" $MOD_DIR/cnf + + if $backup; then + if [[ ! -d "$repo".bak || "$(getVersion "$repo".bak)" != "$stock_tag" ]]; then + rm -rf "$repo".bak + mv -f "$repo" "$repo".bak + fi + + rm -rf "$repo" + mv -f "$repo".mod "$repo" + fi + fi + done + + $backup || download /etc .pihole https://github.com/"$MOD_REPO"/pi-hole "$mod_core_ver" "$CORE_BRANCH" $stable + swapScripts + \cp -af $CORE_DIR/advanced/Scripts/speedtestmod/. $OPT_DIR/speedtestmod/ + pihole -a -s + $backup || download $HTML_DIR admin https://github.com/"$MOD_REPO"/AdminLTE "$mod_admin_ver" "$ADMIN_BRANCH" $stable + setCnf mod-$CORE_DIR "$(getVersion $CORE_DIR)" $MOD_DIR/cnf $reinstall + setCnf mod-$HTML_DIR/admin "$(getVersion $HTML_DIR/admin)" $MOD_DIR/cnf $reinstall + fi + + pihole updatechecker + pihole -v || : + popd >/dev/null + fi + + exit 0 +} + +if [[ $EUID != 0 ]]; then + sudo "$0" "$@" + exit $? +fi + +rm -f /tmp/pimod.log +touch /tmp/pimod.log +main "$@" 2>&1 | tee -a /tmp/pimod.log +grep -q false "$cleanup" || mv -f /tmp/pimod.log /var/log/pihole/mod.log && rm -f /tmp/pimod.log +return_status=$(<"$aborted") +rm -f "$cleanup" +rm -f "$aborted" +[[ "$return_status" == "true" ]] && exit 1 || exit 0 diff --git a/advanced/Scripts/speedtestmod/speedtest.sh b/advanced/Scripts/speedtestmod/speedtest.sh new file mode 100755 index 0000000000..0e3ff08ed7 --- /dev/null +++ b/advanced/Scripts/speedtestmod/speedtest.sh @@ -0,0 +1,256 @@ +#!/bin/bash +# +# The Test Script, Speedtest Mod for Pi-hole Run Supervisor +# Please run this with the --help option for usage information +# +# shellcheck disable=SC2015 +# + +declare -r MOD_REPO="arevindh" +declare -r CORE_BRANCH="master" +declare -r OPT_DIR="/opt/pihole" +declare -r OUT_FILE=/tmp/speedtest.log +declare -r CREATE_TABLE="create table if not exists speedtest ( +id integer primary key autoincrement, +start_time text, +stop_time text, +from_server text, +from_ip text, +server text, +server_dist real, +server_ping real, +download real, +upload real, +share_url text +);" +declare START +START=$(date -u --rfc-3339='seconds') +readonly START +serverid=$(grep 'SPEEDTEST_SERVER' "/etc/pihole/setupVars.conf" | cut -d '=' -f2) +run_status=$(mktemp) +database="/etc/pihole/speedtest.db" +echo "0" >"$run_status" +# shellcheck disable=SC1090,SC1091 +[[ -f "$OPT_DIR/speedtestmod/lib.sh" ]] && source "$OPT_DIR/speedtestmod/lib.sh" || source <(curl -sSLN https://github.com/"$MOD_REPO"/pi-hole/raw/"$CORE_BRANCH"/advanced/Scripts/speedtestmod/lib.sh) + +####################################### +# Display the help message +# Globals: +# None +# Arguments: +# None +# Outputs: +# The help message +####################################### +help() { + local -r help_text=( + "The Test Script" + "Usage: sudo bash /path/to/speedtest.sh [options]" + " or: curl -sSLN //link/to/speedtest.sh | sudo bash [-s -- options]" + " or: pihole -a -sn [options]" + "Run the speedtest" + "" + "Options:" + " -s, --server= Speedtest server id" + " -l, --list List all speedtest servers" + " -o, --output= Sqlite3 database (default: /etc/pihole/speedtest.db)" + " -a, --attempts= Number of attempts (default: 3)" + " -x, --verbose Show the commands being run" + " -h, --help Display this help message" + "" + "Examples:" + " pihole -a -sn -a 1" + " sudo bash /opt/pihole/speedtestmod/speedtest.sh" + " curl -sSL https://github.com/$MOD_REPO/pihole-speedtest/raw/$CORE_BRANCH/test | sudo bash" + " curl -sSLN https://github.com/$MOD_REPO/pi-hole/raw/$CORE_BRANCH/advanced/Scripts/speedtestmod/speedtest.sh | sudo bash -s -- --verbose" + ) + + printf "%s\n" "${help_text[@]}" + exit 1 +} + +####################################### +# Run the speedtest +# Globals: +# serverid +# Arguments: +# None +# Outputs: +# The speedtest results +####################################### +speedtest() { + if /usr/bin/speedtest --version | grep -q "official"; then + [[ -n "${serverid}" ]] && /usr/bin/speedtest -s "$serverid" --accept-gdpr --accept-license -f json || /usr/bin/speedtest --accept-gdpr --accept-license -f json + else + [[ -n "${serverid}" ]] && /usr/bin/speedtest --server "$serverid" --json --share --secure || /usr/bin/speedtest --json --share --secure + fi +} + +####################################### +# Run the speedtest and save the results +# Globals: +# PKG_MANAGER +# START +# Arguments: +# $1: Number of attempts (optional, 3 by default) +# $2: Current attempt (optional, 0 by default) +# Returns: +# 1 if the speedtest failed, 0 if successful +####################################### +run() { + local isp="No Internet" + local from_ip="-" + local server_name="-" + local server_dist=0 + local server_ping=0 + local download=0 + local upload=0 + local share_url="#" + local res + local stop + + if [[ "${2:-0}" -gt 0 || ! -f /usr/bin/speedtest ]]; then + if notInstalled speedtest && notInstalled speedtest-cli; then + [[ ! -f /usr/bin/speedtest ]] || rm -f /usr/bin/speedtest + ! ooklaSpeed && ! swivelSpeed && libreSpeed || : + elif ! notInstalled speedtest && isAvailable speedtest-cli; then + ! swivelSpeed && ! libreSpeed && ooklaSpeed || : + else + ! libreSpeed && ! ooklaSpeed && swivelSpeed || : + fi + fi + + if [[ "${1}" -gt "${2:-0}" ]]; then + [[ -n "${2:-}" ]] || echo "Running Test..." + speedtest | jq . >/tmp/speedtest_results || echo "Attempt ${2:-0} Failed!" + stop=$(date -u --rfc-3339='seconds') + + if [[ -s /tmp/speedtest_results ]]; then + res=$(/dev/null; then + local server_id + local servers + server_id=$(jq -r '.server.id' <<<"$res") + servers="$(curl 'https://www.speedtest.net/api/js/servers' --compressed -H 'Upgrade-Insecure-Requests: 1' -H 'DNT: 1' -H 'Sec-GPC: 1')" + server_dist=$(jq --arg id "$server_id" '.[] | select(.id == $id) | .distance' <<<"$servers") + + if /usr/bin/speedtest --version | grep -q "official"; then # ookla + server_name=$(jq -r '.server.name' <<<"$res") + download=$(jq -r '.download.bandwidth' <<<"$res" | awk '{$1=$1*8/1000/1000; print $1;}' | sed 's/,/./g') + upload=$(jq -r '.upload.bandwidth' <<<"$res" | awk '{$1=$1*8/1000/1000; print $1;}' | sed 's/,/./g') + isp=$(jq -r '.isp' <<<"$res") + from_ip=$(jq -r '.interface.externalIp' <<<"$res") + server_ping=$(jq -r '.ping.latency' <<<"$res") + share_url=$(jq -r '.result.url' <<<"$res") + [[ -n "$server_dist" ]] || server_dist="-1" + else # speedtest-cli + server_name=$(jq -r '.server.sponsor' <<<"$res") + download=$(jq -r '.download' <<<"$res" | awk '{$1=$1/1000/1000; print $1;}' | sed 's/,/./g') + upload=$(jq -r '.upload' <<<"$res" | awk '{$1=$1/1000/1000; print $1;}' | sed 's/,/./g') + isp=$(jq -r '.client.isp' <<<"$res") + from_ip=$(jq -r '.client.ip' <<<"$res") + server_ping=$(jq -r '.ping' <<<"$res") + share_url=$(jq -r '.share' <<<"$res") + [[ -n "$server_dist" ]] || server_dist=$(jq -r '.server.d' <<<"$res") + fi + else # if jq -e '.[].server' /tmp/speedtest_results &>/dev/null; then # librespeed + server_name=$(jq -r '.[].server.name' <<<"$res") + download=$(jq -r '.[].download' <<<"$res") + upload=$(jq -r '.[].upload' <<<"$res") + isp="Unknown" + from_ip=$(curl -sSL https://ipv4.icanhazip.com) + server_ping=$(jq -r '.[].ping' <<<"$res") + share_url=$(jq -r '.[].share' <<<"$res") + server_dist="-1" + fi + else + run $1 $((${2:-0} + 1)) + fi + else + echo "Timeout!" + fi + + local -r rm_empty=" + def walk(f): . as \$in | if type == \"object\" then reduce keys_unsorted[] as \$key ({}; . + { (\$key): (\$in[\$key] | walk(f)) }) | f else if type == \"array\" then map( walk(f) ) | f else f end; + def nonempty: . and length > 0 and (type != \"object\" or . != {}) and (type != \"array\" or any(.[]; . != \"\")); + if type == \"array\" then map(walk(if type == \"object\" then with_entries(select(.value | nonempty)) else . end)) else walk(if type == \"object\" then with_entries(select(.value | nonempty)) else . end) end +" + local -r temp_file=$(mktemp) + local -r json_file="/tmp/speedtest_results" + jq "$rm_empty" "$json_file" >"$temp_file" && mv -f "$temp_file" "$json_file" + rm -f "$temp_file" + chmod 644 /tmp/speedtest_results + + if [[ -f /usr/local/bin/pihole ]]; then + mv -f /tmp/speedtest_results /var/log/pihole/speedtest.log + \cp -af /var/log/pihole/speedtest.log /etc/pihole/speedtest.log + fi + + sqlite3 "$database" "$CREATE_TABLE" + sqlite3 "$database" "insert into speedtest values (NULL, '${START}', '${stop}', '${isp}', '${from_ip}', '${server_name}', ${server_dist}, ${server_ping}, ${download}, ${upload}, '${share_url}');" + [[ "$isp" == "No Internet" ]] && return 1 || return 0 +} + +####################################### +# Start the runner +# Globals: +# PKG_MANAGER +# Arguments: +# None +# Outputs: +# The speedtest status +####################################### +main() { + local -r short_opts=-s:lo:a:xh + local -r long_opts=server:,list,output:,attempts:,verbose,help + local -r parsed_opts=$(getopt --options ${short_opts} --longoptions ${long_opts} --name "$0" -- "$@") + local POSITIONAL=() + local attempts="3" + local verbose=false + eval set -- "${parsed_opts}" + + while [[ $# -gt 0 ]]; do + case "$1" in + -s | --server) + serverid="$2" + shift + ;; + -l | --list) + /usr/bin/speedtest --version | grep -q official && sudo /usr/bin/speedtest -L || /usr/bin/speedtest --secure --list 2>&1 + exit 0 + ;; + -o | --output) + database="$2" + shift + ;; + -a | --attempts) + attempts="$2" + shift + ;; + -x | --verbose) verbose=true ;; + -h | --help) help ;; + *) POSITIONAL+=("$1") ;; + esac + shift + done + + set -- "${POSITIONAL[@]}" + [[ "$attempts" =~ ^[0-9]+$ ]] || attempts="3" + ! $verbose || set -x + run $attempts + echo "$?" >"$run_status" +} + +if [[ $EUID != 0 ]]; then + sudo "$0" "$@" + exit $? +fi + +rm -f "$OUT_FILE" +touch "$OUT_FILE" +main "$@" 2>&1 | tee -a "$OUT_FILE" +mv -f "$OUT_FILE" /var/log/pihole/speedtest-run.log || rm -f "$OUT_FILE" +exit_code=$(<"$run_status") +rm -f "$run_status" +[[ "$exit_code" -eq 1 ]] && exit 1 || exit 0 diff --git a/advanced/Scripts/update.sh b/advanced/Scripts/update.sh index ce1478ab0b..8c6f59c349 100755 --- a/advanced/Scripts/update.sh +++ b/advanced/Scripts/update.sh @@ -17,7 +17,7 @@ readonly PI_HOLE_GIT_URL="https://github.com/arevindh/pi-hole.git" readonly PI_HOLE_FILES_DIR="/etc/.pihole" # shellcheck disable=SC2034 -PH_TEST=true +SKIP_INSTALL=true # when --check-only is passed to this script, it will not perform the actual update CHECK_ONLY=false @@ -216,9 +216,8 @@ main() { fi if [[ "${FTL_update}" == true || "${core_update}" == true || "${web_update}" == true ]]; then - # Force an update of the updatechecker + # Update local and remote versions via updatechecker /opt/pihole/updatecheck.sh - /opt/pihole/updatecheck.sh x remote echo -e " ${INFO} Local version file information updated." fi diff --git a/advanced/Scripts/updatecheck.sh b/advanced/Scripts/updatecheck.sh index afb03ebb7f..e71d73339b 100755 --- a/advanced/Scripts/updatecheck.sh +++ b/advanced/Scripts/updatecheck.sh @@ -8,87 +8,169 @@ # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. -# Credit: https://stackoverflow.com/a/46324904 -function json_extract() { - local key=$1 - local json=$2 - - local string_regex='"([^"\]|\\.)*"' - local number_regex='-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?' - local value_regex="${string_regex}|${number_regex}|true|false|null" - local pair_regex="\"${key}\"[[:space:]]*:[[:space:]]*(${value_regex})" - - if [[ ${json} =~ ${pair_regex} ]]; then - echo $(sed 's/^"\|"$//g' <<< "${BASH_REMATCH[1]}") - else - return 1 - fi -} - function get_local_branch() { # Return active branch cd "${1}" 2> /dev/null || return 1 - git rev-parse --abbrev-ref HEAD || return 1 + local foundBranch + foundBranch=$(git status --porcelain=2 -b | grep branch.head | awk '{print $3;}') + [[ $foundBranch != *"("* ]] || foundBranch=$(git show-ref --heads | grep "$(git rev-parse HEAD)" | awk '{print $2;}' | cut -d '/' -f 3) + echo "${foundBranch:-HEAD}" } function get_local_version() { - # Return active branch + # Return active version cd "${1}" 2> /dev/null || return 1 - git describe --long --dirty --tags 2> /dev/null || return 1 + local tags + local foundVersion + tags=$(git ls-remote -t origin || git show-ref --tags) + foundVersion=$(git status --porcelain=2 -b | grep branch.oid | awk '{print $3;}') + [[ $foundVersion != *"("* ]] || foundVersion=$(git rev-parse HEAD 2>/dev/null) + local foundTag=$foundVersion + # shellcheck disable=SC2015 + grep -q "^$foundVersion" <<<"$tags" && foundTag=$(grep "^$foundVersion.*/v[0-9].*$" <<<"$tags" | awk '{print $2;}' | cut -d '/' -f 3 | sort -V | tail -n1) || : + [[ -z "${foundTag}" ]] || foundVersion="${foundTag}" + echo "${foundVersion}" +} + +function get_local_hash() { + cd "${1}" 2> /dev/null || return 1 + foundHash=$(git status --porcelain=2 -b | grep branch.oid | awk '{print $3;}') + [[ $foundHash != *"("* ]] || foundHash=$(git rev-parse HEAD 2>/dev/null) + echo "${foundHash}" +} + +function get_remote_version() { + if [[ "${1}" == "docker-pi-hole" || "${1}" == "FTL" ]]; then + curl -s "https://api.github.com/repos/pi-hole/${1}/releases/latest" 2> /dev/null | jq --raw-output .tag_name || return 1 + else + curl -s "https://api.github.com/repos/arevindh/${1}/releases/latest" 2> /dev/null | jq --raw-output .tag_name || { curl -s "https://api.github.com/repos/pi-hole/${1}/releases/latest" 2> /dev/null | jq --raw-output .tag_name || return 1; } + fi +} + +function get_remote_hash(){ + local foundHash="" + + for repo in "arevindh" "pi-hole" "ipitio"; do + [[ "${repo}" != "pi-hole" || "${1}" != "pihole-speedtest" ]] || continue + foundHash=$(git ls-remote "https://github.com/${repo}/${1}" --tags "${2}" | awk '{print $1;}' 2> /dev/null) + [[ -z "${foundHash}" ]] || break + done + + [[ -n "${foundHash}" ]] && echo "${foundHash}" || return 1 } # Source the setupvars config file # shellcheck disable=SC1091 . /etc/pihole/setupVars.conf -if [[ "$2" == "remote" ]]; then +# Source the utils file for addOrEditKeyValPair() +# shellcheck disable=SC1091 +. /opt/pihole/utils.sh + +# 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}" +chmod 644 "${VERSION_FILE}" + +# if /pihole.docker.tag file exists, we will use it's value later in this script +DOCKER_TAG=$(cat /pihole.docker.tag 2>/dev/null) +regex='^([0-9]+\.){1,2}(\*|[0-9]+)(-.*)?$|(^nightly$)|(^dev.*$)' +if [[ ! "${DOCKER_TAG}" =~ $regex ]]; then + # DOCKER_TAG does not match the pattern (see https://regex101.com/r/RsENuz/1), so unset it. + unset DOCKER_TAG +fi - if [[ "$3" == "reboot" ]]; then +# used in cronjob +if [[ "$1" == "reboot" ]]; then sleep 30 - fi +fi - GITHUB_VERSION_FILE="/etc/pihole/GitHubVersions" - GITHUB_CORE_VERSION="$(json_extract tag_name "$(curl -s 'https://api.github.com/repos/pi-hole/pi-hole/releases/latest' 2> /dev/null)")" - echo -n "${GITHUB_CORE_VERSION}" > "${GITHUB_VERSION_FILE}" - chmod 644 "${GITHUB_VERSION_FILE}" +# get Core versions - if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then - GITHUB_WEB_VERSION="$(json_extract tag_name "$(curl -s 'https://api.github.com/repos/pi-hole/AdminLTE/releases/latest' 2> /dev/null)")" - echo -n " ${GITHUB_WEB_VERSION}" >> "${GITHUB_VERSION_FILE}" - fi +CORE_VERSION="$(get_local_version /etc/.pihole)" +addOrEditKeyValPair "${VERSION_FILE}" "CORE_VERSION" "${CORE_VERSION}" - GITHUB_FTL_VERSION="$(json_extract tag_name "$(curl -s 'https://api.github.com/repos/pi-hole/FTL/releases/latest' 2> /dev/null)")" - echo -n " ${GITHUB_FTL_VERSION}" >> "${GITHUB_VERSION_FILE}" +CORE_BRANCH="$(get_local_branch /etc/.pihole)" +addOrEditKeyValPair "${VERSION_FILE}" "CORE_BRANCH" "${CORE_BRANCH}" -else +CORE_HASH="$(get_local_hash /etc/.pihole)" +addOrEditKeyValPair "${VERSION_FILE}" "CORE_HASH" "${CORE_HASH}" - LOCAL_BRANCH_FILE="/etc/pihole/localbranches" +GITHUB_CORE_VERSION="$(get_remote_version pi-hole)" +addOrEditKeyValPair "${VERSION_FILE}" "GITHUB_CORE_VERSION" "${GITHUB_CORE_VERSION}" - CORE_BRANCH="$(get_local_branch /etc/.pihole)" - echo -n "${CORE_BRANCH}" > "${LOCAL_BRANCH_FILE}" - chmod 644 "${LOCAL_BRANCH_FILE}" +GITHUB_CORE_HASH="$(get_remote_hash pi-hole "${CORE_BRANCH}")" +addOrEditKeyValPair "${VERSION_FILE}" "GITHUB_CORE_HASH" "${GITHUB_CORE_HASH}" - if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then - WEB_BRANCH="$(get_local_branch /var/www/html/admin)" - echo -n " ${WEB_BRANCH}" >> "${LOCAL_BRANCH_FILE}" - fi - FTL_BRANCH="$(pihole-FTL branch)" - echo -n " ${FTL_BRANCH}" >> "${LOCAL_BRANCH_FILE}" +# get Web versions - LOCAL_VERSION_FILE="/etc/pihole/localversions" +if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then - CORE_VERSION="$(get_local_version /etc/.pihole)" - echo -n "${CORE_VERSION}" > "${LOCAL_VERSION_FILE}" - chmod 644 "${LOCAL_VERSION_FILE}" + WEB_VERSION="$(get_local_version /var/www/html/admin)" + addOrEditKeyValPair "${VERSION_FILE}" "WEB_VERSION" "${WEB_VERSION}" - if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then - WEB_VERSION="$(get_local_version /var/www/html/admin)" - echo -n " ${WEB_VERSION}" >> "${LOCAL_VERSION_FILE}" - fi + WEB_BRANCH="$(get_local_branch /var/www/html/admin)" + addOrEditKeyValPair "${VERSION_FILE}" "WEB_BRANCH" "${WEB_BRANCH}" + + WEB_HASH="$(get_local_hash /var/www/html/admin)" + addOrEditKeyValPair "${VERSION_FILE}" "WEB_HASH" "${WEB_HASH}" + + GITHUB_WEB_VERSION="$(get_remote_version AdminLTE)" + addOrEditKeyValPair "${VERSION_FILE}" "GITHUB_WEB_VERSION" "${GITHUB_WEB_VERSION}" - FTL_VERSION="$(pihole-FTL version)" - echo -n " ${FTL_VERSION}" >> "${LOCAL_VERSION_FILE}" + GITHUB_WEB_HASH="$(get_remote_hash AdminLTE "${WEB_BRANCH}")" + addOrEditKeyValPair "${VERSION_FILE}" "GITHUB_WEB_HASH" "${GITHUB_WEB_HASH}" fi + +# get FTL versions + +FTL_VERSION="$(pihole-FTL version)" +addOrEditKeyValPair "${VERSION_FILE}" "FTL_VERSION" "${FTL_VERSION}" + +FTL_BRANCH="$(pihole-FTL branch)" +addOrEditKeyValPair "${VERSION_FILE}" "FTL_BRANCH" "${FTL_BRANCH}" + +FTL_HASH="$(pihole-FTL --hash)" +addOrEditKeyValPair "${VERSION_FILE}" "FTL_HASH" "${FTL_HASH}" + +GITHUB_FTL_VERSION="$(get_remote_version FTL)" +addOrEditKeyValPair "${VERSION_FILE}" "GITHUB_FTL_VERSION" "${GITHUB_FTL_VERSION}" + +GITHUB_FTL_HASH="$(get_remote_hash FTL "${FTL_BRANCH}")" +addOrEditKeyValPair "${VERSION_FILE}" "GITHUB_FTL_HASH" "${GITHUB_FTL_HASH}" + + +# get Docker versions + +if [[ "${DOCKER_TAG}" ]]; then + addOrEditKeyValPair "${VERSION_FILE}" "DOCKER_VERSION" "${DOCKER_TAG}" + + GITHUB_DOCKER_VERSION="$(get_remote_version docker-pi-hole)" + addOrEditKeyValPair "${VERSION_FILE}" "GITHUB_DOCKER_VERSION" "${GITHUB_DOCKER_VERSION}" +fi + + +# get Speedtest versions + +SPEEDTEST_VERSION="$(get_local_version /etc/pihole-speedtest)" +addOrEditKeyValPair "${VERSION_FILE}" "SPEEDTEST_VERSION" "${SPEEDTEST_VERSION}" + +SPEEDTEST_BRANCH="$(get_local_branch /etc/pihole-speedtest)" +addOrEditKeyValPair "${VERSION_FILE}" "SPEEDTEST_BRANCH" "${SPEEDTEST_BRANCH}" + +SPEEDTEST_HASH="$(get_local_hash /etc/pihole-speedtest)" +addOrEditKeyValPair "${VERSION_FILE}" "SPEEDTEST_HASH" "${SPEEDTEST_HASH}" + +GITHUB_SPEEDTEST_VERSION="$(get_remote_version pihole-speedtest)" +addOrEditKeyValPair "${VERSION_FILE}" "GITHUB_SPEEDTEST_VERSION" "${GITHUB_SPEEDTEST_VERSION}" + +GITHUB_SPEEDTEST_HASH="$(get_remote_hash pihole-speedtest "${SPEEDTEST_BRANCH}")" +addOrEditKeyValPair "${VERSION_FILE}" "GITHUB_SPEEDTEST_HASH" "${GITHUB_SPEEDTEST_HASH}" diff --git a/advanced/Scripts/utils.sh b/advanced/Scripts/utils.sh index f0a7cc3739..f655e56ce0 100755 --- a/advanced/Scripts/utils.sh +++ b/advanced/Scripts/utils.sh @@ -12,7 +12,7 @@ # Basic Housekeeping rules # - Functions must be self contained -# - Functions must be added in alphabetical order +# - Functions should be grouped with other similar functions # - Functions must be documented # - New functions must have a test added for them in test/test_any_utils.py @@ -31,9 +31,12 @@ 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}" + # Key already exists in file, modify the value + sed -i "/^${key}=/c\\${key}=${value}" "${file}" else # Key does not already exist, add it and it's value echo "${key}=${value}" >> "${file}" @@ -51,9 +54,16 @@ addKey(){ local file="${1}" local key="${2}" - if ! grep -q "^${key}" "${file}"; then - # Key does not exist, add it. - echo "${key}" >> "${file}" + # touch file to prevent grep error if file does not exist yet + touch "${file}" + + # Match key against entire line, using both anchors. We assume + # that the file's keys never have bounding whitespace. Anchors + # are necessary to ensure the key is considered absent when it + # is a substring of another key present in the file. + if ! grep -q "^${key}$" "${file}"; then + # Key does not exist, add it. + echo "${key}" >> "${file}" fi } @@ -70,29 +80,68 @@ removeKey() { sed -i "/^${key}/d" "${file}" } + ####################### -# returns FTL's current telnet API port -####################### +# returns FTL's current telnet API port based on the setting in /etc/pihole-FTL.conf +######################## getFTLAPIPort(){ + local FTLCONFFILE="/etc/pihole/pihole-FTL.conf" + local DEFAULT_FTL_PORT=4711 + local ftl_api_port + + if [ -s "$FTLCONFFILE" ]; then + # if FTLPORT is not set in pihole-FTL.conf, use the default port + ftl_api_port="$({ grep '^FTLPORT=' "${FTLCONFFILE}" || echo "${DEFAULT_FTL_PORT}"; } | cut -d'=' -f2-)" + # Exploit prevention: set the port to the default port if there is malicious (non-numeric) + # content set in pihole-FTL.conf + expr "${ftl_api_port}" : "[^[:digit:]]" > /dev/null && ftl_api_port="${DEFAULT_FTL_PORT}" + else + # if there is no pihole-FTL.conf, use the default port + ftl_api_port="${DEFAULT_FTL_PORT}" + fi + + echo "${ftl_api_port}" +} + +####################### +# returns path of FTL's PID file +####################### +getFTLPIDFile() { local FTLCONFFILE="/etc/pihole/pihole-FTL.conf" - local DEFAULT_PORT_FILE="/run/pihole-FTL.port" - local DEFAULT_FTL_PORT=4711 - local PORTFILE - local ftl_api_port - - if [ -f "$FTLCONFFILE" ]; then - # if PORTFILE is not set in pihole-FTL.conf, use the default path - PORTFILE="$( (grep "^PORTFILE=" $FTLCONFFILE || echo "$DEFAULT_PORT_FILE") | cut -d"=" -f2-)" - fi + local DEFAULT_PID_FILE="/run/pihole-FTL.pid" + local FTL_PID_FILE - if [ -s "$PORTFILE" ]; then - # -s: FILE exists and has a size greater than zero - ftl_api_port=$(cat "${PORTFILE}") - # Exploit prevention: unset the variable if there is malicious content - # Verify that the value read from the file is numeric - expr "$ftl_api_port" : "[^[:digit:]]" > /dev/null && unset ftl_api_port + if [ -s "${FTLCONFFILE}" ]; then + # if PIDFILE is not set in pihole-FTL.conf, use the default path + FTL_PID_FILE="$({ grep '^PIDFILE=' "${FTLCONFFILE}" || echo "${DEFAULT_PID_FILE}"; } | cut -d'=' -f2-)" + else + # if there is no pihole-FTL.conf, use the default path + FTL_PID_FILE="${DEFAULT_PID_FILE}" fi - # echo the port found in the portfile or default to the default port - echo "${ftl_api_port:=$DEFAULT_FTL_PORT}" + echo "${FTL_PID_FILE}" +} + +####################### +# returns FTL's PID based on the content of the pihole-FTL.pid file +# +# Takes one argument: path to pihole-FTL.pid +# Example getFTLPID "/run/pihole-FTL.pid" +####################### +getFTLPID() { + local FTL_PID_FILE="${1}" + local FTL_PID + + if [ -s "${FTL_PID_FILE}" ]; then + # -s: FILE exists and has a size greater than zero + FTL_PID="$(cat "${FTL_PID_FILE}")" + # Exploit prevention: unset the variable if there is malicious content + # Verify that the value read from the file is numeric + expr "${FTL_PID}" : "[^[:digit:]]" > /dev/null && unset FTL_PID + fi + + # If FTL is not running, or the PID file contains malicious stuff, substitute + # negative PID to signal this + FTL_PID=${FTL_PID:=-1} + echo "${FTL_PID}" } diff --git a/advanced/Scripts/version.sh b/advanced/Scripts/version.sh index 14d41e9242..cc76ba3155 100755 --- a/advanced/Scripts/version.sh +++ b/advanced/Scripts/version.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh # Pi-hole: A black hole for Internet advertisements # (c) 2017 Pi-hole, LLC (https://pi-hole.net) # Network-wide ad blocking via your own hardware. @@ -8,183 +8,109 @@ # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. -# Variables -DEFAULT="-1" -COREGITDIR="/etc/.pihole/" -WEBGITDIR="/var/www/html/admin/" - # Source the setupvars config file # shellcheck disable=SC1091 -source /etc/pihole/setupVars.conf +. /etc/pihole/setupVars.conf -getLocalVersion() { - # FTL requires a different method - if [[ "$1" == "FTL" ]]; then - pihole-FTL version - return 0 - fi +# Source the versions file poupulated by updatechecker.sh +cachedVersions="/etc/pihole/versions" - # Get the tagged version of the local repository - local directory="${1}" - local version +if [ -f ${cachedVersions} ]; then + # shellcheck disable=SC1090 + . "$cachedVersions" +else + echo "Could not find /etc/pihole/versions. Running update now." + pihole updatechecker + # shellcheck disable=SC1090 + . "$cachedVersions" +fi - cd "${directory}" 2> /dev/null || { echo "${DEFAULT}"; return 1; } - version=$(git describe --tags --always || echo "$DEFAULT") - if [[ "${version}" =~ ^v ]]; then - echo "${version}" - elif [[ "${version}" == "${DEFAULT}" ]]; then - echo "ERROR" - return 1 - else - echo "Untagged" - fi - return 0 +getLocalVersion() { + case ${1} in + "Pi-hole" ) echo "${CORE_VERSION:=N/A}";; + "web" ) [ "${INSTALL_WEB_INTERFACE}" = true ] && echo "${WEB_VERSION:=N/A}";; + "FTL" ) echo "${FTL_VERSION:=N/A}";; + "Speedtest" ) echo "${SPEEDTEST_VERSION:=N/A}";; + esac } getLocalHash() { - # Local FTL hash does not exist on filesystem - if [[ "$1" == "FTL" ]]; then - echo "N/A" - return 0 - fi - - # Get the short hash of the local repository - local directory="${1}" - local hash - - cd "${directory}" 2> /dev/null || { echo "${DEFAULT}"; return 1; } - hash=$(git rev-parse --short HEAD || echo "$DEFAULT") - if [[ "${hash}" == "${DEFAULT}" ]]; then - echo "ERROR" - return 1 - else - echo "${hash}" - fi - return 0 + case ${1} in + "Pi-hole" ) echo "${CORE_HASH:=N/A}";; + "web" ) [ "${INSTALL_WEB_INTERFACE}" = true ] && echo "${WEB_HASH:=N/A}";; + "FTL" ) echo "${FTL_HASH:=N/A}";; + "Speedtest" ) echo "${SPEEDTEST_HASH:=N/A}";; + esac } getRemoteHash(){ - # Remote FTL hash is not applicable - if [[ "$1" == "FTL" ]]; then - echo "N/A" - return 0 - fi - - local daemon="${1}" - local branch="${2}" - - hash=$(git ls-remote --heads "https://github.com/pi-hole/${daemon}" | \ - awk -v bra="$branch" '$0~bra {print substr($0,0,8);exit}') - if [[ -n "$hash" ]]; then - echo "$hash" - else - echo "ERROR" - return 1 - fi - return 0 + case ${1} in + "Pi-hole" ) echo "${GITHUB_CORE_HASH:=N/A}";; + "web" ) [ "${INSTALL_WEB_INTERFACE}" = true ] && echo "${GITHUB_WEB_HASH:=N/A}";; + "FTL" ) echo "${GITHUB_FTL_HASH:=N/A}";; + "Speedtest" ) echo "${GITHUB_SPEEDTEST_HASH:=N/A}";; + esac } getRemoteVersion(){ - # Get the version from the remote origin - local daemon="${1}" - local version - local cachedVersions - local arrCache - local owner="pi-hole" - cachedVersions="/etc/pihole/GitHubVersions" - - #If the above file exists, then we can read from that. Prevents overuse of GitHub API - if [[ -f "$cachedVersions" ]]; then - IFS=' ' read -r -a arrCache < "$cachedVersions" - - case $daemon in - "pi-hole" ) echo "${arrCache[0]}";; - "AdminLTE" ) [[ "${INSTALL_WEB_INTERFACE}" == true ]] && echo "${arrCache[1]}";; - "FTL" ) [[ "${INSTALL_WEB_INTERFACE}" == true ]] && echo "${arrCache[2]}" || echo "${arrCache[1]}";; - esac - - return 0 - fi - - if [[ "$daemon" == "AdminLTE" ]]; then - owner="arevindh" - fi - - version=$(curl --silent --fail "https://api.github.com/repos/${owner}/${daemon}/releases/latest" | \ - awk -F: '$1 ~/tag_name/ { print $2 }' | \ - tr -cd '[[:alnum:]]._-') - if [[ "${version}" =~ ^v ]]; then - echo "${version}" - else - echo "ERROR" - return 1 - fi - return 0 + case ${1} in + "Pi-hole" ) echo "${GITHUB_CORE_VERSION:=N/A}";; + "web" ) [ "${INSTALL_WEB_INTERFACE}" = true ] && echo "${GITHUB_WEB_VERSION:=N/A}";; + "FTL" ) echo "${GITHUB_FTL_VERSION:=N/A}";; + "Speedtest" ) echo "${GITHUB_SPEEDTEST_VERSION:=N/A}";; + esac } getLocalBranch(){ - # Get the checked out branch of the local directory - local directory="${1}" - local branch - - # Local FTL btranch is stored in /etc/pihole/ftlbranch - if [[ "$1" == "FTL" ]]; then - branch="$(pihole-FTL branch)" - else - cd "${directory}" 2> /dev/null || { echo "${DEFAULT}"; return 1; } - branch=$(git rev-parse --abbrev-ref HEAD || echo "$DEFAULT") - fi - if [[ ! "${branch}" =~ ^v ]]; then - if [[ "${branch}" == "master" ]]; then - echo "" - elif [[ "${branch}" == "HEAD" ]]; then - echo "in detached HEAD state at " - else - echo "${branch} " - fi - else - # Branch started in "v" - echo "release " - fi - return 0 + case ${1} in + "Pi-hole" ) echo "${CORE_BRANCH:=N/A}";; + "web" ) [ "${INSTALL_WEB_INTERFACE}" = true ] && echo "${WEB_BRANCH:=N/A}";; + "FTL" ) echo "${FTL_BRANCH:=N/A}";; + "Speedtest" ) echo "${SPEEDTEST_BRANCH:=N/A}";; + esac } versionOutput() { - if [[ "$1" == "AdminLTE" && "${INSTALL_WEB_INTERFACE}" != true ]]; then + if [ "$1" = "web" ] && [ "${INSTALL_WEB_INTERFACE}" != true ]; then echo " WebAdmin not installed" return 1 fi - [[ "$1" == "pi-hole" ]] && GITDIR=$COREGITDIR - [[ "$1" == "AdminLTE" ]] && GITDIR=$WEBGITDIR - [[ "$1" == "FTL" ]] && GITDIR="FTL" + [ "$2" = "-c" ] || [ "$2" = "--current" ] || [ -z "$2" ] && current=$(getLocalVersion "${1}") && branch=$(getLocalBranch "${1}") + [ "$2" = "-l" ] || [ "$2" = "--latest" ] || [ -z "$2" ] && latest=$(getRemoteVersion "${1}") + if [ "$2" = "--hash" ]; then + [ "$3" = "-c" ] || [ "$3" = "--current" ] || [ -z "$3" ] && curHash=$(getLocalHash "${1}") && branch=$(getLocalBranch "${1}") + [ "$3" = "-l" ] || [ "$3" = "--latest" ] || [ -z "$3" ] && latHash=$(getRemoteHash "${1}") && branch=$(getLocalBranch "${1}") + fi - [[ "$2" == "-c" ]] || [[ "$2" == "--current" ]] || [[ -z "$2" ]] && current=$(getLocalVersion $GITDIR) && branch=$(getLocalBranch $GITDIR) - [[ "$2" == "-l" ]] || [[ "$2" == "--latest" ]] || [[ -z "$2" ]] && latest=$(getRemoteVersion "$1") - if [[ "$2" == "-h" ]] || [[ "$2" == "--hash" ]]; then - [[ "$3" == "-c" ]] || [[ "$3" == "--current" ]] || [[ -z "$3" ]] && curHash=$(getLocalHash "$GITDIR") && branch=$(getLocalBranch $GITDIR) - [[ "$3" == "-l" ]] || [[ "$3" == "--latest" ]] || [[ -z "$3" ]] && latHash=$(getRemoteHash "$1" "$(cd "$GITDIR" 2> /dev/null && git rev-parse --abbrev-ref HEAD)") + # We do not want to show the branch name when we are on master, + # blank out the variable in this case + if [ "$branch" = "master" ]; then + branch="" + else + branch="$branch " fi - if [[ -n "$current" ]] && [[ -n "$latest" ]]; then - output="${1^} version is $branch$current (Latest: $latest)" - elif [[ -n "$current" ]] && [[ -z "$latest" ]]; then - output="Current ${1^} version is $branch$current" - elif [[ -z "$current" ]] && [[ -n "$latest" ]]; then - output="Latest ${1^} version is $latest" - elif [[ "$curHash" == "N/A" ]] || [[ "$latHash" == "N/A" ]]; then - output="${1^} hash is not applicable" - elif [[ -n "$curHash" ]] && [[ -n "$latHash" ]]; then - output="${1^} hash is $curHash (Latest: $latHash)" - elif [[ -n "$curHash" ]] && [[ -z "$latHash" ]]; then - output="Current ${1^} hash is $curHash" - elif [[ -z "$curHash" ]] && [[ -n "$latHash" ]]; then - output="Latest ${1^} hash is $latHash" + + if [ -n "$current" ] && [ -n "$latest" ]; then + output="${1} version is $branch$current (Latest: $latest)" + elif [ -n "$current" ] && [ -z "$latest" ]; then + output="Current ${1} version is $branch$current" + elif [ -z "$current" ] && [ -n "$latest" ]; then + output="Latest ${1} version is $latest" + elif [ -n "$curHash" ] && [ -n "$latHash" ]; then + output="Local ${1} hash is $curHash (Remote: $latHash)" + elif [ -n "$curHash" ] && [ -z "$latHash" ]; then + output="Current local ${1} hash is $curHash" + elif [ -z "$curHash" ] && [ -n "$latHash" ]; then + output="Latest remote ${1} hash is $latHash" + elif [ -z "$curHash" ] && [ -z "$latHash" ]; then + output="Hashes for ${1} not available" else errorOutput return 1 fi - [[ -n "$output" ]] && echo " $output" + [ -n "$output" ] && echo " $output" } errorOutput() { @@ -193,13 +119,14 @@ errorOutput() { } defaultOutput() { - versionOutput "pi-hole" "$@" + versionOutput "Pi-hole" "$@" - if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then - versionOutput "AdminLTE" "$@" + if [ "${INSTALL_WEB_INTERFACE}" = true ]; then + versionOutput "web" "$@" fi versionOutput "FTL" "$@" + versionOutput "Speedtest" "$@" } helpFunc() { @@ -209,8 +136,9 @@ Show Pi-hole, Admin Console & FTL versions Repositories: -p, --pihole Only retrieve info regarding Pi-hole repository - -a, --admin Only retrieve info regarding AdminLTE repository + -a, --admin Only retrieve info regarding web repository -f, --ftl Only retrieve info regarding FTL repository + -s, --speedtest Only retrieve info regarding Speedtest repository Options: -c, --current Return the current version @@ -221,9 +149,10 @@ Options: } case "${1}" in - "-p" | "--pihole" ) shift; versionOutput "pi-hole" "$@";; - "-a" | "--admin" ) shift; versionOutput "AdminLTE" "$@";; + "-p" | "--pihole" ) shift; versionOutput "Pi-hole" "$@";; + "-a" | "--admin" ) shift; versionOutput "web" "$@";; "-f" | "--ftl" ) shift; versionOutput "FTL" "$@";; + "-s" | "--speedtest" ) shift; versionOutput "Speedtest" "$@";; "-h" | "--help" ) helpFunc;; * ) defaultOutput "$@";; esac diff --git a/advanced/Scripts/webpage.sh b/advanced/Scripts/webpage.sh index a032da720a..aa4c9c6373 100755 --- a/advanced/Scripts/webpage.sh +++ b/advanced/Scripts/webpage.sh @@ -19,17 +19,19 @@ readonly FTLconf="/etc/pihole/pihole-FTL.conf" readonly dhcpstaticconfig="/etc/dnsmasq.d/04-pihole-static-dhcp.conf" readonly dnscustomfile="/etc/pihole/custom.list" readonly dnscustomcnamefile="/etc/dnsmasq.d/05-pihole-custom-cname.conf" -readonly speedtestfile="/var/www/html/admin/scripts/pi-hole/speedtest/speedtest.sh" -readonly speedtestdb="/etc/pihole/speedtest.db" - readonly gravityDBfile="/etc/pihole/gravity.db" -# Source install script for ${setupVars}, ${PI_HOLE_BIN_DIR} and valid_ip() -readonly PI_HOLE_FILES_DIR="/etc/.pihole" -# shellcheck disable=SC2034 # used in basic-install -PH_TEST="true" -source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" +# speedtest mod +readonly speedtestmod="/opt/pihole/speedtestmod/mod.sh" +readonly speedtestfile="/opt/pihole/speedtestmod/speedtest.sh" + +readonly setupVars="/etc/pihole/setupVars.conf" +readonly PI_HOLE_BIN_DIR="/usr/local/bin" +# Root of the web server +readonly webroot="/var/www/html" + +# Source utils script utilsfile="/opt/pihole/utils.sh" source "${utilsfile}" @@ -48,15 +50,14 @@ Options: -c, celsius Set Celsius as preferred temperature unit -f, fahrenheit Set Fahrenheit as preferred temperature unit -k, kelvin Set Kelvin as preferred temperature unit - -e, email Set an administrative contact address for the Block Page -h, --help Show this help dialog -i, interface Specify dnsmasq's interface listening behavior - -s, speedtest Set speedtest intevel , user 0 to disable Speedtests use -sn to prevent logging to results list + -s, speedtest Set speedtest interval, add 0 to disable -sd Set speedtest display range - -sn Run speedtest now - -sm Speedtest Mode - -sc Clear speedtest data + -sm [options] Run the Mod Script in the background (tmux a -t pimod) + -sn [options] Run the Test Script in the background (tmux a -t pimodtest) -ss Set custom server + -st Set default speedtest chart type (line, bar) -l, privacylevel Set privacy level (0 = lowest, 3 = highest) -t, teleporter Backup configuration as an archive -t, teleporter myname.tar.gz Backup configuration to archive with name myname.tar.gz as specified" @@ -107,6 +108,47 @@ HashPassword() { echo "${return}" } +# Check an IP address to see if it is a valid one +valid_ip() { + # Local, named variables + local ip=${1} + local stat=1 + + # Regex matching one IPv4 component, i.e. an integer from 0 to 255. + # See https://tools.ietf.org/html/rfc1340 + local ipv4elem="(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)"; + # Regex matching an optional port (starting with '#') range of 1-65536 + local portelem="(#(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}|0))?"; + # Build a full IPv4 regex from the above subexpressions + local regex="^${ipv4elem}\\.${ipv4elem}\\.${ipv4elem}\\.${ipv4elem}${portelem}$" + + # Evaluate the regex, and return the result + [[ $ip =~ ${regex} ]] + + stat=$? + return "${stat}" +} + +valid_ip6() { + local ip=${1} + local stat=1 + + # Regex matching one IPv6 element, i.e. a hex value from 0000 to FFFF + local ipv6elem="[0-9a-fA-F]{1,4}" + # Regex matching an IPv6 CIDR, i.e. 1 to 128 + local v6cidr="(\\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}" + # Regex matching an optional port (starting with '#') range of 1-65536 + local portelem="(#(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}|0))?"; + # Build a full IPv6 regex from the above subexpressions + local regex="^(((${ipv6elem}))*((:${ipv6elem}))*::((${ipv6elem}))*((:${ipv6elem}))*|((${ipv6elem}))((:${ipv6elem})){7})${v6cidr}${portelem}$" + + # Evaluate the regex, and return the result + [[ ${ip} =~ ${regex} ]] + + stat=$? + return "${stat}" +} + SetWebPassword() { if [ "${SUDO_USER}" == "www-data" ]; then echo "Security measure: user www-data is not allowed to change webUI password!" @@ -304,7 +346,7 @@ trust-anchor=.,20326,8,2,E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC68345710423 # changes in the non-FQDN forwarding. This cannot be done in 01-pihole.conf # as we don't want to delete all local=/.../ lines so it's much safer to # simply rewrite the entire corresponding config file (which is what the - # DHCP settings subroutie is doing) + # DHCP settings subroutine is doing) ProcessDHCPSettings } @@ -314,7 +356,7 @@ SetDNSServers() { IFS=',' read -r -a array <<< "${args[2]}" for index in "${!array[@]}" do - # Replace possible "\#" by "#". This fixes AdminLTE#1427 + # Replace possible "\#" by "#". This fixes web#1427 local ip ip="${array[index]//\\#/#}" @@ -402,13 +444,8 @@ ProcessDHCPSettings() { if [[ "${DHCP_LEASETIME}" == "0" ]]; then leasetime="infinite" elif [[ "${DHCP_LEASETIME}" == "" ]]; then - leasetime="24" - addOrEditKeyValPair "${setupVars}" "DHCP_LEASETIME" "${leasetime}" - elif [[ "${DHCP_LEASETIME}" == "24h" ]]; then - #Installation is affected by known bug, introduced in a previous version. - #This will automatically clean up setupVars.conf and remove the unnecessary "h" - leasetime="24" - addOrEditKeyValPair "${setupVars}" "DHCP_LEASETIME" "${leasetime}" + leasetime="24h" + addOrEditKeyValPair "${setupVars}" "DHCP_LEASETIME" "24" else leasetime="${DHCP_LEASETIME}h" fi @@ -448,8 +485,8 @@ dhcp-leasefile=/etc/pihole/dhcp.leases echo "#quiet-dhcp6 #enable-ra dhcp-option=option6:dns-server,[::] -dhcp-range=::100,::1ff,constructor:${interface},ra-names,slaac,64,3600 -ra-param=*,0,0 +dhcp-range=::,constructor:${interface},ra-names,ra-stateless,64 + " >> "${dhcpconfig}" fi @@ -503,94 +540,86 @@ SetWebUILayout() { addOrEditKeyValPair "${setupVars}" "WEBUIBOXEDLAYOUT" "${args[2]}" } +ChangeSpeedTestSchedule() { + local interval="${args[2]%\.}" -ClearSpeedtestData(){ - mv $speedtestdb $speedtestdb"_old" - cp /var/www/html/admin/scripts/pi-hole/speedtest/speedtest.db $speedtestdb -} - -ChageSpeedTestSchedule(){ - if [[ "${args[2]}" =~ ^[0-9]+$ ]]; then - if [ "${args[2]}" -ge 0 -a "${args[2]}" -le 24 ]; then - change_setting "SPEEDTESTSCHEDULE" "${args[2]}" - SetCronTab ${args[2]} - fi - fi -} - -SpeedtestServer(){ - if [[ "${args[2]}" =~ ^[0-9]+$ ]]; then - change_setting "SPEEDTEST_SERVER" "${args[2]}" - # SetCronTab ${args[2]} - else - # Autoselect for invalid data - change_setting "SPEEDTEST_SERVER" "" - fi - -} + if [[ "${interval-}" =~ ^-?([0-9]+(\.[0-9]*)?|\.[0-9]+)$ ]]; then + if (($(echo "$interval < 0" | bc -l))); then + interval="0" + else + addOrEditKeyValPair "${setupVars}" "SPEEDTESTSCHEDULE" "$interval" + fi + else + interval=$(grep "SPEEDTESTSCHEDULE" "${setupVars}" | cut -f2 -d"=") + if [[ ! "${interval-}" =~ ^([0-9]+(\.[0-9]*)?|\.[0-9]+)$ ]]; then + interval="nan" + fi + fi -RunSpeedtestNow(){ - mkdir -p /tmp/speedtest - lockfile="/tmp/speedtest/lock" - if [ -f $speedtestdb ] - then - echo "" - else - cp /var/www/html/admin/scripts/pi-hole/speedtest/speedtest.db $speedtestdb - sleep 2 - fi - if [ -f "$lockfile" ] - then - echo "Speedtest is already in progress, is something went wrong delete this file - "$lockfile - else - touch $lockfile - if [[ "${args[2]}" == "-n" ]]; then - speedtest-cli + if [[ ! -d /run/systemd/system ]]; then + # shellcheck disable=SC1091 + source "/opt/pihole/speedtestmod/lib.sh" + generate_cron_job "$interval" + elif [[ "$interval" == "0" ]] || [[ "$interval" == "nan" ]]; then + systemctl disable --now pihole-speedtest.timer &>/dev/null else - echo "Testing Speed" - result=`$speedtestfile` - echo $result - rm $lockfile + # shellcheck disable=SC1091 + source "/opt/pihole/speedtestmod/lib.sh" + generate_systemd_service "$interval" fi - fi } -SpeedtestMode(){ - if [[ "${args[2]}" ]]; then - change_setting "SPEEDTEST_MODE" "${args[2]}" - else - # Autoselect for invalid data - change_setting "SPEEDTEST_MODE" "python" - fi +UpdateSpeedTestRange() { + [[ ! "${args[2]}" =~ ^-?[0-9]+$ || "${args[2]}" -lt -1 ]] || addOrEditKeyValPair "${setupVars}" "SPEEDTEST_CHART_DAYS" "${args[2]}" +} +UpdateSpeedTestChartType() { + local chart_type="line" + [[ ! "${args[2]}" =~ ^(bar|line)$ ]] || chart_type="${args[2]}" + addOrEditKeyValPair "${setupVars}" "SPEEDTEST_CHART_TYPE" "$chart_type" } +SpeedtestServer() { + local test_server="" + [[ ! "${args[2]}" =~ ^[0-9]+$ ]] || test_server="${args[2]}" + addOrEditKeyValPair "${setupVars}" "SPEEDTEST_SERVER" "$test_server" +} -SetCronTab() -{ - # Remove OLD - crontab -l > crontab.tmp || true +RunSpeedtestMod() { + # if there is a running session, wait for it to finish + while [[ $(tmux list-sessions 2>/dev/null | grep -c pimod) -gt 0 ]]; do + sleep 1 + ((counter++)) - if [[ "$1" == "0" ]]; then - sed -i '/speedtest/d' crontab.tmp - crontab crontab.tmp && rm -f crontab.tmp - else - sed -i '/speedtest/d' crontab.tmp + if [[ $counter -gt 300 ]]; then + tmux kill-session -t pimod + break + fi + done + + # discard indexes 0 and 1 from args + args=("${args[@]:2}") + tmux new-session -d -s pimod "sudo bash $speedtestmod ${args[*]}" +} - mode=$(sed -n -e '/SPEEDTEST_MODE/ s/.*\= *//p' $setupVars) +RunSpeedtestNow() { + # if the session is still running after 5 minutes, kill it + while [[ $(tmux list-sessions 2>/dev/null | grep -c pimodtest) -gt 0 ]]; do + sleep 1 + ((counter++)) - if [[ "$mode" =~ "official" ]]; then - speedtest_file="/var/www/html/admin/scripts/pi-hole/speedtest/speedtest-official.sh" - else - speedtest_file="/var/www/html/admin/scripts/pi-hole/speedtest/speedtest.sh" - fi + if [[ $counter -gt 300 ]]; then + tmux kill-session -t pimodtest + break + fi + done - newtab="0 */"${1}" * * * sudo \""${speedtest_file}"\" > /dev/null 2>&1" - printf '%s\n' "$newtab" >>crontab.tmp - crontab crontab.tmp && rm -f crontab.tmp - fi + # discard indexes 0 and 1 from args + args=("${args[@]:2}") + tmux new-session -d -s pimodtest "sudo bash $speedtestfile ${args[*]}" } + SetWebUITheme() { addOrEditKeyValPair "${setupVars}" "WEBTHEME" "${args[2]}" } @@ -619,13 +648,13 @@ CustomizeAdLists() { if CheckUrl "${address}"; then if [[ "${args[2]}" == "enable" ]]; then - pihole-FTL sqlite3 "${gravityDBfile}" "UPDATE adlist SET enabled = 1 WHERE address = '${address}'" + pihole-FTL sqlite3 -ni "${gravityDBfile}" "UPDATE adlist SET enabled = 1 WHERE address = '${address}'" elif [[ "${args[2]}" == "disable" ]]; then - pihole-FTL sqlite3 "${gravityDBfile}" "UPDATE adlist SET enabled = 0 WHERE address = '${address}'" + pihole-FTL sqlite3 -ni "${gravityDBfile}" "UPDATE adlist SET enabled = 0 WHERE address = '${address}'" elif [[ "${args[2]}" == "add" ]]; then - pihole-FTL sqlite3 "${gravityDBfile}" "INSERT OR IGNORE INTO adlist (address, comment) VALUES ('${address}', '${comment}')" + pihole-FTL sqlite3 -ni "${gravityDBfile}" "INSERT OR IGNORE INTO adlist (address, comment) VALUES ('${address}', '${comment}')" elif [[ "${args[2]}" == "del" ]]; then - pihole-FTL sqlite3 "${gravityDBfile}" "DELETE FROM adlist WHERE address = '${address}'" + pihole-FTL sqlite3 -ni "${gravityDBfile}" "DELETE FROM adlist WHERE address = '${address}'" else echo "Not permitted" return 1 @@ -636,33 +665,6 @@ CustomizeAdLists() { fi } -function UpdateSpeedTestRange(){ - if [[ "${args[2]}" =~ ^[0-9]+$ ]]; then - if [ "${args[2]}" -ge 0 -a "${args[2]}" -le 30 ]; then - change_setting "SPEEDTEST_CHART_DAYS" "${args[2]}" - fi - fi -} - -SetPrivacyMode() { - if [[ "${args[2]}" == "true" ]]; then - change_setting "API_PRIVACY_MODE" "true" - else - change_setting "API_PRIVACY_MODE" "false" - fi -} - -ResolutionSettings() { - typ="${args[2]}" - state="${args[3]}" - - if [[ "${typ}" == "forward" ]]; then - change_setting "API_GET_UPSTREAM_DNS_HOSTNAME" "${state}" - elif [[ "${typ}" == "clients" ]]; then - change_setting "API_GET_CLIENT_HOSTNAME" "${state}" - fi -} - AddDHCPStaticAddress() { mac="${args[2]}" ip="${args[3]}" @@ -691,37 +693,6 @@ RemoveDHCPStaticAddress() { } -SetAdminEmail() { - if [[ "${1}" == "-h" ]] || [[ "${1}" == "--help" ]]; then - echo "Usage: pihole -a email
-Example: 'pihole -a email admin@address.com' -Set an administrative contact address for the Block Page - -Options: - \"\" Empty: Remove admin contact - -h, --help Show this help dialog" - exit 0 - fi - - if [[ -n "${args[2]}" ]]; then - - # Sanitize email address in case of security issues - # Regex from https://stackoverflow.com/a/2138832/4065967 - local regex - regex="^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\$" - if [[ ! "${args[2]}" =~ ${regex} ]]; then - echo -e " ${CROSS} Invalid email address" - exit 0 - fi - - addOrEditKeyValPair "${setupVars}" "ADMIN_EMAIL" "${args[2]}" - echo -e " ${TICK} Setting admin contact to ${args[2]}" - else - addOrEditKeyValPair "${setupVars}" "ADMIN_EMAIL" "" - echo -e " ${TICK} Removing admin contact" - fi -} - SetListeningMode() { source "${setupVars}" @@ -773,7 +744,7 @@ Teleporter() { host="${host//./_}" filename="pi-hole-${host:-noname}-teleporter_${datetimestamp}.tar.gz" fi - php /var/www/html/admin/scripts/pi-hole/php/teleporter.php > "${filename}" + php "${webroot}/admin/scripts/pi-hole/php/teleporter.php" > "${filename}" } checkDomain() @@ -781,11 +752,19 @@ checkDomain() local domain validDomain # Convert to lowercase domain="${1,,}" - validDomain=$(grep -P "^((-|_)*[a-z\\d]((-|_)*[a-z\\d])*(-|_)*)(\\.(-|_)*([a-z\\d]((-|_)*[a-z\\d])*))*$" <<< "${domain}") # Valid chars check + validDomain=$(grep -P "^((-|_)*[a-z0-9]((-|_)*[a-z0-9])*(-|_)*)(\\.(-|_)*([a-z0-9]((-|_)*[a-z0-9])*))*$" <<< "${domain}") # Valid chars check validDomain=$(grep -P "^[^\\.]{1,63}(\\.[^\\.]{1,63})*$" <<< "${validDomain}") # Length of each label echo "${validDomain}" } +escapeDots() +{ + # SC suggest bashism ${variable//search/replace} + # shellcheck disable=SC2001 + escaped=$(echo "$1" | sed 's/\./\\./g') + echo "${escaped}" +} + addAudit() { shift # skip "-a" @@ -809,12 +788,12 @@ addAudit() done # Insert only the domain here. The date_added field will be # filled with its default value (date_added = current timestamp) - pihole-FTL sqlite3 "${gravityDBfile}" "INSERT INTO domain_audit (domain) VALUES ${domains};" + pihole-FTL sqlite3 -ni "${gravityDBfile}" "INSERT INTO domain_audit (domain) VALUES ${domains};" } clearAudit() { - pihole-FTL sqlite3 "${gravityDBfile}" "DELETE FROM domain_audit;" + pihole-FTL sqlite3 -ni "${gravityDBfile}" "DELETE FROM domain_audit;" } SetPrivacyLevel() { @@ -861,6 +840,7 @@ RemoveCustomDNSAddress() { validHost="$(checkDomain "${host}")" if [[ -n "${validHost}" ]]; then if valid_ip "${ip}" || valid_ip6 "${ip}" ; then + validHost=$(escapeDots "${validHost}") sed -i "/^${ip} ${validHost}$/Id" "${dnscustomfile}" else echo -e " ${CROSS} Invalid IP has been passed" @@ -888,7 +868,12 @@ AddCustomCNAMERecord() { if [[ -n "${validDomain}" ]]; then validTarget="$(checkDomain "${target}")" if [[ -n "${validTarget}" ]]; then - echo "cname=${validDomain},${validTarget}" >> "${dnscustomcnamefile}" + if [ "${validDomain}" = "${validTarget}" ]; then + echo " ${CROSS} Domain and target are the same. This would cause a DNS loop." + exit 1 + else + echo "cname=${validDomain},${validTarget}" >> "${dnscustomcnamefile}" + fi else echo " ${CROSS} Invalid Target Passed!" exit 1 @@ -914,7 +899,9 @@ RemoveCustomCNAMERecord() { if [[ -n "${validDomain}" ]]; then validTarget="$(checkDomain "${target}")" if [[ -n "${validTarget}" ]]; then - sed -i "/cname=${validDomain},${validTarget}$/Id" "${dnscustomcnamefile}" + validDomain=$(escapeDots "${validDomain}") + validTarget=$(escapeDots "${validTarget}") + sed -i "/^cname=${validDomain},${validTarget}$/Id" "${dnscustomcnamefile}" else echo " ${CROSS} Invalid Target Passed!" exit 1 @@ -969,19 +956,18 @@ main() { "-h" | "--help" ) helpFunc;; "addstaticdhcp" ) AddDHCPStaticAddress;; "removestaticdhcp" ) RemoveDHCPStaticAddress;; - "-e" | "email" ) SetAdminEmail "$3";; "-i" | "interface" ) SetListeningMode "$@";; "-t" | "teleporter" ) Teleporter;; "adlist" ) CustomizeAdLists;; "audit" ) addAudit "$@";; "clearaudit" ) clearAudit;; "-l" | "privacylevel" ) SetPrivacyLevel;; - "-s" | "speedtest" ) ChageSpeedTestSchedule;; + "-s" | "speedtest" ) ChangeSpeedTestSchedule;; "-sd" ) UpdateSpeedTestRange;; - "-sn" ) RunSpeedtestNow;; - "-sm" ) SpeedtestMode;; - "-sc" ) ClearSpeedtestData;; + "-sm" ) RunSpeedtestMod ;; + "-sn" ) RunSpeedtestNow ;; "-ss" ) SpeedtestServer;; + "-st" ) UpdateSpeedTestChartType;; "addcustomdns" ) AddCustomDNSAddress;; "removecustomdns" ) RemoveCustomDNSAddress;; "addcustomcname" ) AddCustomCNAMERecord;; @@ -989,8 +975,10 @@ main() { "ratelimit" ) SetRateLimit;; * ) helpFunc;; esac + shift - if [[ $# = 0 ]]; then + + if [[ $# = 0 ]]; then helpFunc fi } diff --git a/advanced/Templates/gravity_copy.sql b/advanced/Templates/gravity_copy.sql index 3bea731d1b..ed11b61a78 100644 --- a/advanced/Templates/gravity_copy.sql +++ b/advanced/Templates/gravity_copy.sql @@ -19,8 +19,6 @@ INSERT OR REPLACE INTO adlist SELECT * FROM OLD.adlist; DELETE FROM OLD.adlist_by_group WHERE adlist_id NOT IN (SELECT id FROM OLD.adlist); INSERT OR REPLACE INTO adlist_by_group SELECT * FROM OLD.adlist_by_group; -INSERT OR REPLACE INTO info SELECT * FROM OLD.info; - INSERT OR REPLACE INTO client SELECT * FROM OLD.client; DELETE FROM OLD.client_by_group WHERE client_id NOT IN (SELECT id FROM OLD.client); INSERT OR REPLACE INTO client_by_group SELECT * FROM OLD.client_by_group; diff --git a/advanced/Templates/logrotate b/advanced/Templates/logrotate index ffed910b9d..9a56b55297 100644 --- a/advanced/Templates/logrotate +++ b/advanced/Templates/logrotate @@ -1,4 +1,4 @@ -/var/log/pihole.log { +/var/log/pihole/pihole.log { # su # daily copytruncate @@ -9,7 +9,7 @@ nomail } -/var/log/pihole-FTL.log { +/var/log/pihole/FTL.log { # su # weekly copytruncate diff --git a/advanced/Templates/pihole-FTL-poststop.sh b/advanced/Templates/pihole-FTL-poststop.sh new file mode 100755 index 0000000000..ac3898d2f0 --- /dev/null +++ b/advanced/Templates/pihole-FTL-poststop.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env sh + +# Source utils.sh for getFTLPIDFile() +PI_HOLE_SCRIPT_DIR='/opt/pihole' +utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" +# shellcheck disable=SC1090 +. "${utilsfile}" + +# Get file paths +FTL_PID_FILE="$(getFTLPIDFile)" + +# Cleanup +rm -f /run/pihole/FTL.sock /dev/shm/FTL-* "${FTL_PID_FILE}" diff --git a/advanced/Templates/pihole-FTL-prestart.sh b/advanced/Templates/pihole-FTL-prestart.sh new file mode 100755 index 0000000000..ff4abf3abe --- /dev/null +++ b/advanced/Templates/pihole-FTL-prestart.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env sh + +# Source utils.sh for getFTLPIDFile() +PI_HOLE_SCRIPT_DIR='/opt/pihole' +utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" +# shellcheck disable=SC1090 +. "${utilsfile}" + +# Get file paths +FTL_PID_FILE="$(getFTLPIDFile)" + +# Touch files to ensure they exist (create if non-existing, preserve if existing) +# shellcheck disable=SC2174 +mkdir -pm 0755 /run/pihole /var/log/pihole +[ -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 644 -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 /etc/pihole/dhcp.leases ] || install -m 644 -o pihole -g pihole /dev/null /etc/pihole/dhcp.leases +# Ensure that permissions are set so that pihole-FTL can edit all necessary files +chown pihole:pihole /run/pihole /etc/pihole /var/log/pihole /var/log/pihole/FTL.log /var/log/pihole/pihole.log /etc/pihole/dhcp.leases +# Ensure that permissions are set so that pihole-FTL can edit the files. We ignore errors as the file may not (yet) exist +chmod -f 0644 /etc/pihole/macvendor.db /etc/pihole/dhcp.leases /var/log/pihole/FTL.log +chmod -f 0640 /var/log/pihole/pihole.log +# Chown database files to the user FTL runs as. We ignore errors as the files may not (yet) exist +chown -f pihole:pihole /etc/pihole/pihole-FTL.db /etc/pihole/gravity.db /etc/pihole/macvendor.db +# Chmod database file permissions so that the pihole group (web interface) can edit the file. We ignore errors as the files may not (yet) exist +chmod -f 0664 /etc/pihole/pihole-FTL.db + +# Backward compatibility for user-scripts that still expect log files in /var/log instead of /var/log/pihole +# Should be removed with Pi-hole v6.0 +if [ ! -f /var/log/pihole.log ]; then + ln -sf /var/log/pihole/pihole.log /var/log/pihole.log + chown -h pihole:pihole /var/log/pihole.log +fi +if [ ! -f /var/log/pihole-FTL.log ]; then + ln -sf /var/log/pihole/FTL.log /var/log/pihole-FTL.log + chown -h pihole:pihole /var/log/pihole-FTL.log +fi diff --git a/advanced/Templates/pihole-FTL.service b/advanced/Templates/pihole-FTL.service index 41ab801811..460339ae71 100644 --- a/advanced/Templates/pihole-FTL.service +++ b/advanced/Templates/pihole-FTL.service @@ -9,8 +9,23 @@ # Description: Enable service provided by pihole-FTL daemon ### END INIT INFO +# Source utils.sh for getFTLPIDFile(), getFTLPID() +PI_HOLE_SCRIPT_DIR="/opt/pihole" +utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" +# shellcheck disable=SC1090 +. "${utilsfile}" + + is_running() { - pgrep -xo "pihole-FTL" > /dev/null + if [ -d "/proc/${FTL_PID}" ]; then + return 0 + fi + return 1 +} + +cleanup() { + # Run post-stop script, which does cleanup among runtime files + sh "${PI_HOLE_SCRIPT_DIR}/pihole-FTL-poststop.sh" } @@ -19,27 +34,21 @@ start() { if is_running; then echo "pihole-FTL is already running" else - # Touch files to ensure they exist (create if non-existing, preserve if existing) - mkdir -pm 0755 /run/pihole - [ ! -f /run/pihole-FTL.pid ] && install -m 644 -o pihole -g pihole /dev/null /run/pihole-FTL.pid - [ ! -f /run/pihole-FTL.port ] && install -m 644 -o pihole -g pihole /dev/null /run/pihole-FTL.port - [ ! -f /var/log/pihole-FTL.log ] && install -m 644 -o pihole -g pihole /dev/null /var/log/pihole-FTL.log - [ ! -f /var/log/pihole.log ] && install -m 644 -o pihole -g pihole /dev/null /var/log/pihole.log - [ ! -f /etc/pihole/dhcp.leases ] && install -m 644 -o pihole -g pihole /dev/null /etc/pihole/dhcp.leases - # Ensure that permissions are set so that pihole-FTL can edit all necessary files - chown pihole:pihole /run/pihole /etc/pihole /var/log/pihole.log /var/log/pihole.log /etc/pihole/dhcp.leases - # Ensure that permissions are set so that pihole-FTL can edit the files. We ignore errors as the file may not (yet) exist - chmod -f 0644 /etc/pihole/macvendor.db /etc/pihole/dhcp.leases /var/log/pihole-FTL.log /var/log/pihole.log - # Chown database files to the user FTL runs as. We ignore errors as the files may not (yet) exist - chown -f pihole:pihole /etc/pihole/pihole-FTL.db /etc/pihole/gravity.db /etc/pihole/macvendor.db - # Chown database file permissions so that the pihole group (web interface) can edit the file. We ignore errors as the files may not (yet) exist - chmod -f 0664 /etc/pihole/pihole-FTL.db + # Run pre-start script, which pre-creates all expected files with correct permissions + sh "${PI_HOLE_SCRIPT_DIR}/pihole-FTL-prestart.sh" + if setcap CAP_NET_BIND_SERVICE,CAP_NET_RAW,CAP_NET_ADMIN,CAP_SYS_NICE,CAP_IPC_LOCK,CAP_CHOWN+eip "/usr/bin/pihole-FTL"; then su -s /bin/sh -c "/usr/bin/pihole-FTL" pihole else echo "Warning: Starting pihole-FTL as root because setting capabilities is not supported on this system" /usr/bin/pihole-FTL fi + rc=$? + # Cleanup if startup failed + if [ "${rc}" != 0 ]; then + cleanup + exit $rc + fi echo fi } @@ -47,7 +56,7 @@ start() { # Stop the service stop() { if is_running; then - pkill -xo "pihole-FTL" + kill "${FTL_PID}" for i in 1 2 3 4 5; do if ! is_running; then break @@ -60,16 +69,14 @@ stop() { if is_running; then echo "Not stopped; may still be shutting down or shutdown may have failed, killing now" - pkill -xo -9 "pihole-FTL" - exit 1 + kill -9 "${FTL_PID}" else echo "Stopped" fi else echo "Not running" fi - # Cleanup - rm -f /run/pihole/FTL.sock /dev/shm/FTL-* + cleanup echo } @@ -86,6 +93,16 @@ status() { ### main logic ### + +# catch sudden termination +trap 'cleanup; exit 1' INT HUP TERM ABRT + +# Get FTL's PID file path +FTL_PID_FILE="$(getFTLPIDFile)" + +# Get FTL's current PID +FTL_PID="$(getFTLPID "${FTL_PID_FILE}")" + case "$1" in stop) stop diff --git a/advanced/Templates/pihole-FTL.systemd b/advanced/Templates/pihole-FTL.systemd new file mode 100644 index 0000000000..2a11419906 --- /dev/null +++ b/advanced/Templates/pihole-FTL.systemd @@ -0,0 +1,41 @@ +[Unit] +Description=Pi-hole FTL +# This unit is supposed to indicate when network functionality is available, but it is only +# very weakly defined what that is supposed to mean, with one exception: at shutdown, a unit +# that is ordered after network-online.target will be stopped before the network +Wants=network-online.target +After=network-online.target +# A target that should be used as synchronization point for all host/network name service lookups. +# All services for which the availability of full host/network name resolution is essential should +# be ordered after this target, but not pull it in. +Wants=nss-lookup.target +Before=nss-lookup.target + +# Limit (re)start loop to 5 within 1 minute +StartLimitBurst=5 +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 + +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 + +# Use graceful shutdown with a reasonable timeout +TimeoutStopSec=10s + +# Make /usr, /boot, /etc and possibly some more folders read-only... +ProtectSystem=full +# ... except /etc/pihole +# This merely retains r/w access rights, it does not add any new. +# Must still be writable on the host! +ReadWriteDirectories=/etc/pihole + +[Install] +WantedBy=multi-user.target diff --git a/advanced/Templates/pihole.cron b/advanced/Templates/pihole.cron index 37724d2e81..c62d31ab3b 100644 --- a/advanced/Templates/pihole.cron +++ b/advanced/Templates/pihole.cron @@ -18,7 +18,7 @@ # early morning. Download any updates from the adlists # Squash output to log, then splat the log to stdout on error to allow for # standard crontab job error handling. -59 1 * * 7 root PATH="$PATH:/usr/sbin:/usr/local/bin/" pihole updateGravity >/var/log/pihole_updateGravity.log || cat /var/log/pihole_updateGravity.log +59 1 * * 7 root PATH="$PATH:/usr/sbin:/usr/local/bin/" pihole updateGravity >/var/log/pihole/pihole_updateGravity.log || cat /var/log/pihole/pihole_updateGravity.log # Pi-hole: Flush the log daily at 00:00 # The flush script will use logrotate if available @@ -28,9 +28,6 @@ @reboot root /usr/sbin/logrotate --state /var/lib/logrotate/pihole /etc/pihole/logrotate -# Pi-hole: Grab local version and branch every 10 minutes -*/10 * * * * root PATH="$PATH:/usr/sbin:/usr/local/bin/" pihole updatechecker local - -# Pi-hole: Grab remote version every 24 hours -59 17 * * * root PATH="$PATH:/usr/sbin:/usr/local/bin/" pihole updatechecker remote -@reboot root PATH="$PATH:/usr/sbin:/usr/local/bin/" pihole updatechecker remote reboot +# Pi-hole: Grab remote and local version every 24 hours +59 17 * * * root PATH="$PATH:/usr/sbin:/usr/local/bin/" pihole updatechecker +@reboot root PATH="$PATH:/usr/sbin:/usr/local/bin/" pihole updatechecker reboot diff --git a/advanced/bash-completion/pihole b/advanced/bash-completion/pihole index 25208a3577..29a3270de1 100644 --- a/advanced/bash-completion/pihole +++ b/advanced/bash-completion/pihole @@ -15,7 +15,7 @@ _pihole() { COMPREPLY=( $(compgen -W "${opts_lists}" -- ${cur}) ) ;; "admin") - opts_admin="celsius email fahrenheit interface kelvin password privacylevel" + opts_admin="celsius fahrenheit interface kelvin password privacylevel" COMPREPLY=( $(compgen -W "${opts_admin}" -- ${cur}) ) ;; "checkout") diff --git a/advanced/blockingpage.css b/advanced/blockingpage.css deleted file mode 100644 index 0cc7a65cb3..0000000000 --- a/advanced/blockingpage.css +++ /dev/null @@ -1,455 +0,0 @@ -/* Pi-hole: A black hole for Internet advertisements -* (c) 2017 Pi-hole, LLC (https://pi-hole.net) -* Network-wide ad blocking via your own hardware. -* -* This file is copyright under the latest version of the EUPL. -* Please see LICENSE file for your rights under this license. */ - -/* Text Customisation Options ======> */ -.title::before { content: "Website Blocked"; } -.altBtn::before { content: "Why am I here?"; } -.linkPH::before { content: "About Pi-hole"; } -.linkEmail::before { content: "Contact Admin"; } - -#bpOutput.add::before { content: "Info"; } -#bpOutput.add::after { content: "The domain is being whitelisted..."; } -#bpOutput.error::before, .unhandled::before { content: "Error"; } -#bpOutput.unhandled::after { content: "An unhandled exception occurred. This may happen when your browser is unable to load jQuery, or when the webserver is denying access to the Pi-hole API."; } -#bpOutput.success::before { content: "Success"; } -#bpOutput.success::after { content: "Website has been whitelisted! You may need to flush your DNS cache"; } - -.recentwl::before { content: "This site has been whitelisted. Please flush your DNS cache and/or restart your browser."; } -.unknown::before { content: "This website is not found in any of Pi-hole's blacklists. The reason you have arrived here is unknown."; } -.cname::before { content: "This site is an alias for "; } /* cname.com */ -.cname::after { content: ", which may be blocked by Pi-hole."; } - -.blacklist::before { content: "Manually Blacklisted"; } -.wildcard::before { content: "Manually Blacklisted by Wildcard"; } -.noblock::before { content: "Not found on any Blacklist"; } - -#bpBlock::before { content: "Access to the following website has been denied:"; } -#bpFlag::before { content: "This is primarily due to being flagged as:"; } - -#bpHelpTxt::before { content: "If you have an ongoing use for this website, please "; } -#bpHelpTxt a::before, #bpHelpTxt span::before { content: "ask the administrator"; } -#bpHelpTxt::after{ content: " of the Pi-hole on this network to have it whitelisted"; } - -#bpBack::before { content: "Back to safety"; } -#bpInfo::before { content: "Technical Info"; } -#bpFoundIn::before { content: "This site is found in "; } -#bpFoundIn span::after { content: " of "; } -#bpFoundIn::after { content: " lists:"; } -#bpWhitelist::before { content: "Whitelist"; } - -footer span::before { content: "Page generated on "; } - -/* Hide whitelisting form entirely */ -/* #bpWLButtons { display: none; } */ - -/* Text Customisation Options <=============================== */ - -/* http://necolas.github.io/normalize.css ======> */ -html { font-family: sans-serif; line-height: 1.15; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; } -body { margin: 0; } -article, aside, footer, header, nav, section { display: block; } -h1 { font-size: 2em; margin: 0.67em 0; } -figcaption, figure, main { display: block; } -figure { margin: 1em 40px; } -hr { box-sizing: content-box; height: 0; overflow: visible; } -pre { font-family: monospace, monospace; font-size: 1em; } -a { background-color: transparent; -webkit-text-decoration-skip: objects; } -a:active, a:hover { outline-width: 0; } -abbr[title] { border-bottom: none; text-decoration: underline; text-decoration: underline dotted; } -b, strong { font-weight: inherit; } -b, strong { font-weight: bolder; } -code, kbd, samp { font-family: monospace, monospace; font-size: 1em; } -dfn { font-style: italic; } -mark { background-color: #ff0; color: #000; } -small { font-size: 80%; } -sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } -sub { bottom: -0.25em; } -sup { top: -0.5em; } -audio, video { display: inline-block; } -audio:not([controls]) { display: none; height: 0; } -img { border-style: none; } -svg:not(:root) { overflow: hidden; } -button, input, optgroup, select, textarea { font-family: sans-serif; font-size: 100%; line-height: 1.15; margin: 0; } -button, input { overflow: visible; } -button, select { text-transform: none; } -button, html [type="button"], [type="reset"], [type="submit"] { -webkit-appearance: button; } -button::-moz-focus-inner, [type="button"]::-moz-focus-inner, [type="reset"]::-moz-focus-inner, [type="submit"]::-moz-focus-inner { border-style: none; padding: 0; } -button:-moz-focusring, [type="button"]:-moz-focusring, [type="reset"]:-moz-focusring, [type="submit"]:-moz-focusring { outline: 1px dotted ButtonText; } -fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; } -legend { box-sizing: border-box; color: inherit; display: table; max-width: 100%; padding: 0; white-space: normal; } -progress { display: inline-block; vertical-align: baseline; } -textarea { overflow: auto; } -[type="checkbox"], [type="radio"] { box-sizing: border-box; padding: 0; } -[type="number"]::-webkit-inner-spin-button, [type="number"]::-webkit-outer-spin-button { height: auto; } -[type="search"] { -webkit-appearance: textfield; outline-offset: -2px; } -[type="search"]::-webkit-search-cancel-button, [type="search"]::-webkit-search-decoration { -webkit-appearance: none; } -::-webkit-file-upload-button { -webkit-appearance: button; font: inherit; } -details, menu { display: block; } -summary { display: list-item; } -canvas { display: inline-block; } -template { display: none; } -[hidden] { display: none; } -/* Normalize.css <=============================== */ - -html { font-size: 62.5%; } - -a { color: #3c8dbc; text-decoration: none; } -a:hover { color: #72afda; text-decoration: underline; } -b { color: rgb(68, 68, 68); } -p { margin: 0; } - -label, .buttons a { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - -label, .buttons *:not([disabled]) { cursor: pointer; } - -/* Touch device dark tap highlight */ -header h1 a, label, .buttons * { -webkit-tap-highlight-color: transparent; } - -/* Webkit Focus Glow */ -textarea, input, button { outline: none; } - -@font-face { - font-family: "Source Sans Pro"; - font-style: normal; - font-weight: 400; - font-display: swap; - src: local("Source Sans Pro Regular"), local("SourceSansPro-Regular"), - url("/admin/style/vendor/SourceSansPro/source-sans-pro-v13-latin-regular.woff2") format("woff2"), - url("/admin/style/vendor/SourceSansPro/source-sans-pro-v13-latin-regular.woff") format("woff"); -} - -@font-face { - font-family: "Source Sans Pro"; - font-style: normal; - font-weight: 700; - font-display: swap; - src: local("Source Sans Pro Bold"), local("SourceSansPro-Bold"), - url("/admin/style/vendor/SourceSansPro/source-sans-pro-v13-latin-700.woff2") format("woff2"), - url("/admin/style/vendor/SourceSansPro/source-sans-pro-v13-latin-700.woff") format("woff"); -} - -body { - background: #dbdbdb url("/admin/img/boxed-bg.jpg") repeat fixed; - color: #333; - font: 1.4rem "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif; - line-height: 2.2rem; -} - -/* User is greeted with a splash page when browsing to Pi-hole IP address */ -#splashpage { - background: #222; - color: rgba(255, 255, 255, 0.7); - text-align: center; - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; -} - -#splashpage img { margin: 5px; width: 256px; } -#splashpage b { color: inherit; } - -#bpWrapper { - margin: 0 auto; - max-width: 1250px; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.5); -} - -header { - background: #3c8dbc; - display: table; - position: relative; - width: 100%; -} - -header h1, header h1 a, header .spc, header #bpAlt label { - display: table-cell; - color: #fff; - white-space: nowrap; - vertical-align: middle; - height: 50px; /* Must match #bpAbout top value */ -} - -h1 a { - background-color: rgba(0, 0, 0, 0.1); - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 2rem; - font-weight: 400; - min-width: 230px; - text-align: center; -} - -h1 a:hover, header #bpAlt:hover { background-color: rgba(0, 0, 0, 0.12); color: inherit; text-decoration: none; } - -header .spc { width: 100%; } - -header #bpAlt label { - background: url("/admin/img/logo.svg") no-repeat center left 15px; - background-size: 15px 23px; - padding: 0 15px; - text-indent: 30px; -} - -[type="checkbox"][id$="Toggle"] { display: none; } -[type="checkbox"][id$="Toggle"]:checked ~ #bpAbout, -[type="checkbox"][id$="Toggle"]:checked ~ #bpMoreInfo { - display: block; -} - -html, body { - height: 100%; -} - -#pihole_card { - width: 400px; - height: auto; - max-width: 400px; -} - - #pihole_card p, #pihole_card a { - font-size: 13pt; - text-align: center; - } - -#pihole_logo_splash { - height: auto; - width: 100%; -} - -/* Click anywhere else on screen to hide #bpAbout */ -#bpAboutToggle:checked { - display: block; - height: 300px; /* VH Fallback */ - height: 100vh; - left: 0; - top: 0; - opacity: 0; - position: absolute; - width: 100%; -} - -#bpAbout { - background: #3c8dbc; - border-bottom-left-radius: 5px; - border: 1px solid #fff; - border-right-width: 0; - box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.12); - box-sizing: border-box; - display: none; - font-size: 1.7rem; - top: 50px; - position: absolute; - right: 0; - width: 280px; - z-index: 1; -} - -.aboutPH { - box-sizing: border-box; - color: rgba(255, 255, 255, 0.8); - display: block; - padding: 10px; - width: 100%; - text-align: center; -} - -.aboutImg { - background: url("/admin/img/logo.svg") no-repeat center; - background-size: 90px 90px; - height: 90px; - margin: 0 auto; - padding: 2px; - width: 90px; -} - -.aboutPH p { margin: 10px 0; } -.aboutPH small { display: block; font-size: 1.2rem; } - -.aboutLink { - background: #fff; - border-top: 1px solid #ddd; - display: table; - font-size: 1.4rem; - text-align: center; - width: 100%; -} - -.aboutLink a { - display: table-cell; - padding: 14px; - min-width: 50%; -} - -main { - background: #ecf0f5; - font-size: 1.65rem; - padding: 10px; -} - -#bpOutput { - background: #00c0ef; - border-radius: 3px; - border: 1px solid rgba(0, 0, 0, 0.1); - color: #fff; - font-size: 1.4rem; - margin-bottom: 10px; - margin-top: 5px; - padding: 15px; -} - -#bpOutput::before { - background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='7' height='14' viewBox='0 0 7 14'%3E%3Cpath fill='%23fff' d='M6 11a1.371 1.371 0 011 1v1a1.371 1.371 0 01-1 1H1a1.371 1.371 0 01-1-1v-1a1.371 1.371 0 011-1h1V8H1a1.371 1.371 0 01-1-1V6a1.371 1.371 0 011-1h3a1.371 1.371 0 011 1v5h1zM3.5 0A1.5 1.5 0 112 1.5 1.5 1.5 0 013.5 0z'/%3E%3C/svg%3E") no-repeat center left; - display: block; - font-size: 1.8rem; - text-indent: 15px; -} - -#bpOutput.hidden { display: none; } -#bpOutput.success { background: #00a65a; } -#bpOutput.error { background: #dd4b39; } - -.blockMsg, .flagMsg { - font: 700 1.8rem Consolas, Courier, monospace; - padding: 5px 10px 10px; - text-indent: 15px; -} - -#bpHelpTxt { padding-bottom: 10px; } - -.buttons { - border-spacing: 5px 0; - display: table; - width: 100%; -} - -.buttons * { - -moz-appearance: none; - -webkit-appearance: none; - border-radius: 3px; - border: 1px solid rgba(0, 0, 0, 0.1); - box-sizing: content-box; - display: table-cell; - font-size: 1.65rem; - margin-right: 5px; - min-height: 20px; - padding: 6px 12px; - position: relative; - text-align: center; - vertical-align: top; - white-space: nowrap; - width: auto; -} - -.buttons a:hover { text-decoration: none; } - -/* Button hover dark overlay */ -.buttons *:not(input):not([disabled]):hover { - background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.1)); - color: #fff; -} - -/* Button active shadow inset */ -.buttons *:not([disabled]):not(input):active { - box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); -} - -/* Input border color */ -.buttons *:not([disabled]):hover, .buttons input:focus { - border-color: rgba(0, 0, 0, 0.25); -} - -#bpButtons * { width: 50%; color: #fff; } -#bpBack { background-color: #00a65a; } -#bpInfo { background-color: #3c8dbc; } -#bpWhitelist { background-color: #dd4b39; } - -#blockpage .buttons [type="password"][disabled] { color: rgba(0, 0, 0, 1); } -#blockpage .buttons [disabled] { color: rgba(0, 0, 0, 0.55); background-color: #e3e3e3; } -#blockpage .buttons [type="password"]:-ms-input-placeholder { color: rgba(51, 51, 51, 0.8); } - -input[type="password"] { font-size: 1.5rem; } - -@-webkit-keyframes slidein { from { max-height: 0; opacity: 0; } to { max-height: 300px; opacity: 1; } } - -@keyframes slidein { from { max-height: 0; opacity: 0; } to { max-height: 300px; opacity: 1; } } -#bpMoreToggle:checked ~ #bpMoreInfo { display: block; margin-top: 8px; -webkit-animation: slidein 0.05s linear; animation: slidein 0.05s linear; } -#bpMoreInfo { display: none; margin-top: 10px; } - -#bpQueryOutput { - font-size: 1.2rem; - line-height: 1.65rem; - margin: 5px 0 0; - overflow: auto; - padding: 0 5px; - -webkit-overflow-scrolling: touch; -} - -#bpQueryOutput span { margin-right: 4px; } - -#bpWLButtons { width: auto; margin-top: 10px; } -#bpWLButtons * { display: inline-block; } -#bpWLDomain { display: none; } -#bpWLPassword { width: 160px; } -#bpWhitelist { color: #fff; } - -footer { - background: #fff; - border-top: 1px solid #d2d6de; - color: #444; - font: 1.2rem Consolas, Courier, monospace; - padding: 8px; -} - -/* Responsive Content */ -@media only screen and (max-width: 500px) { - h1 a { - font-size: 1.8rem; - min-width: 170px; - } - - footer span::before { - content: "Generated "; - } - - footer span { - display: block; - } -} - -@media only screen and (min-width: 1251px) { - #bpWrapper, footer { - border-radius: 0 0 5px 5px; - } - - #bpAbout { - border-right-width: 1px; - } -} - -@media only screen and (max-width: 400px) { - #pihole_card { - width: 100%; - height: auto; - } - - #pihole_card p, #pihole_card a { - font-size: 100%; - } -} - -@media only screen and (max-width: 256px) { - #pihole_logo_splash { - width: 90% !important; - height: auto; - } -} diff --git a/advanced/cmdline.txt b/advanced/cmdline.txt deleted file mode 100644 index 84d52b79b0..0000000000 --- a/advanced/cmdline.txt +++ /dev/null @@ -1 +0,0 @@ -dwc_otg.lpm_enable=0 console=ttyAMA0,115200 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait fbcon=map:10 fbcon=font:VGA8x8 consoleblank=0 diff --git a/advanced/console-setup b/advanced/console-setup deleted file mode 100644 index f12be6eb44..0000000000 --- a/advanced/console-setup +++ /dev/null @@ -1,17 +0,0 @@ -# CONFIGURATION FILE FOR SETUPCON - -# Consult the console-setup(5) manual page. - -ACTIVE_CONSOLES="/dev/tty[1-6]" - -CHARMAP="UTF-8" - -# For best results with the Adafruit 2.8 LCD and Pi-hole's chronometer -CODESET="guess" -FONTFACE="Terminus" -FONTSIZE="10x20" - -VIDEOMODE= - -# The following is an example how to use a braille font -# FONT='lat9w-08.psf.gz brl-8x8.psf' diff --git a/advanced/dnsmasq.conf.original b/advanced/dnsmasq.conf.original index 6758f0b8e8..4aa5a8bfc8 100644 --- a/advanced/dnsmasq.conf.original +++ b/advanced/dnsmasq.conf.original @@ -507,7 +507,7 @@ # (using /etc/hosts) then that name can be specified as the # tftp_servername (the third option to dhcp-boot) and in that # case dnsmasq resolves this name and returns the resultant IP -# addresses in round robin fasion. This facility can be used to +# addresses in round robin fashion. This facility can be used to # load balance the tftp load among a set of servers. #dhcp-boot=/var/ftpd/pxelinux.0,boothost,tftp_server_name diff --git a/advanced/index.php b/advanced/index.php deleted file mode 100644 index cf0ab854b0..0000000000 --- a/advanced/index.php +++ /dev/null @@ -1,417 +0,0 @@ -/etc/pihole/setupVars.conf"); - -// Get values from setupVars.conf -$setupVars = parse_ini_file("/etc/pihole/setupVars.conf"); -$svPasswd = !empty($setupVars["WEBPASSWORD"]); -$svEmail = (!empty($setupVars["ADMIN_EMAIL"]) && filter_var($setupVars["ADMIN_EMAIL"], FILTER_VALIDATE_EMAIL)) ? $setupVars["ADMIN_EMAIL"] : ""; -unset($setupVars); - -// Set landing page location, found within /var/www/html/ -$landPage = "../landing.php"; - -// Define array for hostnames to be accepted as self address for splash page -$authorizedHosts = [ "localhost" ]; -if (!empty($_SERVER["FQDN"])) { - // If setenv.add-environment = ("fqdn" => "true") is configured in lighttpd, - // append $serverName to $authorizedHosts - array_push($authorizedHosts, $serverName); -} else if (!empty($_SERVER["VIRTUAL_HOST"])) { - // Append virtual hostname to $authorizedHosts - array_push($authorizedHosts, $_SERVER["VIRTUAL_HOST"]); -} - -// Set which extension types render as Block Page (Including "" for index.ext) -$validExtTypes = array("asp", "htm", "html", "php", "rss", "xml", ""); - -// Get extension of current URL -$currentUrlExt = pathinfo($_SERVER["REQUEST_URI"], PATHINFO_EXTENSION); - -// Set mobile friendly viewport -$viewPort = ''; - -// Set response header -function setHeader($type = "x") { - header("X-Pi-hole: A black hole for Internet advertisements."); - if (isset($type) && $type === "js") header("Content-Type: application/javascript"); -} - -// Determine block page type -if ($serverName === "pi.hole" - || (!empty($_SERVER["VIRTUAL_HOST"]) && $serverName === $_SERVER["VIRTUAL_HOST"])) { - // Redirect to Web Interface - exit(header("Location: /admin")); -} elseif (filter_var($serverName, FILTER_VALIDATE_IP) || in_array($serverName, $authorizedHosts)) { - // When directly browsing via IP or authorized hostname - // Render splash/landing page based off presence of $landPage file - // Unset variables so as to not be included in $landPage or $splashPage - unset($svPasswd, $svEmail, $authorizedHosts, $validExtTypes, $currentUrlExt); - // If $landPage file is present - if (is_file(getcwd()."/$landPage")) { - unset($serverName, $viewPort); // unset extra variables not to be included in $landpage - include $landPage; - exit(); - } - // If $landPage file was not present, Set Splash Page output - $splashPage = << - - - - $viewPort - ● $serverName - - - - -
- Pi-hole logo -

Pi-hole: Your black hole for Internet advertisements

- Did you mean to go to the admin panel? -
- - -EOT; - exit($splashPage); -} elseif ($currentUrlExt === "js") { - // Serve Pi-hole JavaScript for blocked domains requesting JS - exit(setHeader("js").'var x = "Pi-hole: A black hole for Internet advertisements."'); -} elseif (strpos($_SERVER["REQUEST_URI"], "?") !== FALSE && isset($_SERVER["HTTP_REFERER"])) { - // Serve blank image upon receiving REQUEST_URI w/ query string & HTTP_REFERRER - // e.g: An iframe of a blocked domain - exit(setHeader().' - - - - - - - - '); -} elseif (!in_array($currentUrlExt, $validExtTypes) || substr_count($_SERVER["REQUEST_URI"], "?")) { - // Serve SVG upon receiving non $validExtTypes URL extension or query string - // e.g: Not an iframe of a blocked domain, such as when browsing to a file/query directly - // QoL addition: Allow the SVG to be clicked on in order to quickly show the full Block Page - $blockImg = ' - - - - - Blocked by Pi-hole - - - '; - exit(setHeader()." - - - - $viewPort - - $blockImg - "); -} - -/* Start processing Block Page from here */ - -// Define admin email address text based off $svEmail presence -$bpAskAdmin = !empty($svEmail) ? '' : ""; - -// Get possible non-standard location of FTL's database -$FTLsettings = parse_ini_file("/etc/pihole/pihole-FTL.conf"); -if (isset($FTLsettings["GRAVITYDB"])) { - $gravityDBFile = $FTLsettings["GRAVITYDB"]; -} else { - $gravityDBFile = "/etc/pihole/gravity.db"; -} - -// Connect to gravity.db -try { - $db = new SQLite3($gravityDBFile, SQLITE3_OPEN_READONLY); -} catch (Exception $exception) { - die("[ERROR]: Failed to connect to gravity.db"); -} - -// Get all adlist addresses -$adlistResults = $db->query("SELECT address FROM vw_adlist"); -$adlistsUrls = array(); -while ($row = $adlistResults->fetchArray()) { - array_push($adlistsUrls, $row[0]); -} - -if (empty($adlistsUrls)) - die("[ERROR]: There are no adlists enabled"); - -// Get total number of blocklists (Including Whitelist, Blacklist & Wildcard lists) -$adlistsCount = count($adlistsUrls) + 3; - -// Set query timeout -ini_set("default_socket_timeout", 3); - -// Logic for querying blocklists -function queryAds($serverName) { - // Determine the time it takes while querying adlists - $preQueryTime = microtime(true)-$_SERVER["REQUEST_TIME_FLOAT"]; - - // Determine which protocol should be used - $protocol = "http"; - if ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') || - (isset($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] === 'https') || - (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') - ) { - $protocol = "https"; - } - - // Format the URL - $queryAdsURL = sprintf( - "%s://127.0.0.1:%s/admin/scripts/pi-hole/php/queryads.php?domain=%s&bp", - $protocol, - $_SERVER["SERVER_PORT"], - $serverName - ); - - // Request the file and receive the response - $queryAdsFile = file($queryAdsURL, FILE_IGNORE_NEW_LINES); - - // $queryAdsFile must be an array (to avoid PHP 8.0+ error) - if (is_array($queryAdsFile)) { - $queryAds = array_values(array_filter(preg_replace("/data:\s+/", "", $queryAdsFile))); - } else { - // if not an array, return an error message - return array("0" => "error", "1" => "
(".gettype($queryAdsFile).")
".print_r($queryAdsFile, true)); - } - - $queryTime = sprintf("%.0f", (microtime(true)-$_SERVER["REQUEST_TIME_FLOAT"]) - $preQueryTime); - - // Exception Handling - try { - // Define Exceptions - if (strpos($queryAds[0], "No exact results") !== FALSE) { - // Return "none" into $queryAds array - return array("0" => "none"); - } else if ($queryTime >= ini_get("default_socket_timeout")) { - // Connection Timeout - throw new Exception ("Connection timeout (".ini_get("default_socket_timeout")."s)"); - } elseif (!strpos($queryAds[0], ".") !== false) { - // Unknown $queryAds output - throw new Exception ("Unhandled error message ($queryAds[0])"); - } - return $queryAds; - } catch (Exception $e) { - // Return exception as array - return array("0" => "error", "1" => $e->getMessage()); - } -} - -// Get results of queryads.php exact search -$queryAds = queryAds($serverName); - -// Pass error through to Block Page -if ($queryAds[0] === "error") - die("[ERROR]: Unable to parse results from queryads.php: ".$queryAds[1].""); - -// Count total number of matching blocklists -$featuredTotal = count($queryAds); - -// Place results into key => value array -$queryResults = null; -foreach ($queryAds as $str) { - $value = explode(" ", $str); - @$queryResults[$value[0]] .= "$value[1]"; -} - -// Determine if domain has been blacklisted, whitelisted, wildcarded or CNAME blocked -if (strpos($queryAds[0], "blacklist") !== FALSE) { - $notableFlagClass = "blacklist"; - $adlistsUrls = array("π" => substr($queryAds[0], 2)); -} elseif (strpos($queryAds[0], "whitelist") !== FALSE) { - $notableFlagClass = "noblock"; - $adlistsUrls = array("π" => substr($queryAds[0], 2)); - $wlInfo = "recentwl"; -} elseif (strpos($queryAds[0], "wildcard") !== FALSE) { - $notableFlagClass = "wildcard"; - $adlistsUrls = array("π" => substr($queryAds[0], 2)); -} elseif ($queryAds[0] === "none") { - $featuredTotal = "0"; - $notableFlagClass = "noblock"; - - // QoL addition: Determine appropriate info message if CNAME exists - // Suggests to the user that $serverName has a CNAME (alias) that may be blocked - $dnsRecord = dns_get_record("$serverName")[0]; - if (array_key_exists("target", $dnsRecord)) { - $wlInfo = $dnsRecord['target']; - } else { - $wlInfo = "unknown"; - } -} - -// Set #bpOutput notification -$wlOutputClass = (isset($wlInfo) && $wlInfo === "recentwl") ? $wlInfo : "hidden"; -$wlOutput = (isset($wlInfo) && $wlInfo !== "recentwl") ? "$wlInfo" : ""; - -// Get Pi-hole Core version -$phVersion = exec("cd /etc/.pihole/ && git describe --long --tags"); - -// Print $execTime on development branches -// Testing for - is marginally faster than "git rev-parse --abbrev-ref HEAD" -if (explode("-", $phVersion)[1] != "0") - $execTime = microtime(true)-$_SERVER["REQUEST_TIME_FLOAT"]; - -// Please Note: Text is added via CSS to allow an admin to provide a localized -// language without the need to edit this file - -setHeader(); -?> - - - - - - - - - - - ● <?=$serverName ?> - - - -
-
-

- -

-
- - -
-
-
-

Open Source Ad Blocker - Designed for Raspberry Pi -

-
- -
- -
- -
-
- -
-
-
-

-
- -
-

-
- -
-
- - 0) echo ''; ?> -
- -
- -
 0) foreach ($queryResults as $num => $value) { echo "[$num]:$adlistsUrls[$num]\n"; } ?>
- -
- - - -
-
-
- -
. Pi-hole ()
-
- - - diff --git a/advanced/lighttpd.conf.debian b/advanced/lighttpd.conf.debian index b8656a2460..f31f7bcd82 100644 --- a/advanced/lighttpd.conf.debian +++ b/advanced/lighttpd.conf.debian @@ -7,17 +7,18 @@ # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. -############################################################################### -# FILE AUTOMATICALLY OVERWRITTEN BY PI-HOLE INSTALL/UPDATE PROCEDURE. # -# ANY CHANGES MADE TO THIS FILE AFTER INSTALL WILL BE LOST ON THE NEXT UPDATE # -# # -# CHANGES SHOULD BE MADE IN A SEPARATE CONFIG FILE: # -# /etc/lighttpd/external.conf # -############################################################################### +################################################################################################### +# IF THIS HEADER EXISTS, THE FILE WILL BE OVERWRITTEN BY PI-HOLE'S UPDATE PROCEDURE. # +# ANY CHANGES MADE TO THIS FILE WILL BE LOST ON THE NEXT UPDATE UNLESS YOU REMOVE THIS HEADER # +# # +# ENSURE THAT YOU DO NOT REMOVE THE REQUIRED LINE: # +# # +# include "/etc/lighttpd/conf-enabled/*.conf" # +# # +################################################################################################### server.modules = ( "mod_access", - "mod_accesslog", "mod_auth", "mod_expire", "mod_redirect", @@ -26,15 +27,14 @@ server.modules = ( ) server.document-root = "/var/www/html" -server.error-handler-404 = "/pihole/index.php" server.upload-dirs = ( "/var/cache/lighttpd/uploads" ) -server.errorlog = "/var/log/lighttpd/error.log" +server.errorlog = "/var/log/lighttpd/error-pihole.log" server.pid-file = "/run/lighttpd.pid" server.username = "www-data" server.groupname = "www-data" +# For lighttpd version 1.4.46 or above, the port can be overwritten in `/etc/lighttpd/external.conf` using the := operator +# e.g. server.port := 8000 server.port = 80 -accesslog.filename = "/var/log/lighttpd/access.log" -accesslog.format = "%{%s}t|%V|%r|%s|%b" # Allow streaming response # reference: https://redmine.lighttpd.net/projects/lighttpd/wiki/Server_stream-response-bodyDetails @@ -65,37 +65,9 @@ mimetype.assign = ( ".woff2" => "font/woff2" ) -# Add user chosen options held in external file -# This uses include_shell instead of an include wildcard for compatibility -include_shell "cat external.conf 2>/dev/null" +# Add user chosen options held in (optional) external file +include "external*.conf" # default listening port for IPv6 falls back to the IPv4 port include_shell "/usr/share/lighttpd/use-ipv6.pl " + server.port - -# Prevent Lighttpd from enabling Let's Encrypt SSL for every blocked domain -#include_shell "/usr/share/lighttpd/include-conf-enabled.pl" -include_shell "find /etc/lighttpd/conf-enabled -name '*.conf' -a ! -name 'letsencrypt.conf' -printf 'include \"%p\"\n' 2>/dev/null" - -# If the URL starts with /admin, it is the Web interface -$HTTP["url"] =~ "^/admin/" { - # Create a response header for debugging using curl -I - setenv.add-response-header = ( - "X-Pi-hole" => "The Pi-hole Web interface is working!", - "X-Frame-Options" => "DENY" - ) -} - -# Block . files from being served, such as .git, .github, .gitignore -$HTTP["url"] =~ "^/admin/\.(.*)" { - url.access-deny = ("") -} - -# allow teleporter and API qr code iframe on settings page -$HTTP["url"] =~ "/(teleporter|api_token)\.php$" { - $HTTP["referer"] =~ "/admin/settings\.php" { - setenv.add-response-header = ( "X-Frame-Options" => "SAMEORIGIN" ) - } -} - -# Default expire header -expire.url = ( "" => "access plus 0 seconds" ) +include "/etc/lighttpd/conf-enabled/*.conf" diff --git a/advanced/lighttpd.conf.fedora b/advanced/lighttpd.conf.fedora index 79d5f3b275..e09d7760c4 100644 --- a/advanced/lighttpd.conf.fedora +++ b/advanced/lighttpd.conf.fedora @@ -7,13 +7,15 @@ # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. -############################################################################### -# FILE AUTOMATICALLY OVERWRITTEN BY PI-HOLE INSTALL/UPDATE PROCEDURE. # -# ANY CHANGES MADE TO THIS FILE AFTER INSTALL WILL BE LOST ON THE NEXT UPDATE # -# # -# CHANGES SHOULD BE MADE IN A SEPARATE CONFIG FILE: # -# /etc/lighttpd/external.conf # -############################################################################### +################################################################################################### +# IF THIS HEADER EXISTS, THE FILE WILL BE OVERWRITTEN BY PI-HOLE'S UPDATE PROCEDURE. # +# ANY CHANGES MADE TO THIS FILE WILL BE LOST ON THE NEXT UPDATE UNLESS YOU REMOVE THIS HEADER # +# # +# ENSURE THAT YOU DO NOT REMOVE THE REQUIRED LINE: # +# # +# include "/etc/lighttpd/conf.d/pihole-admin.conf" # +# # +################################################################################################### server.modules = ( "mod_access", @@ -27,15 +29,14 @@ server.modules = ( ) server.document-root = "/var/www/html" -server.error-handler-404 = "/pihole/index.php" server.upload-dirs = ( "/var/cache/lighttpd/uploads" ) -server.errorlog = "/var/log/lighttpd/error.log" +server.errorlog = "/var/log/lighttpd/error-pihole.log" server.pid-file = "/run/lighttpd.pid" server.username = "lighttpd" server.groupname = "lighttpd" +# For lighttpd version 1.4.46 or above, the port can be overwritten in `/etc/lighttpd/external.conf` using the := operator +# e.g. server.port := 8000 server.port = 80 -accesslog.filename = "/var/log/lighttpd/access.log" -accesslog.format = "%{%s}t|%V|%r|%s|%b" # Allow streaming response # reference: https://redmine.lighttpd.net/projects/lighttpd/wiki/Server_stream-response-bodyDetails @@ -66,9 +67,8 @@ mimetype.assign = ( ".woff2" => "font/woff2" ) -# Add user chosen options held in external file -# This uses include_shell instead of an include wildcard for compatibility -include_shell "cat external.conf 2>/dev/null" +# Add user chosen options held in (optional) external file +include "external*.conf" # default listening port for IPv6 falls back to the IPv4 port #include_shell "/usr/share/lighttpd/use-ipv6.pl " + server.port @@ -84,26 +84,4 @@ fastcgi.server = ( ) ) -# If the URL starts with /admin, it is the Web interface -$HTTP["url"] =~ "^/admin/" { - # Create a response header for debugging using curl -I - setenv.add-response-header = ( - "X-Pi-hole" => "The Pi-hole Web interface is working!", - "X-Frame-Options" => "DENY" - ) -} - -# Block . files from being served, such as .git, .github, .gitignore -$HTTP["url"] =~ "^/admin/\.(.*)" { - url.access-deny = ("") -} - -# allow teleporter and API qr code iframe on settings page -$HTTP["url"] =~ "/(teleporter|api_token)\.php$" { - $HTTP["referer"] =~ "/admin/settings\.php" { - setenv.add-response-header = ( "X-Frame-Options" => "SAMEORIGIN" ) - } -} - -# Default expire header -expire.url = ( "" => "access plus 0 seconds" ) +include "/etc/lighttpd/conf.d/pihole-admin.conf" diff --git a/advanced/pihole-admin.conf b/advanced/pihole-admin.conf new file mode 100644 index 0000000000..0bb6eac9d6 --- /dev/null +++ b/advanced/pihole-admin.conf @@ -0,0 +1,82 @@ +# Pi-hole: A black hole for Internet advertisements +# (c) 2017 Pi-hole, LLC (https://pi-hole.net) +# Network-wide ad blocking via your own hardware. +# +# Lighttpd config for Pi-hole +# +# This file is copyright under the latest version of the EUPL. +# Please see LICENSE file for your rights under this license. + +############################################################################### +# FILE AUTOMATICALLY OVERWRITTEN BY PI-HOLE INSTALL/UPDATE PROCEDURE. # +# ANY CHANGES MADE TO THIS FILE AFTER INSTALL WILL BE LOST ON THE NEXT UPDATE # +############################################################################### + +server.errorlog := "/var/log/lighttpd/error-pihole.log" + +$HTTP["url"] =~ "^/admin/" { + server.document-root = "/var/www/html" + server.stream-response-body = 1 + accesslog.filename = "/var/log/lighttpd/access-pihole.log" + accesslog.format = "%{%s}t|%h|%V|%r|%s|%b" + + fastcgi.server = ( + ".php" => ( + "localhost" => ( + "socket" => "/run/lighttpd/pihole-php-fastcgi.socket", + "bin-path" => "/usr/bin/php-cgi", + "min-procs" => 1, + "max-procs" => 1, + "bin-environment" => ( + "PHP_FCGI_CHILDREN" => "4", + "PHP_FCGI_MAX_REQUESTS" => "10000", + ), + "bin-copy-environment" => ( + "PATH", "SHELL", "USER" + ), + "broken-scriptfilename" => "enable", + ) + ) + ) + + # X-Pi-hole is a response header for debugging using curl -I + # X-Frame-Options prevents clickjacking attacks and helps ensure your content is not embedded into other sites via < frame >, < iframe > or < object >. + # X-XSS-Protection sets the configuration for the cross-site scripting filters built into most browsers. This is important because it tells the browser to block the response if a malicious script has been inserted from a user input. (deprecated; disabled) + # X-Content-Type-Options stops a browser from trying to MIME-sniff the content type and forces it to stick with the declared content-type. This is important because the browser will only load external resources if their content-type matches what is expected, and not malicious hidden code. + # Content-Security-Policy tells the browser where resources are allowed to be loaded and if it’s allowed to parse/run inline styles or Javascript. This is important because it prevents content injection attacks, such as Cross Site Scripting (XSS). + # X-Permitted-Cross-Domain-Policies is an XML document that grants a web client, such as Adobe Flash Player or Adobe Acrobat (though not necessarily limited to these), permission to handle data across domains. + # Referrer-Policy allows control/restriction of the amount of information present in the referral header for links away from your page—the URL path or even if the header is sent at all. + setenv.add-response-header = ( + "X-Pi-hole" => "The Pi-hole Web interface is working!", + "X-Frame-Options" => "DENY", + "X-XSS-Protection" => "0", + "X-Content-Type-Options" => "nosniff", + "Content-Security-Policy" => "default-src 'self' 'unsafe-inline';", + "X-Permitted-Cross-Domain-Policies" => "none", + "Referrer-Policy" => "same-origin" + ) + + # Block . files from being served, such as .git, .github, .gitignore + $HTTP["url"] =~ "^/admin/\." { + url.access-deny = ("") + } + + # allow teleporter and API qr code iframe on settings page + $HTTP["url"] =~ "/(teleporter|api_token)\.php$" { + $HTTP["referer"] =~ "/admin/settings\.php" { + setenv.set-response-header = ( "X-Frame-Options" => "SAMEORIGIN" ) + } + } +} +else $HTTP["url"] == "/admin" { + url.redirect = ("" => "/admin/") +} + +$HTTP["host"] == "pi.hole" { + $HTTP["url"] == "/" { + url.redirect = ("" => "/admin/") + } +} + +# (keep this on one line for basic-install.sh filtering during install) +server.modules += ( "mod_access", "mod_accesslog", "mod_redirect", "mod_fastcgi", "mod_setenv" ) diff --git a/automated install/basic-install.sh b/automated install/basic-install.sh index 5a90ff1155..32b3e695c0 100755 --- a/automated install/basic-install.sh +++ b/automated install/basic-install.sh @@ -2,7 +2,7 @@ # shellcheck disable=SC1090 # Pi-hole: A black hole for Internet advertisements -# (c) 2017-2021 Pi-hole, LLC (https://pi-hole.net) +# (c) Pi-hole (https://pi-hole.net) # Network-wide ad blocking via your own hardware. # # Installs and Updates Pi-hole @@ -32,6 +32,14 @@ export PATH+=':/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' # Local variables will be in lowercase and will exist only within functions # It's still a work in progress, so you may see some variance in this guideline until it is complete +# Dialog result codes +# dialog code values can be set by environment variables, we only override if +# the env var is not set or empty. +: "${DIALOG_OK:=0}" +: "${DIALOG_CANCEL:=1}" +: "${DIALOG_ESC:=255}" + + # List of supported DNS servers DNS_SERVERS=$(cat << EOM Google (ECS, DNSSEC);8.8.8.8;8.8.4.4;2001:4860:4860:0:0:0:0:8888;2001:4860:4860:0:0:0:0:8844 @@ -60,7 +68,7 @@ webroot="/var/www/html" # We clone (or update) two git repositories during the install. This helps to make sure that we always have the latest versions of the relevant files. -# AdminLTE is used to set up the Web admin interface. +# web is used to set up the Web admin interface. # Pi-hole contains various setup scripts and files which are critical to the installation. # Search for "PI_HOLE_LOCAL_REPO" in this file to see all such scripts. # Two notable scripts are gravity.sh (used to generate the HOSTS file) and advanced/Scripts/webpage.sh (used to install the Web admin interface) @@ -74,7 +82,7 @@ PI_HOLE_FILES=(chronometer list piholeDebug piholeLogFlush setupLCD update versi PI_HOLE_INSTALL_DIR="/opt/pihole" PI_HOLE_CONFIG_DIR="/etc/pihole" PI_HOLE_BIN_DIR="/usr/local/bin" -PI_HOLE_BLOCKPAGE_DIR="${webroot}/pihole" +FTL_CONFIG_FILE="${PI_HOLE_CONFIG_DIR}/pihole-FTL.conf" if [ -z "$useUpdateVars" ]; then useUpdateVars=false fi @@ -93,7 +101,7 @@ if [ -z "${USER}" ]; then USER="$(id -un)" fi -# whiptail dialog dimensions: 20 rows and 70 chars width assures to fit on small screens and is known to hold all content. +# dialog dimensions: Let dialog handle appropriate sizing. r=20 c=70 @@ -195,7 +203,7 @@ os_check() { distro_part="${distro_and_versions%%=*}" versions_part="${distro_and_versions##*=}" - # If the distro part is a (case-insensistive) substring of the computer OS + # 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}" @@ -234,7 +242,7 @@ os_check() { 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/prerequesites/#supported-operating-systems%b\\n" "${COL_LIGHT_GREEN}" "${COL_NC}" + 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" @@ -284,6 +292,9 @@ test_dpkg_lock() { # Compatibility package_manager_detect() { + # TODO - pull common packages for both distributions out into a common variable, then add + # the distro-specific ones below. + # First check to see if apt-get is installed. if is_command apt-get ; then # Set some global variables here @@ -300,21 +311,30 @@ package_manager_detect() { # Check for and determine version number (major and minor) of current php install local phpVer="php" if is_command php ; then - printf " %b Existing PHP installation detected : PHP version %s\\n" "${INFO}" "$(php <<< "")" - printf -v phpInsMajor "%d" "$(php <<< "")" - printf -v phpInsMinor "%d" "$(php <<< "")" - phpVer="php$phpInsMajor.$phpInsMinor" + phpVer="$(php <<< "")" + # Check if the first character of the string is numeric + if [[ ${phpVer:0:1} =~ [1-9] ]]; then + printf " %b Existing PHP installation detected : PHP version %s\\n" "${INFO}" "${phpVer}" + printf -v phpInsMajor "%d" "$(php <<< "")" + printf -v phpInsMinor "%d" "$(php <<< "")" + phpVer="php$phpInsMajor.$phpInsMinor" + else + printf " %b No valid PHP installation detected!\\n" "${CROSS}" + printf " %b PHP version : %s\\n" "${INFO}" "${phpVer}" + printf " %b Aborting installation.\\n" "${CROSS}" + exit 1 + fi fi - # Packages required to perfom the os_check (stored as an array) + # Packages required to perform the os_check (stored as an array) OS_CHECK_DEPS=(grep dnsutils) # Packages required to run this install script (stored as an array) - INSTALLER_DEPS=(git iproute2 whiptail ca-certificates) + INSTALLER_DEPS=(git iproute2 dialog ca-certificates) # Packages required to run Pi-hole (stored as an array) - PIHOLE_DEPS=(cron curl iputils-ping psmisc sudo unzip idn2 libcap2-bin dns-root-data libcap2 netcat-openbsd procps) + PIHOLE_DEPS=(cron curl iputils-ping psmisc sudo unzip idn2 libcap2-bin dns-root-data libcap2 netcat-openbsd procps jq) # Packages required for the Web admin interface (stored as an array) # It's useful to separate this from Pi-hole, since the two repos are also setup separately PIHOLE_WEB_DEPS=(lighttpd "${phpVer}-common" "${phpVer}-cgi" "${phpVer}-sqlite3" "${phpVer}-xml" "${phpVer}-intl") - # Prior to PHP8.0, JSON functionality is provided as dedicated module, required by Pi-hole AdminLTE: https://www.php.net/manual/json.installation.php + # Prior to PHP8.0, JSON functionality is provided as dedicated module, required by Pi-hole web: https://www.php.net/manual/json.installation.php if [[ -z "${phpInsMajor}" || "${phpInsMajor}" -lt 8 ]]; then PIHOLE_WEB_DEPS+=("${phpVer}-json") fi @@ -336,15 +356,31 @@ package_manager_detect() { # These variable names match the ones for apt-get. See above for an explanation of what they are for. PKG_INSTALL=("${PKG_MANAGER}" install -y) - PKG_COUNT="${PKG_MANAGER} check-update | egrep '(.i686|.x86|.noarch|.arm|.src)' | wc -l" + # CentOS package manager returns 100 when there are packages to update so we need to || true to prevent the script from exiting. + PKG_COUNT="${PKG_MANAGER} check-update | grep -E '(.i686|.x86|.noarch|.arm|.src|.riscv64)' | wc -l || true" OS_CHECK_DEPS=(grep bind-utils) - INSTALLER_DEPS=(git iproute newt procps-ng which chkconfig ca-certificates) - PIHOLE_DEPS=(cronie curl findutils sudo unzip libidn2 psmisc libcap nmap-ncat) + INSTALLER_DEPS=(git dialog iproute newt procps-ng chkconfig ca-certificates) + PIHOLE_DEPS=(cronie curl findutils sudo unzip libidn2 psmisc libcap nmap-ncat jq) PIHOLE_WEB_DEPS=(lighttpd lighttpd-fastcgi php-common php-cli php-pdo php-xml php-json php-intl) LIGHTTPD_USER="lighttpd" LIGHTTPD_GROUP="lighttpd" LIGHTTPD_CFG="lighttpd.conf.fedora" + # If the host OS is centos (or a derivative), epel is required for lighttpd + if ! grep -qiE 'fedora|fedberry' /etc/redhat-release; then + if rpm -qa | grep -qi 'epel'; then + printf " %b EPEL repository already installed\\n" "${TICK}" + else + local RH_RELEASE EPEL_PKG + # EPEL not already installed, add it based on the release version + RH_RELEASE=$(grep -oP '(?<= )[0-9]+(?=\.?)' /etc/redhat-release) + EPEL_PKG="https://dl.fedoraproject.org/pub/epel/epel-release-latest-${RH_RELEASE}.noarch.rpm" + printf " %b Enabling EPEL package repository (https://fedoraproject.org/wiki/EPEL)\\n" "${INFO}" + "${PKG_INSTALL[@]}" "${EPEL_PKG}" + printf " %b Installed %s\\n" "${TICK}" "${EPEL_PKG}" + fi + fi + # If neither apt-get or yum/dnf package managers were found else # we cannot install required packages @@ -354,90 +390,6 @@ package_manager_detect() { fi } -select_rpm_php(){ - # If the host OS is Fedora, - if grep -qiE 'fedora|fedberry' /etc/redhat-release; then - # all required packages should be available by default with the latest fedora release - : # continue - # or if host OS is CentOS, - elif grep -qiE 'centos|scientific' /etc/redhat-release; then - # Pi-Hole currently supports CentOS 7+ with PHP7+ - SUPPORTED_CENTOS_VERSION=7 - SUPPORTED_CENTOS_PHP_VERSION=7 - # Check current CentOS major release version - CURRENT_CENTOS_VERSION=$(grep -oP '(?<= )[0-9]+(?=\.?)' /etc/redhat-release) - # Check if CentOS version is supported - if [[ $CURRENT_CENTOS_VERSION -lt $SUPPORTED_CENTOS_VERSION ]]; then - printf " %b CentOS %s is not supported.\\n" "${CROSS}" "${CURRENT_CENTOS_VERSION}" - printf " Please update to CentOS release %s or later.\\n" "${SUPPORTED_CENTOS_VERSION}" - # exit the installer - exit - fi - # php-json is not required on CentOS 7 as it is already compiled into php - # verifiy via `php -m | grep json` - if [[ $CURRENT_CENTOS_VERSION -eq 7 ]]; then - # create a temporary array as arrays are not designed for use as mutable data structures - CENTOS7_PIHOLE_WEB_DEPS=() - for i in "${!PIHOLE_WEB_DEPS[@]}"; do - if [[ ${PIHOLE_WEB_DEPS[i]} != "php-json" ]]; then - CENTOS7_PIHOLE_WEB_DEPS+=( "${PIHOLE_WEB_DEPS[i]}" ) - fi - done - # re-assign the clean dependency array back to PIHOLE_WEB_DEPS - PIHOLE_WEB_DEPS=("${CENTOS7_PIHOLE_WEB_DEPS[@]}") - unset CENTOS7_PIHOLE_WEB_DEPS - fi - # CentOS requires the EPEL repository to gain access to Fedora packages - EPEL_PKG="epel-release" - rpm -q ${EPEL_PKG} &> /dev/null || rc=$? - if [[ $rc -ne 0 ]]; then - printf " %b Enabling EPEL package repository (https://fedoraproject.org/wiki/EPEL)\\n" "${INFO}" - "${PKG_INSTALL[@]}" ${EPEL_PKG} &> /dev/null - printf " %b Installed %s\\n" "${TICK}" "${EPEL_PKG}" - fi - - # The default php on CentOS 7.x is 5.4 which is EOL - # Check if the version of PHP available via installed repositories is >= to PHP 7 - AVAILABLE_PHP_VERSION=$("${PKG_MANAGER}" info php | grep -i version | grep -o '[0-9]\+' | head -1) - if [[ $AVAILABLE_PHP_VERSION -ge $SUPPORTED_CENTOS_PHP_VERSION ]]; then - # Since PHP 7 is available by default, install via default PHP package names - : # do nothing as PHP is current - else - REMI_PKG="remi-release" - REMI_REPO="remi-php72" - rpm -q ${REMI_PKG} &> /dev/null || rc=$? - if [[ $rc -ne 0 ]]; then - # The PHP version available via default repositories is older than version 7 - if ! whiptail --defaultno --title "PHP 7 Update (recommended)" --yesno "PHP 7.x is recommended for both security and language features.\\nWould you like to install PHP7 via Remi's RPM repository?\\n\\nSee: https://rpms.remirepo.net for more information" "${r}" "${c}"; then - # User decided to NOT update PHP from REMI, attempt to install the default available PHP version - printf " %b User opt-out of PHP 7 upgrade on CentOS. Deprecated PHP may be in use.\\n" "${INFO}" - : # continue with unsupported php version - else - printf " %b Enabling Remi's RPM repository (https://rpms.remirepo.net)\\n" "${INFO}" - "${PKG_INSTALL[@]}" "https://rpms.remirepo.net/enterprise/${REMI_PKG}-$(rpm -E '%{rhel}').rpm" &> /dev/null - # enable the PHP 7 repository via yum-config-manager (provided by yum-utils) - "${PKG_INSTALL[@]}" "yum-utils" &> /dev/null - yum-config-manager --enable ${REMI_REPO} &> /dev/null - printf " %b Remi's RPM repository has been enabled for PHP7\\n" "${TICK}" - # trigger an install/update of PHP to ensure previous version of PHP is updated from REMI - if "${PKG_INSTALL[@]}" "php-cli" &> /dev/null; then - printf " %b PHP7 installed/updated via Remi's RPM repository\\n" "${TICK}" - else - printf " %b There was a problem updating to PHP7 via Remi's RPM repository\\n" "${CROSS}" - exit 1 - fi - fi - fi # Warn user of unsupported version of Fedora or CentOS - if ! whiptail --defaultno --title "Unsupported RPM based distribution" --yesno "Would you like to continue installation on an unsupported RPM based distribution?\\n\\nPlease ensure the following packages have been installed manually:\\n\\n- lighttpd\\n- lighttpd-fastcgi\\n- PHP version 7+" "${r}" "${c}"; then - printf " %b Aborting installation due to unsupported RPM based distribution\\n" "${CROSS}" - exit - else - printf " %b Continuing installation with unsupported RPM based distribution\\n" "${INFO}" - fi - fi - fi -} - # A function for checking if a directory is a git repository is_repo() { # Use a named, local variable instead of the vague $1, which is the first argument passed to this function @@ -625,42 +577,45 @@ get_available_interfaces() { # A function for displaying the dialogs the user sees when first running the installer welcomeDialogs() { # Display the welcome dialog using an appropriately sized window via the calculation conducted earlier in the script - whiptail --msgbox --backtitle "Welcome" --title "Pi-hole automated installer" "\\n\\nThis installer will transform your device into a network-wide ad blocker!" "${r}" "${c}" - - # Request that users donate if they enjoy the software since we all work on it in our free time - whiptail --msgbox --backtitle "Plea" --title "Free and open source" "\\n\\nThe Pi-hole is free, but powered by your donations: https://pi-hole.net/donate/" "${r}" "${c}" - - whiptail --msgbox --backtitle "Speedtest Mod" --title "Speedtest Mod Included" "\n\nSpeedtestMod faq @ https://github.com/arevindh/pihole-speedtest" ${r} ${c} - - # Explain the need for a static address - if whiptail --defaultno --backtitle "Initiating network interface" --title "Static IP Needed" --yesno "\\n\\nThe Pi-hole is a SERVER so it needs a STATIC IP ADDRESS to function properly. - -IMPORTANT: If you have not already done so, you must ensure that this device has a static IP. Either through DHCP reservation, or by manually assigning one. Depending on your operating system, there are many ways to achieve this. - -Choose yes to indicate that you have understood this message, and wish to continue" "${r}" "${c}"; then - #Nothing to do, continue - echo - else - printf " %b Installer exited at static IP message.\\n" "${INFO}" - exit 1 - fi + dialog --no-shadow --clear --keep-tite \ + --backtitle "Welcome" \ + --title "Pi-hole Automated Installer : Speedtest Mod Included" \ + --msgbox "\\n\\nThis installer will transform your device into a network-wide ad blocker! https://github.com/arevindh/pihole-speedtest" \ + "${r}" "${c}" \ + --and-widget --clear \ + --backtitle "Support Pi-hole" \ + --title "Open Source Software" \ + --msgbox "\\n\\nThe Pi-hole is free, but powered by your donations: https://pi-hole.net/donate/" \ + "${r}" "${c}" \ + --and-widget --clear \ + --colors \ + --backtitle "Initiating network interface" \ + --title "Static IP Needed" \ + --no-button "Exit" --yes-button "Continue" \ + --defaultno \ + --yesno "\\n\\nThe Pi-hole is a SERVER so it needs a STATIC IP ADDRESS to function properly.\\n\\n\ +\\Zb\\Z1IMPORTANT:\\Zn If you have not already done so, you must ensure that this device has a static IP.\\n\\n\ +Depending on your operating system, there are many ways to achieve this, through DHCP reservation, or by manually assigning one.\\n\\n\ +Please continue when the static addressing has been configured."\ + "${r}" "${c}" && result=0 || result="$?" + + case "${result}" in + "${DIALOG_CANCEL}" | "${DIALOG_ESC}") + printf " %b Installer exited at static IP message.\\n" "${INFO}" + exit 1 + ;; + esac } # A function that lets the user pick an interface to use with Pi-hole chooseInterface() { - # Turn the available interfaces into an array so it can be used with a whiptail dialog - local interfacesArray=() + # Turn the available interfaces into a string so it can be used with dialog + local interfacesList # Number of available interfaces local interfaceCount - # Whiptail variable storage - local chooseInterfaceCmd - # Temporary Whiptail options storage - local chooseInterfaceOptions - # Loop sentinel variable - local firstLoop=1 - # Find out how many interfaces are available to choose from - interfaceCount=$(wc -l <<< "${availableInterfaces}") + # POSIX compliant way to get the number of elements in an array + interfaceCount=$(printf "%s\n" "${availableInterfaces}" | wc -l) # If there is one interface, if [[ "${interfaceCount}" -eq 1 ]]; then @@ -668,33 +623,33 @@ chooseInterface() { PIHOLE_INTERFACE="${availableInterfaces}" # Otherwise, else + # Set status for the first entry to be selected + status="ON" + # While reading through the available interfaces - while read -r line; do - # Use a variable to set the option as OFF to begin with - mode="OFF" - # If it's the first loop, - if [[ "${firstLoop}" -eq 1 ]]; then - # set this as the interface to use (ON) - firstLoop=0 - mode="ON" - fi - # Put all these interfaces into an array - interfacesArray+=("${line}" "available" "${mode}") - # Feed the available interfaces into this while loop - done <<< "${availableInterfaces}" - # The whiptail command that will be run, stored in a variable - chooseInterfaceCmd=(whiptail --separate-output --radiolist "Choose An Interface (press space to toggle selection)" "${r}" "${c}" 6) - # Now run the command using the interfaces saved into the array - chooseInterfaceOptions=$("${chooseInterfaceCmd[@]}" "${interfacesArray[@]}" 2>&1 >/dev/tty) || \ - # If the user chooses Cancel, exit - { printf " %bCancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"; exit 1; } - # For each interface - for desiredInterface in ${chooseInterfaceOptions}; do - # Set the one the user selected as the interface to use - PIHOLE_INTERFACE=${desiredInterface} - # and show this information to the user - printf " %b Using interface: %s\\n" "${INFO}" "${PIHOLE_INTERFACE}" + for interface in ${availableInterfaces}; do + # Put all these interfaces into a string + interfacesList="${interfacesList}${interface} available ${status} " + # 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 + PIHOLE_INTERFACE=$(dialog --no-shadow --keep-tite --output-fd 1 \ + --cancel-label "Exit" --ok-label "Select" \ + --radiolist "Choose An Interface (press space to toggle selection)" \ + ${r} ${c} "${interfaceCount}" ${interfacesList}) + + result=$? + case ${result} in + "${DIALOG_CANCEL}"|"${DIALOG_ESC}") + # Show an error message and exit + printf " %b %s\\n" "${CROSS}" "No interface selected, exiting installer" + exit 1 + ;; + esac + + printf " %b Using interface: %s\\n" "${INFO}" "${PIHOLE_INTERFACE}" fi } @@ -727,7 +682,7 @@ testIPv6() { find_IPv6_information() { # Detects IPv6 address used for communication to WAN addresses. - IPV6_ADDRESSES=($(ip -6 address | grep 'scope global' | awk '{print $2}')) + mapfile -t IPV6_ADDRESSES <<<"$(ip -6 address | grep 'scope global' | awk '{print $2}')" # For each address in the array above, determine the type of IPv6 address it is for i in "${IPV6_ADDRESSES[@]}"; do @@ -742,13 +697,13 @@ find_IPv6_information() { # Determine which address to be used: Prefer ULA over GUA or don't use any if none found # If the ULA_ADDRESS contains a value, - if [[ ! -z "${ULA_ADDRESS}" ]]; then + if [[ -n "${ULA_ADDRESS}" ]]; then # set the IPv6 address to the ULA address IPV6_ADDRESS="${ULA_ADDRESS}" # Show this info to the user printf " %b Found IPv6 ULA address\\n" "${INFO}" # Otherwise, if the GUA_ADDRESS has a value, - elif [[ ! -z "${GUA_ADDRESS}" ]]; then + elif [[ -n "${GUA_ADDRESS}" ]]; then # Let the user know printf " %b Found IPv6 GUA address\\n" "${INFO}" # And assign it to the global variable @@ -780,63 +735,103 @@ getStaticIPv4Settings() { local ipSettingsCorrect local DHCPChoice # Ask if the user wants to use DHCP settings as their static IP - # This is useful for users that are using DHCP reservations; then we can just use the information gathered via our functions - DHCPChoice=$(whiptail --backtitle "Calibrating network interface" --title "Static IP Address" --menu --separate-output "Do you want to use your current network settings as a static address? \\n - IP address: ${IPV4_ADDRESS} \\n - Gateway: ${IPv4gw} \\n" "${r}" "${c}" 3\ - "Yes" "Set static IP using current values" \ - "No" "Set static IP using custom values" \ - "Skip" "I will set a static IP later, or have already done so" 3>&2 2>&1 1>&3) || \ - { printf " %bCancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"; exit 1; } - - case ${DHCPChoice} in - "Yes") - # If they choose yes, let the user know that the IP address will not be available via DHCP and may cause a conflict. - whiptail --msgbox --backtitle "IP information" --title "FYI: IP Conflict" "It is possible your router could still try to assign this IP to a device, which would cause a conflict. But in most cases the router is smart enough to not do that. - If you are worried, either manually set the address, or modify the DHCP reservation pool so it does not include the IP you want. - It is also possible to use a DHCP reservation, but if you are going to do that, you might as well set a static address." "${r}" "${c}" - # Nothing else to do since the variables are already set above - setDHCPCD - ;; - - "No") - # Otherwise, we need to ask the user to input their desired settings. - # Start by getting the IPv4 address (pre-filling it with info gathered from DHCP) - # Start a loop to let the user enter their information with the chance to go back and edit it if necessary - until [[ "${ipSettingsCorrect}" = True ]]; do - - # Ask for the IPv4 address - IPV4_ADDRESS=$(whiptail --backtitle "Calibrating network interface" --title "IPv4 address" --inputbox "Enter your desired IPv4 address" "${r}" "${c}" "${IPV4_ADDRESS}" 3>&1 1>&2 2>&3) || \ - # Canceling IPv4 settings window - { ipSettingsCorrect=False; echo -e " ${COL_LIGHT_RED}Cancel was selected, exiting installer${COL_NC}"; exit 1; } - printf " %b Your static IPv4 address: %s\\n" "${INFO}" "${IPV4_ADDRESS}" - - # Ask for the gateway - IPv4gw=$(whiptail --backtitle "Calibrating network interface" --title "IPv4 gateway (router)" --inputbox "Enter your desired IPv4 default gateway" "${r}" "${c}" "${IPv4gw}" 3>&1 1>&2 2>&3) || \ - # Canceling gateway settings window - { ipSettingsCorrect=False; echo -e " ${COL_LIGHT_RED}Cancel was selected, exiting installer${COL_NC}"; exit 1; } - printf " %b Your static IPv4 gateway: %s\\n" "${INFO}" "${IPv4gw}" - - # Give the user a chance to review their settings before moving on - if whiptail --backtitle "Calibrating network interface" --title "Static IP Address" --yesno "Are these settings correct? - IP address: ${IPV4_ADDRESS} - Gateway: ${IPv4gw}" "${r}" "${c}"; then - # After that's done, the loop ends and we move on - ipSettingsCorrect=True - else - # If the settings are wrong, the loop continues - ipSettingsCorrect=False - fi + # This is useful for users that are using DHCP reservations; we can use the information gathered + DHCPChoice=$(dialog --no-shadow --keep-tite --output-fd 1 \ + --cancel-label "Exit" --ok-label "Continue" \ + --backtitle "Calibrating network interface" \ + --title "Static IP Address" \ + --menu "Do you want to use your current network settings as a static address?\\n \ + IP address: ${IPV4_ADDRESS}\\n \ + Gateway: ${IPv4gw}\\n" \ + "${r}" "${c}" 3 \ + "Yes" "Set static IP using current values" \ + "No" "Set static IP using custom values" \ + "Skip" "I will set a static IP later, or have already done so") + + 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 + + case ${DHCPChoice} in + "Skip") + return + ;; + "Yes") + # If they choose yes, let the user know that the IP address will not be available via DHCP and may cause a conflict. + dialog --no-shadow --keep-tite \ + --cancel-label "Exit" \ + --backtitle "IP information" \ + --title "FYI: IP Conflict" \ + --msgbox "\\nIt is possible your router could still try to assign this IP to a device, which would cause a conflict, \ +but in most cases the router is smart enough to not do that.\n\n\ +If you are worried, either manually set the address, or modify the DHCP reservation pool so it does not include the IP you want.\n\n\ +It is also possible to use a DHCP reservation, but if you are going to do that, you might as well set a static address."\ + "${r}" "${c}" && result=0 || 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 + ;; + + "No") + # Otherwise, we need to ask the user to input their desired settings. + # Start by getting the IPv4 address (pre-filling it with info gathered from DHCP) + # Start a loop to let the user enter their information with the chance to go back and edit it if necessary + ipSettingsCorrect=false + until [[ "${ipSettingsCorrect}" = True ]]; do + + # Ask for the IPv4 address + _staticIPv4Temp=$(dialog --no-shadow --keep-tite --output-fd 1 \ + --cancel-label "Exit" \ + --ok-label "Continue" \ + --backtitle "Calibrating network interface" \ + --title "IPv4 Address" \ + --form "\\nEnter your desired IPv4 address" \ + "${r}" "${c}" 0 \ + "IPv4 Address:" 1 1 "${IPV4_ADDRESS}" 1 15 19 0 \ + "IPv4 Gateway:" 2 1 "${IPv4gw}" 2 15 19 0) + + 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 + + IPV4_ADDRESS=${_staticIPv4Temp%$'\n'*} + IPv4gw=${_staticIPv4Temp#*$'\n'} + + # Give the user a chance to review their settings before moving on + dialog --no-shadow --keep-tite \ + --no-label "Edit IP" \ + --backtitle "Calibrating network interface" \ + --title "Static IP Address" \ + --defaultno \ + --yesno "Are these settings correct? + IP address: ${IPV4_ADDRESS} + Gateway: ${IPv4gw}" \ + "${r}" "${c}" && ipSettingsCorrect=True done - setDHCPCD - ;; - esac + ;; + esac + setDHCPCD } # Configure networking via dhcpcd setDHCPCD() { - # Check if the IP is already in the file - if grep -q "${IPV4_ADDRESS}" /etc/dhcpcd.conf; then + # Regex for matching a non-commented static ip address setting + local regex="^[ \t]*static ip_address[ \t]*=[ \t]*${IPV4_ADDRESS}" + + # Check if static IP is already set in file + if grep -q "${regex}" /etc/dhcpcd.conf; then printf " %b Static IP already configured\\n" "${INFO}" # If it's not, else @@ -920,11 +915,19 @@ setDNS() { DNSChooseOptions[DNSServerCount]="" # Restore the IFS to what it was IFS=${OIFS} - # In a whiptail dialog, show the options - DNSchoices=$(whiptail --separate-output --menu "Select Upstream DNS Provider. To use your own, select Custom." "${r}" "${c}" 7 \ - "${DNSChooseOptions[@]}" 2>&1 >/dev/tty) || \ - # Exit if the user selects "Cancel" - { printf " %bCancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"; exit 1; } + # In a dialog, show the options + DNSchoices=$(dialog --no-shadow --keep-tite --output-fd 1 \ + --cancel-label "Exit" \ + --menu "Select Upstream DNS Provider. To use your own, select Custom." "${r}" "${c}" 7 \ + "${DNSChooseOptions[@]}") + + 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 # Depending on the user's choice, set the GLOBAL variables to the IP of the respective provider if [[ "${DNSchoices}" == "Custom" ]] @@ -938,7 +941,7 @@ setDNS() { # If the first and second upstream servers do not exist, do not prepopulate an IP address prePopulate="" else - # Otherwise, prepopulate the whiptail dialogue with the appropriate DNS value(s) + # Otherwise, prepopulate the dialogue with the appropriate DNS value(s) prePopulate=", ${PIHOLE_DNS_2}" fi elif [[ "${PIHOLE_DNS_1}" ]] && [[ ! "${PIHOLE_DNS_2}" ]]; then @@ -948,8 +951,22 @@ setDNS() { fi # Prompt the user to enter custom upstream servers - piholeDNS=$(whiptail --backtitle "Specify Upstream DNS Provider(s)" --inputbox "Enter your desired upstream DNS provider(s), separated by a comma.\\n\\nFor example '8.8.8.8, 8.8.4.4'" "${r}" "${c}" "${prePopulate}" 3>&1 1>&2 2>&3) || \ - { printf " %bCancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"; exit 1; } + piholeDNS=$(dialog --no-shadow --keep-tite --output-fd 1 \ + --cancel-label "Exit" \ + --backtitle "Specify Upstream DNS Provider(s)" \ + --inputbox "Enter your desired upstream DNS provider(s), separated by a comma.\ +If you want to specify a port other than 53, separate it with a hash.\ +\\n\\nFor example '8.8.8.8, 8.8.4.4' or '127.0.0.1#5335'"\ + "${r}" "${c}" "${prePopulate}") + + 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 + # Clean user input and replace whitespace with comma. piholeDNS=$(sed 's/[, \t]\+/,/g' <<< "${piholeDNS}") @@ -968,7 +985,13 @@ setDNS() { # If either of the DNS servers are invalid, if [[ "${PIHOLE_DNS_1}" == "${strInvalid}" ]] || [[ "${PIHOLE_DNS_2}" == "${strInvalid}" ]]; then # explain this to the user, - whiptail --msgbox --backtitle "Invalid IP" --title "Invalid IP" "One or both entered IP addresses were invalid. Please try again.\\n\\n DNS Server 1: $PIHOLE_DNS_1\\n DNS Server 2: ${PIHOLE_DNS_2}" ${r} ${c} + dialog --no-shadow --keep-tite \ + --title "Invalid IP Address(es)" \ + --backtitle "Invalid IP" \ + --msgbox "\\nOne or both of the entered IP addresses were invalid. Please try again.\ +\\n\\nInvalid IPs: ${PIHOLE_DNS_1}, ${PIHOLE_DNS_2}" \ + "${r}" "${c}" + # set the variables back to nothing, if [[ "${PIHOLE_DNS_1}" == "${strInvalid}" ]]; then PIHOLE_DNS_1="" @@ -979,12 +1002,24 @@ setDNS() { # and continue the loop. DNSSettingsCorrect=False else - # Otherwise, show the DNS setting to the user, and break the loop if they confirm them. - if (whiptail --backtitle "Specify Upstream DNS Provider(s)" --title "Upstream DNS Provider(s)" --yesno "Are these settings correct?\\n DNS Server 1: $PIHOLE_DNS_1\\n DNS Server 2: ${PIHOLE_DNS_2}" "${r}" "${c}"); then - DNSSettingsCorrect=True - else - DNSSettingsCorrect=False - fi + dialog --no-shadow --no-collapse --keep-tite \ + --backtitle "Specify Upstream DNS Provider(s)" \ + --title "Upstream DNS Provider(s)" \ + --yesno "Are these settings correct?\\n"$'\t'"DNS Server 1:"$'\t'"${PIHOLE_DNS_1}\\n"$'\t'"DNS Server 2:"$'\t'"${PIHOLE_DNS_2}" \ + "${r}" "${c}" && result=0 || result=$? + + case ${result} in + "${DIALOG_OK}") + DNSSettingsCorrect=True + ;; + "${DIALOG_CANCEL}") + DNSSettingsCorrect=False + ;; + "${DIALOG_ESC}") + printf " %b Escape pressed, exiting installer at DNS Settings%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + exit 1 + ;; + esac fi done else @@ -1014,106 +1049,121 @@ setDNS() { # Allow the user to enable/disable logging setLogging() { - # Local, named variables - local LogToggleCommand - local LogChooseOptions - local LogChoices - - # Ask if the user wants to log queries - LogToggleCommand=(whiptail --separate-output --radiolist "Do you want to log queries?" "${r}" "${c}" 6) - # The default selection is on - LogChooseOptions=("On (Recommended)" "" on - Off "" off) - # Get the user's choice - LogChoices=$("${LogToggleCommand[@]}" "${LogChooseOptions[@]}" 2>&1 >/dev/tty) || (printf " %bCancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" && exit 1) - case ${LogChoices} in - # If it's on, - "On (Recommended)") - printf " %b Logging On.\\n" "${INFO}" - # set the GLOBAL variable setting to true + # Ask the user if they want to enable logging + dialog --no-shadow --keep-tite \ + --backtitle "Pihole Installation" \ + --title "Enable Logging" \ + --yesno "\\n\\nWould you like to enable query logging?" \ + "${r}" "${c}" && result=0 || result=$? + + case ${result} in + "${DIALOG_OK}") + # If they chose yes, + printf " %b Query Logging on.\\n" "${INFO}" QUERY_LOGGING=true ;; - # Otherwise, it's off, - Off) - printf " %b Logging Off.\\n" "${INFO}" - # set the GLOBAL variable setting to false + "${DIALOG_CANCEL}") + # If they chose no, + printf " %b Query Logging off.\\n" "${INFO}" QUERY_LOGGING=false ;; + "${DIALOG_ESC}") + # User pressed + printf " %b Escape pressed, exiting installer at Query Logging choice.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + exit 1 + ;; esac } # Allow the user to set their FTL privacy level setPrivacyLevel() { - local LevelCommand - local LevelOptions - - LevelCommand=(whiptail --separate-output --radiolist "Select a privacy mode for FTL. https://docs.pi-hole.net/ftldns/privacylevels/" "${r}" "${c}" 6) - # The default selection is level 0 - LevelOptions=( - "0" "Show everything" on - "1" "Hide domains" off - "2" "Hide domains and clients" off - "3" "Anonymous mode" off - ) - - # Get the user's choice - PRIVACY_LEVEL=$("${LevelCommand[@]}" "${LevelOptions[@]}" 2>&1 >/dev/tty) || (echo -e " ${COL_LIGHT_RED}Cancel was selected, exiting installer${COL_NC}" && exit 1) - - printf " %b Privacy level %d" "${INFO}" "${PRIVACY_LEVEL}" + PRIVACY_LEVEL=$(dialog --no-shadow --keep-tite --output-fd 1 \ + --cancel-label "Exit" \ + --ok-label "Continue" \ + --radiolist "Select a privacy mode for FTL. https://docs.pi-hole.net/ftldns/privacylevels/" \ + "${r}" "${c}" 6 \ + "0" "Show everything" on \ + "1" "Hide domains" off \ + "2" "Hide domains and clients" off \ + "3" "Anonymous mode" off) + + result=$? + case ${result} in + "${DIALOG_OK}") + 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}" + exit 1 + ;; + esac } # Function to ask the user if they want to install the dashboard setAdminFlag() { - # Local, named variables - local WebToggleCommand - local WebChooseOptions - local WebChoices - # Similar to the logging function, ask what the user wants - WebToggleCommand=(whiptail --separate-output --radiolist "Do you wish to install the web admin interface?" "${r}" "${c}" 6) - # with the default being enabled - WebChooseOptions=("On (Recommended)" "" on - Off "" off) - WebChoices=$("${WebToggleCommand[@]}" "${WebChooseOptions[@]}" 2>&1 >/dev/tty) || (printf " %bCancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" && exit 1) - # Depending on their choice - case ${WebChoices} in - "On (Recommended)") - printf " %b Web Interface On\\n" "${INFO}" - # Set it to true + dialog --no-shadow --keep-tite \ + --backtitle "Pihole Installation" \ + --title "Admin Web Interface" \ + --yesno "\\n\\nDo you want to install the Admin Web Interface?" \ + "${r}" "${c}" && result=0 || result=$? + + case ${result} in + "${DIALOG_OK}") + # If they chose yes, + printf " %b Installing Admin Web Interface\\n" "${INFO}" + # Set the flag to install the web interface INSTALL_WEB_INTERFACE=true ;; - Off) - printf " %b Web Interface Off\\n" "${INFO}" - # or false + "${DIALOG_CANCEL}") + # If they chose no, + printf " %b Not installing Admin Web Interface\\n" "${INFO}" + # Set the flag to not install the web interface INSTALL_WEB_INTERFACE=false - # Deselect the web server as well, since it is obsolete then INSTALL_WEB_SERVER=false ;; + "${DIALOG_ESC}") + # User pressed + printf " %b Escape pressed, exiting installer at Admin Web Interface choice.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + exit 1 + ;; esac - # If the user wants to install the Web admin interface (i.e. it has not been deselected above) - if [[ "${INSTALL_WEB_SERVER}" == true ]]; then + # If the user wants to install the Web admin interface (i.e. it has not been deselected above) and did not deselect the web server via command-line argument + if [[ "${INSTALL_WEB_INTERFACE}" == true && "${INSTALL_WEB_SERVER}" == true ]]; then # Get list of required PHP modules, excluding base package (common) and handler (cgi) local i php_modules for i in "${PIHOLE_WEB_DEPS[@]}"; do [[ $i == 'php'* && $i != *'-common' && $i != *'-cgi' ]] && php_modules+=" ${i#*-}"; done - WebToggleCommand=(whiptail --separate-output --radiolist "Do you wish to install the web server (lighttpd) and required PHP modules?\\n\\nNB: If you disable this, and, do not have an existing web server and required PHP modules (${php_modules# }) installed, the web interface will not function. Additionally the web server user needs to be member of the \"pihole\" group for full functionality." "${r}" "${c}" 6) - # Enable as default and recommended option - WebChooseOptions=("On (Recommended)" "" on - Off "" off) - WebChoices=$("${WebToggleCommand[@]}" "${WebChooseOptions[@]}" 2>&1 >/dev/tty) || (printf " %bCancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" && exit 1) - # Depending on their choice - case ${WebChoices} in - "On (Recommended)") - printf " %b Web Server On\\n" "${INFO}" - # set it to true, as clearly seen below. + dialog --no-shadow --keep-tite \ + --backtitle "Pi-hole Installation" \ + --title "Web Server" \ + --yesno "\\n\\nA web server is required for the Admin Web Interface.\ +\\n\\nDo you want to install lighttpd and the required PHP modules?\ +\\n\\nNB: If you disable this, and, do not have an existing web server \ +and required PHP modules (${php_modules# }) installed, the web interface \ +will not function. Additionally the web server user needs to be member of \ +the \"pihole\" group for full functionality." \ + "${r}" "${c}" && result=0 || result=$? + + case ${result} in + "${DIALOG_OK}") + # If they chose yes, + printf " %b Installing lighttpd\\n" "${INFO}" + # Set the flag to install the web server INSTALL_WEB_SERVER=true ;; - Off) - printf " %b Web Server Off\\n" "${INFO}" - # or false + "${DIALOG_CANCEL}") + # If they chose no, + printf " %b Not installing lighttpd\\n" "${INFO}" + # Set the flag to not install the web server INSTALL_WEB_SERVER=false ;; + "${DIALOG_ESC}") + # User pressed + printf " %b Escape pressed, exiting installer at web server choice.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + exit 1 + ;; esac fi } @@ -1124,18 +1174,32 @@ chooseBlocklists() { if [[ -f "${adlistFile}" ]]; then mv "${adlistFile}" "${adlistFile}.old" fi - # Let user select (or not) blocklists via a checklist - cmd=(whiptail --separate-output --checklist "Pi-hole relies on third party lists in order to block ads.\\n\\nYou can use the suggestion below, and/or add your own after installation\\n\\nTo deselect the suggested list, use spacebar" "${r}" "${c}" 5) - # In an array, show the options available (all off by default): - options=(StevenBlack "StevenBlack's Unified Hosts List" on) - - # In a variable, show the choices available; exit if Cancel is selected - choices=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty) || { printf " %bCancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"; rm "${adlistFile}" ;exit 1; } - # Add all selected choices to the lists file - for choice in ${choices} - do - appendToListsFile "${choice}" - done + # Let user select (or not) blocklists + dialog --no-shadow --keep-tite \ + --backtitle "Pi-hole Installation" \ + --title "Blocklists" \ + --yesno "\\nPi-hole relies on third party lists in order to block ads.\ +\\n\\nYou can use the suggestion below, and/or add your own after installation.\ +\\n\\nSelect 'Yes' to include:\ +\\n\\nStevenBlack's Unified Hosts List" \ + "${r}" "${c}" && result=0 || result=$? + + case ${result} in + "${DIALOG_OK}") + # If they chose yes, + printf " %b Installing StevenBlack's Unified Hosts List\\n" "${INFO}" + echo "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" >> "${adlistFile}" + ;; + "${DIALOG_CANCEL}") + # If they chose no, + printf " %b Not installing StevenBlack's Unified Hosts List\\n" "${INFO}" + ;; + "${DIALOG_ESC}") + # User pressed + printf " %b Escape pressed, exiting installer at blocklist choice.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + exit 1 + ;; + esac # Create an empty adList file with appropriate permissions. if [ ! -f "${adlistFile}" ]; then install -m 644 /dev/null "${adlistFile}" @@ -1144,14 +1208,6 @@ chooseBlocklists() { fi } -# Accept a string parameter, it must be one of the default lists -# This function saves duplication between chooseBlocklists and installDefaultBlocklists -appendToListsFile() { - case $1 in - StevenBlack ) echo "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" >> "${adlistFile}";; - esac -} - # Used only in unattended setup # If there is already the adListFile, we keep it, else we create it using all default lists installDefaultBlocklists() { @@ -1160,7 +1216,7 @@ installDefaultBlocklists() { if [[ -f "${adlistFile}" ]]; then return; fi - appendToListsFile StevenBlack + echo "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" >> "${adlistFile}" } # Check if /etc/dnsmasq.conf is from pi-hole. If so replace with an original and install new in .d directory @@ -1211,35 +1267,30 @@ version_check_dnsmasq() { # Copy the new Pi-hole DNS config file into the dnsmasq.d directory install -D -m 644 -T "${dnsmasq_pihole_01_source}" "${dnsmasq_pihole_01_target}" printf "%b %b Installed %s\n" "${OVER}" "${TICK}" "${dnsmasq_pihole_01_target}" - # Replace our placeholder values with the GLOBAL DNS variables that we populated earlier - # First, swap in the interface to listen on, - sed -i "s/@INT@/$PIHOLE_INTERFACE/" "${dnsmasq_pihole_01_target}" + # Add settings with the GLOBAL DNS variables that we populated earlier + # First, set the interface to listen on + addOrEditKeyValPair "${dnsmasq_pihole_01_target}" "interface" "$PIHOLE_INTERFACE" if [[ "${PIHOLE_DNS_1}" != "" ]]; then - # then swap in the primary DNS server. - sed -i "s/@DNS1@/$PIHOLE_DNS_1/" "${dnsmasq_pihole_01_target}" - else - # Otherwise, remove the line which sets DNS1. - sed -i '/^server=@DNS1@/d' "${dnsmasq_pihole_01_target}" + # then add in the primary DNS server. + addOrEditKeyValPair "${dnsmasq_pihole_01_target}" "server" "$PIHOLE_DNS_1" fi # Ditto if DNS2 is not empty if [[ "${PIHOLE_DNS_2}" != "" ]]; then - sed -i "s/@DNS2@/$PIHOLE_DNS_2/" "${dnsmasq_pihole_01_target}" - else - sed -i '/^server=@DNS2@/d' "${dnsmasq_pihole_01_target}" + addKey "${dnsmasq_pihole_01_target}" "server=$PIHOLE_DNS_2" fi # Set the cache size - sed -i "s/@CACHE_SIZE@/$CACHE_SIZE/" "${dnsmasq_pihole_01_target}" + addOrEditKeyValPair "${dnsmasq_pihole_01_target}" "cache-size" "$CACHE_SIZE" sed -i 's/^#conf-dir=\/etc\/dnsmasq.d$/conf-dir=\/etc\/dnsmasq.d/' "${dnsmasq_conf}" # If the user does not want to enable logging, if [[ "${QUERY_LOGGING}" == false ]] ; then - # disable it by commenting out the directive in the DNS config file - sed -i 's/^log-queries/#log-queries/' "${dnsmasq_pihole_01_target}" + # remove itfrom the DNS config file + removeKey "${dnsmasq_pihole_01_target}" "log-queries" else - # Otherwise, enable it by uncommenting the directive in the DNS config file - sed -i 's/^#log-queries/log-queries/' "${dnsmasq_pihole_01_target}" + # Otherwise, enable it by adding the directive to the DNS config file + addKey "${dnsmasq_pihole_01_target}" "log-queries" fi printf " %b Installing %s..." "${INFO}" "${dnsmasq_rfc6761_06_source}" @@ -1312,10 +1363,10 @@ installConfigs() { chmod 644 "${PI_HOLE_CONFIG_DIR}/dns-servers.conf" # Install template file if it does not exist - if [[ ! -r "${PI_HOLE_CONFIG_DIR}/pihole-FTL.conf" ]]; then + if [[ ! -r "${FTL_CONFIG_FILE}" ]]; then install -d -m 0755 ${PI_HOLE_CONFIG_DIR} - if ! install -T -o pihole -m 664 "${PI_HOLE_LOCAL_REPO}/advanced/Templates/pihole-FTL.conf" "${PI_HOLE_CONFIG_DIR}/pihole-FTL.conf" &>/dev/null; then - printf " %bError: Unable to initialize configuration file %s/pihole-FTL.conf\\n" "${COL_LIGHT_RED}" "${PI_HOLE_CONFIG_DIR}" + if ! install -T -o pihole -m 664 "${PI_HOLE_LOCAL_REPO}/advanced/Templates/pihole-FTL.conf" "${FTL_CONFIG_FILE}" &>/dev/null; then + printf " %b Error: Unable to initialize configuration file %s/pihole-FTL.conf\\n" "${COL_LIGHT_RED}" "${PI_HOLE_CONFIG_DIR}" return 1 fi fi @@ -1323,39 +1374,101 @@ installConfigs() { # Install empty custom.list file if it does not exist if [[ ! -r "${PI_HOLE_CONFIG_DIR}/custom.list" ]]; then if ! install -o root -m 644 /dev/null "${PI_HOLE_CONFIG_DIR}/custom.list" &>/dev/null; then - printf " %bError: Unable to initialize configuration file %s/custom.list\\n" "${COL_LIGHT_RED}" "${PI_HOLE_CONFIG_DIR}" + printf " %b Error: Unable to initialize configuration file %s/custom.list\\n" "${COL_LIGHT_RED}" "${PI_HOLE_CONFIG_DIR}" 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 + 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 + if [[ -e '/etc/init.d/pihole-FTL' ]]; then + rm '/etc/init.d/pihole-FTL' + update-rc.d pihole-FTL remove + fi + + # 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' + 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" + # If the user chose to install the dashboard, if [[ "${INSTALL_WEB_SERVER}" == true ]]; then - # and if the Web server conf directory does not exist, - if [[ ! -d "/etc/lighttpd" ]]; then - # make it and set the owners - install -d -m 755 -o "${USER}" -g root /etc/lighttpd - # Otherwise, if the config file already exists - elif [[ -f "${lighttpdConfig}" ]]; then - # back up the original - mv "${lighttpdConfig}"{,.orig} - fi - # and copy in the config file Pi-hole needs - install -D -m 644 -T ${PI_HOLE_LOCAL_REPO}/advanced/${LIGHTTPD_CFG} "${lighttpdConfig}" - # Make sure the external.conf file exists, as lighttpd v1.4.50 crashes without it - if [ ! -f /etc/lighttpd/external.conf ]; then - install -m 644 /dev/null /etc/lighttpd/external.conf - fi - # If there is a custom block page in the html/pihole directory, replace 404 handler in lighttpd config - if [[ -f "${PI_HOLE_BLOCKPAGE_DIR}/custom.php" ]]; then - sed -i 's/^\(server\.error-handler-404\s*=\s*\).*$/\1"\/pihole\/custom\.php"/' "${lighttpdConfig}" - fi - # Make the directories if they do not exist and set the owners + # set permissions on /etc/lighttpd/lighttpd.conf so pihole user (other) can read the file + chmod o+x /etc/lighttpd + chmod o+r "${lighttpdConfig}" + + # Ensure /run/lighttpd exists and is owned by lighttpd user + # Needed for the php socket mkdir -p /run/lighttpd chown ${LIGHTTPD_USER}:${LIGHTTPD_GROUP} /run/lighttpd - mkdir -p /var/cache/lighttpd/compress - chown ${LIGHTTPD_USER}:${LIGHTTPD_GROUP} /var/cache/lighttpd/compress - mkdir -p /var/cache/lighttpd/uploads - chown ${LIGHTTPD_USER}:${LIGHTTPD_GROUP} /var/cache/lighttpd/uploads + + if grep -q -F "OVERWRITTEN BY PI-HOLE" "${lighttpdConfig}"; then + # Attempt to preserve backwards compatibility with older versions + install -D -m 644 -T ${PI_HOLE_LOCAL_REPO}/advanced/${LIGHTTPD_CFG} "${lighttpdConfig}" + # Make the directories if they do not exist and set the owners + mkdir -p /var/cache/lighttpd/compress + chown ${LIGHTTPD_USER}:${LIGHTTPD_GROUP} /var/cache/lighttpd/compress + mkdir -p /var/cache/lighttpd/uploads + chown ${LIGHTTPD_USER}:${LIGHTTPD_GROUP} /var/cache/lighttpd/uploads + fi + # Copy the config file to include for pihole admin interface + if [[ -d "/etc/lighttpd/conf.d" ]]; then + install -D -m 644 -T ${PI_HOLE_LOCAL_REPO}/advanced/pihole-admin.conf /etc/lighttpd/conf.d/pihole-admin.conf + if grep -q -F 'include "/etc/lighttpd/conf.d/pihole-admin.conf"' "${lighttpdConfig}"; then + : + else + echo 'include "/etc/lighttpd/conf.d/pihole-admin.conf"' >> "${lighttpdConfig}" + fi + # Avoid some warnings trace from lighttpd, which might break tests + conf=/etc/lighttpd/conf.d/pihole-admin.conf + if lighttpd -f "${lighttpdConfig}" -tt 2>&1 | grep -q -F "WARNING: unknown config-key: dir-listing\."; then + echo '# Avoid some warnings trace from lighttpd, which might break tests' >> $conf + echo 'server.modules += ( "mod_dirlisting" )' >> $conf + fi + if lighttpd -f "${lighttpdConfig}" -tt 2>&1 | grep -q -F "warning: please use server.use-ipv6"; then + echo '# Avoid some warnings trace from lighttpd, which might break tests' >> $conf + echo 'server.use-ipv6 := "disable"' >> $conf + fi + elif [[ -d "/etc/lighttpd/conf-available" ]]; then + conf=/etc/lighttpd/conf-available/15-pihole-admin.conf + install -D -m 644 -T ${PI_HOLE_LOCAL_REPO}/advanced/pihole-admin.conf $conf + + # Get the version number of lighttpd + version=$(dpkg-query -f='${Version}\n' --show lighttpd) + # Test if that version is greater than or euqal to 1.4.56 + if dpkg --compare-versions "$version" "ge" "1.4.56"; then + # If it is, then we don't need to disable the modules + # (server.modules duplication is ignored in lighttpd 1.4.56+) + : + else + # disable server.modules += ( ... ) in $conf to avoid module dups + if awk '!/^server\.modules/{print}' $conf > $conf.$$ && mv $conf.$$ $conf; then + : + else + rm $conf.$$ + fi + fi + + chmod 644 $conf + if is_command lighty-enable-mod ; then + lighty-enable-mod pihole-admin access accesslog redirect fastcgi setenv > /dev/null || true + else + # Otherwise, show info about installing them + printf " %b Warning: 'lighty-enable-mod' utility not found\\n" "${INFO}" + printf " Please ensure fastcgi is enabled if you experience issues\\n" + fi + else + # lighttpd config include dir not found + printf " %b Warning: lighttpd config include dir not found\\n" "${INFO}" + printf " Please manually install pihole-admin.conf\\n" + fi fi } @@ -1480,7 +1593,7 @@ disable_resolved_stublistener() { # Check if Systemd-resolved's DNSStubListener is enabled and active on port 53 if check_service_active "systemd-resolved"; then # Check if DNSStubListener is enabled - printf " %b %b Testing if systemd-resolved DNSStub-Listener is active" "${OVER}" "${INFO}" + printf " %b %b Testing if systemd-resolved DNSStub-Listener is active" "${OVER}" "${INFO}" if ( grep -E '#?DNSStubListener=yes' /etc/systemd/resolved.conf &> /dev/null ); then # Disable the DNSStubListener to unbind it from port 53 # Note that this breaks dns functionality on host until dnsmasq/ftl are up and running @@ -1517,7 +1630,7 @@ update_package_cache() { UPDATE_PKG_CACHE="apt update" fi printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" - printf " %bError: 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_LIGHT_RED}" "sudo ${UPDATE_PKG_CACHE}" "${COL_NC}" return 1 fi } @@ -1572,9 +1685,9 @@ install_dependent_packages() { # Running apt-get install with minimal output can cause some issues with # requiring user input (e.g password for phpmyadmin see #218) printf " %b Processing %s install(s) for: %s, please wait...\\n" "${INFO}" "${PKG_MANAGER}" "${installArray[*]}" - printf '%*s\n' "$columns" '' | tr " " -; + printf '%*s\n' "${c}" '' | tr " " -; "${PKG_INSTALL[@]}" "${installArray[@]}" - printf '%*s\n' "$columns" '' | tr " " -; + printf '%*s\n' "${c}" '' | tr " " -; return fi printf "\\n" @@ -1595,9 +1708,9 @@ install_dependent_packages() { # If there's anything to install, install everything in the list. if [[ "${#installArray[@]}" -gt 0 ]]; then printf " %b Processing %s install(s) for: %s, please wait...\\n" "${INFO}" "${PKG_MANAGER}" "${installArray[*]}" - printf '%*s\n' "$columns" '' | tr " " -; + printf '%*s\n' "${c}" '' | tr " " -; "${PKG_INSTALL[@]}" "${installArray[@]}" - printf '%*s\n' "$columns" '' | tr " " -; + printf '%*s\n' "${c}" '' | tr " " -; return fi printf "\\n" @@ -1606,35 +1719,6 @@ install_dependent_packages() { # Install the Web interface dashboard installPiholeWeb() { - printf "\\n %b Installing blocking page...\\n" "${INFO}" - - local str="Creating directory for blocking page, and copying files" - printf " %b %s..." "${INFO}" "${str}" - # Install the directory, - install -d -m 0755 ${PI_HOLE_BLOCKPAGE_DIR} - # and the blockpage - install -D -m 644 ${PI_HOLE_LOCAL_REPO}/advanced/{index,blockingpage}.* ${PI_HOLE_BLOCKPAGE_DIR}/ - - # Remove superseded file - if [[ -e "${PI_HOLE_BLOCKPAGE_DIR}/index.js" ]]; then - rm "${PI_HOLE_BLOCKPAGE_DIR}/index.js" - fi - - printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" - - local str="Backing up index.lighttpd.html" - printf " %b %s..." "${INFO}" "${str}" - # If the default index file exists, - if [[ -f "${webroot}/index.lighttpd.html" ]]; then - # back it up - mv ${webroot}/index.lighttpd.html ${webroot}/index.lighttpd.orig - printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" - else - # Otherwise, don't do anything - printf "%b %b %s\\n" "${OVER}" "${INFO}" "${str}" - printf " No default index.lighttpd.html file found... not backing up\\n" - fi - # Install Sudoers file local str="Installing sudoer file" printf "\\n %b %s..." "${INFO}" "${str}" @@ -1710,20 +1794,35 @@ create_pihole_user() { else # If the pihole user doesn't exist, printf "%b %b %s" "${OVER}" "${CROSS}" "${str}" - local str="Creating user 'pihole'" - printf "%b %b %s..." "${OVER}" "${INFO}" "${str}" - # create her with the useradd command, + local str="Checking for group 'pihole'" + printf " %b %s..." "${INFO}" "${str}" if getent group pihole > /dev/null 2>&1; then - # then add her to the pihole group (as it already exists) + # group pihole exists + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" + # 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 printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" else printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" fi else - # add user pihole with default group settings - if useradd -r -s /usr/sbin/nologin pihole; then + # group pihole does not exist + printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" + local str="Creating group 'pihole'" + # if group can be created + if groupadd pihole; then printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" + # 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 + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" + else + printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" + fi + else printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" fi @@ -1733,48 +1832,28 @@ create_pihole_user() { # This function saves any changes to the setup variables into the setupvars.conf file for future runs finalExports() { - # If the Web interface is not set to be installed, - if [[ "${INSTALL_WEB_INTERFACE}" == false ]]; then - # and if there is not an IPv4 address, - if [[ "${IPV4_ADDRESS}" ]]; then - # there is no block page, so set IPv4 to 0.0.0.0 (all IP addresses) - IPV4_ADDRESS="0.0.0.0" - fi - if [[ "${IPV6_ADDRESS}" ]]; then - # and IPv6 to ::/0 - IPV6_ADDRESS="::/0" - fi - fi + # set or update the variables in the file + + addOrEditKeyValPair "${setupVars}" "PIHOLE_INTERFACE" "${PIHOLE_INTERFACE}" + addOrEditKeyValPair "${setupVars}" "PIHOLE_DNS_1" "${PIHOLE_DNS_1}" + addOrEditKeyValPair "${setupVars}" "PIHOLE_DNS_2" "${PIHOLE_DNS_2}" + addOrEditKeyValPair "${setupVars}" "QUERY_LOGGING" "${QUERY_LOGGING}" + addOrEditKeyValPair "${setupVars}" "INSTALL_WEB_SERVER" "${INSTALL_WEB_SERVER}" + addOrEditKeyValPair "${setupVars}" "INSTALL_WEB_INTERFACE" "${INSTALL_WEB_INTERFACE}" + addOrEditKeyValPair "${setupVars}" "LIGHTTPD_ENABLED" "${LIGHTTPD_ENABLED}" + addOrEditKeyValPair "${setupVars}" "CACHE_SIZE" "${CACHE_SIZE}" + addOrEditKeyValPair "${setupVars}" "DNS_FQDN_REQUIRED" "${DNS_FQDN_REQUIRED:-true}" + addOrEditKeyValPair "${setupVars}" "DNS_BOGUS_PRIV" "${DNS_BOGUS_PRIV:-true}" + addOrEditKeyValPair "${setupVars}" "DNSMASQ_LISTENING" "${DNSMASQ_LISTENING:-local}" - # If the setup variable file exists, - if [[ -e "${setupVars}" ]]; then - # update the variables in the file - sed -i.update.bak '/PIHOLE_INTERFACE/d;/IPV4_ADDRESS/d;/IPV6_ADDRESS/d;/PIHOLE_DNS_1\b/d;/PIHOLE_DNS_2\b/d;/QUERY_LOGGING/d;/INSTALL_WEB_SERVER/d;/INSTALL_WEB_INTERFACE/d;/LIGHTTPD_ENABLED/d;/CACHE_SIZE/d;/DNS_FQDN_REQUIRED/d;/DNS_BOGUS_PRIV/d;/DNSMASQ_LISTENING/d;' "${setupVars}" - fi - # echo the information to the user - { - echo "PIHOLE_INTERFACE=${PIHOLE_INTERFACE}" - echo "IPV4_ADDRESS=${IPV4_ADDRESS}" - echo "IPV6_ADDRESS=${IPV6_ADDRESS}" - echo "PIHOLE_DNS_1=${PIHOLE_DNS_1}" - echo "PIHOLE_DNS_2=${PIHOLE_DNS_2}" - echo "QUERY_LOGGING=${QUERY_LOGGING}" - echo "INSTALL_WEB_SERVER=${INSTALL_WEB_SERVER}" - echo "INSTALL_WEB_INTERFACE=${INSTALL_WEB_INTERFACE}" - echo "LIGHTTPD_ENABLED=${LIGHTTPD_ENABLED}" - echo "CACHE_SIZE=${CACHE_SIZE}" - echo "DNS_FQDN_REQUIRED=${DNS_FQDN_REQUIRED:-true}" - echo "DNS_BOGUS_PRIV=${DNS_BOGUS_PRIV:-true}" - echo "DNSMASQ_LISTENING=${DNSMASQ_LISTENING:-local}" - }>> "${setupVars}" chmod 644 "${setupVars}" # Set the privacy level - sed -i '/PRIVACYLEVEL/d' "${PI_HOLE_CONFIG_DIR}/pihole-FTL.conf" - echo "PRIVACYLEVEL=${PRIVACY_LEVEL}" >> "${PI_HOLE_CONFIG_DIR}/pihole-FTL.conf" + addOrEditKeyValPair "${FTL_CONFIG_FILE}" "PRIVACYLEVEL" "${PRIVACY_LEVEL}" # Bring in the current settings and the functions to manipulate them source "${setupVars}" + # shellcheck source=advanced/Scripts/webpage.sh source "${PI_HOLE_LOCAL_REPO}/advanced/Scripts/webpage.sh" # Look for DNS server settings which would have to be reapplied @@ -1791,6 +1870,16 @@ installLogrotate() { printf "\\n %b %s..." "${INFO}" "${str}" if [[ -f ${target} ]]; then + + # Account for changed logfile paths from /var/log -> /var/log/pihole/ made in core v5.11. + if grep -q "/var/log/pihole.log" ${target} || grep -q "/var/log/pihole-FTL.log" ${target}; then + sed -i 's/\/var\/log\/pihole.log/\/var\/log\/pihole\/pihole.log/g' ${target} + sed -i 's/\/var\/log\/pihole-FTL.log/\/var\/log\/pihole\/FTL.log/g' ${target} + + printf "\\n\\t%b Old log file paths updated in existing logrotate file. \\n" "${INFO}" + return 3 + fi + printf "\\n\\t%b Existing logrotate file found. No changes made.\\n" "${INFO}" # Return value isn't that important, using 2 to indicate that it's not a fatal error but # the function did not complete. @@ -1832,15 +1921,6 @@ installPihole() { # Give lighttpd access to the pihole group so the web interface can # manage the gravity.db database usermod -a -G pihole ${LIGHTTPD_USER} - # If the lighttpd command is executable, - if is_command lighty-enable-mod ; then - # enable fastcgi and fastcgi-php - lighty-enable-mod fastcgi fastcgi-php > /dev/null || true - else - # Otherwise, show info about installing them - printf " %b Warning: 'lighty-enable-mod' utility not found\\n" "${INFO}" - printf " Please ensure fastcgi is enabled if you experience issues\\n" - fi fi fi # Install base files and web interface @@ -1848,6 +1928,16 @@ installPihole() { printf " %b Failure in dependent script copy function.\\n" "${CROSS}" exit 1 fi + + # /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 + source "${PI_HOLE_INSTALL_DIR}/utils.sh" + else + printf " %b Failure: /opt/pihole/utils.sh does not exist .\\n" "${CROSS}" + exit 1 + fi + # Install config files if ! installConfigs; then printf " %b Failure in dependent config copy function.\\n" "${CROSS}" @@ -1886,22 +1976,22 @@ checkSelinux() { DEFAULT_SELINUX=$(awk -F= '/^SELINUX=/ {print $2}' /etc/selinux/config) case "${DEFAULT_SELINUX,,}" in enforcing) - printf " %b %bDefault SELinux: %s%b\\n" "${CROSS}" "${COL_RED}" "${DEFAULT_SELINUX}" "${COL_NC}" + printf " %b %bDefault SELinux: %s%b\\n" "${CROSS}" "${COL_RED}" "${DEFAULT_SELINUX,,}" "${COL_NC}" SELINUX_ENFORCING=1 ;; *) # 'permissive' and 'disabled' - printf " %b %bDefault SELinux: %s%b\\n" "${TICK}" "${COL_GREEN}" "${DEFAULT_SELINUX}" "${COL_NC}" + printf " %b %bDefault SELinux: %s%b\\n" "${TICK}" "${COL_GREEN}" "${DEFAULT_SELINUX,,}" "${COL_NC}" ;; esac # Check the current state of SELinux CURRENT_SELINUX=$(getenforce) case "${CURRENT_SELINUX,,}" in enforcing) - printf " %b %bCurrent SELinux: %s%b\\n" "${CROSS}" "${COL_RED}" "${CURRENT_SELINUX}" "${COL_NC}" + printf " %b %bCurrent SELinux: %s%b\\n" "${CROSS}" "${COL_RED}" "${CURRENT_SELINUX,,}" "${COL_NC}" SELINUX_ENFORCING=1 ;; *) # 'permissive' and 'disabled' - printf " %b %bCurrent SELinux: %s%b\\n" "${TICK}" "${COL_GREEN}" "${CURRENT_SELINUX}" "${COL_NC}" + printf " %b %bCurrent SELinux: %s%b\\n" "${TICK}" "${COL_GREEN}" "${CURRENT_SELINUX,,}" "${COL_NC}" ;; esac else @@ -1937,20 +2027,17 @@ displayFinalMessage() { # If the user wants to install the dashboard, if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then # Store a message in a variable and display it - additional="View the web interface at http://pi.hole/admin or http://${IPV4_ADDRESS%/*}/admin - -Your Admin Webpage login password is ${pwstring}" + additional="View the web interface at http://pi.hole/admin or http://${IPV4_ADDRESS%/*}/admin\\n\\nYour Admin Webpage login password is ${pwstring}" fi # Final completion message to user - whiptail --msgbox --backtitle "Make it so." --title "Installation Complete!" "Configure your devices to use the Pi-hole as their DNS server using: - -IPv4: ${IPV4_ADDRESS%/*} -IPv6: ${IPV6_ADDRESS:-"Not Configured"} - -If you have not done so already, the above IP should be set to static. - -${additional}" "${r}" "${c}" + 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() { @@ -1970,20 +2057,32 @@ update_dialogs() { opt2b="Resets Pi-hole and allows re-selecting settings." # Display the information to the user - UpdateCmd=$(whiptail --title "Existing Install Detected!" --menu "\\n\\nWe have detected an existing install.\\n\\nPlease choose from the following options: \\n($strAdd)" "${r}" "${c}" 2 \ + 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}" 3>&2 2>&1 1>&3) || \ - { printf " %bCancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"; exit 1; } + "${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}) + "${opt1a}") printf " %b %s option selected\\n" "${INFO}" "${opt1a}" useUpdateVars=true ;; # reconfigure, - ${opt2a}) + "${opt2a}") printf " %b %s option selected\\n" "${INFO}" "${opt2a}" useUpdateVars=false ;; @@ -2064,11 +2163,7 @@ checkout_pull_branch() { git_pull=$(git pull --no-rebase || return 1) - if [[ "$git_pull" == *"up-to-date"* ]]; then - printf " %b %s\\n" "${INFO}" "${git_pull}" - else - printf "%s\\n" "$git_pull" - fi + printf " %b %s\\n" "${INFO}" "${git_pull}" return 0 } @@ -2079,14 +2174,14 @@ clone_or_update_repos() { printf " %b Performing reconfiguration, skipping download of local repos\\n" "${INFO}" # Reset the Core repo resetRepo ${PI_HOLE_LOCAL_REPO} || \ - { printf " %bUnable 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_LIGHT_RED}" "${PI_HOLE_LOCAL_REPO}" "${COL_NC}"; \ exit 1; \ } # If the Web interface was installed, if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then # reset it's repo resetRepo ${webInterfaceDir} || \ - { printf " %bUnable to reset %s, exiting installer%b\\n" "${COL_LIGHT_RED}" "${webInterfaceDir}" "${COL_NC}"; \ + { printf " %b Unable to reset %s, exiting installer%b\\n" "${COL_LIGHT_RED}" "${webInterfaceDir}" "${COL_NC}"; \ exit 1; \ } fi @@ -2094,14 +2189,14 @@ clone_or_update_repos() { else # so get git files for Core getGitFiles ${PI_HOLE_LOCAL_REPO} ${piholeGitUrl} || \ - { printf " %bUnable 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_LIGHT_RED}" "${piholeGitUrl}" "${PI_HOLE_LOCAL_REPO}" "${COL_NC}"; \ exit 1; \ } # If the Web interface was installed, if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then # get the Web git files getGitFiles ${webInterfaceDir} ${webInterfaceGitUrl} || \ - { printf " %bUnable 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_LIGHT_RED}" "${webInterfaceGitUrl}" "${COL_NC}"; \ exit 1; \ } fi @@ -2119,9 +2214,6 @@ FTLinstall() { # Move into the temp ftl directory pushd "$(mktemp -d)" > /dev/null || { printf "Unable to make temporary directory for FTL binary download\\n"; return 1; } - # Always replace pihole-FTL.service - install -T -m 0755 "${PI_HOLE_LOCAL_REPO}/advanced/Templates/pihole-FTL.service" "/etc/init.d/pihole-FTL" - local ftlBranch local url @@ -2168,7 +2260,7 @@ FTLinstall() { # Otherwise, the hash download failed, so print and exit. popd > /dev/null || { printf "Unable to return to original directory after FTL binary download.\\n"; return 1; } printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" - printf " %bError: 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_LIGHT_RED}" "${url}" "${binary}" "${COL_NC}" return 1 fi else @@ -2176,7 +2268,7 @@ FTLinstall() { popd > /dev/null || { printf "Unable to return to original directory after FTL binary download.\\n"; return 1; } printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" # The URL could not be found - printf " %bError: 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_LIGHT_RED}" "${url}" "${binary}" "${COL_NC}" return 1 fi } @@ -2218,7 +2310,7 @@ get_binary_name() { local rev rev=$(uname -m | sed "s/[^0-9]//g;") local lib - lib=$(ldd "$(which sh)" | grep -E '^\s*/lib' | awk '{ print $1 }') + lib=$(ldd "$(command -v sh)" | grep -E '^\s*/lib' | awk '{ print $1 }') if [[ "${lib}" == "/lib/ld-linux-aarch64.so.1" ]]; then printf "%b %b Detected AArch64 (64 Bit ARM) processor\\n" "${OVER}" "${TICK}" # set the binary to be used @@ -2274,6 +2366,9 @@ get_binary_name() { # set the binary to be used l_binary="pihole-FTL-linux-x86_64" fi + elif [[ "${machine}" == "riscv64" ]]; then + printf "%b %b Detected riscv64 processor\\n" "${OVER}" "${TICK}" + l_binary="pihole-FTL-riscv64-linux-gnu" else # Something else - we try to use 32bit executable and warn the user if [[ ! "${machine}" == "i686" ]]; then @@ -2455,6 +2550,9 @@ main() { fi fi + # Check if SELinux is Enforcing and exit before doing anything else + checkSelinux + # Check for supported package managers so that we may install dependencies package_manager_detect @@ -2472,19 +2570,11 @@ main() { printf " %b Checking for / installing Required dependencies for this install script...\\n" "${INFO}" install_dependent_packages "${INSTALLER_DEPS[@]}" - #In case of RPM based distro, select the proper PHP version - if [[ "$PKG_MANAGER" == "yum" || "$PKG_MANAGER" == "dnf" ]] ; then - select_rpm_php - fi - - # Check if SELinux is Enforcing - checkSelinux - # If the setup variable file exists, if [[ -f "${setupVars}" ]]; then # if it's running unattended, if [[ "${runUnattended}" == true ]]; then - printf " %b Performing unattended setup, no whiptail dialogs will be displayed\\n" "${INFO}" + printf " %b Performing unattended setup, no dialogs will be displayed\\n" "${INFO}" # Use the setup variables useUpdateVars=true # also disable debconf-apt-progress dialogs @@ -2524,8 +2614,9 @@ main() { source "${setupVars}" # Get the privacy level if it exists (default is 0) - if [[ -f "${PI_HOLE_CONFIG_DIR}/pihole-FTL.conf" ]]; then - PRIVACY_LEVEL=$(sed -ne 's/PRIVACYLEVEL=\(.*\)/\1/p' "${PI_HOLE_CONFIG_DIR}/pihole-FTL.conf") + if [[ -f "${FTL_CONFIG_FILE}" ]]; then + # get the value from $FTL_CONFIG_FILE (and ignoring all commented lines) + PRIVACY_LEVEL=$(sed -e '/^[[:blank:]]*#/d' "${FTL_CONFIG_FILE}" | grep "PRIVACYLEVEL" | awk -F "=" 'NR==1{printf$2}') # If no setting was found, default to 0 PRIVACY_LEVEL="${PRIVACY_LEVEL:-0}" @@ -2594,7 +2685,7 @@ main() { # Check for and disable systemd-resolved-DNSStubListener before reloading resolved # DNSStubListener needs to remain in place for installer to download needed files, # so this change needs to be made after installation is complete, - # but before starting or resarting the dnsmasq or ftl services + # but before starting or restarting the dnsmasq or ftl services disable_resolved_stublistener # If the Web server was installed, @@ -2615,14 +2706,42 @@ main() { # Fixes a problem reported on Ubuntu 18.04 where trying to start # the service before enabling causes installer to exit enable_service pihole-FTL + + # If this is an update from a previous Pi-hole installation + # we need to move any existing `pihole*` logs from `/var/log` to `/var/log/pihole` + # if /var/log/pihole.log is not a symlink (set during FTL startup) move the files + # can be removed with Pi-hole v6.0 + # To be sure FTL is not running when we move the files we explicitly stop it here + + stop_service pihole-FTL &> /dev/null + + if [ ! -d /var/log/pihole/ ]; then + mkdir -m 0755 /var/log/pihole/ + fi + + # Special handling for pihole-FTL.log -> pihole/FTL.log + if [ -f /var/log/pihole-FTL.log ] && [ ! -L /var/log/pihole-FTL.log ]; then + # /var/log/pihole-FTL.log -> /var/log/pihole/FTL.log + # /var/log/pihole-FTL.log.1 -> /var/log/pihole/FTL.log.1 + # /var/log/pihole-FTL.log.2.gz -> /var/log/pihole/FTL.log.2.gz + # /var/log/pihole-FTL.log.3.gz -> /var/log/pihole/FTL.log.3.gz + # /var/log/pihole-FTL.log.4.gz -> /var/log/pihole/FTL.log.4.gz + # /var/log/pihole-FTL.log.5.gz -> /var/log/pihole/FTL.log.5.gz + for f in /var/log/pihole-FTL.log*; do mv "$f" "$( sed "s/pihole-/pihole\//" <<< "$f")"; done + fi + + # Remaining log files + if [ -f /var/log/pihole.log ] && [ ! -L /var/log/pihole.log ]; then + mv /var/log/pihole*.* /var/log/pihole/ 2>/dev/null + fi + restart_service pihole-FTL # Download and compile the aggregated block list runGravity - # Force an update of the updatechecker + # Update local and remote versions via updatechecker /opt/pihole/updatecheck.sh - /opt/pihole/updatecheck.sh x remote if [[ "${useUpdateVars}" == false ]]; then displayFinalMessage "${pw}" @@ -2655,7 +2774,7 @@ main() { # Display where the log file is printf "\\n %b The install log is located at: %s\\n" "${INFO}" "${installLogLoc}" - printf "%b%s Complete! %b\\n" "${COL_LIGHT_GREEN}" "${INSTALL_TYPE}" "${COL_NC}" + printf " %b %b%s complete! %b\\n" "${TICK}" "${COL_LIGHT_GREEN}" "${INSTALL_TYPE}" "${COL_NC}" if [[ "${INSTALL_TYPE}" == "Update" ]]; then printf "\\n" @@ -2663,6 +2782,7 @@ main() { fi } -if [[ "${PH_TEST}" != true ]] ; then +# allow to source this script without running it +if [[ "${SKIP_INSTALL}" != true ]] ; then main "$@" fi diff --git a/automated install/uninstall.sh b/automated install/uninstall.sh index 9d3fca31f5..7a1a290de1 100755 --- a/automated install/uninstall.sh +++ b/automated install/uninstall.sh @@ -36,7 +36,7 @@ else fi readonly PI_HOLE_FILES_DIR="/etc/.pihole" -PH_TEST="true" +SKIP_INSTALL="true" source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" # setupVars set in basic-install.sh source "${setupVars}" @@ -44,8 +44,8 @@ source "${setupVars}" # package_manager_detect() sourced from basic-install.sh package_manager_detect -# Install packages used by the Pi-hole -DEPS=("${INSTALLER_DEPS[@]}" "${PIHOLE_DEPS[@]}") +# Uninstall packages used by the Pi-hole +DEPS=("${INSTALLER_DEPS[@]}" "${PIHOLE_DEPS[@]}" "${OS_CHECK_DEPS[@]}") if [[ "${INSTALL_WEB_SERVER}" == true ]]; then # Install the Web dependencies DEPS+=("${PIHOLE_WEB_DEPS[@]}") @@ -131,6 +131,7 @@ removeNoPurge() { fi if package_check lighttpd > /dev/null; then + # Attempt to preserve backwards compatibility with older versions if [[ -f /etc/lighttpd/lighttpd.conf.orig ]]; then ${SUDO} mv /etc/lighttpd/lighttpd.conf.orig /etc/lighttpd/lighttpd.conf fi @@ -139,6 +140,29 @@ removeNoPurge() { ${SUDO} rm /etc/lighttpd/external.conf fi + # Fedora-based + if [[ -f /etc/lighttpd/conf.d/pihole-admin.conf ]]; then + ${SUDO} rm /etc/lighttpd/conf.d/pihole-admin.conf + conf=/etc/lighttpd/lighttpd.conf + tconf=/tmp/lighttpd.conf.$$ + if awk '!/^include "\/etc\/lighttpd\/conf\.d\/pihole-admin\.conf"$/{print}' \ + $conf > $tconf && mv $tconf $conf; then + : + else + rm $tconf + fi + ${SUDO} chown root:root $conf + ${SUDO} chmod 644 $conf + fi + + # Debian-based + if [[ -f /etc/lighttpd/conf-available/pihole-admin.conf ]]; then + if is_command lighty-disable-mod ; then + ${SUDO} lighty-disable-mod pihole-admin > /dev/null || true + fi + ${SUDO} rm /etc/lighttpd/conf-available/15-pihole-admin.conf + fi + echo -e " ${TICK} Removed lighttpd configs" fi @@ -146,6 +170,7 @@ removeNoPurge() { ${SUDO} rm -f /etc/dnsmasq.d/01-pihole.conf &> /dev/null ${SUDO} rm -f /etc/dnsmasq.d/06-rfc6761.conf &> /dev/null ${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 @@ -168,6 +193,18 @@ removeNoPurge() { else service pihole-FTL stop fi + ${SUDO} rm -f /etc/systemd/system/pihole-FTL.service + 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 + 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 echo -e "${OVER} ${TICK} Removed pihole-FTL" diff --git a/gravity.sh b/gravity.sh index c5e9e41440..636cde0dda 100755 --- a/gravity.sh +++ b/gravity.sh @@ -40,6 +40,7 @@ gravityDBschema="${piholeGitDir}/advanced/Templates/gravity.db.sql" gravityDBcopy="${piholeGitDir}/advanced/Templates/gravity_copy.sql" domainsExtension="domains" +curl_connect_timeout=10 # Source setupVars from install script setupVars="${piholeDir}/setupVars.conf" @@ -51,6 +52,14 @@ else exit 1 fi +# Set up tmp dir variable in case it's not configured +: "${GRAVITY_TMPDIR:=/tmp}" + +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}" + GRAVITY_TMPDIR="/tmp" +fi + # Source pihole-FTL from install script pihole_FTL="${piholeDir}/pihole-FTL.conf" if [[ -f "${pihole_FTL}" ]]; then @@ -75,7 +84,7 @@ fi # Generate new SQLite3 file from schema template generate_gravity_database() { - if ! pihole-FTL sqlite3 "${gravityDBfile}" < "${gravityDBschema}"; then + if ! pihole-FTL sqlite3 -ni "${gravityDBfile}" < "${gravityDBschema}"; then echo -e " ${CROSS} Unable to create ${gravityDBfile}" return 1 fi @@ -90,7 +99,7 @@ gravity_swap_databases() { echo -ne " ${INFO} ${str}..." # The index is intentionally not UNIQUE as poor quality adlists may contain domains more than once - output=$( { pihole-FTL sqlite3 "${gravityTEMPfile}" "CREATE INDEX idx_gravity ON gravity (domain, adlist_id);"; } 2>&1 ) + output=$( { pihole-FTL sqlite3 -ni "${gravityTEMPfile}" "CREATE INDEX idx_gravity ON gravity (domain, adlist_id);"; } 2>&1 ) status="$?" if [[ "${status}" -ne 0 ]]; then @@ -120,13 +129,13 @@ gravity_swap_databases() { echo -e "${OVER} ${TICK} ${str}" if $oldAvail; then - echo -e " ${TICK} The old database remains available." + echo -e " ${TICK} The old database remains available" fi } # Update timestamp when the gravity table was last updated successfully update_gravity_timestamp() { - output=$( { printf ".timeout 30000\\nINSERT OR REPLACE INTO info (property,value) values ('updated',cast(strftime('%%s', 'now') as int));" | pihole-FTL sqlite3 "${gravityDBfile}"; } 2>&1 ) + output=$( { printf ".timeout 30000\\nINSERT OR REPLACE INTO info (property,value) values ('updated',cast(strftime('%%s', 'now') as int));" | pihole-FTL sqlite3 -ni "${gravityDBfile}"; } 2>&1 ) status="$?" if [[ "${status}" -ne 0 ]]; then @@ -139,12 +148,15 @@ update_gravity_timestamp() { # Import domains from file and store them in the specified database table database_table_from_file() { # Define locals - local table source backup_path backup_file tmpFile type + local table src backup_path backup_file tmpFile list_type table="${1}" - source="${2}" + src="${2}" backup_path="${piholeDir}/migration_backup" backup_file="${backup_path}/$(basename "${2}")" - tmpFile="$(mktemp -p "/tmp" --suffix=".gravity")" + # Create a temporary file. We don't use '--suffix' here because not all + # implementations of mktemp support it, e.g. on Alpine + tmpFile="$(mktemp -p "${GRAVITY_TMPDIR}")" + mv "${tmpFile}" "${tmpFile%.*}.gravity" local timestamp timestamp="$(date --utc +'%s')" @@ -155,28 +167,28 @@ database_table_from_file() { # Special handling for domains to be imported into the common domainlist table if [[ "${table}" == "whitelist" ]]; then - type="0" + list_type="0" table="domainlist" elif [[ "${table}" == "blacklist" ]]; then - type="1" + list_type="1" table="domainlist" elif [[ "${table}" == "regex" ]]; then - type="3" + list_type="3" table="domainlist" fi # Get MAX(id) from domainlist when INSERTing into this table if [[ "${table}" == "domainlist" ]]; then - rowid="$(pihole-FTL sqlite3 "${gravityDBfile}" "SELECT MAX(id) FROM domainlist;")" + rowid="$(pihole-FTL sqlite3 -ni "${gravityDBfile}" "SELECT MAX(id) FROM domainlist;")" if [[ -z "$rowid" ]]; then rowid=0 fi rowid+=1 fi - # Loop over all domains in ${source} file + # Loop over all domains in ${src} file # Read file line by line - grep -v '^ *#' < "${source}" | while IFS= read -r domain + grep -v '^ *#' < "${src}" | while IFS= read -r domain do # Only add non-empty lines if [[ -n "${domain}" ]]; then @@ -185,10 +197,10 @@ database_table_from_file() { echo "${rowid},\"${domain}\",${timestamp}" >> "${tmpFile}" elif [[ "${table}" == "adlist" ]]; then # Adlist table format - echo "${rowid},\"${domain}\",1,${timestamp},${timestamp},\"Migrated from ${source}\",,0,0,0" >> "${tmpFile}" + echo "${rowid},\"${domain}\",1,${timestamp},${timestamp},\"Migrated from ${src}\",,0,0,0" >> "${tmpFile}" else # White-, black-, and regexlist table format - echo "${rowid},${type},\"${domain}\",1,${timestamp},${timestamp},\"Migrated from ${source}\"" >> "${tmpFile}" + echo "${rowid},${list_type},\"${domain}\",1,${timestamp},${timestamp},\"Migrated from ${src}\"" >> "${tmpFile}" fi rowid+=1 fi @@ -197,38 +209,27 @@ database_table_from_file() { # Store domains in database table specified by ${table} # Use printf as .mode and .import need to be on separate lines # see https://unix.stackexchange.com/a/445615/83260 - output=$( { printf ".timeout 30000\\n.mode csv\\n.import \"%s\" %s\\n" "${tmpFile}" "${table}" | pihole-FTL sqlite3 "${gravityDBfile}"; } 2>&1 ) + output=$( { printf ".timeout 30000\\n.mode csv\\n.import \"%s\" %s\\n" "${tmpFile}" "${table}" | pihole-FTL sqlite3 -ni "${gravityDBfile}"; } 2>&1 ) status="$?" if [[ "${status}" -ne 0 ]]; then - echo -e "\\n ${CROSS} Unable to fill table ${table}${type} in database ${gravityDBfile}\\n ${output}" + echo -e "\\n ${CROSS} Unable to fill table ${table}${list_type} in database ${gravityDBfile}\\n ${output}" gravity_Cleanup "error" fi # Move source file to backup directory, create directory if not existing mkdir -p "${backup_path}" - mv "${source}" "${backup_file}" 2> /dev/null || \ - echo -e " ${CROSS} Unable to backup ${source} to ${backup_path}" + mv "${src}" "${backup_file}" 2> /dev/null || \ + echo -e " ${CROSS} Unable to backup ${src} to ${backup_path}" # Delete tmpFile rm "${tmpFile}" > /dev/null 2>&1 || \ echo -e " ${CROSS} Unable to remove ${tmpFile}" } -# Update timestamp of last update of this list. We store this in the "old" database as all values in the new database will later be overwritten -database_adlist_updated() { - output=$( { printf ".timeout 30000\\nUPDATE adlist SET date_updated = (cast(strftime('%%s', 'now') as int)) WHERE id = %i;\\n" "${1}" | pihole-FTL sqlite3 "${gravityDBfile}"; } 2>&1 ) - status="$?" - - if [[ "${status}" -ne 0 ]]; then - echo -e "\\n ${CROSS} Unable to update timestamp of adlist with ID ${1} in database ${gravityDBfile}\\n ${output}" - gravity_Cleanup "error" - fi -} - # Check if a column with name ${2} exists in gravity table with name ${1} gravity_column_exists() { - output=$( { printf ".timeout 30000\\nSELECT EXISTS(SELECT * FROM pragma_table_info('%s') WHERE name='%s');\\n" "${1}" "${2}" | pihole-FTL sqlite3 "${gravityDBfile}"; } 2>&1 ) + output=$( { printf ".timeout 30000\\nSELECT EXISTS(SELECT * FROM pragma_table_info('%s') WHERE name='%s');\\n" "${1}" "${2}" | pihole-FTL sqlite3 -ni "${gravityTEMPfile}"; } 2>&1 ) if [[ "${output}" == "1" ]]; then return 0 # Bash 0 is success fi @@ -243,11 +244,11 @@ database_adlist_number() { return; fi - output=$( { printf ".timeout 30000\\nUPDATE adlist SET number = %i, invalid_domains = %i WHERE id = %i;\\n" "${num_source_lines}" "${num_invalid}" "${1}" | pihole-FTL sqlite3 "${gravityDBfile}"; } 2>&1 ) + output=$( { printf ".timeout 30000\\nUPDATE adlist SET number = %i, invalid_domains = %i WHERE id = %i;\\n" "${2}" "${3}" "${1}" | pihole-FTL sqlite3 -ni "${gravityTEMPfile}"; } 2>&1 ) status="$?" if [[ "${status}" -ne 0 ]]; then - echo -e "\\n ${CROSS} Unable to update number of domains in adlist with ID ${1} in database ${gravityDBfile}\\n ${output}" + echo -e "\\n ${CROSS} Unable to update number of domains in adlist with ID ${1} in database ${gravityTEMPfile}\\n ${output}" gravity_Cleanup "error" fi } @@ -259,11 +260,11 @@ database_adlist_status() { return; fi - output=$( { printf ".timeout 30000\\nUPDATE adlist SET status = %i WHERE id = %i;\\n" "${2}" "${1}" | pihole-FTL sqlite3 "${gravityDBfile}"; } 2>&1 ) + output=$( { printf ".timeout 30000\\nUPDATE adlist SET status = %i WHERE id = %i;\\n" "${2}" "${1}" | pihole-FTL sqlite3 -ni "${gravityTEMPfile}"; } 2>&1 ) status="$?" if [[ "${status}" -ne 0 ]]; then - echo -e "\\n ${CROSS} Unable to update status of adlist with ID ${1} in database ${gravityDBfile}\\n ${output}" + echo -e "\\n ${CROSS} Unable to update status of adlist with ID ${1} in database ${gravityTEMPfile}\\n ${output}" gravity_Cleanup "error" fi } @@ -377,8 +378,8 @@ gravity_DownloadBlocklists() { # Retrieve source URLs from gravity database # We source only enabled adlists, SQLite3 stores boolean values as 0 (false) or 1 (true) - mapfile -t sources <<< "$(pihole-FTL sqlite3 "${gravityDBfile}" "SELECT address FROM vw_adlist;" 2> /dev/null)" - mapfile -t sourceIDs <<< "$(pihole-FTL sqlite3 "${gravityDBfile}" "SELECT id FROM vw_adlist;" 2> /dev/null)" + mapfile -t sources <<< "$(pihole-FTL sqlite3 -ni "${gravityDBfile}" "SELECT address FROM vw_adlist;" 2> /dev/null)" + mapfile -t sourceIDs <<< "$(pihole-FTL sqlite3 -ni "${gravityDBfile}" "SELECT id FROM vw_adlist;" 2> /dev/null)" # Parse source domains from $sources mapfile -t sourceDomains <<< "$( @@ -400,14 +401,14 @@ gravity_DownloadBlocklists() { unset sources fi - local url domain agent cmd_ext str target compression + local url domain str target compression echo "" # Prepare new gravity database str="Preparing new gravity database" echo -ne " ${INFO} ${str}..." rm "${gravityTEMPfile}" > /dev/null 2>&1 - output=$( { pihole-FTL sqlite3 "${gravityTEMPfile}" < "${gravityDBschema}"; } 2>&1 ) + output=$( { pihole-FTL sqlite3 -ni "${gravityTEMPfile}" < "${gravityDBschema}"; } 2>&1 ) status="$?" if [[ "${status}" -ne 0 ]]; then @@ -417,7 +418,24 @@ gravity_DownloadBlocklists() { echo -e "${OVER} ${TICK} ${str}" fi - target="$(mktemp -p "/tmp" --suffix=".gravity")" + str="Creating new gravity databases" + echo -ne " ${INFO} ${str}..." + + # Gravity copying SQL script + copyGravity="$(cat "${gravityDBcopy}")" + if [[ "${gravityDBfile}" != "${gravityDBfile_default}" ]]; then + # Replace default gravity script location by custom location + copyGravity="${copyGravity//"${gravityDBfile_default}"/"${gravityDBfile}"}" + fi + + output=$( { pihole-FTL sqlite3 -ni "${gravityTEMPfile}" <<< "${copyGravity}"; } 2>&1 ) + status="$?" + + if [[ "${status}" -ne 0 ]]; then + echo -e "\\n ${CROSS} Unable to copy data from ${gravityDBfile} to ${gravityTEMPfile}\\n ${output}" + return 1 + fi + echo -e "${OVER} ${TICK} ${str}" # Use compression to reduce the amount of data that is transferred # between the Pi-hole and the ad list provider. Use this feature @@ -439,15 +457,6 @@ gravity_DownloadBlocklists() { saveLocation="${piholeDir}/list.${id}.${domain}.${domainsExtension}" activeDomains[$i]="${saveLocation}" - # Default user-agent (for Cloudflare's Browser Integrity Check: https://support.cloudflare.com/hc/en-us/articles/200170086-What-does-the-Browser-Integrity-Check-do-) - agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36" - - # Provide special commands for blocklists which may need them - case "${domain}" in - "pgl.yoyo.org") cmd_ext="-d mimetype=plaintext -d hostformat=hosts";; - *) cmd_ext="";; - esac - echo -e " ${INFO} Target: ${url}" local regex check_url # Check for characters NOT allowed in URLs @@ -460,103 +469,14 @@ gravity_DownloadBlocklists() { if [[ "${check_url}" =~ ${regex} ]]; then echo -e " ${CROSS} Invalid Target" else - gravity_DownloadBlocklistFromUrl "${url}" "${cmd_ext}" "${agent}" "${sourceIDs[$i]}" "${saveLocation}" "${target}" "${compression}" + gravity_DownloadBlocklistFromUrl "${url}" "${sourceIDs[$i]}" "${saveLocation}" "${target}" "${compression}" fi echo "" done - str="Creating new gravity databases" - echo -ne " ${INFO} ${str}..." - - # Gravity copying SQL script - copyGravity="$(cat "${gravityDBcopy}")" - if [[ "${gravityDBfile}" != "${gravityDBfile_default}" ]]; then - # Replace default gravity script location by custom location - copyGravity="${copyGravity//"${gravityDBfile_default}"/"${gravityDBfile}"}" - fi - - output=$( { pihole-FTL sqlite3 "${gravityTEMPfile}" <<< "${copyGravity}"; } 2>&1 ) - status="$?" - - if [[ "${status}" -ne 0 ]]; then - echo -e "\\n ${CROSS} Unable to copy data from ${gravityDBfile} to ${gravityTEMPfile}\\n ${output}" - return 1 - fi - echo -e "${OVER} ${TICK} ${str}" - - str="Storing downloaded domains in new gravity database" - echo -ne " ${INFO} ${str}..." - output=$( { printf ".timeout 30000\\n.mode csv\\n.import \"%s\" gravity\\n" "${target}" | pihole-FTL sqlite3 "${gravityTEMPfile}"; } 2>&1 ) - status="$?" - - if [[ "${status}" -ne 0 ]]; then - echo -e "\\n ${CROSS} Unable to fill gravity table in database ${gravityTEMPfile}\\n ${output}" - gravity_Cleanup "error" - else - echo -e "${OVER} ${TICK} ${str}" - fi - - if [[ "${status}" -eq 0 && -n "${output}" ]]; then - echo -e " Encountered non-critical SQL warnings. Please check the suitability of the lists you're using!\\n\\n SQL warnings:" - local warning file line lineno - while IFS= read -r line; do - echo " - ${line}" - warning="$(grep -oh "^[^:]*:[0-9]*" <<< "${line}")" - file="${warning%:*}" - lineno="${warning#*:}" - if [[ -n "${file}" && -n "${lineno}" ]]; then - echo -n " Line contains: " - awk "NR==${lineno}" < "${file}" - fi - done <<< "${output}" - echo "" - fi - - rm "${target}" > /dev/null 2>&1 || \ - echo -e " ${CROSS} Unable to remove ${target}" - gravity_Blackbody=true } -# num_target_lines does increase for every correctly added domain in pareseList() -num_target_lines=0 -num_source_lines=0 -num_invalid=0 -parseList() { - local adlistID="${1}" src="${2}" target="${3}" incorrect_lines - # This sed does the following things: - # 1. Remove all domains containing invalid characters. Valid are: a-z, A-Z, 0-9, dot (.), minus (-), underscore (_) - # 2. Append ,adlistID to every line - # 3. Remove trailing period (see https://github.com/pi-hole/pi-hole/issues/4701) - # 4. Ensures there is a newline on the last line - sed -e "/[^a-zA-Z0-9.\_-]/d;s/\.$//;s/$/,${adlistID}/;/.$/a\\" "${src}" >> "${target}" - # Find (up to) five domains containing invalid characters (see above) - incorrect_lines="$(sed -e "/[^a-zA-Z0-9.\_-]/!d" "${src}" | head -n 5)" - - local num_target_lines_new num_correct_lines - # Get number of lines in source file - num_source_lines="$(grep -c "^" "${src}")" - # Get the new number of lines in destination file - num_target_lines_new="$(grep -c "^" "${target}")" - # Number of new correctly added lines - num_correct_lines="$(( num_target_lines_new-num_target_lines ))" - # Upate number of lines in target file - num_target_lines="$num_target_lines_new" - num_invalid="$(( num_source_lines-num_correct_lines ))" - if [[ "${num_invalid}" -eq 0 ]]; then - echo " ${INFO} Analyzed ${num_source_lines} domains" - else - echo " ${INFO} Analyzed ${num_source_lines} domains, ${num_invalid} domains invalid!" - fi - - # Display sample of invalid lines if we found some - if [[ -n "${incorrect_lines}" ]]; then - echo " Sample of invalid domains:" - while IFS= read -r line; do - echo " - ${line}" - done <<< "${incorrect_lines}" - fi -} compareLists() { local adlistID="${1}" target="${2}" @@ -567,7 +487,6 @@ compareLists() { sha1sum "${target}" > "${target}.sha1" echo " ${INFO} List has been updated" database_adlist_status "${adlistID}" "1" - database_adlist_updated "${adlistID}" else echo " ${INFO} List stayed unchanged" database_adlist_status "${adlistID}" "2" @@ -577,17 +496,19 @@ compareLists() { sha1sum "${target}" > "${target}.sha1" # We assume here it was changed upstream database_adlist_status "${adlistID}" "1" - database_adlist_updated "${adlistID}" fi } # Download specified URL and perform checks on HTTP status and file content gravity_DownloadBlocklistFromUrl() { - local url="${1}" cmd_ext="${2}" agent="${3}" adlistID="${4}" saveLocation="${5}" target="${6}" compression="${7}" - local heisenbergCompensator="" patternBuffer str httpCode success="" ip + local url="${1}" adlistID="${2}" saveLocation="${3}" target="${4}" compression="${5}" + local heisenbergCompensator="" listCurlBuffer str httpCode success="" ip cmd_ext + local file_path permissions ip_addr port blocked=false download=true # Create temp file to store content on disk instead of RAM - patternBuffer=$(mktemp -p "/tmp" --suffix=".phgpb") + # We don't use '--suffix' here because not all implementations of mktemp support it, e.g. on Alpine + listCurlBuffer="$(mktemp -p "${GRAVITY_TMPDIR}")" + mv "${listCurlBuffer}" "${listCurlBuffer%.*}.phgpb" # Determine if $saveLocation has read permission if [[ -r "${saveLocation}" && $url != "file"* ]]; then @@ -599,7 +520,6 @@ gravity_DownloadBlocklistFromUrl() { str="Status:" echo -ne " ${INFO} ${str} Pending..." - blocked=false case $BLOCKINGMODE in "IP-NODATA-AAAA"|"IP") # Get IP address of this domain @@ -637,19 +557,47 @@ gravity_DownloadBlocklistFromUrl() { bad_list=$(pihole -q -adlist "${domain}" | head -n1 | awk -F 'Match found in ' '{print $2}') echo -e "${OVER} ${CROSS} ${str} ${domain} is blocked by ${bad_list%:}. Using DNS on ${PIHOLE_DNS_1} to download ${url}"; echo -ne " ${INFO} ${str} Pending..." - cmd_ext="--resolve $domain:$port:$ip $cmd_ext" + cmd_ext="--resolve $domain:$port:$ip" + 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 + # Get the file path + 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)" + download=false + fi + fi fi - # shellcheck disable=SC2086 - httpCode=$(curl -s -L ${compression} ${cmd_ext} ${heisenbergCompensator} -w "%{http_code}" -A "${agent}" "${url}" -o "${patternBuffer}" 2> /dev/null) + if [[ "${download}" == true ]]; then + # shellcheck disable=SC2086 + httpCode=$(curl --connect-timeout ${curl_connect_timeout} -s -L ${compression} ${cmd_ext} ${heisenbergCompensator} -w "%{http_code}" "${url}" -o "${listCurlBuffer}" 2> /dev/null) + fi case $url in # Did we "download" a local file? "file"*) - if [[ -s "${patternBuffer}" ]]; then + if [[ -s "${listCurlBuffer}" ]]; then echo -e "${OVER} ${TICK} ${str} Retrieval successful"; success=true else - echo -e "${OVER} ${CROSS} ${str} Not found / empty list" + echo -e "${OVER} ${CROSS} ${str} Retrieval failed / empty list" fi;; # Did we "download" a remote file? *) @@ -675,24 +623,22 @@ gravity_DownloadBlocklistFromUrl() { if [[ "${success}" == true ]]; then if [[ "${httpCode}" == "304" ]]; then # Add domains to database table file - parseList "${adlistID}" "${saveLocation}" "${target}" + pihole-FTL gravity parseList "${saveLocation}" "${gravityTEMPfile}" "${adlistID}" database_adlist_status "${adlistID}" "2" - database_adlist_number "${adlistID}" done="true" - # Check if $patternbuffer is a non-zero length file - elif [[ -s "${patternBuffer}" ]]; then + # 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 "${patternBuffer}" "${saveLocation}" + gravity_ParseFileIntoDomains "${listCurlBuffer}" "${saveLocation}" + # Remove curl buffer file after its use + rm "${listCurlBuffer}" # Add domains to database table file - parseList "${adlistID}" "${saveLocation}" "${target}" + pihole-FTL gravity parseList "${saveLocation}" "${gravityTEMPfile}" "${adlistID}" # Compare lists, are they identical? compareLists "${adlistID}" "${saveLocation}" - # Update gravity database table (status and updated timestamp are set in - # compareLists) - database_adlist_number "${adlistID}" done="true" else - # Fall back to previously cached list if $patternBuffer is empty + # Fall back to previously cached list if $listCurlBuffer is empty echo -e " ${INFO} Received empty file" fi fi @@ -703,15 +649,12 @@ gravity_DownloadBlocklistFromUrl() { if [[ -r "${saveLocation}" ]]; then echo -e " ${CROSS} List download failed: ${COL_LIGHT_GREEN}using previously cached list${COL_NC}" # Add domains to database table file - parseList "${adlistID}" "${saveLocation}" "${target}" - database_adlist_number "${adlistID}" + pihole-FTL gravity parseList "${saveLocation}" "${gravityTEMPfile}" "${adlistID}" database_adlist_status "${adlistID}" "3" else echo -e " ${CROSS} List download failed: ${COL_LIGHT_RED}no cached list available${COL_NC}" # Manually reset these two numbers because we do not call parseList here - num_source_lines=0 - num_invalid=0 - database_adlist_number "${adlistID}" + database_adlist_number "${adlistID}" 0 0 database_adlist_status "${adlistID}" "4" fi fi @@ -719,72 +662,33 @@ gravity_DownloadBlocklistFromUrl() { # Parse source files into domains format gravity_ParseFileIntoDomains() { - local source="${1}" destination="${2}" firstLine - - # Determine if we are parsing a consolidated list - #if [[ "${source}" == "${piholeDir}/${matterAndLight}" ]]; then - # 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) Remove carriage returns - # 2) Convert all characters to lowercase - # 3) Remove comments (text starting with "#", include possible spaces before the hash sign) - # 4) Remove lines containing "/" - # 5) Remove leading tabs, spaces, etc. - # 6) Delete lines not matching domain names - < "${source}" tr -d '\r' | \ - tr '[:upper:]' '[:lower:]' | \ - sed 's/\s*#.*//g' | \ - sed -r '/(\/).*$/d' | \ - sed -r 's/^.*\s+//g' | \ - sed -r '/([^\.]+\.)+[^\.]{2,}/!d' > "${destination}" - chmod 644 "${destination}" - return 0 - #fi - - # Individual file parsing: Keep comments, while parsing domains from each line - # We keep comments to respect the list maintainer's licensing - read -r firstLine < "${source}" - - # Determine how to parse individual source file formats - if [[ "${firstLine,,}" =~ (adblock|ublock|^!) ]]; then - # Compare $firstLine against lower case words found in Adblock lists - echo -e " ${CROSS} Format: Adblock (list type not supported)" - elif grep -q "^address=/" "${source}" &> /dev/null; then - # Parse Dnsmasq format lists - echo -e " ${CROSS} Format: Dnsmasq (list type not supported)" - elif grep -q -E "^https?://" "${source}" &> /dev/null; then - # Parse URL list if source file contains "http://" or "https://" - # Scanning for "^IPv4$" is too slow with large (1M) lists on low-end hardware - echo -ne " ${INFO} Format: URL" - - awk ' - # Remove URL scheme, optional "username:password@", and ":?/;" - # The scheme must be matched carefully to avoid blocking the wrong URL - # in cases like: - # http://www.evil.com?http://www.good.com - # See RFC 3986 section 3.1 for details. - /[:?\/;]/ { gsub(/(^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/(.*:.*@)?|[:?\/;].*)/, "", $0) } - # Skip lines which are only IPv4 addresses - /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ { next } - # Print if nonempty - length { print } - ' "${source}" 2> /dev/null > "${destination}" - chmod 644 "${destination}" - - echo -e "${OVER} ${TICK} Format: URL" - else - # Default: Keep hosts/domains file in same format as it was downloaded - output=$( { mv "${source}" "${destination}"; } 2>&1 ) - chmod 644 "${destination}" - - if [[ ! -e "${destination}" ]]; then - echo -e "\\n ${CROSS} Unable to move tmp file to ${piholeDir} - ${output}" - gravity_Cleanup "error" - fi - fi + 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 ("##", "#!#", "#@#", "#?#") 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,1}\#/d' \ + -e 's/\s*#.*//g' \ + -e 's/^.*\s+//g' \ + -e '/^$/d' "${destination}" + + chmod 644 "${destination}" } # Report number of entries in a table @@ -792,12 +696,12 @@ gravity_Table_Count() { local table="${1}" local str="${2}" local num - num="$(pihole-FTL sqlite3 "${gravityDBfile}" "SELECT COUNT(*) FROM ${table};")" - if [[ "${table}" == "vw_gravity" ]]; then + num="$(pihole-FTL sqlite3 -ni "${gravityDBfile}" "SELECT COUNT(*) FROM ${table};")" + if [[ "${table}" == "gravity" ]]; then local unique - unique="$(pihole-FTL sqlite3 "${gravityDBfile}" "SELECT COUNT(DISTINCT domain) FROM ${table};")" + unique="$(pihole-FTL sqlite3 -ni "${gravityDBfile}" "SELECT COUNT(*) FROM (SELECT DISTINCT domain FROM ${table});")" echo -e " ${INFO} Number of ${str}: ${num} (${COL_BOLD}${unique} unique domains${COL_NC})" - pihole-FTL sqlite3 "${gravityDBfile}" "INSERT OR REPLACE INTO info (property,value) VALUES ('gravity_count',${unique});" + pihole-FTL sqlite3 -ni "${gravityDBfile}" "INSERT OR REPLACE INTO info (property,value) VALUES ('gravity_count',${unique});" else echo -e " ${INFO} Number of ${str}: ${num}" fi @@ -805,7 +709,9 @@ gravity_Table_Count() { # Output count of blacklisted domains and regex filters gravity_ShowCount() { - gravity_Table_Count "vw_gravity" "gravity domains" "" + # 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. + gravity_Table_Count "gravity" "gravity domains" "" gravity_Table_Count "vw_blacklist" "exact blacklisted domains" gravity_Table_Count "vw_regex_blacklist" "regex blacklist filters" gravity_Table_Count "vw_whitelist" "exact whitelisted domains" @@ -839,7 +745,10 @@ gravity_Cleanup() { # Delete tmp content generated by Gravity rm ${piholeDir}/pihole.*.txt 2> /dev/null rm ${piholeDir}/*.tmp 2> /dev/null - rm /tmp/*.phgpb 2> /dev/null + # listCurlBuffer location + rm "${GRAVITY_TMPDIR}"/*.phgpb 2> /dev/null + # 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 @@ -870,15 +779,19 @@ gravity_Cleanup() { database_recovery() { local result - local str="Checking integrity of existing gravity database" + local str="Checking integrity of existing gravity database (this can take a while)" local option="${1}" echo -ne " ${INFO} ${str}..." - if result="$(pihole-FTL sqlite3 "${gravityDBfile}" "PRAGMA integrity_check" 2>&1)"; then + result="$(pihole-FTL sqlite3 -ni "${gravityDBfile}" "PRAGMA integrity_check" 2>&1)" + + if [[ ${result} = "ok" ]]; then echo -e "${OVER} ${TICK} ${str} - no errors found" - str="Checking foreign keys of existing gravity database" + str="Checking foreign keys of existing gravity database (this can take a while)" echo -ne " ${INFO} ${str}..." - if result="$(pihole-FTL sqlite3 "${gravityDBfile}" "PRAGMA foreign_key_check" 2>&1)"; then + unset result + result="$(pihole-FTL sqlite3 -ni "${gravityDBfile}" "PRAGMA foreign_key_check" 2>&1)" + if [[ -z ${result} ]]; then echo -e "${OVER} ${TICK} ${str} - no errors found" if [[ "${option}" != "force" ]]; then return @@ -896,7 +809,7 @@ database_recovery() { echo -ne " ${INFO} ${str}..." # We have to remove any possibly existing recovery database or this will fail rm -f "${gravityDBfile}.recovered" > /dev/null 2>&1 - if result="$(pihole-FTL sqlite3 "${gravityDBfile}" ".recover" | pihole-FTL sqlite3 "${gravityDBfile}.recovered" 2>&1)"; then + if result="$(pihole-FTL sqlite3 -ni "${gravityDBfile}" ".recover" | pihole-FTL sqlite3 -ni "${gravityDBfile}.recovered" 2>&1)"; then echo -e "${OVER} ${TICK} ${str} - success" mv "${gravityDBfile}" "${gravityDBfile}.old" mv "${gravityDBfile}.recovered" "${gravityDBfile}" @@ -998,7 +911,10 @@ if ! gravity_CheckDNSResolutionAvailable; then exit 1 fi -gravity_DownloadBlocklists +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 +fi # Create local.list gravity_generateLocalList diff --git a/manpages/pihole.8 b/manpages/pihole.8 index aaaa8d7eb3..fec1fa5ecf 100644 --- a/manpages/pihole.8 +++ b/manpages/pihole.8 @@ -11,8 +11,6 @@ Pi-hole : A black-hole for internet advertisements .br \fBpihole -a\fR (\fB-c|-f|-k\fR) .br -\fBpihole -a -e\fR email -.br \fBpihole -a -i\fR interface .br \fBpihole -a -l\fR privacylevel @@ -25,7 +23,7 @@ Pi-hole : A black-hole for internet advertisements .br pihole -r .br -pihole -t +\fBpihole\fR \fB-t\fR [arg] .br pihole -g\fR .br @@ -115,11 +113,15 @@ Available commands and options: Reconfigure or Repair Pi-hole subsystems .br -\fB-t, tail\fR +\fB-t, tail\fR [arg] .br View the live output of the Pi-hole log .br + [arg] Optional argument to filter the log for + (regular expressions are supported) +.br + \fB-a, admin\fR [options] .br @@ -132,9 +134,6 @@ Available commands and options: -f, fahrenheit Set Fahrenheit as preferred temperature unit .br -k, kelvin Set Kelvin as preferred temperature unit -.br - -e, email Set an administrative contact address for the - Block Page .br -i, interface Specify dnsmasq's interface listening behavior .br @@ -187,12 +186,12 @@ Available commands and options: (Logging options): .br - on Enable the Pi-hole log at /var/log/pihole.log + on Enable the Pi-hole log at /var/log/pihole/pihole.log .br off Disable and flush the Pi-hole log at - /var/log/pihole.log + /var/log/pihole/pihole.log .br - off noflush Disable the Pi-hole log at /var/log/pihole.log + off noflush Disable the Pi-hole log at /var/log/pihole/pihole.log .br \fB-up, updatePihole\fR [--check-only] @@ -213,7 +212,7 @@ Available commands and options: .br -p, --pihole Only retrieve info regarding Pi-hole repository .br - -a, --admin Only retrieve info regarding AdminLTE + -a, --admin Only retrieve info regarding web repository .br -f, --ftl Only retrieve info regarding FTL repository @@ -340,7 +339,7 @@ Displaying version information \fBpihole -v -a -c\fR .br - Display the current version of AdminLTE + Display the current version of web .br Temporarily disabling Pi-hole diff --git a/pihole b/pihole index f51fd9561b..1d9ad82c3e 100755 --- a/pihole +++ b/pihole @@ -16,7 +16,6 @@ readonly PI_HOLE_SCRIPT_DIR="/opt/pihole" # error due to modifying a readonly variable. setupVars="/etc/pihole/setupVars.conf" PI_HOLE_BIN_DIR="/usr/local/bin" -readonly FTL_PID_FILE="/run/pihole-FTL.pid" readonly colfile="${PI_HOLE_SCRIPT_DIR}/COL_TABLE" source "${colfile}" @@ -24,6 +23,14 @@ source "${colfile}" utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" source "${utilsfile}" +versionsfile="/etc/pihole/versions" +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 + source "${versionsfile}" +fi + webpageFunc() { source "${PI_HOLE_SCRIPT_DIR}/webpage.sh" main "$@" @@ -36,19 +43,20 @@ listFunc() { } debugFunc() { - local automated - local web - - # Pull off the `debug` leaving passed call augmentation flags in $1 - shift - if [[ "$@" == *"-a"* ]]; then - automated="true" - fi - if [[ "$@" == *"-w"* ]]; then - web="true" - fi - - AUTOMATED=${automated:-} WEBCALL=${web:-} "${PI_HOLE_SCRIPT_DIR}"/piholeDebug.sh + local automated + local web + local check_database_integrity + # Pull off the `debug` leaving passed call augmentation flags in $1 + shift + + for value in "$@"; do + [[ "$value" == *"-a"* ]] && automated="true" + [[ "$value" == *"-w"* ]] && web="true" + [[ "$value" == *"-c"* ]] && check_database_integrity="true" + [[ "$value" == *"--check_database"* ]] && check_database_integrity="true" + done + + AUTOMATED=${automated:-} WEBCALL=${web:-} CHECK_DATABASE=${check_database_integrity:-} "${PI_HOLE_SCRIPT_DIR}"/piholeDebug.sh exit 0 } @@ -63,14 +71,22 @@ arpFunc() { } updatePiholeFunc() { - shift - "${PI_HOLE_SCRIPT_DIR}"/update.sh "$@" - exit 0 + if [ -n "${DOCKER_VERSION}" ]; then + unsupportedFunc + else + shift + "${PI_HOLE_SCRIPT_DIR}"/update.sh "$@" + exit 0 + fi } reconfigurePiholeFunc() { - /etc/.pihole/automated\ install/basic-install.sh --reconfigure - exit 0; + if [ -n "${DOCKER_VERSION}" ]; then + unsupportedFunc + else + /etc/.pihole/automated\ install/basic-install.sh --reconfigure + exit 0; + fi } updateGravityFunc() { @@ -91,8 +107,12 @@ chronometerFunc() { uninstallFunc() { - "${PI_HOLE_SCRIPT_DIR}"/uninstall.sh - exit 0 + if [ -n "${DOCKER_VERSION}" ]; then + unsupportedFunc + else + "${PI_HOLE_SCRIPT_DIR}"/uninstall.sh + exit 0 + fi } versionFunc() { @@ -100,34 +120,21 @@ versionFunc() { exec "${PI_HOLE_SCRIPT_DIR}"/version.sh "$@" } -# Get PID of main pihole-FTL process -getFTLPID() { - local pid - - if [ -s "${FTL_PID_FILE}" ]; then - # -s: FILE exists and has a size greater than zero - pid="$(<"$FTL_PID_FILE")" - # Exploit prevention: unset the variable if there is malicious content - # Verify that the value read from the file is numeric - [[ "$pid" =~ [^[:digit:]] ]] && unset pid - fi - - # If FTL is not running, or the PID file contains malicious stuff, substitute - # negative PID to signal this to the caller - echo "${pid:=-1}" -} - restartDNS() { - local svcOption svc str output status pid icon + local svcOption svc str output status pid icon FTL_PID_FILE svcOption="${1:-restart}" + # get the current path to the pihole-FTL.pid + FTL_PID_FILE="$(getFTLPIDFile)" + # Determine if we should reload or restart if [[ "${svcOption}" =~ "reload-lists" ]]; then # Reloading of the lists has been requested # Note 1: This will NOT re-read any *.conf files # Note 2: We cannot use killall here as it does # not know about real-time signals - pid="$(getFTLPID)" + + pid="$(getFTLPID ${FTL_PID_FILE})" if [[ "$pid" -eq "-1" ]]; then svc="true" str="FTL is not running" @@ -140,7 +147,7 @@ restartDNS() { elif [[ "${svcOption}" =~ "reload" ]]; then # Reloading of the DNS cache has been requested # Note: This will NOT re-read any *.conf files - pid="$(getFTLPID)" + pid="$(getFTLPID ${FTL_PID_FILE})" if [[ "$pid" -eq "-1" ]]; then svc="true" str="FTL is not running" @@ -254,9 +261,9 @@ Example: 'pihole logging on' Specify whether the Pi-hole log should be used Options: - on Enable the Pi-hole log at /var/log/pihole.log - off Disable and flush the Pi-hole log at /var/log/pihole.log - off noflush Disable the Pi-hole log at /var/log/pihole.log" + on Enable the Pi-hole log at /var/log/pihole/pihole.log + off Disable and flush the Pi-hole log at /var/log/pihole/pihole.log + off noflush Disable the Pi-hole log at /var/log/pihole/pihole.log" exit 0 elif [[ "${1}" == "off" ]]; then # Disable logging @@ -315,33 +322,36 @@ analyze_ports() { } statusFunc() { - # Determine if there is pihole-FTL service is listening - local pid port ftl_api_port + # Determine if there is pihole-FTL service is listening + local pid port ftl_api_port ftl_pid_file - pid="$(getFTLPID)" - ftl_api_port="$(getFTLAPIPort)" - if [[ "$pid" -eq "-1" ]]; then - case "${1}" in - "web") echo "-1";; - *) echo -e " ${CROSS} DNS service is NOT running";; - esac - return 0 - else - #get the DNS port pihole-FTL is listening on by using FTL's telnet API - port="$(echo ">dns-port >quit" | nc 127.0.0.1 "$ftl_api_port")" - if [[ "${port}" == "0" ]]; then - case "${1}" in - "web") echo "-1";; - *) echo -e " ${CROSS} DNS service is NOT listening";; - esac - return 0 + ftl_pid_file="$(getFTLPIDFile)" + + pid="$(getFTLPID ${ftl_pid_file})" + + ftl_api_port="$(getFTLAPIPort)" + if [[ "$pid" -eq "-1" ]]; then + case "${1}" in + "web") echo "-1";; + *) echo -e " ${CROSS} DNS service is NOT running";; + esac + return 0 else - if [[ "${1}" != "web" ]]; then - echo -e " ${TICK} FTL is listening on port ${port}" - analyze_ports "${port}" - fi + #get the DNS port pihole-FTL is listening on by using FTL's telnet API + port="$(echo ">dns-port >quit" | nc 127.0.0.1 "$ftl_api_port")" + if [[ "${port}" == "0" ]]; then + case "${1}" in + "web") echo "-1";; + *) echo -e " ${CROSS} DNS service is NOT listening";; + esac + return 0 + else + if [[ "${1}" != "web" ]]; then + echo -e " ${TICK} FTL is listening on port ${port}" + analyze_ports "${port}" + fi + fi fi - fi # Determine if Pi-hole's blocking is enabled if grep -q "BLOCKING_ENABLED=false" /etc/pihole/setupVars.conf; then @@ -382,7 +392,7 @@ tailFunc() { # Color blocklist/blacklist/wildcard entries as red # Color A/AAAA/DHCP strings as white # Color everything else as gray - tail -f /var/log/pihole.log | grep --line-buffered "${1}" | sed -E \ + tail -f /var/log/pihole/pihole.log | grep --line-buffered "${1}" | sed -E \ -e "s,($(date +'%b %d ')| dnsmasq\[[0-9]*\]),,g" \ -e "s,(.*(blacklisted |gravity blocked ).*),${COL_RED}&${COL_NC}," \ -e "s,.*(query\\[A|DHCP).*,${COL_NC}&${COL_NC}," \ @@ -439,6 +449,11 @@ updateCheckFunc() { exit 0 } +unsupportedFunc(){ + echo "Function not supported in Docker images" + exit 0 +} + helpFunc() { echo "Usage: pihole [options] Example: 'pihole -w -h' @@ -455,6 +470,7 @@ Whitelist/Blacklist Options: 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 @@ -503,7 +519,7 @@ case "${1}" in "-c" | "chronometer" ) chronometerFunc "$@";; "-q" | "query" ) queryFunc "$@";; "status" ) statusFunc "$2";; - "-t" | "tail" ) tailFunc "$2";; + "tricorder" ) tricorderFunc;; # we need to add all arguments that require sudo power to not trigger the * argument @@ -527,6 +543,7 @@ case "${1}" in "checkout" ) ;; "updatechecker" ) ;; "arpflush" ) ;; + "-t" | "tail" ) ;; * ) helpFunc;; esac @@ -561,6 +578,7 @@ case "${1}" in "restartdns" ) restartDNS "$2";; "-a" | "admin" ) webpageFunc "$@";; "checkout" ) piholeCheckoutFunc "$@";; - "updatechecker" ) updateCheckFunc "$@";; + "updatechecker" ) shift; updateCheckFunc "$@";; "arpflush" ) arpFunc "$@";; + "-t" | "tail" ) tailFunc "$2";; esac diff --git a/test/_centos_8.Dockerfile b/test/_centos_8.Dockerfile index 86e5a7787d..a07a67e9ee 100644 --- a/test/_centos_8.Dockerfile +++ b/test/_centos_8.Dockerfile @@ -1,5 +1,5 @@ FROM quay.io/centos/centos:stream8 -RUN yum install -y git +RUN yum install -y git initscripts ENV GITDIR /etc/.pihole ENV SCRIPTDIR /opt/pihole @@ -12,7 +12,7 @@ ENV PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR RUN true && \ chmod +x $SCRIPTDIR/* -ENV PH_TEST true +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/_centos_9.Dockerfile b/test/_centos_9.Dockerfile new file mode 100644 index 0000000000..6ccd18b649 --- /dev/null +++ b/test/_centos_9.Dockerfile @@ -0,0 +1,18 @@ +FROM quay.io/centos/centos:stream9 +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 $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 +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_10.Dockerfile b/test/_debian_10.Dockerfile index 54800d3c47..3b177cc89a 100644 --- a/test/_debian_10.Dockerfile +++ b/test/_debian_10.Dockerfile @@ -11,7 +11,7 @@ ENV PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR RUN true && \ chmod +x $SCRIPTDIR/* -ENV PH_TEST true +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 39be027eac..58c67e0f4d 100644 --- a/test/_debian_11.Dockerfile +++ b/test/_debian_11.Dockerfile @@ -11,7 +11,7 @@ ENV PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR RUN true && \ chmod +x $SCRIPTDIR/* -ENV PH_TEST true +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_34.Dockerfile b/test/_debian_12.Dockerfile similarity index 90% rename from test/_fedora_34.Dockerfile rename to test/_debian_12.Dockerfile index fbbaacd6da..a762fee03b 100644 --- a/test/_fedora_34.Dockerfile +++ b/test/_debian_12.Dockerfile @@ -1,5 +1,4 @@ -FROM fedora:34 -RUN dnf install -y git +FROM buildpack-deps:bookworm-scm ENV GITDIR /etc/.pihole ENV SCRIPTDIR /opt/pihole @@ -12,7 +11,7 @@ ENV PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR RUN true && \ chmod +x $SCRIPTDIR/* -ENV PH_TEST true +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/_centos_7.Dockerfile b/test/_fedora_38.Dockerfile similarity index 87% rename from test/_centos_7.Dockerfile rename to test/_fedora_38.Dockerfile index 355f4fdb41..76f697717f 100644 --- a/test/_centos_7.Dockerfile +++ b/test/_fedora_38.Dockerfile @@ -1,5 +1,5 @@ -FROM centos:7 -RUN yum install -y git +FROM fedora:38 +RUN dnf install -y git initscripts ENV GITDIR /etc/.pihole ENV SCRIPTDIR /opt/pihole @@ -12,7 +12,7 @@ ENV PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR RUN true && \ chmod +x $SCRIPTDIR/* -ENV PH_TEST true +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_9.Dockerfile b/test/_fedora_39.Dockerfile similarity index 87% rename from test/_debian_9.Dockerfile rename to test/_fedora_39.Dockerfile index c590a6576c..1727a3aa22 100644 --- a/test/_debian_9.Dockerfile +++ b/test/_fedora_39.Dockerfile @@ -1,4 +1,5 @@ -FROM buildpack-deps:stretch-scm +FROM fedora:39 +RUN dnf install -y git initscripts ENV GITDIR /etc/.pihole ENV SCRIPTDIR /opt/pihole @@ -11,7 +12,7 @@ ENV PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR RUN true && \ chmod +x $SCRIPTDIR/* -ENV PH_TEST true +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_16.Dockerfile b/test/_ubuntu_16.Dockerfile deleted file mode 100644 index e572efd149..0000000000 --- a/test/_ubuntu_16.Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM buildpack-deps:xenial-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 $SCRIPTDIR/ -ENV PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR - -RUN true && \ - chmod +x $SCRIPTDIR/* - -ENV PH_TEST 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 && \ \ No newline at end of file diff --git a/test/_ubuntu_18.Dockerfile b/test/_ubuntu_18.Dockerfile deleted file mode 100644 index 592c5c3fba..0000000000 --- a/test/_ubuntu_18.Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM buildpack-deps:bionic-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 $SCRIPTDIR/ -ENV PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR - -RUN true && \ - chmod +x $SCRIPTDIR/* - -ENV PH_TEST 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_20.Dockerfile b/test/_ubuntu_20.Dockerfile index 80e2e0071d..c63f883aad 100644 --- a/test/_ubuntu_20.Dockerfile +++ b/test/_ubuntu_20.Dockerfile @@ -12,7 +12,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN true && \ chmod +x $SCRIPTDIR/* -ENV PH_TEST true +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_21.Dockerfile b/test/_ubuntu_22.Dockerfile similarity index 91% rename from test/_ubuntu_21.Dockerfile rename to test/_ubuntu_22.Dockerfile index 6d4d7fbc3a..d44518b4e1 100644 --- a/test/_ubuntu_21.Dockerfile +++ b/test/_ubuntu_22.Dockerfile @@ -1,4 +1,4 @@ -FROM buildpack-deps:impish-scm +FROM buildpack-deps:jammy-scm ENV GITDIR /etc/.pihole ENV SCRIPTDIR /opt/pihole @@ -12,7 +12,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN true && \ chmod +x $SCRIPTDIR/* -ENV PH_TEST true +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_33.Dockerfile b/test/_ubuntu_23.Dockerfile similarity index 85% rename from test/_fedora_33.Dockerfile rename to test/_ubuntu_23.Dockerfile index 5cdd66eeca..f9b3910b85 100644 --- a/test/_fedora_33.Dockerfile +++ b/test/_ubuntu_23.Dockerfile @@ -1,5 +1,4 @@ -FROM fedora:33 -RUN dnf install -y git +FROM buildpack-deps:lunar-scm ENV GITDIR /etc/.pihole ENV SCRIPTDIR /opt/pihole @@ -8,11 +7,12 @@ RUN mkdir -p $GITDIR $SCRIPTDIR /etc/pihole ADD . $GITDIR RUN cp $GITDIR/advanced/Scripts/*.sh $GITDIR/gravity.sh $GITDIR/pihole $GITDIR/automated\ install/*.sh $SCRIPTDIR/ ENV PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR +ENV DEBIAN_FRONTEND=noninteractive RUN true && \ chmod +x $SCRIPTDIR/* -ENV PH_TEST true +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 fb7e1eea33..e395ec279f 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -6,12 +6,12 @@ SETUPVARS = { - 'PIHOLE_INTERFACE': 'eth99', - 'PIHOLE_DNS_1': '4.2.2.1', - 'PIHOLE_DNS_2': '4.2.2.2' + "PIHOLE_INTERFACE": "eth99", + "PIHOLE_DNS_1": "4.2.2.1", + "PIHOLE_DNS_2": "4.2.2.2", } -IMAGE = 'pytest_pihole:test_container' +IMAGE = "pytest_pihole:test_container" tick_box = "[\x1b[1;32m\u2713\x1b[0m]" cross_box = "[\x1b[1;31m\u2717\x1b[0m]" @@ -38,132 +38,187 @@ def run_bash(self, command, *args, **kwargs): @pytest.fixture def host(): # run a container - docker_id = subprocess.check_output( - ['docker', 'run', '-t', '-d', '--cap-add=ALL', IMAGE]).decode().strip() + docker_id = ( + subprocess.check_output(["docker", "run", "-t", "-d", "--cap-add=ALL", IMAGE]) + .decode() + .strip() + ) # return a testinfra connection to the container docker_host = testinfra.get_host("docker://" + docker_id) yield docker_host # at the end of the test suite, destroy the container - subprocess.check_call(['docker', 'rm', '-f', docker_id]) + subprocess.check_call(["docker", "rm", "-f", docker_id]) # Helper functions def mock_command(script, args, container): - ''' + """ Allows for setup of commands we don't really want to have to run for real in unit tests - ''' - full_script_path = '/usr/local/bin/{}'.format(script) - mock_script = dedent(r'''\ + """ + full_script_path = "/usr/local/bin/{}".format(script) + 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''') - container.run(''' + mock_script += dedent( + """ + esac""" + ) + container.run( + """ cat < {script}\n{content}\nEOF chmod +x {script} - rm -f /var/log/{scriptlog}'''.format(script=full_script_path, - content=mock_script, - scriptlog=script)) + rm -f /var/log/{scriptlog}""".format( + script=full_script_path, content=mock_script, scriptlog=script + ) + ) def mock_command_passthrough(script, args, container): - ''' + """ Per other mock_command* functions, allows intercepting of commands we don't want to run for real in unit tests, however also allows only specific arguments to be mocked. Anything not defined will be passed through to the actual command. Example use-case: mocking `git pull` but still allowing `git clone` to work as intended - ''' - orig_script_path = container.check_output('command -v {}'.format(script)) - full_script_path = '/usr/local/bin/{}'.format(script) - mock_script = dedent(r'''\ + """ + orig_script_path = container.check_output("command -v {}".format(script)) + full_script_path = "/usr/local/bin/{}".format(script) + 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''') - container.run(''' + ;;""".format( + orig_script_path=orig_script_path + ) + ) + mock_script += dedent( + """ + esac""" + ) + container.run( + """ cat < {script}\n{content}\nEOF chmod +x {script} - rm -f /var/log/{scriptlog}'''.format(script=full_script_path, - content=mock_script, - scriptlog=script)) + rm -f /var/log/{scriptlog}""".format( + script=full_script_path, content=mock_script, scriptlog=script + ) + ) def mock_command_run(script, args, container): - ''' + """ Allows for setup of commands we don't really want to have to run for real in unit tests - ''' - full_script_path = '/usr/local/bin/{}'.format(script) - mock_script = dedent(r'''\ + """ + full_script_path = "/usr/local/bin/{}".format(script) + 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''') - container.run(''' + mock_script += dedent( + """ + esac""" + ) + container.run( + """ cat < {script}\n{content}\nEOF chmod +x {script} - rm -f /var/log/{scriptlog}'''.format(script=full_script_path, - content=mock_script, - scriptlog=script)) + rm -f /var/log/{scriptlog}""".format( + script=full_script_path, content=mock_script, scriptlog=script + ) + ) def mock_command_2(script, args, container): - ''' + """ Allows for setup of commands we don't really want to have to run for real in unit tests - ''' - full_script_path = '/usr/local/bin/{}'.format(script) - mock_script = dedent(r'''\ + """ + full_script_path = "/usr/local/bin/{}".format(script) + 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''') - container.run(''' + mock_script += dedent( + """ + esac""" + ) + container.run( + """ cat < {script}\n{content}\nEOF chmod +x {script} - rm -f /var/log/{scriptlog}'''.format(script=full_script_path, - content=mock_script, - scriptlog=script)) + rm -f /var/log/{scriptlog}""".format( + script=full_script_path, content=mock_script, scriptlog=script + ) + ) def run_script(Pihole, script): diff --git a/test/requirements.txt b/test/requirements.txt index d65ee6a5cf..d4415e2b32 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -1,6 +1,6 @@ -docker-compose -pytest -pytest-xdist -pytest-cov -pytest-testinfra -tox +pyyaml == 6.0.1 +pytest == 8.0.2 +pytest-xdist == 3.5.0 +pytest-testinfra == 10.1.0 +tox == 4.14.1 + diff --git a/test/setup.py b/test/setup.py index 0e393bc131..cdde20d393 100644 --- a/test/setup.py +++ b/test/setup.py @@ -1,6 +1,7 @@ from setuptools import setup setup( - setup_requires=['pytest-runner'], - tests_require=['pytest'], + py_modules=[], + setup_requires=["pytest-runner"], + tests_require=["pytest"], ) diff --git a/test/test_any_automated_install.py b/test/test_any_automated_install.py index 41a939e016..c1b9166446 100644 --- a/test/test_any_automated_install.py +++ b/test/test_any_automated_install.py @@ -10,39 +10,42 @@ mock_command_run, mock_command_2, mock_command_passthrough, - run_script + run_script, ) def test_supported_package_manager(host): - ''' + """ confirm installer exits when no supported package manager found - ''' + """ # 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 /usr/bin/apt-get") + host.run("rm -rf /usr/bin/rpm") + package_manager_detect = host.run( + """ source /opt/pihole/basic-install.sh package_manager_detect - ''') - expected_stdout = cross_box + ' No supported package manager found' + """ + ) + expected_stdout = cross_box + " No supported package manager found" assert expected_stdout in package_manager_detect.stdout # assert package_manager_detect.rc == 1 def test_setupVars_are_sourced_to_global_scope(host): - ''' + """ currently update_dialogs sources setupVars with a dot, then various other functions use the variables. This confirms the sourced variables are in scope between functions - ''' - setup_var_file = 'cat < /etc/pihole/setupVars.conf\n' + """ + setup_var_file = "cat < /etc/pihole/setupVars.conf\n" for k, v in SETUPVARS.items(): setup_var_file += "{}={}\n".format(k, v) setup_var_file += "EOF\n" host.run(setup_var_file) - script = dedent('''\ + script = dedent( + """\ set -e printSetupVars() { # Currently debug test function only @@ -56,7 +59,8 @@ def test_setupVars_are_sourced_to_global_scope(host): } update_dialogs printSetupVars - ''') + """ + ) output = run_script(host, script).stdout @@ -65,27 +69,32 @@ def test_setupVars_are_sourced_to_global_scope(host): def test_setupVars_saved_to_file(host): - ''' - confirm saved settings are written to a file for future updates to re-use - ''' + """ + confirm saved settings are written to a file for future updates to reuse + """ # dedent works better with this and padding matching script below - set_setup_vars = '\n' + set_setup_vars = "\n" for k, v in SETUPVARS.items(): set_setup_vars += " {}={}\n".format(k, v) host.run(set_setup_vars) - script = dedent('''\ + script = dedent( + """\ set -e echo start TERM=xterm source /opt/pihole/basic-install.sh + source /opt/pihole/utils.sh {} mkdir -p /etc/dnsmasq.d version_check_dnsmasq echo "" > /etc/pihole/pihole-FTL.conf finalExports cat /etc/pihole/setupVars.conf - '''.format(set_setup_vars)) + """.format( + set_setup_vars + ) + ) output = run_script(host, script).stdout @@ -94,116 +103,87 @@ def test_setupVars_saved_to_file(host): 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' + """ + ) + expected_stdout = info_box + " SELinux not detected" assert expected_stdout in check_selinux.stdout assert check_selinux.rc == 0 def test_installPiholeWeb_fresh_install_no_errors(host): - ''' + """ confirms all web page assets from Core repo are installed on a fresh build - ''' - installWeb = host.run(''' + """ + installWeb = host.run( + """ umask 0027 source /opt/pihole/basic-install.sh installPiholeWeb - ''') - expected_stdout = info_box + ' Installing blocking page...' - assert expected_stdout in installWeb.stdout - expected_stdout = tick_box + (' Creating directory for blocking page, ' - 'and copying files') - assert expected_stdout in installWeb.stdout - expected_stdout = info_box + ' Backing up index.lighttpd.html' - assert expected_stdout in installWeb.stdout - expected_stdout = ('No default index.lighttpd.html file found... ' - 'not backing up') - assert expected_stdout in installWeb.stdout - expected_stdout = tick_box + ' Installing sudoer file' + """ + ) + expected_stdout = tick_box + " Installing sudoer file" assert expected_stdout in installWeb.stdout - web_directory = host.run('ls -r /var/www/html/pihole').stdout - assert 'index.php' in web_directory - assert 'blockingpage.css' in web_directory def get_directories_recursive(host, directory): if directory is None: return directory - ls = host.run('ls -d {}'.format(directory + '/*/')) - directories = list(filter(bool, ls.stdout.splitlines())) - dirs = directories - for dirval in directories: - dir_rec = get_directories_recursive(host, dirval) - if isinstance(dir_rec, str): - dirs.extend([dir_rec]) - else: - dirs.extend(dir_rec) + # returns all non-hidden subdirs of 'directory' + dirs_raw = host.run("find {} -type d -not -path '*/.*'".format(directory)) + dirs = list(filter(bool, dirs_raw.stdout.splitlines())) return dirs def test_installPihole_fresh_install_readableFiles(host): - ''' - confirms all neccessary files are readable by pihole user - ''' - # Whiptail dialog returns Cancel for user prompt - mock_command('whiptail', {'*': ('', '0')}, host) + """ + confirms all necessary files are readable by pihole user + """ + # dialog returns Cancel for user prompt + mock_command("dialog", {"*": ("", "0")}, host) # mock git pull - mock_command_passthrough('git', {'pull': ('', '0')}, host) + mock_command_passthrough("git", {"pull": ("", "0")}, host) # mock systemctl to not start lighttpd and FTL mock_command_2( - 'systemctl', + "systemctl", { - 'enable lighttpd': ( - '', - '0' - ), - 'restart lighttpd': ( - '', - '0' - ), - 'start lighttpd': ( - '', - '0' - ), - 'enable pihole-FTL': ( - '', - '0' - ), - 'restart pihole-FTL': ( - '', - '0' - ), - 'start pihole-FTL': ( - '', - '0' - ), - '*': ( - 'echo "systemctl call with $@"', - '0' - ), + "enable lighttpd": ("", "0"), + "restart lighttpd": ("", "0"), + "start lighttpd": ("", "0"), + "enable pihole-FTL": ("", "0"), + "restart pihole-FTL": ("", "0"), + "start pihole-FTL": ("", "0"), + "*": ('echo "systemctl call with $@"', "0"), }, - host + 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 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") # create configuration file - setup_var_file = 'cat < /etc/pihole/setupVars.conf\n' + setup_var_file = "cat < /etc/pihole/setupVars.conf\n" for k, v in SETUPVARS.items(): setup_var_file += "{}={}\n".format(k, v) setup_var_file += "INSTALL_WEB_SERVER=true\n" setup_var_file += "INSTALL_WEB_INTERFACE=true\n" setup_var_file += "EOF\n" host.run(setup_var_file) - install = host.run(''' + # Install FTL's development branch to get the latest features + host.run( + """ + echo "development" > /etc/pihole/ftlbranch + """ + ) + install = host.run( + """ export TERM=xterm export DEBIAN_FRONTEND=noninteractive umask 0027 @@ -213,193 +193,184 @@ def test_installPihole_fresh_install_readableFiles(host): runUnattended=true useUpdateVars=true main - ''') + """ + ) assert 0 == install.rc maninstalled = True - if (info_box + ' man not installed') in install.stdout: + if (info_box + " man not installed") in install.stdout: + maninstalled = False + if (info_box + " man pages not installed") in install.stdout: maninstalled = False - piholeuser = 'pihole' + piholeuser = "pihole" exit_status_success = 0 test_cmd = 'su --shell /bin/bash --command "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) + check_etc = test_cmd.format("r", "/etc/pihole", piholeuser) actual_rc = host.run(check_etc).rc assert exit_status_success == actual_rc - check_etc = test_cmd.format('x', '/etc/pihole', piholeuser) + check_etc = test_cmd.format("x", "/etc/pihole", piholeuser) actual_rc = host.run(check_etc).rc assert exit_status_success == actual_rc # readable and writable dhcp.leases - check_leases = test_cmd.format('r', '/etc/pihole/dhcp.leases', piholeuser) + check_leases = test_cmd.format("r", "/etc/pihole/dhcp.leases", piholeuser) actual_rc = host.run(check_leases).rc assert exit_status_success == actual_rc - check_leases = test_cmd.format('w', '/etc/pihole/dhcp.leases', piholeuser) + check_leases = test_cmd.format("w", "/etc/pihole/dhcp.leases", piholeuser) actual_rc = host.run(check_leases).rc # readable dns-servers.conf assert exit_status_success == actual_rc - check_servers = test_cmd.format( - 'r', '/etc/pihole/dns-servers.conf', piholeuser) + check_servers = test_cmd.format("r", "/etc/pihole/dns-servers.conf", piholeuser) actual_rc = host.run(check_servers).rc assert exit_status_success == actual_rc - # readable GitHubVersions - check_version = test_cmd.format( - 'r', '/etc/pihole/GitHubVersions', piholeuser) - actual_rc = host.run(check_version).rc - assert exit_status_success == actual_rc # readable install.log - check_install = test_cmd.format( - 'r', '/etc/pihole/install.log', piholeuser) + check_install = test_cmd.format("r", "/etc/pihole/install.log", piholeuser) actual_rc = host.run(check_install).rc assert exit_status_success == actual_rc - # readable localbranches - check_localbranch = test_cmd.format( - 'r', '/etc/pihole/localbranches', piholeuser) - actual_rc = host.run(check_localbranch).rc - assert exit_status_success == actual_rc - # readable localversions - check_localversion = test_cmd.format( - 'r', '/etc/pihole/localversions', piholeuser) + # readable versions + 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) + 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) + check_macvendor = test_cmd.format("r", "/etc/pihole/macvendor.db", piholeuser) actual_rc = host.run(check_macvendor).rc assert exit_status_success == actual_rc # readable and writeable pihole-FTL.conf - check_FTLconf = test_cmd.format( - 'r', '/etc/pihole/pihole-FTL.conf', piholeuser) + check_FTLconf = test_cmd.format("r", "/etc/pihole/pihole-FTL.conf", piholeuser) actual_rc = host.run(check_FTLconf).rc assert exit_status_success == actual_rc - check_FTLconf = test_cmd.format( - 'w', '/etc/pihole/pihole-FTL.conf', piholeuser) + check_FTLconf = test_cmd.format("w", "/etc/pihole/pihole-FTL.conf", piholeuser) actual_rc = host.run(check_FTLconf).rc assert exit_status_success == actual_rc # readable setupVars.conf - check_setup = test_cmd.format( - 'r', '/etc/pihole/setupVars.conf', piholeuser) + check_setup = test_cmd.format("r", "/etc/pihole/setupVars.conf", piholeuser) actual_rc = host.run(check_setup).rc assert exit_status_success == actual_rc # check dnsmasq files # readable /etc/dnsmasq.conf - check_dnsmasqconf = test_cmd.format( - 'r', '/etc/dnsmasq.conf', piholeuser) + check_dnsmasqconf = test_cmd.format("r", "/etc/dnsmasq.conf", piholeuser) actual_rc = host.run(check_dnsmasqconf).rc assert exit_status_success == actual_rc # readable /etc/dnsmasq.d/01-pihole.conf - check_dnsmasqconf = test_cmd.format( - 'r', '/etc/dnsmasq.d', piholeuser) + check_dnsmasqconf = test_cmd.format("r", "/etc/dnsmasq.d", piholeuser) actual_rc = host.run(check_dnsmasqconf).rc assert exit_status_success == actual_rc - check_dnsmasqconf = test_cmd.format( - 'x', '/etc/dnsmasq.d', piholeuser) + check_dnsmasqconf = test_cmd.format("x", "/etc/dnsmasq.d", piholeuser) actual_rc = host.run(check_dnsmasqconf).rc assert exit_status_success == actual_rc check_dnsmasqconf = test_cmd.format( - 'r', '/etc/dnsmasq.d/01-pihole.conf', piholeuser) + "r", "/etc/dnsmasq.d/01-pihole.conf", piholeuser + ) actual_rc = host.run(check_dnsmasqconf).rc assert exit_status_success == actual_rc # check readable and executable /etc/init.d/pihole-FTL - check_init = test_cmd.format( - 'x', '/etc/init.d/pihole-FTL', piholeuser) + check_init = test_cmd.format("x", "/etc/init.d/pihole-FTL", piholeuser) actual_rc = host.run(check_init).rc assert exit_status_success == actual_rc - check_init = test_cmd.format( - 'r', '/etc/init.d/pihole-FTL', piholeuser) + check_init = test_cmd.format("r", "/etc/init.d/pihole-FTL", piholeuser) actual_rc = host.run(check_init).rc assert exit_status_success == actual_rc # check readable /etc/lighttpd/lighttpd.conf - check_lighttpd = test_cmd.format( - 'r', '/etc/lighttpd/lighttpd.conf', piholeuser) + check_lighttpd = test_cmd.format("r", "/etc/lighttpd/lighttpd.conf", piholeuser) actual_rc = host.run(check_lighttpd).rc assert exit_status_success == actual_rc + # check readable /etc/lighttpd/conf*/pihole-admin.conf + check_lighttpd = test_cmd.format("r", "/etc/lighttpd/conf.d", piholeuser) + if host.run(check_lighttpd).rc == exit_status_success: + check_lighttpd = test_cmd.format( + "r", "/etc/lighttpd/conf.d/pihole-admin.conf", piholeuser + ) + actual_rc = host.run(check_lighttpd).rc + assert exit_status_success == actual_rc + else: + check_lighttpd = test_cmd.format( + "r", "/etc/lighttpd/conf-available", piholeuser + ) + if host.run(check_lighttpd).rc == exit_status_success: + check_lighttpd = test_cmd.format( + "r", "/etc/lighttpd/conf-available/15-pihole-admin.conf", piholeuser + ) + actual_rc = host.run(check_lighttpd).rc + assert exit_status_success == actual_rc # check readable and executable manpages if maninstalled is True: - check_man = test_cmd.format( - 'x', '/usr/local/share/man', piholeuser) + check_man = test_cmd.format("x", "/usr/local/share/man", piholeuser) actual_rc = host.run(check_man).rc assert exit_status_success == actual_rc - check_man = test_cmd.format( - 'r', '/usr/local/share/man', piholeuser) + check_man = test_cmd.format("r", "/usr/local/share/man", piholeuser) actual_rc = host.run(check_man).rc assert exit_status_success == actual_rc - check_man = test_cmd.format( - 'x', '/usr/local/share/man/man8', piholeuser) + check_man = test_cmd.format("x", "/usr/local/share/man/man8", 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', piholeuser) + 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) + 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) + 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) + "r", "/usr/local/share/man/man8/pihole.8", 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-FTL.8', piholeuser) + "r", "/usr/local/share/man/man8/pihole-FTL.8", piholeuser + ) actual_rc = host.run(check_man).rc assert exit_status_success == actual_rc # check not readable sudoers file - check_sudo = test_cmd.format( - 'r', '/etc/sudoers.d/pihole', piholeuser) + check_sudo = test_cmd.format("r", "/etc/sudoers.d/pihole", piholeuser) actual_rc = host.run(check_sudo).rc assert exit_status_success != actual_rc # check not readable cron file - check_sudo = test_cmd.format( - 'x', '/etc/cron.d/', piholeuser) + check_sudo = test_cmd.format("x", "/etc/cron.d/", piholeuser) actual_rc = host.run(check_sudo).rc assert exit_status_success == actual_rc - check_sudo = test_cmd.format( - 'r', '/etc/cron.d/', piholeuser) + check_sudo = test_cmd.format("r", "/etc/cron.d/", piholeuser) actual_rc = host.run(check_sudo).rc assert exit_status_success == actual_rc - check_sudo = test_cmd.format( - 'r', '/etc/cron.d/pihole', piholeuser) + check_sudo = test_cmd.format("r", "/etc/cron.d/pihole", piholeuser) actual_rc = host.run(check_sudo).rc assert exit_status_success == actual_rc - directories = get_directories_recursive(host, '/etc/.pihole/') + directories = get_directories_recursive(host, "/etc/.pihole/") for directory in directories: - check_pihole = test_cmd.format('r', directory, piholeuser) + check_pihole = test_cmd.format("r", directory, piholeuser) actual_rc = host.run(check_pihole).rc - check_pihole = test_cmd.format('x', directory, piholeuser) + check_pihole = test_cmd.format("x", directory, piholeuser) actual_rc = host.run(check_pihole).rc findfiles = 'find "{}" -maxdepth 1 -type f -exec echo {{}} \\;;' filelist = host.run(findfiles.format(directory)) files = list(filter(bool, filelist.stdout.splitlines())) for file in files: - check_pihole = test_cmd.format('r', file, piholeuser) + check_pihole = test_cmd.format("r", file, piholeuser) actual_rc = host.run(check_pihole).rc @pytest.mark.parametrize("test_webpage", [True]) def test_installPihole_fresh_install_readableBlockpage(host, test_webpage): - ''' + """ confirms all web page assets from Core repo are readable by $LIGHTTPD_USER on a fresh build - ''' + """ piholeWebpage = [ "127.0.0.1", # "pi.hole" ] - # Whiptail dialog returns Cancel for user prompt - mock_command('whiptail', {'*': ('', '0')}, host) + # dialog returns Cancel for user prompt + mock_command("dialog", {"*": ("", "0")}, host) # mock git pull - mock_command_passthrough('git', {'pull': ('', '0')}, host) + mock_command_passthrough("git", {"pull": ("", "0")}, host) # mock systemctl to start lighttpd and FTL - ligthttpdcommand = dedent(r'''\"\" + ligthttpdcommand = dedent( + r'''\"\" echo 'starting lighttpd with {}' if [ command -v "apt-get" >/dev/null 2>&1 ]; then LIGHTTPD_USER="www-data" @@ -429,64 +400,51 @@ def test_installPihole_fresh_install_readableBlockpage(host, test_webpage): /usr/sbin/lighttpd -tt -f '{config}' /usr/sbin/lighttpd -f '{config}' echo \"\"'''.format( - '{}', - usergroup='${{LIGHTTPD_USER}}:${{LIGHTTPD_GROUP}}', - chmodarg='{{}}', - config='/etc/lighttpd/lighttpd.conf', - run='/var/run/lighttpd', - cache='/var/cache/lighttpd', - uploads='/var/cache/lighttpd/uploads', - compress='/var/cache/lighttpd/compress' + "{}", + usergroup="${{LIGHTTPD_USER}}:${{LIGHTTPD_GROUP}}", + chmodarg="{{}}", + config="/etc/lighttpd/lighttpd.conf", + run="/run/lighttpd", + cache="/var/cache/lighttpd", + uploads="/var/cache/lighttpd/uploads", + compress="/var/cache/lighttpd/compress", ) ) - FTLcommand = dedent('''\"\" + FTLcommand = dedent( + '''\"\" set -x /etc/init.d/pihole-FTL restart - echo \"\"''') + echo \"\"''' + ) mock_command_run( - 'systemctl', + "systemctl", { - 'enable lighttpd': ( - '', - '0' - ), - 'restart lighttpd': ( - ligthttpdcommand.format('restart'), - '0' - ), - 'start lighttpd': ( - ligthttpdcommand.format('start'), - '0' - ), - 'enable pihole-FTL': ( - '', - '0' - ), - 'restart pihole-FTL': ( - FTLcommand, - '0' - ), - 'start pihole-FTL': ( - FTLcommand, - '0' - ), - '*': ( - 'echo "systemctl call with $@"', - '0' - ), + "enable lighttpd": ("", "0"), + "restart lighttpd": (ligthttpdcommand.format("restart"), "0"), + "start lighttpd": (ligthttpdcommand.format("start"), "0"), + "enable pihole-FTL": ("", "0"), + "restart pihole-FTL": (FTLcommand, "0"), + "start pihole-FTL": (FTLcommand, "0"), + "*": ('echo "systemctl call with $@"', "0"), }, - host + host, ) # create configuration file - setup_var_file = 'cat < /etc/pihole/setupVars.conf\n' + setup_var_file = "cat < /etc/pihole/setupVars.conf\n" for k, v in SETUPVARS.items(): setup_var_file += "{}={}\n".format(k, v) setup_var_file += "INSTALL_WEB_SERVER=true\n" setup_var_file += "INSTALL_WEB_INTERFACE=true\n" - setup_var_file += "IPV4_ADDRESS=127.0.0.1\n" setup_var_file += "EOF\n" host.run(setup_var_file) - installWeb = host.run(''' + # Install FTL's development branch to get the latest features + host.run( + """ + echo "development" > /etc/pihole/ftlbranch + """ + ) + installWeb = host.run( + """ export TERM=xterm export DEBIAN_FRONTEND=noninteractive umask 0027 @@ -500,33 +458,32 @@ def test_installPihole_fresh_install_readableBlockpage(host, test_webpage): echo "webroot=${webroot}" echo "INSTALL_WEB_INTERFACE=${INSTALL_WEB_INTERFACE}" echo "INSTALL_WEB_SERVER=${INSTALL_WEB_SERVER}" - ''') + """ + ) assert 0 == installWeb.rc - piholeuser = 'pihole' - webuser = '' - user = re.findall( - r"^\s*LIGHTTPD_USER=.*$", installWeb.stdout, re.MULTILINE) + piholeuser = "pihole" + webuser = "" + user = re.findall(r"^\s*LIGHTTPD_USER=.*$", installWeb.stdout, re.MULTILINE) for match in user: - webuser = match.replace('LIGHTTPD_USER=', '').strip() - webroot = '' - user = re.findall( - r"^\s*webroot=.*$", installWeb.stdout, re.MULTILINE) + webuser = match.replace("LIGHTTPD_USER=", "").strip() + webroot = "" + user = re.findall(r"^\s*webroot=.*$", installWeb.stdout, re.MULTILINE) for match in user: - webroot = match.replace('webroot=', '').strip() + webroot = match.replace("webroot=", "").strip() if not webroot.strip(): - webroot = '/var/www/html' + webroot = "/var/www/html" installWebInterface = True interface = re.findall( - r"^\s*INSTALL_WEB_INTERFACE=.*$", installWeb.stdout, re.MULTILINE) + r"^\s*INSTALL_WEB_INTERFACE=.*$", installWeb.stdout, re.MULTILINE + ) for match in interface: - testvalue = match.replace('INSTALL_WEB_INTERFACE=', '').strip().lower() + testvalue = match.replace("INSTALL_WEB_INTERFACE=", "").strip().lower() if not testvalue.strip(): installWebInterface = testvalue == "true" installWebServer = True - server = re.findall( - r"^\s*INSTALL_WEB_SERVER=.*$", installWeb.stdout, re.MULTILINE) + server = re.findall(r"^\s*INSTALL_WEB_SERVER=.*$", installWeb.stdout, re.MULTILINE) for match in server: - testvalue = match.replace('INSTALL_WEB_SERVER=', '').strip().lower() + testvalue = match.replace("INSTALL_WEB_SERVER=", "").strip().lower() if not testvalue.strip(): installWebServer = testvalue == "true" # if webserver install was not requested @@ -537,91 +494,78 @@ def test_installPihole_fresh_install_readableBlockpage(host, test_webpage): test_cmd = 'su --shell /bin/bash --command "test -{0} {1}" -p {2}' # check files that need a running FTL to be created # readable and writeable pihole-FTL.db - check_FTLconf = test_cmd.format( - 'r', '/etc/pihole/pihole-FTL.db', piholeuser) + check_FTLconf = test_cmd.format("r", "/etc/pihole/pihole-FTL.db", piholeuser) actual_rc = host.run(check_FTLconf).rc assert exit_status_success == actual_rc - check_FTLconf = test_cmd.format( - 'w', '/etc/pihole/pihole-FTL.db', piholeuser) + check_FTLconf = test_cmd.format("w", "/etc/pihole/pihole-FTL.db", piholeuser) actual_rc = host.run(check_FTLconf).rc assert exit_status_success == actual_rc # check directories above $webroot for read and execute permission - check_var = test_cmd.format('r', '/var', webuser) + check_var = test_cmd.format("r", "/var", webuser) actual_rc = host.run(check_var).rc assert exit_status_success == actual_rc - check_var = test_cmd.format('x', '/var', webuser) + check_var = test_cmd.format("x", "/var", webuser) actual_rc = host.run(check_var).rc assert exit_status_success == actual_rc - check_www = test_cmd.format('r', '/var/www', webuser) + check_www = test_cmd.format("r", "/var/www", webuser) actual_rc = host.run(check_www).rc assert exit_status_success == actual_rc - check_www = test_cmd.format('x', '/var/www', webuser) + check_www = test_cmd.format("x", "/var/www", webuser) actual_rc = host.run(check_www).rc assert exit_status_success == actual_rc - check_html = test_cmd.format('r', '/var/www/html', webuser) + check_html = test_cmd.format("r", "/var/www/html", webuser) actual_rc = host.run(check_html).rc assert exit_status_success == actual_rc - check_html = test_cmd.format('x', '/var/www/html', webuser) + check_html = test_cmd.format("x", "/var/www/html", webuser) actual_rc = host.run(check_html).rc assert exit_status_success == actual_rc # check directories below $webroot for read and execute permission - check_admin = test_cmd.format('r', webroot + '/admin', webuser) + check_admin = test_cmd.format("r", webroot + "/admin", webuser) actual_rc = host.run(check_admin).rc assert exit_status_success == actual_rc - check_admin = test_cmd.format('x', webroot + '/admin', webuser) + check_admin = test_cmd.format("x", webroot + "/admin", webuser) actual_rc = host.run(check_admin).rc assert exit_status_success == actual_rc - directories = get_directories_recursive(host, webroot + '/admin/*/') + directories = get_directories_recursive(host, webroot + "/admin/") for directory in directories: - check_pihole = test_cmd.format('r', directory, webuser) + check_pihole = test_cmd.format("r", directory, webuser) actual_rc = host.run(check_pihole).rc - check_pihole = test_cmd.format('x', directory, webuser) + check_pihole = test_cmd.format("x", directory, webuser) actual_rc = host.run(check_pihole).rc findfiles = 'find "{}" -maxdepth 1 -type f -exec echo {{}} \\;;' filelist = host.run(findfiles.format(directory)) files = list(filter(bool, filelist.stdout.splitlines())) for file in files: - check_pihole = test_cmd.format('r', file, webuser) + check_pihole = test_cmd.format("r", file, webuser) actual_rc = host.run(check_pihole).rc # check web interface files # change nameserver to pi-hole # setting nameserver in /etc/resolv.conf to pi-hole does # not work here because of the way docker uses this file - ns = host.run( - r"sed -i 's/nameserver.*/nameserver 127.0.0.1/' /etc/resolv.conf") + ns = host.run(r"sed -i 's/nameserver.*/nameserver 127.0.0.1/' /etc/resolv.conf") pihole_is_ns = ns.rc == 0 def is_ip(address): m = re.match(r"(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})", address) return bool(m) + if installWebInterface is True: - check_pihole = test_cmd.format('r', webroot + '/pihole', webuser) - actual_rc = host.run(check_pihole).rc - assert exit_status_success == actual_rc - check_pihole = test_cmd.format('x', webroot + '/pihole', webuser) - actual_rc = host.run(check_pihole).rc - assert exit_status_success == actual_rc - # check most important files in $webroot for read permission - check_index = test_cmd.format( - 'r', webroot + '/pihole/index.php', webuser) - actual_rc = host.run(check_index).rc - assert exit_status_success == actual_rc - check_blockpage = test_cmd.format( - 'r', webroot + '/pihole/blockingpage.css', webuser) - actual_rc = host.run(check_blockpage).rc - assert exit_status_success == actual_rc if test_webpage is True: # check webpage for unreadable files noPHPfopen = re.compile( - (r"PHP Error(%d+):\s+fopen([^)]+):\s+" + - r"failed to open stream: " + - r"Permission denied in"), - re.I) + ( + r"PHP Error(%d+):\s+fopen([^)]+):\s+" + + r"failed to open stream: " + + r"Permission denied in" + ), + re.I, + ) # using cURL option --dns-servers is not possible status = ( - 'curl -s --head "{}" | ' + - 'head -n 1 | ' + - 'grep "HTTP/1.[01] [23].." > /dev/null') + 'curl -s --head "{}" | ' + + "head -n 1 | " + + 'grep "HTTP/1.[01] [23].." > /dev/null' + ) digcommand = r"dig A +short {} @127.0.0.1 | head -n 1" pagecontent = 'curl --verbose -L "{}"' for page in piholeWebpage: @@ -641,448 +585,501 @@ def is_ip(address): 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' + """ + ) + expected_stdout = tick_box + " Update local cache of available packages" assert expected_stdout in updateCache.stdout - assert 'error' not in updateCache.stdout.lower() + assert "error" not in updateCache.stdout.lower() 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(''' + """ + mock_command("apt-get", {"update": ("", "1")}, host) + 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' + """ + ) + 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 + assert "Error: Unable to update package cache." in updateCache.stdout def test_FTL_detect_aarch64_no_errors(host): - ''' + """ confirms only aarch64 package is downloaded for FTL engine - ''' + """ # mock uname to return aarch64 platform - mock_command('uname', {'-m': ('aarch64', '0')}, host) - # mock `which sh` to return `/bin/sh` - mock_command('which', {'sh': ('/bin/sh', '0')}, host) + mock_command("uname", {"-m": ("aarch64", "0")}, host) # mock ldd to respond with aarch64 shared library - mock_command('ldd', {'/bin/sh': ('/lib/ld-linux-aarch64.so.1', '0')}, host) - detectPlatform = host.run(''' + mock_command( + "ldd", + { + "/bin/sh": ("/lib/ld-linux-aarch64.so.1", "0"), + "/usr/bin/sh": ("/lib/ld-linux-aarch64.so.1", "0"), + }, + host, + ) + 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}" - ''') - expected_stdout = info_box + ' FTL Checks...' + """ + ) + expected_stdout = info_box + " FTL Checks..." assert expected_stdout in detectPlatform.stdout - expected_stdout = tick_box + ' Detected AArch64 (64 Bit ARM) processor' + expected_stdout = tick_box + " Detected AArch64 (64 Bit ARM) processor" assert expected_stdout in detectPlatform.stdout - expected_stdout = tick_box + ' Downloading and Installing FTL' + expected_stdout = tick_box + " Downloading and Installing FTL" assert expected_stdout in detectPlatform.stdout def test_FTL_detect_armv4t_no_errors(host): - ''' + """ confirms only armv4t package is downloaded for FTL engine - ''' + """ # mock uname to return armv4t platform - mock_command('uname', {'-m': ('armv4t', '0')}, host) - # mock `which sh` to return `/bin/sh` - mock_command('which', {'sh': ('/bin/sh', '0')}, host) + mock_command("uname", {"-m": ("armv4t", "0")}, host) # mock ldd to respond with armv4t shared library - mock_command('ldd', {'/bin/sh': ('/lib/ld-linux.so.3', '0')}, host) - detectPlatform = host.run(''' + mock_command( + "ldd", + { + "/bin/sh": ("/lib/ld-linux.so.3", "0"), + "/usr/bin/sh": ("/lib/ld-linux.so.3", "0"), + }, + host, + ) + 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}" - ''') - expected_stdout = info_box + ' FTL Checks...' + """ + ) + expected_stdout = info_box + " FTL Checks..." assert expected_stdout in detectPlatform.stdout - expected_stdout = tick_box + (' Detected ARMv4 processor') + expected_stdout = tick_box + (" Detected ARMv4 processor") assert expected_stdout in detectPlatform.stdout - expected_stdout = tick_box + ' Downloading and Installing FTL' + expected_stdout = tick_box + " Downloading and Installing FTL" assert expected_stdout in detectPlatform.stdout def test_FTL_detect_armv5te_no_errors(host): - ''' + """ confirms only armv5te package is downloaded for FTL engine - ''' + """ # mock uname to return armv5te platform - mock_command('uname', {'-m': ('armv5te', '0')}, host) - # mock `which sh` to return `/bin/sh` - mock_command('which', {'sh': ('/bin/sh', '0')}, host) + mock_command("uname", {"-m": ("armv5te", "0")}, host) # mock ldd to respond with ld-linux shared library - mock_command('ldd', {'/bin/sh': ('/lib/ld-linux.so.3', '0')}, host) - detectPlatform = host.run(''' + mock_command( + "ldd", + { + "/bin/sh": ("/lib/ld-linux.so.3", "0"), + "/usr/bin/sh": ("/lib/ld-linux.so.3", "0"), + }, + host, + ) + 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}" - ''') - expected_stdout = info_box + ' FTL Checks...' + """ + ) + expected_stdout = info_box + " FTL Checks..." assert expected_stdout in detectPlatform.stdout - expected_stdout = tick_box + (' Detected ARMv5 (or newer) processor') + expected_stdout = tick_box + (" Detected ARMv5 (or newer) processor") assert expected_stdout in detectPlatform.stdout - expected_stdout = tick_box + ' Downloading and Installing FTL' + expected_stdout = tick_box + " Downloading and Installing FTL" assert expected_stdout in detectPlatform.stdout def test_FTL_detect_armv6l_no_errors(host): - ''' + """ confirms only armv6l package is downloaded for FTL engine - ''' + """ # mock uname to return armv6l platform - mock_command('uname', {'-m': ('armv6l', '0')}, host) + mock_command("uname", {"-m": ("armv6l", "0")}, host) # mock ldd to respond with ld-linux-armhf shared library - # mock `which sh` to return `/bin/sh` - mock_command('which', {'sh': ('/bin/sh', '0')}, host) - mock_command('ldd', {'/bin/sh': ('/lib/ld-linux-armhf.so.3', '0')}, host) - detectPlatform = host.run(''' + mock_command( + "ldd", + { + "/bin/sh": ("/lib/ld-linux-armhf.so.3", "0"), + "/usr/bin/sh": ("/lib/ld-linux-armhf.so.3", "0"), + }, + host, + ) + 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}" - ''') - expected_stdout = info_box + ' FTL Checks...' + """ + ) + expected_stdout = info_box + " FTL Checks..." assert expected_stdout in detectPlatform.stdout - expected_stdout = tick_box + (' Detected ARMv6 processor ' - '(with hard-float support)') + expected_stdout = tick_box + ( + " Detected ARMv6 processor " "(with hard-float support)" + ) assert expected_stdout in detectPlatform.stdout - expected_stdout = tick_box + ' Downloading and Installing FTL' + expected_stdout = tick_box + " Downloading and Installing FTL" assert expected_stdout in detectPlatform.stdout def test_FTL_detect_armv7l_no_errors(host): - ''' + """ confirms only armv7l package is downloaded for FTL engine - ''' + """ # mock uname to return armv7l platform - mock_command('uname', {'-m': ('armv7l', '0')}, host) + mock_command("uname", {"-m": ("armv7l", "0")}, host) # mock ldd to respond with ld-linux-armhf shared library - # mock `which sh` to return `/bin/sh` - mock_command('which', {'sh': ('/bin/sh', '0')}, host) - mock_command('ldd', {'/bin/sh': ('/lib/ld-linux-armhf.so.3', '0')}, host) - detectPlatform = host.run(''' + mock_command( + "ldd", + { + "/bin/sh": ("/lib/ld-linux-armhf.so.3", "0"), + "/usr/bin/sh": ("/lib/ld-linux-armhf.so.3", "0"), + }, + host, + ) + 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}" - ''') - expected_stdout = info_box + ' FTL Checks...' + """ + ) + expected_stdout = info_box + " FTL Checks..." assert expected_stdout in detectPlatform.stdout - expected_stdout = tick_box + (' Detected ARMv7 processor ' - '(with hard-float support)') + expected_stdout = tick_box + ( + " Detected ARMv7 processor " "(with hard-float support)" + ) assert expected_stdout in detectPlatform.stdout - expected_stdout = tick_box + ' Downloading and Installing FTL' + expected_stdout = tick_box + " Downloading and Installing FTL" assert expected_stdout in detectPlatform.stdout def test_FTL_detect_armv8a_no_errors(host): - ''' + """ confirms only armv8a package is downloaded for FTL engine - ''' + """ # mock uname to return armv8a platform - mock_command('uname', {'-m': ('armv8a', '0')}, host) - # mock `which sh` to return `/bin/sh` - mock_command('which', {'sh': ('/bin/sh', '0')}, host) + mock_command("uname", {"-m": ("armv8a", "0")}, host) # mock ldd to respond with ld-linux-armhf shared library - mock_command('ldd', {'/bin/sh': ('/lib/ld-linux-armhf.so.3', '0')}, host) - detectPlatform = host.run(''' + mock_command( + "ldd", + { + "/bin/sh": ("/lib/ld-linux-armhf.so.3", "0"), + "/usr/bin/sh": ("/lib/ld-linux-armhf.so.3", "0"), + }, + host, + ) + 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}" - ''') - expected_stdout = info_box + ' FTL Checks...' + """ + ) + expected_stdout = info_box + " FTL Checks..." assert expected_stdout in detectPlatform.stdout - expected_stdout = tick_box + ' Detected ARMv8 (or newer) processor' + expected_stdout = tick_box + " Detected ARMv8 (or newer) processor" assert expected_stdout in detectPlatform.stdout - expected_stdout = tick_box + ' Downloading and Installing FTL' + expected_stdout = tick_box + " Downloading and Installing FTL" assert expected_stdout in detectPlatform.stdout def test_FTL_detect_x86_64_no_errors(host): - ''' + """ confirms only x86_64 package is downloaded for FTL engine - ''' - # mock `which sh` to return `/bin/sh` - mock_command('which', {'sh': ('/bin/sh', '0')}, host) - 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}" - ''') - expected_stdout = info_box + ' FTL Checks...' + """ + ) + expected_stdout = info_box + " FTL Checks..." assert expected_stdout in detectPlatform.stdout - expected_stdout = tick_box + ' Detected x86_64 processor' + expected_stdout = tick_box + " Detected x86_64 processor" assert expected_stdout in detectPlatform.stdout - expected_stdout = tick_box + ' Downloading and Installing FTL' + expected_stdout = tick_box + " Downloading and Installing FTL" assert expected_stdout in detectPlatform.stdout def test_FTL_detect_unknown_no_errors(host): - ''' confirms only generic package is downloaded for FTL engine ''' + """confirms only generic package is downloaded for FTL engine""" # mock uname to return generic platform - mock_command('uname', {'-m': ('mips', '0')}, host) - # mock `which sh` to return `/bin/sh` - mock_command('which', {'sh': ('/bin/sh', '0')}, host) - detectPlatform = host.run(''' + mock_command("uname", {"-m": ("mips", "0")}, host) + 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}" - ''') - expected_stdout = 'Not able to detect processor (unknown: mips)' + """ + ) + expected_stdout = "Not able to detect processor (unknown: mips)" assert expected_stdout in detectPlatform.stdout def test_FTL_download_aarch64_no_errors(host): - ''' + """ confirms only aarch64 package is downloaded for FTL engine - ''' - # mock whiptail answers and ensure installer dependencies - mock_command('whiptail', {'*': ('', '0')}, host) - host.run(''' + """ + # mock dialog answers and ensure installer dependencies + mock_command("dialog", {"*": ("", "0")}, host) + host.run( + """ source /opt/pihole/basic-install.sh package_manager_detect install_dependent_packages ${INSTALLER_DEPS[@]} - ''') - download_binary = host.run(''' + """ + ) + download_binary = host.run( + """ source /opt/pihole/basic-install.sh create_pihole_user FTLinstall "pihole-FTL-aarch64-linux-gnu" - ''') - expected_stdout = tick_box + ' Downloading and Installing FTL' + """ + ) + expected_stdout = tick_box + " Downloading and Installing FTL" assert expected_stdout in download_binary.stdout - assert 'error' not in download_binary.stdout.lower() + assert "error" not in download_binary.stdout.lower() def test_FTL_binary_installed_and_responsive_no_errors(host): - ''' + """ confirms FTL binary is copied and functional in installed location - ''' - installed_binary = host.run(''' + """ + host.run( + """ source /opt/pihole/basic-install.sh create_pihole_user funcOutput=$(get_binary_name) + echo "development" > /etc/pihole/ftlbranch binary="pihole-FTL${funcOutput##*pihole-FTL}" theRest="${funcOutput%pihole-FTL*}" FTLdetect "${binary}" "${theRest}" - pihole-FTL version - ''') - expected_stdout = 'v' - assert expected_stdout in installed_binary.stdout - - -# def test_FTL_support_files_installed(host): -# ''' -# confirms FTL support files are installed -# ''' -# support_files = host.run(''' -# source /opt/pihole/basic-install.sh -# FTLdetect -# stat -c '%a %n' /var/log/pihole-FTL.log -# stat -c '%a %n' /run/pihole-FTL.port -# stat -c '%a %n' /run/pihole-FTL.pid -# ls -lac /run -# ''') -# assert '644 /run/pihole-FTL.port' in support_files.stdout -# assert '644 /run/pihole-FTL.pid' in support_files.stdout -# assert '644 /var/log/pihole-FTL.log' in support_files.stdout + """ + ) + version_check = host.run( + """ + VERSION=$(pihole-FTL version) + echo ${VERSION:0:1} + """ + ) + expected_stdout = "v" + assert expected_stdout in version_check.stdout def test_IPv6_only_link_local(host): - ''' + """ confirms IPv6 blocking is disabled for Link-local address - ''' + """ # mock ip -6 address to return Link-local address mock_command_2( - 'ip', - { - '-6 address': ( - 'inet6 fe80::d210:52fa:fe00:7ad7/64 scope link', - '0' - ) - }, - host + "ip", + {"-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') + """ + ) + expected_stdout = "Unable to find IPv6 ULA/GUA address" assert expected_stdout in detectPlatform.stdout def test_IPv6_only_ULA(host): - ''' + """ confirms IPv6 blocking is enabled for ULA addresses - ''' + """ # mock ip -6 address to return ULA address mock_command_2( - 'ip', + "ip", { - '-6 address': ( - 'inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global', - '0' + "-6 address": ( + "inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global", + "0", ) }, - host + host, ) - detectPlatform = host.run(''' + detectPlatform = host.run( + """ source /opt/pihole/basic-install.sh find_IPv6_information - ''') - expected_stdout = 'Found IPv6 ULA address' + """ + ) + expected_stdout = "Found IPv6 ULA address" assert expected_stdout in detectPlatform.stdout def test_IPv6_only_GUA(host): - ''' + """ confirms IPv6 blocking is enabled for GUA addresses - ''' + """ # mock ip -6 address to return GUA address mock_command_2( - 'ip', + "ip", { - '-6 address': ( - 'inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global', - '0' + "-6 address": ( + "inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global", + "0", ) }, - host + host, ) - detectPlatform = host.run(''' + detectPlatform = host.run( + """ source /opt/pihole/basic-install.sh find_IPv6_information - ''') - expected_stdout = 'Found IPv6 GUA address' + """ + ) + expected_stdout = "Found IPv6 GUA address" assert expected_stdout in detectPlatform.stdout def test_IPv6_GUA_ULA_test(host): - ''' + """ confirms IPv6 blocking is enabled for GUA and ULA addresses - ''' + """ # mock ip -6 address to return GUA and ULA addresses mock_command_2( - 'ip', + "ip", { - '-6 address': ( - 'inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global\n' - 'inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global', - '0' + "-6 address": ( + "inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global\n" + "inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global", + "0", ) }, - host + host, ) - detectPlatform = host.run(''' + detectPlatform = host.run( + """ source /opt/pihole/basic-install.sh find_IPv6_information - ''') - expected_stdout = 'Found IPv6 ULA address' + """ + ) + expected_stdout = "Found IPv6 ULA address" assert expected_stdout in detectPlatform.stdout def test_IPv6_ULA_GUA_test(host): - ''' + """ confirms IPv6 blocking is enabled for GUA and ULA addresses - ''' + """ # mock ip -6 address to return ULA and GUA addresses mock_command_2( - 'ip', + "ip", { - '-6 address': ( - 'inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global\n' - 'inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global', - '0' + "-6 address": ( + "inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global\n" + "inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global", + "0", ) }, - host + host, ) - detectPlatform = host.run(''' + detectPlatform = host.run( + """ source /opt/pihole/basic-install.sh find_IPv6_information - ''') - expected_stdout = 'Found IPv6 ULA address' + """ + ) + expected_stdout = "Found IPv6 ULA address" assert expected_stdout in detectPlatform.stdout def test_validate_ip(host): - ''' + """ Tests valid_ip for various IP addresses - ''' + """ 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 - test_address('192.168.1.1') - test_address('127.0.0.1') - test_address('255.255.255.255') - test_address('255.255.255.256', False) - test_address('255.255.256.255', False) - test_address('255.256.255.255', False) - test_address('256.255.255.255', False) - test_address('1092.168.1.1', False) - test_address('not an IP', False) - test_address('8.8.8.8#', False) - test_address('8.8.8.8#0') - test_address('8.8.8.8#1') - test_address('8.8.8.8#42') - test_address('8.8.8.8#888') - test_address('8.8.8.8#1337') - test_address('8.8.8.8#65535') - test_address('8.8.8.8#65536', False) - test_address('8.8.8.8#-1', False) - test_address('00.0.0.0', False) - test_address('010.0.0.0', False) - test_address('001.0.0.0', False) - test_address('0.0.0.0#00', False) - test_address('0.0.0.0#01', False) - test_address('0.0.0.0#001', False) - test_address('0.0.0.0#0001', False) - test_address('0.0.0.0#00001', False) + test_address("192.168.1.1") + test_address("127.0.0.1") + test_address("255.255.255.255") + test_address("255.255.255.256", False) + test_address("255.255.256.255", False) + test_address("255.256.255.255", False) + test_address("256.255.255.255", False) + test_address("1092.168.1.1", False) + test_address("not an IP", False) + test_address("8.8.8.8#", False) + test_address("8.8.8.8#0") + test_address("8.8.8.8#1") + test_address("8.8.8.8#42") + test_address("8.8.8.8#888") + test_address("8.8.8.8#1337") + test_address("8.8.8.8#65535") + test_address("8.8.8.8#65536", False) + test_address("8.8.8.8#-1", False) + test_address("00.0.0.0", False) + test_address("010.0.0.0", False) + test_address("001.0.0.0", False) + test_address("0.0.0.0#00", False) + test_address("0.0.0.0#01", False) + test_address("0.0.0.0#001", False) + test_address("0.0.0.0#0001", False) + test_address("0.0.0.0#00001", False) def test_os_check_fails(host): - ''' Confirms install fails on unsupported OS ''' - host.run(''' + """Confirms install fails on unsupported OS""" + host.run( + """ source /opt/pihole/basic-install.sh package_manager_detect install_dependent_packages ${OS_CHECK_DEPS[@]} @@ -1091,67 +1088,105 @@ def test_os_check_fails(host): ID=UnsupportedOS VERSION_ID="2" EOT - ''') - detectOS = host.run('''t + """ + ) + detectOS = host.run( + """t source /opt/pihole/basic-install.sh os_check - ''') - expected_stdout = 'Unsupported OS detected: UnsupportedOS' + """ + ) + 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(''' + """Confirms OS meets the requirements""" + host.run( + """ source /opt/pihole/basic-install.sh package_manager_detect install_dependent_packages ${OS_CHECK_DEPS[@]} install_dependent_packages ${INSTALLER_DEPS[@]} - ''') - detectOS = host.run(''' + """ + ) + detectOS = host.run( + """ source /opt/pihole/basic-install.sh os_check - ''') - expected_stdout = 'Supported OS detected' + """ + ) + expected_stdout = "Supported OS detected" assert expected_stdout in detectOS.stdout def test_package_manager_has_installer_deps(host): - ''' Confirms OS is able to install the required packages for the installer''' - mock_command('whiptail', {'*': ('', '0')}, host) - output = host.run(''' + """Confirms OS is able to install the required packages for the installer""" + mock_command("dialog", {"*": ("", "0")}, host) + output = host.run( + """ source /opt/pihole/basic-install.sh package_manager_detect install_dependent_packages ${INSTALLER_DEPS[@]} - ''') + """ + ) - assert 'No package' not in output.stdout # centos7 still exits 0... + assert "No package" not in output.stdout assert output.rc == 0 def test_package_manager_has_pihole_deps(host): - ''' Confirms OS is able to install the required packages for Pi-hole ''' - mock_command('whiptail', {'*': ('', '0')}, host) - output = host.run(''' + """Confirms OS is able to install the required packages for Pi-hole""" + mock_command("dialog", {"*": ("", "0")}, host) + output = host.run( + """ source /opt/pihole/basic-install.sh package_manager_detect - select_rpm_php install_dependent_packages ${PIHOLE_DEPS[@]} - ''') + """ + ) - assert 'No package' not in output.stdout # centos7 still exits 0... + assert "No package" not in output.stdout assert output.rc == 0 def test_package_manager_has_web_deps(host): - ''' Confirms OS is able to install the required packages for web ''' - mock_command('whiptail', {'*': ('', '0')}, host) - output = host.run(''' + """Confirms OS is able to install the required packages for web""" + mock_command("dialog", {"*": ("", "0")}, host) + output = host.run( + """ source /opt/pihole/basic-install.sh package_manager_detect - select_rpm_php install_dependent_packages ${PIHOLE_WEB_DEPS[@]} - ''') + """ + ) - assert 'No package' not in output.stdout # centos7 still exits 0... + assert "No package" not in output.stdout assert output.rc == 0 + + +def test_webpage_sh_valid_domain(host): + """Confirms checkDomain function in webpage.sh works as expected""" + check1 = host.run( + """ + source /opt/pihole/webpage.sh + checkDomain "pi-hole.net" + """ + ) + check2 = host.run( + """ + source /opt/pihole/webpage.sh + checkDomain "ab.pi-hole.net" + """ + ) + + check3 = host.run( + """ + source /opt/pihole/webpage.sh + checkDomain "abc.pi-hole.net" + """ + ) + + assert "pi-hole.net" in check1.stdout + assert "ab.pi-hole.net" in check2.stdout + assert "abc.pi-hole.net" in check3.stdout diff --git a/test/test_any_utils.py b/test/test_any_utils.py index b30ff7fded..b3fabe6c48 100644 --- a/test/test_any_utils.py +++ b/test/test_any_utils.py @@ -1,22 +1,27 @@ def test_key_val_replacement_works(host): - ''' Confirms addOrEditKeyValPair either adds or replaces a key value pair in a given file ''' - host.run(''' + """Confirms addOrEditKeyValPair either adds or replaces a key value pair in a given file""" + host.run( + """ source /opt/pihole/utils.sh 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' + """ + ) + expected_stdout = "KEY_ONE=value3\nKEY_TWO=value2\nKEY_FOUR=value4\n" assert expected_stdout == output.stdout def test_key_addition_works(host): - ''' Confirms addKey adds a key (no value) to a file without duplicating it ''' - host.run(''' + """Confirms addKey adds a key (no value) to a file without duplicating it""" + host.run( + """ source /opt/pihole/utils.sh addKey "./testoutput" "KEY_ONE" addKey "./testoutput" "KEY_ONE" @@ -24,17 +29,41 @@ def test_key_addition_works(host): addKey "./testoutput" "KEY_TWO" addKey "./testoutput" "KEY_THREE" addKey "./testoutput" "KEY_THREE" - ''') - output = host.run(''' + """ + ) + output = host.run( + """ cat ./testoutput - ''') - expected_stdout = 'KEY_ONE\nKEY_TWO\nKEY_THREE\n' + """ + ) + expected_stdout = "KEY_ONE\nKEY_TWO\nKEY_THREE\n" + assert expected_stdout == output.stdout + + +def test_key_addition_substr(host): + """Confirms addKey adds substring keys (no value) to a file""" + host.run( + """ + source /opt/pihole/utils.sh + addKey "./testoutput" "KEY_ONE" + addKey "./testoutput" "KEY_O" + addKey "./testoutput" "KEY_TWO" + addKey "./testoutput" "Y_TWO" + """ + ) + output = host.run( + """ + cat ./testoutput + """ + ) + expected_stdout = "KEY_ONE\nKEY_O\nKEY_TWO\nY_TWO\n" assert expected_stdout == output.stdout def test_key_removal_works(host): - ''' Confirms removeKey removes a key or key/value pair ''' - host.run(''' + """Confirms removeKey removes a key or key/value pair""" + host.run( + """ source /opt/pihole/utils.sh addOrEditKeyValPair "./testoutput" "KEY_ONE" "value1" addOrEditKeyValPair "./testoutput" "KEY_TWO" "value2" @@ -42,33 +71,102 @@ def test_key_removal_works(host): addKey "./testoutput" "KEY_FOUR" removeKey "./testoutput" "KEY_TWO" removeKey "./testoutput" "KEY_FOUR" - ''') - output = host.run(''' + """ + ) + output = host.run( + """ cat ./testoutput - ''') - expected_stdout = 'KEY_ONE=value1\nKEY_THREE=value3\n' + """ + ) + expected_stdout = "KEY_ONE=value1\nKEY_THREE=value3\n" assert expected_stdout == output.stdout def test_getFTLAPIPort_default(host): - ''' Confirms getFTLAPIPort returns the default API port ''' - output = host.run(''' + """Confirms getFTLAPIPort returns the default API port""" + output = host.run( + """ source /opt/pihole/utils.sh getFTLAPIPort - ''') - expected_stdout = '4711\n' + """ + ) + expected_stdout = "4711\n" assert expected_stdout == output.stdout def test_getFTLAPIPort_custom(host): - ''' Confirms getFTLAPIPort returns a custom API port in a custom PORTFILE location ''' - host.run(''' - echo "PORTFILE=/tmp/port.file" > /etc/pihole/pihole-FTL.conf - echo "1234" > /tmp/port.file - ''') - output = host.run(''' + """Confirms getFTLAPIPort returns a custom API port""" + host.run( + """ + echo "FTLPORT=1234" > /etc/pihole/pihole-FTL.conf + """ + ) + output = host.run( + """ + source /opt/pihole/utils.sh + getFTLAPIPort + """ + ) + expected_stdout = "1234\n" + assert expected_stdout == output.stdout + + +def test_getFTLAPIPort_malicious(host): + """Confirms getFTLAPIPort returns 4711 if the setting in pihole-FTL.conf contains non-digits""" + host.run( + """ + echo "FTLPORT=*$ssdfsd" > /etc/pihole/pihole-FTL.conf + """ + ) + output = host.run( + """ source /opt/pihole/utils.sh getFTLAPIPort - ''') - expected_stdout = '1234\n' + """ + ) + expected_stdout = "4711\n" + assert expected_stdout == output.stdout + + +def test_getFTLPIDFile_default(host): + """Confirms getFTLPIDFile returns the default PID file path""" + output = host.run( + """ + source /opt/pihole/utils.sh + getFTLPIDFile + """ + ) + expected_stdout = "/run/pihole-FTL.pid\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( + """ + source /opt/pihole/utils.sh + getFTLPID + """ + ) + expected_stdout = "-1\n" + assert expected_stdout == output.stdout + + +def test_getFTLPIDFile_and_getFTLPID_custom(host): + """Confirms getFTLPIDFile returns a custom PID file path""" + host.run( + """ + tmpfile=$(mktemp) + echo "PIDFILE=${tmpfile}" > /etc/pihole/pihole-FTL.conf + echo "1234" > ${tmpfile} + """ + ) + output = host.run( + """ + source /opt/pihole/utils.sh + FTL_PID_FILE=$(getFTLPIDFile) + getFTLPID "${FTL_PID_FILE}" + """ + ) + expected_stdout = "1234\n" assert expected_stdout == output.stdout diff --git a/test/test_centos_7_support.py b/test/test_centos_7_support.py deleted file mode 100644 index f72740a6b8..0000000000 --- a/test/test_centos_7_support.py +++ /dev/null @@ -1,63 +0,0 @@ -from .conftest import ( - tick_box, - info_box, - mock_command, -) - - -def test_php_upgrade_default_optout_centos_eq_7(host): - ''' - confirms the default behavior to opt-out of installing PHP7 from REMI - ''' - package_manager_detect = host.run(''' - source /opt/pihole/basic-install.sh - package_manager_detect - select_rpm_php - ''') - expected_stdout = info_box + (' User opt-out of PHP 7 upgrade on CentOS. ' - 'Deprecated PHP may be in use.') - assert expected_stdout in package_manager_detect.stdout - remi_package = host.package('remi-release') - assert not remi_package.is_installed - - -def test_php_upgrade_user_optout_centos_eq_7(host): - ''' - confirms installer behavior when user opt-out of installing PHP7 from REMI - (php not currently installed) - ''' - # Whiptail dialog returns Cancel for user prompt - mock_command('whiptail', {'*': ('', '1')}, host) - package_manager_detect = host.run(''' - source /opt/pihole/basic-install.sh - package_manager_detect - select_rpm_php - ''') - expected_stdout = info_box + (' User opt-out of PHP 7 upgrade on CentOS. ' - 'Deprecated PHP may be in use.') - assert expected_stdout in package_manager_detect.stdout - remi_package = host.package('remi-release') - assert not remi_package.is_installed - - -def test_php_upgrade_user_optin_centos_eq_7(host): - ''' - confirms installer behavior when user opt-in to installing PHP7 from REMI - (php not currently installed) - ''' - # Whiptail dialog returns Continue for user prompt - mock_command('whiptail', {'*': ('', '0')}, host) - package_manager_detect = host.run(''' - source /opt/pihole/basic-install.sh - package_manager_detect - select_rpm_php - ''') - assert 'opt-out' not in package_manager_detect.stdout - expected_stdout = info_box + (' Enabling Remi\'s RPM repository ' - '(https://rpms.remirepo.net)') - assert expected_stdout in package_manager_detect.stdout - expected_stdout = tick_box + (' Remi\'s RPM repository has ' - 'been enabled for PHP7') - assert expected_stdout in package_manager_detect.stdout - remi_package = host.package('remi-release') - assert remi_package.is_installed diff --git a/test/test_centos_8_support.py b/test/test_centos_8_support.py deleted file mode 100644 index 464055b412..0000000000 --- a/test/test_centos_8_support.py +++ /dev/null @@ -1,68 +0,0 @@ -from .conftest import ( - tick_box, - info_box, - mock_command, -) - - -def test_php_upgrade_default_continue_centos_gte_8(host): - ''' - confirms the latest version of CentOS continues / does not optout - (should trigger on CentOS7 only) - ''' - package_manager_detect = host.run(''' - source /opt/pihole/basic-install.sh - package_manager_detect - select_rpm_php - ''') - unexpected_stdout = info_box + (' User opt-out of PHP 7 upgrade on CentOS.' - ' Deprecated PHP may be in use.') - assert unexpected_stdout not in package_manager_detect.stdout - # ensure remi was not installed on latest CentOS - remi_package = host.package('remi-release') - assert not remi_package.is_installed - - -def test_php_upgrade_user_optout_skipped_centos_gte_8(host): - ''' - confirms installer skips user opt-out of installing PHP7 from REMI on - latest CentOS (should trigger on CentOS7 only) - (php not currently installed) - ''' - # Whiptail dialog returns Cancel for user prompt - mock_command('whiptail', {'*': ('', '1')}, host) - package_manager_detect = host.run(''' - source /opt/pihole/basic-install.sh - package_manager_detect - select_rpm_php - ''') - unexpected_stdout = info_box + (' User opt-out of PHP 7 upgrade on CentOS.' - ' Deprecated PHP may be in use.') - assert unexpected_stdout not in package_manager_detect.stdout - # ensure remi was not installed on latest CentOS - remi_package = host.package('remi-release') - assert not remi_package.is_installed - - -def test_php_upgrade_user_optin_skipped_centos_gte_8(host): - ''' - confirms installer skips user opt-in to installing PHP7 from REMI on - latest CentOS (should trigger on CentOS7 only) - (php not currently installed) - ''' - # Whiptail dialog returns Continue for user prompt - mock_command('whiptail', {'*': ('', '0')}, host) - package_manager_detect = host.run(''' - source /opt/pihole/basic-install.sh - package_manager_detect - select_rpm_php - ''') - assert 'opt-out' not in package_manager_detect.stdout - unexpected_stdout = info_box + (' Enabling Remi\'s RPM repository ' - '(https://rpms.remirepo.net)') - assert unexpected_stdout not in package_manager_detect.stdout - unexpected_stdout = tick_box + (' Remi\'s RPM repository has ' - 'been enabled for PHP7') - assert unexpected_stdout not in package_manager_detect.stdout - remi_package = host.package('remi-release') - assert not remi_package.is_installed diff --git a/test/test_centos_common_support.py b/test/test_centos_common_support.py index 8903a7a07e..871fee2983 100644 --- a/test/test_centos_common_support.py +++ b/test/test_centos_common_support.py @@ -7,119 +7,21 @@ ) -def test_release_supported_version_check_centos(host): - ''' - confirms installer exits on unsupported releases of CentOS - ''' - # modify /etc/redhat-release to mock an unsupported CentOS release - host.run('echo "CentOS Linux release 6.9" > /etc/redhat-release') - package_manager_detect = host.run(''' - source /opt/pihole/basic-install.sh - package_manager_detect - select_rpm_php - ''') - expected_stdout = cross_box + (' CentOS 6 is not supported.') - assert expected_stdout in package_manager_detect.stdout - expected_stdout = 'Please update to CentOS release 7 or later' - assert expected_stdout in package_manager_detect.stdout - - def test_enable_epel_repository_centos(host): - ''' + """ confirms the EPEL package repository is enabled when installed on CentOS - ''' - package_manager_detect = host.run(''' + """ + package_manager_detect = host.run( + """ source /opt/pihole/basic-install.sh package_manager_detect - select_rpm_php - ''') - expected_stdout = info_box + (' Enabling EPEL package repository ' - '(https://fedoraproject.org/wiki/EPEL)') + """ + ) + expected_stdout = info_box + ( + " Enabling EPEL package repository " "(https://fedoraproject.org/wiki/EPEL)" + ) assert expected_stdout in package_manager_detect.stdout - expected_stdout = tick_box + ' Installed epel-release' + expected_stdout = tick_box + " Installed" assert expected_stdout in package_manager_detect.stdout - epel_package = host.package('epel-release') + epel_package = host.package("epel-release") assert epel_package.is_installed - - -def test_php_version_lt_7_detected_upgrade_default_optout_centos(host): - ''' - confirms the default behavior to opt-out of upgrading to PHP7 from REMI - ''' - # first we will install the default php version to test installer behavior - php_install = host.run('yum install -y php') - assert php_install.rc == 0 - php_package = host.package('php') - default_centos_php_version = php_package.version.split('.')[0] - if int(default_centos_php_version) >= 7: # PHP7 is supported/recommended - pytest.skip("Test deprecated . Detected default PHP version >= 7") - package_manager_detect = host.run(''' - source /opt/pihole/basic-install.sh - package_manager_detect - select_rpm_php - ''') - expected_stdout = info_box + (' User opt-out of PHP 7 upgrade on CentOS. ' - 'Deprecated PHP may be in use.') - assert expected_stdout in package_manager_detect.stdout - remi_package = host.package('remi-release') - assert not remi_package.is_installed - - -def test_php_version_lt_7_detected_upgrade_user_optout_centos(host): - ''' - confirms installer behavior when user opt-out to upgrade to PHP7 via REMI - ''' - # first we will install the default php version to test installer behavior - php_install = host.run('yum install -y php') - assert php_install.rc == 0 - php_package = host.package('php') - default_centos_php_version = php_package.version.split('.')[0] - if int(default_centos_php_version) >= 7: # PHP7 is supported/recommended - pytest.skip("Test deprecated . Detected default PHP version >= 7") - # Whiptail dialog returns Cancel for user prompt - mock_command('whiptail', {'*': ('', '1')}, host) - package_manager_detect = host.run(''' - source /opt/pihole/basic-install.sh - package_manager_detect - select_rpm_php - ''') - expected_stdout = info_box + (' User opt-out of PHP 7 upgrade on CentOS. ' - 'Deprecated PHP may be in use.') - assert expected_stdout in package_manager_detect.stdout - remi_package = host.package('remi-release') - assert not remi_package.is_installed - - -def test_php_version_lt_7_detected_upgrade_user_optin_centos(host): - ''' - confirms installer behavior when user opt-in to upgrade to PHP7 via REMI - ''' - # first we will install the default php version to test installer behavior - php_install = host.run('yum install -y php') - assert php_install.rc == 0 - php_package = host.package('php') - default_centos_php_version = php_package.version.split('.')[0] - if int(default_centos_php_version) >= 7: # PHP7 is supported/recommended - pytest.skip("Test deprecated . Detected default PHP version >= 7") - # Whiptail dialog returns Continue for user prompt - mock_command('whiptail', {'*': ('', '0')}, host) - package_manager_detect = host.run(''' - source /opt/pihole/basic-install.sh - package_manager_detect - select_rpm_php - install_dependent_packages PIHOLE_WEB_DEPS[@] - ''') - expected_stdout = info_box + (' User opt-out of PHP 7 upgrade on CentOS. ' - 'Deprecated PHP may be in use.') - assert expected_stdout not in package_manager_detect.stdout - expected_stdout = info_box + (' Enabling Remi\'s RPM repository ' - '(https://rpms.remirepo.net)') - assert expected_stdout in package_manager_detect.stdout - expected_stdout = tick_box + (' Remi\'s RPM repository has ' - 'been enabled for PHP7') - assert expected_stdout in package_manager_detect.stdout - remi_package = host.package('remi-release') - assert remi_package.is_installed - updated_php_package = host.package('php') - updated_php_version = updated_php_package.version.split('.')[0] - assert int(updated_php_version) == 7 diff --git a/test/test_centos_fedora_common_support.py b/test/test_centos_fedora_common_support.py index a2a13048fc..7e0bae4e7b 100644 --- a/test/test_centos_fedora_common_support.py +++ b/test/test_centos_fedora_common_support.py @@ -6,60 +6,70 @@ def mock_selinux_config(state, host): - ''' + """ Creates a mock SELinux config file with expected content - ''' + """ # validate state string - valid_states = ['enforcing', 'permissive', 'disabled'] + valid_states = ["enforcing", "permissive", "disabled"] assert state in valid_states # getenforce returns the running state of SELinux - mock_command('getenforce', {'*': (state.capitalize(), '0')}, host) + 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): - ''' + """ 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' + """ + ) + expected_stdout = cross_box + " Current SELinux: enforcing" assert expected_stdout in check_selinux.stdout - expected_stdout = 'SELinux Enforcing detected, exiting installer' + expected_stdout = "SELinux Enforcing detected, exiting installer" assert expected_stdout in check_selinux.stdout assert check_selinux.rc == 1 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' + """ + ) + expected_stdout = tick_box + " Current SELinux: permissive" assert expected_stdout in check_selinux.stdout assert check_selinux.rc == 0 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' + """ + ) + expected_stdout = tick_box + " Current SELinux: disabled" assert expected_stdout in check_selinux.stdout assert check_selinux.rc == 0 diff --git a/test/test_fedora_support.py b/test/test_fedora_support.py index 63fde90e0d..e7d31a5dbf 100644 --- a/test/test_fedora_support.py +++ b/test/test_fedora_support.py @@ -1,16 +1,15 @@ def test_epel_and_remi_not_installed_fedora(host): - ''' + """ confirms installer does not attempt to install EPEL/REMI repositories on Fedora - ''' - package_manager_detect = host.run(''' + """ + package_manager_detect = host.run( + """ source /opt/pihole/basic-install.sh package_manager_detect - select_rpm_php - ''') - assert package_manager_detect.stdout == '' + """ + ) + assert package_manager_detect.stdout == "" - epel_package = host.package('epel-release') + epel_package = host.package("epel-release") assert not epel_package.is_installed - remi_package = host.package('remi-release') - assert not remi_package.is_installed diff --git a/test/tox.centos_7.ini b/test/tox.centos_7.ini deleted file mode 100644 index 319465dd1a..0000000000 --- a/test/tox.centos_7.ini +++ /dev/null @@ -1,8 +0,0 @@ -[tox] -envlist = py38 - -[testenv] -whitelist_externals = docker -deps = -rrequirements.txt -commands = docker build -f _centos_7.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 ./test_centos_common_support.py ./test_centos_7_support.py diff --git a/test/tox.centos_8.ini b/test/tox.centos_8.ini index c792628994..dca77c93bc 100644 --- a/test/tox.centos_8.ini +++ b/test/tox.centos_8.ini @@ -1,8 +1,8 @@ [tox] -envlist = py38 +envlist = py3 -[testenv] -whitelist_externals = docker +[testenv:py3] +allowlist_externals = docker deps = -rrequirements.txt -commands = docker build -f _centos_8.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 ./test_centos_common_support.py ./test_centos_8_support.py +commands = docker buildx build --load --progress plain -f _centos_8.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 ./test_centos_common_support.py diff --git a/test/tox.centos_9.ini b/test/tox.centos_9.ini new file mode 100644 index 0000000000..a69c336afd --- /dev/null +++ b/test/tox.centos_9.ini @@ -0,0 +1,8 @@ +[tox] +envlist = py3 + +[testenv:py3] +allowlist_externals = docker +deps = -rrequirements.txt +commands = docker buildx build --load --progress plain -f _centos_9.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 ./test_centos_common_support.py diff --git a/test/tox.debian_10.ini b/test/tox.debian_10.ini index 3b182cdccb..f107300f24 100644 --- a/test/tox.debian_10.ini +++ b/test/tox.debian_10.ini @@ -1,8 +1,8 @@ [tox] -envlist = py38 +envlist = py3 -[testenv] -whitelist_externals = docker +[testenv:py3] +allowlist_externals = docker deps = -rrequirements.txt -commands = docker build -f _debian_10.Dockerfile -t pytest_pihole:test_container ../ +commands = docker buildx build --load --progress plain -f _debian_10.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.debian_11.ini b/test/tox.debian_11.ini index c7e41a91cd..c38a15fb90 100644 --- a/test/tox.debian_11.ini +++ b/test/tox.debian_11.ini @@ -1,8 +1,8 @@ [tox] -envlist = py38 +envlist = py3 -[testenv] -whitelist_externals = docker +[testenv:py3] +allowlist_externals = docker deps = -rrequirements.txt -commands = docker build -f _debian_11.Dockerfile -t pytest_pihole:test_container ../ +commands = docker buildx build --load --progress plain -f _debian_11.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.debian_12.ini b/test/tox.debian_12.ini new file mode 100644 index 0000000000..ee70e8bd7d --- /dev/null +++ b/test/tox.debian_12.ini @@ -0,0 +1,8 @@ +[tox] +envlist = py3 + +[testenv:py3] +allowlist_externals = docker +deps = -rrequirements.txt +commands = docker buildx build --load --progress plain -f _debian_12.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.debian_9.ini b/test/tox.debian_9.ini deleted file mode 100644 index 56b9d37f09..0000000000 --- a/test/tox.debian_9.ini +++ /dev/null @@ -1,8 +0,0 @@ -[tox] -envlist = py38 - -[testenv] -whitelist_externals = docker -deps = -rrequirements.txt -commands = docker build -f _debian_9.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_34.ini b/test/tox.fedora_38.ini similarity index 55% rename from test/tox.fedora_34.ini rename to test/tox.fedora_38.ini index 2685698467..0aa7612e11 100644 --- a/test/tox.fedora_34.ini +++ b/test/tox.fedora_38.ini @@ -1,8 +1,8 @@ [tox] -envlist = py38 +envlist = py3 [testenv] -whitelist_externals = docker +allowlist_externals = docker deps = -rrequirements.txt -commands = docker build -f _fedora_34.Dockerfile -t pytest_pihole:test_container ../ +commands = docker buildx build --load --progress plain -f _fedora_38.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 ./test_fedora_support.py diff --git a/test/tox.fedora_33.ini b/test/tox.fedora_39.ini similarity index 55% rename from test/tox.fedora_33.ini rename to test/tox.fedora_39.ini index b17bd56323..7a53837175 100644 --- a/test/tox.fedora_33.ini +++ b/test/tox.fedora_39.ini @@ -1,8 +1,8 @@ [tox] -envlist = py38 +envlist = py3 [testenv] -whitelist_externals = docker +allowlist_externals = docker deps = -rrequirements.txt -commands = docker build -f _fedora_33.Dockerfile -t pytest_pihole:test_container ../ +commands = docker buildx build --load --progress plain -f _fedora_39.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 ./test_fedora_support.py diff --git a/test/tox.ubuntu_16.ini b/test/tox.ubuntu_16.ini deleted file mode 100644 index f8f6e92a73..0000000000 --- a/test/tox.ubuntu_16.ini +++ /dev/null @@ -1,8 +0,0 @@ -[tox] -envlist = py38 - -[testenv] -whitelist_externals = docker -deps = -rrequirements.txt -commands = docker build -f _ubuntu_16.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.ubuntu_18.ini b/test/tox.ubuntu_18.ini deleted file mode 100644 index a2513dfdcd..0000000000 --- a/test/tox.ubuntu_18.ini +++ /dev/null @@ -1,8 +0,0 @@ -[tox] -envlist = py38 - -[testenv] -whitelist_externals = docker -deps = -rrequirements.txt -commands = docker build -f _ubuntu_18.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.ubuntu_20.ini b/test/tox.ubuntu_20.ini index fb3d20d7b4..49a6153e2a 100644 --- a/test/tox.ubuntu_20.ini +++ b/test/tox.ubuntu_20.ini @@ -1,8 +1,8 @@ [tox] -envlist = py38 +envlist = py3 -[testenv] -whitelist_externals = docker +[testenv:py3] +allowlist_externals = docker deps = -rrequirements.txt -commands = docker build -f _ubuntu_20.Dockerfile -t pytest_pihole:test_container ../ +commands = docker buildx build --load --progress plain -f _ubuntu_20.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.ubuntu_21.ini b/test/tox.ubuntu_21.ini deleted file mode 100644 index 070d3a7218..0000000000 --- a/test/tox.ubuntu_21.ini +++ /dev/null @@ -1,8 +0,0 @@ -[tox] -envlist = py38 - -[testenv] -whitelist_externals = docker -deps = -rrequirements.txt -commands = docker build -f _ubuntu_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.ubuntu_22.ini b/test/tox.ubuntu_22.ini new file mode 100644 index 0000000000..8014d6d6ea --- /dev/null +++ b/test/tox.ubuntu_22.ini @@ -0,0 +1,8 @@ +[tox] +envlist = py3 + +[testenv:py3] +allowlist_externals = docker +deps = -rrequirements.txt +commands = docker buildx build --load --progress plain -f _ubuntu_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.ubuntu_23.ini b/test/tox.ubuntu_23.ini new file mode 100644 index 0000000000..767ed9ef56 --- /dev/null +++ b/test/tox.ubuntu_23.ini @@ -0,0 +1,8 @@ +[tox] +envlist = py3 + +[testenv:py3] +allowlist_externals = docker +deps = -rrequirements.txt +commands = docker buildx build --load --progress plain -f _ubuntu_23.Dockerfile -t pytest_pihole:test_container ../ + pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py