diff --git a/.codespellignore b/.codespellignore new file mode 100644 index 0000000000..501a3d6732 --- /dev/null +++ b/.codespellignore @@ -0,0 +1,3 @@ +doubleclick +wan +nwe diff --git a/.editorconfig b/.editorconfig index ee415d1f82..714ed210b9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -# EditorConfig is awesome: http://EditorConfig.org +# EditorConfig is awesome: https://editorconfig.org/ # top-most EditorConfig file root = true @@ -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/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 3a75dc1213..0000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,4 +0,0 @@ -# These are supported funding model platforms - -patreon: pihole -custom: https://pi-hole.net/donate diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 4a9c585ad1..0000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,37 +0,0 @@ -**In raising this issue, I confirm the following:** `{please fill the checkboxes, e.g: [X]}` - -- [] I have read and understood the [contributors guide](https://github.com/pi-hole/pi-hole/blob/master/CONTRIBUTING.md). -- [] The issue I am reporting can be *replicated*. -- [] The issue I am reporting isn't a duplicate (see [FAQs](https://github.com/pi-hole/pi-hole/wiki/FAQs), [closed issues](https://github.com/pi-hole/pi-hole/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), and [open issues](https://github.com/pi-hole/pi-hole/issues)). - -**How familiar are you with the the source code relevant to this issue?:** - -`{Replace this with a number from 1 to 10. 1 being not familiar, and 10 being very familiar}` - ---- -**Expected behaviour:** - -`{A detailed description of what you expect to see}` - -**Actual behaviour:** - -`{A detailed description and/or screenshots of what you do see}` - -**Steps to reproduce:** - -`{Detailed steps of how we can reproduce this}` - -**Debug token provided by [uploading `pihole -d` log](https://discourse.pi-hole.net/t/the-pihole-command-with-examples/738#debug):** - -`{Alphanumeric token}` - -**Troubleshooting undertaken, and/or other relevant information:** - -`{Steps of what you have done to fix this}` - -> * `{Please delete this quoted section when opening your issue}` -> * You must follow the template instructions. Failure to do so will result in your issue being closed. -> * Please [submit any feature requests here](https://discourse.pi-hole.net/c/feature-requests), so it is votable and trackable by the community. -> * Please respect that Pi-hole is developed by volunteers, who can only reply in their spare time. -> * Detail helps us understand and resolve an issue quicker, but please ensure it's relevant. -> * _This template was created based on the work of [`udemy-dl`](https://github.com/nishad/udemy-dl/blob/master/LICENSE)._ diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 41564b6544..0000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,31 +0,0 @@ -**By submitting this pull request, I confirm the following:** -*please fill any appropriate checkboxes, e.g: [X]* - -- [ ] I have read and understood the [contributors guide](https://github.com/pi-hole/pi-hole/blob/master/CONTRIBUTING.md), as well as this entire template. -- [ ] I have made only one major change in my proposed changes. -- [ ] I have commented my proposed changes within the code. -- [ ] I have tested my proposed changes, and have included unit tests where possible. -- [ ] I am willing to help maintain this change if there are issues with it later. -- [ ] I give this submission freely and claim no ownership. -- [ ] It is compatible with the [EUPL 1.2 license](https://opensource.org/licenses/EUPL-1.1) -- [ ] I have squashed any insignificant commits. ([`git rebase`](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html)) - -Please make sure you [Sign Off](https://github.com/pi-hole/pi-hole/wiki/How-to-signoff-your-commits.) all commits. Pi-hole enforces the [DCO](https://github.com/pi-hole/pi-hole/wiki/Contributing-to-the-project). - ---- -**What does this PR aim to accomplish?:** -*A detailed description, screenshots (if necessary), as well as links to any relevant GitHub issues* - - -**How does this PR accomplish the above?:** -*A detailed description (such as a changelog) and screenshots (if necessary) of the implemented fix* - - -**What documentation changes (if any) are needed to support this PR?:** -*A detailed list of any necessary changes* - - ---- -* You must follow the template instructions. Failure to do so will result in your pull request being closed. -* Please respect that Pi-hole is developed by volunteers, who can only reply in their spare time. - diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..f91a9b82ef --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + day: saturday + time: "10:00" + open-pull-requests-limit: 10 + target-branch: development + reviewers: + - "pi-hole/core-maintainers" diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000000..2e8776e999 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,7 @@ +changelog: + exclude: + labels: + - internal + authors: + - dependabot + - github-actions diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000000..b0ebb90e3d --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,40 @@ +name: "CodeQL" + +on: + push: + branches: + - master + - development + pull_request: + branches: + - master + - development + schedule: + - cron: '32 11 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + permissions: + actions: read + contents: read + security-events: write + + steps: + - + name: Checkout repository + uses: actions/checkout@v3.1.0 + # Initializes the CodeQL tools for scanning. + - + name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: 'python' + - + name: Autobuild + uses: github/codeql-action/autobuild@v2 + - + name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000..a17d5a94c2 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,26 @@ +name: Mark stale issues + +on: + schedule: + - cron: '0 8 * * *' + workflow_dispatch: + +jobs: + stale: + + runs-on: ubuntu-latest + permissions: + issues: write + + steps: + - uses: actions/stale@v6.0.1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + days-before-stale: 30 + days-before-close: 5 + stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Please comment or update this issue or it will be closed in 5 days.' + stale-issue-label: 'stale' + 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' diff --git a/.github/workflows/sync-back-to-dev.yml b/.github/workflows/sync-back-to-dev.yml new file mode 100644 index 0000000000..f689ae364c --- /dev/null +++ b/.github/workflows/sync-back-to-dev.yml @@ -0,0 +1,27 @@ +name: Sync Back to Development + +on: + push: + branches: + - master + +jobs: + sync-branches: + runs-on: ubuntu-latest + name: Syncing branches + steps: + - name: Checkout + uses: actions/checkout@v3.1.0 + - name: Opening pull request + id: pull + uses: tretuna/sync-branches@1.4.0 + with: + 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.1.3 + 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 new file mode 100644 index 0000000000..daa18c8578 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,76 @@ +name: Test Supported Distributions + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: read + +jobs: + smoke-tests: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3.1.0 + + - 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-tests + strategy: + fail-fast: false + matrix: + distro: + [ + debian_10, + debian_11, + ubuntu_20, + ubuntu_22, + centos_8, + centos_9, + fedora_35, + fedora_36, + ] + env: + DISTRO: ${{matrix.distro}} + steps: + - name: Checkout repository + uses: actions/checkout@v3.1.0 + + - name: Set up Python 3.10 + uses: actions/setup-python@v4.3.0 + with: + python-version: "3.10" + + - name: Install dependencies + run: pip install -r test/requirements.txt + + - name: Test with tox + run: tox -c test/tox.${DISTRO}.ini diff --git a/.gitignore b/.gitignore index 1e80dfb876..8016472b10 100644 --- a/.gitignore +++ b/.gitignore @@ -7,70 +7,6 @@ __pycache__ .tox .eggs *.egg-info - - -# Created by https://www.gitignore.io/api/jetbrains+iml - -### JetBrains+iml ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# All idea files, with execptions -.idea -!.idea/codeStyles/* -!.idea/codeStyleSettings.xml - - -# Sensitive or high-churn files: -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.xml -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml - -# Gradle: -.idea/**/gradle.xml -.idea/**/libraries - -# CMake -cmake-build-debug/ - -# Mongo Explorer plugin: -.idea/**/mongoSettings.xml - -## File-based project format: -*.iws - -## Plugin-specific files: - -# IntelliJ -/out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Ruby plugin and RubyMine -/.rakeTasks - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -### JetBrains+iml Patch ### -# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 - +.idea/ *.iml -.idea/misc.xml -*.ipr - -# End of https://www.gitignore.io/api/jetbrains+iml +.vscode/ diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml deleted file mode 100644 index 6ad75d683b..0000000000 --- a/.idea/codeStyleSettings.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 79a710fd98..0000000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 79ee123c2b..0000000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.stickler.yml b/.stickler.yml index 0eaae8cb22..5fdbbf1ec1 100644 --- a/.stickler.yml +++ b/.stickler.yml @@ -1,6 +1,10 @@ +--- linters: shellcheck: shell: bash phpcs: - csslint: flake8: + max-line-length: 120 + yamllint: + config: ./.yamllint.conf + remarklint: diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fa525e01c6..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -sudo: required -services: - - docker -language: python -python: - - "2.7" -install: - - pip install -r requirements.txt - -script: - # tox.ini handles setup, ordering of docker build first, and then run tests - - tox 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 e32b500e2e..1ea98df296 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,37 +2,4 @@ Please read and understand the contribution guide before creating an issue or pull request. -## Etiquette - -- Our goal for Pi-hole is **stability before features**. This means we focus on squashing critical bugs before adding new features. Often, we can do both in tandem, but bugs will take priority over a new feature. -- Pi-hole is open source and [powered by donations](https://pi-hole.net/donate/), and as such, we give our **free time** to build, maintain, and **provide user support** for this project. It would be extremely unfair for us to suffer abuse or anger for our hard work, so please take a moment to consider that. -- Please be considerate towards the developers and other users when raising issues or presenting pull requests. -- Respect our decision(s), and do not be upset or abusive if your submission is not used. - -## Viability - -When requesting or submitting new features, first consider whether it might be useful to others. Open source projects are used by many people, who may have entirely different needs to your own. Think about whether or not your feature is likely to be used by other users of the project. - -## Procedure - -**Before filing an issue:** - -- Attempt to replicate and **document** the problem, to ensure that it wasn't a coincidental incident. -- Check to make sure your feature suggestion isn't already present within the project. -- Check the pull requests tab to ensure that the bug doesn't have a fix in progress. -- Check the pull requests tab to ensure that the feature isn't already in progress. - -**Before submitting a pull request:** - -- Check the codebase to ensure that your feature doesn't already exist. -- Check the pull requests to ensure that another person hasn't already submitted the feature or fix. -- Read and understand the [DCO guidelines](https://github.com/pi-hole/pi-hole/wiki/Contributing-to-the-project) for the project. - -## Technical Requirements - -- Submit Pull Requests to the **development branch only**. -- Before Submitting your Pull Request, merge `development` with your new branch and fix any conflicts. (Make sure you don't break anything in development!) -- Please use the [Google Style Guide for Shell](https://google.github.io/styleguide/shell.xml) for your code submission styles. -- Commit Unix line endings. -- Please use the Pi-hole brand: **Pi-hole** (Take a special look at the capitalized 'P' and a low 'h' with a hyphen) -- (Optional fun) keep to the theme of Star Trek/black holes/gravity. +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 fb2179ebbd..adfd345017 100644 --- a/README.md +++ b/README.md @@ -1,213 +1,169 @@ + + +# +

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

-The Pi-hole[®](https://pi-hole.net/trademark-rules-and-brand-guidelines/) is a [DNS sinkhole](https://en.wikipedia.org/wiki/DNS_Sinkhole) that protects your devices from unwanted content, without installing any client-side software. + + +The Pi-hole® is a [DNS sinkhole](https://en.wikipedia.org/wiki/DNS_Sinkhole) that protects your devices from unwanted content without installing any client-side software. -- **Easy-to-install**: our versatile installer walks you through the process, and [takes less than ten minutes](https://www.youtube.com/watch?v=vKWjx1AQYgs) +- **Easy-to-install**: our dialogs walk you through the simple installation process in less than ten minutes - **Resolute**: content is blocked in _non-browser locations_, such as ad-laden mobile apps and smart TVs - **Responsive**: seamlessly speeds up the feel of everyday browsing by caching DNS queries -- **Lightweight**: runs smoothly with [minimal hardware and software requirements](https://discourse.pi-hole.net/t/hardware-software-requirements/273) -- **Robust**: a command line interface that is quality assured for interoperability +- **Lightweight**: runs smoothly with [minimal hardware and software requirements](https://docs.pi-hole.net/main/prerequisites/) +- **Robust**: a command-line interface that is quality assured for interoperability - **Insightful**: a beautiful responsive Web Interface dashboard to view and control your Pi-hole -- **Versatile**: can optionally function as a [DHCP server](https://discourse.pi-hole.net/t/how-do-i-use-pi-holes-built-in-dhcp-server-and-why-would-i-want-to/3026), ensuring *all* your devices are protected automatically +- **Versatile**: can optionally function as a [DHCP server](https://discourse.pi-hole.net/t/how-do-i-use-pi-holes-built-in-dhcp-server-and-why-would-i-want-to/3026), ensuring _all_ your devices are protected automatically - **Scalable**: [capable of handling hundreds of millions of queries](https://pi-hole.net/2017/05/24/how-much-traffic-can-pi-hole-handle/) when installed on server-grade hardware - **Modern**: blocks ads over both IPv4 and IPv6 -- **Free**: open source software which helps ensure _you_ are the sole person in control of your privacy +- **Free**: open source software that helps ensure _you_ are the sole person in control of your privacy ----- -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/c558a0f8d7124c99b02b84f0f5564238)](https://www.codacy.com/app/Pi-hole/pi-hole?utm_source=github.com&utm_medium=referral&utm_content=pi-hole/pi-hole&utm_campaign=Badge_Grade) -[![Build Status](https://travis-ci.org/pi-hole/pi-hole.svg?branch=development)](https://travis-ci.org/pi-hole/pi-hole) -[![BountySource](https://www.bountysource.com/badge/tracker?tracker_id=3011939)](https://www.bountysource.com/trackers/3011939-pi-hole-pi-hole?utm_source=3011939&utm_medium=shield&utm_campaign=TRACKER_BADGE) ## One-Step Automated Install + Those who want to get started quickly and conveniently may install Pi-hole using the following command: -#### `curl -sSL https://install.pi-hole.net | bash` +### `curl -sSL https://install.pi-hole.net | bash` ## Alternative Install Methods -[Piping to `bash` is controversial](https://pi-hole.net/2016/07/25/curling-and-piping-to-bash), as it prevents you from [reading code that is about to run](https://github.com/pi-hole/pi-hole/blob/master/automated%20install/basic-install.sh) on your system. Therefore, we provide these alternative installation methods which allow code review before installation: + +Piping to `bash` is [controversial](https://pi-hole.net/2016/07/25/curling-and-piping-to-bash), as it prevents you from [reading code that is about to run](https://github.com/pi-hole/pi-hole/blob/master/automated%20install/basic-install.sh) on your system. Therefore, we provide these alternative installation methods which allow code review before installation: ### Method 1: Clone our repository and run -``` + +```bash git clone --depth 1 https://github.com/pi-hole/pi-hole.git Pi-hole cd "Pi-hole/automated install/" sudo bash basic-install.sh ``` ### Method 2: Manually download the installer and run -``` + +```bash wget -O basic-install.sh https://install.pi-hole.net sudo bash basic-install.sh ``` -## Post-install: Make your network take advantage of Pi-hole +### Method 3: Using Docker to deploy Pi-hole + +Please refer to the [Pi-hole docker repo](https://github.com/pi-hole/docker-pi-hole) to use the Official Docker Images. -Once the installer has been run, you will need to [configure your router to have **DHCP clients use Pi-hole as their DNS server**](https://discourse.pi-hole.net/t/how-do-i-configure-my-devices-to-use-pi-hole-as-their-dns-server/245) which ensures that all devices connecting to your network will have content blocked without any further intervention. +## [Post-install: Make your network take advantage of Pi-hole](https://docs.pi-hole.net/main/post-install/) -If your router does not support setting the DNS server, you can [use Pi-hole's built-in DHCP server](https://discourse.pi-hole.net/t/how-do-i-use-pi-holes-built-in-dhcp-server-and-why-would-i-want-to/3026); just be sure to disable DHCP on your router first (if it has that feature available). +Once the installer has been run, you will need to [configure your router to have **DHCP clients use Pi-hole as their DNS server**](https://discourse.pi-hole.net/t/how-do-i-configure-my-devices-to-use-pi-hole-as-their-dns-server/245). This router configuration will ensure that all devices connecting to your network will have content blocked without any further intervention. -As a last resort, you can always manually set each device to use Pi-hole as their DNS server. +If your router does not support setting the DNS server, you can [use Pi-hole's built-in DHCP server](https://discourse.pi-hole.net/t/how-do-i-use-pi-holes-built-in-dhcp-server-and-why-would-i-want-to/3026); be sure to disable DHCP on your router first (if it has that feature available). + +As a last resort, you can manually set each device to use Pi-hole as their DNS server. ----- -## Pi-hole is free, but powered by your support -There are many reoccurring costs involved with maintaining free, open source, and privacy-respecting software; expenses which [our volunteer developers](https://github.com/orgs/pi-hole/people) pitch in to cover out-of-pocket. This is just one example of how strongly we feel about our software, as well as the importance of keeping it maintained. +## Pi-hole is free but powered by your support + +There are many reoccurring costs involved with maintaining free, open-source, and privacy-respecting software; expenses which [our volunteer developers](https://github.com/orgs/pi-hole/people) pitch in to cover out-of-pocket. This is just one example of how strongly we feel about our software and the importance of keeping it maintained. Make no mistake: **your support is absolutely vital to help keep us innovating!** -### Donations -Sending a donation using our links below is **extremely helpful** in offsetting a portion of our monthly expenses: +### [Donations](https://pi-hole.net/donate) -- PP Donate via PayPal
-- BTC [Bitcoin, Bitcoin Cash, Ethereum, Litecoin](https://commerce.coinbase.com/checkout/dd304d04-f324-4a77-931b-0db61c77a41b) +Donating using our Sponsor Button is **extremely helpful** in offsetting a portion of our monthly expenses: ### Alternative support -If you'd rather not [donate](https://pi-hole.net/donate/) (_which is okay!_), there are other ways you can help support us: -- [Patreon](https://patreon.com/pihole) _Become a patron for rewards_ -- [Digital Ocean](http://www.digitalocean.com/?refcode=344d234950e1) _affiliate link_ + +If you'd rather not donate (_which is okay!_), there are other ways you can help support us: + +- [GitHub Sponsors](https://github.com/sponsors/pi-hole/) +- [Patreon](https://patreon.com/pihole) +- [Hetzner Cloud](https://hetzner.cloud/?ref=7aceisRX3AzA) _affiliate link_ +- [Digital Ocean](https://www.digitalocean.com/?refcode=344d234950e1) _affiliate link_ - [Stickermule](https://www.stickermule.com/unlock?ref_id=9127301701&utm_medium=link&utm_source=invite) _earn a $10 credit after your first purchase_ -- [Pi-hole Swag Store](https://pi-hole.net/shop/) _affiliate link_ -- [Amazon](http://www.amazon.com/exec/obidos/redirect-home/pihole09-20) _affiliate link_ -- [DNS Made Easy](https://cp.dnsmadeeasy.com/u/133706) _affiliate link_ -- [Vultr](http://www.vultr.com/?ref=7190426) _affiliate link_ -- Spreading the word about our software, and how you have benefited from it +- [Amazon US](https://www.amazon.com/exec/obidos/redirect-home/pihole09-20) _affiliate link_ +- Spreading the word about our software and how you have benefited from it ### Contributing via GitHub + We welcome _everyone_ to contribute to issue reports, suggest new features, and create pull requests. -If you have something to add - anything from a typo through to a whole new feature, we're happy to check it out! Just make sure to fill out our template when submitting your request; the questions that it asks will help the volunteers quickly understand what you're aiming to achieve. +If you have something to add - anything from a typo through to a whole new feature, we're happy to check it out! Just make sure to fill out our template when submitting your request; the questions it asks will help the volunteers quickly understand what you're aiming to achieve. You'll find that the [install script](https://github.com/pi-hole/pi-hole/blob/master/automated%20install/basic-install.sh) and the [debug script](https://github.com/pi-hole/pi-hole/blob/master/advanced/Scripts/piholeDebug.sh) have an abundance of comments, which will help you better understand how Pi-hole works. They're also a valuable resource to those who want to learn how to write scripts or code a program! We encourage anyone who likes to tinker to read through it and submit a pull request for us to review. -### Presentations about Pi-hole -Word-of-mouth continues to help our project grow immensely, and so we are helping make this easier for people. - -If you are going to be presenting Pi-hole at a conference, meetup or even a school project, [get in touch with us](https://pi-hole.net/2017/05/17/giving-a-presentation-on-pi-hole-contact-us-first-for-some-goodies-and-support/) so we can hook you up with free swag to hand out to your audience! - ----- ## Getting in touch with us -While we are primarily reachable on our Discourse User Forum, we can also be found on a variety of social media outlets. **Please be sure to check the FAQ's** before starting a new discussion, as we do not have the spare time to reply to every request for assistance. - - ------ +While we are primarily reachable on our [Discourse User Forum](https://discourse.pi-hole.net/), we can also be found on various social media outlets. -## Breakdown of Features -### The Command Line Interface -The `pihole` command has all the functionality necessary to be able to fully administer the Pi-hole, without the need of the Web Interface. It's fast, user-friendly, and auditable by anyone with an understanding of `bash`. +**Please be sure to check the FAQs** before starting a new discussion, as we do not have the spare time to reply to every request for assistance. -Pi-hole Blacklist Demo +- [Frequently Asked Questions](https://discourse.pi-hole.net/c/faqs) +- [Feature Requests](https://discourse.pi-hole.net/c/feature-requests?order=votes) +- [Reddit](https://www.reddit.com/r/pihole/) +- [Twitter](https://twitter.com/The_Pi_hole) -Some notable features include: -* [Whitelisting, Blacklisting and Wildcards](https://github.com/pi-hole/pi-hole/wiki/Core-Function-Breakdown#whitelisting-blacklisting-and-wildcards) -* [Debugging utility](https://github.com/pi-hole/pi-hole/wiki/Core-Function-Breakdown#debugger) -* [Viewing the live log file](https://github.com/pi-hole/pi-hole/wiki/Core-Function-Breakdown#tail) -* [Real-time Statistics via `ssh`](https://github.com/pi-hole/pi-hole/wiki/Core-Function-Breakdown#chronometer) or [your TFT LCD screen](http://www.amazon.com/exec/obidos/ASIN/B00ID39LM4/pihole09-20) -* [Updating Ad Lists](https://github.com/pi-hole/pi-hole/wiki/Core-Function-Breakdown#gravity) -* [Querying Ad Lists for blocked domains](https://github.com/pi-hole/pi-hole/wiki/Core-Function-Breakdown#query) -* [Enabling and Disabling Pi-hole](https://github.com/pi-hole/pi-hole/wiki/Core-Function-Breakdown#enable--disable) -* ... and *many* more! +----- -You can read our [Core Feature Breakdown](https://github.com/pi-hole/pi-hole/wiki/Core-Function-Breakdown), as well as read up on [example usage](https://discourse.pi-hole.net/t/the-pihole-command-with-examples/738) for more information. +## Breakdown of Features -### The Web Interface Dashboard -This [optional dashboard](https://github.com/pi-hole/AdminLTE) allows you to view stats, change settings, and configure your Pi-hole. It's the power of the Command Line Interface, with none of the learning curve! +### [Faster-than-light Engine](https://github.com/pi-hole/ftl) -Pi-hole Dashboard +[FTLDNS](https://github.com/pi-hole/ftl) is a lightweight, purpose-built daemon used to provide statistics needed for the Web Interface, and its API can be easily integrated into your own projects. As the name implies, FTLDNS does this all _very quickly_! -Some notable features include: -* Mobile friendly interface -* Password protection -* Detailed graphs and doughnut charts -* Top lists of domains and clients -* A filterable and sortable query log -* Long Term Statistics to view data over user-defined time ranges -* The ability to easily manage and configure Pi-hole features -* ... and all the main features of the Command Line Interface! +Some of the statistics you can integrate include: -There are several ways to [access the dashboard](https://discourse.pi-hole.net/t/how-do-i-access-pi-holes-dashboard-admin-interface/3168): +- Total number of domains being blocked +- Total number of DNS queries today +- Total number of ads blocked today +- Percentage of ads blocked +- Unique domains +- Queries forwarded (to your chosen upstream DNS server) +- Queries cached +- Unique clients -1. `http:///admin/` -2. `http://pi.hole/admin/` (when using Pi-hole as your DNS server) -3. `http://pi.hole/` (when using Pi-hole as your DNS server) +Access the API via [`telnet`](https://github.com/pi-hole/FTL), the Web (`admin/api.php`) and Command Line (`pihole -c -j`). You can find out [more details over here](https://discourse.pi-hole.net/t/pi-hole-api/1863). -## Faster-than-light Engine -FTLDNS is a lightweight, purpose-built daemon used to provide statistics needed for the Web Interface, and its API can be easily integrated into your own projects. As the name implies, FTLDNS does this all *very quickly*! +### The Command-Line Interface -Some of the statistics you can integrate include: -* Total number of domains being blocked -* Total number of DNS queries today -* Total number of ads blocked today -* Percentage of ads blocked -* Unique domains -* Queries forwarded (to your chosen upstream DNS server) -* Queries cached -* Unique clients +The [pihole](https://docs.pi-hole.net/core/pihole-command/) command has all the functionality necessary to fully administer the Pi-hole, without the need for the Web Interface. It's fast, user-friendly, and auditable by anyone with an understanding of `bash`. -The API can be accessed via [`telnet`](https://github.com/pi-hole/FTL), the Web (`admin/api.php`) and Command Line (`pihole -c -j`). You can out find [more details over here](https://discourse.pi-hole.net/t/pi-hole-api/1863). +Some notable features include: ------ +- [Whitelisting, Blacklisting, and Regex](https://docs.pi-hole.net/core/pihole-command/#whitelisting-blacklisting-and-regex) +- [Debugging utility](https://docs.pi-hole.net/core/pihole-command/#debugger) +- [Viewing the live log file](https://docs.pi-hole.net/core/pihole-command/#tail) +- [Updating Ad Lists](https://docs.pi-hole.net/core/pihole-command/#gravity) +- [Querying Ad Lists for blocked domains](https://docs.pi-hole.net/core/pihole-command/#query) +- [Enabling and Disabling Pi-hole](https://docs.pi-hole.net/core/pihole-command/#enable-disable) +- ... and _many_ more! -## The Origin Of Pi-hole -Pi-hole being an **advertising-aware DNS/Web server**, makes use of the following technologies: +You can read our [Core Feature Breakdown](https://docs.pi-hole.net/core/pihole-command/#pi-hole-core) for more information. -* [`dnsmasq`](http://www.thekelleys.org.uk/dnsmasq/doc.html) - a lightweight DNS and DHCP server -* [`curl`](https://curl.haxx.se) - A command line tool for transferring data with URL syntax -* [`lighttpd`](https://www.lighttpd.net) - web server designed and optimized for high performance -* [`php`](https://secure.php.net) - a popular general-purpose web scripting language -* [AdminLTE Dashboard](https://github.com/almasaeed2010/AdminLTE) - premium admin control panel based on Bootstrap 3.x +### The Web Interface Dashboard -While quite outdated at this point, [this original blog post about Pi-hole](https://jacobsalmela.com/2015/06/16/block-millions-ads-network-wide-with-a-raspberry-pi-hole-2-0/) goes into **great detail** about how Pi-hole was originally set up and how it works. Syntactically, it's no longer accurate, but the same basic principles and logic still apply to Pi-hole's current state. +This [optional dashboard](https://github.com/pi-hole/AdminLTE) allows you to view stats, change settings, and configure your Pi-hole. It's the power of the Command Line Interface, with none of the learning curve! ------ +Some notable features include: -## Coverage -- [Lifehacker: Turn A Raspberry Pi Into An Ad Blocker With A Single Command](https://www.lifehacker.com.au/2015/02/turn-a-raspberry-pi-into-an-ad-blocker-with-a-single-command/) (Feburary, 2015) -- [MakeUseOf: Adblock Everywhere: The Raspberry Pi-Hole Way](http://www.makeuseof.com/tag/adblock-everywhere-raspberry-pi-hole-way/) (March, 2015) -- [Catchpoint: Ad-Blocking on Apple iOS9: Valuing the End User Experience](http://blog.catchpoint.com/2015/09/14/ad-blocking-apple/) (September, 2015) -- [Security Now Netcast: Pi-hole](https://www.youtube.com/watch?v=p7-osq_y8i8&t=100m26s) (October, 2015) -- [TekThing: Raspberry Pi-Hole Makes Ads Disappear!](https://youtu.be/8Co59HU2gY0?t=2m) (December, 2015) -- [Foolish Tech Show](https://youtu.be/bYyena0I9yc?t=2m4s) (December, 2015) -- [Block Ads on All Home Devices for $53.18](https://medium.com/@robleathern/block-ads-on-all-home-devices-for-53-18-a5f1ec139693#.gj1xpgr5d) (December, 2015) -- [Pi-Hole for Ubuntu 14.04](http://www.boyter.org/2015/12/pi-hole-ubuntu-14-04/) (December, 2015) -- [MacObserver Podcast 585](https://www.macobserver.com/tmo/podcast/macgeekgab-585) (December, 2015) -- [The Defrag Show: Endoscope USB Camera, The Final [HoloLens] Vote, Adblock Pi and more](https://channel9.msdn.com/Shows/The-Defrag-Show/Defrag-Endoscope-USB-Camera-The-Final-HoloLens-Vote-Adblock-Pi-and-more?WT.mc_id=dlvr_twitter_ch9#time=20m39s) (January, 2016) -- [Adafruit: Pi-hole is a black hole for internet ads](https://blog.adafruit.com/2016/03/04/pi-hole-is-a-black-hole-for-internet-ads-piday-raspberrypi-raspberry_pi/) (March, 2016) -- [Digital Trends: 5 Fun, Easy Projects You Can Try With a $35 Raspberry Pi](https://youtu.be/QwrKlyC2kdM?t=1m42s) (March, 2016) -- [Adafruit: Raspberry Pi Quick Look at Pi Hole ad blocking server with Tony D](https://www.youtube.com/watch?v=eg4u2j1HYlI) (June, 2016) -- [Devacron: OrangePi Zero as an Ad-Block server with Pi-Hole](http://www.devacron.com/orangepi-zero-as-an-ad-block-server-with-pi-hole/) (December, 2016) -- [Linux Pro: The Hole Truth](http://www.linuxpromagazine.com/Issues/2017/200/The-sysadmin-s-daily-grind-Pi-hole) (July, 2017) -- [Adafruit: installing Pi-hole on a Pi Zero W](https://learn.adafruit.com/pi-hole-ad-blocker-with-pi-zero-w/install-pi-hole) (August, 2017) -- [CryptoAUSTRALIA: How We Tried 5 Privacy Focused Raspberry Pi Projects](https://blog.cryptoaustralia.org.au/2017/10/05/5-privacy-focused-raspberry-pi-projects/) (October, 2017) -- [CryptoAUSTRALIA: Pi-hole Workshop](https://blog.cryptoaustralia.org.au/2017/11/02/pi-hole-network-wide-ad-blocker/) (November, 2017) -- [Know How 355: Killing ads with a Raspberry Pi-Hole!](https://www.twit.tv/shows/know-how/episodes/355) (November, 2017) -- [Hobohouse: Block Advertising on your Network with Pi-hole and Raspberry Pi](https://hobo.house/2018/02/27/block-advertising-with-pi-hole-and-raspberry-pi/) (March, 2018) -- [Scott Helme: Securing DNS across all of my devices with Pi-Hole + DNS-over-HTTPS + 1.1.1.1](https://scotthelme.co.uk/securing-dns-across-all-of-my-devices-with-pihole-dns-over-https-1-1-1-1/) (April, 2018) -- [Scott Helme: Catching and dealing with naughty devices on my home network](https://scotthelme.co.uk/catching-naughty-devices-on-my-home-network/) (April, 2018) -- [Bloomberg Business Week: Brotherhood of the Ad blockers](https://www.bloomberg.com/news/features/2018-05-10/inside-the-brotherhood-of-pi-hole-ad-blockers) (May, 2018) -- [Software Engineering Daily: Interview with the creator of Pi-hole](https://softwareengineeringdaily.com/2018/05/29/pi-hole-ad-blocker-hardware-with-jacob-salmela/) (May, 2018) -- [Raspberry Pi: Block ads at home using Pi-hole and a Raspberry Pi](https://www.raspberrypi.org/blog/pi-hole-raspberry-pi/) (July, 2018) -- [Troy Hunt: Mmm... Pi-hole...](https://www.troyhunt.com/mmm-pi-hole/) (September, 2018) -- [PEBKAK Podcast: Interview With Jacob Salmela](https://www.jerseystudios.net/2018/10/11/150-pi-hole/) (October, 2018) +- Mobile-friendly interface +- Password protection +- Detailed graphs and doughnut charts +- Top lists of domains and clients +- A filterable and sortable query log +- Long Term Statistics to view data over user-defined time ranges +- The ability to easily manage and configure Pi-hole features +- ... and all the main features of the Command Line Interface! ------ +There are several ways to [access the dashboard](https://discourse.pi-hole.net/t/how-do-i-access-pi-holes-dashboard-admin-interface/3168): -## Pi-hole Projects -- [The Big Blocklist Collection](https://wally3k.github.io) -- [Pie in the Sky-Hole](https://dlaa.me/blog/post/skyhole) -- [Copernicus: Windows Tray Application](https://github.com/goldbattle/copernicus) -- [Magic Mirror with DNS Filtering](https://zonksec.com/blog/magic-mirror-dns-filtering/#dnssoftware) -- [Windows DNS Swapper](https://github.com/roots84/DNS-Swapper) +1. `http://pi.hole/admin/` (when using Pi-hole as your DNS server) +2. `http:///admin/` diff --git a/advanced/01-pihole.conf b/advanced/01-pihole.conf index cd74e186c4..677910f654 100644 --- a/advanced/01-pihole.conf +++ b/advanced/01-pihole.conf @@ -19,6 +19,7 @@ ############################################################################### addn-hosts=/etc/pihole/local.list +addn-hosts=/etc/pihole/custom.list domain-needed @@ -28,16 +29,7 @@ bogus-priv no-resolv -server=@DNS1@ -server=@DNS2@ - -interface=@INT@ - -cache-size=10000 - log-queries -log-facility=/var/log/pihole.log - -local-ttl=2 +log-facility=/var/log/pihole/pihole.log log-async diff --git a/advanced/06-rfc6761.conf b/advanced/06-rfc6761.conf new file mode 100644 index 0000000000..fcdd00108a --- /dev/null +++ b/advanced/06-rfc6761.conf @@ -0,0 +1,42 @@ +# Pi-hole: A black hole for Internet advertisements +# (c) 2021 Pi-hole, LLC (https://pi-hole.net) +# Network-wide ad blocking via your own hardware. +# +# RFC 6761 config file 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 POPULATED 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 # +# WITHIN /etc/dnsmasq.d/yourname.conf # +############################################################################### + +# RFC 6761: Caching DNS servers SHOULD recognize +# test, localhost, invalid +# names as special and SHOULD NOT attempt to look up NS records for them, or +# otherwise query authoritative DNS servers in an attempt to resolve these +# names. +server=/test/ +server=/localhost/ +server=/invalid/ + +# The same RFC requests something similar for +# 10.in-addr.arpa. 21.172.in-addr.arpa. 27.172.in-addr.arpa. +# 16.172.in-addr.arpa. 22.172.in-addr.arpa. 28.172.in-addr.arpa. +# 17.172.in-addr.arpa. 23.172.in-addr.arpa. 29.172.in-addr.arpa. +# 18.172.in-addr.arpa. 24.172.in-addr.arpa. 30.172.in-addr.arpa. +# 19.172.in-addr.arpa. 25.172.in-addr.arpa. 31.172.in-addr.arpa. +# 20.172.in-addr.arpa. 26.172.in-addr.arpa. 168.192.in-addr.arpa. +# Pi-hole implements this via the dnsmasq option "bogus-priv" (see +# 01-pihole.conf) because this also covers IPv6. + +# OpenWRT furthermore blocks bind, local, onion domains +# see https://git.openwrt.org/?p=openwrt/openwrt.git;a=blob_plain;f=package/network/services/dnsmasq/files/rfc6761.conf;hb=HEAD +# and https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml +# We do not include the ".local" rule ourselves, see https://github.com/pi-hole/pi-hole/pull/4282#discussion_r689112972 +server=/bind/ +server=/onion/ 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/COL_TABLE b/advanced/Scripts/COL_TABLE index 57aab4dd14..2d2b074bc1 100644 --- a/advanced/Scripts/COL_TABLE +++ b/advanced/Scripts/COL_TABLE @@ -1,7 +1,7 @@ -# Determine if terminal is capable of showing colours -if [[ -t 1 ]] && [[ $(tput colors) -ge 8 ]]; then +# Determine if terminal is capable of showing colors +if ([[ -t 1 ]] && [[ $(tput colors) -ge 8 ]]) || [[ "${WEBCALL}" ]]; then # Bold and underline may not show up on all clients - # If something MUST be emphasised, use both + # If something MUST be emphasized, use both COL_BOLD='' COL_ULINE='' diff --git a/advanced/Scripts/chronometer.sh b/advanced/Scripts/chronometer.sh index 1a4ce993ac..d69a56d33c 100755 --- a/advanced/Scripts/chronometer.sh +++ b/advanced/Scripts/chronometer.sh @@ -13,19 +13,23 @@ LC_NUMERIC=C # Retrieve stats from FTL engine pihole-FTL() { - ftl_port=$(cat /var/run/pihole-FTL.port 2> /dev/null) + local ftl_port LINE + # 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" # Test if connection is open if { "true" >&3; } 2> /dev/null; then - # Send command to FTL - echo -e ">$1" >&3 + # Send command to FTL and ask to quit when finished + echo -e ">$1 >quit" >&3 - # Read input + # Read input until we received an empty string and the connection is + # closed read -r -t 1 LINE <&3 - until [[ ! $? ]] || [[ "$LINE" == *"EOM"* ]]; do + until [[ -z "${LINE}" ]] && [[ ! -t 3 ]]; do echo "$LINE" >&1 read -r -t 1 LINE <&3 done @@ -72,7 +76,7 @@ printFunc() { # Remove excess characters from main text if [[ "$text_main_len" -gt "$text_main_max_len" ]]; then - # Trim text without colours + # Trim text without colors text_main_trim="${text_main_nocol:0:$text_main_max_len}" # Replace with trimmed text text_main="${text_main/$text_main_nocol/$text_main_trim}" @@ -88,7 +92,7 @@ printFunc() { [[ "$spc_num" -le 0 ]] && spc_num="0" spc=$(printf "%${spc_num}s") - #spc="${spc// /.}" # Debug: Visualise spaces + #spc="${spc// /.}" # Debug: Visualize spaces printf "%s%s$spc" "$title" "$text_main" @@ -131,7 +135,7 @@ get_init_stats() { printf "%s%02d:%02d:%02d\\n" "$days" "$hrs" "$mins" "$secs" } - # Set Colour Codes + # Set Color Codes coltable="/opt/pihole/COL_TABLE" if [[ -f "${coltable}" ]]; then source ${coltable} @@ -153,7 +157,7 @@ get_init_stats() { sys_throttle_raw=$(vgt=$(sudo vcgencmd get_throttled); echo "${vgt##*x}") - # Active Throttle Notice: http://bit.ly/2gnunOo + # Active Throttle Notice: https://bit.ly/2gnunOo if [[ "$sys_throttle_raw" != "0" ]]; then case "$sys_throttle_raw" in *0001) thr_type="${COL_YELLOW}Under Voltage";; @@ -228,15 +232,21 @@ get_sys_stats() { mapfile -t ph_ver_raw < <(pihole -v -c 2> /dev/null | sed -n 's/^.* v/v/p') if [[ -n "${ph_ver_raw[0]}" ]]; then ph_core_ver="${ph_ver_raw[0]}" - ph_lte_ver="${ph_ver_raw[1]}" - ph_ftl_ver="${ph_ver_raw[2]}" + if [[ ${#ph_ver_raw[@]} -eq 2 ]]; then + # AdminLTE not installed + ph_lte_ver="(not installed)" + ph_ftl_ver="${ph_ver_raw[1]}" + else + ph_lte_ver="${ph_ver_raw[1]}" + ph_ftl_ver="${ph_ver_raw[2]}" + fi else ph_core_ver="-1" fi sys_name=$(hostname) - [[ -n "$TEMPERATUREUNIT" ]] && temp_unit="$TEMPERATUREUNIT" || temp_unit="c" + [[ -n "$TEMPERATUREUNIT" ]] && temp_unit="${TEMPERATUREUNIT^^}" || temp_unit="C" # Get storage stats for partition mounted on / read -r -a disk_raw <<< "$(df -B1 / 2> /dev/null | awk 'END{ print $3,$2,$5 }')" @@ -269,7 +279,7 @@ get_sys_stats() { scr_lines="${scr_size[0]}" scr_cols="${scr_size[1]}" - # Determine Chronometer size behaviour + # Determine Chronometer size behavior if [[ "$scr_cols" -ge 58 ]]; then chrono_width="large" elif [[ "$scr_cols" -gt 40 ]]; then @@ -308,7 +318,7 @@ get_sys_stats() { [[ "${cpu_freq}" == *".0"* ]] && cpu_freq="${cpu_freq/.0/}" fi - # Determine colour for temperature + # Determine color for temperature if [[ -n "$temp_file" ]]; then if [[ "$temp_unit" == "C" ]]; then cpu_temp=$(printf "%.0fc\\n" "$(calcFunc "$(< $temp_file) / 1000")") @@ -321,8 +331,8 @@ get_sys_stats() { *) cpu_col="$COL_URG_RED";; esac - # $COL_NC$COL_DARK_GRAY is needed for $COL_URG_RED - cpu_temp_str=" @ $cpu_col$cpu_temp$COL_NC$COL_DARK_GRAY" + # $COL_NC$COL_DARK_GRAY is needed for $COL_URG_RED + cpu_temp_str=" @ $cpu_col$cpu_temp$COL_NC$COL_DARK_GRAY" elif [[ "$temp_unit" == "F" ]]; then cpu_temp=$(printf "%.0ff\\n" "$(calcFunc "($(< $temp_file) / 1000) * 9 / 5 + 32")") @@ -349,7 +359,7 @@ get_sys_stats() { ram_used="${ram_raw[1]}" ram_total="${ram_raw[2]}" - if [[ "$(pihole status web 2> /dev/null)" == "1" ]]; then + if [[ "$(pihole status web 2> /dev/null)" -ge "1" ]]; then ph_status="${COL_LIGHT_GREEN}Active" else ph_status="${COL_LIGHT_RED}Offline" @@ -437,7 +447,7 @@ get_strings() { lan_info="Gateway: $net_gateway" dhcp_info="$leased_str$ph_dhcp_num of $ph_dhcp_max" - ads_info="$total_str$ads_blocked_today of $dns_queries_today" + ads_info="$total_str$ads_blocked_today of $dns_queries_today" dns_info="$dns_count DNS servers" [[ "$recent_blocked" == "0" ]] && recent_blocked="${COL_LIGHT_RED}FTL offline${COL_NC}" @@ -480,7 +490,7 @@ chronoFunc() { ${COL_LIGHT_RED}Press Ctrl-C to exit${COL_NC} ${COL_DARK_GRAY}$scr_line_str${COL_NC}" else - echo -e "|¯¯¯(¯)_|¯|_ ___|¯|___$phc_ver_str\\n| ¯_/¯|_| ' \\/ _ \\ / -_)$lte_ver_str\\n|_| |_| |_||_\\___/_\\___|$ftl_ver_str\\n ${COL_DARK_GRAY}$scr_line_str${COL_NC}" + echo -e "|¯¯¯(¯)_|¯|_ ___|¯|___$phc_ver_str\\n| ¯_/¯|_| ' \\/ _ \\ / -_)$lte_ver_str\\n|_| |_| |_||_\\___/_\\___|$ftl_ver_str\\n ${COL_DARK_GRAY}$scr_line_str${COL_NC}" fi printFunc " Hostname: " "$sys_name" "$host_info" @@ -490,20 +500,16 @@ chronoFunc() { printFunc " RAM usage: " "$ram_perc%" "$ram_info" printFunc " HDD usage: " "$disk_perc" "$disk_info" - if [[ "$scr_lines" -gt 17 ]] && [[ "$chrono_width" != "small" ]]; then - printFunc " LAN addr: " "${IPV4_ADDRESS/\/*/}" "$lan_info" - fi - if [[ "$DHCP_ACTIVE" == "true" ]]; then printFunc "DHCP usage: " "$ph_dhcp_percent%" "$dhcp_info" 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 @@ -551,7 +557,7 @@ Calculates stats and displays to an LCD Options: -j, --json Output stats as JSON formatted string -r, --refresh Set update frequency (in seconds) - -e, --exit Output stats and exit witout refreshing + -e, --exit Output stats and exit without refreshing -h, --help Display this help text" fi diff --git a/advanced/Scripts/database_migration/gravity-db.sh b/advanced/Scripts/database_migration/gravity-db.sh old mode 100644 new mode 100755 index 0fe90d8a80..a7ba60a919 --- a/advanced/Scripts/database_migration/gravity-db.sh +++ b/advanced/Scripts/database_migration/gravity-db.sh @@ -10,6 +10,8 @@ # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. +readonly scriptPath="/etc/.pihole/advanced/Scripts/database_migration/gravity" + upgrade_gravityDB(){ local database piholeDir auditFile version database="${1}" @@ -17,13 +19,13 @@ upgrade_gravityDB(){ auditFile="${piholeDir}/auditlog.list" # Get database version - version="$(sqlite3 "${database}" "SELECT \"value\" FROM \"info\" WHERE \"property\" = 'version';")" + version="$(pihole-FTL sqlite3 "${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" - sqlite3 "${database}" < "/etc/.pihole/advanced/Scripts/database_migration/gravity/1_to_2.sql" + pihole-FTL sqlite3 "${database}" < "${scriptPath}/1_to_2.sql" version=2 # Store audit domains in database table @@ -38,7 +40,92 @@ 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" - sqlite3 "${database}" < "/etc/.pihole/advanced/Scripts/database_migration/gravity/2_to_3.sql" + pihole-FTL sqlite3 "${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" + 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" + 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" + version=6 + fi + if [[ "$version" == "6" ]]; then + # This migration script adds a special group with ID 0 + # 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" + version=7 + fi + if [[ "$version" == "7" ]]; then + # This migration script recreated the group table + # 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" + 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" + 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" + version=10 + fi + if [[ "$version" == "10" ]]; then + # This adds timestamp and an optional comment field to the client table + # These fields are only temporary and will be replaces by the columns + # defined in gravity.db.sql during gravity swapping. We add them here + # 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" + 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" + 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" + 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" + 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" + version=15 + fi } diff --git a/advanced/Scripts/database_migration/gravity/10_to_11.sql b/advanced/Scripts/database_migration/gravity/10_to_11.sql new file mode 100644 index 0000000000..b073f83bc0 --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/10_to_11.sql @@ -0,0 +1,16 @@ +.timeout 30000 + +BEGIN TRANSACTION; + +ALTER TABLE client ADD COLUMN date_added INTEGER; +ALTER TABLE client ADD COLUMN date_modified INTEGER; +ALTER TABLE client ADD COLUMN comment TEXT; + +CREATE TRIGGER tr_client_update AFTER UPDATE ON client + BEGIN + UPDATE client SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE id = NEW.id; + END; + +UPDATE info SET value = 11 WHERE property = 'version'; + +COMMIT; diff --git a/advanced/Scripts/database_migration/gravity/11_to_12.sql b/advanced/Scripts/database_migration/gravity/11_to_12.sql new file mode 100644 index 0000000000..d480d46efc --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/11_to_12.sql @@ -0,0 +1,19 @@ +.timeout 30000 + +PRAGMA FOREIGN_KEYS=OFF; + +BEGIN TRANSACTION; + +UPDATE "group" SET name = 'Default' WHERE id = 0; +UPDATE "group" SET description = 'The default group' WHERE id = 0; + +DROP TRIGGER IF EXISTS tr_group_zero; + +CREATE TRIGGER tr_group_zero AFTER DELETE ON "group" + BEGIN + INSERT OR IGNORE INTO "group" (id,enabled,name,description) VALUES (0,1,'Default','The default group'); + END; + +UPDATE info SET value = 12 WHERE property = 'version'; + +COMMIT; diff --git a/advanced/Scripts/database_migration/gravity/12_to_13.sql b/advanced/Scripts/database_migration/gravity/12_to_13.sql new file mode 100644 index 0000000000..7d85cb05e2 --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/12_to_13.sql @@ -0,0 +1,18 @@ +.timeout 30000 + +PRAGMA FOREIGN_KEYS=OFF; + +BEGIN TRANSACTION; + +ALTER TABLE adlist ADD COLUMN date_updated INTEGER; + +DROP TRIGGER tr_adlist_update; + +CREATE TRIGGER tr_adlist_update AFTER UPDATE OF address,enabled,comment ON adlist + BEGIN + UPDATE adlist SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE id = NEW.id; + END; + +UPDATE info SET value = 13 WHERE property = 'version'; + +COMMIT; diff --git a/advanced/Scripts/database_migration/gravity/13_to_14.sql b/advanced/Scripts/database_migration/gravity/13_to_14.sql new file mode 100644 index 0000000000..0a465d1d4f --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/13_to_14.sql @@ -0,0 +1,13 @@ +.timeout 30000 + +PRAGMA FOREIGN_KEYS=OFF; + +BEGIN TRANSACTION; + +ALTER TABLE adlist ADD COLUMN number INTEGER NOT NULL DEFAULT 0; +ALTER TABLE adlist ADD COLUMN invalid_domains INTEGER NOT NULL DEFAULT 0; +ALTER TABLE adlist ADD COLUMN status INTEGER NOT NULL DEFAULT 0; + +UPDATE info SET value = 14 WHERE property = 'version'; + +COMMIT; diff --git a/advanced/Scripts/database_migration/gravity/14_to_15.sql b/advanced/Scripts/database_migration/gravity/14_to_15.sql new file mode 100644 index 0000000000..41cb751753 --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/14_to_15.sql @@ -0,0 +1,15 @@ +.timeout 30000 + +PRAGMA FOREIGN_KEYS=OFF; + +BEGIN TRANSACTION; +DROP VIEW vw_adlist; + +CREATE VIEW vw_adlist AS SELECT DISTINCT address, id + FROM adlist + WHERE enabled = 1 + ORDER BY id; + +UPDATE info SET value = 15 WHERE property = 'version'; + +COMMIT; diff --git a/advanced/Scripts/database_migration/gravity/3_to_4.sql b/advanced/Scripts/database_migration/gravity/3_to_4.sql new file mode 100644 index 0000000000..05231f7299 --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/3_to_4.sql @@ -0,0 +1,96 @@ +.timeout 30000 + +PRAGMA FOREIGN_KEYS=OFF; + +BEGIN TRANSACTION; + +CREATE TABLE domainlist +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type INTEGER NOT NULL DEFAULT 0, + domain TEXT UNIQUE NOT NULL, + enabled BOOLEAN NOT NULL DEFAULT 1, + date_added INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), + date_modified INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), + comment TEXT +); + +ALTER TABLE whitelist ADD COLUMN type INTEGER; +UPDATE whitelist SET type = 0; +INSERT INTO domainlist (type,domain,enabled,date_added,date_modified,comment) + SELECT type,domain,enabled,date_added,date_modified,comment FROM whitelist; + +ALTER TABLE blacklist ADD COLUMN type INTEGER; +UPDATE blacklist SET type = 1; +INSERT INTO domainlist (type,domain,enabled,date_added,date_modified,comment) + SELECT type,domain,enabled,date_added,date_modified,comment FROM blacklist; + +ALTER TABLE regex_whitelist ADD COLUMN type INTEGER; +UPDATE regex_whitelist SET type = 2; +INSERT INTO domainlist (type,domain,enabled,date_added,date_modified,comment) + SELECT type,domain,enabled,date_added,date_modified,comment FROM regex_whitelist; + +ALTER TABLE regex_blacklist ADD COLUMN type INTEGER; +UPDATE regex_blacklist SET type = 3; +INSERT INTO domainlist (type,domain,enabled,date_added,date_modified,comment) + SELECT type,domain,enabled,date_added,date_modified,comment FROM regex_blacklist; + +DROP TABLE whitelist_by_group; +DROP TABLE blacklist_by_group; +DROP TABLE regex_whitelist_by_group; +DROP TABLE regex_blacklist_by_group; +CREATE TABLE domainlist_by_group +( + domainlist_id INTEGER NOT NULL REFERENCES domainlist (id), + group_id INTEGER NOT NULL REFERENCES "group" (id), + PRIMARY KEY (domainlist_id, group_id) +); + +DROP TRIGGER tr_whitelist_update; +DROP TRIGGER tr_blacklist_update; +DROP TRIGGER tr_regex_whitelist_update; +DROP TRIGGER tr_regex_blacklist_update; +CREATE TRIGGER tr_domainlist_update AFTER UPDATE ON domainlist + BEGIN + UPDATE domainlist SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE domain = NEW.domain; + END; + +DROP VIEW vw_whitelist; +CREATE VIEW vw_whitelist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id + FROM domainlist + LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id + LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id + WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1) + AND domainlist.type = 0 + ORDER BY domainlist.id; + +DROP VIEW vw_blacklist; +CREATE VIEW vw_blacklist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id + FROM domainlist + LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id + LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id + WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1) + AND domainlist.type = 1 + ORDER BY domainlist.id; + +DROP VIEW vw_regex_whitelist; +CREATE VIEW vw_regex_whitelist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id + FROM domainlist + LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id + LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id + WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1) + AND domainlist.type = 2 + ORDER BY domainlist.id; + +DROP VIEW vw_regex_blacklist; +CREATE VIEW vw_regex_blacklist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id + FROM domainlist + LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id + LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id + WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1) + AND domainlist.type = 3 + ORDER BY domainlist.id; + +UPDATE info SET value = 4 WHERE property = 'version'; + +COMMIT; diff --git a/advanced/Scripts/database_migration/gravity/4_to_5.sql b/advanced/Scripts/database_migration/gravity/4_to_5.sql new file mode 100644 index 0000000000..4ae9f980fb --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/4_to_5.sql @@ -0,0 +1,38 @@ +.timeout 30000 + +PRAGMA FOREIGN_KEYS=OFF; + +BEGIN TRANSACTION; + +DROP TABLE gravity; +CREATE TABLE gravity +( + domain TEXT NOT NULL, + adlist_id INTEGER NOT NULL REFERENCES adlist (id), + PRIMARY KEY(domain, adlist_id) +); + +DROP VIEW vw_gravity; +CREATE VIEW vw_gravity AS SELECT domain, adlist_by_group.group_id AS group_id + FROM gravity + LEFT JOIN adlist_by_group ON adlist_by_group.adlist_id = gravity.adlist_id + LEFT JOIN adlist ON adlist.id = gravity.adlist_id + LEFT JOIN "group" ON "group".id = adlist_by_group.group_id + WHERE adlist.enabled = 1 AND (adlist_by_group.group_id IS NULL OR "group".enabled = 1); + +CREATE TABLE client +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ip TEXT NOL NULL UNIQUE +); + +CREATE TABLE client_by_group +( + client_id INTEGER NOT NULL REFERENCES client (id), + group_id INTEGER NOT NULL REFERENCES "group" (id), + PRIMARY KEY (client_id, group_id) +); + +UPDATE info SET value = 5 WHERE property = 'version'; + +COMMIT; diff --git a/advanced/Scripts/database_migration/gravity/5_to_6.sql b/advanced/Scripts/database_migration/gravity/5_to_6.sql new file mode 100644 index 0000000000..d2bb3145bb --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/5_to_6.sql @@ -0,0 +1,18 @@ +.timeout 30000 + +PRAGMA FOREIGN_KEYS=OFF; + +BEGIN TRANSACTION; + +DROP VIEW vw_adlist; +CREATE VIEW vw_adlist AS SELECT DISTINCT address, adlist.id AS id + FROM adlist + LEFT JOIN adlist_by_group ON adlist_by_group.adlist_id = adlist.id + LEFT JOIN "group" ON "group".id = adlist_by_group.group_id + WHERE adlist.enabled = 1 AND (adlist_by_group.group_id IS NULL OR "group".enabled = 1) + ORDER BY adlist.id; + +UPDATE info SET value = 6 WHERE property = 'version'; + +COMMIT; + diff --git a/advanced/Scripts/database_migration/gravity/6_to_7.sql b/advanced/Scripts/database_migration/gravity/6_to_7.sql new file mode 100644 index 0000000000..22d9dfafc3 --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/6_to_7.sql @@ -0,0 +1,35 @@ +.timeout 30000 + +PRAGMA FOREIGN_KEYS=OFF; + +BEGIN TRANSACTION; + +INSERT OR REPLACE INTO "group" (id,enabled,name) VALUES (0,1,'Unassociated'); + +INSERT INTO domainlist_by_group (domainlist_id, group_id) SELECT id, 0 FROM domainlist; +INSERT INTO client_by_group (client_id, group_id) SELECT id, 0 FROM client; +INSERT INTO adlist_by_group (adlist_id, group_id) SELECT id, 0 FROM adlist; + +CREATE TRIGGER tr_domainlist_add AFTER INSERT ON domainlist + BEGIN + INSERT INTO domainlist_by_group (domainlist_id, group_id) VALUES (NEW.id, 0); + END; + +CREATE TRIGGER tr_client_add AFTER INSERT ON client + BEGIN + INSERT INTO client_by_group (client_id, group_id) VALUES (NEW.id, 0); + END; + +CREATE TRIGGER tr_adlist_add AFTER INSERT ON adlist + BEGIN + INSERT INTO adlist_by_group (adlist_id, group_id) VALUES (NEW.id, 0); + END; + +CREATE TRIGGER tr_group_zero AFTER DELETE ON "group" + BEGIN + INSERT OR REPLACE INTO "group" (id,enabled,name) VALUES (0,1,'Unassociated'); + END; + +UPDATE info SET value = 7 WHERE property = 'version'; + +COMMIT; diff --git a/advanced/Scripts/database_migration/gravity/7_to_8.sql b/advanced/Scripts/database_migration/gravity/7_to_8.sql new file mode 100644 index 0000000000..ccf0c148c8 --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/7_to_8.sql @@ -0,0 +1,35 @@ +.timeout 30000 + +PRAGMA FOREIGN_KEYS=OFF; + +BEGIN TRANSACTION; + +ALTER TABLE "group" RENAME TO "group__"; + +CREATE TABLE "group" +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + enabled BOOLEAN NOT NULL DEFAULT 1, + name TEXT UNIQUE NOT NULL, + date_added INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), + date_modified INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), + description TEXT +); + +CREATE TRIGGER tr_group_update AFTER UPDATE ON "group" + BEGIN + UPDATE "group" SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE id = NEW.id; + END; + +INSERT OR IGNORE INTO "group" (id,enabled,name,description) SELECT id,enabled,name,description FROM "group__"; + +DROP TABLE "group__"; + +CREATE TRIGGER tr_group_zero AFTER DELETE ON "group" + BEGIN + INSERT OR IGNORE INTO "group" (id,enabled,name) VALUES (0,1,'Unassociated'); + END; + +UPDATE info SET value = 8 WHERE property = 'version'; + +COMMIT; diff --git a/advanced/Scripts/database_migration/gravity/8_to_9.sql b/advanced/Scripts/database_migration/gravity/8_to_9.sql new file mode 100644 index 0000000000..0d873e2a18 --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/8_to_9.sql @@ -0,0 +1,27 @@ +.timeout 30000 + +PRAGMA FOREIGN_KEYS=OFF; + +BEGIN TRANSACTION; + +DROP TRIGGER IF EXISTS tr_group_update; +DROP TRIGGER IF EXISTS tr_group_zero; + +PRAGMA legacy_alter_table=ON; +ALTER TABLE "group" RENAME TO "group__"; +PRAGMA legacy_alter_table=OFF; +ALTER TABLE "group__" RENAME TO "group"; + +CREATE TRIGGER tr_group_update AFTER UPDATE ON "group" + BEGIN + UPDATE "group" SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE id = NEW.id; + END; + +CREATE TRIGGER tr_group_zero AFTER DELETE ON "group" + BEGIN + INSERT OR IGNORE INTO "group" (id,enabled,name) VALUES (0,1,'Unassociated'); + END; + +UPDATE info SET value = 9 WHERE property = 'version'; + +COMMIT; diff --git a/advanced/Scripts/database_migration/gravity/9_to_10.sql b/advanced/Scripts/database_migration/gravity/9_to_10.sql new file mode 100644 index 0000000000..a5636a232d --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/9_to_10.sql @@ -0,0 +1,29 @@ +.timeout 30000 + +PRAGMA FOREIGN_KEYS=OFF; + +BEGIN TRANSACTION; + +DROP TABLE IF EXISTS whitelist; +DROP TABLE IF EXISTS blacklist; +DROP TABLE IF EXISTS regex_whitelist; +DROP TABLE IF EXISTS regex_blacklist; + +CREATE TRIGGER tr_domainlist_delete AFTER DELETE ON domainlist + BEGIN + DELETE FROM domainlist_by_group WHERE domainlist_id = OLD.id; + END; + +CREATE TRIGGER tr_adlist_delete AFTER DELETE ON adlist + BEGIN + DELETE FROM adlist_by_group WHERE adlist_id = OLD.id; + END; + +CREATE TRIGGER tr_client_delete AFTER DELETE ON client + BEGIN + DELETE FROM client_by_group WHERE client_id = OLD.id; + END; + +UPDATE info SET value = 10 WHERE property = 'version'; + +COMMIT; diff --git a/advanced/Scripts/list.sh b/advanced/Scripts/list.sh index 6a606665bb..b76a7ef7ac 100755 --- a/advanced/Scripts/list.sh +++ b/advanced/Scripts/list.sh @@ -1,4 +1,6 @@ #!/usr/bin/env bash +# shellcheck disable=SC1090 + # 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. @@ -9,11 +11,19 @@ # Please see LICENSE file for your rights under this license. # Globals -basename=pihole -piholeDir=/etc/"${basename}" -gravityDBfile="${piholeDir}/gravity.db" +piholeDir="/etc/pihole" +GRAVITYDB="${piholeDir}/gravity.db" +# Source pihole-FTL from install script +pihole_FTL="${piholeDir}/pihole-FTL.conf" +if [[ -f "${pihole_FTL}" ]]; then + source "${pihole_FTL}" +fi -reload=false +# Set this only after sourcing pihole-FTL.conf as the gravity database path may +# have changed +gravityDBfile="${GRAVITYDB}" + +noReloadRequested=false addmode=true verbose=true wildcard=false @@ -21,174 +31,194 @@ web=false domList=() -listType="" -listname="" +typeId="" +comment="" +declare -i domaincount +domaincount=0 +reload=false colfile="/opt/pihole/COL_TABLE" source ${colfile} +# IDs are hard-wired to domain interpretation in the gravity database scheme +# Clients (including FTL) will read them through the corresponding views +readonly whitelist="0" +readonly blacklist="1" +readonly regex_whitelist="2" +readonly regex_blacklist="3" + +GetListnameFromTypeId() { + if [[ "$1" == "${whitelist}" ]]; then + echo "whitelist" + elif [[ "$1" == "${blacklist}" ]]; then + echo "blacklist" + elif [[ "$1" == "${regex_whitelist}" ]]; then + echo "regex whitelist" + elif [[ "$1" == "${regex_blacklist}" ]]; then + echo "regex blacklist" + fi +} -helpFunc() { - if [[ "${listType}" == "whitelist" ]]; then - param="w" - type="whitelist" - elif [[ "${listType}" == "regex_blacklist" && "${wildcard}" == true ]]; then - param="-wild" - type="wildcard blacklist" - elif [[ "${listType}" == "regex_blacklist" ]]; then - param="-regex" - type="regex blacklist filter" - elif [[ "${listType}" == "regex_whitelist" && "${wildcard}" == true ]]; then - param="-white-wild" - type="wildcard whitelist" - elif [[ "${listType}" == "regex_whitelist" ]]; then - param="-white-regex" - type="regex whitelist filter" - else - param="b" - type="blacklist" +GetListParamFromTypeId() { + if [[ "${typeId}" == "${whitelist}" ]]; then + echo "w" + elif [[ "${typeId}" == "${blacklist}" ]]; then + echo "b" + elif [[ "${typeId}" == "${regex_whitelist}" && "${wildcard}" == true ]]; then + echo "-white-wild" + elif [[ "${typeId}" == "${regex_whitelist}" ]]; then + echo "-white-regex" + elif [[ "${typeId}" == "${regex_blacklist}" && "${wildcard}" == true ]]; then + echo "-wild" + elif [[ "${typeId}" == "${regex_blacklist}" ]]; then + echo "-regex" fi +} + +helpFunc() { + local listname param + + listname="$(GetListnameFromTypeId "${typeId}")" + param="$(GetListParamFromTypeId)" echo "Usage: pihole -${param} [options] Example: 'pihole -${param} site.com', or 'pihole -${param} site1.com site2.com' -${type^} one or more domains +${listname^} one or more domains Options: - -d, --delmode Remove domain(s) from the ${type} - -nr, --noreload Update ${type} without reloading the DNS server + -d, --delmode Remove domain(s) from the ${listname} + -nr, --noreload Update ${listname} without reloading the DNS server -q, --quiet Make output less verbose -h, --help Show this help dialog - -l, --list Display all your ${type}listed domains - --nuke Removes all entries in a list" + -l, --list Display all your ${listname}listed domains + --nuke Removes all entries in a list + --comment \"text\" Add a comment to the domain. If adding multiple domains the same comment will be used for all" exit 0 } -EscapeRegexp() { - # This way we may safely insert an arbitrary - # string in our regular expressions - # This sed is intentionally executed in three steps to ease maintainability - # The first sed removes any amount of leading dots - echo $* | sed 's/^\.*//' | sed "s/[]\.|$(){}?+*^]/\\\\&/g" | sed "s/\\//\\\\\//g" -} - -HandleOther() { +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 [[ ( "${listType}" == "regex_blacklist" || "${listType}" == "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}) + 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)) } ProcessDomainList() { - local is_regexlist - if [[ "${listType}" == "regex_blacklist" ]]; then - # Regex black filter list - listname="regex blacklist filters" - is_regexlist=true - elif [[ "${listType}" == "regex_whitelist" ]]; then - # Regex white filter list - listname="regex whitelist filters" - is_regexlist=true - else - # Whitelist / Blacklist - listname="${listType}" - is_regexlist=false - fi - for dom in "${domList[@]}"; do # Format domain into regex filter if requested if [[ "${wildcard}" == true ]]; then - dom="(^|\\.)${dom//\./\\.}$" + dom="(\\.|^)${dom//\./\\.}$" fi # Logic: If addmode then add to desired list and remove from the other; # if delmode then remove from desired list but do not add to the other if ${addmode}; then - AddDomain "${dom}" "${listType}" - if ! ${is_regexlist}; then - RemoveDomain "${dom}" "${listAlt}" - fi + AddDomain "${dom}" else - RemoveDomain "${dom}" "${listType}" + RemoveDomain "${dom}" fi - done + done } AddDomain() { - local domain list num - # Use printf to escape domain. %q prints the argument in a form that can be reused as shell input + local domain num requestedListname existingTypeId existingListname domain="$1" - list="$2" # Is the domain in the list we want to add it to? - num="$(sqlite3 "${gravityDBfile}" "SELECT COUNT(*) FROM ${list} WHERE domain = '${domain}';")" + num="$(pihole-FTL sqlite3 "${gravityDBfile}" "SELECT COUNT(*) FROM domainlist WHERE domain = '${domain}';")" + requestedListname="$(GetListnameFromTypeId "${typeId}")" if [[ "${num}" -ne 0 ]]; then - if [[ "${verbose}" == true ]]; then - echo -e " ${INFO} ${1} already exists in ${listname}, no need to add!" - fi - return + existingTypeId="$(pihole-FTL sqlite3 "${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}';" + if [[ "${verbose}" == true ]]; then + echo -e " ${INFO} ${1} already exists in ${existingListname}, it has been moved to ${requestedListname}!" + fi + fi + return fi # Domain not found in the table, add it! if [[ "${verbose}" == true ]]; then - echo -e " ${INFO} Adding ${1} to the ${listname}..." + echo -e " ${INFO} Adding ${domain} to the ${requestedListname}..." fi reload=true # Insert only the domain here. The enabled and date_added fields will be filled # with their default values (enabled = true, date_added = current timestamp) - sqlite3 "${gravityDBfile}" "INSERT INTO ${list} (domain) VALUES ('${domain}');" + if [[ -z "${comment}" ]]; then + pihole-FTL sqlite3 "${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}');" + fi } RemoveDomain() { - local domain list num - # Use printf to escape domain. %q prints the argument in a form that can be reused as shell input + local domain num requestedListname domain="$1" - list="$2" # Is the domain in the list we want to remove it from? - num="$(sqlite3 "${gravityDBfile}" "SELECT COUNT(*) FROM ${list} WHERE domain = '${domain}';")" + num="$(pihole-FTL sqlite3 "${gravityDBfile}" "SELECT COUNT(*) FROM domainlist WHERE domain = '${domain}' AND type = ${typeId};")" + + requestedListname="$(GetListnameFromTypeId "${typeId}")" if [[ "${num}" -eq 0 ]]; then - if [[ "${verbose}" == true ]]; then - echo -e " ${INFO} ${1} does not exist in ${list}, no need to remove!" - fi - return + if [[ "${verbose}" == true ]]; then + echo -e " ${INFO} ${domain} does not exist in ${requestedListname}, no need to remove!" + fi + return fi # Domain found in the table, remove it! if [[ "${verbose}" == true ]]; then - echo -e " ${INFO} Removing ${1} from the ${listname}..." + echo -e " ${INFO} Removing ${domain} from the ${requestedListname}..." fi reload=true # Remove it from the current list - sqlite3 "${gravityDBfile}" "DELETE FROM ${list} WHERE domain = '${domain}';" + pihole-FTL sqlite3 "${gravityDBfile}" "DELETE FROM domainlist WHERE domain = '${domain}' AND type = ${typeId};" } Displaylist() { - local list listname count num_pipes domain enabled status nicedate + local count num_pipes domain enabled status nicedate requestedListname - listname="${listType}" - data="$(sqlite3 "${gravityDBfile}" "SELECT domain,enabled,date_modified FROM ${listType};" 2> /dev/null)" + requestedListname="$(GetListnameFromTypeId "${typeId}")" + data="$(pihole-FTL sqlite3 "${gravityDBfile}" "SELECT domain,enabled,date_modified FROM domainlist WHERE type = ${typeId};" 2> /dev/null)" if [[ -z $data ]]; then echo -e "Not showing empty list" else - echo -e "Displaying ${listname}:" + echo -e "Displaying ${requestedListname}:" count=1 while IFS= read -r line do @@ -221,31 +251,49 @@ Displaylist() { } NukeList() { - sqlite3 "${gravityDBfile}" "DELETE FROM ${listType};" + count=$(pihole-FTL sqlite3 "${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};" + echo " ${TICK} Removed ${count} domain(s) from the ${listname}" + else + echo " ${INFO} ${listname} already empty. Nothing to do!" + fi + exit 0; +} + +GetComment() { + comment="$1" + if [[ "${comment}" =~ [^a-zA-Z0-9_\#:/\.,\ -] ]]; then + echo " ${CROSS} Found invalid characters in domain comment!" + exit + fi } -for var in "$@"; do - case "${var}" in - "-w" | "whitelist" ) listType="whitelist"; listAlt="blacklist";; - "-b" | "blacklist" ) listType="blacklist"; listAlt="whitelist";; - "--wild" | "wildcard" ) listType="regex_blacklist"; wildcard=true;; - "--regex" | "regex" ) listType="regex_blacklist";; - "--white-regex" | "white-regex" ) listType="regex_whitelist";; - "--white-wild" | "white-wild" ) listType="regex_whitelist"; wildcard=true;; - "-nr"| "--noreload" ) reload=false;; +while (( "$#" )); do + case "${1}" in + "-w" | "whitelist" ) typeId=0;; + "-b" | "blacklist" ) typeId=1;; + "--white-regex" | "white-regex" ) typeId=2;; + "--white-wild" | "white-wild" ) typeId=2; wildcard=true;; + "--wild" | "wildcard" ) typeId=3; wildcard=true;; + "--regex" | "regex" ) typeId=3;; + "-nr"| "--noreload" ) noReloadRequested=true;; "-d" | "--delmode" ) addmode=false;; "-q" | "--quiet" ) verbose=false;; "-h" | "--help" ) helpFunc;; "-l" | "--list" ) Displaylist;; "--nuke" ) NukeList;; "--web" ) web=true;; - * ) HandleOther "${var}";; + "--comment" ) GetComment "${2}"; shift;; + * ) ValidateDomain "${1}";; esac + shift done shift -if [[ $# = 0 ]]; then +if [[ ${domaincount} == 0 ]]; then helpFunc fi @@ -253,9 +301,9 @@ ProcessDomainList # Used on web interface if $web; then -echo "DONE" + echo "DONE" fi -if [[ "${reload}" != false ]]; then - pihole restartdns reload +if [[ ${reload} == true && ${noReloadRequested} == false ]]; then + pihole restartdns reload-lists fi diff --git a/advanced/Scripts/pihole-reenable.sh b/advanced/Scripts/pihole-reenable.sh new file mode 100755 index 0000000000..93ec3b958a --- /dev/null +++ b/advanced/Scripts/pihole-reenable.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Pi-hole: A black hole for Internet advertisements +# (c) 2020 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. +# +# +# The pihole disable command has the option to set a specified time before +# blocking is automatically re-enabled. +# +# Present script is responsible for the sleep & re-enable part of the job and +# is automatically terminated if it is still running when pihole is enabled by +# other means. +# +# This ensures that pihole ends up in the correct state after a sequence of +# commands suchs as: `pihole disable 30s; pihole enable; pihole disable` + +readonly PI_HOLE_BIN_DIR="/usr/local/bin" + +sleep "${1}" +"${PI_HOLE_BIN_DIR}"/pihole enable diff --git a/advanced/Scripts/piholeARPTable.sh b/advanced/Scripts/piholeARPTable.sh index aa45f9ad59..5daa025d49 100755 --- a/advanced/Scripts/piholeARPTable.sh +++ b/advanced/Scripts/piholeARPTable.sh @@ -36,17 +36,10 @@ flushARP(){ echo -ne " ${INFO} Flushing network table ..." fi - # Flush ARP cache to avoid re-adding of dead entries - if ! output=$(ip neigh flush all 2>&1); then - echo -e "${OVER} ${CROSS} Failed to clear ARP cache" - echo " Output: ${output}" - return 1 - fi - # Truncate network_addresses table in pihole-FTL.db # This needs to be done before we can truncate the network table due to - # foreign key contraints - if ! output=$(sqlite3 "${DBFILE}" "DELETE FROM network_addresses" 2>&1); then + # foreign key constraints + if ! output=$(pihole-FTL sqlite3 "${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}" @@ -54,7 +47,7 @@ flushARP(){ fi # Truncate network table in pihole-FTL.db - if ! output=$(sqlite3 "${DBFILE}" "DELETE FROM network" 2>&1); then + if ! output=$(pihole-FTL sqlite3 "${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 old mode 100644 new mode 100755 index 673ded0bc1..cf57800c4c --- a/advanced/Scripts/piholeCheckout.sh +++ b/advanced/Scripts/piholeCheckout.sh @@ -3,13 +3,13 @@ # (c) 2017 Pi-hole, LLC (https://pi-hole.net) # Network-wide ad blocking via your own hardware. # -# Switch Pi-hole subsystems to a different Github branch. +# Switch Pi-hole subsystems to a different GitHub branch. # # This file is copyright under the latest version of the EUPL. # 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 @@ -36,7 +36,7 @@ warning1() { return 0 ;; *) - echo -e "\\n ${INFO} Branch change has been cancelled" + echo -e "\\n ${INFO} Branch change has been canceled" return 1 ;; esac @@ -46,6 +46,12 @@ checkout() { local corebranches local webbranches + # Check if FTL is installed - do this early on as FTL is a hard dependency for Pi-hole + local funcOutput + funcOutput=$(get_binary_name) #Store output of get_binary_name here + local binary + binary="pihole-FTL${funcOutput##*pihole-FTL}" #binary name will be the last line of the output of get_binary_name (it always begins with pihole-FTL) + # Avoid globbing set -f @@ -78,7 +84,7 @@ checkout() { echo -e " ${INFO} Shortcut \"dev\" detected - checking out development / devel branches..." echo "" echo -e " ${INFO} Pi-hole Core" - fetch_checkout_pull_branch "${PI_HOLE_FILES_DIR}" "development" || { echo " ${CROSS} Unable to pull Core developement branch"; exit 1; } + fetch_checkout_pull_branch "${PI_HOLE_FILES_DIR}" "development" || { echo " ${CROSS} Unable to pull Core development branch"; exit 1; } if [[ "${INSTALL_WEB_INTERFACE}" == "true" ]]; then echo "" echo -e " ${INFO} Web interface" @@ -86,7 +92,6 @@ checkout() { fi #echo -e " ${TICK} Pi-hole Core" - get_binary_name local path path="development/${binary}" echo "development" > /etc/pihole/ftlbranch @@ -101,7 +106,6 @@ checkout() { fetch_checkout_pull_branch "${webInterfaceDir}" "master" || { echo " ${CROSS} Unable to pull Web master branch"; exit 1; } fi #echo -e " ${TICK} Web Interface" - get_binary_name local path path="master/${binary}" echo "master" > /etc/pihole/ftlbranch @@ -160,18 +164,24 @@ checkout() { exit 1 fi checkout_pull_branch "${webInterfaceDir}" "${2}" + # Update local and remote versions via updatechecker + /opt/pihole/updatecheck.sh elif [[ "${1}" == "ftl" ]] ; then - get_binary_name local path + local oldbranch path="${2}/${binary}" + oldbranch="$(pihole-FTL -b)" if check_download_exists "$path"; then echo " ${TICK} Branch ${2} exists" echo "${2}" > /etc/pihole/ftlbranch chmod 644 /etc/pihole/ftlbranch + echo -e " ${INFO} Switching to branch: \"${2}\" from \"${oldbranch}\"" 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 84e3441625..3cd782bfa7 100755 --- a/advanced/Scripts/piholeDebug.sh +++ b/advanced/Scripts/piholeDebug.sh @@ -27,7 +27,7 @@ PIHOLE_COLTABLE_FILE="${PIHOLE_SCRIPTS_DIRECTORY}/COL_TABLE" # These provide the colors we need for making the log more readable if [[ -f ${PIHOLE_COLTABLE_FILE} ]]; then - source ${PIHOLE_COLTABLE_FILE} + source ${PIHOLE_COLTABLE_FILE} else COL_NC='\e[0m' # No Color COL_RED='\e[1;91m' @@ -41,25 +41,18 @@ 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://discourse.pi-hole.net/t/hardware-software-requirements/273${COL_NC}" -FAQ_HARDWARE_REQUIREMENTS_PORTS="${COL_CYAN}https://discourse.pi-hole.net/t/hardware-software-requirements/273#ports${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}" -TRICORDER_CONTEST="${COL_CYAN}https://pi-hole.net/2016/11/07/crack-our-medical-tricorder-win-a-raspberry-pi-3/${COL_NC}" - -# Port numbers used for uploading the debug log -TRICORDER_NC_PORT_NUMBER=9999 -TRICORDER_SSL_PORT_NUMBER=9998 # Directories required by Pi-hole # https://discourse.pi-hole.net/t/what-files-does-pi-hole-use/1684 @@ -70,24 +63,20 @@ 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" HTML_DIRECTORY="/var/www/html" WEB_GIT_DIRECTORY="${HTML_DIRECTORY}/admin" -#BLOCK_PAGE_DIRECTORY="${HTML_DIRECTORY}/pihole" SHM_DIRECTORY="/dev/shm" +ETC="/etc" # Files required by Pi-hole # https://discourse.pi-hole.net/t/what-files-does-pi-hole-use/1684 PIHOLE_CRON_FILE="${CRON_D_DIRECTORY}/pihole" -PIHOLE_DNS_CONFIG_FILE="${DNSMASQ_D_DIRECTORY}/01-pihole.conf" -PIHOLE_DHCP_CONFIG_FILE="${DNSMASQ_D_DIRECTORY}/02-pihole-dhcp.conf" -PIHOLE_WILDCARD_CONFIG_FILE="${DNSMASQ_D_DIRECTORY}/03-wildcard.conf" - WEB_SERVER_CONFIG_FILE="${WEB_SERVER_CONFIG_DIRECTORY}/lighttpd.conf" -#WEB_SERVER_CUSTOM_CONFIG_FILE="${WEB_SERVER_CONFIG_DIRECTORY}/external.conf" +WEB_SERVER_CUSTOM_CONFIG_FILE="${WEB_SERVER_CONFIG_DIRECTORY}/external.conf" PIHOLE_INSTALL_LOG_FILE="${PIHOLE_DIRECTORY}/install.log" PIHOLE_RAW_BLOCKLIST_FILES="${PIHOLE_DIRECTORY}/list.*" @@ -95,6 +84,8 @@ PIHOLE_LOCAL_HOSTS_FILE="${PIHOLE_DIRECTORY}/local.list" 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,63 +115,50 @@ get_ftl_conf_value() { PIHOLE_GRAVITY_DB_FILE="$(get_ftl_conf_value "GRAVITYDB" "${PIHOLE_DIRECTORY}/gravity.db")" +PIHOLE_FTL_DB_FILE="$(get_ftl_conf_value "DBFILE" "${PIHOLE_DIRECTORY}/pihole-FTL.db")" + 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" -# An array of operating system "pretty names" that we officialy support -# We can loop through the array at any time to see if it matches a value -#SUPPORTED_OS=("Raspbian" "Ubuntu" "Fedora" "Debian" "CentOS") +RESOLVCONF="${ETC}/resolv.conf" +DNSMASQ_CONF="${ETC}/dnsmasq.conf" # 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}" -"${PIHOLE_DNS_CONFIG_FILE}" -"${PIHOLE_DHCP_CONFIG_FILE}" -"${PIHOLE_WILDCARD_CONFIG_FILE}" "${WEB_SERVER_CONFIG_FILE}" +"${WEB_SERVER_CUSTOM_CONFIG_FILE}" "${PIHOLE_INSTALL_LOG_FILE}" "${PIHOLE_RAW_BLOCKLIST_FILES}" "${PIHOLE_LOCAL_HOSTS_FILE}" "${PIHOLE_LOGROTATE_FILE}" "${PIHOLE_SETUP_VARS_FILE}" +"${PIHOLE_FTL_CONF_FILE}" "${PIHOLE_COMMAND}" "${PIHOLE_COLTABLE_FILE}" "${FTL_PID}" -"${FTL_PORT}" "${PIHOLE_LOG}" "${PIHOLE_LOG_GZIPS}" "${PIHOLE_DEBUG_LOG}" "${PIHOLE_FTL_LOG}" "${PIHOLE_WEB_SERVER_ACCESS_LOG_FILE}" -"${PIHOLE_WEB_SERVER_ERROR_LOG_FILE}") +"${PIHOLE_WEB_SERVER_ERROR_LOG_FILE}" +"${RESOLVCONF}" +"${DNSMASQ_CONF}" +"${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. @@ -230,6 +208,7 @@ copy_to_debug_log() { } initialize_debug() { + local system_uptime # Clear the screen so the debug log is readable clear show_disclaimer @@ -237,9 +216,13 @@ initialize_debug() { log_write "${COL_PURPLE}*** [ INITIALIZING ]${COL_NC}" # Timestamp the start of the log log_write "${INFO} $(date "+%Y-%m-%d:%H:%M:%S") debug log has been initialized." + # Uptime of the system + # credits to https://stackoverflow.com/questions/28353409/bash-format-uptime-to-show-days-hours-minutes + system_uptime=$(uptime | awk -F'( |,|:)+' '{if ($7=="min") m=$6; else {if ($7~/^day/){if ($9=="min") {d=$6;m=$8} else {d=$6;h=$8;m=$9}} else {h=$6;m=$7}}} {print d+0,"days,",h+0,"hours,",m+0,"minutes"}') + log_write "${INFO} System has been running for ${system_uptime}" } -# This is a function for visually displaying the curent test that is being run. +# 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() { @@ -253,15 +236,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 @@ -274,42 +249,38 @@ 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}" - # If it is any other branch, they are in a developement branch + 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 + # Replace new lines in the status with 12 spaces to make the output cleaner log_write "${INFO} Status: ${local_status//$'\n'/'\n '}" local local_diff local_diff=$(git diff) @@ -325,23 +296,43 @@ compare_local_version_to_git_version() { return 1 fi else - : + # There is no git directory so check if the web interface was disabled + local setup_vars_web_interface + setup_vars_web_interface=$(< ${PIHOLE_SETUP_VARS_FILE} grep ^INSTALL_WEB_INTERFACE | cut -d '=' -f2) + if [[ "${pihole_component}" == "Web" ]] && [[ "${setup_vars_web_interface}" == "false" ]]; then + log_write "${INFO} ${pihole_component}: Disabled in setupVars.conf via INSTALL_WEB_INTERFACE=false" + else + # Return an error message + log_write "${COL_RED}Directory ${git_dir} doesn't exist${COL_NC}" + # and exit with a non zero code + return 1 + fi fi } 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 @@ -357,14 +348,14 @@ check_component_versions() { get_program_version() { local program_name="${1}" - # Create a loval variable so this function can be safely reused + # Create a local variable so this function can be safely reused local program_version echo_current_diagnostic "${program_name} version" - # Evalutate the program we are checking, if it is any of the ones below, show the version + # Evaluate the program we are checking, if it is any of the ones below, show the version case "${program_name}" in - "lighttpd") program_version="$(${program_name} -v |& head -n1 | cut -d '/' -f2 | cut -d ' ' -f1)" + "lighttpd") program_version="$(${program_name} -v 2> /dev/null | head -n1 | cut -d '/' -f2 | cut -d ' ' -f1)" ;; - "php") program_version="$(${program_name} -v |& head -n1 | cut -d '-' -f1 | cut -d ' ' -f2)" + "php") program_version="$(${program_name} -v 2> /dev/null | head -n1 | cut -d '-' -f1 | cut -d ' ' -f2)" ;; # If a match is not found, show an error *) echo "Unrecognized program"; @@ -387,53 +378,58 @@ check_critical_program_versions() { get_program_version "php" } -is_os_supported() { - local os_to_check="${1}" - # Strip just the base name of the system using sed - # shellcheck disable=SC2001 - the_os=$(echo "${os_to_check}" | sed 's/ .*//') - # If the variable is one of our supported OSes, - case "${the_os}" in - # Print it in green - "Raspbian") log_write "${TICK} ${COL_GREEN}${os_to_check}${COL_NC}";; - "Ubuntu") log_write "${TICK} ${COL_GREEN}${os_to_check}${COL_NC}";; - "Fedora") log_write "${TICK} ${COL_GREEN}${os_to_check}${COL_NC}";; - "Debian") log_write "${TICK} ${COL_GREEN}${os_to_check}${COL_NC}";; - "CentOS") log_write "${TICK} ${COL_GREEN}${os_to_check}${COL_NC}";; - # If not, show it in red and link to our software requirements page - *) log_write "${CROSS} ${COL_RED}${os_to_check}${COL_NC} (${FAQ_HARDWARE_REQUIREMENTS})"; - esac -} +os_check() { + # This function gets a list of supported OS versions from a TXT record at versions.pi-hole.net + # and determines whether or not the script is running on one of those systems + local remote_os_domain valid_os valid_version detected_os detected_version cmdResult digReturnCode response + remote_os_domain=${OS_CHECK_DOMAIN_NAME:-"versions.pi-hole.net"} + + detected_os=$(grep "\bID\b" /etc/os-release | cut -d '=' -f2 | tr -d '"') + detected_version=$(grep VERSION_ID /etc/os-release | cut -d '=' -f2 | tr -d '"') + + cmdResult="$(dig +short -t txt "${remote_os_domain}" @ns1.pi-hole.net 2>&1; echo $?)" + #Get the return code of the previous command (last line) + digReturnCode="${cmdResult##*$'\n'}" + + # Extract dig response + response="${cmdResult%%$'\n'*}" + + 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 -get_distro_attributes() { - # Put the current Internal Field Separator into another variable so it can be restored later - OLD_IFS="$IFS" - # Store the distro info in an array and make it global since the OS won't change, - # but we'll keep it within the function for better unit testing - local distro_info - #shellcheck disable=SC2016 - IFS=$'\r\n' command eval 'distro_info=( $(cat /etc/*release) )' + log_write "${INFO} dig return code: ${digReturnCode}" + log_write "${INFO} dig response: ${response}" - # Set a named variable for better readability - local distro_attribute - # For each line found in an /etc/*release file, - for distro_attribute in "${distro_info[@]}"; do - # store the key in a variable - local pretty_name_key - pretty_name_key=$(echo "${distro_attribute}" | grep "PRETTY_NAME" | cut -d '=' -f1) - # we need just the OS PRETTY_NAME, - if [[ "${pretty_name_key}" == "PRETTY_NAME" ]]; then - # so save in in a variable when we find it - PRETTY_NAME_VALUE=$(echo "${distro_attribute}" | grep "PRETTY_NAME" | cut -d '=' -f2- | tr -d '"') - # then pass it as an argument that checks if the OS is supported - is_os_supported "${PRETTY_NAME_VALUE}" + 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}" else - # Since we only need the pretty name, we can just skip over anything that is not a match - : + 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}" fi - done - # Set the IFS back to what it was - IFS="$OLD_IFS" + 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}" + fi } diagnose_operating_system() { @@ -442,10 +438,13 @@ diagnose_operating_system() { # Display the current test that is running echo_current_diagnostic "Operating system" + # 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 # display the attributes to the user from the function made earlier - get_distro_attributes + os_check else # If it doesn't exist, it's not a system we currently support and link to FAQ log_write "${CROSS} ${COL_RED}${error_msg}${COL_NC} (${FAQ_HARDWARE_REQUIREMENTS})" @@ -482,6 +481,58 @@ check_selinux() { fi } +check_firewalld() { + # FirewallD ships by default on Fedora/CentOS/RHEL and enabled upon clean install + # FirewallD is not configured by the installer and is the responsibility of the user + echo_current_diagnostic "FirewallD" + # Check if FirewallD service is enabled + if command -v systemctl &> /dev/null; then + # get its status via systemctl + local firewalld_status + firewalld_status=$(systemctl is-active firewalld) + log_write "${INFO} ${COL_GREEN}Firewalld service ${firewalld_status}${COL_NC}"; + if [ "${firewalld_status}" == "active" ]; then + # test common required service ports + local firewalld_enabled_services + firewalld_enabled_services=$(firewall-cmd --list-services) + local firewalld_expected_services=("http" "dns" "dhcp" "dhcpv6") + for i in "${firewalld_expected_services[@]}"; do + if [[ "${firewalld_enabled_services}" =~ ${i} ]]; then + log_write "${TICK} ${COL_GREEN} Allow Service: ${i}${COL_NC}"; + else + log_write "${CROSS} ${COL_RED} Allow Service: ${i}${COL_NC} (${FAQ_HARDWARE_REQUIREMENTS_FIREWALLD})" + fi + done + # check for custom FTL FirewallD zone + local firewalld_zones + firewalld_zones=$(firewall-cmd --get-zones) + if [[ "${firewalld_zones}" =~ "ftl" ]]; then + log_write "${TICK} ${COL_GREEN}FTL Custom Zone Detected${COL_NC}"; + # check FTL custom zone interface: lo + local firewalld_ftl_zone_interfaces + firewalld_ftl_zone_interfaces=$(firewall-cmd --zone=ftl --list-interfaces) + if [[ "${firewalld_ftl_zone_interfaces}" =~ "lo" ]]; then + log_write "${TICK} ${COL_GREEN} Local Interface Detected${COL_NC}"; + else + log_write "${CROSS} ${COL_RED} Local Interface Not Detected${COL_NC} (${FAQ_HARDWARE_REQUIREMENTS_FIREWALLD})" + fi + # check FTL custom zone port: 4711 + local firewalld_ftl_zone_ports + firewalld_ftl_zone_ports=$(firewall-cmd --zone=ftl --list-ports) + if [[ "${firewalld_ftl_zone_ports}" =~ "4711/tcp" ]]; then + log_write "${TICK} ${COL_GREEN} FTL Port 4711/tcp Detected${COL_NC}"; + else + log_write "${CROSS} ${COL_RED} FTL Port 4711/tcp Not Detected${COL_NC} (${FAQ_HARDWARE_REQUIREMENTS_FIREWALLD})" + fi + else + log_write "${CROSS} ${COL_RED}FTL Custom Zone Not Detected${COL_NC} (${FAQ_HARDWARE_REQUIREMENTS_FIREWALLD})" + fi + fi + else + log_write "${TICK} ${COL_GREEN}Firewalld service not detected${COL_NC}"; + fi +} + processor_check() { echo_current_diagnostic "Processor" # Store the processor type in a variable @@ -494,7 +545,7 @@ processor_check() { else # Check if the architecture is currently supported for FTL case "${PROCESSOR}" in - "amd64") log_write "${TICK} ${COL_GREEN}${PROCESSOR}${COL_NC}" + "amd64" | "x86_64") log_write "${TICK} ${COL_GREEN}${PROCESSOR}${COL_NC}" ;; "armv6l") log_write "${TICK} ${COL_GREEN}${PROCESSOR}${COL_NC}" ;; @@ -510,6 +561,27 @@ processor_check() { fi } +disk_usage() { + local file_system + local hide + + echo_current_diagnostic "Disk usage" + mapfile -t file_system < <(df -h) + + # 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 + # Additional keywords can be added, separated by "|" + hide="curlftpfs" + + # only show those lines not containing a sensitive phrase + for line in "${file_system[@]}"; do + if [[ ! $line =~ $hide ]]; then + log_write " ${line}" + fi + done +} + parse_setup_vars() { echo_current_diagnostic "Setup variables" # If the file exists, @@ -529,43 +601,11 @@ parse_locale() { parse_file "${pihole_locale}" } -does_ip_match_setup_vars() { - # Check for IPv4 or 6 - local protocol="${1}" - # IP address to check for - local ip_address="${2}" - # See what IP is in the setupVars.conf file - local setup_vars_ip - setup_vars_ip=$(< ${PIHOLE_SETUP_VARS_FILE} grep IPV"${protocol}"_ADDRESS | cut -d '=' -f2) - # If it's an IPv6 address - if [[ "${protocol}" == "6" ]]; then - # Strip off the / (CIDR notation) - if [[ "${ip_address%/*}" == "${setup_vars_ip%/*}" ]]; then - # if it matches, show it in green - log_write " ${COL_GREEN}${ip_address%/*}${COL_NC} matches the IP found in ${PIHOLE_SETUP_VARS_FILE}" - else - # otherwise show it in red with an FAQ URL - log_write " ${COL_RED}${ip_address%/*}${COL_NC} does not match the IP found in ${PIHOLE_SETUP_VARS_FILE} (${FAQ_ULA})" - fi - - else - # if the protocol isn't 6, it's 4 so no need to strip the CIDR notation - # since it exists in the setupVars.conf that way - if [[ "${ip_address}" == "${setup_vars_ip}" ]]; then - # show in green if it matches - log_write " ${COL_GREEN}${ip_address}${COL_NC} matches the IP found in ${PIHOLE_SETUP_VARS_FILE}" - else - # otherwise show it in red - log_write " ${COL_RED}${ip_address}${COL_NC} does not match the IP found in ${PIHOLE_SETUP_VARS_FILE} (${FAQ_ULA})" - fi - fi -} - detect_ip_addresses() { # First argument should be a 4 or a 6 local protocol=${1} # Use ip to show the addresses for the chosen protocol - # Store the values in an arry so they can be looped through + # Store the values in an array so they can be looped through # Get the lines that are in the file(s) and store them in an array for parsing later mapfile -t ip_addr_list < <(ip -"${protocol}" addr show dev "${PIHOLE_INTERFACE}" | awk -F ' ' '{ for(i=1;i<=NF;i++) if ($i ~ '/^inet/') print $(i+1) }') @@ -577,8 +617,7 @@ detect_ip_addresses() { log_write "${TICK} IPv${protocol} address(es) bound to the ${PIHOLE_INTERFACE} interface:" # Since there may be more than one IP address, store them in an array for i in "${!ip_addr_list[@]}"; do - # For each one in the list, print it out - does_ip_match_setup_vars "${protocol}" "${ip_addr_list[$i]}" + log_write " ${ip_addr_list[$i]}" done # Print a blank line just for formatting log_write "" @@ -587,13 +626,6 @@ detect_ip_addresses() { log_write "${CROSS} ${COL_RED}No IPv${protocol} address(es) found on the ${PIHOLE_INTERFACE}${COL_NC} interface.\\n" return 1 fi - # If the protocol is v6 - if [[ "${protocol}" == "6" ]]; then - # let the user know that as long as there is one green address, things should be ok - log_write " ^ Please note that you may have more than one IP address listed." - log_write " As long as one of them is green, and it matches what is in ${PIHOLE_SETUP_VARS_FILE}, there is no need for concern.\\n" - log_write " The link to the FAQ is for an issue that sometimes occurs when the IPv6 address changes, which is why we check for it.\\n" - fi } ping_ipv4_or_ipv6() { @@ -617,15 +649,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 | 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, @@ -662,19 +699,21 @@ ping_internet() { } compare_port_to_service_assigned() { - local service_name="${1}" - # The programs we use may change at some point, so they are in a varible here - local resolver="pihole-FTL" - local web_server="lighttpd" - local ftl="pihole-FTL" + local service_name + local expected_service + local port + + service_name="${2}" + expected_service="${1}" + port="${3}" # If the service is a Pi-hole service, highlight it in green - if [[ "${service_name}" == "${resolver}" ]] || [[ "${service_name}" == "${web_server}" ]] || [[ "${service_name}" == "${ftl}" ]]; then - log_write "[${COL_GREEN}${port_number}${COL_NC}] is in use by ${COL_GREEN}${service_name}${COL_NC}" + if [[ "${service_name}" == "${expected_service}" ]]; then + log_write "${TICK} ${COL_GREEN}${port}${COL_NC} is in use by ${COL_GREEN}${service_name}${COL_NC}" # Otherwise, else # Show the service name in red since it's non-standard - log_write "[${COL_RED}${port_number}${COL_NC}] is in use by ${COL_RED}${service_name}${COL_NC} (${FAQ_HARDWARE_REQUIREMENTS_PORTS})" + log_write "${CROSS} ${COL_RED}${port}${COL_NC} is in use by ${COL_RED}${service_name}${COL_NC} (${FAQ_HARDWARE_REQUIREMENTS_PORTS})" fi } @@ -690,36 +729,47 @@ check_required_ports() { # Sort the addresses and remove duplicates while IFS= read -r line; do ports_in_use+=( "$line" ) - done < <( lsof -iTCP -sTCP:LISTEN -P -n +c 10 ) + done < <( ss --listening --numeric --tcp --udp --processes --no-header ) # Now that we have the values stored, for i in "${!ports_in_use[@]}"; do # loop through them and assign some local variables local service_name - service_name=$(echo "${ports_in_use[$i]}" | awk '{print $1}') + service_name=$(echo "${ports_in_use[$i]}" | awk '{gsub(/users:\(\("/,"",$7);gsub(/".*/,"",$7);print $7}') local protocol_type - protocol_type=$(echo "${ports_in_use[$i]}" | awk '{print $5}') + protocol_type=$(echo "${ports_in_use[$i]}" | awk '{print $1}') local port_number - port_number="$(echo "${ports_in_use[$i]}" | awk '{print $9}')" + port_number="$(echo "${ports_in_use[$i]}" | awk '{print $5}')" # | awk '{gsub(/^.*:/,"",$5);print $5}') - # Skip the line if it's the titles of the columns the lsof command produces - if [[ "${service_name}" == COMMAND ]]; then - continue - fi # Use a case statement to determine if the right services are using the right ports - case "$(echo "$port_number" | rev | cut -d: -f1 | rev)" in - 53) compare_port_to_service_assigned "${resolver}" + case "$(echo "${port_number}" | rev | cut -d: -f1 | rev)" in + 53) compare_port_to_service_assigned "${resolver}" "${service_name}" "${protocol_type}:${port_number}" ;; - 80) compare_port_to_service_assigned "${web_server}" + 80) compare_port_to_service_assigned "${web_server}" "${service_name}" "${protocol_type}:${port_number}" ;; - 4711) compare_port_to_service_assigned "${ftl}" + 4711) compare_port_to_service_assigned "${ftl}" "${service_name}" "${protocol_type}:${port_number}" ;; # If it's not a default port that Pi-hole needs, just print it out for the user to see - *) log_write "${port_number} ${service_name} (${protocol_type})"; + *) log_write " ${protocol_type}:${port_number} is in use by ${service_name:=}"; esac done } +ip_command() { + # Obtain and log information from "ip XYZ show" commands + echo_current_diagnostic "${2}" + local entries=() + mapfile -t entries < <(ip "${1}" show) + for line in "${entries[@]}"; do + log_write " ${line}" + done +} + +check_ip_command() { + ip_command "addr" "Network interfaces and addresses" + ip_command "route" "Network routing table" +} + check_networking() { # Runs through several of the functions made earlier; we just clump them # together since they are all related to the networking aspect of things @@ -728,7 +778,9 @@ check_networking() { detect_ip_addresses "6" ping_gateway "4" ping_gateway "6" - check_required_ports + # 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 "${DOCKER_VERSION}" ] && check_required_ports } check_x_headers() { @@ -738,39 +790,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') - # Store what the X-Header shoud be in variables for comparision later - local block_page_working - block_page_working="X-Pi-hole: A black hole for Internet advertisements." + 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 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 dashbord as above, if the X-Header matches what a working system shoud 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 - # Othewise, it's a failure since the X-Headers either don't exist or have been modified in some way + # 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 } @@ -781,13 +818,13 @@ dig_at() { # Store the arguments as variables with names local protocol="${1}" - local IP="${2}" echo_current_diagnostic "Name resolution (IPv${protocol}) using a random blocked domain and a known ad-serving domain" # Set more local variables # We need to test name resolution locally, via Pi-hole, and via a public resolver local local_dig - local pihole_dig local remote_dig + local interfaces + local addresses # Use a static domain that we know has IPv4 and IPv6 to avoid false positives # Sometimes the randomly chosen domains don't use IPv6, or something else is wrong with them local remote_url="doubleclick.com" @@ -796,15 +833,15 @@ dig_at() { if [[ ${protocol} == "6" ]]; then # Set the IPv6 variables and record type local local_address="::1" - local pihole_address="${IP}" local remote_address="2001:4860:4860::8888" + local sed_selector="inet6" local record_type="AAAA" - # Othwerwise, it should be 4 + # Otherwise, it should be 4 else # so use the IPv4 values local local_address="127.0.0.1" - local pihole_address="${IP}" local remote_address="8.8.8.8" + local sed_selector="inet" local record_type="A" fi @@ -812,34 +849,59 @@ dig_at() { # 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=$(sqlite3 "${PIHOLE_GRAVITY_DB_FILE}" "SELECT domain FROM vw_gravity ORDER BY RANDOM() LIMIT 1") - - # First, do a dig on localhost to see if Pi-hole can use itself to block a domain - if local_dig=$(dig +tries=1 +time=2 -"${protocol}" "${random_url}" @${local_address} +short "${record_type}"); then - # If it can, show sucess - log_write "${TICK} ${random_url} ${COL_GREEN}is ${local_dig}${COL_NC} via ${COL_CYAN}localhost$COL_NC (${local_address})" - else - # Otherwise, show a failure - log_write "${CROSS} ${COL_RED}Failed to resolve${COL_NC} ${random_url} via ${COL_RED}localhost${COL_NC} (${local_address})" - fi + random_url=$(pihole-FTL sqlite3 "${PIHOLE_GRAVITY_DB_FILE}" "SELECT domain FROM vw_gravity ORDER BY RANDOM() LIMIT 1") # 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 # just asing itself locally - # The default timeouts and tries are reduced in case the DNS server isn't working, so the user isn't waiting for too long - - # If Pi-hole can dig itself from it's IP (not the loopback address) - if pihole_dig=$(dig +tries=1 +time=2 -"${protocol}" "${random_url}" @"${pihole_address}" +short "${record_type}"); then - # show a success - log_write "${TICK} ${random_url} ${COL_GREEN}is ${pihole_dig}${COL_NC} via ${COL_CYAN}Pi-hole${COL_NC} (${pihole_address})" - else - # Othewise, show a failure - log_write "${CROSS} ${COL_RED}Failed to resolve${COL_NC} ${random_url} via ${COL_RED}Pi-hole${COL_NC} (${pihole_address})" - fi + # The default timeouts and tries are reduced in case the DNS server isn't working, so the user isn't + # waiting for too long + # + # Turn off history expansion such that the "!" in the sed command cannot do silly things + set +H + # Get interfaces + # sed logic breakdown: + # / master /d; + # Removes all interfaces that are slaves of others (e.g. virtual docker interfaces) + # /UP/!d; + # Removes all interfaces which are not UP + # s/^[0-9]*: //g; + # Removes interface index + # s/@.*//g; + # Removes everything after @ (if found) + # s/: <.*//g; + # Removes everything after the interface name + interfaces="$(ip link show | sed "/ master /d;/UP/!d;s/^[0-9]*: //g;s/@.*//g;s/: <.*//g;")" + + while IFS= read -r iface ; do + # Get addresses of current interface + # sed logic breakdown: + # /inet(|6) /!d; + # Removes all lines from ip a that do not contain either "inet " or "inet6 " + # s/^.*inet(|6) //g; + # Removes all leading whitespace as well as the "inet " or "inet6 " string + # s/\/.*$//g; + # Removes CIDR and everything thereafter (e.g., scope properties) + addresses="$(ip address show dev "${iface}" | sed "/${sed_selector} /!d;s/^.*${sed_selector} //g;s/\/.*$//g;")" + if [ -n "${addresses}" ]; then + while IFS= read -r local_address ; do + # Check if Pi-hole can use itself to block a domain + if local_dig=$(dig +tries=1 +time=2 -"${protocol}" "${random_url}" @"${local_address}" +short "${record_type}"); then + # If it can, show success + log_write "${TICK} ${random_url} ${COL_GREEN}is ${local_dig}${COL_NC} on ${COL_CYAN}${iface}${COL_NC} (${COL_CYAN}${local_address}${COL_NC})" + else + # Otherwise, show a failure + log_write "${CROSS} ${COL_RED}Failed to resolve${COL_NC} ${random_url} on ${COL_RED}${iface}${COL_NC} (${COL_RED}${local_address}${COL_NC})" + fi + done <<< "${addresses}" + else + log_write "${TICK} No IPv${protocol} address available on ${COL_CYAN}${iface}${COL_NC}" + fi + done <<< "${interfaces}" # Finally, we need to make sure legitimate queries can out to the Internet using an external, public DNS server # We are using the static remote_url here instead of a random one because we know it works with IPv4 and IPv6 - if remote_dig=$(dig +tries=1 +time=2 -"${protocol}" "${remote_url}" @${remote_address} +short "${record_type}" | head -n1); then + if remote_dig=$(dig +tries=1 +time=2 -"${protocol}" "${remote_url}" @"${remote_address}" +short "${record_type}" | head -n1); then # If successful, the real IP of the domain will be returned instead of Pi-hole's IP log_write "${TICK} ${remote_url} ${COL_GREEN}is ${remote_dig}${COL_NC} via ${COL_CYAN}a remote, public DNS server${COL_NC} (${remote_address})" else @@ -863,10 +925,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 @@ -880,6 +953,18 @@ process_status(){ done } +ftl_full_status(){ + # if using systemd print the full status of pihole-FTL + echo_current_diagnostic "Pi-hole-FTL full status" + local FTL_status + if command -v systemctl &> /dev/null; then + FTL_status=$(systemctl status --full --no-pager pihole-FTL.service) + log_write " ${FTL_status}" + else + log_write "${INFO} systemctl: command not found" + 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 @@ -896,8 +981,8 @@ 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 - new_line=$(echo "${line}" | sed -e 's/#.*$//' -e '/^$/d') + # 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 # Put it into the array @@ -942,7 +1027,7 @@ parse_file() { local file_lines # For each line in the file, for file_lines in "${file_info[@]}"; do - if [[ ! -z "${file_lines}" ]]; then + if [[ -n "${file_lines}" ]]; then # don't include the Web password hash [[ "${file_lines}" =~ ^\#.*$ || ! "${file_lines}" || "${file_lines}" == "WEBPASSWORD="* ]] && continue # otherwise, display the lines of the file @@ -954,20 +1039,16 @@ parse_file() { } check_name_resolution() { - # Check name resoltion 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 "${IPV4_ADDRESS%/*}" - # If IPv6 enabled, - if [[ "${IPV6_ADDRESS}" ]]; then - # check resolution - dig_at 6 "${IPV6_ADDRESS%/*}" - fi + dig_at 4 + dig_at 6 } # This function can check a directory exists # Pi-hole has files in several places, so we will reuse this function dir_check() { - # Set the first argument passed to tihs function as a named variable for better readability + # Set the first argument passed to this function as a named variable for better readability local directory="${1}" # Display the current test that is running echo_current_diagnostic "contents of ${COL_CYAN}${directory}${COL_NC}" @@ -985,14 +1066,14 @@ dir_check() { } list_files_in_dir() { - # Set the first argument passed to tihs function as a named variable for better readability + # Set the first argument passed to this function as a named variable for better readability local dir_to_parse="${1}" # Store the files found in an array mapfile -t files_found < <(ls "${dir_to_parse}") # For each file in the array, for each_file in "${files_found[@]}"; do if [[ -d "${dir_to_parse}/${each_file}" ]]; then - # If it's a directoy, do nothing + # If it's a directory, do nothing : elif [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_DEBUG_LOG}" ]] || \ [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_RAW_BLOCKLIST_FILES}" ]] || \ @@ -1004,17 +1085,21 @@ list_files_in_dir() { : 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 -ld "${dir_to_parse}"/"${each_file}")" + 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 + log_write "\\n${COL_GREEN}$(ls -lhd "${dir_to_parse}"/"${each_file}")${COL_NC}" + make_array_from_file "${dir_to_parse}/${each_file}" else # Then, parse the file's content into an array so each line can be analyzed if need be for i in "${!REQUIRED_FILES[@]}"; do if [[ "${dir_to_parse}/${each_file}" == "${REQUIRED_FILES[$i]}" ]]; then # display the filename - log_write "\\n${COL_GREEN}$(ls -ld "${dir_to_parse}"/"${each_file}")${COL_NC}" + log_write "\\n${COL_GREEN}$(ls -lhd "${dir_to_parse}"/"${each_file}")${COL_NC}" # Check if the file we want to view has a limit (because sometimes we just need a little bit of info from the file, not the entire thing) case "${dir_to_parse}/${each_file}" in - # If it's Web server error log, just give the first 25 lines - "${PIHOLE_WEB_SERVER_ERROR_LOG_FILE}") make_array_from_file "${dir_to_parse}/${each_file}" 25 + # If it's Web server error log, give the first and last 25 lines + "${PIHOLE_WEB_SERVER_ERROR_LOG_FILE}") head_tail_log "${dir_to_parse}/${each_file}" 25 ;; # Same for the FTL log "${PIHOLE_FTL_LOG}") head_tail_log "${dir_to_parse}/${each_file}" 35 @@ -1049,6 +1134,7 @@ show_content_of_pihole_files() { show_content_of_files_in_dir "${WEB_SERVER_LOG_DIRECTORY}" show_content_of_files_in_dir "${LOG_DIRECTORY}" show_content_of_files_in_dir "${SHM_DIRECTORY}" + show_content_of_files_in_dir "${ETC}" } head_tail_log() { @@ -1090,7 +1176,7 @@ show_db_entries() { IFS=$'\r\n' local entries=() mapfile -t entries < <(\ - sqlite3 "${PIHOLE_GRAVITY_DB_FILE}" \ + pihole-FTL sqlite3 "${PIHOLE_GRAVITY_DB_FILE}" \ -cmd ".headers on" \ -cmd ".mode column" \ -cmd ".width ${widths}" \ @@ -1104,46 +1190,93 @@ show_db_entries() { IFS="$OLD_IFS" } +show_FTL_db_entries() { + local title="${1}" + local query="${2}" + local widths="${3}" + + echo_current_diagnostic "${title}" + + OLD_IFS="$IFS" + IFS=$'\r\n' + local entries=() + mapfile -t entries < <(\ + pihole-FTL sqlite3 "${PIHOLE_FTL_DB_FILE}" \ + -cmd ".headers on" \ + -cmd ".mode column" \ + -cmd ".width ${widths}" \ + "${query}"\ + ) + + for line in "${entries[@]}"; do + log_write " ${line}" + done + + IFS="$OLD_IFS" +} + +check_dhcp_servers() { + echo_current_diagnostic "Discovering active DHCP servers (takes 10 seconds)" + + OLD_IFS="$IFS" + IFS=$'\n' + local entries=() + mapfile -t entries < <(pihole-FTL dhcp-discover & spinner) + + for line in "${entries[@]}"; do + log_write " ${line}" + done + + IFS="$OLD_IFS" +} + show_groups() { - show_db_entries "Groups" "SELECT * FROM \"group\"" "4 4 30 50" + show_db_entries "Groups" "SELECT id,CASE enabled WHEN '0' THEN ' 0' WHEN '1' THEN ' 1' ELSE enabled END enabled,name,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,description FROM \"group\"" "4 7 50 19 19 50" } show_adlists() { - show_db_entries "Adlists" "SELECT id,address,enabled,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,comment FROM adlist" "4 100 7 19 19 50" - show_db_entries "Adlist groups" "SELECT * FROM adlist_by_group" "4 4" + show_db_entries "Adlists" "SELECT id,CASE enabled WHEN '0' THEN ' 0' WHEN '1' THEN ' 1' ELSE enabled END enabled,GROUP_CONCAT(adlist_by_group.group_id) group_ids,address,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,comment FROM adlist LEFT JOIN adlist_by_group ON adlist.id = adlist_by_group.adlist_id GROUP BY id;" "5 7 12 100 19 19 50" } -show_whitelist() { - show_db_entries "Exact whitelist" "SELECT id,domain,enabled,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,comment FROM whitelist" "4 100 7 19 19 50" - show_db_entries "Exact whitelist groups" "SELECT * FROM whitelist_by_group" "4 4" - show_db_entries "Regex whitelist" "SELECT id,domain,enabled,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,comment FROM regex_whitelist" "4 100 7 19 19 50" - show_db_entries "Regex whitelist groups" "SELECT * FROM regex_whitelist_by_group" "4 4" +show_domainlist() { + show_db_entries "Domainlist (0/1 = exact white-/blacklist, 2/3 = regex white-/blacklist)" "SELECT id,CASE type WHEN '0' THEN '0 ' WHEN '1' THEN ' 1 ' WHEN '2' THEN ' 2 ' WHEN '3' THEN ' 3' ELSE type END type,CASE enabled WHEN '0' THEN ' 0' WHEN '1' THEN ' 1' ELSE enabled END enabled,GROUP_CONCAT(domainlist_by_group.group_id) group_ids,domain,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,comment FROM domainlist LEFT JOIN domainlist_by_group ON domainlist.id = domainlist_by_group.domainlist_id GROUP BY id;" "5 4 7 12 100 19 19 50" } -show_blacklist() { - show_db_entries "Exact blacklist" "SELECT id,domain,enabled,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,comment FROM blacklist" "4 100 7 19 19 50" - show_db_entries "Exact blacklist groups" "SELECT * FROM blacklist_by_group" "4 4" - show_db_entries "Regex blacklist" "SELECT id,domain,enabled,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,comment FROM regex_blacklist" "4 100 7 19 19 50" - show_db_entries "Regex blacklist groups" "SELECT * FROM regex_blacklist_by_group" "4 4" +show_clients() { + show_db_entries "Clients" "SELECT id,GROUP_CONCAT(client_by_group.group_id) group_ids,ip,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,comment FROM client LEFT JOIN client_by_group ON client.id = client_by_group.client_id GROUP BY id;" "4 12 100 19 19 50" +} + +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 List and Database" + echo_current_diagnostic "Gravity Database" - local gravity_permissions - gravity_permissions=$(ls -ld "${PIHOLE_GRAVITY_DB_FILE}") - log_write "${COL_GREEN}${gravity_permissions}${COL_NC}" + database_permissions "${PIHOLE_GRAVITY_DB_FILE}" - local gravity_size - gravity_size=$(sqlite3 "${PIHOLE_GRAVITY_DB_FILE}" "SELECT COUNT(*) FROM vw_gravity") - log_write " Size (excluding blacklist): ${COL_CYAN}${gravity_size}${COL_NC} entries" + # 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="$(date -d @"${gravity_updated_raw}")" + log_write " Last gravity run finished at: ${COL_CYAN}${gravity_updated}${COL_NC}" log_write "" OLD_IFS="$IFS" IFS=$'\r\n' local gravity_sample=() - mapfile -t gravity_sample < <(sqlite3 "${PIHOLE_GRAVITY_DB_FILE}" "SELECT domain FROM vw_gravity LIMIT 10") - log_write " ${COL_CYAN}----- First 10 Domains -----${COL_NC}" + mapfile -t gravity_sample < <(pihole-FTL sqlite3 "${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 log_write " ${line}" @@ -1153,72 +1286,114 @@ analyze_gravity_list() { IFS="$OLD_IFS" } -analyze_pihole_log() { - echo_current_diagnostic "Pi-hole log" - local head_line - # 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' - local pihole_log_permissions - pihole_log_permissions=$(ls -ld "${PIHOLE_LOG}") - log_write "${COL_GREEN}${pihole_log_permissions}${COL_NC}" - local pihole_log_head=() - mapfile -t pihole_log_head < <(head -n 20 ${PIHOLE_LOG}) - log_write " ${COL_CYAN}-----head of $(basename ${PIHOLE_LOG})------${COL_NC}" - local error_to_check_for - local line_to_obfuscate - local obfuscated_line - for head_line in "${pihole_log_head[@]}"; 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 "${head_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 "${head_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}${head_line}${COL_NC} (${FAQ_BAD_ADDRESS})" - else - # If the variable does not a value (the current default behavior), so do not obfuscate anything - if [[ -z ${OBFUSCATE} ]]; then - log_write " ${head_line}" - # Othwerise, a flag was passed to this command to obfuscate domains in the log +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 "${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 + 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 + + 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 - # 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 substitue 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 " ${head_line}" - fi + printf "\r${_spin:_i++%${#_spin}:1} %02d:%02d:%02d" $((_elapsed/3600)) $(((_elapsed/60)%60)) $((_elapsed%60)) >"$(tty)" fi - fi - done - log_write "" - # Set the IFS back to what it was - IFS="$OLD_IFS" + 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 } -tricorder_use_nc_or_curl() { - # Users can submit their debug logs using nc (unencrypted) or curl (encrypted) if available - # Check for curl first since encryption is a good thing - if command -v curl &> /dev/null; then - # If the command exists, - log_write " * Using ${COL_GREEN}curl${COL_NC} for transmission." - # transmit he log via TLS and store the token returned in a variable - tricorder_token=$(curl --silent --upload-file ${PIHOLE_DEBUG_LOG} https://tricorder.pi-hole.net:${TRICORDER_SSL_PORT_NUMBER}) - # Otherwise, - else - # use net cat - log_write "${INFO} Using ${COL_YELLOW}netcat${COL_NC} for transmission." - # Save the token returned by our server in a variable - tricorder_token=$(< ${PIHOLE_DEBUG_LOG} nc tricorder.pi-hole.net ${TRICORDER_NC_PORT_NUMBER}) +analyze_pihole_log() { + echo_current_diagnostic "Pi-hole log" + local pihole_log_permissions + local logging_enabled + + logging_enabled=$(grep -c "^log-queries" /etc/dnsmasq.d/01-pihole.conf) + if [[ "${logging_enabled}" == "0" ]]; then + # Inform user that logging has been disabled and pihole.log does not contain queries + log_write "${INFO} Query logging is disabled" + log_write "" + fi + + pihole_log_permissions=$(ls -lhd "${PIHOLE_LOG}") + log_write "${COL_GREEN}${pihole_log_permissions}${COL_NC}" + head_tail_log "${PIHOLE_LOG}" 20 +} + +curl_to_tricorder() { + # Users can submit their debug logs using curl (encrypted) + log_write " * Using ${COL_GREEN}curl${COL_NC} for transmission." + # transmit the log via TLS and store the token returned in a variable + tricorder_token=$(curl --silent --fail --show-error --upload-file ${PIHOLE_DEBUG_LOG} https://tricorder.pi-hole.net 2>&1) + if [[ "${tricorder_token}" != "https://tricorder.pi-hole.net/"* ]]; then + log_write " * ${COL_GREEN}curl${COL_NC} failed, contact Pi-hole support for assistance." + # Log curl error (if available) + if [ -n "${tricorder_token}" ]; then + log_write " * Error message: ${COL_RED}${tricorder_token}${COL_NC}\\n" + tricorder_token="" + fi fi } @@ -1226,7 +1401,7 @@ tricorder_use_nc_or_curl() { 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 @@ -1236,27 +1411,29 @@ upload_to_tricorder() { log_write "${TICK} ${COL_GREEN}** FINISHED DEBUGGING! **${COL_NC}\\n" # Provide information on what they should do with their token - log_write " * The debug log can be uploaded to tricorder.pi-hole.net for sharing with developers only." - log_write " * For more information, see: ${TRICORDER_CONTEST}" - log_write " * If available, we'll use openssl to upload the log, otherwise it will fall back to netcat." - # If pihole -d is running automatically (usually throught the dashboard) + log_write " * The debug log can be uploaded to tricorder.pi-hole.net for sharing with developers only." + + # If pihole -d is running automatically if [[ "${AUTOMATED}" ]]; then # let the user know log_write "${INFO} Debug script running in automated mode" # and then decide again which tool to use to submit it - tricorder_use_nc_or_curl + curl_to_tricorder # If we're not running in automated mode, else - echo "" - # give the user a choice of uploading it or not - # Users can review the log file locally (or the output of the script since they are the same) and try to self-diagnose their problem - read -r -p "[?] Would you like to upload the log? [y/N] " response - case ${response} in - # If they say yes, run our function for uploading the log - [yY][eE][sS]|[yY]) tricorder_use_nc_or_curl;; - # If they choose no, just exit out of the script - *) log_write " * Log will ${COL_GREEN}NOT${COL_NC} be uploaded to tricorder.";exit; - esac + # if not being called from the web interface + if [[ ! "${WEBCALL}" ]]; then + echo "" + # give the user a choice of uploading it or not + # Users can review the log file locally (or the output of the script since they are the same) and try to self-diagnose their problem + read -r -p "[?] Would you like to upload the log? [y/N] " response + case ${response} in + # If they say yes, run our function for uploading the log + [yY][eE][sS]|[yY]) curl_to_tricorder;; + # If they choose no, just exit out of the script + *) log_write " * Log will ${COL_GREEN}NOT${COL_NC} be uploaded to tricorder.\\n * A local copy of the debug log can be found at: ${COL_CYAN}${PIHOLE_DEBUG_LOG}${COL_NC}\\n";exit; + esac + fi fi # Check if tricorder.pi-hole.net is reachable and provide token # along with some additional useful information @@ -1264,20 +1441,25 @@ upload_to_tricorder() { # Again, try to make this visually striking so the user realizes they need to do something with this information # Namely, provide the Pi-hole devs with the token log_write "" - log_write "${COL_PURPLE}***********************************${COL_NC}" - log_write "${COL_PURPLE}***********************************${COL_NC}" + log_write "${COL_PURPLE}*****************************************************************${COL_NC}" + log_write "${COL_PURPLE}*****************************************************************${COL_NC}\\n" log_write "${TICK} Your debug token is: ${COL_GREEN}${tricorder_token}${COL_NC}" - log_write "${COL_PURPLE}***********************************${COL_NC}" - log_write "${COL_PURPLE}***********************************${COL_NC}" + log_write "${INFO}${COL_RED} Logs are deleted 48 hours after upload.${COL_NC}\\n" + log_write "${COL_PURPLE}*****************************************************************${COL_NC}" + log_write "${COL_PURPLE}*****************************************************************${COL_NC}" log_write "" - log_write " * Provide the token above to the Pi-hole team for assistance at" - log_write " * ${FORUMS_URL}" - log_write " * Your log will self-destruct on our server after ${COL_RED}48 hours${COL_NC}." + log_write " * Provide the token above to the Pi-hole team for assistance at ${FORUMS_URL}" + # If no token was generated else # Show an error and some help instructions - 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." + # Skip this if being called from web interface and autmatic 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 " * Please try again or contact the Pi-hole team for assistance." + fi fi # Finally, show where the log file is no matter the outcome of the function so users can look at it log_write " * A local copy of the debug log can be found at: ${COL_CYAN}${PIHOLE_DEBUG_LOG}${COL_NC}\\n" @@ -1293,18 +1475,25 @@ check_component_versions check_critical_program_versions diagnose_operating_system check_selinux +check_firewalld processor_check +disk_usage +check_ip_command check_networking check_name_resolution +check_dhcp_servers process_status +ftl_full_status parse_setup_vars check_x_headers +analyze_ftl_db analyze_gravity_list show_groups +show_domainlist +show_clients show_adlists -show_whitelist -show_blacklist show_content_of_pihole_files +show_messages parse_locale analyze_pihole_log copy_to_debug_log diff --git a/advanced/Scripts/piholeLogFlush.sh b/advanced/Scripts/piholeLogFlush.sh index 51e94d7cbd..3473fad591 100755 --- a/advanced/Scripts/piholeLogFlush.sh +++ b/advanced/Scripts/piholeLogFlush.sh @@ -11,6 +11,11 @@ colfile="/opt/pihole/COL_TABLE" source ${colfile} +# In case we're running at the same time as a system logrotate, use a +# separate logrotate state file to prevent stepping on each other's +# toes. +STATEFILE="/var/lib/logrotate/pihole" + # Determine database location # Obtain DBFILE=... setting from pihole-FTL.db # Constructed to return nothing when @@ -26,45 +31,45 @@ 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 if command -v /usr/sbin/logrotate >/dev/null; then # Logrotate once - /usr/sbin/logrotate --force /etc/pihole/logrotate + /usr/sbin/logrotate --force --state "${STATEFILE}" /etc/pihole/logrotate else # Copy pihole.log over to pihole.log.1 # and empty out pihole.log # Note that moving the file is not an option, as # dnsmasq would happily continue writing into the # moved file (it will have the same file handler) - 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 if command -v /usr/sbin/logrotate >/dev/null; then # Logrotate twice to move all data out of sight of FTL - /usr/sbin/logrotate --force /etc/pihole/logrotate; sleep 3 - /usr/sbin/logrotate --force /etc/pihole/logrotate + /usr/sbin/logrotate --force --state "${STATEFILE}" /etc/pihole/logrotate; sleep 3 + /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=$(sqlite3 "${DBFILE}" "DELETE FROM queries WHERE timestamp >= strftime('%s','now')-86400; select changes() from queries limit 1") + deleted=$(pihole-FTL sqlite3 "${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 old mode 100644 new mode 100755 index 1e1b159c27..ae266ec047 --- a/advanced/Scripts/query.sh +++ b/advanced/Scripts/query.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash # shellcheck disable=SC1090 + # Pi-hole: A black hole for Internet advertisements # (c) 2018 Pi-hole, LLC (https://pi-hole.net) # Network-wide ad blocking via your own hardware. @@ -11,13 +12,20 @@ # Globals piholeDir="/etc/pihole" -gravityDBfile="${piholeDir}/gravity.db" +GRAVITYDB="${piholeDir}/gravity.db" options="$*" -adlist="" all="" exact="" -blockpage="" matchType="match" +# Source pihole-FTL from install script +pihole_FTL="${piholeDir}/pihole-FTL.conf" +if [[ -f "${pihole_FTL}" ]]; then + source "${pihole_FTL}" +fi + +# Set this only after sourcing pihole-FTL.conf as the gravity database path may +# have changed +gravityDBfile="${GRAVITYDB}" colfile="/opt/pihole/COL_TABLE" source "${colfile}" @@ -25,24 +33,26 @@ source "${colfile}" # Scan an array of files for matching strings scanList(){ # Escape full stops - local domain="${1}" esc_domain="${1//./\\.}" lists="${2}" type="${3:-}" + local domain="${1}" esc_domain="${1//./\\.}" lists="${2}" list_type="${3:-}" # Prevent grep from printing file path cd "$piholeDir" || exit 1 - # Prevent grep -i matching slowly: http://bit.ly/2xFXtUX + # 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 - # shellcheck disable=SC2086 - case "${type}" in + case "${list_type}" in "exact" ) grep -i -E -l "(^|(?/dev/null;; - # Create array of regexps # 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" ) awk 'NR==FNR{regexps[$0];next}{for (r in regexps)if($0 ~ r)print r}' \ - <(echo "${lists}") <(echo "${domain}") 2>/dev/null;; + "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 } @@ -53,27 +63,21 @@ Example: 'pihole -q -exact domain.com' Query the adlists for a specified domain Options: - -adlist Print the name of the block list URL - -exact Search the block lists for exact domain matches - -all Return all query matches within a block list + -exact Search the adlists for exact domain matches + -all Return all query matches within the adlists -h, --help Show this help dialog" exit 0 fi # Handle valid options -if [[ "${options}" == *"-bp"* ]]; then - exact="exact"; blockpage=true -else - [[ "${options}" == *"-adlist"* ]] && adlist=true - [[ "${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/ ?-(adlists?|all|exact) ?//g' <<< "${options}") # Handle remaining options # If $options contain non ASCII characters, convert to punycode @@ -90,53 +94,66 @@ if [[ -n "${str:-}" ]]; then fi scanDatabaseTable() { - local domain table type querystr result + local domain table list_type querystr result extra 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. - case "${type}" in - "exact" ) querystr="SELECT domain FROM vw_${table} WHERE domain = '${domain}'";; - * ) querystr="SELECT domain FROM vw_${table} WHERE domain LIKE '%${domain//_/\\_}%' ESCAPE '\\'";; - esac + if [[ "${table}" == "gravity" ]]; then + 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 '\\'";; + esac + else + case "${exact}" in + "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="$(sqlite3 "${gravityDBfile}" "${querystr}")" 2> /dev/null + result="$(pihole-FTL sqlite3 "${gravityDBfile}" "${querystr}")" 2> /dev/null if [[ -z "${result}" ]]; then # Return early when there are no matches in this table return fi + if [[ "${table}" == "gravity" ]]; then + echo "${result}" + return + fi + # Mark domain as having been white-/blacklist matched (global variable) wbMatch=true # Print table name - if [[ -z "${blockpage}" ]]; then - echo " ${matchType^} found in ${COL_BOLD}${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 + domain="${result/|*}" + if [[ "${result#*|}" == "0" ]]; then + extra=" (disabled)" + else + extra="" fi - echo " ${result}" + echo " ${domain}${extra}" done } scanRegexDatabaseTable() { - local domain list + local domain list list_type domain="${1}" list="${2}" + list_type="${3:-}" # Query all regex from the corresponding database tables - mapfile -t regexList < <(sqlite3 "${gravityDBfile}" "SELECT domain FROM vw_regex_${list}" 2> /dev/null) + mapfile -t regexList < <(pihole-FTL sqlite3 "${gravityDBfile}" "SELECT domain FROM domainlist WHERE type = ${list_type}" 2> /dev/null) # If we have regexps to process if [[ "${#regexList[@]}" -ne 0 ]]; then @@ -149,57 +166,35 @@ scanRegexDatabaseTable() { # Split matching regexps over a new line str_regexMatches=$(printf '%s\n' "${regexMatches[@]}") # Form a "matched" message - str_message="${matchType^} found in ${COL_BOLD}Regex ${list}${COL_NC}" + str_message="${matchType^} found in ${COL_BOLD}regex ${list}${COL_NC}" # 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 } # Scan Whitelist and Blacklist -scanDatabaseTable "${domainQuery}" "whitelist" "${exact}" -scanDatabaseTable "${domainQuery}" "blacklist" "${exact}" +scanDatabaseTable "${domainQuery}" "whitelist" "0" +scanDatabaseTable "${domainQuery}" "blacklist" "1" # Scan Regex table -scanRegexDatabaseTable "${domainQuery}" "whitelist" -scanRegexDatabaseTable "${domainQuery}" "blacklist" - -# Get version sorted *.domains filenames (without dir path) -lists=("$(cd "$piholeDir" || exit 0; printf "%s\\n" -- *.domains | sort -V)") - -# Query blocklists for occurences of domain -mapfile -t results <<< "$(scanList "${domainQuery}" "${lists[*]}" "${exact}")" - -# Remove unwanted content from $results -# Each line in $results is formatted as such: [fileName]:[line] -# 1. Delete lines starting with # -# 2. Remove comments after domain -# 3. Remove hosts format IP address -# 4. Remove any lines that no longer contain the queried domain name (in case the matched domain name was in a comment) -esc_domain="${domainQuery//./\\.}" -mapfile -t results <<< "$(IFS=$'\n'; sed \ - -e "/:#/d" \ - -e "s/[ \\t]#.*//g" \ - -e "s/:.*[ \\t]/:/g" \ - -e "/${esc_domain}/!d" \ - <<< "${results[*]}")" +scanRegexDatabaseTable "${domainQuery}" "whitelist" "2" +scanRegexDatabaseTable "${domainQuery}" "blacklist" "3" + +# Query block lists +mapfile -t results <<< "$(scanDatabaseTable "${domainQuery}" "gravity")" # Handle notices if [[ -z "${wbMatch:-}" ]] && [[ -z "${wcMatch:-}" ]] && [[ -z "${results[*]}" ]]; then - echo -e " ${INFO} No ${exact/t/t }results found for ${COL_BOLD}${domainQuery}${COL_NC} within the block lists" + echo -e " ${INFO} No ${exact/t/t }results found for ${COL_BOLD}${domainQuery}${COL_NC} within the adlists" exit 0 elif [[ -z "${results[*]}" ]]; then # Result found in WL/BL/Wildcards @@ -210,41 +205,30 @@ elif [[ -z "${all}" ]] && [[ "${#results[*]}" -ge 100 ]]; then exit 0 fi -# Get adlist file content as array -if [[ -n "${adlist}" ]] || [[ -n "${blockpage}" ]]; then - # Retrieve source URLs from gravity database - mapfile -t adlists <<< "$(sqlite3 "${gravityDBfile}" "SELECT address FROM vw_adlist;" 2> /dev/null)" -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 - fileName="${result/:*/}" - - # Determine *.domains URL using filename's number - if [[ -n "${adlist}" ]] || [[ -n "${blockpage}" ]]; then - fileNum="${fileName/list./}"; fileNum="${fileNum%%.*}" - fileName="${adlists[$fileNum]}" - - # Discrepency occurs when adlists has been modified, but Gravity has not been run - if [[ -z "${fileName}" ]]; then - fileName="${COL_LIGHT_RED}(no associated adlists URL found)${COL_NC}" - fi + match="${result/|*/}" + extra="${result#*|}" + adlistAddress="${extra/|*/}" + extra="${extra#*|}" + if [[ "${extra}" == "0" ]]; then + extra=" (disabled)" + else + extra="" fi - if [[ -n "${blockpage}" ]]; then - echo "${fileNum} ${fileName}" - elif [[ -n "${exact}" ]]; then - echo " ${fileName}" + if [[ -n "${exact}" ]]; then + echo " - ${adlistAddress}${extra}" else - if [[ ! "${fileName}" == "${fileName_prev:-}" ]]; then + if [[ ! "${adlistAddress}" == "${adlistAddress_prev:-}" ]]; then count="" - echo " ${matchType^} found in ${COL_BOLD}${fileName}${COL_NC}:" - fileName_prev="${fileName}" + echo " ${matchType^} found in ${COL_BOLD}${adlistAddress}${COL_NC}:" + adlistAddress_prev="${adlistAddress}" fi : $((count++)) @@ -254,7 +238,7 @@ for result in "${results[@]}"; do [[ "${count}" -gt "${max_count}" ]] && continue echo " ${COL_GRAY}Over ${count} results found, skipping rest of file${COL_NC}" else - echo " ${result#*:}" + echo " ${match}${extra}" fi fi done diff --git a/advanced/Scripts/setupLCD.sh b/advanced/Scripts/setupLCD.sh deleted file mode 100755 index 00eb963f7a..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 "Unrecognised 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: -# http://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/update.sh b/advanced/Scripts/update.sh index 4d35277780..c41c923230 100755 --- a/advanced/Scripts/update.sh +++ b/advanced/Scripts/update.sh @@ -17,7 +17,7 @@ readonly PI_HOLE_GIT_URL="https://github.com/pi-hole/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 @@ -31,11 +31,11 @@ source "/opt/pihole/COL_TABLE" # make_repo() sourced from basic-install.sh # update_repo() source from basic-install.sh # getGitFiles() sourced from basic-install.sh -# get_binary_name() sourced from basic-install.sh # FTLcheckUpdate() sourced from basic-install.sh GitCheckUpdateAvail() { local directory + local curBranch directory="${1}" curdir=$PWD cd "${directory}" || return @@ -43,18 +43,29 @@ GitCheckUpdateAvail() { # Fetch latest changes in this repo git fetch --quiet origin - # @ alone is a shortcut for HEAD. Older versions of git - # need @{0} - LOCAL="$(git rev-parse "@{0}")" + # Check current branch. If it is master, then check for the latest available tag instead of latest commit. + curBranch=$(git rev-parse --abbrev-ref HEAD) + if [[ "${curBranch}" == "master" ]]; then + # get the latest local tag + LOCAL=$(git describe --abbrev=0 --tags master) + # get the latest tag from remote + REMOTE=$(git describe --abbrev=0 --tags origin/master) + + else + # @ alone is a shortcut for HEAD. Older versions of git + # need @{0} + LOCAL="$(git rev-parse "@{0}")" + + # The suffix @{upstream} to a branchname + # (short form @{u}) refers + # to the branch that the branch specified + # by branchname is set to build on top of# + # (configured with branch..remote and + # branch..merge). A missing branchname + # defaults to the current one. + REMOTE="$(git rev-parse "@{upstream}")" + fi - # The suffix @{upstream} to a branchname - # (short form @{u}) refers - # to the branch that the branch specified - # by branchname is set to build on top of# - # (configured with branch..remote and - # branch..merge). A missing branchname - # defaults to the current one. - REMOTE="$(git rev-parse "@{upstream}")" if [[ "${#LOCAL}" == 0 ]]; then echo -e "\\n ${COL_LIGHT_RED}Error: Local revision could not be obtained, please contact Pi-hole Support" @@ -96,6 +107,10 @@ main() { # shellcheck disable=1090,2154 source "${setupVars}" + # Install packages used by this installation script (necessary if users have removed e.g. git from their systems) + package_manager_detect + install_dependent_packages "${INSTALLER_DEPS[@]}" + # This is unlikely if ! is_repo "${PI_HOLE_FILES_DIR}" ; then echo -e "\\n ${COL_LIGHT_RED}Error: Core Pi-hole repo is missing from system!" @@ -129,7 +144,12 @@ main() { fi fi - if FTLcheckUpdate > /dev/null; then + local funcOutput + funcOutput=$(get_binary_name) #Store output of get_binary_name here + local binary + binary="pihole-FTL${funcOutput##*pihole-FTL}" #binary name will be the last line of the output of get_binary_name (it always begins with pihole-FTL) + + if FTLcheckUpdate "${binary}" > /dev/null; then FTL_update=true echo -e " ${INFO} FTL:\\t\\t${COL_YELLOW}update available${COL_NC}" else @@ -192,8 +212,15 @@ main() { if [[ "${FTL_update}" == true || "${core_update}" == true ]]; then ${PI_HOLE_FILES_DIR}/automated\ install/basic-install.sh --reconfigure --unattended || \ - echo -e "${basicError}" && exit 1 + echo -e "${basicError}" && exit 1 + fi + + if [[ "${FTL_update}" == true || "${core_update}" == true || "${web_update}" == true ]]; then + # Update local and remote versions via updatechecker + /opt/pihole/updatecheck.sh + echo -e " ${INFO} Local version file information updated." fi + echo "" exit 0 } diff --git a/advanced/Scripts/updatecheck.sh b/advanced/Scripts/updatecheck.sh index afb03ebb7f..0ce9ad5d30 100755 --- a/advanced/Scripts/updatecheck.sh +++ b/advanced/Scripts/updatecheck.sh @@ -8,23 +8,6 @@ # 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 @@ -32,63 +15,119 @@ function get_local_branch() { } 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 + git describe --tags --always 2> /dev/null || return 1 +} + +function get_local_hash() { + cd "${1}" 2> /dev/null || return 1 + git rev-parse --short HEAD || return 1 +} + +function get_remote_version() { + curl -s "https://api.github.com/repos/pi-hole/${1}/releases/latest" 2> /dev/null | jq --raw-output .tag_name || return 1 +} + + +function get_remote_hash(){ + git ls-remote "https://github.com/pi-hole/${1}" --tags "${2}" | awk '{print substr($0, 0,8);}' || 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}" + +CORE_HASH="$(get_local_hash /etc/.pihole)" +addOrEditKeyValPair "${VERSION_FILE}" "CORE_HASH" "${CORE_HASH}" + +GITHUB_CORE_VERSION="$(get_remote_version pi-hole)" +addOrEditKeyValPair "${VERSION_FILE}" "GITHUB_CORE_VERSION" "${GITHUB_CORE_VERSION}" + +GITHUB_CORE_HASH="$(get_remote_hash pi-hole "${CORE_BRANCH}")" +addOrEditKeyValPair "${VERSION_FILE}" "GITHUB_CORE_HASH" "${GITHUB_CORE_HASH}" + + +# get Web versions + +if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then + + WEB_VERSION="$(get_local_version /var/www/html/admin)" + addOrEditKeyValPair "${VERSION_FILE}" "WEB_VERSION" "${WEB_VERSION}" + + 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}" + + GITHUB_WEB_HASH="$(get_remote_hash AdminLTE "${WEB_BRANCH}")" + addOrEditKeyValPair "${VERSION_FILE}" "GITHUB_WEB_HASH" "${GITHUB_WEB_HASH}" + +fi -else +# get FTL versions - LOCAL_BRANCH_FILE="/etc/pihole/localbranches" +FTL_VERSION="$(pihole-FTL version)" +addOrEditKeyValPair "${VERSION_FILE}" "FTL_VERSION" "${FTL_VERSION}" - CORE_BRANCH="$(get_local_branch /etc/.pihole)" - echo -n "${CORE_BRANCH}" > "${LOCAL_BRANCH_FILE}" - chmod 644 "${LOCAL_BRANCH_FILE}" +FTL_BRANCH="$(pihole-FTL branch)" +addOrEditKeyValPair "${VERSION_FILE}" "FTL_BRANCH" "${FTL_BRANCH}" - if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then - WEB_BRANCH="$(get_local_branch /var/www/html/admin)" - echo -n " ${WEB_BRANCH}" >> "${LOCAL_BRANCH_FILE}" - fi +FTL_HASH="$(pihole-FTL --hash)" +addOrEditKeyValPair "${VERSION_FILE}" "FTL_HASH" "${FTL_HASH}" - FTL_BRANCH="$(pihole-FTL branch)" - echo -n " ${FTL_BRANCH}" >> "${LOCAL_BRANCH_FILE}" +GITHUB_FTL_VERSION="$(get_remote_version FTL)" +addOrEditKeyValPair "${VERSION_FILE}" "GITHUB_FTL_VERSION" "${GITHUB_FTL_VERSION}" - LOCAL_VERSION_FILE="/etc/pihole/localversions" +GITHUB_FTL_HASH="$(get_remote_hash FTL "${FTL_BRANCH}")" +addOrEditKeyValPair "${VERSION_FILE}" "GITHUB_FTL_HASH" "${GITHUB_FTL_HASH}" - CORE_VERSION="$(get_local_version /etc/.pihole)" - echo -n "${CORE_VERSION}" > "${LOCAL_VERSION_FILE}" - chmod 644 "${LOCAL_VERSION_FILE}" - if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then - WEB_VERSION="$(get_local_version /var/www/html/admin)" - echo -n " ${WEB_VERSION}" >> "${LOCAL_VERSION_FILE}" - fi +# get Docker versions - FTL_VERSION="$(pihole-FTL version)" - echo -n " ${FTL_VERSION}" >> "${LOCAL_VERSION_FILE}" +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 diff --git a/advanced/Scripts/utils.sh b/advanced/Scripts/utils.sh new file mode 100755 index 0000000000..3751647292 --- /dev/null +++ b/advanced/Scripts/utils.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env sh +# shellcheck disable=SC3043 #https://github.com/koalaman/shellcheck/wiki/SC3043#exceptions + +# Pi-hole: A black hole for Internet advertisements +# (c) 2017 Pi-hole, LLC (https://pi-hole.net) +# Network-wide ad blocking via your own hardware. +# +# Script to hold utility functions for use in other scripts +# +# This file is copyright under the latest version of the EUPL. +# Please see LICENSE file for your rights under this license. + +# Basic Housekeeping rules +# - Functions must be self contained +# - 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 + +####################### +# Takes Three arguments: file, key, and value. +# +# Checks the target file for the existence of the key +# - If it exists, it changes the value +# - If it does not exist, it adds the value +# +# Example usage: +# addOrEditKeyValPair "/etc/pihole/setupVars.conf" "BLOCKING_ENABLED" "true" +####################### +addOrEditKeyValPair() { + local file="${1}" + 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}" + else + # Key does not already exist, add it and it's value + echo "${key}=${value}" >> "${file}" + fi +} + +####################### +# Takes two arguments: file, and key. +# Adds a key to target file +# +# Example usage: +# addKey "/etc/dnsmasq.d/01-pihole.conf" "log-queries" +####################### +addKey(){ + local file="${1}" + local key="${2}" + + # touch file to prevent grep error if file does not exist yet + touch "${file}" + + if ! grep -q "^${key}" "${file}"; then + # Key does not exist, add it. + echo "${key}" >> "${file}" + fi +} + +####################### +# Takes two arguments: file, and key. +# Deletes a key or key/value pair from target file +# +# Example usage: +# removeKey "/etc/pihole/setupVars.conf" "PIHOLE_DNS_1" +####################### +removeKey() { + local file="${1}" + local key="${2}" + sed -i "/^${key}/d" "${file}" +} + + +####################### +# 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_PID_FILE="/run/pihole-FTL.pid" + local FTL_PID_FILE + + 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 "${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 f6d4d34475..946c69fe56 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,126 +8,104 @@ # 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 +. /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}";; + "AdminLTE" ) [ "${INSTALL_WEB_INTERFACE}" = true ] && echo "${WEB_VERSION:=N/A}";; + "FTL" ) echo "${FTL_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}";; + "AdminLTE" ) [ "${INSTALL_WEB_INTERFACE}" = true ] && echo "${WEB_HASH:=N/A}";; + "FTL" ) echo "${FTL_HASH:=N/A}";; + esac } getRemoteHash(){ - # Remote FTL hash is not applicable - if [[ "$1" == "FTL" ]]; then - echo "N/A" - return 0 - fi + case ${1} in + "Pi-hole" ) echo "${GITHUB_CORE_HASH:=N/A}";; + "AdminLTE" ) [ "${INSTALL_WEB_INTERFACE}" = true ] && echo "${GITHUB_WEB_HASH:=N/A}";; + "FTL" ) echo "${GITHUB_FTL_HASH:=N/A}";; + esac +} - local daemon="${1}" - local branch="${2}" +getRemoteVersion(){ + case ${1} in + "Pi-hole" ) echo "${GITHUB_CORE_VERSION:=N/A}";; + "AdminLTE" ) [ "${INSTALL_WEB_INTERFACE}" = true ] && echo "${GITHUB_WEB_VERSION:=N/A}";; + "FTL" ) echo "${GITHUB_FTL_VERSION:=N/A}";; + esac +} - 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 +getLocalBranch(){ + case ${1} in + "Pi-hole" ) echo "${CORE_BRANCH:=N/A}";; + "AdminLTE" ) [ "${INSTALL_WEB_INTERFACE}" = true ] && echo "${WEB_BRANCH:=N/A}";; + "FTL" ) echo "${FTL_BRANCH:=N/A}";; + esac } -getRemoteVersion(){ - # Get the version from the remote origin - local daemon="${1}" - local version - - version=$(curl --silent --fail "https://api.github.com/repos/pi-hole/${daemon}/releases/latest" | \ - awk -F: '$1 ~/tag_name/ { print $2 }' | \ - tr -cd '[[:alnum:]]._-') - if [[ "${version}" =~ ^v ]]; then - echo "${version}" - else - echo "ERROR" +versionOutput() { + if [ "$1" = "AdminLTE" ] && [ "${INSTALL_WEB_INTERFACE}" != true ]; then + echo " WebAdmin not installed" return 1 fi - return 0 -} -versionOutput() { - [[ "$1" == "pi-hole" ]] && GITDIR=$COREGITDIR - [[ "$1" == "AdminLTE" ]] && GITDIR=$WEBGITDIR - [[ "$1" == "FTL" ]] && GITDIR="FTL" - - [[ "$2" == "-c" ]] || [[ "$2" == "--current" ]] || [[ -z "$2" ]] && current=$(getLocalVersion $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") - [[ "$3" == "-l" ]] || [[ "$3" == "--latest" ]] || [[ -z "$3" ]] && latHash=$(getRemoteHash "$1" "$(cd "$GITDIR" 2> /dev/null && git rev-parse --abbrev-ref HEAD)") + [ "$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 - if [[ -n "$current" ]] && [[ -n "$latest" ]]; then - output="${1^} version is $current (Latest: $latest)" - elif [[ -n "$current" ]] && [[ -z "$latest" ]]; then - output="Current ${1^} version is $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" + # 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 [ -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() { @@ -136,13 +114,9 @@ errorOutput() { } defaultOutput() { - # Source the setupvars config file - # shellcheck disable=SC1091 - source /etc/pihole/setupVars.conf - - versionOutput "pi-hole" "$@" + versionOutput "Pi-hole" "$@" - if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then + if [ "${INSTALL_WEB_INTERFACE}" = true ]; then versionOutput "AdminLTE" "$@" fi @@ -162,13 +136,13 @@ Repositories: Options: -c, --current Return the current version -l, --latest Return the latest version - --hash Return the Github hash from your local repositories + --hash Return the GitHub hash from your local repositories -h, --help Show this help dialog" exit 0 } case "${1}" in - "-p" | "--pihole" ) shift; versionOutput "pi-hole" "$@";; + "-p" | "--pihole" ) shift; versionOutput "Pi-hole" "$@";; "-a" | "--admin" ) shift; versionOutput "AdminLTE" "$@";; "-f" | "--ftl" ) shift; versionOutput "FTL" "$@";; "-h" | "--help" ) helpFunc;; diff --git a/advanced/Scripts/webpage.sh b/advanced/Scripts/webpage.sh index 8aa3fa0830..5ccdf733f6 100755 --- a/advanced/Scripts/webpage.sh +++ b/advanced/Scripts/webpage.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash # shellcheck disable=SC1090 +# shellcheck disable=SC2154 + # Pi-hole: A black hole for Internet advertisements # (c) 2017 Pi-hole, LLC (https://pi-hole.net) @@ -10,16 +12,25 @@ # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. -readonly setupVars="/etc/pihole/setupVars.conf" readonly dnsmasqconfig="/etc/dnsmasq.d/01-pihole.conf" readonly dhcpconfig="/etc/dnsmasq.d/02-pihole-dhcp.conf" readonly FTLconf="/etc/pihole/pihole-FTL.conf" # 03 -> wildcards readonly dhcpstaticconfig="/etc/dnsmasq.d/04-pihole-static-dhcp.conf" -readonly PI_HOLE_BIN_DIR="/usr/local/bin" +readonly dnscustomfile="/etc/pihole/custom.list" +readonly dnscustomcnamefile="/etc/dnsmasq.d/05-pihole-custom-cname.conf" 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 to source the script without running it +SKIP_INSTALL="true" +source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" + +utilsfile="/opt/pihole/utils.sh" +source "${utilsfile}" + coltable="/opt/pihole/COL_TABLE" if [[ -f ${coltable} ]]; then source ${coltable} @@ -31,58 +42,52 @@ Example: pihole -a -p password Set options for the Admin Console Options: - -p, password Set Admin Console password - -c, celsius Set Celsius as preferred temperature unit - -f, fahrenheit Set Fahrenheit as preferred temperature unit - -k, kelvin Set Kelvin as preferred temperature unit - -r, hostrecord Add a name to the DNS associated to an IPv4/IPv6 address - -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 - -l, privacylevel Set privacy level (0 = lowest, 4 = highest)" + -p, password Set Admin Console password + -c, celsius Set Celsius as preferred temperature unit + -f, fahrenheit Set Fahrenheit as preferred temperature unit + -k, kelvin Set Kelvin as preferred temperature unit + -h, --help Show this help dialog + -i, interface Specify dnsmasq's interface listening behavior + -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" exit 0 } add_setting() { - echo "${1}=${2}" >> "${setupVars}" + addOrEditKeyValPair "${setupVars}" "${1}" "${2}" } delete_setting() { - sed -i "/${1}/d" "${setupVars}" + removeKey "${setupVars}" "${1}" } change_setting() { - delete_setting "${1}" - add_setting "${1}" "${2}" + addOrEditKeyValPair "${setupVars}" "${1}" "${2}" } addFTLsetting() { - echo "${1}=${2}" >> "${FTLconf}" + addOrEditKeyValPair "${FTLconf}" "${1}" "${2}" } deleteFTLsetting() { - sed -i "/${1}/d" "${FTLconf}" + removeKey "${FTLconf}" "${1}" } changeFTLsetting() { - deleteFTLsetting "${1}" - addFTLsetting "${1}" "${2}" + addOrEditKeyValPair "${FTLconf}" "${1}" "${2}" } add_dnsmasq_setting() { - if [[ "${2}" != "" ]]; then - echo "${1}=${2}" >> "${dnsmasqconfig}" - else - echo "${1}" >> "${dnsmasqconfig}" - fi + addOrEditKeyValPair "${dnsmasqconfig}" "${1}" "${2}" } delete_dnsmasq_setting() { - sed -i "/${1}/d" "${dnsmasqconfig}" + removeKey "${dnsmasqconfig}" "${1}" } SetTemperatureUnit() { - change_setting "TEMPERATUREUNIT" "${unit}" + addOrEditKeyValPair "${setupVars}" "TEMPERATUREUNIT" "${unit}" echo -e " ${TICK} Set temperature unit to ${unit}" } @@ -116,21 +121,21 @@ SetWebPassword() { read -s -r -p "Enter New Password (Blank for no password): " PASSWORD echo "" - if [ "${PASSWORD}" == "" ]; then - change_setting "WEBPASSWORD" "" - echo -e " ${TICK} Password Removed" - exit 0 - fi + if [ "${PASSWORD}" == "" ]; then + addOrEditKeyValPair "${setupVars}" "WEBPASSWORD" "" + echo -e " ${TICK} Password Removed" + exit 0 + fi - read -s -r -p "Confirm Password: " CONFIRM - echo "" + read -s -r -p "Confirm Password: " CONFIRM + echo "" fi if [ "${PASSWORD}" == "${CONFIRM}" ] ; then # We do not wrap this in brackets, otherwise BASH will expand any appropriate syntax hash=$(HashPassword "$PASSWORD") # Save hash to file - change_setting "WEBPASSWORD" "${hash}" + addOrEditKeyValPair "${setupVars}" "WEBPASSWORD" "${hash}" echo -e " ${TICK} New password set" else echo -e " ${CROSS} Passwords don't match. Your password has not been changed" @@ -141,7 +146,7 @@ SetWebPassword() { ProcessDNSSettings() { source "${setupVars}" - delete_dnsmasq_setting "server" + removeKey "${dnsmasqconfig}" "server" COUNTER=1 while true ; do @@ -149,114 +154,194 @@ ProcessDNSSettings() { if [ -z "${!var}" ]; then break; fi - add_dnsmasq_setting "server" "${!var}" + addKey "${dnsmasqconfig}" "server=${!var}" (( COUNTER++ )) done # The option LOCAL_DNS_PORT is deprecated # We apply it once more, and then convert it into the current format if [ -n "${LOCAL_DNS_PORT}" ]; then - add_dnsmasq_setting "server" "127.0.0.1#${LOCAL_DNS_PORT}" - add_setting "PIHOLE_DNS_${COUNTER}" "127.0.0.1#${LOCAL_DNS_PORT}" - delete_setting "LOCAL_DNS_PORT" + addOrEditKeyValPair "${dnsmasqconfig}" "server" "127.0.0.1#${LOCAL_DNS_PORT}" + addOrEditKeyValPair "${setupVars}" "PIHOLE_DNS_${COUNTER}" "127.0.0.1#${LOCAL_DNS_PORT}" + removeKey "${setupVars}" "LOCAL_DNS_PORT" fi - delete_dnsmasq_setting "domain-needed" + removeKey "${dnsmasqconfig}" "domain-needed" + removeKey "${dnsmasqconfig}" "expand-hosts" if [[ "${DNS_FQDN_REQUIRED}" == true ]]; then - add_dnsmasq_setting "domain-needed" + addKey "${dnsmasqconfig}" "domain-needed" + addKey "${dnsmasqconfig}" "expand-hosts" fi - delete_dnsmasq_setting "bogus-priv" + removeKey "${dnsmasqconfig}" "bogus-priv" if [[ "${DNS_BOGUS_PRIV}" == true ]]; then - add_dnsmasq_setting "bogus-priv" + addKey "${dnsmasqconfig}" "bogus-priv" fi - delete_dnsmasq_setting "dnssec" - delete_dnsmasq_setting "trust-anchor=" + removeKey "${dnsmasqconfig}" "dnssec" + removeKey "${dnsmasqconfig}" "trust-anchor" if [[ "${DNSSEC}" == true ]]; then echo "dnssec -trust-anchor=.,19036,8,2,49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5 trust-anchor=.,20326,8,2,E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D " >> "${dnsmasqconfig}" fi - delete_dnsmasq_setting "host-record" + removeKey "${dnsmasqconfig}" "host-record" if [ -n "${HOSTRECORD}" ]; then - add_dnsmasq_setting "host-record" "${HOSTRECORD}" + addOrEditKeyValPair "${dnsmasqconfig}" "host-record" "${HOSTRECORD}" fi # Setup interface listening behavior of dnsmasq - delete_dnsmasq_setting "interface" - delete_dnsmasq_setting "local-service" + removeKey "${dnsmasqconfig}" "interface" + removeKey "${dnsmasqconfig}" "local-service" + removeKey "${dnsmasqconfig}" "except-interface" + removeKey "${dnsmasqconfig}" "bind-interfaces" if [[ "${DNSMASQ_LISTENING}" == "all" ]]; then # Listen on all interfaces, permit all origins - add_dnsmasq_setting "except-interface" "nonexisting" + addOrEditKeyValPair "${dnsmasqconfig}" "except-interface" "nonexisting" elif [[ "${DNSMASQ_LISTENING}" == "local" ]]; then # Listen only on all interfaces, but only local subnets - add_dnsmasq_setting "local-service" + addKey "${dnsmasqconfig}" "local-service" else + # Options "bind" and "single" # Listen only on one interface # Use eth0 as fallback interface if interface is missing in setupVars.conf if [ -z "${PIHOLE_INTERFACE}" ]; then PIHOLE_INTERFACE="eth0" fi - add_dnsmasq_setting "interface" "${PIHOLE_INTERFACE}" + addOrEditKeyValPair "${dnsmasqconfig}" "interface" "${PIHOLE_INTERFACE}" + + if [[ "${DNSMASQ_LISTENING}" == "bind" ]]; then + # Really bind to interface + addKey "${dnsmasqconfig}" "bind-interfaces" + fi fi if [[ "${CONDITIONAL_FORWARDING}" == true ]]; then - add_dnsmasq_setting "server=/${CONDITIONAL_FORWARDING_DOMAIN}/${CONDITIONAL_FORWARDING_IP}" - add_dnsmasq_setting "server=/${CONDITIONAL_FORWARDING_REVERSE}/${CONDITIONAL_FORWARDING_IP}" + # Convert legacy "conditional forwarding" to rev-server configuration + # Remove any existing REV_SERVER settings + removeKey "${setupVars}" "REV_SERVER" + removeKey "${setupVars}" "REV_SERVER_DOMAIN" + removeKey "${setupVars}" "REV_SERVER_TARGET" + removeKey "${setupVars}" "REV_SERVER_CIDR" + + REV_SERVER=true + addOrEditKeyValPair "${setupVars}" "REV_SERVER" "true" + + REV_SERVER_DOMAIN="${CONDITIONAL_FORWARDING_DOMAIN}" + addOrEditKeyValPair "${setupVars}" "REV_SERVER_DOMAIN" "${REV_SERVER_DOMAIN}" + + REV_SERVER_TARGET="${CONDITIONAL_FORWARDING_IP}" + addOrEditKeyValPair "${setupVars}" "REV_SERVER_TARGET" "${REV_SERVER_TARGET}" + + #Convert CONDITIONAL_FORWARDING_REVERSE if necessary e.g: + # 1.1.168.192.in-addr.arpa to 192.168.1.1/32 + # 1.168.192.in-addr.arpa to 192.168.1.0/24 + # 168.192.in-addr.arpa to 192.168.0.0/16 + # 192.in-addr.arpa to 192.0.0.0/8 + if [[ "${CONDITIONAL_FORWARDING_REVERSE}" == *"in-addr.arpa" ]];then + arrRev=("${CONDITIONAL_FORWARDING_REVERSE//./ }") + case ${#arrRev[@]} in + 6 ) REV_SERVER_CIDR="${arrRev[3]}.${arrRev[2]}.${arrRev[1]}.${arrRev[0]}/32";; + 5 ) REV_SERVER_CIDR="${arrRev[2]}.${arrRev[1]}.${arrRev[0]}.0/24";; + 4 ) REV_SERVER_CIDR="${arrRev[1]}.${arrRev[0]}.0.0/16";; + 3 ) REV_SERVER_CIDR="${arrRev[0]}.0.0.0/8";; + esac + else + # Set REV_SERVER_CIDR to whatever value it was set to + REV_SERVER_CIDR="${CONDITIONAL_FORWARDING_REVERSE}" + fi + + # If REV_SERVER_CIDR is not converted by the above, then use the REV_SERVER_TARGET variable to derive it + if [ -z "${REV_SERVER_CIDR}" ]; then + # Convert existing input to /24 subnet (preserves legacy behavior) + # This sed converts "192.168.1.2" to "192.168.1.0/24" + # shellcheck disable=2001 + REV_SERVER_CIDR="$(sed "s+\\.[0-9]*$+\\.0/24+" <<< "${REV_SERVER_TARGET}")" + fi + addOrEditKeyValPair "${setupVars}" "REV_SERVER_CIDR" "${REV_SERVER_CIDR}" + + # Remove obsolete settings from setupVars.conf + removeKey "${setupVars}" "CONDITIONAL_FORWARDING" + removeKey "${setupVars}" "CONDITIONAL_FORWARDING_REVERSE" + removeKey "${setupVars}" "CONDITIONAL_FORWARDING_DOMAIN" + removeKey "${setupVars}" "CONDITIONAL_FORWARDING_IP" fi - # Prevent Firefox from automatically switching over to DNS-over-HTTPS - # This follows https://support.mozilla.org/en-US/kb/configuring-networks-disable-dns-over-https - # (sourced 7th September 2019) - add_dnsmasq_setting "server=/use-application-dns.net/" + removeKey "${dnsmasqconfig}" "rev-server" + + if [[ "${REV_SERVER}" == true ]]; then + addKey "${dnsmasqconfig}" "rev-server=${REV_SERVER_CIDR},${REV_SERVER_TARGET}" + if [ -n "${REV_SERVER_DOMAIN}" ]; then + # Forward local domain names to the CF target, too + addKey "${dnsmasqconfig}" "server=/${REV_SERVER_DOMAIN}/${REV_SERVER_TARGET}" + fi + + if [[ "${DNS_FQDN_REQUIRED}" != true ]]; then + # Forward unqualified names to the CF target only when the "never + # forward non-FQDN" option is unticked + addKey "${dnsmasqconfig}" "server=//${REV_SERVER_TARGET}" + fi + + fi + + # We need to process DHCP settings here as well to account for possible + # 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 subroutine is doing) + ProcessDHCPSettings } SetDNSServers() { # Save setting to file - delete_setting "PIHOLE_DNS" + removeKey "${setupVars}" "PIHOLE_DNS" IFS=',' read -r -a array <<< "${args[2]}" for index in "${!array[@]}" do - add_setting "PIHOLE_DNS_$((index+1))" "${array[index]}" + # Replace possible "\#" by "#". This fixes AdminLTE#1427 + local ip + ip="${array[index]//\\#/#}" + + if valid_ip "${ip}" || valid_ip6 "${ip}" ; then + addOrEditKeyValPair "${setupVars}" "PIHOLE_DNS_$((index+1))" "${ip}" + else + echo -e " ${CROSS} Invalid IP has been passed" + exit 1 + fi done if [[ "${args[3]}" == "domain-needed" ]]; then - change_setting "DNS_FQDN_REQUIRED" "true" + addOrEditKeyValPair "${setupVars}" "DNS_FQDN_REQUIRED" "true" else - change_setting "DNS_FQDN_REQUIRED" "false" + addOrEditKeyValPair "${setupVars}" "DNS_FQDN_REQUIRED" "false" fi if [[ "${args[4]}" == "bogus-priv" ]]; then - change_setting "DNS_BOGUS_PRIV" "true" + addOrEditKeyValPair "${setupVars}" "DNS_BOGUS_PRIV" "true" else - change_setting "DNS_BOGUS_PRIV" "false" + addOrEditKeyValPair "${setupVars}" "DNS_BOGUS_PRIV" "false" fi if [[ "${args[5]}" == "dnssec" ]]; then - change_setting "DNSSEC" "true" + addOrEditKeyValPair "${setupVars}" "DNSSEC" "true" else - change_setting "DNSSEC" "false" + addOrEditKeyValPair "${setupVars}" "DNSSEC" "false" fi - if [[ "${args[6]}" == "conditional_forwarding" ]]; then - change_setting "CONDITIONAL_FORWARDING" "true" - change_setting "CONDITIONAL_FORWARDING_IP" "${args[7]}" - change_setting "CONDITIONAL_FORWARDING_DOMAIN" "${args[8]}" - change_setting "CONDITIONAL_FORWARDING_REVERSE" "${args[9]}" + if [[ "${args[6]}" == "rev-server" ]]; then + addOrEditKeyValPair "${setupVars}" "REV_SERVER" "true" + addOrEditKeyValPair "${setupVars}" "REV_SERVER_CIDR" "${args[7]}" + addOrEditKeyValPair "${setupVars}" "REV_SERVER_TARGET" "${args[8]}" + addOrEditKeyValPair "${setupVars}" "REV_SERVER_DOMAIN" "${args[9]}" else - change_setting "CONDITIONAL_FORWARDING" "false" - delete_setting "CONDITIONAL_FORWARDING_IP" - delete_setting "CONDITIONAL_FORWARDING_DOMAIN" - delete_setting "CONDITIONAL_FORWARDING_REVERSE" + addOrEditKeyValPair "${setupVars}" "REV_SERVER" "false" fi ProcessDNSSettings @@ -266,11 +351,11 @@ SetDNSServers() { } SetExcludeDomains() { - change_setting "API_EXCLUDE_DOMAINS" "${args[2]}" + addOrEditKeyValPair "${setupVars}" "API_EXCLUDE_DOMAINS" "${args[2]}" } SetExcludeClients() { - change_setting "API_EXCLUDE_CLIENTS" "${args[2]}" + addOrEditKeyValPair "${setupVars}" "API_EXCLUDE_CLIENTS" "${args[2]}" } Poweroff(){ @@ -286,41 +371,36 @@ RestartDNS() { } SetQueryLogOptions() { - change_setting "API_QUERY_LOG_SHOW" "${args[2]}" + addOrEditKeyValPair "${setupVars}" "API_QUERY_LOG_SHOW" "${args[2]}" } ProcessDHCPSettings() { source "${setupVars}" if [[ "${DHCP_ACTIVE}" == "true" ]]; then - interface="${PIHOLE_INTERFACE}" + interface="${PIHOLE_INTERFACE}" - # Use eth0 as fallback interface - if [ -z ${interface} ]; then - interface="eth0" - fi + # Use eth0 as fallback interface + if [ -z ${interface} ]; then + interface="eth0" + fi - if [[ "${PIHOLE_DOMAIN}" == "" ]]; then - PIHOLE_DOMAIN="lan" - change_setting "PIHOLE_DOMAIN" "${PIHOLE_DOMAIN}" - fi + if [[ "${PIHOLE_DOMAIN}" == "" ]]; then + PIHOLE_DOMAIN="lan" + addOrEditKeyValPair "${setupVars}" "PIHOLE_DOMAIN" "${PIHOLE_DOMAIN}" + fi - if [[ "${DHCP_LEASETIME}" == "0" ]]; then - leasetime="infinite" - elif [[ "${DHCP_LEASETIME}" == "" ]]; then - leasetime="24" - change_setting "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" - change_setting "DHCP_LEASETIME" "${leasetime}" - else - leasetime="${DHCP_LEASETIME}h" - fi + if [[ "${DHCP_LEASETIME}" == "0" ]]; then + leasetime="infinite" + elif [[ "${DHCP_LEASETIME}" == "" ]]; then + leasetime="24h" + addOrEditKeyValPair "${setupVars}" "DHCP_LEASETIME" "24" + else + leasetime="${DHCP_LEASETIME}h" + fi - # Write settings to file - echo "############################################################################### + # Write settings to file + echo "############################################################################### # DHCP SERVER CONFIG FILE AUTOMATICALLY POPULATED BY PI-HOLE WEB INTERFACE. # # ANY CHANGES MADE TO THIS FILE WILL BE LOST ON CHANGE # ############################################################################### @@ -330,26 +410,34 @@ dhcp-option=option:router,${DHCP_ROUTER} dhcp-leasefile=/etc/pihole/dhcp.leases #quiet-dhcp " > "${dhcpconfig}" - chmod 644 "${dhcpconfig}" - - if [[ "${PIHOLE_DOMAIN}" != "none" ]]; then - echo "domain=${PIHOLE_DOMAIN}" >> "${dhcpconfig}" - fi + chmod 644 "${dhcpconfig}" + + if [[ "${PIHOLE_DOMAIN}" != "none" ]]; then + echo "domain=${PIHOLE_DOMAIN}" >> "${dhcpconfig}" + + # When there is a Pi-hole domain set and "Never forward non-FQDNs" is + # ticked, we add `local=/domain/` to tell FTL that this domain is purely + # local and FTL may answer queries from /etc/hosts or DHCP but should + # never forward queries on that domain to any upstream servers + if [[ "${DNS_FQDN_REQUIRED}" == true ]]; then + echo "local=/${PIHOLE_DOMAIN}/" >> "${dhcpconfig}" + fi + fi - # Sourced from setupVars - # shellcheck disable=SC2154 - if [[ "${DHCP_rapid_commit}" == "true" ]]; then - echo "dhcp-rapid-commit" >> "${dhcpconfig}" - fi + # Sourced from setupVars + # shellcheck disable=SC2154 + if [[ "${DHCP_rapid_commit}" == "true" ]]; then + echo "dhcp-rapid-commit" >> "${dhcpconfig}" + fi - if [[ "${DHCP_IPv6}" == "true" ]]; then - echo "#quiet-dhcp6 + if [[ "${DHCP_IPv6}" == "true" ]]; then + echo "#quiet-dhcp6 #enable-ra dhcp-option=option6:dns-server,[::] -dhcp-range=::100,::1ff,constructor:${interface},ra-names,slaac,${leasetime} -ra-param=*,0,0 +dhcp-range=::,constructor:${interface},ra-names,ra-stateless,64 + " >> "${dhcpconfig}" - fi + fi else if [[ -f "${dhcpconfig}" ]]; then @@ -359,24 +447,24 @@ ra-param=*,0,0 } EnableDHCP() { - change_setting "DHCP_ACTIVE" "true" - change_setting "DHCP_START" "${args[2]}" - change_setting "DHCP_END" "${args[3]}" - change_setting "DHCP_ROUTER" "${args[4]}" - change_setting "DHCP_LEASETIME" "${args[5]}" - change_setting "PIHOLE_DOMAIN" "${args[6]}" - change_setting "DHCP_IPv6" "${args[7]}" - change_setting "DHCP_rapid_commit" "${args[8]}" + addOrEditKeyValPair "${setupVars}" "DHCP_ACTIVE" "true" + addOrEditKeyValPair "${setupVars}" "DHCP_START" "${args[2]}" + addOrEditKeyValPair "${setupVars}" "DHCP_END" "${args[3]}" + addOrEditKeyValPair "${setupVars}" "DHCP_ROUTER" "${args[4]}" + addOrEditKeyValPair "${setupVars}" "DHCP_LEASETIME" "${args[5]}" + addOrEditKeyValPair "${setupVars}" "PIHOLE_DOMAIN" "${args[6]}" + addOrEditKeyValPair "${setupVars}" "DHCP_IPv6" "${args[7]}" + addOrEditKeyValPair "${setupVars}" "DHCP_rapid_commit" "${args[8]}" # Remove possible old setting from file - delete_dnsmasq_setting "dhcp-" - delete_dnsmasq_setting "quiet-dhcp" + removeKey "${dnsmasqconfig}" "dhcp-" + removeKey "${dnsmasqconfig}" "quiet-dhcp" # If a DHCP client claims that its name is "wpad", ignore that. # This fixes a security hole. see CERT Vulnerability VU#598349 # We also ignore "localhost" as Windows behaves strangely if a # device claims this host name - add_dnsmasq_setting "dhcp-name-match=set:hostname-ignore,wpad + addKey "${dnsmasqconfig}" "dhcp-name-match=set:hostname-ignore,wpad dhcp-name-match=set:hostname-ignore,localhost dhcp-ignore-names=tag:hostname-ignore" @@ -386,11 +474,11 @@ dhcp-ignore-names=tag:hostname-ignore" } DisableDHCP() { - change_setting "DHCP_ACTIVE" "false" + addOrEditKeyValPair "${setupVars}" "DHCP_ACTIVE" "false" # Remove possible old setting from file - delete_dnsmasq_setting "dhcp-" - delete_dnsmasq_setting "quiet-dhcp" + removeKey "${dnsmasqconfig}" "dhcp-" + removeKey "${dnsmasqconfig}" "quiet-dhcp" ProcessDHCPSettings @@ -398,43 +486,51 @@ DisableDHCP() { } SetWebUILayout() { - change_setting "WEBUIBOXEDLAYOUT" "${args[2]}" + addOrEditKeyValPair "${setupVars}" "WEBUIBOXEDLAYOUT" "${args[2]}" } -CustomizeAdLists() { - local address - address="${args[3]}" - - if [[ "${args[2]}" == "enable" ]]; then - sqlite3 "${gravityDBfile}" "UPDATE adlist SET enabled = 1 WHERE address = '${address}'" - elif [[ "${args[2]}" == "disable" ]]; then - sqlite3 "${gravityDBfile}" "UPDATE adlist SET enabled = 0 WHERE address = '${address}'" - elif [[ "${args[2]}" == "add" ]]; then - sqlite3 "${gravityDBfile}" "INSERT OR IGNORE INTO adlist (address) VALUES ('${address}')" - elif [[ "${args[2]}" == "del" ]]; then - sqlite3 "${gravityDBfile}" "DELETE FROM adlist WHERE address = '${address}'" - else - echo "Not permitted" - return 1 - fi +SetWebUITheme() { + addOrEditKeyValPair "${setupVars}" "WEBTHEME" "${args[2]}" } -SetPrivacyMode() { - if [[ "${args[2]}" == "true" ]]; then - change_setting "API_PRIVACY_MODE" "true" +CheckUrl(){ + local regex check_url + # Check for characters NOT allowed in URLs + regex="[^a-zA-Z0-9:/?&%=~._()-;]" + + # this will remove first @ that is after schema and before domain + # \1 is optional schema, \2 is userinfo + check_url="$( sed -re 's#([^:/]*://)?([^/]+)@#\1\2#' <<< "$1" )" + + if [[ "${check_url}" =~ ${regex} ]]; then + return 1 else - change_setting "API_PRIVACY_MODE" "false" + return 0 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}" +CustomizeAdLists() { + local address + address="${args[3]}" + local comment + comment="${args[4]}" + + if CheckUrl "${address}"; then + if [[ "${args[2]}" == "enable" ]]; then + pihole-FTL sqlite3 "${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}'" + elif [[ "${args[2]}" == "add" ]]; then + pihole-FTL sqlite3 "${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}'" + else + echo "Not permitted" + return 1 + fi + else + echo "Invalid Url" + return 1 fi } @@ -457,54 +553,13 @@ AddDHCPStaticAddress() { RemoveDHCPStaticAddress() { mac="${args[2]}" - sed -i "/dhcp-host=${mac}.*/d" "${dhcpstaticconfig}" -} - -SetHostRecord() { - if [[ "${1}" == "-h" ]] || [[ "${1}" == "--help" ]]; then - echo "Usage: pihole -a hostrecord [IPv4-address],[IPv6-address] -Example: 'pihole -a hostrecord home.domain.com 192.168.1.1,2001:db8:a0b:12f0::1' -Add a name to the DNS associated to an IPv4/IPv6 address - -Options: - \"\" Empty: Remove host record - -h, --help Show this help dialog" - exit 0 - fi - - if [[ -n "${args[3]}" ]]; then - change_setting "HOSTRECORD" "${args[2]},${args[3]}" - echo -e " ${TICK} Setting host record for ${args[2]} to ${args[3]}" + if [[ "$mac" =~ ^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$ ]]; then + sed -i "/dhcp-host=${mac}.*/d" "${dhcpstaticconfig}" else - change_setting "HOSTRECORD" "" - echo -e " ${TICK} Removing host record" - fi - - ProcessDNSSettings - - # Restart dnsmasq to load new configuration - RestartDNS -} - -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 + echo " ${CROSS} Invalid Mac Passed!" + exit 1 fi - if [[ -n "${args[2]}" ]]; then - change_setting "ADMIN_EMAIL" "${args[2]}" - echo -e " ${TICK} Setting admin contact to ${args[2]}" - else - change_setting "ADMIN_EMAIL" "" - echo -e " ${TICK} Removing admin contact" - fi } SetListeningMode() { @@ -516,22 +571,26 @@ Example: 'pihole -a -i local' Specify dnsmasq's network interface listening behavior Interfaces: - local Listen on all interfaces, but only allow queries from - devices that are at most one hop away (local devices) - single Listen only on ${PIHOLE_INTERFACE} interface + local Only respond to queries from devices that + are at most one hop away (local devices) + single Respond only on interface ${PIHOLE_INTERFACE} + bind Bind only on interface ${PIHOLE_INTERFACE} all Listen on all interfaces, permit all origins" exit 0 - fi + fi if [[ "${args[2]}" == "all" ]]; then echo -e " ${INFO} Listening on all interfaces, permitting all origins. Please use a firewall!" - change_setting "DNSMASQ_LISTENING" "all" + addOrEditKeyValPair "${setupVars}" "DNSMASQ_LISTENING" "all" elif [[ "${args[2]}" == "local" ]]; then echo -e " ${INFO} Listening on all interfaces, permitting origins from one hop away (LAN)" - change_setting "DNSMASQ_LISTENING" "local" + addOrEditKeyValPair "${setupVars}" "DNSMASQ_LISTENING" "local" + elif [[ "${args[2]}" == "bind" ]]; then + echo -e " ${INFO} Binding on interface ${PIHOLE_INTERFACE}" + addOrEditKeyValPair "${setupVars}" "DNSMASQ_LISTENING" "bind" else echo -e " ${INFO} Listening only on interface ${PIHOLE_INTERFACE}" - change_setting "DNSMASQ_LISTENING" "single" + addOrEditKeyValPair "${setupVars}" "DNSMASQ_LISTENING" "single" fi # Don't restart DNS server yet because other settings @@ -544,9 +603,18 @@ Interfaces: } Teleporter() { - local datetimestamp - datetimestamp=$(date "+%Y-%m-%d_%H-%M-%S") - php /var/www/html/admin/scripts/pi-hole/php/teleporter.php > "pi-hole-teleporter_${datetimestamp}.tar.gz" + local filename + filename="${args[2]}" + if [[ -z "${filename}" ]]; then + local datetimestamp + local host + datetimestamp=$(date "+%Y-%m-%d_%H-%M-%S") + host=$(hostname) + host="${host//./_}" + filename="pi-hole-${host:-noname}-teleporter_${datetimestamp}.tar.gz" + fi + # webroot is sourced from basic-install above + php "${webroot}/admin/scripts/pi-hole/php/teleporter.php" > "${filename}" } checkDomain() @@ -559,6 +627,14 @@ checkDomain() echo "${validDomain}" } +escapeDots() +{ + # SC suggest bashism ${variable//search/replace} + # shellcheck disable=SC2001 + escaped=$(echo "$1" | sed 's/\./\\./g') + echo "${escaped}" +} + addAudit() { shift # skip "-a" @@ -567,33 +643,164 @@ addAudit() domains="" for domain in "$@" do - # Check domain to be added. Only continue if it is valid - validDomain="$(checkDomain "${domain}")" - if [[ -n "${validDomain}" ]]; then - # Put comma in between domains when there is - # more than one domains to be added - # SQL INSERT allows adding multiple rows at once using the format - ## INSERT INTO table (domain) VALUES ('abc.de'),('fgh.ij'),('klm.no'),('pqr.st'); - if [[ -n "${domains}" ]]; then - domains="${domains}," + # Check domain to be added. Only continue if it is valid + validDomain="$(checkDomain "${domain}")" + if [[ -n "${validDomain}" ]]; then + # Put comma in between domains when there is + # more than one domains to be added + # SQL INSERT allows adding multiple rows at once using the format + ## INSERT INTO table (domain) VALUES ('abc.de'),('fgh.ij'),('klm.no'),('pqr.st'); + if [[ -n "${domains}" ]]; then + domains="${domains}," + fi + domains="${domains}('${domain}')" fi - domains="${domains}('${domain}')" - fi done # Insert only the domain here. The date_added field will be # filled with its default value (date_added = current timestamp) - sqlite3 "${gravityDBfile}" "INSERT INTO domain_audit (domain) VALUES ${domains};" + pihole-FTL sqlite3 "${gravityDBfile}" "INSERT INTO domain_audit (domain) VALUES ${domains};" } clearAudit() { - sqlite3 "${gravityDBfile}" "DELETE FROM domain_audit;" + pihole-FTL sqlite3 "${gravityDBfile}" "DELETE FROM domain_audit;" } SetPrivacyLevel() { - # Set privacy level. Minimum is 0, maximum is 4 - if [ "${args[2]}" -ge 0 ] && [ "${args[2]}" -le 4 ]; then - changeFTLsetting "PRIVACYLEVEL" "${args[2]}" + # Set privacy level. Minimum is 0, maximum is 3 + if [ "${args[2]}" -ge 0 ] && [ "${args[2]}" -le 3 ]; then + addOrEditKeyValPair "${FTLconf}" "PRIVACYLEVEL" "${args[2]}" + pihole restartdns reload-lists + fi +} + +AddCustomDNSAddress() { + echo -e " ${TICK} Adding custom DNS entry..." + + ip="${args[2]}" + host="${args[3]}" + reload="${args[4]}" + + validHost="$(checkDomain "${host}")" + if [[ -n "${validHost}" ]]; then + if valid_ip "${ip}" || valid_ip6 "${ip}" ; then + echo "${ip} ${validHost}" >> "${dnscustomfile}" + else + echo -e " ${CROSS} Invalid IP has been passed" + exit 1 + fi + else + echo " ${CROSS} Invalid Domain passed!" + exit 1 + fi + + # Restart dnsmasq to load new custom DNS entries only if $reload not false + if [[ ! $reload == "false" ]]; then + RestartDNS + fi +} + +RemoveCustomDNSAddress() { + echo -e " ${TICK} Removing custom DNS entry..." + + ip="${args[2]}" + host="${args[3]}" + reload="${args[4]}" + + 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" + exit 1 + fi + else + echo " ${CROSS} Invalid Domain passed!" + exit 1 + fi + + # Restart dnsmasq to load new custom DNS entries only if reload is not false + if [[ ! $reload == "false" ]]; then + RestartDNS + fi +} + +AddCustomCNAMERecord() { + echo -e " ${TICK} Adding custom CNAME record..." + + domain="${args[2]}" + target="${args[3]}" + reload="${args[4]}" + + validDomain="$(checkDomain "${domain}")" + if [[ -n "${validDomain}" ]]; then + validTarget="$(checkDomain "${target}")" + if [[ -n "${validTarget}" ]]; then + 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 + fi + else + echo " ${CROSS} Invalid Domain passed!" + exit 1 + fi + # Restart dnsmasq to load new custom CNAME records only if reload is not false + if [[ ! $reload == "false" ]]; then + RestartDNS + fi +} + +RemoveCustomCNAMERecord() { + echo -e " ${TICK} Removing custom CNAME record..." + + domain="${args[2]}" + target="${args[3]}" + reload="${args[4]}" + + validDomain="$(checkDomain "${domain}")" + if [[ -n "${validDomain}" ]]; then + validTarget="$(checkDomain "${target}")" + if [[ -n "${validTarget}" ]]; then + validDomain=$(escapeDots "${validDomain}") + validTarget=$(escapeDots "${validTarget}") + sed -i "/^cname=${validDomain},${validTarget}$/Id" "${dnscustomcnamefile}" + else + echo " ${CROSS} Invalid Target Passed!" + exit 1 + fi + else + echo " ${CROSS} Invalid Domain passed!" + exit 1 + fi + + # Restart dnsmasq to update removed custom CNAME records only if $reload not false + if [[ ! $reload == "false" ]]; then + RestartDNS + fi +} + +SetRateLimit() { + local rate_limit_count rate_limit_interval reload + rate_limit_count="${args[2]}" + rate_limit_interval="${args[3]}" + reload="${args[4]}" + + # Set rate-limit setting inf valid + if [ "${rate_limit_count}" -ge 0 ] && [ "${rate_limit_interval}" -ge 0 ]; then + addOrEditKeyValPair "${FTLconf}" "RATE_LIMIT" "${rate_limit_count}/${rate_limit_interval}" + fi + + # Restart FTL to update rate-limit settings only if $reload not false + if [[ ! $reload == "false" ]]; then + RestartDNS fi } @@ -615,19 +822,21 @@ main() { "enabledhcp" ) EnableDHCP;; "disabledhcp" ) DisableDHCP;; "layout" ) SetWebUILayout;; + "theme" ) SetWebUITheme;; "-h" | "--help" ) helpFunc;; - "privacymode" ) SetPrivacyMode;; - "resolve" ) ResolutionSettings;; "addstaticdhcp" ) AddDHCPStaticAddress;; "removestaticdhcp" ) RemoveDHCPStaticAddress;; - "-r" | "hostrecord" ) SetHostRecord "$3";; - "-e" | "email" ) SetAdminEmail "$3";; "-i" | "interface" ) SetListeningMode "$@";; "-t" | "teleporter" ) Teleporter;; "adlist" ) CustomizeAdLists;; "audit" ) addAudit "$@";; "clearaudit" ) clearAudit;; "-l" | "privacylevel" ) SetPrivacyLevel;; + "addcustomdns" ) AddCustomDNSAddress;; + "removecustomdns" ) RemoveCustomDNSAddress;; + "addcustomcname" ) AddCustomCNAMERecord;; + "removecustomcname" ) RemoveCustomCNAMERecord;; + "ratelimit" ) SetRateLimit;; * ) helpFunc;; esac diff --git a/advanced/Scripts/wildcard_regex_converter.sh b/advanced/Scripts/wildcard_regex_converter.sh deleted file mode 100644 index 8c9578a304..0000000000 --- a/advanced/Scripts/wildcard_regex_converter.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/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. -# -# Provides an automated migration subroutine to convert Pi-hole v3.x wildcard domains to Pi-hole v4.x regex filters -# -# This file is copyright under the latest version of the EUPL. -# Please see LICENSE file for your rights under this license. - -# regexFile set in gravity.sh - -wildcardFile="/etc/dnsmasq.d/03-pihole-wildcard.conf" - -convert_wildcard_to_regex() { - if [ ! -f "${wildcardFile}" ]; then - return - fi - local addrlines domains uniquedomains - # Obtain wildcard domains from old file - addrlines="$(grep -oE "/.*/" ${wildcardFile})" - # Strip "/" from domain names and convert "." to regex-compatible "\." - domains="$(sed 's/\///g;s/\./\\./g' <<< "${addrlines}")" - # Remove repeated domains (may have been inserted two times due to A and AAAA blocking) - uniquedomains="$(uniq <<< "${domains}")" - # Automatically generate regex filters and remove old wildcards file - awk '{print "(^|\\.)"$0"$"}' <<< "${uniquedomains}" >> "${regexFile:?}" && rm "${wildcardFile}" -} diff --git a/advanced/Templates/gravity.db.sql b/advanced/Templates/gravity.db.sql index d0c744f4c1..3f696d6df1 100644 --- a/advanced/Templates/gravity.db.sql +++ b/advanced/Templates/gravity.db.sql @@ -1,142 +1,191 @@ -PRAGMA FOREIGN_KEYS=ON; +PRAGMA foreign_keys=OFF; +BEGIN TRANSACTION; CREATE TABLE "group" ( id INTEGER PRIMARY KEY AUTOINCREMENT, enabled BOOLEAN NOT NULL DEFAULT 1, - name TEXT NOT NULL, + name TEXT UNIQUE NOT NULL, + date_added INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), + date_modified INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), description TEXT ); +INSERT INTO "group" (id,enabled,name,description) VALUES (0,1,'Default','The default group'); -CREATE TABLE whitelist +CREATE TABLE domainlist ( id INTEGER PRIMARY KEY AUTOINCREMENT, - domain TEXT UNIQUE NOT NULL, + type INTEGER NOT NULL DEFAULT 0, + domain TEXT NOT NULL, enabled BOOLEAN NOT NULL DEFAULT 1, date_added INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), date_modified INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), - comment TEXT + comment TEXT, + UNIQUE(domain, type) ); -CREATE TABLE whitelist_by_group -( - whitelist_id INTEGER NOT NULL REFERENCES whitelist (id), - group_id INTEGER NOT NULL REFERENCES "group" (id), - PRIMARY KEY (whitelist_id, group_id) -); - -CREATE TABLE blacklist +CREATE TABLE adlist ( id INTEGER PRIMARY KEY AUTOINCREMENT, - domain TEXT UNIQUE NOT NULL, + address TEXT UNIQUE NOT NULL, enabled BOOLEAN NOT NULL DEFAULT 1, date_added INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), date_modified INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), - comment TEXT + comment TEXT, + date_updated INTEGER, + number INTEGER NOT NULL DEFAULT 0, + invalid_domains INTEGER NOT NULL DEFAULT 0, + status INTEGER NOT NULL DEFAULT 0 ); -CREATE TABLE blacklist_by_group +CREATE TABLE adlist_by_group ( - blacklist_id INTEGER NOT NULL REFERENCES blacklist (id), + adlist_id INTEGER NOT NULL REFERENCES adlist (id), group_id INTEGER NOT NULL REFERENCES "group" (id), - PRIMARY KEY (blacklist_id, group_id) + PRIMARY KEY (adlist_id, group_id) ); -CREATE TABLE regex +CREATE TABLE gravity +( + domain TEXT NOT NULL, + adlist_id INTEGER NOT NULL REFERENCES adlist (id) +); + +CREATE TABLE info +( + property TEXT PRIMARY KEY, + value TEXT NOT NULL +); + +INSERT INTO "info" VALUES('version','15'); + +CREATE TABLE domain_audit ( id INTEGER PRIMARY KEY AUTOINCREMENT, domain TEXT UNIQUE NOT NULL, - enabled BOOLEAN NOT NULL DEFAULT 1, - date_added INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), - date_modified INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), - comment TEXT + date_added INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)) ); -CREATE TABLE regex_by_group +CREATE TABLE domainlist_by_group ( - regex_id INTEGER NOT NULL REFERENCES regex (id), + domainlist_id INTEGER NOT NULL REFERENCES domainlist (id), group_id INTEGER NOT NULL REFERENCES "group" (id), - PRIMARY KEY (regex_id, group_id) + PRIMARY KEY (domainlist_id, group_id) ); -CREATE TABLE adlist +CREATE TABLE client ( id INTEGER PRIMARY KEY AUTOINCREMENT, - address TEXT UNIQUE NOT NULL, - enabled BOOLEAN NOT NULL DEFAULT 1, + ip TEXT NOT NULL UNIQUE, date_added INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), date_modified INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), comment TEXT ); -CREATE TABLE adlist_by_group +CREATE TABLE client_by_group ( - adlist_id INTEGER NOT NULL REFERENCES adlist (id), + client_id INTEGER NOT NULL REFERENCES client (id), group_id INTEGER NOT NULL REFERENCES "group" (id), - PRIMARY KEY (adlist_id, group_id) + PRIMARY KEY (client_id, group_id) ); -CREATE TABLE gravity -( - domain TEXT PRIMARY KEY -); +CREATE TRIGGER tr_adlist_update AFTER UPDATE OF address,enabled,comment ON adlist + BEGIN + UPDATE adlist SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE id = NEW.id; + END; -CREATE TABLE info -( - property TEXT PRIMARY KEY, - value TEXT NOT NULL -); +CREATE TRIGGER tr_client_update AFTER UPDATE ON client + BEGIN + UPDATE client SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE ip = NEW.ip; + END; -INSERT INTO info VALUES("version","1"); +CREATE TRIGGER tr_domainlist_update AFTER UPDATE ON domainlist + BEGIN + UPDATE domainlist SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE domain = NEW.domain; + END; -CREATE VIEW vw_whitelist AS SELECT DISTINCT domain - FROM whitelist - LEFT JOIN whitelist_by_group ON whitelist_by_group.whitelist_id = whitelist.id - LEFT JOIN "group" ON "group".id = whitelist_by_group.group_id - WHERE whitelist.enabled = 1 AND (whitelist_by_group.group_id IS NULL OR "group".enabled = 1) - ORDER BY whitelist.id; +CREATE VIEW vw_whitelist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id + FROM domainlist + LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id + LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id + WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1) + AND domainlist.type = 0 + ORDER BY domainlist.id; + +CREATE VIEW vw_blacklist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id + FROM domainlist + LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id + LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id + WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1) + AND domainlist.type = 1 + ORDER BY domainlist.id; + +CREATE VIEW vw_regex_whitelist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id + FROM domainlist + LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id + LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id + WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1) + AND domainlist.type = 2 + ORDER BY domainlist.id; + +CREATE VIEW vw_regex_blacklist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id + FROM domainlist + LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id + LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id + WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1) + AND domainlist.type = 3 + ORDER BY domainlist.id; + +CREATE VIEW vw_gravity AS SELECT domain, adlist_by_group.group_id AS group_id + FROM gravity + LEFT JOIN adlist_by_group ON adlist_by_group.adlist_id = gravity.adlist_id + LEFT JOIN adlist ON adlist.id = gravity.adlist_id + LEFT JOIN "group" ON "group".id = adlist_by_group.group_id + WHERE adlist.enabled = 1 AND (adlist_by_group.group_id IS NULL OR "group".enabled = 1); + +CREATE VIEW vw_adlist AS SELECT DISTINCT address, id + FROM adlist + WHERE enabled = 1 + ORDER BY id; -CREATE TRIGGER tr_whitelist_update AFTER UPDATE ON whitelist +CREATE TRIGGER tr_domainlist_add AFTER INSERT ON domainlist BEGIN - UPDATE whitelist SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE domain = NEW.domain; + INSERT INTO domainlist_by_group (domainlist_id, group_id) VALUES (NEW.id, 0); END; -CREATE VIEW vw_blacklist AS SELECT DISTINCT domain - FROM blacklist - LEFT JOIN blacklist_by_group ON blacklist_by_group.blacklist_id = blacklist.id - LEFT JOIN "group" ON "group".id = blacklist_by_group.group_id - WHERE blacklist.enabled = 1 AND (blacklist_by_group.group_id IS NULL OR "group".enabled = 1) - ORDER BY blacklist.id; +CREATE TRIGGER tr_client_add AFTER INSERT ON client + BEGIN + INSERT INTO client_by_group (client_id, group_id) VALUES (NEW.id, 0); + END; + +CREATE TRIGGER tr_adlist_add AFTER INSERT ON adlist + BEGIN + INSERT INTO adlist_by_group (adlist_id, group_id) VALUES (NEW.id, 0); + END; -CREATE TRIGGER tr_blacklist_update AFTER UPDATE ON blacklist +CREATE TRIGGER tr_group_update AFTER UPDATE ON "group" BEGIN - UPDATE blacklist SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE domain = NEW.domain; + UPDATE "group" SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE id = NEW.id; END; -CREATE VIEW vw_regex AS SELECT DISTINCT domain - FROM regex - LEFT JOIN regex_by_group ON regex_by_group.regex_id = regex.id - LEFT JOIN "group" ON "group".id = regex_by_group.group_id - WHERE regex.enabled = 1 AND (regex_by_group.group_id IS NULL OR "group".enabled = 1) - ORDER BY regex.id; +CREATE TRIGGER tr_group_zero AFTER DELETE ON "group" + BEGIN + INSERT OR IGNORE INTO "group" (id,enabled,name) VALUES (0,1,'Default'); + END; -CREATE TRIGGER tr_regex_update AFTER UPDATE ON regex +CREATE TRIGGER tr_domainlist_delete AFTER DELETE ON domainlist BEGIN - UPDATE regex SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE domain = NEW.domain; + DELETE FROM domainlist_by_group WHERE domainlist_id = OLD.id; END; -CREATE VIEW vw_adlist AS SELECT DISTINCT address - FROM adlist - LEFT JOIN adlist_by_group ON adlist_by_group.adlist_id = adlist.id - LEFT JOIN "group" ON "group".id = adlist_by_group.group_id - WHERE adlist.enabled = 1 AND (adlist_by_group.group_id IS NULL OR "group".enabled = 1) - ORDER BY adlist.id; +CREATE TRIGGER tr_adlist_delete AFTER DELETE ON adlist + BEGIN + DELETE FROM adlist_by_group WHERE adlist_id = OLD.id; + END; -CREATE TRIGGER tr_adlist_update AFTER UPDATE ON adlist +CREATE TRIGGER tr_client_delete AFTER DELETE ON client BEGIN - UPDATE adlist SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE address = NEW.address; + DELETE FROM client_by_group WHERE client_id = OLD.id; END; -CREATE VIEW vw_gravity AS SELECT domain - FROM gravity - WHERE domain NOT IN (SELECT domain from vw_whitelist); +COMMIT; diff --git a/advanced/Templates/gravity_copy.sql b/advanced/Templates/gravity_copy.sql new file mode 100644 index 0000000000..3bea731d1b --- /dev/null +++ b/advanced/Templates/gravity_copy.sql @@ -0,0 +1,45 @@ +.timeout 30000 + +ATTACH DATABASE '/etc/pihole/gravity.db' AS OLD; + +BEGIN TRANSACTION; + +DROP TRIGGER tr_domainlist_add; +DROP TRIGGER tr_client_add; +DROP TRIGGER tr_adlist_add; + +INSERT OR REPLACE INTO "group" SELECT * FROM OLD."group"; +INSERT OR REPLACE INTO domain_audit SELECT * FROM OLD.domain_audit; + +INSERT OR REPLACE INTO domainlist SELECT * FROM OLD.domainlist; +DELETE FROM OLD.domainlist_by_group WHERE domainlist_id NOT IN (SELECT id FROM OLD.domainlist); +INSERT OR REPLACE INTO domainlist_by_group SELECT * FROM OLD.domainlist_by_group; + +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; + + +CREATE TRIGGER tr_domainlist_add AFTER INSERT ON domainlist + BEGIN + INSERT INTO domainlist_by_group (domainlist_id, group_id) VALUES (NEW.id, 0); + END; + +CREATE TRIGGER tr_client_add AFTER INSERT ON client + BEGIN + INSERT INTO client_by_group (client_id, group_id) VALUES (NEW.id, 0); + END; + +CREATE TRIGGER tr_adlist_add AFTER INSERT ON adlist + BEGIN + INSERT INTO adlist_by_group (adlist_id, group_id) VALUES (NEW.id, 0); + END; + + +COMMIT; 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.conf b/advanced/Templates/pihole-FTL.conf new file mode 100644 index 0000000000..269fcf9d47 --- /dev/null +++ b/advanced/Templates/pihole-FTL.conf @@ -0,0 +1,2 @@ +#; Pi-hole FTL config file +#; Comments should start with #; to avoid issues with PHP and bash reading this file diff --git a/advanced/Templates/pihole-FTL.service b/advanced/Templates/pihole-FTL.service index 8a4c7ce676..bc1b1d20e0 100644 --- a/advanced/Templates/pihole-FTL.service +++ b/advanced/Templates/pihole-FTL.service @@ -1,30 +1,25 @@ -#!/bin/bash +#!/usr/bin/env sh ### BEGIN INIT INFO # Provides: pihole-FTL -# Required-Start: $remote_fs $syslog -# Required-Stop: $remote_fs $syslog +# Required-Start: $remote_fs $syslog $network +# Required-Stop: $remote_fs $syslog $network # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: pihole-FTL daemon # Description: Enable service provided by pihole-FTL daemon ### END INIT INFO -FTLUSER=pihole -PIDFILE=/var/run/pihole-FTL.pid +#source utils.sh for getFTLPIDFile(), getFTLPID () +PI_HOLE_SCRIPT_DIR="/opt/pihole" +utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" +. "${utilsfile}" -get_pid() { - # First, try to obtain PID from PIDFILE - if [ -s "${PIDFILE}" ]; then - cat "${PIDFILE}" - return - fi - - # If the PIDFILE is empty or not available, obtain the PID using pidof - pidof "pihole-FTL" | awk '{print $(NF)}' -} is_running() { - ps "$(get_pid)" > /dev/null 2>&1 + if [ -d "/proc/${FTL_PID}" ]; then + return 0 + fi + return 1 } @@ -34,26 +29,38 @@ start() { echo "pihole-FTL is already running" else # Touch files to ensure they exist (create if non-existing, preserve if existing) - touch /var/log/pihole-FTL.log /var/log/pihole.log - touch /run/pihole-FTL.pid /run/pihole-FTL.port - touch /etc/pihole/dhcp.leases - mkdir -p /var/run/pihole - mkdir -p /var/log/pihole - chown pihole:pihole /var/run/pihole /var/log/pihole - # Remove possible leftovers from previous pihole-FTL processes - rm -f /dev/shm/FTL-* 2> /dev/null - rm /var/run/pihole/FTL.sock 2> /dev/null + 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-FTL.pid /run/pihole-FTL.port - chown pihole:pihole /etc/pihole /etc/pihole/dhcp.leases 2> /dev/null - chown pihole:pihole /var/log/pihole-FTL.log /var/log/pihole.log - chmod 0644 /var/log/pihole-FTL.log /run/pihole-FTL.pid /run/pihole-FTL.port /var/log/pihole.log - echo "nameserver 127.0.0.1" | /sbin/resolvconf -a lo.piholeFTL - if setcap CAP_NET_BIND_SERVICE,CAP_NET_RAW,CAP_NET_ADMIN+eip "$(which pihole-FTL)"; then - su -s /bin/sh -c "/usr/bin/pihole-FTL" "$FTLUSER" + 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 + # 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 + + # 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 -s /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 -s /var/log/pihole/FTL.log /var/log/pihole-FTL.log + chown -h pihole:pihole /var/log/pihole-FTL.log + fi + + 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 || exit $? else echo "Warning: Starting pihole-FTL as root because setting capabilities is not supported on this system" - pihole-FTL + /usr/bin/pihole-FTL || exit $? fi echo fi @@ -62,28 +69,28 @@ start() { # Stop the service stop() { if is_running; then - /sbin/resolvconf -d lo.piholeFTL - kill "$(get_pid)" - for i in {1..5}; do + kill "${FTL_PID}" + for i in 1 2 3 4 5; do if ! is_running; then break fi - echo -n "." + printf "." sleep 1 done echo if is_running; then echo "Not stopped; may still be shutting down or shutdown may have failed, killing now" - kill -9 "$(get_pid)" - 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-* "${FTL_PID_FILE}" echo } @@ -100,6 +107,13 @@ status() { ### main logic ### + +# Get file paths +FTL_PID_FILE="$(getFTLPIDFile)" + +# Get FTL's current PID +FTL_PID="$(getFTLPID ${FTL_PID_FILE})" + case "$1" in stop) stop @@ -112,7 +126,7 @@ case "$1" in start ;; *) - echo $"Usage: $0 {start|stop|restart|reload|status}" + echo "Usage: $0 {start|stop|restart|reload|status}" exit 1 esac diff --git a/advanced/Templates/pihole.cron b/advanced/Templates/pihole.cron index 8dc9872138..c62d31ab3b 100644 --- a/advanced/Templates/pihole.cron +++ b/advanced/Templates/pihole.cron @@ -10,7 +10,7 @@ # # # This file is under source-control of the Pi-hole installation and update -# scripts, any changes made to this file will be overwritten when the softare +# scripts, any changes made to this file will be overwritten when the software # is updated or re-installed. Please make any changes to the appropriate crontab # or other cron file snippets. @@ -18,19 +18,16 @@ # 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/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 # parameter "once": logrotate only once (default is twice) # parameter "quiet": don't print messages -00 00 * * * root PATH="$PATH:/usr/local/bin/" pihole flush once quiet +00 00 * * * root PATH="$PATH:/usr/sbin:/usr/local/bin/" pihole flush once quiet -@reboot root /usr/sbin/logrotate /etc/pihole/logrotate +@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/local/bin/" pihole updatechecker local - -# Pi-hole: Grab remote version every 24 hours -59 17 * * * root PATH="$PATH:/usr/local/bin/" pihole updatechecker remote -@reboot root PATH="$PATH:/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 cea3606059..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 hostrecord interface kelvin password privacylevel" + opts_admin="celsius fahrenheit interface kelvin password privacylevel" COMPREPLY=( $(compgen -W "${opts_admin}" -- ${cur}) ) ;; "checkout") @@ -56,11 +56,11 @@ _pihole() { ;; "privacylevel") if ( [[ "$prev2" == "admin" ]] || [[ "$prev2" == "-a" ]] ); then - opts_privacy="0 1 2 3 4" + opts_privacy="0 1 2 3" COMPREPLY=( $(compgen -W "${opts_privacy}" -- ${cur}) ) - else + else return 1 - fi + fi ;; "core"|"admin"|"ftl") if [[ "$prev2" == "checkout" ]]; then diff --git a/advanced/blockingpage.css b/advanced/blockingpage.css deleted file mode 100644 index e74844d109..0000000000 --- a/advanced/blockingpage.css +++ /dev/null @@ -1,383 +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 occured. 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; - src: local("Source Sans Pro"), local("SourceSansPro-Regular"), url("/admin/style/vendor/SourceSansPro/SourceSansPro-Regular.ttf") format("truetype"); -} - -@font-face { - font-family: "Source Sans Pro"; - font-style: normal; - font-weight: 700; - src: local("Source Sans Pro Bold"), local("SourceSansPro-Bold"), url("/admin/style/vendor/SourceSansPro/SourceSansPro-Bold.ttf") format("truetype"); -} - -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; } -#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: normal; - 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; } - -/* 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,0,1,1,1v1a1.371,1.371,0,0,1-1,1H1a1.371,1.371,0,0,1-1-1V12a1.371,1.371,0,0,1,1-1H2V8H1A1.371,1.371,0,0,1,0,7V6A1.371,1.371,0,0,1,1,5H4A1.371,1.371,0,0,1,5,6v5H6ZM3.5,0A1.5,1.5,0,1,1,2,1.5,1.5,1.5,0,0,1,3.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: bold 1.8rem Consolas, Courier, monospace; - padding: 5px 10px 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 colour */ -.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; } - -@keyframes slidein { from { max-height: 0; opacity: 0; } to { max-height: 300px; opacity: 1; } } -#bpMoreToggle:checked ~ #bpMoreInfo { display: block; margin-top: 8px; animation: slidein 0.05s linear; } -#bpMoreInfo { display: none; margin-top: 10px; } - -#bpQueryOutput { - font-size: 1.2rem; - line-height: 1.65rem; - margin: 5px 0 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; } -} 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 9e4cc92eae..4aa5a8bfc8 100644 --- a/advanced/dnsmasq.conf.original +++ b/advanced/dnsmasq.conf.original @@ -46,7 +46,7 @@ #resolv-file= # By default, dnsmasq will send queries to any of the upstream -# servers it knows about and tries to favour servers to are known +# servers it knows about and tries to favor servers to are known # to be up. Uncommenting this forces dnsmasq to try each query # with each server strictly in the order they appear in # /etc/resolv.conf @@ -189,7 +189,7 @@ # add names to the DNS for the IPv6 address of SLAAC-configured dual-stack # hosts. Use the DHCPv4 lease to derive the name, network segment and # MAC address and assume that the host will also have an -# IPv6 address calculated using the SLAAC alogrithm. +# IPv6 address calculated using the SLAAC algorithm. #dhcp-range=1234::, ra-names # Do Router Advertisements, BUT NOT DHCP for this subnet. @@ -210,7 +210,7 @@ #dhcp-range=1234::, ra-stateless, ra-names # Do router advertisements for all subnets where we're doing DHCPv6 -# Unless overriden by ra-stateless, ra-names, et al, the router +# Unless overridden by ra-stateless, ra-names, et al, the router # advertisements will have the M and O bits set, so that the clients # get addresses and configuration from DHCPv6, and the A bit reset, so the # clients don't use SLAAC addresses. @@ -281,7 +281,7 @@ # Give a fixed IPv6 address and name to client with # DUID 00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2 # Note the MAC addresses CANNOT be used to identify DHCPv6 clients. -# Note also the they [] around the IPv6 address are obilgatory. +# Note also the they [] around the IPv6 address are obligatory. #dhcp-host=id:00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2, fred, [1234::5] # Ignore any clients which are not specified in dhcp-host lines @@ -404,14 +404,14 @@ #dhcp-option=vendor:MSFT,2,1i # Send the Encapsulated-vendor-class ID needed by some configurations of -# Etherboot to allow is to recognise the DHCP server. +# Etherboot to allow is to recognize the DHCP server. #dhcp-option=vendor:Etherboot,60,"Etherboot" # Send options to PXELinux. Note that we need to send the options even # though they don't appear in the parameter request list, so we need # to use dhcp-option-force here. # See http://syslinux.zytor.com/pxe.php#special for details. -# Magic number - needed before anything else is recognised +# Magic number - needed before anything else is recognized #dhcp-option-force=208,f1:00:74:7e # Configuration file name #dhcp-option-force=209,configs/common @@ -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 index 62e45091de..f3f2ce1cac 100644 --- a/advanced/index.php +++ b/advanced/index.php @@ -6,25 +6,16 @@ * This file is copyright under the latest version of the EUPL. * Please see LICENSE file for your rights under this license. */ -// Sanitise HTTP_HOST output -$serverName = htmlspecialchars($_SERVER["HTTP_HOST"]); +// Sanitize SERVER_NAME output +$serverName = htmlspecialchars($_SERVER["SERVER_NAME"]); // Remove external ipv6 brackets if any $serverName = preg_replace('/^\[(.*)\]$/', '${1}', $serverName); -if (!is_file("/etc/pihole/setupVars.conf")) - die("[ERROR] File not found: /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 = []; +$authorizedHosts = [ "localhost" ]; if (!empty($_SERVER["FQDN"])) { // If setenv.add-environment = ("fqdn" => "true") is configured in lighttpd, // append $serverName to $authorizedHosts @@ -34,329 +25,57 @@ 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") { +if ($serverName === "pi.hole" + || (!empty($_SERVER["VIRTUAL_HOST"]) && $serverName === $_SERVER["VIRTUAL_HOST"])) { // Redirect to Web Interface - exit(header("Location: /admin")); + header("Location: /admin"); + exit(); } elseif (filter_var($serverName, FILTER_VALIDATE_IP) || in_array($serverName, $authorizedHosts)) { - // Set Splash Page output - $splashPage = " - - $viewPort - -
Pi-hole: Your black hole for Internet advertisements
Did you mean to go to the admin panel? - "; - - // Set splash/landing page based off presence of $landPage - $renderPage = is_file(getcwd()."/$landPage") ? include $landPage : "$splashPage"; - - // Unset variables so as to not be included in $landPage - unset($serverName, $svPasswd, $svEmail, $authorizedHosts, $validExtTypes, $currentUrlExt, $viewPort); - - // Render splash/landing page when directly browsing via IP or authorised hostname - exit($renderPage); -} 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) ? '' : ""; - -// Determine if at least one block list has been generated -$blocklistglob = glob("/etc/pihole/list.0.*.domains"); -if ($blocklistglob === array()) { - die("[ERROR] There are no domain lists generated lists within /etc/pihole/! Please update gravity by running pihole -g, or repair Pi-hole using pihole -r."); -} - -// 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"]; - $queryAds = file("http://127.0.0.1/admin/scripts/pi-hole/php/queryads.php?domain=$serverName&bp", FILE_IGNORE_NEW_LINES); - $queryAds = array_values(array_filter(preg_replace("/data:\s+/", "", $queryAds))); - $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()); + // 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($authorizedHosts); + // 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 = << + + + + + ● $serverName + + + + +
+ Pi-hole logo +
+

Pi-hole: Your black hole for Internet advertisements

+ Did you mean to go to the admin panel? +
+ + +EOT; + exit($splashPage); } -// 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 localised -// language without the need to edit this file - -setHeader(); +header("HTTP/1.1 404 Not Found"); +exit(); ?> - - - - - - - - - - - ● <?=$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 2215bbdbd2..21e48d6c40 100644 --- a/advanced/lighttpd.conf.debian +++ b/advanced/lighttpd.conf.debian @@ -16,43 +16,60 @@ ############################################################################### server.modules = ( - "mod_access", - "mod_accesslog", - "mod_auth", - "mod_expire", - "mod_compress", - "mod_redirect", - "mod_setenv", - "mod_rewrite" + "mod_access", + "mod_accesslog", + "mod_auth", + "mod_expire", + "mod_redirect", + "mod_setenv", + "mod_rewrite" ) 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.pid-file = "/var/run/lighttpd.pid" +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.filename = "/var/log/lighttpd/access-pihole.log" accesslog.format = "%{%s}t|%V|%r|%s|%b" +# Allow streaming response +# reference: https://redmine.lighttpd.net/projects/lighttpd/wiki/Server_stream-response-bodyDetails +server.stream-response-body = 1 +#ssl.read-ahead = "disable" + index-file.names = ( "index.php", "index.html", "index.lighttpd.html" ) url.access-deny = ( "~", ".inc", ".md", ".yml", ".ini" ) static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" ) -compress.cache-dir = "/var/cache/lighttpd/compress/" -compress.filetype = ( "application/javascript", "text/css", "text/html", "text/plain" ) +mimetype.assign = ( + ".ico" => "image/x-icon", + ".jpeg" => "image/jpeg", + ".jpg" => "image/jpeg", + ".png" => "image/png", + ".svg" => "image/svg+xml", + ".css" => "text/css; charset=utf-8", + ".html" => "text/html; charset=utf-8", + ".js" => "text/javascript; charset=utf-8", + ".json" => "application/json; charset=utf-8", + ".map" => "application/json; charset=utf-8", + ".txt" => "text/plain; charset=utf-8", + ".eot" => "application/vnd.ms-fontobject", + ".otf" => "font/otf", + ".ttc" => "font/collection", + ".ttf" => "font/ttf", + ".woff" => "font/woff", + ".woff2" => "font/woff2" +) -mimetype.assign = ( ".png" => "image/png", - ".jpg" => "image/jpeg", - ".jpeg" => "image/jpeg", - ".html" => "text/html", - ".css" => "text/css; charset=utf-8", - ".js" => "application/javascript", - ".json" => "application/json", - ".txt" => "text/plain", - ".svg" => "image/svg+xml" ) +# 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" # default listening port for IPv6 falls back to the IPv4 port include_shell "/usr/share/lighttpd/use-ipv6.pl " + server.port @@ -63,23 +80,35 @@ include_shell "find /etc/lighttpd/conf-enabled -name '*.conf' -a ! -name 'letsen # If the URL starts with /admin, it is the Web interface $HTTP["url"] =~ "^/admin/" { - # Create a response header for debugging using curl -I + # 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. + # 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-Frame-Options" => "DENY", + "X-XSS-Protection" => "1; mode=block", + "X-Content-Type-Options" => "nosniff", + "Content-Security-Policy" => "default-src 'self' 'unsafe-inline';", + "X-Permitted-Cross-Domain-Policies" => "none", + "Referrer-Policy" => "same-origin" ) - - $HTTP["url"] =~ ".ttf$" { - # Allow Block Page access to local fonts - setenv.add-response-header = ( "Access-Control-Allow-Origin" => "*" ) - } } # Block . files from being served, such as .git, .github, .gitignore $HTTP["url"] =~ "^/admin/\.(.*)" { - url.access-deny = ("") + url.access-deny = ("") } -# 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" +# 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" ) diff --git a/advanced/lighttpd.conf.fedora b/advanced/lighttpd.conf.fedora index 4232c90fe5..3da62839db 100644 --- a/advanced/lighttpd.conf.fedora +++ b/advanced/lighttpd.conf.fedora @@ -2,7 +2,7 @@ # (c) 2017 Pi-hole, LLC (https://pi-hole.net) # Network-wide ad blocking via your own hardware. # -# lighttpd config for Pi-hole +# 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. @@ -16,79 +16,107 @@ ############################################################################### server.modules = ( - "mod_access", - "mod_auth", - "mod_fastcgi", - "mod_accesslog", - "mod_expire", - "mod_compress", - "mod_redirect", - "mod_setenv", - "mod_rewrite" + "mod_access", + "mod_auth", + "mod_expire", + "mod_fastcgi", + "mod_accesslog", + "mod_redirect", + "mod_setenv", + "mod_rewrite" ) 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.pid-file = "/var/run/lighttpd.pid" +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.filename = "/var/log/lighttpd/access-pihole.log" accesslog.format = "%{%s}t|%V|%r|%s|%b" +# Allow streaming response +# reference: https://redmine.lighttpd.net/projects/lighttpd/wiki/Server_stream-response-bodyDetails +server.stream-response-body = 1 +#ssl.read-ahead = "disable" index-file.names = ( "index.php", "index.html", "index.lighttpd.html" ) url.access-deny = ( "~", ".inc", ".md", ".yml", ".ini" ) static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" ) -compress.cache-dir = "/var/cache/lighttpd/compress/" -compress.filetype = ( "application/javascript", "text/css", "text/html", "text/plain" ) +mimetype.assign = ( + ".ico" => "image/x-icon", + ".jpeg" => "image/jpeg", + ".jpg" => "image/jpeg", + ".png" => "image/png", + ".svg" => "image/svg+xml", + ".css" => "text/css; charset=utf-8", + ".html" => "text/html; charset=utf-8", + ".js" => "text/javascript; charset=utf-8", + ".json" => "application/json; charset=utf-8", + ".map" => "application/json; charset=utf-8", + ".txt" => "text/plain; charset=utf-8", + ".eot" => "application/vnd.ms-fontobject", + ".otf" => "font/otf", + ".ttc" => "font/collection", + ".ttf" => "font/ttf", + ".woff" => "font/woff", + ".woff2" => "font/woff2" +) -mimetype.assign = ( ".png" => "image/png", - ".jpg" => "image/jpeg", - ".jpeg" => "image/jpeg", - ".html" => "text/html", - ".css" => "text/css; charset=utf-8", - ".js" => "application/javascript", - ".json" => "application/json", - ".txt" => "text/plain", - ".svg" => "image/svg+xml" ) +# 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" # default listening port for IPv6 falls back to the IPv4 port #include_shell "/usr/share/lighttpd/use-ipv6.pl " + server.port #include_shell "/usr/share/lighttpd/create-mime.assign.pl" #include_shell "/usr/share/lighttpd/include-conf-enabled.pl" -fastcgi.server = ( ".php" => - ( "localhost" => - ( - "socket" => "/tmp/php-fastcgi.socket", - "bin-path" => "/usr/bin/php-cgi" - ) - ) - ) +fastcgi.server = ( + ".php" => ( + "localhost" => ( + "socket" => "/tmp/php-fastcgi.socket", + "bin-path" => "/usr/bin/php-cgi" + ) + ) +) # If the URL starts with /admin, it is the Web interface $HTTP["url"] =~ "^/admin/" { - # Create a response header for debugging using curl -I + # 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. + # 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-Frame-Options" => "DENY", + "X-XSS-Protection" => "1; mode=block", + "X-Content-Type-Options" => "nosniff", + "Content-Security-Policy" => "default-src 'self' 'unsafe-inline';", + "X-Permitted-Cross-Domain-Policies" => "none", + "Referrer-Policy" => "same-origin" ) - - $HTTP["url"] =~ ".ttf$" { - # Allow Block Page access to local fonts - setenv.add-response-header = ( "Access-Control-Allow-Origin" => "*" ) - } } # Block . files from being served, such as .git, .github, .gitignore $HTTP["url"] =~ "^/admin/\.(.*)" { - url.access-deny = ("") + url.access-deny = ("") } -# 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" +# 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" ) diff --git a/automated install/basic-install.sh b/automated install/basic-install.sh index c887a6c659..b9a6142b2a 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-2018 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 @@ -21,6 +21,10 @@ # instead of continuing the installation with something broken set -e +# Append common folders to the PATH to ensure that all basic commands are available. +# When using "su" an incomplete PATH could be passed: https://github.com/pi-hole/pi-hole/issues/3209 +export PATH+=':/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' + ######## VARIABLES ######### # For better maintainability, we store as much information that can change in variables # This allows us to make a change in one place that can propagate to all instances of the variable @@ -28,85 +32,83 @@ set -e # 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);8.8.8.8;8.8.4.4;2001:4860:4860:0:0:0:0:8888;2001:4860:4860:0:0:0:0:8844 -OpenDNS (ECS);208.67.222.222;208.67.220.220;2620:119:35::35;2620:119:53::53 +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 +OpenDNS (ECS, DNSSEC);208.67.222.222;208.67.220.220;2620:119:35::35;2620:119:53::53 Level3;4.2.2.1;4.2.2.2;; Comodo;8.26.56.26;8.20.247.20;; -DNS.WATCH;84.200.69.80;84.200.70.40;2001:1608:10:25:0:0:1c04:b12f;2001:1608:10:25:0:0:9249:d69b +DNS.WATCH (DNSSEC);84.200.69.80;84.200.70.40;2001:1608:10:25:0:0:1c04:b12f;2001:1608:10:25:0:0:9249:d69b Quad9 (filtered, DNSSEC);9.9.9.9;149.112.112.112;2620:fe::fe;2620:fe::9 Quad9 (unfiltered, no DNSSEC);9.9.9.10;149.112.112.10;2620:fe::10;2620:fe::fe:10 -Quad9 (filtered + ECS);9.9.9.11;149.112.112.11;2620:fe::11; -Cloudflare;1.1.1.1;1.0.0.1;2606:4700:4700::1111;2606:4700:4700::1001 +Quad9 (filtered, ECS, DNSSEC);9.9.9.11;149.112.112.11;2620:fe::11;2620:fe::fe:11 +Cloudflare (DNSSEC);1.1.1.1;1.0.0.1;2606:4700:4700::1111;2606:4700:4700::1001 EOM ) # Location for final installation log storage -installLogLoc=/etc/pihole/install.log +installLogLoc="/etc/pihole/install.log" # This is an important file as it contains information specific to the machine it's being installed on -setupVars=/etc/pihole/setupVars.conf +setupVars="/etc/pihole/setupVars.conf" # Pi-hole uses lighttpd as a Web server, and this is the config file for it -# shellcheck disable=SC2034 -lighttpdConfig=/etc/lighttpd/lighttpd.conf +lighttpdConfig="/etc/lighttpd/lighttpd.conf" # This is a file used for the colorized output -coltable=/opt/pihole/COL_TABLE +coltable="/opt/pihole/COL_TABLE" # Root of the web server webroot="/var/www/html" -# We store several other directories and + +# 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. +# 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) webInterfaceGitUrl="https://github.com/pi-hole/AdminLTE.git" webInterfaceDir="${webroot}/admin" piholeGitUrl="https://github.com/pi-hole/pi-hole.git" PI_HOLE_LOCAL_REPO="/etc/.pihole" -# These are the names of pi-holes files, stored in an array +# List of pihole scripts, stored in an array PI_HOLE_FILES=(chronometer list piholeDebug piholeLogFlush setupLCD update version gravity uninstall webpage) # This directory is where the Pi-hole scripts will be installed 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" -useUpdateVars=false +PI_HOLE_404_DIR="${webroot}/pihole" +FTL_CONFIG_FILE="${PI_HOLE_CONFIG_DIR}/pihole-FTL.conf" +if [ -z "$useUpdateVars" ]; then + useUpdateVars=false +fi adlistFile="/etc/pihole/adlists.list" -# Pi-hole needs an IP address; to begin, these variables are empty since we don't know what the IP is until -# this script can run -IPV4_ADDRESS="" -IPV6_ADDRESS="" -# By default, query logging is enabled and the dashboard is set to be installed +# Pi-hole needs an IP address; to begin, these variables are empty since we don't know what the IP is until this script can run +IPV4_ADDRESS=${IPV4_ADDRESS} +IPV6_ADDRESS=${IPV6_ADDRESS} +# Give settings their default values. These may be changed by prompts later in the script. QUERY_LOGGING=true INSTALL_WEB_INTERFACE=true PRIVACY_LEVEL=0 +CACHE_SIZE=10000 if [ -z "${USER}" ]; then - USER="$(id -un)" + USER="$(id -un)" fi - -# Check if we are running on a real terminal and find the rows and columns -# If there is no real terminal, we will default to 80x24 -if [ -t 0 ] ; then - screen_size=$(stty size) -else - screen_size="24 80" -fi -# Set rows variable to contain first number -printf -v rows '%d' "${screen_size%% *}" -# Set columns variable to contain second number -printf -v columns '%d' "${screen_size##* }" - -# Divide by two so the dialogs take up half of the screen, which looks nice. -r=$(( rows / 2 )) -c=$(( columns / 2 )) -# Unless the screen is tiny -r=$(( r < 20 ? 20 : r )) -c=$(( c < 70 ? 70 : c )) +# dialog dimensions: Let dialog handle appropriate sizing. +r=20 +c=70 ######## Undocumented Flags. Shhh ######## # These are undocumented flags; some of which we can use when repairing an installation # The runUnattended flag is one example of this -skipSpaceCheck=false reconfigure=false runUnattended=false INSTALL_WEB_SERVER=true @@ -114,7 +116,6 @@ INSTALL_WEB_SERVER=true for var in "$@"; do case "$var" in "--reconfigure" ) reconfigure=true;; - "--i_do_not_follow_recommendations" ) skipSpaceCheck=true;; "--unattended" ) runUnattended=true;; "--disable-install-webserver" ) INSTALL_WEB_SERVER=false;; esac @@ -138,13 +139,10 @@ else OVER="\\r\\033[K" fi -# Define global binary variable -binary="tbd" - # A simple function that just echoes out our logo in ASCII format # This lets users know that it is a Pi-hole, LLC product show_ascii_berry() { - echo -e " + echo -e " ${COL_LIGHT_GREEN}.;;,. .ccccc:,. :cccclll:. ..,, @@ -169,209 +167,228 @@ show_ascii_berry() { } is_command() { - # Checks for existence of string passed in as only function argument. - # Exit value of 0 when exists, 1 if not exists. Value is the result - # of the `command` shell built-in call. + # Checks to see if the given command (passed as a string argument) exists on the system. + # The function returns 0 (success) if the command exists, and 1 if it doesn't. local check_command="$1" command -v "${check_command}" >/dev/null 2>&1 } -# Compatibility -distro_check() { -# If apt-get is installed, then we know it's part of the Debian family -if is_command apt-get ; then - # Set some global variables here - # We don't set them earlier since the family might be Red Hat, so these values would be different - PKG_MANAGER="apt-get" - # A variable to store the command used to update the package cache - UPDATE_PKG_CACHE="${PKG_MANAGER} update" - # An array for something... - PKG_INSTALL=("${PKG_MANAGER}" --yes --no-install-recommends install) - # grep -c will return 1 retVal on 0 matches, block this throwing the set -e with an OR TRUE - PKG_COUNT="${PKG_MANAGER} -s -o Debug::NoLocking=true upgrade | grep -c ^Inst || true" - # Some distros vary slightly so these fixes for dependencies may apply - # on Ubuntu 18.04.1 LTS we need to add the universe repository to gain access to dialog and dhcpcd5 - APT_SOURCES="/etc/apt/sources.list" - if awk 'BEGIN{a=1;b=0}/bionic main/{a=0}/bionic.*universe/{b=1}END{exit a + b}' ${APT_SOURCES}; then - if ! whiptail --defaultno --title "Dependencies Require Update to Allowed Repositories" --yesno "Would you like to enable 'universe' repository?\\n\\nThis repository is required by the following packages:\\n\\n- dhcpcd5\\n- dialog" "${r}" "${c}"; then - printf " %b Aborting installation: dependencies could not be installed.\\n" "${CROSS}" - exit # exit the installer +os_check() { + if [ "$PIHOLE_SKIP_OS_CHECK" != true ]; then + # This function gets a list of supported OS versions from a TXT record at versions.pi-hole.net + # and determines whether or not the script is running on one of those systems + local remote_os_domain valid_os valid_version valid_response detected_os detected_version display_warning cmdResult digReturnCode response + remote_os_domain=${OS_CHECK_DOMAIN_NAME:-"versions.pi-hole.net"} + + detected_os=$(grep '^ID=' /etc/os-release | cut -d '=' -f2 | tr -d '"') + detected_version=$(grep VERSION_ID /etc/os-release | cut -d '=' -f2 | tr -d '"') + + cmdResult="$(dig +short -t txt "${remote_os_domain}" @ns1.pi-hole.net 2>&1; echo $?)" + # Gets the return code of the previous command (last line) + digReturnCode="${cmdResult##*$'\n'}" + + if [ ! "${digReturnCode}" == "0" ]; then + valid_response=false else - printf " %b Enabling universe package repository for Ubuntu Bionic\\n" "${INFO}" - cp -p ${APT_SOURCES} ${APT_SOURCES}.backup # Backup current repo list - printf " %b Backed up current configuration to %s\\n" "${TICK}" "${APT_SOURCES}.backup" - add-apt-repository universe - printf " %b Enabled %s\\n" "${TICK}" "'universe' repository" + # Dig returned 0 (success), so get the actual response, and loop through it to determine if the detected variables above are valid + response="${cmdResult%%$'\n'*}" + # If the value of ${response} is a single 0, then this is the return code, not an actual response. + if [ "${response}" == 0 ]; then + valid_response=false + fi + + IFS=" " read -r -a supportedOS < <(echo "${response}" | tr -d '"') + for distro_and_versions in "${supportedOS[@]}" + do + distro_part="${distro_and_versions%%=*}" + versions_part="${distro_and_versions##*=}" + + # If the distro part is a (case-insensitive) substring of the computer OS + if [[ "${detected_os^^}" =~ ${distro_part^^} ]]; then + valid_os=true + IFS="," read -r -a supportedVer <<<"${versions_part}" + for version in "${supportedVer[@]}" + do + if [[ "${detected_version}" =~ $version ]]; then + valid_version=true + break + fi + done + break + fi + done fi - fi - # Debian 7 doesn't have iproute2 so if the dry run install is successful, - if "${PKG_MANAGER}" install --dry-run iproute2 > /dev/null 2>&1; then - # we can install it - iproute_pkg="iproute2" - # Otherwise, - else - # use iproute - iproute_pkg="iproute" - fi - # Check for and determine version number (major and minor) of current php install - 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 <<< "")" - # Is installed php version 7.0 or greater - if [ "${phpInsMajor}" -ge 7 ]; then - phpInsNewer=true + + if [ "$valid_os" = true ] && [ "$valid_version" = true ] && [ ! "$valid_response" = false ]; then + display_warning=false fi - fi - # Check if installed php is v 7.0, or newer to determine packages to install - if [[ "$phpInsNewer" != true ]]; then - # Prefer the php metapackage if it's there - if "${PKG_MANAGER}" install --dry-run php > /dev/null 2>&1; then - phpVer="php" - # fall back on the php5 packages + + if [ "$display_warning" != false ]; then + if [ "$valid_response" = false ]; then + + if [ "${digReturnCode}" -eq 0 ]; then + errStr="dig succeeded, but response was blank. Please contact support" + else + errStr="dig failed with return code ${digReturnCode}" + fi + printf " %b %bRetrieval of supported OS list failed. %s. %b\\n" "${CROSS}" "${COL_LIGHT_RED}" "${errStr}" "${COL_NC}" + printf " %bUnable to determine if the detected OS (%s %s) is supported%b\\n" "${COL_LIGHT_RED}" "${detected_os^}" "${detected_version}" "${COL_NC}" + printf " Possible causes for this include:\\n" + printf " - Firewall blocking certain DNS lookups from Pi-hole device\\n" + printf " - ns1.pi-hole.net being blocked (required to obtain TXT record from versions.pi-hole.net containing supported operating systems)\\n" + printf " - Other internet connectivity issues\\n" + else + printf " %b %bUnsupported OS detected: %s %s%b\\n" "${CROSS}" "${COL_LIGHT_RED}" "${detected_os^}" "${detected_version}" "${COL_NC}" + printf " If you are seeing this message and you do have a supported OS, please contact support.\\n" + fi + printf "\\n" + printf " %bhttps://docs.pi-hole.net/main/prerequisites/#supported-operating-systems%b\\n" "${COL_LIGHT_GREEN}" "${COL_NC}" + printf "\\n" + printf " If you wish to attempt to continue anyway, you can try one of the following commands to skip this check:\\n" + printf "\\n" + printf " e.g: If you are seeing this message on a fresh install, you can run:\\n" + printf " %bcurl -sSL https://install.pi-hole.net | sudo PIHOLE_SKIP_OS_CHECK=true bash%b\\n" "${COL_LIGHT_GREEN}" "${COL_NC}" + printf "\\n" + printf " If you are seeing this message after having run pihole -up:\\n" + printf " %bsudo PIHOLE_SKIP_OS_CHECK=true pihole -r%b\\n" "${COL_LIGHT_GREEN}" "${COL_NC}" + printf " (In this case, your previous run of pihole -up will have already updated the local repository)\\n" + printf "\\n" + printf " It is possible that the installation will still fail at this stage due to an unsupported configuration.\\n" + printf " If that is the case, you can feel free to ask the community on Discourse with the %bCommunity Help%b category:\\n" "${COL_LIGHT_RED}" "${COL_NC}" + printf " %bhttps://discourse.pi-hole.net/c/bugs-problems-issues/community-help/%b\\n" "${COL_LIGHT_GREEN}" "${COL_NC}" + printf "\\n" + exit 1 + else - phpVer="php5" + printf " %b %bSupported OS detected%b\\n" "${TICK}" "${COL_LIGHT_GREEN}" "${COL_NC}" fi else - # Newer php is installed, its common, cgi & sqlite counterparts are deps - phpVer="php$phpInsMajor.$phpInsMinor" + printf " %b %bPIHOLE_SKIP_OS_CHECK env variable set to true - installer will continue%b\\n" "${INFO}" "${COL_LIGHT_GREEN}" "${COL_NC}" fi - # We also need the correct version for `php-sqlite` (which differs across distros) - if "${PKG_MANAGER}" install --dry-run "${phpVer}-sqlite3" > /dev/null 2>&1; then - phpSqlite="sqlite3" - else - phpSqlite="sqlite" - fi - # Since our install script is so large, we need several other programs to successfully get a machine provisioned - # These programs are stored in an array so they can be looped through later - INSTALLER_DEPS=(apt-utils dialog debconf dhcpcd5 git "${iproute_pkg}" whiptail) - # Pi-hole itself has several dependencies that also need to be installed - PIHOLE_DEPS=(cron curl dnsutils iputils-ping lsof netcat psmisc sudo unzip wget idn2 sqlite3 libcap2-bin dns-root-data resolvconf libcap2) - # The Web dashboard has some that also need to be installed - # It's useful to separate the two since our repos are also setup as "Core" code and "Web" code - PIHOLE_WEB_DEPS=(lighttpd "${phpVer}-common" "${phpVer}-cgi" "${phpVer}-${phpSqlite}") - # The Web server user, - LIGHTTPD_USER="www-data" - # group, - LIGHTTPD_GROUP="www-data" - # and config file - LIGHTTPD_CFG="lighttpd.conf.debian" - - # A function to check... - test_dpkg_lock() { - # An iterator used for counting loop iterations - i=0 - # fuser is a program to show which processes use the named files, sockets, or filesystems - # So while the command is true - while fuser /var/lib/dpkg/lock >/dev/null 2>&1 ; do - # Wait half a second - sleep 0.5 - # and increase the iterator - ((i=i+1)) - done - # Always return success, since we only return if there is no - # lock (anymore) - return 0 - } +} -# If apt-get is not found, check for rpm to see if it's a Red Hat family OS -elif is_command rpm ; then - # Then check if dnf or yum is the package manager - if is_command dnf ; then - PKG_MANAGER="dnf" - else - PKG_MANAGER="yum" - fi - - # Fedora and family update cache on every PKG_INSTALL call, no need for a separate update. - UPDATE_PKG_CACHE=":" - PKG_INSTALL=("${PKG_MANAGER}" install -y) - PKG_COUNT="${PKG_MANAGER} check-update | egrep '(.i686|.x86|.noarch|.arm|.src)' | wc -l" - INSTALLER_DEPS=(dialog git iproute newt procps-ng which chkconfig) - PIHOLE_DEPS=(bind-utils cronie curl findutils nmap-ncat sudo unzip wget libidn2 psmisc sqlite libcap) - PIHOLE_WEB_DEPS=(lighttpd lighttpd-fastcgi php-common php-cli php-pdo) - LIGHTTPD_USER="lighttpd" - LIGHTTPD_GROUP="lighttpd" - LIGHTTPD_CFG="lighttpd.conf.fedora" - # 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 - # ensure 'php-json' is installed on Fedora (installed as dependency on CentOS7 + Remi repository) - PIHOLE_WEB_DEPS+=('php-json') - # 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 - # on CentOS we need to add 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}" +# This function waits for dpkg to unlock, which signals that the previous apt-get command has finished. +test_dpkg_lock() { + i=0 + printf " %b Waiting for package manager to finish (up to 30 seconds)\\n" "${INFO}" + # fuser is a program to show which processes use the named files, sockets, or filesystems + # So while the lock is held, + while fuser /var/lib/dpkg/lock >/dev/null 2>&1 + do + # we wait half a second, + sleep 0.5 + # increase the iterator, + ((i=i+1)) + # exit if waiting for more then 30 seconds + if [[ $i -gt 60 ]]; then + printf " %b %bError: Could not verify package manager finished and released lock. %b\\n" "${CROSS}" "${COL_LIGHT_RED}" "${COL_NC}" + printf " Attempt to install packages manually and retry.\\n" + exit 1; fi + done + # and then report success once dpkg is unlocked. + return 0 +} - # 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 +# 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 + # We don't set them earlier since the installed package manager might be rpm, so these values would be different + PKG_MANAGER="apt-get" + # A variable to store the command used to update the package cache + UPDATE_PKG_CACHE="${PKG_MANAGER} update" + # The command we will use to actually install packages + PKG_INSTALL=("${PKG_MANAGER}" -qq --no-install-recommends install) + # grep -c will return 1 if there are no matches. This is an acceptable condition, so we OR TRUE to prevent set -e exiting the script. + PKG_COUNT="${PKG_MANAGER} -s -o Debug::NoLocking=true upgrade | grep -c ^Inst || true" + # Update package cache + update_package_cache || exit 1 + # Check for and determine version number (major and minor) of current php install + local phpVer="php" + if is_command php ; then + 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 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 + 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 - fi - else - # 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 # exit the installer + # 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 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 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 + if [[ -z "${phpInsMajor}" || "${phpInsMajor}" -lt 8 ]]; then + PIHOLE_WEB_DEPS+=("${phpVer}-json") + fi + # The Web server user, + LIGHTTPD_USER="www-data" + # group, + LIGHTTPD_GROUP="www-data" + # and config file + LIGHTTPD_CFG="lighttpd.conf.debian" + + # If apt-get is not found, check for rpm. + elif is_command rpm ; then + # Then check if dnf or yum is the package manager + if is_command dnf ; then + PKG_MANAGER="dnf" else - printf " %b Continuing installation with unsupported RPM based distribution\\n" "${INFO}" + PKG_MANAGER="yum" fi - fi -# If neither apt-get or yum/dnf package managers were found -else - # it's not an OS we can support, - printf " %b OS distribution not supported\\n" "${CROSS}" - # so exit the installer - exit -fi + # 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) + # 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)' | wc -l || true" + OS_CHECK_DEPS=(grep bind-utils) + INSTALLER_DEPS=(git dialog iproute newt procps-ng which 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 + printf " %b No supported package manager found\\n" "${CROSS}" + # so exit the installer + exit + fi } # A function for checking if a directory is a git repository @@ -379,16 +396,12 @@ is_repo() { # Use a named, local variable instead of the vague $1, which is the first argument passed to this function # These local variables should always be lowercase local directory="${1}" - # A local variable for the current directory - local curdir # A variable to store the return code local rc - # Assign the current directory variable by using pwd - curdir="${PWD}" # If the first argument passed to this function is a directory, if [[ -d "${directory}" ]]; then # move into the directory - cd "${directory}" + pushd "${directory}" &> /dev/null || return 1 # Use git to check if the directory is a repo # git -C is not used here to support git versions older than 1.8.4 git status --short &> /dev/null || rc=$? @@ -398,7 +411,7 @@ is_repo() { rc=1 fi # Move back into the directory the user started in - cd "${curdir}" + popd &> /dev/null || return 1 # Return the code; if one is not set, return 0 return "${rc:-0}" } @@ -408,23 +421,35 @@ make_repo() { # Set named variables for better readability local directory="${1}" local remoteRepo="${2}" + # The message to display when this function is running str="Clone ${remoteRepo} into ${directory}" # Display the message and use the color table to preface the message with an "info" indicator printf " %b %s..." "${INFO}" "${str}" # If the directory exists, if [[ -d "${directory}" ]]; then - # delete everything in it so git can clone into it - rm -rf "${directory}" + # Return with a 1 to exit the installer. We don't want to overwrite what could already be here in case it is not ours + str="Unable to clone ${remoteRepo} into ${directory} : Directory already exists" + printf "%b %b%s\\n" "${OVER}" "${CROSS}" "${str}" + return 1 fi # Clone the repo and return the return code from this command git clone -q --depth 20 "${remoteRepo}" "${directory}" &> /dev/null || return $? - # Data in the repositories is public anyway so we can make it readable by everyone (+r to keep executable permission if already set by git) - chmod -R a+rX "${directory}" - + # Move into the directory that was passed as an argument + pushd "${directory}" &> /dev/null || return 1 + # Check current branch. If it is master, then reset to the latest available tag. + # In case extra commits have been added after tagging/release (i.e in case of metadata updates/README.MD tweaks) + curBranch=$(git rev-parse --abbrev-ref HEAD) + if [[ "${curBranch}" == "master" ]]; then + # If we're calling make_repo() then it should always be master, we may not need to check. + git reset --hard "$(git describe --abbrev=0 --tags)" || return $? + fi # Show a colored message showing it's status printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" - # Always return 0? Not sure this is correct + # Data in the repositories is public anyway so we can make it readable by everyone (+r to keep executable permission if already set by git) + chmod -R a+rX "${directory}" + # Move back into the original directory + popd &> /dev/null || return 1 return 0 } @@ -435,34 +460,37 @@ update_repo() { # but since they are local, their scope does not go beyond this function # This helps prevent the wrong value from being assigned if you were to set the variable as a GLOBAL one local directory="${1}" - local curdir + local curBranch # A variable to store the message we want to display; # Again, it's useful to store these in variables in case we need to reuse or change the message; # we only need to make one change here local str="Update repo in ${1}" - - # Make sure we know what directory we are in so we can move back into it - curdir="${PWD}" # Move into the directory that was passed as an argument - cd "${directory}" &> /dev/null || return 1 + pushd "${directory}" &> /dev/null || return 1 # Let the user know what's happening printf " %b %s..." "${INFO}" "${str}" # Stash any local commits as they conflict with our working code git stash --all --quiet &> /dev/null || true # Okay for stash failure git clean --quiet --force -d || true # Okay for already clean directory # Pull the latest commits - git pull --quiet &> /dev/null || return $? + git pull --no-rebase --quiet &> /dev/null || return $? + # Check current branch. If it is master, then reset to the latest available tag. + # In case extra commits have been added after tagging/release (i.e in case of metadata updates/README.MD tweaks) + curBranch=$(git rev-parse --abbrev-ref HEAD) + if [[ "${curBranch}" == "master" ]]; then + git reset --hard "$(git describe --abbrev=0 --tags)" || return $? + fi # Show a completion message printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" # Data in the repositories is public anyway so we can make it readable by everyone (+r to keep executable permission if already set by git) chmod -R a+rX "${directory}" # Move back into the original directory - cd "${curdir}" &> /dev/null || return 1 + popd &> /dev/null || return 1 return 0 } -# A function that combines the functions previously made +# A function that combines the previous git functions to update or clone a repo getGitFiles() { # Setup named variables for the git repos # We need the directory @@ -486,9 +514,8 @@ getGitFiles() { # Attempt to make the repository, showing an error on failure make_repo "${directory}" "${remoteRepo}" || { printf "\\n %bError: Could not update local repository. Contact support.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"; exit 1; } fi - # echo a blank line echo "" - # and return success? + # Success via one of the two branches, as the commands would exit if they failed. return 0 } @@ -497,7 +524,7 @@ resetRepo() { # Use named variables for arguments local directory="${1}" # Move into the directory - cd "${directory}" &> /dev/null || return 1 + pushd "${directory}" &> /dev/null || return 1 # Store the message in a variable str="Resetting repository within ${1}..." # Show the message @@ -508,7 +535,9 @@ resetRepo() { chmod -R a+rX "${directory}" # And show the status printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" - # Returning success anyway? + # Return to where we came from + popd &> /dev/null || return 1 + # Function succeeded, as "git reset" would have triggered a return earlier if it failed return 0 } @@ -549,79 +578,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: http://pi-hole.net/donate" "${r}" "${c}" - - # Explain the need for a static address - whiptail --msgbox --backtitle "Initiating network interface" --title "Static IP Needed" "\\n\\nThe Pi-hole is a SERVER so it needs a STATIC IP ADDRESS to function properly. - -In the next section, you can choose to use your current network settings (DHCP) or to manually edit them." "${r}" "${c}" -} - -# We need to make sure there is enough space before installing, so there is a function to check this -verifyFreeDiskSpace() { - # 50MB is the minimum space needed (45MB install (includes web admin bootstrap/jquery libraries etc) + 5MB one day of logs.) - # - Fourdee: Local ensures the variable is only created, and accessible within this function/void. Generally considered a "good" coding practice for non-global variables. - local str="Disk space check" - # Required space in KB - local required_free_kilobytes=51200 - # Calculate existing free space on this machine - local existing_free_kilobytes - existing_free_kilobytes=$(df -Pk | grep -m1 '\/$' | awk '{print $4}') - - # If the existing space is not an integer, - if ! [[ "${existing_free_kilobytes}" =~ ^([0-9])+$ ]]; then - # show an error that we can't determine the free space - printf " %b %s\\n" "${CROSS}" "${str}" - printf " %b Unknown free disk space! \\n" "${INFO}" - printf " We were unable to determine available free disk space on this system.\\n" - printf " You may override this check, however, it is not recommended.\\n" - printf " The option '%b--i_do_not_follow_recommendations%b' can override this.\\n" "${COL_LIGHT_RED}" "${COL_NC}" - printf " e.g: curl -L https://install.pi-hole.net | bash /dev/stdin %b